3rd Mediterranean Conference on Embedded Computing
MECO - 2014
Budva, Montenegro
Embedded software development in research environment A practical guide for non-experts Marcin Bajer Senior IT Specialist ABB Corporate Research Center Krakow, Poland
[email protected] Abstract—Embedded microcomputer systems have grown tremendously in recent years and influence almost every aspect of our lives. They increase not only in popularity, but also in complexity. Rising computation power combined with low energy consumption enables new breed of applications and services. More often their development requires significant effort for technology research. The goal of this publication is to provide brief description of embedded software development process which fits specific requirements of research projects. Although, the software lifecycle contains larger number of steps this document describes only those related to requirements specification, architecture planning and development. The part related to testing and maintenance are not covered and will be subject of separate publication. All techniques described in this paper are based on use of popular, well documented, open source tools. Even though, presented solution was described to be use by small - even one person teams, it can be easily scaled to larger projects. Keywords—embedded design patterns, embedded programming, embedded architecture, graphviz, requrements specification, software documentation
I. INTRODUCTION Together with rapid evolution of computer technology number of research projects which involve software development is increasing. This statement applies both to desktop applications as well as embedded software. Process of creating software in large enterprises differs from the one in academic or corporate research centers. Scientists, rarely have a degree in computer science and usually they are not formally trained in software engineering techniques. Very often, programming is their secondary task apart from research activities. This may result in a poor, non-reusable, nonmaintainable software. Fortunately, many common problems can be addressed with using open-source tools and lightweight, easy to apply software development process. This paper is a practical guideline for non-experts, based on the author’s experience with industrial software development. It is important, especially for beginners, to know principles of software methodologies before start project execution. Even though tutorials, tools and procedures will never replace
experience and skills of professional software engineer, they will help in making software better and easier. II. SOFTWARE DEVELOPMENT LIFE CYCLE The software process is a set of activities undertaken to manage, develop and maintain software systems [1]. Over the years, several methodologies have been proposed to standardize software development and specify approaches to a variety of common tasks and activities that take place during software systems construction. The main goal of all those efforts is to produce better quality code within estimated timeframe. Many big software companies practice software development based on waterfall methodology. In this model software is developed in a sequence of no overlapping phases. The model presented in Fig. 1 has been slightly modified from the original Waterfall approach to show the parallelism of the work on the software and hardware during Design and Implementation phases. Requirements
HW/SW partition
Software Design Software Implementation Hardware Design
Hardware Implementation
HW/SW integration
Verification
Product release
Maintenance
Fig. 1. Waterfall development model modification for embedded devices
Waterfall approach is considered as “heavyweight” because each element of software development process should be thoroughly planned. Special attention must be devoted to preparation of detailed design and comprehensive documentation. Project requirements are fixed in the early stage of the project and it is difficult to change them later. In response to the perceived problems of pure Waterfall numerous variations of this approach were developed - some of them allows cycles between the stages of the project or overlapping project phases. Agile methodology has been proposed as a reaction against “heavyweight” way of producing software. In contrast to sequential Waterfall approach, agile methodologies use
3rd Mediterranean Conference on Embedded Computing iterative attitude towards product development. Working software, delivered in short cycles, is the principal measure of project progress. Instead of making detailed system specification, requirements are clarified in every iteration, based on feedback from a client. For such reason close collaboration with the customer which is able to receive product frequently is required. Project management is less process oriented, with fewer bureaucracy, but smaller degree of predictability. In Agile, people are considered as the most important factor of success – process, environment, management are subject to change if they are having an adverse effect upon the people [2]. The most efficient method for communication within a team is frequent face-to-face conversation. Typically, this can be realized in a form of daily stand-up meetings. Those short, informal meetings can be performed every morning to provide a status update with focus on current activities, problems and short term plans. Besides stand-ups, regular meetings are organized to discuss most important project related problems. There are several implementation of agile process (Scrum, XP programming, Feature Driven Development to name just a few). Each of them introduce additional elements and define implementations details of agile for software development. For the purpose of this paper it is not needed to describe specifics of mentioned methodologies. Nevertheless, it is important to point that selecting and following the right development process for a given project is essential. There is no universal software development model, before making a choice it is important to do research and look at the pros and cons of each approach. The final solution should combine the most suitable practices from all of methodologies. For projects which requires substantial hardware development attitude close to Waterfall model would be still the best, but increasing standardization of hardware components, growing popularity of system-on-a-chip designs and high level programming boost acceptance of agile development. The successive parts of this article have been arranged according to the steps defined in the Waterfall model since they also exists in most of the other software development methodologies. III. REQUIREMENTS SPECIFICATION A recommended starting point for each software development is project charter definition. This simple document defines the content of the project, its objectives, major risks, deliverables and stakeholders. Besides defining a project scope, to narrow project goals and make assumptions about system boundaries, it is suggested it contains also what is definitively out of the project scope. Well defined project charter helps to gain a common understanding of project goals, priorities and constraints. Printable version of this document is highly recommended. The main document of each software project is a requirements specification. This text is a comprehensive description of the system to be developed. It translates customer intentions to formal requirements. Well-defined
MECO - 2014
Budva, Montenegro
requirement express a need together with its necessity and if applicable associated constraints and conditions (i.e. After startup [condition], device must be [necessity] be fully operational [need] within 10s [constraints]). Very often requirements specification uses natural language – in this case statements must be unambiguous. It is recommended to make a glossary and include in it all terms with significant meaning for the specification. There are numerous guidelines for making a Software Requirements Specification (SRS) including IEEE Standard [3]. Sign of well-prepared requirements specification document is ability to prepare all functional tests of the device base on it. In the pure Waterfall approach the content of requirements specification is frozen before the design starts, but in most of the projects it is a living document till the end of the development. In case there are many uncertainties or requirements cannot be specified in advance it is suggested to apply from the beginning more iterative approach and specify only those requirements which are needed for a scope of current product iteration. If iterative approach is not applicable, provide very limited demo of complete system to clarify the requirements. Although, it is very common that the requirements are collected in single document, there are many software tools dedicated for this purpose (i.e. HP ALM or IBM Rational DOORS). The biggest advantage of using them is the fact that those tools allow better change management and traceability. Very often they not only provide possibility of linking requirement to the product release, but also with functional tests and check-ins to source code repository. IV. HARDWARE AND SOFTWARE DESIGN Since an embedded system contains both hardware and software components both needs to be co-design. The process of splitting functionalities between hardware and software implementation is called partitioning decision [4]. Software is more flexible, easier to debug and reuse, but dedicated hardware is usually much faster. The partitioning is a complex optimization problem. Multiple factors need to be considered: price, time to market, available components, extensibility, reusability to name just a few. Fortunately, in case of research projects, partitioning decision is easier since the cost factor is not so important, it is also common that for such projects offthe-shelf evaluation boards are used. After the partitioning decision, hardware and software are developed in parallel. Usually hardware design starts a bit earlier since it is required during firmware development. Very often hardware and software development cycles are done independently, in such case it is essential to schedule a few integration points to synchronize them. The more often synchronization is done, the less problems will occur in the final phase of integration. Misconceptions at the very early stage of the projects are difficult or even impossible to fix later. It is also important to test both hardware and software separately as much as possible, because testing whole system is much more difficult and time consuming.
3rd Mediterranean Conference on Embedded Computing A. Software Architecture Description Software architecture, can be defined as the fundamental organization of a system, embodied in its components, their relationships to each other and the environment, and the principles governing its design and evolution [5]. Graphical, UML-based, form is the most common way for representing system interns. Block diagrams interconnected with directional arrows can be used to describe system blocks, data flow, SW/HWD partitioning and much more. Software architecture description provides only the overview of the system structure. It should be easy to understand and stay valid while system details are changing (i.e. should not contain names of detailed system elements). B. Detailed Software Design Description Interfaces specification and detailed software design description typically follows the development of the system architecture. The main purpose of them is to provide programmers a blueprint to follow. They are important for big projects, but for smaller projects it might by more efficient to replace them with direct interaction between team members. Heavy documents in a small-sized project distract the teams focus from the working software product [6]. Detailed documentation over the time can easily become obsolete. Since, additional effort is needed to keep them synchronized with the real implementation, it is more reasonable to spend more time for making good self-commenting source code instead of creating comprehensive documentation. C. Software Architecture for Embedded Devices Specific requirements for embedded devices such as interaction with various hardware interfaces, real-time performance, energy consumption constraints, limited computation power and many others influence design of embedded software architecture. Reference [7] distinguish four main types of embedded architecture: 1) Round Robin Round robin is the simplest architecture used in the embedded software. All modules are called from the main loop and have the same priority. Such approach is suitable for rather simple devices with no lengthy calculations and similar real-time constraints for every task. If any of device modules requires time to service shorter than the maximal loop time polling of this module can be triggered a few times in main loop. module1 main
module2
module3
module4
void main(void) { while (TRUE) { module1(); module2(); module3(); module4(); } }
Fig. 2. Round robin architecture
2) Round Robin with Interrupts Round robin with interrupts is a step towards allowing greater flexibility in assigning task priorities. Interrupt signal
MECO - 2014
Budva, Montenegro
causes the microprocessor to stop main loop execution and immediately triggers handling interrupt routine. Usually it is also possible to assign priorities to interrupt sources and enable interrupts preemption. Combining both allows to configure a system in a way highest priority signal will be always handled first. One of the main rules for implementing interrupts is to keep interrupt service routine short. In many cases the interrupt is used only for triggering data save (i.e. A/D converters, received communication). To follow-up post processing in the main loop boolean flag is used. The potential drawback of such approach is a problem of shared data. It is necessary to assume every variable changed in interrupt routine can be altered at any time outside this interrupt. If main loop or lower priority interrupt is using this data a security mechanism must be introduced (for example disabling interrupts around critical sections). In most cases it is also reasonable to declare shared data with a volatile keyword – this will prevents a compiler from applying data access optimization to those variables. module1
void main(void) { while (TRUE) { module1(); module2(); module3(); if (irq1_do_calc) irq1AddCalc(); if (irq2_do_calc) irq2AddCalc(); } } void IRQ1_ISR(void) { … irq1_do_calc = true; }
IRQ1 main
module2
IRQ2
module3
IRQ1 time consuming
IRQ2 time consuming
Fig. 3. Round robin with interrupts
Data sharing can lead to potentially very difficult to trace problems. Real life example of non-trivial bug is presented in Fig. 4. This problem was detected during long run release tests of device and occurred once per a few hours. The system was based on 16bit µP which means that in this case incrementation of 32bit timer variable was not atomic operation. In the end phase of firmware development interrupt nesting was enabled. This lead to situation that, in very rare situations when interrupt service routine of ADC interrupted increase of timer variable, the content of timestamp was incorrect. The problem was solved by disabling interrupt preemption in critical section. volatile uint32 timer; * Higher priority ADC interrupt …
void ADC_ISR(void) { volatile uint32 timestamp; timestamp = timer; … } * Lower priority timer increase interrupt (preemption enabled) …
void Timer_ISR(void) { timer++; }
Fig. 4. Data share problem
3) Function Queue Scheduling Function queue scheduling is more sophisticated architecture which provides a method of prioritizing the highest severity tasks. The exemplary implementation of this idea is shown in Fig. 5. The main routine calls in the loop functions
3rd Mediterranean Conference on Embedded Computing defined in array of function pointers. This is particularly useful for post processing interrupt data in the main loop. The post processing routine is added to the queue base on its priority. The worst case time to service for the highest priority task is the execution time of the longest function in the queue. Unfortunately, it is possible that some functions will never be called. To compare, round robin base architectures ensures execution of all modules, but the worst case scenario for finishing interrupt post processing is equal to main loop execution time. Task queue
run_first_from_queue( )
IRQ1 time consuming
main module2 IRQs IRQ1
module1
IRQ2
module2
module3
int queue_top = 0; int queue_buttom = 0; int (*task_queue[255]) (void); void main(void) { while (TRUE) { run_first_from_queue(); } } void run_first_from_queue() { if (queue_top != queue_buttom) (*task_queue[queue_top--]); }
Fig. 5. Function queue scheduling architecture (simplified)
4) Real Time Operating Systems (RTOS) A Real Time Operating System is the most advanced architecture for embedded systems. It provides the best flexibility for designing complex systems, but introduces additional overhead (memory, computation). Using RTOS is strongly recommended for complex applications, but in case of simple systems it might not be the most appropriate solutions. Sometimes it is difficult to decide the most suitable architecture for particular system, but using RTOS brings a few, sometimes not obvious, benefits. First of all, RTOS allows to prioritize tasks to ensure application meets required time constraints. Using RTOS also imposes code modularity. Since tasks are separate modules they can be easily developed, tested and documented independently. RTOS introduces also abstraction layer for hardware and provides off-the-shelf implementation of some abstract data types (queues, semaphores, mutexes…etc.).
MECO - 2014 Idle
Budva, Montenegro
Scheduler
Task1
Suspend Ready
Running
Suspend
Block
Event
Suspend Blocked
Resume
Fig. 6. FreeRTOS valid state transtions
Suspended
Interrupt
Interrupt
Task2 ready
Task 2 blocked
Task 1 blocked
Fig. 7. Sample task transitions chart
The main challenge in developing RTOS based applications is related to inter-task communication and synchronization. A single task run independently from other concurrent tasks and its execution can be switched out at any time. Although, use of global variables is the simplest mechanism to pass data between tasks, it is also the most error-prone one. Most of RTOS provides a various methods for synchronization and data exchange between tasks in form of queues, binary semaphores, counting semaphore, mutexes – they are recommended to be used instead of global variables. The most common way for transferring data between tasks is a queue. If the size of data is large, instead of copying data it is suggested to put only data pointers to the queue. However, since that approach is nothing more than complex form of shared memory, extreme care must be taken to ensure that tasks will not modify content simultaneously. The exemplary solution of this problem has been shown in Fig. 8. For each of tasks single queue is defined to allow communication from other tasks (TaskQueue). Elements of the queue are pointers to elements of TaskEvent type array (EventsArray). Pointers to not used events are kept on stack type variable (FreeEvents). Task implementation provides methods for taking (takeEvent) and returning events (returnEvent) from the stack. EventsArray
element(0)
EventType
element(1) ...
Event1
element(n-1)
An application might contain several tasks. In single core systems only one tasks can be running at given time. Fig. 6 shows the possible task state transitions for FreeRTOS [8], in case of other RTOS it may slightly differs. The task code is executed only in Running state. When tasks requires waiting for external event it goes to Blocked state and the scheduler is triggered. Scheduler, using the scheduling algorithm, chooses from a set of Ready tasks the next task to be switched in. Scheduler is also periodically called to check if there is no task with higher priority available for switch in. To disable a task for some period of time task can be moved to Suspended state. Tasks in Suspended state are not available for scheduler transitions out of this state need to be triggered from other task code.
Task2
Task1 ready
Event2
element(n) TaskQueue &element(n) ...
TaskEvent
type: EventType data: EventExtraData ...
...
&element(0)
&element(1) ... &element(n-1)
EventExtraData Event1Data data1: uint8
... FreeEvents
...
data2: uint16
Task
waitForEvent(): TaskEvent* takeEvent(): TaskEvent* returnEvent(TaskEvent*): void EventsArray[n]: TaskEvent
Event2Data
typedef struct { enum { Event1,Event2,… } EventType; union { Struct { uint8 data1; uint16 data2; } Event1Data; uint8 Event2Data; } EventExtraData; } TaskEvent;
FreeEvents: Stack TaskQueue: Queue
Fig. 8. Intertask communication base on queue
5) Embedded Linux Linux has become the most popular system for embedded devices. In many cases the hardware limitations are no longer a concern since more and more embedded systems fulfill the requirements. Comprehensive documentation, off-the-self components and large community support are the main
3rd Mediterranean Conference on Embedded Computing advantages. Although, typically Linux is not real time system, there are special patches for kernel to embed real time support into Kernel (i.e. Xenomai). In systems where GUI part is important, Android can be a good option. D. Design Patterns for Embedded Software A software pattern is a common solution to a problem that occurs in many different contexts, a general reusable solution to a commonly occurring problem within a given context in software design [9]. Term “software pattern” is well-known in a domain of object oriented programming, but even if it is not always clearly stated, software patterns are also frequently used in C programming for embedded devices. First of all, embedded system architectures described in point C, as they represent a common approach for implementing highest level of embedded software, can be considered as Architectural Patterns. Correspondingly, task queue and shared memory are patterns for inter-task communication. One of the most known design patterns for embedded systems is state machine. Describing algorithm as a finite state machine is a simple and useful way to model complex algorithms. In most of cases state diagram consists of states and transitions. In Fig. 9 exemplary state machine for communication has been described in a form of UML chart. The same state machine was presented in Fig. 10 using state transitions table.
MECO - 2014
Budva, Montenegro
typedef enum {EV_TX_REQ, EV_TX_END, EV_RX_REQ, …} Events; void (*rtuState)(Events event) = rtuIdle; void rtuStateMachine(Events event) { rtuState(event); } void rtuIdle(Events event) { if (event == EV_TX_REQ) rtuState = rtuTxProc; if (event == EV_RX_REQ) rtuState = rtuRxProc; } …
Fig. 11. Serial port state machine implementation (simplified)
The drawback of this solution is that all state functions are executed with the same set of parameters, if this is a problem Events enum can be replaced with union based structure (Fig. 8). Additional advantage of using state machine is possibility to split complicated logic into hierarchical structure as shown in Fig. 12. The state machine in Fig. 9 was extended with additional layer (Communication state diagram) to provide high level communication control. Subset of states and events is used as a communication interface. Since the higher level is device independent it can be easily tested with unit tests. To port the solution to new hardware only small part of code needs to be modified. Communication state diagram [RX_ERROR]
/EV_ERR_HANDLED [TX_ERROR]
[TX_ERROR]
[TX_READY] /EV_RX_REQ
/EV_TX_REQ
TX_ERROR
RX_ERROR
TX_ERROR
Fig. 12. Hierarchical communication algorithm EV_TIMEOUT
RX_PROC
RX_READY
There are multiple other design patterns defined and available. It is recommended to learn and follow them as much as possible.
EV_ERROR
RX_ERROR
EV_ERR_HANDLED
Fig. 9. Serial port state diagram
EV_TX_REQ
TX_PROC
TX_ERROR
TX_READY
TX_READY RX_PROC IDLE
EV_RX_HANDLED
IDLE
EV_TX_HANDLED EV_TIMEOUT
RX_READY IDLE
EV_ERR_HANDLED EV_ERROR
RX_READY RX_ERROR
TX_PROC
EV_TX_END EV_RX_REQ
RX_PROC
TX_READY, RX_READY, TX_ERROR, RX_ERROR
TX_READY TX_PROC
EV_RX_HANDLED
IDLE
RX_READY IDLE
EV_TX_END
EV_RX_REQ
˅ Event | State ˃
CYCLIC_COM_RX
[TX_READY] /EV_RX_REQ
RX_PROC
EV_TX_REQ, EV_RX_REQ, EV_ERR_HANDLED
TX_READY IDLE
CYCLIC_COM_TX
Serial port state diagram EV_TIMEOUT
TX_PROC
[RX_READY] /EV_TX_REQ
HEADER_RX
HEADER_TX
EV_ERR_HANDLED
EV_TX_REQ
[TX_READY] /EV_RX_REQ
[RX_ERROR]
INIT
IDLE
TX_ERROR RX_ERROR
ALWAYS
Fig. 10. Serial port state transitions table
There are multiple strategies for implementation of state machine in C code. The most basic is based on variable keeping current state and long switch/case block with all possible states. While this approach has advantage of being simple, it may results in long, difficult to maintain, segment of nested conditional statements. Alternative solution has been described in Fig. 11, in this case state variable was replaced with pointers to functions. It is important to ensure that assigning a new value to the function pointer is an atomic operation or is placed in critical section.
V. SOFTWARE IMPLEMENTATION It is not in scope of this paper to provide detail guidelines for software implementation in embedded devices, further in this paper only a few selected aspects were described. To get more information about coding standards it is recommended to refer to software development standards such as MISRA or ISO/ANSI C. It is suggested to use at least MISRA 2008 [12] and ANSI C99 [13] in new projects, since older can be too strict and not use full features of the compiler. If you do development in C++ refer to [11]. A. Development Environment Configuration Before programming an engineering environment needs to be set up. This involves software installation (compiler, debugger, source control, bug repository…etc) as well as hardware, if available. The major task during this process is configuration of Integrated Development Environment (IDE). IDE provides comprehensive set of tools which enhance software development process. It typically contains source code editor, compiler and debugger. Nowadays, it is common that it also integrates with source code repository, project management tool and bug tracking system. Further in this
3rd Mediterranean Conference on Embedded Computing paper configuration of Eclipse CDT has been described. Since, Eclipse configuration is well described in the Internet, only the features which are not so well known will be described. 1) Code Repository Support in Eclipse The source control repository must be used in every software development project. Eclipse provides plugins to all of the most popular repositories (SVN, GIT, TFS, Mercurial). In embedded projects SVN (or even CVS) is usually used, but for new projects it is recommended to consider GIT as a main repository. In case where SVN is required it is possible to keep GIT locally and use SVN as a remote repository. 2) Doxygen & Graphviz Plugins In case of automatic generating C code documentation Doxygen is de-facto standard. Eclox plugin enable integration of Doxygen with Eclipse. It provides a basic configuration and allows to parse errors in source code documentation. Doxygen can generate diagrams base on Graphviz tool. In fact, all of the graphics for this paper were generated by Doxygen. Exemplary code for the graphics in Fig. 6 has been presented in Fig. 13. To enable graphs preview in Eclipse Graphviz DOT for Zest (dot4zest) plugin can be used. digraph rtos_task { node[shape="box3d",width=2.3,height=0.6,fontname="Arial"] rankdir = "LR" start [shape="point" height=0.1] start -> Ready Suspended -> Ready [label="Resume"] Ready -> Suspended [label="Suspend"] Blocked -> Suspended [label="Suspend"] Blocked -> Ready [label="Event"] Ready -> Running Running -> Ready Running -> Suspended [label="Suspend"] Running -> Blocked [label="Block"] }
Fig. 13. Graphviz code for Fig. 6
MECO - 2014
Budva, Montenegro
IDE, after writing prefix, the content assist will provide the list of all functions and variables. In C++ prefixes are not required since the language itself provides namespaces which provides the same functionality. It is no longer required to use Hungarian notation or any other kind of embedding the type information in a name. C/C++ allows defining dedicated variable types and programmer is encouraged to do so. Compiler remembers and checks the types so it will forbid hidden type conversions. Additionally, modern IDEs provide fast navigation to declaration of each function and variable, so it is easy to check the type. D. Refactoring Refactoring is the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure [14]. Even the best design over time will be effected by changes which came during the development process. The task of the programmer is to prevent it. This is the reason why it is important to reserve time and make refactoring frequently. It will pay off in the short term and will continue to do so later. Most IDEs have integrated support for refactoring (renaming, function extraction, finding duplicates... etc.). VI. SUMMARY To sum up, this paper describes some chosen aspects of software development for embedded devices. The focus was placed on the architecture preparation, software patterns and general guidelines for software implementation. Description of validation and maintenance was skipped, but it is to be remember testing is one of the most important and time consuming parts of embedded software development. REFERENCES
B. Code Modularity Modularity is an important key to hide code complexity, improve testability and self-documentation. Software should be organized in modules. Logically related functions should be defined in the same source file if possible. Size of functions should be reduced to maximum of one screen and limited to one functionality. Each function should perform a clearly defined task. Selected functions which are to be used outside module should be declared and described in the header file. It is recommended to comment all functions available outside of module (brief description, inputs and outputs).
[1]
C. Naming Convention Naming process is integral, sometimes neglected, part of software development. Choosing good names is a part of documenting the code. Name should be descriptive, pronounceable and unambiguous. Good name reveal all important information about referred entity. If a name requires a comment, then the name does not reveal its intent [11]. In addition the name should not be too long - in the past linker and screen size were limitations. Nowadays the main limitation is code readability - in practice single name should not be longer than 31 characters. It is reasonable in C code, to use module name base prefixes to avoid name collisions between modules. This approach has additional advantage, while using
[7]
[2] [3] [4] [5] [6]
[8] [9] [10] [11] [12] [13] [14]
S.T. Acuna, N. Juristo, “Software Process Modeling” in International Series in Software Engineering, vol. 10, Springer, 2006 R.C. Martin, “Agile Software Development, Principles, Patterns and Practices”, Pearson Education, 2003 “Systems and software engineering - Life cycle processes Requirements engineering”, International Standard, ISO/IEC/IEEE 29148, 2011-12-01 A. Berger, “Embedded Systems Design: An Introduction to Processes, Tools, and Techniques”, Taylor & Francis, 2002 “IEEE Recommended Practice for Architectural Description of Software-Intensive Systems”, IEEE Std 1471-2000, IEEE Computer Society 2000.09.21 K.M. Lui, K.C.C. Chan, “Software Development Rhythms: Harmonizing Agile Practices for Synergy”, John Wiley & Sons, 2008 Simon, D.E., “An Embedded Software Primer”, vol. 1, Addison Wesley, 1999 R. Barry, “Using the FreeRTOS Real Time Kernel: A Practical Guide; Cortex M3 Edition”, 2010 R. Oshana, M. Kraeling, “Software Engineering for Embedded Systems: Methods, Practical Techniques, and Applications”, Elsevier Science, 2013 A. Petersen, “Patterns in C – Part 2: State”, C Vu 17.2 R.C. Martin, “Clean Code: A Handbook of Agile Software Craftsmanship”, Pearson Education, 2008 “MISRA-C:2004 Guidelines for the use of the C language in critical systems”, MIRA Limited, 2008 “Programming languages – C”, ISO/IEC 9899:1999, 1999-12-01 M. Fowler, “Refactoring: Improving the Design of Existing Code”, Pearson Education, 2012