Debugging Embedded Systems



October 01, 1993
URL:http://www.drdobbs.com/debugging-embedded-systems/184402783

October 1993/Debugging Embedded Systems/Figure 1

Figure 1 Debugging ROM in RAM

October 1993/Debugging Embedded Systems/Figure 2

Figure 2 Simulated target hardware debugging

October 1993/Debugging Embedded Systems/Listing 1

Listing 1 RAMROM.C

/*
 * fakeRom : This program supports
 *           debugging of ROM
 *           images in RAM.
 */

/* System includes */

#include <stdio.h>
#include <alloc.h>
#include <string.h>
#include <mem.h>

/* Useful defines and type defs */

define FAKE_ROM_SIZE 65536L

/* Other useful types */
typedef unsigned long ULONG;
typedef unsigned short USHORT;

/* Pointer to void far function */
typedef void (far *FUNCPTR) ();

/* -- procedures -- */

/*
 * getAddrPart : Return the segment
 *               or offset of a
 *               pointer
 */

USHORT getAddrPart
  (void *tptr, char part) {

  /* Overlay for pointer */
  USHORT components[2];

  /*
   * Overlay far pointer on
   * two 16-bit words.
   */

  memcpy ( (void *)components,
          (void *)&tptr,
          (long) 4 );

  /*
   * Return segment or offset
   * as requested by user.
   */

  if ( part == 's' )
    return components[1];
  else
    return components[0];

}

/*
 * physAddr : Make a physical
 *            address from
 *            segment, offset
 */

ULONG physAddr (void *tptr) {

  ULONG addr;  /* return value */

  /* Get segment */
  addr = getAddrPart(tptr,'s');
  addr = addr << 4;

  /* Get offset */
  addr = addr|getAddrPart(tptr,'o');

  /* Return whole thing */
  return addr;

}

/*
 * loadImage : load a file
 *             into a physical
 *             address.
 */

long loadImage ( FILE *fptr,
              char buffer[],
              ULONG size)
 {

   /* Local variables */

  long leftToRead;
  int thisRead;
  long actual;
  int block;

  /* Initialize */

   actual = 0;
   block = 0;
   leftToRead = size;

   /* Assume 512 blocks */

   while( 1 ) {
     /* Calculate amount to read */
     if ( leftToRead > 512L )
       thisRead = 512L;
     else
       thisRead = leftToRead;

     /* Perform the read */
     block=fread
        (&buffer[actual],
        1,
       thisRead,
        fptr);

     /* End-of-file test */
     if ( block == 0 )
       return (actual);

     /* Update counters */
     actual += (long)block;
     leftToRead -= (long)thisRead;
  }

} /* END loadImage */

/*
 * main : main routine of ramrom.c
 */

void main () {

/* Local variables */

ULONG phys;
FILE *ifl;
char *fake_rom_p;
static char imageName[128];
long int actSize;
static char answer[4];
ULONG huge *tptr;
ULONG i;

/*
 * Allocate enough memory
 * for the ROM image
 */

fake_rom_p=
  farmalloc(FAKE_ROM_SIZE);
phys = physAddr ( fake_rom_p );

/*
 * Force all the locations that
 * will not be filled during load
 * to have OxFF in them. This
 * mimics the actions of an
 * EPROM programming device.
 */

 tptr=(ULONG * huge) fake_rom_p;
 for(i=0;i<(FAKE_ROM_SIZE/4);i++){
   *tptr = 0xFFFFFFFF;
   tptr++;
 }

/* Show address to user and tell
  user what to do with it */

printf("\n\nFake rom address:");
printf(" 0x%08lx\n",phys);
printf("\nINSTRUCTIONS FOR ");
printf("COMPLETING EMULATION\n");
printf("1. Go to development ");
printf("system or window.\n");
printf("2. Perform the final ");
printf("absolute link of the \n");
printf("   code specifying ");
printf("0x%08lx\n",phys);
printf("   as absolute address,");
printf(" instead of\n");
printf("   usual EPROM address.\n");
printf("3. Move final image to");
printf(" where it can be \n");
printf("   loaded by this ");
printf(" debug program.\n");
printf("4. Enter image name.\n");
printf("5. Enter YES when you are");
printf(" ready to run.\n");

printf("\n=====================\n");

ifl = NULL;

/* Load EPROM IMAGE */

while (ifl==NULL) {
  printf("\nEnter image name: ");
  (void) gets(imageName);
  ifl=fopen(imageName,"rb");
  if (ifl==NULL) {
    perror(imageName);
    continue;
  }

  actSize=
    loadImage(ifl,
      fake_rom_p,
      FAKE_ROM_SIZE);
  if (  actSize < 1 ) {
    ifl = NULL;
    fclose(ifl);
    continue;
  }
  fclose(ifl);

} /* while ifl==NULL */

printf("%lu bytes loaded",actSize);
printf(" from %s\n", imageName);

/* Run EPROM IMAGE */

/*
 * Note: If you set a breakpoint
 * right here, you can examine
 * your loaded EPROM IMAGE in
 * memory.
 */

printf("Run it? YES/NO: ");
(void) gets( answer );
if (strcmp(answer,"YES")==0) {
  (void)(*((FUNCPTR)fake_rom_p)) ();
}

} /* END main() */

/* End of File */
October 1993/Debugging Embedded Systems/Listing 2

Listing 2 Memory map structures

/*
 * A structure is defined for each
 * area of memory on the target
 * board. The complete memory
 * map consists of an array of
 * these structures. Each element
 * is initialized to different
 * values, when running on MS-DOS
 * or running on the target board.
 */

struct MEM_AREA {
  DWORD boardOffset;
  DWORD baseAddress;
  DWORD areaSize;
  BYTE areaName[6];
};

/*
 * When running on the production
 * board, the variable 'boardBase'
 * is left to be zero, i.e. all
 * memory on the board starts at
 * location 0. When simulating on
 * a PC running MS-DOS, we do a
 * malloc() of a large chunk of
 * memory. 'boardBase' is set to
 * the address of this chunk. Then,
 * for each area of memory defined
 * in the following tables, we
 * add 'boardBase' to 'boardOffset'
 *  to get 'baseAddress'.
 */

 DWORD boardBase = 0;

#ifndef DOS_SIMULATION

 /*
  * Define the board's memory map
  * in the final production
  * version.
  */

 struct MEM_AREA memoryMap[4]=
  {
   {0x20000, 0, 64*1024, "RAM 1"},
   {0x30000, 0, 64*1024, "RAM 2"},
   {0x40000, 0, 64*1024, "RAM 3"},
   {0xE0000, 0,128*1024, "EPROM"},
  };

#else

 /*
  * Define the board's memory map
  * when simulating on DOS.
  */

 struct MEM_AREA memoryMap[4]=
  {
   {0x00000, 0,  8*1024, "RAM 1"},
   {0x02000, 0,  8*1024, "RAM 2"},
   {0x04000, 0,  8*1024, "RAM 3"},
   {0xF0000, 0, 64*1024, "EPROM"},
  };

#endif

/* End of File */

October 1993/Debugging Embedded Systems

Debugging Embedded Systems

Tom Welsh


Tom Welsh has been a programmer for 15 years, the last ten in real-time embedded systems. He is a graduate of George Mason University and holds a CCP. He is currently the president of Eagle Software Group, Inc., which develops customized mail sorting software for the United States Postal Service. He can be reached on CompuServe at 75322,3564.

Debugging an application program on a user-friendly computer system is relatively painless. By now, almost all development environments offer a symbolic debugger with a complete set of features for controlling the flow of a program and examining its data as it runs. You can describe meaningful control scenarios like

GO UNTIL (packets_remaining = 0)
 OR errorHandler();
and sit at your desk, keyboard on your lap, seeking out and destroying logic errors in your program.

Embedded debugging is quite a different matter. The first few steps are familiar to the application programmer. Compile the source code, and link the object modules into an executable. Then, however, things get strange. Convert the executable to a hex file and download it into an EPROM programming device. Write ("burn") the code into the EPROM chips. Take the EPROM chips into the lab. Power off the target chassis, remove the single board computer, remove the old EPROMs, plug in the new EPROMs, plug the board back into the bus, turn it on, and hope your program runs.

Embedded debugging is complicated because embedded programs run in environments where I/O capabilities are limited, or there is no operating system at all. Traditionally, debugging this kind of software has relied on oscilloscopes, firmware monitors, In-Circuit Emulators (ICEs — special hardware devices that replace the CPU on the target board and allow traditional high-level debugging of an embedded system), and various blinking lights. ICE units can be expensive and physically awkward. The ICE probe (a CPU chip with a ribbon cable attached) might not reach into the chassis far enough to plug into the CPU socket. And some ICE units are so specifically built for a particular CPU target that an ICE that emulates a 16 MHz 386 CPU can't emulate a 20 MHz 386, much less a 486 or a Pentium processor. Moreover, the resources required for embedded program debugging, including the target hardware, are often needed by several programmers at once.

Advances in recent years have made this kind of debugging easier. The Intel 80386 chip and its successors contains debug registers that allow a debugging monitor to perform some of the functions (e.g., setting break points in ROM) that used to require an ICE. Nevertheless, there are a variety of tricks available to the embedded systems programmer. This article describes some of those tricks: some are more obvious than others, but all of them should make your job easier.

Like other debugging strategies, these work best in the context of a fully featured software development environment. Given such a context, in almost all cases you will be able to correct the high-level flaws in logic, and with a little creative thinking, even some of the target-specific routines can be nailed down.

Work with the Best Tools Available

Some years ago, I was the lead programmer on a massive data-compression project. Our team had to reduce a 3.5 Gb database down to 100 Mb. The resulting compressed database would reside in 128 Mb of RAM in a piece of mail-sorting equipment at the United States Postal Service. The database was in RAM in order to enable high-speed real-time accesses by a single-board 386 computer in a Multibus II chassis. There was no operating system and the I/O capabilities were limited. The compression routines were very complex, as was required to achieve the high compression ratio. Hence, we knew the decompression routines would be equally complex. The compression software was written in FORTRAN and ran on a network of VAX minicomputers. The real-time database access software was written in PL/M-386, a C-like Intel proprietary language.

After a few days of freewheeling discussions we hit upon a solution. We designed our decompression routines; we then coded these in FORTRAN, taking care to avoid constructs that PL/M could not support, such as arrays of three or more dimensions. We then executed and debugged the decompression routines on a VAX, using symbolic debuggers, until the decompression routines were perfect. We tested by simply decompressing the compressed record and seeing if it produced the original uncompressed data. Then, we transliterated the decompression routines from FORTRAN to PL/M and began testing the embedded software. All the subsequently identified bugs were caused by minor flaws in the transliteration from FORTRAN to PL/M. No bugs were ever uncovered in the decompression logic once it was written in PL/M. Thus, we had killed two birds with one stone: we had tested the compression/decompression functionality and generated near-perfect embedded code on the first try. If we had been forced to rely on blinking LEDs to debug the decompression logic, the product schedule would have suffered.

Build Your Debugging Capabilities First

On the same compression project, the only way to communicate with the Intel 386 processor was to enter a command on a PDP-11 minicomputer, which would then send the command over a proprietary parallel interface to the 386. The 386 would respond with a message sent back over the same interface; the PDP-11 would print the response on a line printer.

The first things we built were poke and peek routines. These gave us the ability to examine and set any memory location or any I/O location. Further down the schedule, when we needed to track down some elusive problems, we already had the tools in place.

Make Your EPROM Only When You're Done

The temptation is always there to compile and link the code, make the EPROM, plug it in, and hope for the best. With a typical turnaround time of at least five minutes, however, this becomes hopelessly inefficient. Code and test as much of your software as possible on another system. Get rid of all the stupid logic mistakes and uninitialized variables. Only make the EPROM when there is absolutely no more testing that can be done without moving to firmware.

For example, I recently wrote the C routine used in United States Postal Service to derive the Delivery Point from the addressee's house number (for example, the Delivery Point for 6158 Main Street is 58). This routine is part of a large program that runs in an embedded environment where debugging is difficult. However, the routine itself only works with a single input, the house number. Since it has no hardware or environmental dependencies, I was able to test it on a VAX, where I had full symbolic debugging capabilities.

Test Your EPROM in RAM

Once you've built the final EPROM image, with all addresses absolutely resolved, there is still some testing you can do without making EPROMs. If you have a computer available that can execute the same instructions as the target processor, you can test your EPROM image. You can even test your EPROMs with absolute addresses.

This discussion assumes that you have a linker with the ability to locate segments at absolute locations. It also assumes that you have a development computer that executes the same instruction set as your target computer (e.g., 386 PC and Intel 386/120).

In a typical Intel-based environment, EPROM code resides at a fixed address, 0xF0000. When you use an absolute linker to build your final image, you specify this address. The trick to testing your EPROM in a friendlier environment is to build a test copy of your EPROM linked at an address that is "debuggable." Code linked at 0xF0000 cannot run anywhere but at 0xF0000, and that is reserved in PCs for the BIOS ROM. A debuggable address is one where you can actually load your code without interfering with the development computer's operating system or debugger.

The approach I use is to load a small program in the debugger that allocates enough memory to hold my EPROM image. I switch to another window or development computer and link my EPROM image at the physical address of the allocated buffer. I then load my EPROM image into that allocated buffer. Finally, I can "call" my loaded EPROM image as a routine. Figure 1 illustrates this process. Listing 1 contains the source code for ramrom.c, a small program for loading and running an EPROM image.

Design for Testing

The best method for debugging an embedded program is to design for testing. I am currently writing diagnostics code for a special 386-based network device. This code will eventually be placed into EPROM and used during the manufacturing process. There are two major hindrances to development of the code: 1) the usual delay of burning and re-burning EPROMs; and 2) only one prototype board exists, and it is used by several engineers.

Since the device was 386-based, I decided to do as much development and testing as possible on a desktop 486. The difference between the two CPUs was not important, since I had written the diagnostics code for any 80x86 processor. There were some assembly routines that took advantage of the 386's 32-bit registers. And since the C compiler I used only generated code up to and including the 386, no 486 instructions would be generated.

With some careful planning, I was able to test the following functions on my PC:

First, I defined a memory map for the target board. (See Listing 2 and Figure 2. ) I implemented the map in C as an array of structures where each element of the array defined a memory area to be tested. I then created two initializations of this area. One initialization was for the actual production board. The other initialization was for testing the diagnostics logic on an MS-DOS machine. Note that in both initializations I define a boardOffset and a baseAddress. The boardOffset indicates the starting address of that memory area relative to the start of the board's physical memory. It is initialized. The baseAddress represents the actual physical address of the first memory location on the board. It is initialized to zero, to be filled in later. A third global variable, boardBaseAddress, holds the actual starting address of the board.

When simulating on MS-DOS, I used the trick from my EPROM loading and allocated a buffer to simulate physical memory on the target board. I set boardBaseAddress to the physical address of the allocated buffer:

#ifndef DOS_SIMULATION

/*
* The lowest memory address on the board is 0.
*/

boardBaseAddress = 0;

#else

/*
* The board's memory is simulated
* by an allocated buffer and the
* lowest memory address is
* simulated by the starting
* address of the buffer
*/

boardBase Address = physAddr
                 (farmalloc
                 ((ULONG)
                 65535));

#endif
When the diagnostic code ran, it would add the boardBaseAddress of the allocated buffer to each to derive the baseAddress of the particular memory area. Thus, other than the malloc and setting of boardBaseAddress, the memory diagnostics code was identical.

To implement and test the EPROM checksum diagnostics, I included a description of the EPROM area in the board's memory map. When running on a PC, I initialized the EPROM area to the actual EPROM area of the PC. The actual production EPROM will be programmed such that the expected exclusive-or checksum is zero. For the PC emulation, I precalculated the PC's EPROM checksum, and conditionally compiled this into my test:

#ifndef DOS_SIMULATION

/* The expected checksum is 0. */

#define EXPECTED_CSUM 0

#else

/*
* The expected checksum is
* that of the PC's EPROM.
* PC_SUM is defined on the
* command line during compilation.
* Its value is precalculated
* by a standalone program.
*/

#define EXPECTED_CSUM PC_CSUM

#endif

Future Directions

At some point in the debugging of embedded systems, you are forced to go to the actual hardware. That point is defined by how much of the actual hardware you can simulate in software. I have already successfully emulated a small subset of the actions of the Intel 8259 Programmable Interrupt Controler. Thus, I was able to test the logic of my PIC diagnostics without actually requiring a PIC. However, on this same project I must also perform diagnostics on an Intel 82596 Ethernet Controller. This is a very complicated device and would require at least hundreds of lines of code to emulate properly. For this device, and those of similar complexity, a public library of routines to allow development and testing of drivers with a minimum of hardware involvement would be helpful. And, since many of these devices contain microcode, the routines have already been developed by the chip manufacturers. In some cases the logic may be proprietary; if so, the routines could be released as object code only and sold at the same cost as the chip.

Conclusion

In my experience, nothing is as rewarding as designing and writing a tight piece of code, burning that code into EPROM, installing the EPROM in the target hardware, and watching the target do what you programmed it to do. And nothing is a frustrating as watching it do nothing. The techniques I describe here were developed to try to minimize experiences of the second kind. I hope they will help make the development of embedded systems an inviting prospect to more programmers, as well as increase the productivity of those already well-experienced in the field.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.