How I write software - Embedded.com

How I write software

In the two decades or so that I've been writing this column, we've covered a lot of topics, ranging from the best implementation of abs(x) to CRC algorithms to logic theory, Karnaugh maps, and electronic gates, to vector and matrix calculus to Fourier and z-transforms to control theory to quaternions, and lots more. I even wrote about whether or not the ancient Egyptians really built the values of π and Φ into the Great Pyramid (they did. And , too).

Almost all of the topics involved math, but they also required software, either for embedded systems or just to illustrate a given algorithm. But I've never talked much about the way I write software. I thought the issue of my own personal software development methodology would be, at best, a distraction from the topic at hand and, at worst, a subject of great hilarity.

I've always known that the way I write software is different from virtually everyone else on the planet. Is this true for everyone? I don't have a clue.

That's why I thought it would be fun to share with you the way I write software. If you do it the same way, that's great. It would be nice to know that there are other folks who share my approaches. On the other hand, if it's not the same way you do it, we have three possible outcomes. Either you'll learn from me, and decide that some of my approaches have merit; I'll learn from you, and decide that I've been doing it wrong all these years; or my revelations will serve as a subject of great hilarity. Any of those outcomes, I submit, would be good. It's a win-win-win situation.

The environment
Let's begin with the software development environment. In my time, I've used some really rotten development environments. I've worked with systems whose only I/O device was a Teletype ASR-33. We got so tired of emptying the chad-catcher on the paper tape punch, we just let it flutter to the floor. (At the end of the day, guess who got to sweep it up.)

I worked on one program where my only “compiler” was a line-by-line assembler (meaning that it didn't accept mnemonic names, only absolute addresses). The bulk storage device was an audio cassette. Just so we're clear, this wasn't a homebrew system I cobbled up out of old Intel 4004s. paper clips, and chewing gum. It was a system built up by serious, if boneheaded, managers in a very large corporation.

Perhaps because of these nightmare environments, I really appreciate a good one. I especially love using integrated development environments (IDEs), in which all the tools are interconnected, preferably by a lovely and fast GUI interface. I know, I know, real men don't use GUIs or IDEs. Some folks much prefer to use only command-line interfaces. and to write ever more complex and inscrutable makefiles.

If that's you, more power to you. Whatever works. But I can't hide my view that such an approach is an affectation, involved more with earning one's credentials as a software guru than on producing good code. I'm sure that there are good, valid reasons to write a makefile rather than let the IDE do it for you. I just can't think of one, at the moment.

So what do I do when I'm told that I must use the environment the project managers already picked out? First of all, I'd ask them (always discretely, of course, in my usual, ultra-tactful manner) why on earth they chose the development environment without discussing it with those of us who had to use it. Second, I'd campaign mightily for an environment more suited to my approaches.

As an aside, I used to teach a short course in the development process for embedded microprocessors. I told the management folks that they should plan to give their software folks every possible computer tool available–hang the expense. I pointed out that software development is very labor-intensive, so the front-end cost of good tools would be quickly made up by increased programmer productivity.

Needless to say, sometimes I'm successful in changing the managers' minds. More often, I'm not. So what do I do then? I make do as best I can, but I discreetly tweak the system and my approach to make the environment behave more like an IDE. For example, I might add script files, hot keys, etc., so that pressing a single button would invoke one tool from another.

Requirements
Crenshaw's Law #42 says: Before setting out to solve a problem, find out what the problem is. Law #43 says design it before you build it, not after.

In the software world, this means performing requirements analysis. Write down, in as much detail as you can, what problem you want the software to solve, how you want it to solve the problem, and how you want people to interface with it.

In the 1980s, the popular project plan was to start with an analysis of the system requirements. Then you decompose into software requirements, design requirements, etc. At the end of each phase, you hold a formal review before moving onto the next step. No fair skipping ahead.

They called this process the “waterfall” approach. If you were writing a formal proposal to NASA, DoD, or whoever, a clever picture of the waterfall could win you the contract. In the waterfall diagram, the “code and unit test” phase of the plan was usually the next-to-last step in the process (the last being integration and test). And woe be upon anyone who skipped ahead to writing code.

Today, the waterfall approach has been completely discredited and deemed ineffective. Personally, I think the only reason it seemed to work was that nobody actually followed it. Some of us practitioners broke the rules, and skipped ahead to try out our ideas on the side. The whole thing was a sham. We had slick-looking and impeccably dressed salespersons giving formal presentations to the customer, showing them how wonderfully the waterfall approach was working, and assuring the customer that we were 90% complete. Meanwhile, a few of us slaved away in a secret back room, using approaches that actually worked.

But those were the bad old days. Today, the hot keywords are spiral development, prototyping, incremental and agile approaches, and extreme programming (XP). I'm a big fan of XP, though I'm not sure that what I do fits the mold.

Today, no one in their right mind would still support the waterfall approach, right? Well, I don't know. In 1984 our software managers had a rule: you were not allowed, on penalty of–something bad–to even run the compiler, until your software had passed code review. Even then, the compiler was never to be used for developing test drivers and supporting unit testing. It was only to be used on the entire system build, after all integration had been done.

You should go back and reread that paragraph, to really get your arms around the concept. We had a nice, and relatively modern, timeshare system (Unix), with a nice compiler, programming editor, debugger, and other aids, but we weren't allowed to use the compiler. We were perfectly free to use the editor to write the code but only to document our designs for the code reviews. We were expressly forbidden to actually run the compiler. How brilliant was that ?

But that was 25 years ago. We don't do anything so ridiculous now, right? Well, let me see ….

On a more recent project, I heard a manager berate a colleague for actually writing code before the requirements review. If he had been working in his old company, the manager said, he would have fired the guy for such a violation of the rules.

That was way back in . . . let me see . . . 2008.

I won't say much more about project planning or about requirements analysis here. In part, it's because using my approach, the requirements evolve with the code. Call it spiral development, if you like.

I start with code
The method I use to develop software may surprise you. In the worst possible tradition of the anti-waterfall plan, I jump right into code. I sit down at the keyboard, flex my fingers, crack my knuckles, and type:

void main(void){}   

That's right: I write the null program. I do this on every new start. If I'm in a really adventurous mood, I'll add:

cout << "Hello, Jack!" << endl;   

and maybe even:

x = 2; y = 3;cout << x + y << endl;   

This is not a joke. I really, really do this. Every time.

Why? I think it's just my way of assuring myself that the genie behind the glass screen is still awake, and still doing his job. And my computer and its operating system haven't gone berserk overnight (not all that unlikely, these days),

Mostly, I do it to get myself in the mode of expecting success, not failure.

Several years ago, educators came up with the notion of “programmed learning.” The idea was to write the textbook very much like a computer program, complete with loops and goto's. After teaching a few facts (never more than three or so), the text asks a series of questions. If you give the right answers, you get to skip to the next section. Otherwise, you may be directed to loop back to the beginning of the section, and read it again. Programmatically, this is the familiar:

while(1){...}   

infinite loop structure. If you're particularly dense, you could be stuck here forever.

Alternatively (and better), you might be directed to a separate section, which explains the facts in more detail. It's a sort of hypertext. Ideally, the writers of the text were smart enough to tell when a given student needed extra instruction.

The inventors of programmed learning were careful to point out that those writing such textbooks should give the information in very small steps. You don't write a whole chapter on, say, the quadratic formula, and save all the questions for the end of the chapter. You teach a very few simple concepts–two or three at most–and ask the questions immediately.

The reason, they say, is that people need lots of positive feedback. If they get too many answers wrong, they get discouraged and give up. By giving them easy questions often, you give them lots of warm fuzzies, and make the study seem a lot more fun than drudgery. Everybody knows that people like to succeed more than to fail, and they like the feeling that they can actually complete the task. The notion of programmed learning capitalizes on these natural tendencies.

I think that's why I start with the null program. I like my warm fuzzies early and often.

When I'm writing code, I want to expect success. I'm mildly surprised if a bit of code doesn't compile without error, the first time. I'm shocked to my core if it compiles, but doesn't run properly, or doesn't give the right answer.

Some programmers, I submit, have never experienced that feeling. Certainly not those poor souls that had to write and integrate the entire system before even trying to compile it. How certain of success would they be?

The biological analogy
Think of a human egg cell. Once fertilized, it starts dividing, first into two cells, then four, then sixteen, and so on. The egg has become a zygote. Early on, every part looks like every other one. Later, they will differentiate, some cells becoming skin cells, some nerve cells, some heart and muscle cells. But in the early stages, they're completely undifferentiated. The organism may ultimately become a lizard, a fish, a bird, or a fern, but at this point, all the cells are alike.

I think of my null program as the software equivalent of a zygote. At the early stages, they can be any program at all. That's why I don't sweat the requirements too much. The null program and its cousins will satisfy any set of requirements, to some degree of fidelity. The requirements will flesh out–differentiate–along with the program that satisfies them.

As I continue the software development process, I stick to the philosophy of programmed learning. I never write more than a few lines of code, before testing again. I'm never more than mildly surprised if they don't work. I guess you could call this approach “programmed programming.”

Theory vs. practice
I have another reason to start coding early: I don't know exactly how the program should be structured. Like the potential fish or duck, many things are not clear yet. In the old, discredited waterfall approach, this was not a problem. We were supposed to assume that the guys who wrote the top-level requirements document got it perfect the first time. From then on, it was simply a matter of meeting those requirements.

To be fair, some visionaries of this approach had the sense to add feedback loops from each phase, back to the previous one. This was so that, in the completely improbable off chance that someone made a mistake further back upstream, there was a mechanism to fix it. Practically speaking, it was almost impossible to do that. Once a given phase had been completed, each change required a formal engineering change request (ECR) and engineering change notice (ECN). We avoided this like the plague. The guys in the fancy suits just kept telling the customer that the final product would meet the original spec. Only those of us in the back room knew the truth.

This is why I like the spiral development approach, also known as incremental, or prototype approaches. As the program evolves, you can still tell the customer, with a straight face, not to worry about all these early versions of the program. They are, after all, only prototypes. He doesn't need to know that one of those prototypes is also the final product.

Top down or bottom up?
The great racing driver, Stirling Moss, was once asked if he preferred a car with oversteer or understeer. He said, “It really doesn't matter all that much. In the end, it just depends on whether you prefer to go out through the fence headfirst, or tailfirst.” Ironically, Moss had a terrible crash that ended his career. He went out through a literal, wooden fence, headfirst.

Software gurus have similar differences of opinion on bottom-up vs. top-down development. Purists will claim that the only way to build a system is top down. By that, they mean design and build the outer layer (often the user interface) first. Put do-nothing stubs in place until they can be fleshed out. Even better, put in “not quite nothing” stubs that return the values you'd expect the final functions to return.

Other practitioners prefer a bottom-up approach, where you build the lowest-level code–sometimes the hardware interfaces–first. Connect them all together properly, and you've got a working program.

Paraphrasing Moss's comment, it all depends on how you prefer your project to fail. You can start with a beautifully perfect top-down design, only to discover in the end that your software is too slow or too big to be useful. Or you can start bottom-up, with a lot of neat little functions, only to discover that you can't figure out how to connect them together. In either case, you don't know until the very end that the system is not going to work.

That's why I prefer what I call the “outside in” approach. I start with both a stubbed-out main program and the low-level functions I know I'm going to need anyway. Quite often, these low-level functions are the ones that talk to the hardware. In a recent column, I talked about how we wrote little test programs to make sure we could interface with the I/O devices. It took only a day, and we walked away with the interface modules in hand. During the course of the rest of the project, it's a good feeling to know that you won't have some horrible surprise, near the end, talking to the hardware.

After all is said and done, there's a good and game-changing reason (I assert) that a pure top-down process won't work. It's because we're not that smart. I've worked on a few systems where I knew exactly what the program was supposed to do before I began. Usually, it was when the program was just like the previous four. But more than once, we've not been able to anticipate how the program might be used. We only realized what it was capable of after we'd been using it awhile. Then we could see how to extend it to solve even more complex problems.

In a top-down approach, there's no room for the “Aha!” moment. That moment when you think, “Say, here's an idea.” That's why I much prefer the spiral, iterative approach. You should never be afraid to toss one version and build a different one. After all, by then you've got the hang of it, and much of the lower-level modules will still be useful.

The top-down approach isn't saved by object-oriented design (OOD), either. One of the big advantages of OOD is supposed to be software reusability. In the OOD world, that means total reusability, meaning that you can drop an object from an existing program, right into a new one, with no changes. I submit that if you follow a top-down approach to OOD, you'll never be able to achieve such reusability. To take a ridiculous example, I might need, in a certain program, a vector class that lets me add and subtract them easily. But for this program, I don't need to compute a cross product. So unless I look ahead and anticipate future uses for the class, it'll never have the cross product function.

KISS
I've been using modular designs and information hiding since I first learned how to program. It's not because I was so much smarter than my coworkers; it's just the opposite: I'm humble (and realistic) enough to know that I need to keep it short and simple (KISS). It always amazes me how little time it takes for me to forget what I wrote a few weeks ago, let alone a few years. The only way I can keep control over the complexity inherent in any software effort, is to keep the pieces small enough and simple enough so I can remember how they work, simply by reading the code.

I was also lucky in that the fellow who taught me to program never bothered to show me how to write anything but modules. He had me writing Fortran functions that took passed parameters, and returned a single (possibly array) result. By the time I found out that other people did things differently, it was too late. I was hooked on small modules.

The term “modularity” means different things to different people. At one conference, a Navy Admiral gave a paper on the Navy's approach to software development. During the question and answer period, I heard this exchange:

Q: In the Navy programs, do you break your software up into modules?

A: Yes, absolutely, In fact, the U.S. Navy has been in the forefront of modular design and programming.

Q: How large is a typical module”

A: 100,000 lines of code.

When I say “small,” I mean something smaller than that. A lot smaller. Like, perhaps, one line of executable code.

In my old Fortran programs, I was forever having to convert angles from degrees to radians, and back. It took me 40 years to figure out: Dang! I can let the computer do that. I wrote:

double radians(double x){   return pi*x/180;}double degrees(double x){   return 180*x/pi;}   

Gee, I wish I'd thought of that sooner! (For the record, some folks prefer different names, that make the function even more explicit. Like RadiansToDegrees , or Rad2Deg , or even RTD ). One colleague likes to write ConvertAngleFromRadiansToDegrees . But I think that's crazy. He's a weird person.

Back in my Z80 assembly-language days, I had a whole series of lexical-scan functions, like isalpha , isnum , isalnum , iscomma , etc. The last function was:

iscomma:   cpi ','           ret   

A whole, useful function in three bytes. When I say “small,” I mean small .

Is this wise? Well, it's sure worked for me. I mentioned in a recent column (It's Too Inefficient) that I've been berated, in the past, for nesting my functions too deeply. One colleague pointed out that “Every time you call a subroutine, you're wasting 180 microseconds.”

Well, at 180 microseconds per call (on an IBM 360), he might have had a point. The 360 had no structure called a stack, so the compiler had to generate code to implement a function call in software.

But certainly, microprocessors don't have that problem. We do have a stack. And we have built-in call and return instructions. The call instruction says, simply, push the address of the next instruction, and jump. return means, pop and jump. On a modern CPU, both instructions are ridiculously fast. Not microseconds, but nanoseconds. If you don't want to use callable functions, fine, but you can't use run time speed as an excuse.

Test, test, test
Now we get to what I consider to be the foundation of my programming style. I test. I test everything, A lot.

The reason may sound odd: I test because I hate testing. Most programmers do, but in my case it's personal. I hate testing because I hate the notion that I wasn't perfect to begin with. So I hate testing. But if I hate testing, I hate testing later even worse. I test every line I write, right after I write it, so I won't have to come back in shame, later, to put it right.

In the best tradition of programmed programming, I don't want to wait until days or weeks later, to find out something's wrong. I sure don't want to wait until the entire program is complete, like as those poor saps on the Unix system had to do. I want to keep that instant gratification going. I want to preserve the expectation of success.

That's why I test as I write. Add a few lines of code (three or four), then test.

A few consequences fall out of this approach. First, the term “code and unit test” implies that you've got a test driver for every unit. And indeed I do. No matter how small the unit under test (UUT), it gets its own test driver. And yes, I absolutely did test iscomma (hey, it didn't take too long. There are only two outcomes, after all).

I'm also a big believer in single-stepping through my code. My pal Jim Adams disagrees with me on this. He says that if you're going to use single-stepping at all, you should only do it once. After all, he argues, the instruction is going to do the same thing each time.

Maybe so, but I do it anyway. Consider it part of the programmed programming paradigm. I never turn down an opportunity to get warm fuzzies. Single-stepping reminds me in no uncertain terms that my processes–mental and computer-wise–are still working.

Finally, there's an implicit assumption that you have a fast IDE that can give you test results in a hurry. I've been spoiled on this point ever since I fell in love with Turbo Pascal. The idea that I could get a compile as fast as I could get my finger off the 'R' button, blew me away. And spoiled me for life.

This wasn't a problem for the Unix team. The reason the managers imposed that ridiculous “no compiling” rule was, the compiler was slow, taking some four hours per build. The team should have–and could have–figured out a way to compile and unit-test the individual modules, but they didn't. For one reason, their code was not modular; everything depended on everything else. So you couldn't just lift out one function for unit testing, you had to do the whole megillah. The programmers didn't mind though. They could launch a build, then spend the next four hours playing Unix computer games.

I once worked on an assembly-language program, an embedded program. After I'd given the team leader some software, I told him I needed to make a change to it. He said, “Well, you can't do that now. You'll have to wait for the next time we do a build.” I asked when that would be. He said, “Two or three weeks.”

I go, “Three weeks ? Are you kidding me? I'm used to getting new builds in three seconds !” Then and there, I resolved to do my unit testing on my own test drivers and save integration until the very last thing. The idea worked like a charm. During integration, we found only one error, requiring a change in one single byte.

Expect success, I say. Get warm fuzzies often. And don't forget to test.

Jack Crenshaw is a systems engineer and the author of Math Toolkit for Real-Time Programming. He holds a PhD in physics from Auburn University. E-mail him at jcrens@earthlink.net. For more information about Jack click here

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.