The Zen of Diagnostics
This is the first of a two part series about adding diagnostics into your programs.
Published in Embedded Systems Programming, June 1990
By Jack Ganssle
For some inexplicable reason most of us embedded programmers rarely concern ourselves with making a product manufacturable. Sure, we work like slaves meeting performance goals, but the product must do more than function correctly - it must be designed to be producable. While our hardware brethren tune their designs to meet cost, manufacturing, and performance goals, we work in relative isolation; a sort of black hole where few dare to tread.
In high school I worked for a while in a machine shop, serving as the lowest sort of helper to highly skilled machinists making parts for the space program. When they discovered I intended to go to college to become an engineer, one grizzled veteran warned me to not "design something we can't make". Now, twenty years later, I have to admit that these words, so casually and freely given, have been more important than most of the EE courses I struggled through later. Designing the most fantastic widget ever conceived is suicidal if it cannot be made and marketed at a profit.
In a typical manufacturing operation, boards are stuffed, assembled into units, tested and perhaps repaired. While the code doesn't impact board stuffing and assembly operations, it can strongly influence product test and repair. Smart designers will produce a product that easily fits into the company's manufacturing operation; software engineers can contribute by writing code that speeds the daily grind of production test.
All employees are hopefully working towards common corporate goals, yet each has a different vision of the company's needs and problems. To a programmer the word "testing" conjures images of correctness proofs, exhaustive software trials, and code coverage analysis. A production person probably has never heard of any of these concepts; he looks at testing as the daily routine of ensuring each and every unit works correctly before being shipped.
Conventional software test is a one time event; once the product is complete it is over, forever (well, would you believe...). Product test goes on every day. Very complex products are tested and repaired by technicians with little formal computer training. The best are usually culled and assigned to work in engineering support, leaving production with workers who may be skilled but who are certainly not rocket scientists. As software engineers it is our responsibility to the company to give the techs the tools they need to ship product. As software managers, it is our responsibility to convince management that this is an important and desirable goal.
Internal Diagnostics
Quite a few embedded systems include diagnostics as part of the product's ROM to give a sort of "go/no-go" indication without using other test equipment. The unit's own display or status lamps show test results.
Internal diagnostics are worthwhile because they do give the test technician some ability to track down problems. They're also an effective marketing tool, giving the customer a (possibly false) feeling of confidence in the integrity of the product each time he turns it on.
Though internal diagnostics are often viewed as a universal solution to finding system problems, their value lies more in giving a crude test of limited system functions. Not that this isn't valuable. Internal diagnostics can test quite a bit of the unit's I/O and some of the "kernal", or the CPU, RAM, and ROM areas.
The computer's kernal frequently defies standalone testing, since so much of it must be functional for the diagnostics to run at all. Most systems couple at least the main ROM and RAM closely to the processor. The result - a single address, data, or control line short prevents the program from running at all.
It's easy to waste a lot of time coding internal diagnostics that will never provide useful information. They may satisfy vague marketing promises of self-testing capability, but why write dishonest code? Realize that internal diagnostics have intrinsic limitations, but if carefully designed can yield some valuable information. Apply your engineering expertise to the diagnostic problem; carefully analyze the tradeoffs and identify reasonable ways to find at least some of the common hardware failures. The first step is to separate the tests into kernal (CPU, RAM, ROM, and decoders) and beyond-kernal (I/O) tests. Then consider the most likely failure modes; try to design tests that will first survive the failure, and second will identify and report it. Yes, the kernal tests will never be very robust since lots of hardware glitches will prevent the program from running at all. But, if carefully designed, they'll really help your buddies in production.
I/O tests can run the gamut of a simple LED blinking routine to A/D and D/A loopbacks that check converter linearity. I/O is just too big of a subject to address in a short article. Today's range of peripherals is so mind boggling that several large books might not adequately cover the subject of testing even the most common devices. I won't attempt to delve into a discussion of I/O tests here.
What portions of the kernal should be tested? Some programmers have a tendency to test the CPU chip itself, running through a sequence of instructions "guaranteed" to prove that this essential chip is operating properly. Witness the ubiquitous PC's BIOS CPU tests. I wonder just how often failures are detected. On the PC an instruction test failure makes the code execute a HALT, causing the CPU to look just as dead as if the it never started. More extensive error reporting with a defective CPU is a fool's dream. Instruction tests stem from minicomputer days, where hundreds of discrete ICs were needed to implement the CPU; a single chip failure might only shut down a small portion of the processor. Today's highly integrated parts tend to either work or not; partial failures are pretty rare.
Similarly, memory tests rely on operating memory to run - a high tech oxymoron that makes one question their value. Obviously, if the ROM is not functioning, then the program won't start and the diagnostic will not be invoked. Or, if the diagnostic is coded with subroutine calls, a RAM (read "stack") failure will prematurely crash the test, before providing any useful information.
The moral is to design diagnostics so to ensure that each test uses as few unproven components as possible.
A carefully engineered RAM test can be quite valuable. In a multi-ROM system, it (and all the diagnostics) should be stored in the boot ROM, preferably near the reset address. The low order address lines must operate to run even a trivial program, and enough of the upper ones must work to select the boot ROM; particularly in a small system with undecoded address lines, try to write test routines that rely on a minimal number of working address wires.
RAM tests commonly write out a pattern of alternating ones and zeroes, read the data back, and repeat the test using data that is the complement of the first set. This amateurish approach reflects poor analysis of the problem. Before writing code, put on your hardware hat (or get some help from another member of the design team) and consider the most likely failure modes. Tailor the test to identify all or most of these problems. A typical list includes:
- Address/data line shorts - In nine times out of ten these problems will crash the diagnostic. Sometimes RAM is isolated from the kernal by buffers; in this case
- post-buffer problems can be found.
- No chip select - One or more signals may be needed to turn each device on. The chip select complexity can range from a simple undecoded address line to a nightmarish spaghetti of PALs and logic. Regardless, every RAM must receive a proper chip select to function.
- Pin not inserted - Socketed RAM devices may not be properly seated. Sometimes a pin bends under the device.
- Bad device - The semiconductor vendors do a wonderful job of delivering functioning chips. Rarely, though, a bad one may slip through (or, through mishandling, one may "toggle to the bad state"). Device geometry is now so small that it is unusual to see the pattern sensitivity that once plagued DRAMs. Usually the entire chip is just plain bad, making it a lot easier to identify problems.
- Multiple addressing - This is a variant of the chip select problem. If more than one memory device is used, several can sometimes be turned on at once.
- Refresh - Dynamic RAMs require a periodic accesses to keep the memory alive. This Refresh signal is generated by external logic, or by the CPU itself. Sometimes the refresh circuitry fails. The entire memory array must be refreshed every few milliseconds to stay within the chips' specifications, but it's surprising just how long DRAMs can remember their data after loosing refresh. One to two seconds seems to be the extreme limit.
With the above in mind, we can design a routine to test RAMs. The first criteria is that the test itself certainly cannot make use of RAM!
Several of the failure modes manifest themselves by the inability to store data. For example, data bus problems, a bad device, chip select failures, or an incorrectly inserted pin will usually exhibit a simple read/write error. The traditional write and read of a 55, followed by an AA, will find these problems quickly.
Writing a pattern of 55s and AAs tests the ability of the devices to hold data, but it doesn't ensure that the RAMs are being addressed correctly. Examples of failures that could pass this simple test are: a post-buffer address short, an open address line (say, from a pin not being inserted properly), or chip select failures causing multiple addressing. It's important to run a second routine that isolates these not-uncommon problems.
An addressing test works by writing a unique data value to each location, and then reading the memory to see that that value is still stored. An easy-to-compute pattern is the low order address of the location; at 100 store 00, at 101 store a 01, etc. This isn't really unique, since an 8 bit location can only store 256 different values. If we repeat the test, using the locations' high order address bytes as the data pattern, then (for memory sizes to 64k) after two passes the entire array will be tested uniquely. The first test insures that address lines A0 to A7 function correctly; the second checks lines A8 to A15 and also the chip select logic.
Since this test also insures that the RAM can store data; why do the 55, AA check? Consider any individual address. At location 0, the addressing diagnostic will write 0 (the low address) and later another 0 (the high address). While addressability will be confirmed, some doubt remains about its ability to store data. The 55, AA check tests every bit.
The peril of these diagnostics is that the address lines cycle throughout all of memory as the test proceeds. If the refresh circuit has failed, most likely the test itself will keep DRAMs alive. This is the worst possible situation; the process of testing camouflages failures. A simple solution is to add a long delay after writing a pattern and before doing the read-back. This delay should be on the order of several seconds. It is also important to constrain the test code to a small area, so the CPU's instruction fetches don't create artificial refreshes.
In the bad old days small DRAMs manufacturing defects and alpha particles caused some memories to exhibit pattern sensitivity problems; selected cells would hold not hold a particular byte if a nearby cell held another specific byte. Elaborate tests were devised to isolate these problems. The "Walking Ones" test, in particular, burned an enormous amount of computer time and could find really complex pattern failures. Fortunately these sorts of problems just don't show up anymore.
Figure 1 is a routine that performs all of these tests. It is cumbersomely coded in 8088 assembly language, using no RAM at all. Simpler, prettier code using CALLs and RETs just will not be dependable, since it would rely on the very RAM we're testing.
While it is easy to see some justification for testing the product's RAM, ROM tests are perhaps not as obviously valuable. If the ROM is not working, how can it test itself? Physician - heal thyself (but not if you're in a coma). As always, a completely dead kernal, one that just doesn't even boot, cannot run diagnostics. If the boot ROM does at least partially work, then some testing is valuable.
In the boot ROM itself we can realistically expect to detect only a simple failure, like a partially programmed device, although with some luck it might be possible to find a shorted or open high order address line. Luck, because if the line floats or is tied to the wrong level, then the diagnostic code will not start.
ROMs are tedious to program - sometimes technicians will unknowingly, in their impatience, remove the chip before the programmer is completely done. If you do elect to include a ROM test, be sure to locate it early in the code so it stands a chance of executing even if the ROM is not entirely programmed. It's easier to make an argument for ROM testing in multiple ROM systems. If the boot ROM starts, then diagnostics located in it can test all of the others.
While the memories can fail in a number of ways, probably the electronics, you'll know that it can be awfully hard to tell if all pins are in the sockets. Other problems cover the usual range of broken circuit board tracks (i.e., address, data, control lines), misprogrammed devices, and non-functioning chip select lines.
One way to test ROMs is to read the devices and compare each byte to a known value. Since such redundancy is impractical, most programmers simply compute an 8 or 16 bit sum of the data in the ROMs and compare it to a known value. Usually this is adequate, but a number of pathological cases will report incorrect results. For example, a long string of zeroes will always checksum to zero, regardless of the number of items summed.
A much better approach than a simple checksum is the Cyclic Redundancy Check (CRC). The CRC is a polynomial that is seeded, typically with FFFF, and then divided into the input data (in this case the ROM data) a byte at a time, using the dividend at each step as the new seed. While mathematically complicated, the CRC is pretty easy to implement. Its great virtue is that each byte of repeated strings (say, zeroes) will yield a different CRC. The CRC is a bit harder to code than a simple checksum, but the code listed in figure 2 is a cookbook solution. Once again, it is written in 8088 assembly language to ensure it uses the minimal number of as-yet-untested CPU resources.
A CRC or checksum test is easy to code and yields useful information, but is sometimes a nuisance to implement because the correct value must be computed and stored in ROM. No assembler or compiler has a built-in function to build this automatically, so generally you must run the program under an emulator, record the CRC or checksum the routine computes, and then manually patch the resulting value into an absolute location in ROM. The only way to automate this is to write a short program that CRCs the linker output file and patches the result into the ROM file.
In Conclusion
It is best to address the diagnostics problem using the engineering thought processes our "significant others" have learned to hate. Examine the system dispassionately and analytically, looking for all possible failure modes. Study the tradeoffs. Look for alternate approaches. Then, implement the best possible solution that does a reasonable job of solving the problem yet doesn't take too much programming time. No one said it would be easy.
Next month I'll look at another aspect of diagnostics - how do you report an error? We'll also look at external diagnostics that help out the technician when the system won't even boot.
Figure 1: ********************************************************** title Ram test code code segment public assume cs:code,ds:data ; ; This routine performs a RAM test on an 80x88 type machine. ; ; This test is sequenced through the following phases: ; ; 55 written to all addresses and read back ; AA written to all addresses and read back ; Low order address written and read ; High order address written and read ; ; Note that this code makes no use of RAM, so some of its ; structure may seem arbitrarily cumbersome. ; ; The test is not coded as a traditional subroutine with a ; "return" instruction. Your code must flow into it - it ; leaves at exit point ram_done. ; ; On entry, the address to test is contained in register ; DS:BX. The length of the test is in CX. It does not ; support overflows into the segment register; be sure ; that BX+CX is no more than FFFF. ; ; On exit, if the test fails register AX will be set non-zero, ; and DS:BX will contain the address of the first failed byte. ; AX will be set to 55 if the 55 test failed, AA if the AA test ; failed, 1 if the low address test failed, and 2 if the high ; test failed. Register CX will contain the number of contiguous ; bytes that are bad; note that only one contiguous block is ; tagged. ; ; If the RAM passes the test, then AX will be 0. ; ram_test: mov si,bx ; save start address in si mov bp,cx ; save length in bp mov ax,55h ram_55_load: mov [bx],al ; load memory with 55s first inc bx loop ram_55_load mov bx,si ; restore start of test address mov cx,bp ; restore length of test dec bx ram_55_check: inc bx cmp [bx],al ; see if we still have 55s loope ram_55_check ; loop as long we get 55s back test cx,cx ; if zero, test passed jz ram_aa_test ; br if test passed mov dx,0 ; dx will be # bad bytes found for now mov si,bx ; set first bad address ram_55_bad: ; failure found - find length of bad block inc si inc dx ; inc # bad bytes found dec cx ; dec total # to test jz ram_bad1 ; end of test cmp [si],al ; see if this location is also bad jnz ram_55_bad ram_bad1: jmp ram_bad ; end of the bad block found ; ; Run the AA test ; ram_aa_test: mov bx,si ; start address of test mov cx,bp ; length of test mov ax,0aah ram_aa_load: mov [bx],al ; load memory with AAs first inc bx loop ram_aa_load mov bx,si ; restore start of test address mov cx,bp ; restore length of test dec bx ram_aa_check: inc bx cmp [bx],al ; see if we still have AAs loope ram_aa_check ; loop as long we get AAs back test cx,cx ; if zero, test passed jz ram_low_test ; br if test passed mov dx,0 ; dx will be # bad bytes found for now mov si,bx ; set first bad address ram_aa_bad: ; failure found - find length of bad block inc si inc dx ; inc # bad bytes found dec cx ; dec total # to test jz ram_bad ; end of test cmp [si],al ; see if this location is also bad jnz ram_aa_bad jmp ram_bad ; end of the bad block found ; ; Run the low address test ; ram_low_test: mov bx,si ; start address of test mov cx,bp ; length of test ram_low_load: mov [bx],bl ; load memory with low address inc bx loop ram_low_load ; ; Add in a delay of about 2-3 seconds to let DRAMs "forget" if ; refresh is not active. ; mov bx,si ; restore start of test address mov cx,bp ; restore length of test dec bx ram_low_check: inc bx cmp [bx],bl ; see if we still have low address loope ram_low_check ; loop as long we get good data back test cx,cx ; if zero, test passed jz ram_hi_test ; br if test passed mov dx,0 ; dx will be # bad bytes found for now mov si,bx ; set first bad address ram_low_bad: ; failure found - find length of bad block inc si inc dx ; inc # bad bytes found dec cx ; dec total # to test jz ram_bad ; end of test mov ax,si ; get address we stored cmp [si],al ; see if this location is also bad jnz ram_low_bad mov ax,1 ; 1 indicates low address failure jmp ram_bad ; end of the bad block found ; ; Run the high address test ; ram_hi_test: mov bx,si ; start address of test mov cx,bp ; length of test mov ax,0 ; assume test will pass ram_hi_load: mov [bx],bh ; load memory with high address inc bx loop ram_hi_load ; ; Add in a delay of about 2-3 seconds to let DRAMs "forget" if ; refresh is not active. ; mov bx,si ; restore start of test address mov cx,bp ; restore length of test dec bx ram_hi_check: inc bx cmp [bx],bh ; see if we still have high address loope ram_hi_check ; loop as long we get good data back test cx,cx ; if zero, test passed jz ram_done ; br if test passed mov dx,0 ; dx will be # bad bytes found for now mov si,bx ; set first bad address ram_hi_bad: ; failure found - find length of bad block inc si inc dx ; inc # bad bytes found dec cx ; dec total # to test jz ram_bad ; end of test mov ax,si ; get address we stored cmp [si],ah ; see if this location is also bad jnz ram_hi_bad mov ax,2 ; 2 indicates high address failure jmp ram_bad ; end of the bad block found ram_bad: mov cx,dx ; cx=# bad bytes ; ax=test number ; bx=start of bad address ram_done: ; exit point code ends data segment public data ends end ******************************************************** Figure 2 follows: ******************************************************** title CRC program code segment public assume cs:code,ds:data ; ; this routine will compute a CRC of a block of ; data starting at the address in DS:BX with the ; length in CX. Don't try to exceed a 64k segment (keep ; BX+CX <= FFFF). ; ; The CRC will be computed into register DX and compared to ; a value saved in ROM. ; rom_test: mov dx,0ffffh ; initialize CRC to -1 rom_loop: mov al,[bx] ; get a character to CRC inc bx ; pt to next value xor al,dl ; compute crc mov ah,al ; save temp result shr al,1 ; shift right 4 shr al,1 shr al,1 shr al,1 xor al,ah ; xor temp with partial product mov ah,al ; new temp shl al,1 ; shift left 4 shl al,1 shl al,1 shl al,1 xor al,dh ; combine with high crc mov dl,al ; save low result mov al,ah shr al,1 ; shift right 3 shr al,1 shr al,1 xor al,dl mov dl,al mov al,ah ; get temp back shl al,1 ; shift left 5 shl al,1 shl al,1 shl al,1 shl al,1 xor al,ah mov dh,al ; high crc result dec cx ; dec data byte count jnz rom_loop ; loop till all CRCed cmp dx,word ptr cs:crc; crc match? jnz rom_error ; error if no match jmp rom_ok ; jmp if ok crc: dw 0 ; save crc here rom_error: ; error location - flag an error rom_ok: ; rom crc compare ok code ends data segment public data ends end