Take It From The Top
Embedded systems aren't what they used to be. It wasn't long ago that a developer had to watch every instruction to make the most out of the tiny program storage and relatively low speeds of most processors. Today, even inexpensive processors outstrip a typical PC from a decade ago. You can afford to be a little sloppier unless you are really pushing the envelope.
I think it is more than just the increase in hardware capability, though. The accessibility of large embedded processors with high-level languages has brought a whole new breed of programmers into the embedded arena. Programmers that have a different background and experience.
Java-based processors haven't made the impact I thought they might, but if you count Android as a kind of Java, the impact was far in excess of my expectations. Small boards like the Raspberry Pi and Beagle Board are making it possible to write embedded applications in JavaScript or Python.
If you look at embedded software, it is usually pretty easy to guess if the developer had a hardware background or a "big" software background. I've seen embedded systems over the last few years that use 128-bit Globally Unique Identifiers (GUIDs) and big databases — things you normally see on workstation and server software.
I'm not saying that these are necessarily bad things. It all depends on the problem at hand. But when I see a PIC with a temperature sensor and a heater controller using 14 C++ classes backed up with six pages of UML charts, I have to wonder.
For simple programs, I often turn to a simple top-down design method that Forth was well suited for (even though I rarely use Forth anymore). The idea is to write your top-level program in the form of your high-level requirements (or your use cases, if you prefer). Then you decompose each of the functions into lower and lower level functions until you have something simple enough to just bang out and test.
For example, suppose I need a data logger that will read a few analog inputs every 5 minutes, write the results to EEPROM, and then when a modem carrier detect appears, empty the EEPROM data over a serial port. My main might look like this:
#include "datalog.h" #define NRCHAN 4 // number of channels to read/store #define CYCLETIME 300 // cycle time in seconds void main() { initialize(); while (true) { if (time_to_acq()) { if (!eeprom_full()) { store_sensor_readings(NRCHAN); } } // if (time_to_acq(CYCLETIME) if (carrier_detect()) { // send until caller acknowledges or hangs up while (!send_receipt() && carrier_detect()); { write_data_set(NRCHAN); } if (send_receipt()) { clear_eeprom(); clear_receipt(); } while (carrier_detect()); // after success, wait } // if (carrier_detect()) } // while (true) } // main
Your tastes in decomposition might be different from mine, but this is reasonably clean and self-documenting (more or less). At this stage, any call to a library function should be off limits. These functions are purely manifestations of your requirements. It is OK to reuse functions (like carrier_detect
) where you need the same function in more than one place.
The next step, of course, is to write all the functions you just made up. In some simple cases, these might actually do the task. Or you may decide to just decompose into more made up functions:
void clear_eeprom() { eeprom_data_pointer=0; erase_eeprom(); }
Library calls would be fair game at this level or below. Depending on what you are writing this can be a very productive way to develop. If you need more efficiency, you can always debug using this structure and then simply reduce the number of functions late in the development process.
This is hardly a new idea. You can consider it top-down design. It also has a lot in common with an idea in robotics called subsumption architecture. In Forth, you'd often go the opposite way: Figure out what little primitive operations you needed and then build them up into more complex words (similar to a function) until you had one word "datalog" (or whatever).