C++ for Embedded Systems

Our authors describe how you can customize Borland C++ to support non-PC environments.


October 01, 1991
URL:http://www.drdobbs.com/cpp/c-for-embedded-systems/184408639

Figure 3


Copyright © 1991, Dr. Dobb's Journal

OCT91: C++ FOR EMBEDDED SYSTEMS

C++ FOR EMBEDDED SYSTEMS

Implementing embedded systems for the 80x86 takes a leap forward in affordability

Stuart G. Phillips, N6TT0 and Kevin J. Rowett, N6RCE

Stuart is director of network products development at Tandem Computers in Cupertino, Calif. Kevin is the project leader for TCP/IP development. You can reach them at Tandem Computers, MS 201-02, 10501 N. Tantau Ave., Cupertino, CA 95014 or by e-mail at [email protected] or [email protected].


Implementing and debugging embedded systems usually requires the designer to use a logic analyzer or in-circuit emulator, devices that can make embedded systems programming an expensive undertaking. With the software and techniques discussed here, implementing embedded systems for the Intel 80x86 family takes a quantum leap forward in affordability.

In this article, we describe how you can use Borland C++ (BC++) to develop applications for embedded systems using the PC. The article is based on our experience with using BC++ to develop software for an intelligent, multifunction communications processor (MIO) that uses the NEC V40 (a functional superset of the Intel 80188). We targeted MIO at the support of high-speed digital radio links to isolate the PC from real-time processing requirements. We developed software for MIO on the PC, then downloaded it into the controller for execution and testing. Because of space constraints, the complete software package for the MIO is available electronically; see "Availability" on page 3 for details.

Here, we explain how to customize BC++ to support a non-PC environment and how to convert DOS executable (EXE) files built using BC++ for an embedded system. We'll also describe how to interface Borland's Turbo Debugger (TD) to the system under test to speed up the debugging process.

The Startup Module

Most C (or C++) programmers think that the main( ) procedure is the first piece of code in a program to be executed. BC++, however, invokes main( ) from a startup module linked with the program. The startup module establishes the environment for main( ), setting up the stack and heap, and creating the argc and argv arguments from the command line. Finally, it calls main( ). The startup module regains control and tidies up before returning to DOS when the main program terminates by returning or calling exit( ).

Borland supplies several different versions of the startup module, depending on the choice of memory module and the use of floating-point operations. When using the Integrated Development Environment (IDE), the choice of memory model (set by compiler options) determines the selection of startup module. You can find the source code for the startup module (C0.ASM) in the EXAMPLES\STARTUP directory created when you installed BC++.

In most cases, our embedded system won't be running DOS, so we have to modify the startup module to make it DOS-independent.

A brief description of MIO will help you understand our particular environment. We designed MIO as a communications processor for high-speed radio links (see the sidebar entitled "Amateur Packet Radio"). We used the NEC V40 because of its high degree of integration (DMA, interrupt controller, timers, and so on -- all on the CPU) and its object code compatibility with the Intel 80x86 family. We can map either 8- or 64-Kbytes of MIO memory into the PC memory map as a shared memory window accessible to both MIO and the PC. We use the shared memory window initially to download control programs into MIO and subsequently for passing commands and data between the PC and MIO. MIO hardware allows the PC and MIO to interrupt each other under program control to provide synchronization between the two processors.

The file BUILD-C0.BAT shows the commands needed to build the startup module for the various memory models. We support all memory models except the huge model. Literals control conditional assembly to generate all memory models from a single source file.

You must specify the startup module as the first module in the object file list given as a parameter to the linker (TLINK). The startup module specifies an entry point (STARTX) that the linker uses as the initial start address for the program. Our startup module for MIO has a jump instruction at the entry point to take execution to the start of the control program located above the shared memory window.

We use a simple two-stage loader to take a control program and load it into MIO. The majority of programs are larger than the size of the shared window, so the loader first loads a program into MIO that copies data to and from the shared window. The loader uses this small program to relocate blocks of code and data into MIO memory, and then causes the control program to be executed.

The startup module begins to execute the main( ) procedure by setting the data segment register to point to the static data area known as DGROUP. Depending on the choice of memory model, DGROUP may contain all data and the stack or only initialized and static data.

Now that we can access DGROUP, we must determine where to locate the stack. We determine the size of the stack from the variable _stklen. Normally you wouldn't declare this variable because Borland includes it in the standard C library. We declare _stklen in the startup module because we make limited use of the standard library. C programs can set the value of _stklen by referencing it as an extern variable.

Regardless of the choice of memory model, we must check whether there is enough memory available for the requested stack size. The startup module contains a special symbol called edata@, located at the end of the static and initialized data in DGROUP. We use this symbol to determine the last location in memory occupied by the control program. We calculate the amount of free memory by converting the address of edata@ into paragraphs and then subtracting that value from the size of memory on MIO.

The startup module applies several consistency checks on the size of the stack and amount of free memory, depending on the choice of memory model. The choice of memory model determines the location of the stack. (See the Borland C++ Programmer's Guide for a full explanation.) After setting the size and location of the stack, the startup module calls all the initialization procedures that the programmer specified through the #pragma startup compiler directive. The startup module also uses the same code to call the termination procedures specified using the #pragma exit directive when the main( ) procedure exits or returns. Borland uses self-modifying code to save memory by sharing the code that calls the startup and exit procedures.

The startup module prepares dummy arguments for argc and argv and then calls main( ). We pass all configuration and optional data to MIO through the shared memory window.

More Details.

You will need to modify the startup module to reflect the hardware environment of your embedded system. The boot PROM on MIO takes care of initial hardware configuration such as disabling interrupts, starting memory refresh, and so on, eliminating the need to duplicate that code in our startup module.

Using the Standard Library

You need to exercise caution when considering the use of procedures in the standard library. Borland designed the library to support a DOS environment on the PC; therefore, it makes extensive use of BIOS and DOS function calls. Using procedures that call the BIOS or DOS will likely cause your program to enter hyperspace when executed on the embedded system.

You have three options to choose from when you need a library procedure:

Write a small test program that calls the procedure of interest; Borland includes example programs in the library reference manual for each procedure. Compile and link the example program specifying the options to include symbolic debugging information. When you have a linked program, invoke the debugger on the program and open a CPU window to display the CPU registers and the code in instruction format. Position the display to the start of the library procedure using the goto command. Page through the instruction decodes looking for INT instructions; both the BIOS and DOS functions use INT instructions to transfer execution from your program. The use of either BIOS or DOS function calls means you will have to write your own version.

Converting DOS Executable Files

Unless your embedded system runs DOS, you won't be able to use the EXE file generated by the linker without conversion. The linker produces relocateable object code when it creates the EXE file. Normally the DOS program loader takes care of "fixing up" the relocateable code based on the initial load address of the program. Figure 1 shows the layout of the EXE file header written by the linker. The linker creates a relocation item for every 16-bit word in the program that requires relocation. Each relocation item contains the segment and offset (relative to the base address of the program) of the word needing relocation. The loader reads the word at the specified location and adds to it the base address at which it loaded the program. The loader writes back the relocated word to its original location.

Figure 1: Format of EXE file header

  struct EXEHDR {
      unsigned short magic;       // EXE file if 0x5a4d
      unsigned short nbytes;      // Size of last page in bytes
      unsigned short npages;      // Size of image in pages
                                  // 1 page = 512 bytes
      unsigned short nreloc;      // Number of relocation items
      unsigned short hdrsize;     // Size of EXE header
      unsigned short endmin;      // Minimum memory
      unsigned short maxmem;      // Maximum memory
      unsigned short ss_offset;   // Stack segment offset
      unsigned short val_sp;      // Initial value for SP
      unsigned short chksum;
      unsigned short val_ip;      // Initial value for IP
      unsigned short cs_offset;   // Code segment offset
      unsigned short rel_offset;  // Offset of relocation items
      unsigned short ovl_num;     // Overlay number
  };

We build control programs for MIO just as we would build programs for DOS; instead of the normal startup module, we use the modified version described earlier. Before we can use the resulting EXE file on MIO, we must relocate the object code based on its eventual load address in MIO memory. Rather than perform the relocation as we download the control program into MIO, we opted to use a separate utility called COMF. The source code for COMF is included in the electronic distribution; see "Availability" on page 3 for details.

COMF takes the EXE file created by the linker and creates a relocated version of the file as a MIO download file. We use the file extension LOD to identify MIO download files. Each LOD file has a file header that includes a version number (so the loader can determine whether it can process the file). It also includes the entry point for the program, the size of the program, and the time stamp of the original EXE file. Figure 2 shows the LOD header format.

Figure 2: Format of LOD file header

  struct LODHDR {
      unsigned short magic;       // LOD file of 0x4655
      unsigned short version;     // Version id of LOD header
      unsigned short val_offset;  // Initial value for IP
      unsigned short val_seg;     // initial value for CS
      long         timestamp;     // DOS time stamp of orig. EXE file
      long        image_size;     // Image size in bytes
  };

The logic for COMF is fairly straightforward and needs little explanation. After validating the EXE file header, COMF builds the LOD header and then begins the relocation process. The linker does not sort the relocation items when it writes the EXE header; relocation items appear in random address order. COMF reads in all relocation items and sorts them into ascending address order before beginning the relocation process. By sorting the relocation items we only need to make one pass through the EXE file as we create the LOD file. We paid little attention to the efficiency of COMF because the conversion process doesn't take long, even for large programs with many relocation items.

The only optional parameter to COMF is the initial load address. By convention, we load MIO control programs at address 0010:0000; COMF assumes this address by default.

Loading a LOD file is simply a matter of copying the object code to MIO memory and beginning execution. The loader will optionally implant the time stamp from the LOD header at address 0000:00FC if we intend to use Turbo Debugger to debug software on MIO.

Debugging Using Turbo Debugger

The biggest challenge we face with an embedded system is debugging our download programs. Without keyboard or display, getting a view of what's going on within the embedded system requires a logic analyzer, in-circuit emulator, or some other technique. With the aid of MIOTDREM, a program described in Listing One (page 124) that runs on MIO and emulates TDREMOTE, we can use Turbo Debugger (TD) to debug our download programs.

In normal DOS environments, Borland supplies a utility called TDREMOTE and then enables TD to control a remote PC over an asynchronous communication link. TD passes TDREMOTE commands for execution and return of results. In this way we can use TD to debug our download programs.

MIO supports four communications ports using two Zilog 82530 Enhanced Serial Communication Controllers, or ESCCs. Zilog developed the 82530 as a functional superset of the 8530; the ESCC has larger FIFOs than the 8530 and a number of features that make it much easier to program.

The module COMM.C contains all the ESCC-dependent software; you can support your own embedded system by writing a version of COMM.C that supports your hardware. You will also have to change MAIN.C to initialize your hardware rather than that of MIO. The remaining modules of MIOTDREM are hardware-independent, apart from their 80x86 idiosyncrasies.

TD uses a simple protocol between itself and its remote partner over an asynchronous link configured for 8-bit characters with no parity and 1 stop bit. You must use a cable that crosses transmit and receive data between the PC and embedded system. The TD manual describes the cable configuration in some detail.

Each exchange of a message between TD and MIOTDREM begins with the sending of a single byte that contains the length of data to follow. The receiver of the length byte acknowledges receipt by sending back a NUL advising that the transfer can continue. The initiator then sends the message. The protocol provides no facilities such as a checksum or negative acknowledgment for error detection or recovery. The protocol makes the assumption that the link between TD and its remote partner is of high quality and not subject to errors. You shouldn't have any problems if you keep the cable between the PC and embedded system short (three to five feet of high-quality cable). Figure 3 shows the protocol exchange graphically.

The procedure tdr_processor( ) in TDRPROC.C processes messages that arrive from TD. tdr_processor( ) loops, waiting for the receive interrupt routine to append a message to the message queue msgq. The receive interrupt handler handles the handshake between TD and MIOTDREM, and then simply queues the message. The structure td_imsg describes the incoming message formats. Each incoming message starts with a byte command value followed by the parameters to the command. Commands allow TD to read and write memory or IO registers, set or move blocks of memory and read or write the processor registers. tdr_processor( ) calls support procedures, depending on the command sent for execution.

The module MACHINE.C contains all the machine-level procedures that handle the transfer of control between MIOTDREM and the program being debugged. The module includes interrupt service routines for divide by 0, single-step (trace), and breakpoint interrupts. When TD sends the command to start executing the program under test, MIOTDREM calls the procedure go_program() to start execution. go_program( ) saves the current processor state on a private stack and then sets the processor registers from the local register copy. Control passes back to MIOTDREM whenever one of the three interrupts occurs or you interrupt the program by pressing Ctrl-BREAK on the PC executing TD. The procedures mc_brk0( ), mc_brk1( ), and mc_brk3( ) (the three interrupt service routines) all call mc_return( ) to pass control back to MIOTDREM. mc_return( ) saves the state of the program under test and then manipulates the stack to simulate a return from go_program( ) with a return value indicating the cause for return. MIOTDREM passes the return value back to TD across the asynchronous link.

The code in MACHINE.C isn't difficult to understand if you use a piece of paper to follow the stack manipulations. The Borland C++ Programmer's Guide explains the entry and exit conventions used by the compiler; you will need to understand these conventions to follow how the return from go_program( ) is faked.

Debugging with TD is straightforward: You invoke TD in the normal manner and specify the name of the EXE file used as the input to COMF. After the initial handshake, TD sends the name of the file to MIOTDREM with the command to return the DOS time stamp for the EXE file. TD uses this mechanism to ensure that the remote has the correct version of the program. In normal DOS operation, TD will transmit the file to its remote partner if the file time stamps don't match. MIO doesn't have a disk, and we have already loaded the program ready for debugging, so MIOTDREM uses the time stamp value implanted at address 0000:00FC by the loader as the return value to TD. TD then sends a command to load the program, which MIOTDREM ignores, simply sending back an acknowledgment that the load was successful. Debugging then continues as normal.

You can locate code for MIOTDREM in EPROM on your embedded system or load it as a normal download program. We opted for the latter approach because we only need MIOTDREM when we are debugging new programs. Our version of MIOTDREM also provides support to the loader to copy code and data from the shared memory window to the rest of memory. We first load MIOTDREM, then load the program that we want to debug.

Conclusion

BC++ makes an excellent toolkit for developing embedded systems software. The Borland tools are high quality and easily adapted to develop code for a non-PC environment. Using TD to debug software on an embedded system makes for faster (less frustrating) development. We hope you find these techniques useful.

In a future article, we'll describe the ins and outs of programming the various versions of the Zilog ESCC.

Amateur Packet Radio

The development of radio technology owes much to Amateur Radio. Originally assigned the "useless" frequencies below 200 meters, Amateur Radio operators (more often called Radio Hams) pioneered the development of much of today's radio technology. Today the US has almost 500,000 Hams licensed by the Federal Communications Commission. Hams come from all walks of life and may be found both having fun at conventions and "hamfests," and providing essential communications in disaster relief operations.

Increasingly, Hams have come to rely on digital communications to carry messages important to public safety, health, and welfare. During the Bay Area earthquake in 1989, Ham Packet Radio carried much of the health and welfare traffic, allowing literally thousands of people to establish the safety of their loved ones.

Packet radio is the "new frontier" for today's Hams; it represents a unique marriage between computers and radio technology, and provides yet another area where Hams can make a difference. A complex network of packet radio stations, over which Hams regularly exchange messages, spans the US and much of the world. However, much of this network is built of links operating at slow speeds of around 1200 bps. Much opportunity exists to further the state of the art and provide higher speed links for the Amateur Service.

Hams have demonstrated experimental links at speeds above 2 Mbps. Work is ongoing to raise the link speed and provide high-speed digital communications at economical prices.

Recently, the FCC (at the urging of the American Radio Relay League, the official body of US Hams) approved a new class of Amateur Radio license which does not require knowledge of Morse code. The new class allows operation on VHF frequencies and opens the way for many technically competent people to join Amateur Radio without the obstacle of learning Morse. Hams with the new license can participate in the marriage of high-speed computer communications and radio technology, providing a new dimension to public service operations.

For information on how to become a Ham Radio Operator and participate in developing Amateur Packet Radio, contact the American Radio Relay League (ARRL) at 225 Main Street, Newington, CT 06111 and mention Dr. Dobb's Journal.

-- S.G.P. and K.J.R.


_C++ FOR EMBEDDED SYSTEMS_
by Stuart G. Phillips and Kevin J. Rowett


[LISTING ONE]


// MIOTDREM
// --------
// Copyright (c) 1991, Stuart G. Phillips.  All rights reserved.
// Permission is granted for non-commercial use of this software.
// You are expressly prohibited from selling this software in any form,
// distributing it with another product, or removing this notice.
// THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
// WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR
// PURPOSE.
// This module contains the main() for the MIO version of TDREMOTE.  The
// module is primarily responsible for initialization; TDREMOTE functions
// are handled in other modules.

#include "mio.h"
#include "8530.h"
#include "miotdr.h"

#ifdef SERIAL_DEBUG

// Communication region used to deposit status information about the
// programs progress etc in the shared memory window.

struct comm_region {    unsigned short status;
                        unsigned short scc_status;
                        unsigned short scc_special;
                        unsigned short int_cnt;
                        unsigned short rx_cnt;
                        unsigned short tx_cnt;
                        unsigned char  command;
                   };
#endif

struct relo_creg {      unsigned short offset;
                        unsigned short segment;
                        unsigned short count;
                        unsigned char command;
                 };

// Command values for RELO.LOD
#define RELO_NULL       0x00
#define RELO_COPYTO     0x01
#define RELO_COPYFROM   0x02
#define RELO_EXIT       0x03

// Magic number signifying presence of "RELO" support
#define RELO_MAGIC      0x5a41

extern void tdr_processor();
static void relo_lod();

#ifdef SERIAL_DEBUG
static struct comm_region *creg = (struct comm_region *)0x80;
#endif

// Globals

unsigned char rx_buffer[BUFLEN],
              tx_buffer[BUFLEN];

unsigned _stklen = 512U;

void main()
{
    unsigned char data,val;
    unsigned short i;

    disable();

    // Enable ICU in the V40 peripheral select register
    data = inportb(OPSEL);
    outportb(OPSEL,data|ICU);
    data = inportb(OPSEL);

#ifdef SERIAL_DEBUG
    creg->status = data;
#endif

    /* Initialize the ICU */

    outportb(IULA,ICUBASE);                     // set base address */
    outportb(IMDW,IIW1|IIW4NR|IEM|IET);         // no IIW4, extended mode,
                                                // edge triggered
                                                //
    outportb(IMKW,IVEC);                        // Set vector base for PIC/
    outportb(IMKW,SI7);                         // IRQ7 is slave

    outportb(IMKW,0xff);                        // Mask off all interrupts
    relo_lod();                                 // Provide relocation support
                                                // to MIOLOAD
#ifdef SERIAL_DEBUG
    creg->status = 0;
    creg->scc_status = 0;
    creg->scc_special = 0;
    creg->int_cnt = 0;
    creg->rx_cnt = 0;
    creg->tx_cnt = 0;
    creg->command = 0xff;
#endif

    comm_init();

    tdr_processor();
    /*NOTREACHED*/
}

static void relo_lod()
{
    struct relo_creg *creg = (struct relo_creg *) 0x80;
    unsigned short *magic = (unsigned short *) 0x88;
    unsigned char *go = (unsigned char *) 0x9f;
    unsigned char *swin, *p;
    unsigned short i, relo_done = 0;

    // Initialize command field in communication region
    creg->command = 0xff;

    // Implant magic number so that MIOLOAD knows we're up and running
    *magic = RELO_MAGIC;

    while (!relo_done){
        // Wait for command value to change from 0xff
        while (creg->command == 0xff) ;
        switch(creg->command){
            case RELO_COPYTO:
                p = (unsigned char *)(((long)creg->segment << 16) +
                                       (long)creg->offset);
                swin = (unsigned char *) 0x100;
                for(i = creg->count; i != 0; i--)
                    *p++ = *swin++;
                creg->command = 0xff;
                break;
            case RELO_COPYFROM:
                p = (unsigned char *)(((long)creg->segment << 16) +
                                       (long)creg->offset);
                swin = (unsigned char *) 0x100;
                for(i = creg->count; i != 0; i--)
                    *swin++ = *p++;
                creg->command = 0xff;
                break;
            case RELO_EXIT:
                creg->command = 0xff;
                relo_done = 1;
                break;
            default:
                creg->command = 0xff;
                break;
        }
    }
    // Clear our magic marker and then reset go flag to 0xf4 (halt) code.
    // Wait for MIOLOAD to set this to 0x90 to indicate we should continue.
    *magic = 0;
    *go = 0xf4;

    while (*go != 0x90) ;
}

Figure 1: Format of EXE file header

struct EXEHDR {
    unsigned short magic;       // EXE file if 0x5a4d
    unsigned short nbytes;      // Size of last page in bytes
    unsigned short npages;      // Size of image in pages
                        // 1 page = 512 bytes
    unsigned short nreloc;      // Number of relocation items
    unsigned short hdrsize;     // Size of EXE header
    unsigned short endmin;      // Minimum memory
    unsigned short hilo_flag;
    unsigned short ss_offset;   // Stack segment offset
    unsigned short val_sp;      // Initial value for SP
    unsigned short chksum;
    unsigned short val_ip;      // Initial value for IP
    unsigned short cs_offset;   // Code segment offset
    unsigned short rel_offset;  // Offset of relocation items
    unsigned short ovl_num;     // Overlay number
};

Figure 2: Format of LOD file header


struct LODHDR {
    unsigned short magic;       // LOD file if 0x4655
    unsigned short version;     // Version id of LOD header
    unsigned short val_offset;  // Initial value for IP
    unsigned short val_seg;     // initial value for CS
    long           timestamp;   // DOS time stamp of orig. EXE file
    long           image_size;  // Image size in bytes
};


Copyright © 1991, Dr. Dobb's Journal

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