General > General Technical Chat
Embedded software development. Best practices.
(1/6) > >>
Remark:
Hello,

Maybe i can ask those who work with embedded systems, what design practices or patterns do you apply in programming? Do you apply state-machines or object oriented programming with C ++, data structures in embedded software design ? I saw a lot of code in projects, that was written between while loop, performing several same operations, for example by scanning sensors and outputting information to the LCD screen. It is good practice and maybe it is widely applied? Or perhaps better and more proven practices are applied? I am currently a beginner in this field, so I would like to read answers from more experienced people than me. Can you recommend what books I should read to know more about embedded software design and its patterns?

Thank you very much for your answers
AaronD:
The fewer things that you handle with interrupts, the more responsive each interrupt can be.  You generally learn polling before interrupts so that you get a good handle on the basics of program flow in the first place, and it's often the best way to do things even when you do understand everything there is to know about interrupts.
Not everything needs microsecond-immediate attention.  As long as you never busy-wait for anything (see the next section), the main-loop polling rate is usually more than sufficient.

Likewise, don't do anything more than what is absolutely necessary in an interrupt handler.  Any time that you spend there is time that you can't be doing something else, including lower-priority interrupts.  (priority as you've told the chip, which is not necessarily what you intended :palm:)  So do the absolute bare minimum that really must be done RIGHT NOW and get out.  If you can pare it all the way down to just setting a flag and leaving, then you can eliminate that interrupt altogether and just poll for the condition itself instead of the flag that you would have set.  That's a big plus!

The entire point of this section is to keep the interrupts reserved for when you really do need a drop-everything-immediate response.  You don't want to have to wait for printf to finish because you standardized on interrupts for everything, including your debug spew.
(Why are you using printf anyway?  It's HUGE!  In fact, most of the desktop-standard functions don't appear very often here.  The environment is just too small to make them practical.)

---

Multitasking is easy once you understand state machines.  Instead of busy-waiting for one thing at a time, you can poll-wait for lots of things at the same time.  Your main function might then look something like this:


--- Code: ---void main()
{
    //clock and other chip-wide set up

    init_module_A();
    init_module_B();
    init_module_C();
    init_module_D();

    while(1)
    {
        run_module_A();
        run_module_B();
        if (run_module_C())    //returns non-zero if some C-related event happened, this allows other modules to synchronize to it
        {
            trigger_module_D();
            module_A_input.foo = module_B_get_next_output();    //module A might have a simple static behavior, whereas module B has a sequence that must be kept
        }
        run_module_D();
    }

}

--- End code ---

and a module file might include:


--- Code: ---enum
{
    STARTING,
    BYTE_ODD,
    BYTE_EVEN,
    DONE
} state_machine;

void init_module_C()
{
    //set up ONLY what is needed to run this module

    state_machine = STARTING;
}

uint8_t run_module_C()
{
    switch(state_machine)
    {
    case STARTING:
        if (ready_to_start)
        {
            //start code here
            state_machine = BYTE_ODD;
        }
        break;

    case BYTE_ODD:
        //do something with the odd-numbered bytes
        state_machine = BYTE_EVEN;
        break;

    case BYTE_EVEN:
        //do something with the even-numbered bytes
        if (more_bytes_to_come)
        {
            state_machine = BYTE_ODD;
        }
        else
        {
            state_machine = DONE;
        }
        break;

    default:
        //error-correction and normal reset
        state_machine = STARTING;
        break;
    }

    return (state_machine == DONE);
}

--- End code ---

Now you can copy a module file to a different project that has a different use for that same concept, or even make a central library out of them, without rewriting anything.

Also notice the poll-wait for case STARTING.  This is how you wait for things without blocking everything else.

And it's usually faster to check for zero than for any other value.  Just load, and check the Z flag; instead of load, subtract, and check the Z flag.  It's not much, but if you're short on code space or processing time, it might help to arrange things like that.

---

Global variables are okay, if used SPARINGLY!  Intentional inputs and outputs of a module, for example, but nothing more.  Declare those in the module's header file; everything else in the source file.  Global variables in the source file are global to that module (below the declaration), but not to the entire program.  That can be useful too, but again, only when needed.

Like desktop programming, try to keep everything as local as you can get away with, except to the point of using trivial access functions.  In that case, just make the variable itself accessible; you often don't have much of a stack to work with.  (Recursion is right out!)

Likewise for function prototypes, custom datatypes, etc.  Only what the rest of the project needs to see goes in the header; everything else goes in the source.

---

There are some good embedded C++ compilers, but most of the time you don't need C++.  When you do, it's nice to have, and it's not really that bloated when you do it right, but most of the time you just don't need those tools at all.  C is perfectly fine.

---

Math is interesting in a lot of cases.  If you're on an 8-bit architecture (0-255 or -128 to 127), then you have a time penalty for using anything bigger.  Sometimes that's okay, sometimes not.  Likewise for using 32-bit numbers on a 16-bit architecture, etc.  The (u)intN_t datatypes tell you exactly how big it is: uint16_t is 16 bits unsigned (0 to 65535).

You might also be restricted to adding, subtracting, and shifting, as the only native operations.  (shifting is essentially multiplying or dividing by powers of 2)  Multiplication by a non-power of 2 can give you a significant time penalty if you don't have the on-chip hardware for it (some compilers are smart enough to convert a constant multiplier into a combination of shifts and adds; others just pull in their standard block of longhand library code), and division by a variable is a nightmare by comparison!  It's literally doing explicit long division in that block of library code.  So try to avoid it if at all possible.

Floating-point is even worse than that (floats and doubles), unless you have a floating-point accelerator, and then it only helps you for the size that it's designed for.  (a 32-bit FP accelerator works for floats, but not doubles)  Fixed-point is guaranteed to work anywhere, which is simply you the programmer keeping track of what fraction you're counting by, and fixing it up (shifting) as needed to keep the answer straight without overflowing or underflowing in the middle somewhere.  Instead of wishing you had 8 times the resolution in your 0-to-15 counter, just count by 1/8ths!  That's fixed-point.  The compiler and the hardware still think you're working with integers, so you need to keep track of the fractional point yourself, but that's how you get fractions on an integer-only machine.

(For a real-world example, "integer" audio is actually fixed-point that is entirely fractional.  Instead of -32768, 16384, 8192, 4096, etc. for a signed number, these bit values are -1, 1/2, 1/4, 1/8, etc.  They're handled by the exact same circuitry that handles "true integers", so the hardware can't tell the difference, but that's how the audio industry and the software that it uses actually interpret it.  A different size, like 8-bit or 24-bit, either truncates the fractional bits or adds more of them so that the peak value is always +/-1.  Small DSP's often have a few bits above the fractional point to make them slightly more forgiving, and larger DSP's almost always use floating-point with FP 1.0 = "integer" 011111111111... at the points of conversion.)
Kjelt:

--- Quote from: Remark on August 14, 2021, 11:50:33 pm --- I saw a lot of code in projects, that was written between while loop, performing several same operations,

--- End quote ---
This is the superloop vs interrupt driven vs RTOS discussion.
And the answer IMO is it all depends.
- Depends on platform (8 bit uC on 16MHz vs 32 bit arm uC on 100+MHz)
- product (code size, code complexity, price point, BOM targets, ROM size)

So in short there is no black and white, simply said on an 8 bit uC you often have to struggle with code size and superloops added with some time critical interrupt driven events were (are?) pretty common to 10 yrs ago.
For more complex products with display, GUI and some fancy protocols it is 32 bit Arm with an RTOS that is default, for even larger more complex fancy products with security Ethernet etc Embedded Linux is the way to go IMO that is etc.

To learn more I would advise to read up in RTOS vs superloop, RMA ( Rate Monotonic Analysis) and just play around with a small 8 bit superloop uC and give it more to eat than it can handle and you gain more experience.
nctnico:

--- Quote from: AaronD on August 15, 2021, 03:05:15 am ---Likewise, don't do anything more than what is absolutely necessary in an interrupt handler.  Any time that you spend there is time that you can't be doing something else, including lower-priority interrupts.

--- End quote ---
No, no, no, no. This is the worst advice ever. For one thing: you will need to spend the time processing the data coming from the interrupt one way or another. So the total amount of processing time stays the same. If you add overhead of buffering, then you actually make things worse because suddenly the slow main loop looking at buttons or blinking a LED becomes time sensitive.

The only proper way is to plan how much time is spend in each interrupt (including processing) and determine which interrupt should have the highest priority. From there it becomes clear if there are conflicting interrupts and you may need buffering but more likely there is a better way out of such situations (like combining interrupts into one). For example: if you are doing digital signal processing, you get input samples and output samples. If you write the output samples from the ADC interrupt then the output samplerate is automatically equal to the input samplerate; you don't need an extra output sample timer interrupt.

Some of my embedded firmware projects spend 90% of the time in interrupts doing signal processing.

All in all a better way to look at the interrupt controller is to regard it as a process scheduler and each interrupt routine is a seperate process. By setting lower / higher priorities and using interrupt nesting, you can have several concurrent processes without using an OS.


--- Quote ---Floating-point is even worse than that (floats and doubles), unless you have a floating-point accelerator, and then it only helps you for the size that it's designed for.

--- End quote ---
OMG  :palm: Really? More nonsense again. It all depends how much processing time you have available and you have plenty nowadays from a typical ARM-Cortex CPU. I have used soft-floating point in audio signal processing on a 70MHz ARM microcontroller about a decade ago.

Floating point makes working with numbers much easier (still keep an eye out for accumulating drift / errors) so you can write software quicker and keep the resulting code more readable / easier maintain. The first mistake to make when writing software is to start with optimisation before determining speed is actually a problem.

For example: if you need to read a temperature sensor input every 10 seconds then using soft-floating point has zero impact on performance. You probably can't even measure the extra time it takes.

I was brought up with soft-floating point being a big no-no in embedded firmware but in now I realise the people who told me that where very wrong.

Edit: and not using printf? Really  :palm:  Please use printf and don't go around re-inventing the wheel. If printf is too big or uses global memory, then implement your own which has a smaller footprint. The MSP430 GCC compiler -for example- comes with a very small vuprintf. And otherwise it is not difficult to find examples of even smaller micro-printf implementations. The worse thing to do by far is to invent your own string printing routines. I've seen those many times and they all sucked so bad that in the end even the original author started using printf. In the end the 'problem' (non re-entrant or code size) is in the vuprintf function, just fix the problem there.  In a professional environment you need to keep to standards as much as possible. The standard C library is such a standard. Don't go doing non-standard stuff because it will confuse and annoy the hell out of the person who needs to maintain the code after you.
Miyuki:
It all depends on the combination of Usage & MCU
Sometimes can be the best solution to use Arduino with its libraries or even some scripting, sometimes go bare metal with even assemble routines and sometimes it is best with some RTOS
When you want to do one calculation every long time and put it on the display, just take the highest level library with easy to use interface
When multitasking RTOS can really be savor even on weak 8bit AVR
There is no single best solution
Same with arithmetics, software floating point, fixed decimal point, or even come magic constant when you need it really fast and accuracy is not relevant.

Only thing that matters is to write clean code. In clean code is easy to find and fix bugs.
Do not copy past function blocks, call the function. If overhead is too much, just add it inline. Avoid mess like goto.
And with interrupts beware of volatile variables hazard.
Navigation
Message Index
Next page
There was an error while thanking
Thanking...

Go to full version
Powered by SMFPacks Advanced Attachments Uploader Mod