Friday, July 24, 2009

Dos and Donts in Embedded system design

Mostly in Embedded systems, you will have the embedded OS, drivers, interrupt service routines, callbacks, tasks, stack, local and global variables, mutual exclusion mechanisms, system calls, etc., around you to play with. So, while designing or building a system, you have to be careful and aware to pick the right one and put it in the right place.

I remember when I was new to embedded, I was designing a USB device firmware in Linux. It is for transferring the data from the host and to store it in the storage disk of the target. What I did was, in the interrupt service routine of the USB firmware, I read the block of data from the USB device endpoint and also called the system call to store it in the storage disk. Of course, it was working. But, what happened you know? The whole Linux system except my firmware stopped working, when a file is transferred to the target. Hope you know the problem. Later, the code was corrected as follows: the USB firmware which is running in the kernel level is added with system calls to communicate with user level application. A separate user application task was created which will call the system call to get the block of data from the USB firmware and to call another system call to write the data in the storage disk. This was working fine with all other tasks also getting scheduled whenever there is a gap when the USB firmware and the storage disk driver has to wait for the hardware response.

Though I knew what is Interrupt Service routine and what is System call, I did not know what will happen if I put it there. So, at first, let me give you some do's and don'ts when designing a system.

  • Do not waste the CPU cycles in busy waiting as follows:

In the above example, the driver writes an IO command to device and wait for the device to complete it. Instead, you can go to sleep so that other tasks can utilize the CPU and the device will interrupt you when the intended job is completed.

Let me modify the code as follows:


  • Do not call blocking calls from contexts where scheduling is forbidden

Blocking calls are the function calls which may cause the calling entity to sleep. There are some contexts which may run with higher priority than the scheduler and the scheduler is forbidden like Interrupt Handler, critical sections, higher priority kernel threads. Calling blocking calls from such contexts may result in unwanted results like deadlocks. It will keep other tasks in waiting unnecessarily. For example, look at the following code. First of all, whether a particular OS will allow such sleep from Interrupt context is a main question. Even if it allows, sleeping in interrupt service routine will stop all lower priority interrupts, will stop the whole scheduling since because Interrupt service routine is a highest priority context. So, the whole system will sleep during that delay.

Instead, you can set a flag inside interrupt service routine and check for the event from a task and call the function dev_bringup(). So, be aware of the blocking calls and use it in context where scheduling can happen. Keeping the code size and processing very less in higher priority contexts such as interrupt handler will improve the overall system performance.
  • Do not do your own processing in callbacks
Callbacks are the facility provided by the busy tasks to notify some events to the user. For example, the following task has to continuously poll many devices and has to inform the user about the event. It will call the function pointer registered by the user. So, the function call is the device task`s context. Not the user`s. So, the user should not call blocking calls in the callback. And, it is better to keep very less processing the callback function. Mostly, in the callback, applications used to set some flag to catch events and process the event in their own task contexts.

  • Do not use local variables in very large size
Do you know the memory space for local variables are allocated from stack? Look at the following example:


In most of systems, each Task is allocated specified and limited size stack space. Since, the local variables consume memory from the stack, the stack memory may not become enough and further it will lead to stack overflow and system crash. So, know the limit and use. In the above example, if the calling task is allocated 300 bytes of stack, what will happen? This function itself uses more than 256 bytes. The task context also might be saved in the stack space. So, it may cause overlap and cause damage. Instead, it is better to allocate the buffer from heap or memory pools dynamically and use. Or, you can allocate from Global memory if mutual exclusion is not a major problem.
  • Do not keep Global resources un-protected

Whenever you are adding a global variable or global buffer memory or global structure, the first thing you have to worry about it is mutual exclusion. Is it getting shared between more than one task? When it is accessed by one context, is it possible to getting pre-empted with another context? If so, it has to be protected with mutual exclusion. When the variable is shared between interrupt service routine and Task, before accessing the variable from the Task, the interrupt has to be disabled. Since the Interrupt service routine is mutually exclusive in nature, no need protect the globals inside the interrupt service routine. When the variable is shared between Tasks, you should use OS primitives for mutual exclusion.

  • Avoid use of the user system calls in driver or kernel mode

Most of Operating systems have separation between user and kernel applications. Though mutual exclusion might be needed in user applications and drivers, it is not good to use the user primitives in driver. Instead, it will have the equivalent for kernel mode. So, use that.

Good luck!

1 comment:

Anonymous said...

https://embeddedgurus.com/barr-code/2011/08/dont-follow-these-5-dangerous-coding-standard-rules/