|For novel ideas about building embedded systems (both hardware and firmware), join the 40,000+ engineers who subscribe to The Embedded Muse, a free biweekly newsletter. The Muse has no hype and no vendor PR. Click here to subscribe.|
By Jack Ganssle
It's possible to write ugly, obfuscated, horribly convoluted and undocumented code that works. God knows there's an awful lot of it out there today, controlling everything from disposable consumer products to mission-critical avionics. One reader who works for an ABS brake company tells me their code is "absolutely hideous", which makes the thought of buying a 1971 VW Beetle appealing.
But it's equally possible, and quite a bit easier, to write finely-crafted functions whose inherent correctness shows through, and which are intrinsically maintainable. Here are some guidelines.
Are you an EE? My informal surveys suggest that around 60-70% of all firmware folks have electrical engineering degrees. That background serves us well in understanding both the physics of our applications as well as the intricacies of the hardware we control.
Yet most EE curriculums ignore software engineering. Sure, the professors teach you to program, and expect each student to be very proficient at building code. But they provide a didactic devoid of the critical tenants of software engineering necessary to building large, reliable, systems. The skills needed to create a working 500 line program do not scale to one of 100,000 lines.
Probably the most well-known yet least used rule of software design is to keep functions short. In my firmware lectures I ask how many attendees have an enforced function size limitation. It's rare to find even a single hand raised. Yet we know that good code is virtually impossible with long routines.
If you write a function that exceeds 50 lines - one page - it's too long. Fact is, you probably cannot remember a string of more than 8 or 10 numeric digits for more than a minute or so; how can you expect to comprehend the thousands of ASCII characters that make up a long function? Worse, trying to follow program flow that extends across page boundaries is tough to impossible, as we flip pages back and forth to understand what a gaggle of nested loops are doing.
Keep functions short. That implies that a function should do just one thing. Convoluted code that struggles to manage many disparate activities is too complex to be reliable or maintainable. I see far too many functions that take 50 arguments that select a dozen interacting modes. Few of these work well.
Express independent ideas independently, each in its own crystal clear function. One rule of thumb is that if you have trouble determining a meaningful name for a function, it's probably doing too many different things.
OOP advocates chant the object mantra of "encapsulation, inheritance and polymorphism" like Krishna devotees. OOP is a useful tool that can solve a lot of problems, but it's not the only tool we possess. It's not appropriate for all applications.
But encapsulation is. If you're so ROM-limited that every byte counts encapsulation may be impossible. But recognize that those applications are inherently very expensive to build. Obviously in a few extremely cost-sensitive applications - like an electronic greeting card - it's critically important to absolutely minimize memory needs. But if you get into a byte-limited situation figure development costs will skyrocket.
Encapsulation means binding the data and the code that operates on that data into a single homogeneous entity. It means no other bit of code can directly access that data.
Encapsulation is possible in C++ and in Java. It's equally available to C and assembly programmers. Define variables within the functions that use them, and insure their scope limits their availability to other routines.
Encapsulation is more than data hiding. A properly encapsulated object or function has high cohesion. It accomplishes its mission completely, without doing unrelated activities. It's exception-safe and thread-safe. The object or function is a completely functional black box requiring little or no external support.
A serial handler might require an ISR to transfer received characters into a circular buffer, a "get_data" routine that extracts data from the data structure, and an "is_data_available" function that tests for received characters. It also handles buffer overrun, serial dropout, parity errors, and all other possible error conditions. It's reentrant so other interrupts don't corrupt its data.
A collarary of embracing encapsulation is to delete dependencies. High cohesion must be accompanied by low coupling - little dependence on other activities. We've all read code where some seemingly simple action is intertwined in the code of a dozen other modules. The simplest design change requires chasing variables and functionality throughout thousands of lines of code, a task sure to drive maintainers to drink. I see them on the street here in Baltimore by the dozen, poor souls huddling in inadequate coats, hungry and unshaven, panhandling for applets. If only they'd "said no" to the temptation of global variables.
Get rid of redundant code. Researchers at Stanford studied 1.6 million lines of Linux and found that redundancies, even when harmless, highly correlate with bugs. (See www. stanford.edu/~engler/p401-xie.pdf).
They defined redundancies as code snippets that have no effect, like assigning a variable to itself, initializing or setting a variable and then never using that value, dead code, or complex conditionals where a subexpression will never be evaluated, since its logic is already part of a setting a memory mapped I/O port, since this sort of operation looks redundant but isn't.
Even harmless redundancies that don't create bugs are problems, since these functions are 50% more likely to contain hard errors than other functions that do not have redundant code. Redundancy suggests the developers are confused, and so are likely to make a series of mistakes.
Watch out for block-copied code. I am a great believer in reuse, and encourage the use of previously-tested chunks of source. But all too often developers copy code without studying all of the implications. Are you really sure all of the variables are initialized as expected, even when this chunk is in a different part of the program? Will a subtle assumption about a mutex create a priority inversion problem?
We copy code to save development time, but remember there is a cost involved. Study that code even more carefully than the new stuff you're writing from scratch. And when Lint or the compiler warns about unused variables, take heed: this may be a signal there are other, more significant, errors lurking.
Reduce Real-Time Code
Real-time code is error-prone, expensive to write, and even more costly to debug. If it's at all possible, move the time-critical sections to a separate task or section of the program. When time issues infiltrate the entire program then every bit of the program will be hard to debug.
Today we're building bigger and more complex systems than ever, with debugging tools whose capabilities are less than those we had a decade ago. Before processor speeds zoomed to near infinity and the proliferation of SMT packages eliminated the ability of coffee drinkers to probe ICs, the debugger of choice was an in-circuit emulator. These included real-time trace circuits, event timers, and even performance analyzers. Today we're saddled with BDM or JTAG debuggers. Though nice for working on procedural problems, they offer essentially no resources for dealing with problems in the time domain.
Remember also two rules of thumb: a system loaded to 90% doubles development time over one at 70% or less. At 95% the schedule triples. Real-time development projects are expensive; highly loaded ones even more so.
Flow With Grace
Flow, don't jump. Avoid continues, gotos, breaks and early returns. These are all useful constructs, but generally reduce a function's clarity. Overused, they are the basic fabric of spaghetti code.
XP and other agile methods emphasize the importance of refactoring, or rewriting crummy code. This is not really a new concept, Capers Jones, Barry Boehm and others have shown that badly-written modules are much more expensive to beat into submission and maintain than ones with a beautiful structure.
Refactoring zealots demand we rewrite any code that can be improved. That's going too far, in my opinion. Our job is to create a viable product in a profitable way; perfection can never be a goal that overrides all other considerations. Yet some functions are so awful they must be rewritten.
If you're afraid to edit a function, if it breaks every time you modify a comment, then it needs to be refactored. Your finely-honed sense as a professional developer that, well, we just better leave this particular chunk of code intact because no one dares mess with it, is a signal that it's time to drop everything else and rewrite the code so it's understandable and maintainable.
The second law of thermodynamics tells us that any closed system will head to more disorder; that is, its entropy will increase. A program obeys this depressing truth. Successive maintenance cycles always increases the software's fragility, making each additional change that much more difficult. As Ron Jeffries pointed out, maintenance without refactoring increases the code's entropy by adding a "mess" factor (m) to each release. The cost to produce each release looks something like: (1+m)(1+m)(1+m)!., or (1+m) 12.0pt">n, where n is the number of releases. Maintenance costs grow exponentially as we grapple with more and more hacks and sloppy shortcuts. This explains that bit of programmer wisdom that infuriates management: "the program is too much of a mess to maintain".
Refactoring incurs its own cost, r. But it eliminates the mess factor, so releases cost 1+r+r+r!, which is linear.
Luke Hohmann advocates "post release entropy reduction." He recognizes that all too often we make some quick hacks to get the product out the door. These entail a maintenance cost, so it's critical we pay off the technical debt incurred in being abusive to the software. Maintenance is more than cramming in new features; it's also reducing accrued entropy.
Refactor to sharpen fuzzy logic. If the code is a convoluted mess or just not absolutely clear, rewrite it so it better demonstrates its meaning. Eliminate deeply nested loops or conditionals - no one is smart enough to understand all permutations of IFs nested 5 levels deep. Clarity leads to accuracy.
Employ Standards and Inspections
Write code using your company's firmware standard. Use formal code inspections to insure adherence to the standard and to find bugs. Test only after conducting an inspection.
Inspections are some 20 times cheaper at finding bugs than traditional debugging. They'll capture entire classes of problems you'll never pick up with conventional testing. Most studies show that traditional debugging checks only about half the code! Without inspections you're most likely shipping a bug-ridden product. It's interesting that the DO-178B standards for building safety critical software rely heavily on the use of tools to insure every line of code gets executed. These code coverage tools are a wonder, but are no substitute for inspections.
I've written much about both of these issues, so refer you to (http://embedded.com/98/9808br.htm) and (March 1998 and April 1998) and (http://embedded.com/story/OEG20020221S0084). But it's important to emphasize how standards and inspections are tightly bound to each other; neither will succeed without the other.
Without standards and inspections it's just not possible to habitually build great firmware products.
When I was just starting my career an older fellow told me what he called The Fundamental Rule of Engineering: if the damn thing works at all, leave it alone. It's an appealing concept, one I used too many times over the years. The rule seems to work with hardware design, but is a disaster when applied to firmware. I believe that part of the software crisis stems from a belief that "pretty much works" is a measure of success. Professionals, though, understand that developmental requirements are just as important as functional and operational requirements. Make it work, make it beautiful, and make it maintainable.