Reader small image

You're reading from  Hands-On Embedded Programming with C++17

Product typeBook
Published inJan 2019
Reading LevelIntermediate
PublisherPackt
ISBN-139781788629300
Edition1st Edition
Languages
Tools
Right arrow
Author (1)
Maya Posch
Maya Posch
author image
Maya Posch

Maya Posch is a senior C++ developer with more than 15 years of experience. Discovering the joys of programming early on, and later the joys of electronics, she has always expressed a profound interest in technology, a passion that she gladly shares with others. Describing herself as a C developer who happens to like C++ and Ada, she likes to seek the limits of what can be done with the minimum of code and hardware to accomplish everything that is cool, new, and exciting. She also enjoys FPGA development, AI, and robotics research, in addition to creative writing, music, and drawing.
Read more about Maya Posch

Right arrow

Appendix 1. Best Practices

As with every software project, there are a number of common issues and pitfalls. With embedded development, the hardware aspect is added to this, creating a unique set of issues. From resource management issues to interrupt troubles and weird behavior induced by hardware issues, this appendix shows you how to prevent and handle many of these issues. In addition, it shows you a variety of optimization approaches and what to be wary of. In this appendix, we'll cover the following topics:

  • Safe ways to optimize your embedded code
  • How to avoid and fix a variety of common software- and hardware-related issues
  • Recognizing the imperfect world of hardware and how to integrate this into your design

All the best-laid plans


As with any project, there's the inevitable divide between the intended design and how it functions in reality. Even with the best planning and bountiful experience, there will always be unforeseen or unnoticed issues. The best you can do is to be as well-prepared as possible.

The first step is to have access to all of the available information for the target platform, understand the tools that are available, and have a solid development and testing plan. We ran through many of these aspects in this book already.

In this appendix, we'll summarize a number of best practices that should help you avoid some of the more common issues.

Working with the hardware


Each target platform has its own quirks and characteristics. Much of this is due to the development history of that platform. For a platform such as AVR, it's fairly coherent, as it was developed by a single company (Atmel) over many years, so it's fairly consistent between different chips and the tools that are used for the platform.

 

A platform such as ESP8266 (and to some extent its ESP32 successor) was never designed to be used as a generic MCU system, which shows in its rather sketchy and fragmented software ecosystem. Though things have gotten better over the past few years, with various frameworks and open source tools smoothing over the roughest spots, it's a platform where it's easy to make mistakes due to a lack of documentation, issues with tools, and a lack of on-chip debugging.

The ARM MCUs (Cortex-M) are being produced by a wide range of manufacturers in a dizzying number of configurations. Though programming these MCUs tends to be fairly consistent, using tools such as OpenOCD, the peripherals added to each MCU tend to be wildly different between manufacturers, as we will look at in the next section.

Finally, ARM SoCs and similar find themselves in a position similar to ARM MCUs, but with significantly more complicated architectures and fewer peripherals than their MCU brethren. To this, ARM SoCs add a complex initialization routine, requiring comprehensive bootloaders, which is why most people opt to use a ready-made Linux image or similar for the SoC, and develop for that instead.

Here, there's no real right or wrong answer. Most of it comes down to what works for the project, but it's essential that you have a good overview of the hardware platforms you work with.

The confusing world of peripherals


A highly amusing reality with ARM MCUs is that they have different and often incompatible peripherals, mapped to highly different areas in the memory space. Worst of all here are timer peripherals, which come in a variety of complexities, with them in general being able to generate any desired output signal on a GPIO pin, including PWM, as well as work as interrupt-based timers to control the execution of the firmware.

Configuring timer peripherals and similar complex peripherals isn't for the fainthearted. Similarly, using a built-in MAC with an external PHY (Ethernet physical interface) requires a lot of in-depth knowledge to know how to configure them. Reading the datasheets and application notes is essential here.

Relying on autogenerated code by tools such as ST's CubeMX software for their STM32 range of ARM MCUs can lead to you wrestling with non-functional code because you forgot to tick a few boxes in CubeMX editor due to not being aware of what those options were for.

 

There's nothing wrong with using such auto-generating tools, or high-level libraries provided by the manufacturer, as they can make life significantly easier. It's however crucial to accept the risks that come with this decision, as it requires one to trust that the provided code is correct, or to spend time validating that it is indeed correct.

To make the use of peripherals across different MCUs and SoCs less confusing, one has to add a level of abstraction somewhere to allow for portability of the code. The key is to ensure that this does indeed make life easier and not just add another potential issue that may derail the current project or a future project.

Knowing your tools


While working on an embedded project, you have to know which tools exist for the target platform and how they work. This ranges from programming an MCU via JTAG or other interface and starting a debugging session for on-chip debugging, to the limitations of on-chip debugging. It pays to read the manual or documentation for a tool before using it and doing some reading up on the experiences of other developers with these tools.

We looked at a number of these tools in previous chapters, both for MCU and SoC platforms, along with ways to validate an MCU design before even flashing it on the target hardware.

Choosing asynchronous methods


Many hardware devices and operations take time to finish. It therefore makes sense to choose asynchronous actions using interrupts and timers instead of blocking operations.

When doing bare-metal programming, you'll tend to use a single loop with interrupt routines and timers that allow you to respond to and poll for events. If programmed in a fully asynchronous manner, this main loop will efficiently work through the tasks while the interrupt handlers update the data that has to be processed.

Even on SoC platforms, the use of asynchronous methods is a good idea, as things such as network operations and other I/O operations may take longer than desirable. Having ways to deal with operations not completing is another issue that pops up.

 

Reading the datasheet


Especially for MCUs, the datasheet gives us a lot of valuable information about how the hardware works, such as how to configure the internal system clock, how individual peripherals work, and available registers and their meaning.

Even if you use an existing board instead of a custom hardware system, it pays to understand the underlying hardware, even if it's from a cursory read of the MCU or SoC datasheet.

Keeping interrupt handlers short


The very nature of an interrupt dictates that it interrupts the regular execution of the processor, switching to the interrupt handler instead. Any microsecond that we spend in the interrupt handler code is a microsecond during which we aren't running the other routines or handling other interrupts.

To prevent any issues arising from this, interrupt handlers (ISRs) should be kept as short as possible, ideally merely updating a single value in a quick and safe manner before ending the ISR and resuming normal operation.

8-bit means 8 bits


Not surprisingly, the use of 16-bit and 32-bit integers on 8-bit MCUs is pretty slow. This is because the system has to perform multiple operations on the same integer value, as it can only fit 8 bits into its registers at a time.

Similarly, the use of floating-point variables on a system without a floating-point unit (FPU) means that such operations are highly suitable for slowing a system down to a crawl as the integer-only processor struggles to keep up with a flow of instructions aimed at simulating floating-point operations.

 

 

Don't reinvent the wheel


If a library or framework exists that's of a good quality and available for your target platform and project license, use it instead of writing your own implementation.

Keep a library of commonly used snippets and examples as a reference-not only for yourself, but also for other team members. It's easier to remember where you can find an example of a feature than it is to remember the exact implementation details of that feature.

Think before you optimize


The trick to optimizing code is that you should never attempt to do this without having a full understanding of what the change you're proposing will affect. Just having a feeling or a vague idea of how it might be a good idea isn't good enough.

While SoC-based platforms with a full OS tend to give you a bit more leeway, for MCU platforms, it's essential that you understand what the addition of a single keyword or use of a different data structure to store some information will mean.

The worst thing to do here is to assume that optimizations that you've used on SBCs and desktop systems will have a similar effect on an MCU platform. Due to the modified Harvard architecture and various quirks of platforms such as AVR, these are most likely to backfire or, if you're lucky, just be ineffective.

Here, the application notes provided for the (MCU) platform are useful to understand how the hardware can be optimized. The take-away message here is to do your research before making optimizations, just as one doesn't just start writing code without considering the project design.

Requirements are not optional


Writing embedded software without having a firm set of requirements for the project is like starting to build a new house without a clear idea of how many rooms it should have, where the windows and doors should be, and where the plumbing and wiring should run.

While you can totally start writing working code and hammer out a functioning prototype in no time, the reality is that these prototypes are usually put into production without having had time to fully consider the life cycle of the product, or those who will have to keep patching up the firmware over the coming years to add features that the original firmware code was never designed for.

 

After completing the requirements that the product has to fulfill, these are then translated into an architecture (the overall structure of the application), which is then translated into a design (what will be implemented). The design is then translated into the actual code.

The advantages of this approach are that not only do you need to answer a lot of questions about why something is done a particular way, it also generates a lot of documentation, which can then be used practically as is once the project is completed.

Additionally, in an embedded project having the full set of requirements can save a lot of money and time as it allows one to pick the right MCU or SoC for the project without having to spend more money on a more powerful than needed chip 'just in case'. It also prevents embarrassing mid-project discoveries where a feature which had been 'forgotten' about suddenly necessitates a change in the hardware design.

Documentation saves lives


It's become somewhat of a running joke that programmers don't like to write documentation and thus refer to the code that they've written as self-documenting code. The reality is that without clear documentation of the design requirements, architecture overview, design plans, and with the API documentation, you risk the future of the project and both one's fellow developers and the end-users who rely on the software to function.

Following procedures and doing all the boring paperwork before you can start writing the first line of code may seem like a complete killjoy. Unfortunately, the reality is that, without this effort, this knowledge will remain locked in the heads of the project's developers, which complicates the integration of the firmware into the rest of an embedded project and makes future maintenance, especially if moved to a different team, a daunting prospect.

The simple fact is that no code is self-documenting, and even if it were, no hardware engineer is going to go through thousands of lines of code to figure out what kind of signal is being put out on a specific GPIO pin when a particular input condition for the MCU occurs.

 

 

Testing code means trying to destroy it


A common mistake when writing tests is to write test scenarios that you expect will work. That's missing the point. While it's wonderful that a particular parsing routine did what it should do when it's handed perfectly formatted data, that's not very helpful in a real-life scenario.

While you can get perfect data, it's equally likely that you'll get completely corrupted or even garbage data in your code. The goal is to ensure that no matter what horrible things you do to the input data, it will never have a negative effect on the rest of the system.

All input should be validated and sanity checked. If something doesn't seem right, it should be rejected rather than you allowing it to cause issues elsewhere in the code later on.

Summary


In this appendix, we ran through a number of common issues and pitfalls that are likely to occur when working on an embedded software design.

The reader should now know what phases exist in projects, along with the reasons behind documenting every step of the project.

lock icon
The rest of the chapter is locked
You have been reading a chapter from
Hands-On Embedded Programming with C++17
Published in: Jan 2019Publisher: PacktISBN-13: 9781788629300
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €14.99/month. Cancel anytime

Author (1)

author image
Maya Posch

Maya Posch is a senior C++ developer with more than 15 years of experience. Discovering the joys of programming early on, and later the joys of electronics, she has always expressed a profound interest in technology, a passion that she gladly shares with others. Describing herself as a C developer who happens to like C++ and Ada, she likes to seek the limits of what can be done with the minimum of code and hardware to accomplish everything that is cool, new, and exciting. She also enjoys FPGA development, AI, and robotics research, in addition to creative writing, music, and drawing.
Read more about Maya Posch