Embedded software development. Best practices. - Page 1
Forum section: electronics / general

author: Remark
author_rank: Contributor
timestamp: August 14, 2021, 11:50:33 pm
content: 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

author: AaronD
author_rank: Frequent Contributor
timestamp: August 15, 2021, 03:05:15 am
content: 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) 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 upinit_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();}}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 modulestate_machine = STARTING;}uint8_t run_module_C(){switch(state_machine){case STARTING:if (ready_to_start){//start code herestate_machine = BYTE_ODD;}break;case BYTE_ODD://do something with the odd-numbered bytesstate_machine = BYTE_EVEN;break;case BYTE_EVEN://do something with the even-numbered bytesif (more_bytes_to_come){state_machine = BYTE_ODD;}else{state_machine = DONE;}break;default://error-correction and normal resetstate_machine = STARTING;break;}return (state_machine == DONE);}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_tdatatypes tell you exactly how big it is:uint16_tis 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.)

author: Kjelt
author_rank: Super Contributor
timestamp: August 15, 2021, 08:26:11 am
content: Quote from: Remark on August 14, 2021, 11:50:33 pmThis 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.

author: nctnico
author_rank: Super Contributor
timestamp: August 15, 2021, 09:21:18 am
content: Quote from: AaronD on August 15, 2021, 03:05:15 amNo, 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.QuoteOMGReally? 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? ReallyPlease 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.

author: Miyuki
author_rank: Frequent Contributor
timestamp: August 15, 2021, 04:41:57 pm
content: It all depends on the combination of Usage & MCUSometimes 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 RTOSWhen 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 interfaceWhen multitasking RTOS can really be savor even on weak 8bit AVRThere is no single best solutionSame 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.

author: Remark
author_rank: Contributor
timestamp: August 15, 2021, 05:47:49 pm
content: Thank you so much for the answers to everyone.And what about binary trees (nodes and leafs), it is used for embedded programming?

author: nctnico
author_rank: Super Contributor
timestamp: August 15, 2021, 07:27:33 pm
content: Quote from: Remark on August 15, 2021, 05:47:49 pmYou can but allocating/freeing memory using functions like malloc/free is a bit iffy because microcontrollers typically don't have enough memory to avoid fragmentation of memory. All in all you'll likely need to create some kind of memory manager and make sure to be able to deal with a shortage of memory in a graceful way.

author: SiliconWizard
author_rank: Super Contributor
timestamp: August 15, 2021, 10:03:11 pm
content: Binary trees, like linked lists, can be implemented without dynamic allocation per se.

author: Neomys Sapiens
author_rank: Super Contributor
timestamp: August 15, 2021, 10:52:23 pm
content: Limiting interrupt processing to the ABSOLUTE MINIMUM NECESSARY is how PLCs achieve their deterministic reaction times and high-integrity program scheduling. Read regular inputs - process cyclical task - write regular outputs. For anything aquired during interrupt processing, you will have to analyse diligently at which point of the program's processing you should introduce this data. Because it would have a detrimental if not even damage-prone effect in most RT applications if you are half through with a calculation and logic processing and then perform the remaining part with a measurement value which does not belong to the same state of the real-world process than your binary inputs.

author: dmills
author_rank: Super Contributor
timestamp: August 16, 2021, 12:06:34 pm
content: Binary trees (Sometimes built as a pre processing step on a real computer), skip lists (Same thing, sort of), hashing, really all of the algorithms stuff gets used (And unlike on something like a PC) you often find yourself writing these things because the generic library ones either do not fit your memory model or bring in unimportant features that cost you too much code space.Doing things like using the low two bits of a pointer (in a 32 bit machine) for a couple of flags very much is a thing in embedded programming, and you just never need to do that shit on a PC.On the interrupt thing, it depends on context, I have DSP codes running on processors that spend 90% of their time in an ISR that actually does all the signal processing in a hard realtime situation, the main loop is literally while (1) {};

author: nctnico
author_rank: Super Contributor
timestamp: August 16, 2021, 12:31:06 pm
content: Quote from: Neomys Sapiens on August 15, 2021, 10:52:23 pmNo. PLCs don't work that way. PLCs have a fixed (configurable) cycle time in which they process all inputs and calculate new output values. And it is possible to run several of these cycles sequentially as if they are parallel processes on more advanced PLCs. But this is all done at a higher (OS) level and the PLC's CPU idles when it is done processing. However, if your PLC program takes too long then the cycle time won't be met. In the end there is no magic bullet to add extra processing power.

author: PaulAm
author_rank: Frequent Contributor
timestamp: August 16, 2021, 02:48:51 pm
content: Be aware of what your target hardware is and code appropriately.As an example, say your target doesn't support floating point hardware. Even one line of C involving a floating point operation will cause the floating point emulation libraries to be loaded into the object. If you have hardware with very limited stack or code space, that could be a disaster.FSAs are very useful in many situations. However, hand managing FSA tables can be a nightmare and even worse for maintenance. The best way I've found to deal with FSAs over a 40 year career is an FSA generator called Libero written by Pieter Hintjens back around 2000. Pieter was an absolute genius. The code is available on github (https://imatix-legacy.github.io/libero) and will compile with out too much effort. The FSA is described using a dialog and the tool will generate the appropriate tables, stubs, etc. Modifications become fairly trivial since you work at a high level in the dialog and then fill in the short action stub routines. I can't say the examples are all that great, but it's worth a couple days effort to master. The really interesting thing about Libero is that it is not language specific. It uses templates to generate the target code and it will happily turn the FSA into anything. There are templates for Perl, shell, PHP, C, C++ , PC assembly and yes, even Cobol, among others. It's really a universal tool if you can get your head wrapped around it.

author: emece67
author_rank: Frequent Contributor
timestamp: August 16, 2021, 04:05:36 pm
content: .

author: Neomys Sapiens
author_rank: Super Contributor
timestamp: August 16, 2021, 04:16:22 pm
content: Some useful resources (several are in German):Real-Time Systems Development, A practical Guide to; Sylvia Goldsmith, Prentice Hall, 1993, ISBN 0-13-718503-0Entwurf komplexer Echtzeitsysteme - State of the Art; Hüsener, BI, 1994, ISBN 3-411-16441-7Software Engineering Handbook; Jessica Keyes, Auerbach, 2003, ISBN 0-203-97278-3Handbook of Real-Time and Embedded Systems; Insup Lee et.al, Chapman&Hall/CRC, 2008, ISBN 1-58488-678-1Design and Analysis of Reliable and Fault-Tolerant Computer Systems; Mostafa Abd-El-Barr, Imperial College Press, 2007, ISBN 1-86094-668-2Programmable controllers: theory and implementation, L.A. Bryan & E.A. Bryan, Industrial Text and Video, 1997, ISBN 0-944107-32-XControl System Fundamentals (The Control Handbook - 2nd ed.); William S. Levine (Ed.), CRC Press, 2011, ISBN 978-1-4200-7363-8Computing Handbook - Computer Science and Software Engineering; Allen B. Tucker (Ed.) Chapman&Hall/CRC, 2014, eISBN 978-1-4398-9853-6Embedded Systems Handbook, Richard Zurawski (Ed.), CRC Press, 2006, ISBN tbdHigh Performance Embedded Computing Handbook - A Systems Perspective; D.R. Martinez, R.A. Bond, M.M. Vai (Eds.), CRC Press, 2008, ISBN 978-0-8493-7197-4Numerical Methods for Real-Time and Embedded Systems Programming; Don Morgan, M&T Books, 1992, ISBN 1-55851-232-2Embedded Systems Building Blocks - Complete and Ready-to-Use Modules in C; Jean J. Labrosse, R&D Books, 2000, ISBN 0-87939-604-1Anwendungsorientierte Mikroprozessoren, Mikro-controller und Digitale Signalprozessoren; H. Baehring, Springer, 2010, ISBN 978-3-642-12291-0Practical Aspects of Embedded System Design using Microcontrollers; Balakrishnan Selvan et.al, Springer, 2008, e-ISBN 978-1-4020-8393-8Software-Implemented Hardware Fault Tolerance; O. Goloubeva et.al, Springer. 2006, ISBN 0-387-32937-4Embedded Systems - Hardware, Design, and Implementation; Krzysztof Iniewski, Wiley, 2013, ISBN 978-1-118-35215-1Real-Time Systems Development; Rob Williams, Butterworth-Heinemann, 2006, ISBN 978-0-7506-6471-4Embedded Systems Design; Steve Heath; Newnes, 2003, ISBN 0-7506-5546-1Embedded Systems and Computer Architecture, Graham Wilson, Newnes, 2002, ISBN 0-7506-5064-8Mission-critical and safety-critical systems handbook : design and development for embedded applications; K. Fowler (Ed.), Newnes, tbd, ISBN 978-0-7506-8567-2Embedded Systems (World Class Designs); Jack Ganssle et.al, Newnes, tbd, ISBN tbdEmbedded Software Know-It-All; Labrosse et.al, Newnes, 2008, ISBN 978-0-7506-8583-2Embedded Hardware Know-It-All; Jack Ganssle et.al, Newnes, 2008, ISBN 978-0-7506-8584-9

author: Remark
author_rank: Contributor
timestamp: August 16, 2021, 04:27:40 pm
content: Quote from: emece67 on August 16, 2021, 04:05:36 pmAnd you probably apply OOP programming when you use the C++ language?

author: AaronD
author_rank: Frequent Contributor
timestamp: August 16, 2021, 05:02:08 pm
content: Quote from: nctnico on August 15, 2021, 09:21:18 amWhat are you running on? Embedded Linux? In that case, you'd be right, and arguably the correct way to do it for the reasons that you stated. But on an 8-bit 10MIPS machine with 300 bytes of RAM (yes, bytes; not even kbytes) that needs a "humanly-instant" response time - the sort of thing that I usually do - your approach could barely do anything at all!Also note that I never said to not do *anything* in interrupts. I have an interrupt-driven ADC module, for example, that runs a lowpass filter from a 10-bit SAR converter to an array of 16-bit "output" variables for everything else to use. The only reason it's interrupt-driven is to keep the sample rate up across all channels so I can have a decently functional filter. And that oversampled filter is indeed done in the ISR because the data that it works on is that ephemeral. Drop the results in the global output array, and then the rest of the code can pick up the "magically existing ADC readings" whenever it gets around to it.When every instruction cycle and every byte of memory is important, you do things differently. Still keep it sensible, but in the sense of someone seeing how your no-extras-at-all debug spew works (which could just as easily be a PWM-driven or bit-banged "pin-wiggler" for an oscilloscope to look at) so they can get on with the actual function of things, instead ofover not getting printf to fit; or how to output an ASCII stream anyway, when your only UART is already tied up with DMX or whatever.(and yes, that includes myself after a few months)---If the main loop takes longer than the shortest required non-interrupt response time, then there's something else wrong. If you can't reduce or redistribute the workload (so you don't end up triggering *everything* on the same pass, often by accident or naivete), then you might poll the more critical things more than once throughout the main loop code.That's one place where a bit-banged "pin-wiggler" comes in really handy! (if you have a spare output pin) Toggle it once per loop, or once per poll of the critical thing, and you can see on the 'scope or logic analyzer, exactly how your timing works out.

author: nctnico
author_rank: Super Contributor
timestamp: August 16, 2021, 06:06:35 pm
content: Quote from: AaronD on August 16, 2021, 05:02:08 pmNo. Regular microcontrollers like NXP's LPC ARM series or TI's MSP430 for example.QuoteBut who would use such a constricted microcontroller nowadays? If you are into high volume products maybe but in such a case you likely start with a bigger microcontroller for test & design verification. For anything produced in less than 10k units the NRE costs will be a huge part of the cost of a product. So if you can reduce the NRE cost by using a microcontroller that doesn't need balancing on one foot while touching your nose with the other then you are already ahead. Plus there is likely room for future extensions as well.

author: AaronD
author_rank: Frequent Contributor
timestamp: August 16, 2021, 06:58:34 pm
content: Quote from: nctnico on August 16, 2021, 06:06:35 pmI used to work for a mass-produced niche company that used something about the size that I've been talking about as their jelly-bean standard. At their volume, they could get pretty much the entire family of that chip for almost nothing. And by the time I started with them, most of the "cramming it all in" work was already done, and they had a somewhat comfortable set of linker commands and in-house libraries to replace the compiler's bloated libraries. ("Don't use the '*' symbol! Call our mult(a, b) function instead, that one of our guys wrote by hand in assembly.")There was one project though, where the hardware designer(s) grossly underestimated the amount of processing it was going to take. Fortunately, I had only spent the first week or so of my allotted 2 months or whatever it was, to figure out how to do it and then write the code that almost did it. Enough to see that it would indeed work that way if I could cram it into the available code space. (RAM and CPU time were not a problem in this case, only code space) So I spent the next month and a half refactoring, hand-optimizing, reading and criticizing what the compiler thought was good assembly (I can do that myself in 2 fewer instructions!), tweaking the linker to fill some holes, and sometimes combining unrelated functions just because they had similar parts so there would only be one copy of those parts, and never using the complete results of that combined function.The result was something that was finished on time and worked beautifully on the outside, but was a little weird on the inside (commented profusely!) and barely fit. The next version was definitely going to have a bigger chip! But then the project was cancelled for unrelated reasons.So yes, you make a very valid point about using a bigger chip than what you think you need. But there are lots of places, even today, where the rules and skills that I mentioned are valid too.

author: indeterminatus
author_rank: Contributor
timestamp: August 16, 2021, 08:16:55 pm
content: Quote from: Miyuki on August 15, 2021, 04:41:57 pmStressing that point.

author: emece67
author_rank: Frequent Contributor
timestamp: August 16, 2021, 09:13:11 pm
content: .

author: PlainName
author_rank: Super Contributor
timestamp: August 17, 2021, 02:45:39 am
content: QuoteThe website appears to be but there are no downloads. Hardly surprising since it's over 20 years old, but I was certainly tempted to take a look!

author: nctnico
author_rank: Super Contributor
timestamp: August 17, 2021, 07:55:44 am
content: Quote from: Miyuki on August 15, 2021, 04:41:57 pmNot just that, also easy to debug code. One of the things that is standard in all my microcontroller projects is a serial port command line interface. This can be used to read the status of the device, attached devices and get some statistics as well (like number of messages received, failed messages, etc). This is something I picked up at one of my employers and it has proven to be extremely handy for field diagnostics / remote debugging. Either I go to the customer's site and look what the output is or a customer can hook up a serial port and send me the output. Either way the problem usually is clear quickly.

author: PaulAm
author_rank: Frequent Contributor
timestamp: August 17, 2021, 02:26:56 pm
content: QuoteI did a download a few months ago, let me track down the source link. It's out there somewhereOK, here it is:https://github.com/imatix-legacy/liberoThey moved the source from the web page to it's own github repository.

author: PlainName
author_rank: Super Contributor
timestamp: August 17, 2021, 03:23:35 pm
content: Blinding, thanks

author: SiliconWizard
author_rank: Super Contributor
timestamp: August 17, 2021, 05:19:51 pm
content: Quote from: indeterminatus on August 16, 2021, 08:16:55 pmOh yeah. I'm all for that too. Problem though is that pretty much everyone has a different view of what clean code is.

author: Sal Ammoniac
author_rank: Super Contributor
timestamp: August 17, 2021, 09:36:34 pm
content: Quote from: nctnico on August 15, 2021, 09:21:18 amYou seem to like broad, sweeping generalizations (and to some extent so did the person you quoted). All embedded systems are different and they all need to be analyzed and their software designed for each specific case. Sure, there are embedded development best practices that should generally be followed, but sometimes it's also necessary to break the rules.

author: AaronD
author_rank: Frequent Contributor
timestamp: August 17, 2021, 10:21:58 pm
content: Quote from: Sal Ammoniac on August 17, 2021, 09:36:34 pmYep! Like one of my grade-school teachers said in English class:QuoteI think the same goes for Engineering, which Embedded Software certainly is. Each level of complexity, from a 4-pin MCU to a Raspberry Pi, has a different set of rules, and there isn't necessarily a clear-cut distinction between adjacent levels.When you're at a high enough level that you can afford the overhead to use someone else's generalized work, then it's usually a good idea to do that. The wide use usually means that whatever bugs it used to have are gone now, so any problems are probably with your use of it and not the library itself.Below that level though, the constraints are often tight enough to forbid using a general-purpose library. (a company's hand-optimized in-house math library, for example, instead of what the compiler does) And there's still a lot of good work both above and below that level...if you can figure out where the cutoff is; good luck with that!---And if you're just starting out, you WILL write bad code. Do it anyway. You'll make it work, and you'll be proud of it in the moment, but when you look back on it after a few years of using those tools, you'll cringe. Yes, it works, but WOW that looks awful now! There's really no way to avoid that. Just do it and get through it.

author: laureng
author_rank: Newbie
timestamp: August 19, 2021, 09:22:19 am
content: A few thoughts based on what I've done/am doing. Not claiming best practice.I'm using C++ on PIC32 microcontrollers, and compiling with the XC32 compiler (from Microchip). Works well, but when you start writing larger programs, you really need to get a paid license to use the "-Os" optimisation, which drastically reduces code size. (There are other free hacks for size - see below.)I like the main "do it" loop, which calls various objects and lets them do their own work. I have a "Runnable" interface that just declares a void run(), and classes can inherit that if they have some work to do during the main loop.Yes, there's the "interrupts good" vs "interrupts bad" camps. Arguments against are that interrupts are non-deterministic. I use them sparingly, mainly for time-critical hardware I/O (e.g. reading from a hardware serial RX FIFO, where the FIFO has limited depth and can overrun quickly).With the "do it" loop approach and objects, you're effectively implementing cooperative multitasking, so, just like if you were using interrupts, each individual object shouldn't be blocking for ages when it gets control. In the past I've used non-blocking state machines, but currently I'm blocking for external I/O (e.g. between command and response) if I think I'll get a guaranteed, quick replyFrom the above, you might find yourself writing code that has a sort of "handle cranking" behaviour, where a higher level object triggers a side effect in a lower level object that then avoids the need for the lower level object to explicitly receive control in the loop. An example would be a business logic class that calls a "write" function on a serial port object, where the serial port object then does some hardware I/O to send data over the serial port immediately (rather than, e.g., queuing the data and waiting for explicit control in the main loop). This works fine if the action can happen immediately and doesn't need repeating - if, e.g., you're sending a message over an RF link and the outbound message gets lost, and then your higher-level object immediately expects a reply (which never arrives), and your higher level object blocks forever, you're in trouble. Options are to have the higher level object timeout and retry, or to make both objects into non-blocking state machines (more robust, but greater complexity).There are also the "heap good" and "heap bad" camps. Arguments against heap are non-deterministic behaviour that can lead to heap fragmentation and/or exhaustion. I shamelessly use the heap, because I like collection classes, and it's sometimes easier to just new() and delete() rather than using the stack, but I'm writing code where if it crashes, the user is just pissed off and will reboot the device. If you're writing something safety critical, where blowing the heap will cause someone to die, you might exclusively use the stack. Consider profiling the heap. This can be done by writing a repeating pattern to the heap, with assembly code, before crt0, then later checking the extent of pattern remaining. I also do it by writing a test harness around my business logic that can be compiled under Linux (with hardware devices like LCD screens stubbed out - more on this below), then running a heap profiler like valgrind. You can also profile stack and check for memory errors with valgrind, which might let you track them down more easily than using the ICD.STL containers use the default allocator, and this throws exceptions. Exceptions create memory bloat. I use a custom allocator (based onthis blog post), that doesn't throw exceptions, then use the STL containers (vector, deque, map...) with that allocator. You can then compile with -fno-exceptions.std::string also throws exceptions, and you can't make it use a different allocator like you can for the STL container classes. I create my own string class, which inherits std::basic_string, then have it use my nothrow allocator.printf adds a lot of bloat (like 20KB or something?). I usea tiny printf by Marco Paland.Further size hacks - with XC32 and other gcc derivatives, take a look at flags like "-Wl,--report-mem -ffunction-sections -fdata-sections -Wl,--gc-sections,--print-gc-sections -save-temps -Wl,-Map,mapfile.map" to (in order) get a memory report from the linker, move things into their own linker sections, garbage collect unused sections at link time (and show what was removed), save intermediate code (assembly) and produce a memory map. Take a look at the memory map to see what's using the most space, then dive into the assembly to see if you can work out why it's bloated. Getting the compiler to separate everything into sections for linker garbage collection runs the risk of having useful code removed by accident, but you'd be surprised how much stuff you don't need that can be culled.Streams are useful for debugging, but add a lot of bloat too! I write my own stream class that uses tiny snprintf for operator<< on ints, uints and doubles.TheDependency Injection Patternis awesome IMHO. I have a builder (seeBuilder Pattern) that creates all necessary objects, injecting most dependencies into each via their constructors. This lets you really easily create different builders for simulators and different hardware configurations, without needing any new code outside of writing a new builder. For example, the builder for real hardware could inject an actual LCD object when creating objects that need to output to an LCD display, but the builder for a simulator could inject a stubbed or debug LCD object. Both would inherit some abstract interface like "LCDInterface", so the object being constructed doesn't know or care if it's getting a real or fake LCD display. For bonus points, use inheritance in builders to reduce code re-use, by putting the common build logic in a base class.Factory Patternis another useful design pattern for loose-coupling between objects. Basically if you need to make things, you don't really want to know how they're made (SOLID - "depend upon abstractions, not concretions"), so you use a factory to make things for you, and the factory hides the construction detail. An example might be creating a UDP socket object (itself inheriting from some socket abstraction), where you could have different factories for hardware UDP sockets (using some hardware library) and for POSIX UDP sockets (for when you're in a simulated build running under Linux). Both factories inherit from a factory abstraction, so the thing making sockets just says "give me a socket" and it gets an object that works like a socket for both simulated and actual hardware builds. You can go too far with design patterns, but for any non-trivial code, this sort of decoupling makes the difference between pro code that's easily maintainable and expandable, and noob code that becomes a total mess. Specifically for embedded systems, loose coupling and abstraction will really help with simulation, porting and debugging.In the above spirit of loose coupling (see also dependency inversion, as part of theSOLID principle), when your embedded code is so modular that it's easy to compile desktop simulators with mock UIs (e.g. implemented using Qt), you realise you can do a lot of other awesome things too. Seeing the same business logic running embedded and on the web (e.g. C++ compiled to WebAssembly with Qt-based I/O devices) is cool.That's all I could think of from the top of my head. Again, I'm not claiming any of these are best practice or how anyone else should code (depends on your chip and application).I'd love to expand on some of this in a blog post some time. I'm not much for reading thick programming books (although some are excellent), and there really isn't a huge amount (that I've found) on embedded C++ that has all these tricks in one place. I really do think C++ can be great in embedded, but as above, some of the language and STL "features" (like exceptions being almost irremovably baked into things) can make it hard to use in practice.EDIT: Added links and blurb on factory pattern.

author: emece67
author_rank: Frequent Contributor
timestamp: August 22, 2021, 02:01:39 pm
content: .

author: miceuz
author_rank: Frequent Contributor
timestamp: August 24, 2021, 10:06:30 am
content: Quote from: nctnico on August 17, 2021, 07:55:44 amAnother thing I have picked up - implement command line interface via said serial port. This proved again and again to be priceless tool for debugging and test automation.