Pat Villani received his B.S.E.E. from Polytechnic Institute of Brooklyn and his M.S.E.E. from Polytechnic Institute of New York. Pat has developed applications ranging from avionics and guidance to computer peripherals. He is president of Network Software Systems, Ltd., which specializes in C applications and embedded control systems. Readers may reach him at (908) 206-0320.
Embedded systems have traditionally represented a specialized area of computer science and electrical engineering, an application where both disciplines merge. System design often requires tradeoffs between programming convenience and system cost. Quite often, code produced for embedded systems is created by both software and hardware engineers.
The first microprocessors used in embedded systems implemented a lot of code in assembly langauage because of the type of software development packages available for these processors. These packages were often expensive and required the manufacturer's development system or a large machine to build code. Only large corporations could afford these programming platforms. As microprocessor capabilities increased, manufacturers introduced programming langauges geared toward embedded systems. These new languages were limited to the same platforms, so the trend of limited embedded system development continued.
Concurrently, great strides were made in desktop computers. These computers, based on the same microprocessors that were used in embedded systems, were capable of running applications and small software development packages that targeted these machines. Further developments in desktop computers enabled the developers of these packages to include more advanced features. The result: powerful langauges such as the Borland Turbo languages and Microsoft languages that include debuggers and integrated development environments that allow rapid development of applications.
Today, low-cost clone motherboards, hybrids, and single chip computers based on XT and AT architectures exist. When combined with low-cost peripherals, these motherboards and devices present a unique opportunity to system developers. Low-cost and small-volume controllers, data loggers, and specialized processors are feasible. Adapting langauges such as Turbo C to produce code for these environments makes these designs even more affordable.
Basic Principles
Compilers such as Borland Turbo C generate efficient code that is independent of any operating system. What ties the compiler to an operating system or any environment is the system startup code and the libraries that are linked to produce the executable object file. Replacing these files with new files specific to embedded systems allows the code to work in an embedded system.
The first file you must replace is the startup file. Turbo-C uses one of the files COx. OBJ (where x is T, S, L, etc., depending on the memory model used) found in \TC\LIB. This file transforms the unique parameters of MS-DOS passed at startup into the conventional C environment argc, argv, envp, and errno. The file also sets up the stacks and aligns the segments of memory.
The next file that you must replace is the corresponding flavor of \TC\LIB\Cx.LIB. This file contains all the operating system calls, such as read and write. It also contains library functions such as strcpy and tolower. Our approach is to replace this file with a new library emulating these calls.
Design Advantages
Rapid development is a key factor in today's market. This approach helps the system developer achieve this goal by reducing development time. The project is developed using the Turbo C integrated environment or Turbo Debugger. When the developer is satisfied with the code's functionality, the code is recompiled and linked using make files and the command line environment. A commercial or public domain locater is then used to create an EPROM for the target system.
The project is quickly debugged in a convenient environment in which code changes are rapidly made and revision control is maintained. Using the Turbo Debugger, nearly all C code and assembly code can be debugged, registers examined, breakpoints set, etc., without the need for a costly in-circuit emulator. If a suitable ROM monitor is used during the final debug cycle, an in-circuit emulator may not be needed at all.
Since the code is transported from an MS-DOS environment to a target system, the functional code can be easily transported to a different 80x86-based target system by simply changing the device drivers and library I/O files. This built-in hardware independence aids the developer in writing reusable code, further reducing development time in future projects.
Example Project
The development of a simple 8086 ROM monitor illustrates this design approach (Listing 1) . The monitor, called mon86, performs two functions:
examine and change memory
go to specified address It uses a simple table-driven executive that can be extended by adding new table entries and suitable functions to execute these new commands. An error function terminates the table.
The monitor uses only Turbo C calls that comply with both SVID and POSIX definitions, improving portability. DOS-unique system calls, such as INT21, etc., are not used. Using them would complicate the code by requiring the use of conditional preprocessor directives to selectively include the non-portable sections of code. It also complicates library coding, since it requires that DOS code also be emulated.
For this project, the C library is broken into two libraries: a system call emulation and a portable C library. The example project uses simple I/O calls such as read and write. The functions open, close, and ioctl are also implemented for completeness. Restricting I/O function calls to this small set reduces the work required to implement the emulation package.
The monitor C library calls are limited to simple getc and putc. These calls are implemented as links to Turbo C calls _fputc and _fgetc, which allow for full use of Turbo C's <stdio.h>, further enhancing portability. Also, simple formatted output is required for memory display. To achieve this with an eye on portability, the library provides an integer-only printf. The choice of a limited printf has two justifications:
it is simpler to implement, because it does not require knowledge of compiler floating point representation
limiting its use to int and long guarantees that the floating-point library is not used, yielding smaller ROM code. A machine library, called libm. lib, is also needed. This library is a collection of all subroutines required to supplment C operations not supported by the processor itself, such as long divides, multiplies, etc. These subroutines are naturally operating system independent. It is best to extract them from the Turbo C library as needed during the ROM build portion of the project. A limited library is built at that time by linking the final MS-DOS code, noting the routines used in the code map, and extracting and creating a new library of just those routines.
Startup Assembly Code
Probably the most important piece of code for an embedded system is the system startup code (Listing 2) . This code will vary from compiler to compiler, but the same operations generally appear in some form. The first part of the module is usually initialization related to the assembler or compiler. For Turbo C, this is the specification of segments. It is here that the order of segments for the module is first specified. Turbo C requires three segments, MS-DOS requires one.
MS-DOS requires a stack segment. In a .EXE file, this is the only segment specified in the header that is separate from the rest. That is why the stack segment is used by some locate utilities as a place to separate RAM code from ROM code when generating hex files suitable for EPROM programmers.
Turbo C requires only three segments, _TEXT, _DATA, and _BSS. These closely follow the UNIX C model. The mon86 generated here is a small version, therefore, the CODE segment is text only, and the DATA segment is used for data, bss (data initialized to zeros), and stack. These segments are mapped into DGROUP.
All 80x86 processors begin executing code after reset at address 0xffff:0x0000. The usual procedure is to place a far jump at this location to the ROM entry address. For our example, the system entry point is defined as the far procedure entry. This label is used as the startup address supplied to the locater.
Next, the processor is initialized. Initializing the processor means intializing any segment registers that the processor needs for proper operation. Since the module presented here is for an 8086 or 8088 machine, only the segment registers must be initialized. Note that segment register initialization is performed with all interrupts disabled, which prevents spurious interrupts that may occur during power-up from crashing the system.
Once the processor is initialized, it usually requires the proper setting of the stack pointer. For this example, all you must do is initialize the sp and bp registers. Note that initializing the bp register is required by Turbo C code generation conventions, not by the 8086.
Special hardware initialization should be performed after the processor has been initialized. This includes initializing the interrupt vector table, moving the data segment to RAM, and initializing the _BSS segment to all zeros to match C programming conventions. The example in Listing 2 includes a hook for hardware initialization with a call to __hdw_init. (For clarity, I've omitted moving data and initializing vectors.)
After system initialization, you must enter main so the application can run. You enter main by building the stack frame containing the arguments envp, argv, and argc. You use these arguments to pass system configuration information, dip switch settings, etc. to your C code. These arguments also serve as a convenient debugging tool, since you can test different configuration switch positions by passing a command line argument during the debugging phase.
One critical piece of code that all systems require is the function exit. This code is very implementation sensitive. You may want to light LEDs and sound alarms. In certain cases, you may want only to display a message and restart. What to do when the application terminates must be decided during system design. The startup code also contains a call to exit just after the call to main, which allows main to return to its caller if an error occurs.
One final small but necessary bit of code is errno. errno is the location where C usually stores an integer value to signify the error condition that occurred during a library or system call. Although using errno this way is not necessary in all embedded systems, it is safer to follow convention than risk an unkown subroutine yielding a link error. Your system call emulation will also make use of this variable, allowing for a closer emulation.
Operating System Emulation
The design example uses a single file for operating system emulation (Listing 3) . This code closely emulates the standard C equivalents. Following this practice minimizes ROM debugging.
A small data structure, fd, tracks file status. The only parameter set by open and close are file open or close status. This structure can be extended to satisfy system requirements, but should be kept as simple as possible to assure ease of debugging. The open call checks for only a single possible file to be opened. /der/con (the control console), stdin, stdout, and stderr are also hard-wired as read-only or write-only, as appropriate. In some systems, open and close will link to the I/O drivers to perform driver initialization.
The most used portion of the emulation code is the functions ioctl, read, and write. The function ioctl is most often used for setting baud rates, screen modes, etc. It is shown here for completeness. The write function simply links to the driver call _cout (Listing 4) . It maps newlines into carriage return and line feed pairs. The read function buffers input. It terminates each request on receipt of a newline. Any necessary processing to put text in canonical C form should be performed here.
Portable Library Functions
In order to simplify debugging, standard I/O calls should be emulated. This example uses getc, puts, and printf. The functions here must closely emulate their respective MS-DOS counterparts, preventing unexpected surprises in the target system.
Examining Turbo C's <stdio.h> file reveals that getc is a macro that invokes fgetc (as is typical in many systems). However, _fgetc calls DOS through another function, _fgetc. Therefore, your code closely follows this by mapping _fgetc and _fputc to return the correct values for success and error (Listing 5) . A simple puts is implemented in a portable fashion through calls to putchar (Listing 6) .
This application uses the abbreviated version of printf described earlier (Listing 7) . This function only calls write, which minimizes unexpected dependencies on other library functions. Guaranteeing that floating point is not used helps in not wasting ROM space, usually a premium when system cost is considered. In many embedded applications, the only requirements for floating point support result from printf's floating point display options.
Programming And Debugging
Turbo C's greatest asset is its integrated programming environment. A bug can be quickly discovered and the code can be quickly recompiled from the same environment. Typically, it is in this environment that the first operational code is developed. (An interesting note: the new environment for Turbo C++ includes a register display window that works well with files assembled using the masm debugging switch). This phase should be used for all logic testing and customer demonstrations, since it is the most flexible of the development phases.
Once the main logic has been debugged, the code should be linked with the portable libraries and tested. Listing 8 shows an example make file. Both DOS and ROM version targets should be defined, so that any changes that may be necessary to the main logic resulting from target system testing can be incorporated into the DOS version and retested in the integrated environment. The make file presented here also contains two additional targets, ex0 and ex0r. These targets are typically limited versions that test the device drivers. Quite often, these versions provide repeating patterns and echo routines for testing peripherals. Upon satisfactory completion of testing, the resulting code is burned into a final EPROM and delivered.
Conclusions
This example provides a good start for an embedded system design. Although the XT and AT architecture is used for this example, the techniques presented here are not limited to them. These techniques have been used for new designs in projects ranging from simple computer peripherals, such as printers and tape transports, to specialized sonar signal analysis hardware. They provide a broad approach that can be applied to both native and cross compilations, substituting cross compilers and cross compilation targets in the make file.
Of course, an embedded system need not include support for printf, read, or write. An application can instead make simple calls directly to the device driver, which results in much smaller code. However, since many tradeoffs are considered during the system design phase, more often than not the extra functions are worth providing.
Listing 1
/************************************* * mon86.c * Simple 8086 rom based monitor. **************************************/ #include <stdio.h> #include <dos.h> void debug(); int smatch(); struct table { char *t_name; /* the name for this entry */ int (*t_fn)(); /* the function to execute */ }; int mem(), go(), error(); struct table cmd[]= { "mem", mem, "go", go, "", error }; void main(argc, argv) int argc; char *argv[]; { if(argc != 2) { printf("%s: incorrect argument count\n", argv [0] ); exit(1); } else printf("mon86 - demo 8086 monitor\n%s version\n", argv[1] ); debug (); } void debug() { char line[BUFSIZ]; char key[32]; char *lp, *p; struct table *tp; char *skipwh(); for(;;) { printf("/nmon86:"); gets(line); lp = skipwh(line); if(*lp == NULL) continue; p = key; while(*lp != NULL && !iswhite(*lp)) *ptt = *lp++; *p = NULL; for(tp = cmd; *(tp -> t_name) != NULL; tp++) if(smatch(key, tp -> t_name)) break; (*(tp -> t_fn))(lp); } } int iswhite(c) register int c; { return (c != NULL && (c == ' ' || c == '\t, || c == '\n' || c == '\r')); } char *skipwh(p) register char *p; { while(iswhite(*p)) ++p; return p; } /********************************************** * smatch: * match two strings. return true if equal *********************************************/ static int smatch(s1, s2) char *s1, *s2; { while(*s1 != '\0') { if(*s1 !=*s2) break; ++s1; ++s2; } return *s1 == *s2; } int mem(lp) char *lp; { unsigned long l = 0l; unsigned int seg, offset; char far *fp; char buf[80]; printf("Memory examine and change\n"); lp = skipwh(lp); while(hexdigit(*lp) >= 0) l = (l << 4) + hexdigit(*lp++); if(*lp == ':') { ++lp; seg = l; l = 0l; while(hexdigit(*lp) >= 0) l = (l << 4) + hexdigit(*lp++); offset = l; } else { seg = 0; offset = l; } fp = MK_FP(seg, offset); for(;;) { printf("%04x:%04x - %02x ", FP_SEG(fp), FP_OFF(fp), *fp & 0xff); gets(buf); lp = skipwh(buf); if(smatch(".", lp)) break; if(!hexdigit(*lp) >= 0) { ++fp; continue; } while(hexdigit(*lp) >= 0) l = (l << 4) + hexdigit(*lp++); *fp++ = l & 0xff; } } int go(lp) char *lp; { int (far *fp)(); unsigned long l = 0l; unsigned int seg, offset; lp = skipwh(lp); while(hexdigit(*lp) >= 0) l = (l << 4) + hexdigit(*lp++); if(*lp == ':') { ++lp; seg= l; l = 0l; while(hexdigit(*lp) >= 0) l = (l << 4) + hexdigit(*lp++); offset = l; } else { seg= 0; offset = l; } fp = NK_FP(seg, offset); printf("Go from %04x:%04x", FP_SEG(fp), FP_OFF(fp)); (*fp)(); } int error() { printf("*** Unknown command\n"); } hexdigit(c) int c; { register char *p; register i; static char *set = "0123456789abcdef", *alt_set = "0123456789ABCDEF"; for(i = 0, p = set; *p != NULL; i++, p++) if(c == *p) return i; for(i = 0, p = alt_set; *p != NULL; i++, p++) if(c == *p) return i; return -1; }
Listing 2
page 60,132 title start1 - simple start-up code ;**************************************************************; ; ; ; start1.asm ; ; ; ; start-up for stand-alone C code - Turbo C/C++ version ; ; ; ; created: November 17, 1990 ; ; ; ; Copyright (c) 1990 ; ; Pasquale J. Villani ; ; All Rights Reserved ; ; ; ;**************************************************************; _TEXT segment byte public 'CODE' assume cs:_TEXT ; small model _TEXT ends _DATA segment word public 'DATA' DGROUP group _DATA,_BSS,_BSSEND ; small model assume ds:DGROUP,ss:DGROUP _DATA ends _BSS segment word public 'BSS' _BSS ends _BSSEND segment byte public 'STACK' _BSSEND ends _TEXT segment byte public 'CODE' extrn _main:near ; C entry point extrn _hdw_init:near ; Hardware init (asm or C) ; ; entry: ; Our ROM madule's entry point. This should be used as the ; 'jmp far ptr entry' at the top of ROM. ; entry proc far public entry; ; ; First order of business is the initialization of the ; machine itself. This is basically the initialization ; of any segment registers that the processor needs for ; proper operation. On an 80X86 class processor, this is ; usually initializing the segmentation registers, going ; to the proper mode (i. e. protected, etc.) for ; 80[234]86 processors, etc. This file is for an 8086 or ; 8088 machine (or any other in real mode). Segment ; registers are the only processor initialization. ; ; NOTE: the cs register is not initialized because the ; processor itself initializes the first cs, followed ; by the far jump changing it to point to segment entry. ; cli ; prevent interrupts while starting mov ax,DGROUP ; initialize the segment registers mov ds,ax mov ss,ax mov es,ax ; ; Once the processor is initialized, it usually requires ; the proper setting of the stack pointer(s). For the ; 8086 case, all we need is to initialize the sp and bp ; registers. ; mov sp,offset DGROUP:tos mov bp,sp ; ; We are now ready to make any special hardware ; initialization. This includes initializing the interrupt ; vector table, moving the data segment to RAM and ; initializing the BSS to zero to match C programming ; conventions. This should end with a call to __hdw_init ; to initialize I/O, etc. call near ptr __hdw_init; initialize the system without ints sti ;now enable them ; ; After the hardware is initialized, we wish to enter ; main(). This is accomplished by building the stack for ; envp, argv and argc. This is a convienent mechanism ; for passing system configuration, dip switch settings, ; etc. to our C code. ; mov ax,offset env ; envp for this example push ax mov ax,offset args ; argv for this example push ax mov ax,2 ; argc = 2 push ax call near ptr _main ; finally enter C code add sp,6 ; clean stack ; ; If main should return, we need a mechanism to catch this ; condition. Depending on the system design, we should ; alarm, restart, etc., therefore, the following code ; is very implementation dependant. In our case, all we ; want to do is call exit with a special -1 exit code. ; mov ax,-1 push ax call near ptr _exit jmp $ ; belt and suspenders entry endp ; ; exit: ; Where to go for error conditions (typically) or shutdown ; conditions. This code is very implementaion sensitive, since ; we may want to light LEDs and sound alarms. In this case, we'll ; only silently stop. In certain cases, we may want to only ; display a message and restart. This should be decided upon ; during system design. ; _exit proc near public _exit cli hlt jmp _exit _exit endp T_EXT ends _DATA segment word public 'DATA' env0 label byte db 'ENV=ROM',0 arg0 label byte db 'ex0s', 0 arg1 label byte db 'rom', 0 env label word dw DGROUP:env0 dw 0 args label word dw DGROUP:arg0 dw DGROUP:arg1 dw 0 _DATA ends ; ; The stack segment. This size should be adjusted to fit ; any particular system requirements. ; _STACK SEGMENT dw 512 dup (?) tos label byte dd (?) ; safety area last label word ; must always be end of stack area _STACK ENDS _BSS segment word public 'BSS' _errno label word ; c lib errno public _errno dw (?) _BSS ends _BSSEND segment byte public 'STACK' _BSSEND ends end
Listing 3
/***************************************/ /* */ /* osem.c */ /* */ /* Operating system emulation routines */ /* for embedded system example ROM */ /* monitor. */ /* */ /* Copyright (c) 1990 */ /* Pasquale J. Villani */ /* All Rights Reserved */ /* */ /* */ /***************************************/ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <errno.h> /* convience defines */ #define STDIN 0 #define STDOUT 1 #define STDERR 2 #define MAXFD 3 #define BUFSIZ 512 /* buffers are 512 for this vsn */ #ifndef TRUE #define TRUE 1 #endif #ifndef FALSE #define FALSE 0 #endif #ifndef ERROR #define ERROR -1 #endif #ifndef OK #define OK 0 #endif #ifndef EOF #define EOF -1 #endif /* make errno accessible to the entire file */ extern int errno; /* special data structure for ioctl optional argument */ union arg { int i; void *p; }; /* a simple file table to keep track of stdin & stdout status */ static char fd[MAXFD] = { FALSE, /* stdin */ FALSE, /* stdout */ FALSE /* stderr */ }; /* a simple set of i/o buffers for input buffering */ struct _buf { int nbytes; char *bp; char buf[BUFSIZ]; }; static struct _buf iobuf = { -1, (char *)0 }; /* */ /* smatch: */ /* match two strings. return true if equal */ /* */ static int smatch(s1, s2) char *s1, *s2; { while(*s1 != '\0') { if(*s1 != *s2) break; ++s1; ++s2; } return s1 == s2; } /* */ /* open: */ /* open a file for read or write */ /* follows posix specification */ /* */ int open(path, oflag, mode) char *path; int oflag, mode; { if(smatch(path, "/dev/con") || ((oflag & O_RDWR) == O_RDWR)) { errno = EACCES; return ERROR; /* error - only console allowed */ } /* for this system, only stdin and stdout are available /* based on oflag and availability, assign appropraie fd if((oflag & O_RDONLY) && !fd[STDIN]) { fd[STDIN] = TRUE; return STDIN; } else if(oflag & O_WRONLY) { fd[STDOUT] = TRUE; return STDOUT; } else { errno = EACCES; return ERROR; } } /* */ /* close: */ /* close a file */ /* */ int close(fildes) int fildes; { if((fildes < 0) || (fildes > MAXFD) || !(fd[fildes])) { errno = EBADF; return ERROR; } fd[fildes] = FALSE; return OK; } /* */ /* ioctl: */ /* direct device control */ /* */ int ioctl(fildes, request, arg) int fildes, request; union arg arg; { errno = EINVAL; return ERROR; } /* */ /* write: */ /* write to a file */ /* */ int write(fildes, buf, nbyte) int fildes; void *buf; unsigned int nbyte; { int cnt = nbyte; if(fildes != STDOUT && fildes != STDERR) { errno = EBADF; return ERROR; } while(cnt-- > 0) { if(*(char *)buf == '\n') _cout('\r'); _cout(*((char *)buf)++); } return nbyte; } int read(fildes, buf, nbyte) int fildes; void *buf; unsigned int nbyte; { register char *bp; register int c; if(fildes != STDIN) { errno = EBADF; return ERROR; } if(iobuf.nbytes <= 0) { bp = iobuf.buf; iobuf.nbytes = 0; do { *bp++ = (_cout(c = _cin())); if(c == '\r') _cout('\n'); ++iobuf.nbytes; } while(c != '\n' && c != '\r'); iobuf.bp = iobuf.buf; } for(bp = iobuf.bp, c = 0; c < iobuf.nbytes && c <nbyte; c++) { *((char *)buf)++ = *bp == '\r' ? ++bp, '\n' : *bp++; } iobuf.bp = bp; iobuf.nbytes -= c; return c; }
Listing 4
; ; _cio - console i/o for embedded system example ; near version ; _TEXT segment byte public 'CODE' DGROUP group _DATA,_BSS assume cs:_TEXT,ds:DGROUP,ss:DGROUP _TEXT ends _DATA segment word public 'DATA' _DATA ends _BSS segment word public 'BSS' _BSS ends _TEXT segment byte public 'CODE' ; ; _cin - input to console ; near version ; __cin proc near public __cin push bp ; perform c entry mov bp,sp push bx ; save bx for c mov ah,0h ; get the byte mov bx,7 int 16h ; using the bios mov ah,0 ; "sign extend" - convert to int pop bx ; restore context pop bp ret __cin endp ; ;_cout - output to console ; near version ; __cout proc near public __cout push bp ; c entry mov bp,sp push bx ; save register for c mov ax,word ptr [bp+4] ; get c mov ah,0eh ; and output the byte mov bx,7 int 10h ; call bios pop bx ; restore context pop bp ret __cout endp ; ; _cinit - console initialization ; **** No initialization required for bios version **** ; ; near version ; __cinit proc near public __cinit push bp ; perform any required hardware initialization here mov ax,1 ; return true for success pop bp ret __cinit endp _TEXT ends end
Listing 5
/****************************************************/ /* */ /* iolink.c */ /* */ /* stdio tie for Turbo C/C++ Compilers */ /* */ /* Copyright (C) 1990 */ /* Pasquale J. Villani */ /* All Rights Reserved */ /* */ /****************************************************/ #include <stdio.h> FILE _streams[20]; fputc(c, f) char c; FILE *f; { return _fputc(c, f); } _fputc(c, f) char c; FILE *f; { if(write(1, &c, 1) == 1) return c; else return EOF; } _fgetc(f) FILE *f; { char c; if(read(0, &c, 1) != 1) return EOF; else return c; }
Listing 6
/*************************************************/ /* */ /* puts.c */ /* */ /* Put a string to console. Uses stdio.h putchar */ /* macro for operating system interface. Outputs */ /* a newline after the string */ /* */ /* Copyright (c) 1990 */ /* Pasquale J. Villani */ /* All Rights Reserved */ /* */ /* */ **************************************************/ #include <stdio.h> puts(s) const char *s; { int c; while ((c = *s++) != '\0') putchar(c); putchar('\n'); }
Listing 7
/* * abbreviated printf routine * * embedded system version * 11/30/90 */ /* ltob -- convert an long integer to a string in any base (2-36) */ static char *ltob(n, s, base) long n; char *s; { unsigned long u; register char *p, *q; register negative, c; if (n < 0 && base == -10) { negative = 1; u = -n; } else { negative = 0; u = n; } if (base == -10) /* signals signed conversion */ base = 10; p = q = s; do { /* generate digits in reverse order */ *p++ = "0123456789abcdef"[u % base]; } while ((u /= base) > 0); if (negative) *p++ = -; *p = '\0'; /* terminate the string */ while (q < --p) { /* reverse the digits */ c = *q; *q++ = *p; *p = c; } return s; } #define NONE 0 #define LEFT 1 #define RIGHT 2 /* printf -- short version of printf to conserve space */ int printf(fmt, args) const char *fmt; char *args; { register base; register char **arg; char s[11], *p, *ltob(); char c, slen, flag, size, fill; arg = &args; flag = NONE; size = 0; while ((c = *fmt++) != '\0') { if (size == 0 && flag == NONE && c != '%') { write(1, &c, 1); continue; } switch(*fmt) { case '-': flag = RIGHT; fill = *(fmt + 1) == '0' ? '0' : ' '; continue; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': if(flag == NONE) flag = LEFT; size = *fmt++ - '0'; while((c = *fmt++) != '\0') { switch(c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': size = size * 10 + (c - '0'); continue; default: --fmt; break; } break; } break; } switch (c = *fmt++) { case 'c': write(1, (char *)arg++, 1); continue; case 'd': base = -10; goto prt; case 'o': base = 8; goto prt; case 'u': base = 10; goto prt; case 'x': base = 16; prt: ltob((long)(*((int *)arg)++), s, base); if(flag == RIGHT || flag == LEFT) { for(slen = 0, p = s; *p != '\0'; p++) ++slen; } if(flag == RIGHT && slen < size) { int i; for(i = size - slen; i > 0; i--) write(1, &fill, 1); } for(p = s; *p != '\0'; p++) write(1, p, 1); if(flag == LEFT) { int i; char sp = ' '; for(i = size - slen; i > 0; i--) write(1, &sp, 1); } size = 0; flag = NONE; continue; case 'l': switch(c = *fmt++) { case 'd': base = -10; goto lprt; case 'o': base = 8; goto lprt; case 'u': base = 10; goto lprt; case 'x': base = 16; lprt: ltob(*((long *)arg)++, s, base); if(flag == RIGHT || flag == LEFT) { for(slen = 0, p = s; *p != '\0'; p++) ++slen; } if(flag == RIGHT && slen < size) { int i; for(i = size - slen; i > 0; i--) write(1, &fill, 1); } for(p = s; *p != '\0'; p++) write(1, p, 1); if(flag == LEFT) { int i; char sp = ' '; for(i = size - slen; i > 0; i--) write(1, &sp, 1); } size = 0; flag = NONE; continue; default: write(1, &c, 1); } case 's': for(p = *arg; *p != '\0'; p++) write(1, p, 1); ++arg; continue; default: write(1, &c, 1); continue; } } }
Listing 8
# # makefile for example 0 # # 12/02/90 1830 EST # CC = tcc CFLAGS = -ms -v AS = masm ASFLAGS = /Mx/Zi LD = tlink LDFLAGS = /v/c OBJSROM = start1.obj cio.obj osem.obj iolink.obj\ hdwinit.obj prf.obj puts.obj gets.obj LIBSROM = libm.lib all: ex0.exe ex0s.exe mon86.exe mon86r.exe mon86.exe: mon86.obj $(CC) $(CFLAGS) -emon86 mon86 osem.obj: osem.c $(CC) $(CFLAGS) -c osem.c mon86.obj: mon86.c $(CC) $(CFLAGS) -c mon86.c iolink.obj: iolink.c $(CC) $(CFLAGS) -c iolink.c prf.obj: prf.c $(CC) $(CFLAGS) -c prf.c puts.obj: puts.c $(CC) $(CFLAGS) -c puts.c gets.obj: gets.c $(CC) $(CFLAGS) -c gets.c start0.obj: start0.asm $(AS) $(ASFLAGS) start0,,,; ex0s.exe: ex0.obj $(OBJSROM) $(LD) $(LDFLAGS) $(OBJSROM) ex0.obj,\ ex0s,nul,$(LIBSROM) mon86r.exe: mon86.obj $(OBJSROM) $(LD) $(LDFLAGS) $(OBJSROM) mon86.obj,\ mon86r,nul,$(LIBSROM) start1.obj: start1.asm $(AS) $(ASFLAGS) start1,,,; cout.obj: cout.asm $(AS) $(ASFLAGS) cout,,,; cin.obj: cin.asm $(AS) $(ASFLAGS) cin,,,; cio.obj: cio.asm $(AS) $(ASFLAGS) cio,,,; ex0.obj: ex0.c $(CC) $(CFLAGS) -c ex0.c