SLTF Consulting
Technology with Business Sense

 Home | Bio | Contact Us | Site Map


 

Monitors are valuable, easy to use and create

Scott Rosenthal
September, 1994

The other day as I was feverishly trying to get my life in order so I could take my first vacation in two years, one of my hardware engineers asked if I could write for him a program to test a new circuit board. Because this board plugs into a PC, I reminded him that DOS comes with DEBUG, a simple monitor program that's probably sufficient. Although the lowly monitor program isn't always at the top of everyone's wish list of debug tools, few others are as portable, flexible, inexpensive and reliable. My last column (see reference) delved into the philosophy behind a monitor program; this time I'll continue by looking at generic monitor programs and providing a few tips that will make life easier when building your own.

Using DEBUG

To recap briefly, DEBUG is probably the most widely known example of a monitor program. Although every DOS system ships with it in ROM, most users would be hard-pressed to think of what they'd use it for. Originally it was the primary aid for debugging PC-based programs. However, with the multitude of development environments that have become available for the PC, the lowly DEBUG monitor has fallen into obscurity. However, just as the situation cited above illustrates, DEBUG still has a place in development work.

The board my engineer was working on is a custom video processing circuit for a medical application, and it contains a Xilinx FPGA and support circuitry such as clocks, A/D and drivers. The FPGA performs some video processing and device selection. Because the design assumes that the application software loads the FPGA program, the preliminary debug technique I settled on was to write a simple FPGA loader and then switch to DEBUG. Once in the monitor, I could exercise the I/O ports with the I (input) and O (output) commands as well as use the A (alpha mode) and G (graphics mode) commands to enter a simple 2-line program to put the system into a simple strobing loop. For example, by using this loop to toggle the I/O bit controlling an LED on the board I could test the device's drive circuitry.

Remember, a monitor program is just one of a series of tools that an embedded-systems designer needs in his personal toolbox. It doesn't replace an emulator, especially when you have flaky hardware or want to debug real application code. It also doesn't replace a logic analyzer. Rather, it's a simple mechanism that allows you to go into and probe a target system's I/O and memory as if you were the CPU.

Build or buy?

This simplicity leads to an interesting dilemma. Although few people would think about building their own emulators or logic analyzers, the question of whether to build or buy a monitor program is valid. I'm sure that many fine monitors are available on the market, but my preference is to build my own. This choice results in three advantages. First, I understand all the intimate details of how the program works. Second, I can make it work exactly as I want it to. Third, I can modify it in any way for whatever reasons. Talk about power!

A good example of the value of designing a custom monitor program, or at least being able to modify someone else's, arose when I designed an instrument using a microcontroller to implement an alarm tone generator. The instrument's main computer had to communicate with this device, so I selected an I2C serial interface as the communication protocol. This decision was great because half the job was already done-the microcontroller had a built-in I2C interface. However, the main computer used software to implement the interface through a 2-bit (no pun intended) interface. Hence I had to write my own I2C drivers, which meant that I also needed to debug the interface. Two options existed for meeting this challenge: I could debug the low-level driver from within the main application and worry about any unexpected interferences from the application, or I could use my trusty monitor. In the end, I took the latter course and added one command for controlling the I2C port. With this one minor change I could communicate with the microcontroller and debug the I2C interface without worrying about conflicts arising from the main application.

How to do it

To create such an extensible basic monitor program is quite simple. The only hardware you need is a serial port for communicating with an external device such as a dumb terminal, a terminal emulation program or even an old teletype.

Next, forget long commands-this command interface might be a user non-friendly interface, but it's that way for a purpose. First, single-keystroke commands make writing a command parser trivial. Second, it reduces the number of keystrokes to complete a given operation. I don't know about you, but I get very tired of typing long sequences of characters to cause a program to take an action.

Even though brevity has its advantages, one disadvantage is that the cryptic codes are easy to forget. Armed with this knowledge and the assurance that I'll lose any instruction sheet or manual I might write, I embed within the monitor program a help key. I use both 'H' and '?' because I can't remember which one to press, and having both return the information solves the problem. My standard monitor program doesn't have many commands. In addition to a function letter, I include a description of how to use the command, being sure to keep it brief to conserve memory.

When you get down to designing a monitor, the first step is to use a high-level language such as C so the work is transportable to many different processors. Assembler works, but you'll have to rewrite the program for every new target processor. Also, abstract as much of the functionality as possible into simple functions. For example, the serial-interface techniques depend on different target-hardware configurations, so set this code aside in a serial-interface function. Likewise, encapsulate address-string translation because different processors use different address representations-it might be two bytes, or two words separated by a colon. On the other hand, I've found that data strings generally stay the same.

As an example of how all this advice fits together, the nearby listing is the basic shell of the monitor I've written and used. Note that the command parser incorporates one entry for each letter in the alphabet so I can add commands by simply removing a NOP and inserting the name of the function that does what I want. Above all, this code demonstrates that it's crucial to keep the program simple. Forget interrupts; use polled I/O for the serial interface. It's clunky and your friends might laugh at it, but hey-you'll finish your work first, be more productive and contribute more to where your paycheck comes from. PE&IN

Reference

Rosenthal, S, "For digging out hardware bugs, monitor programs fill the bill," PE&IN, July 1994, pgs 56-58.

Listing: Basic monitor program

#include "equ.h"
#include "ctype.h"
char buf[100];
main() {		
	sysinit();		
	put_string("\nVersion 2.0, 11-03-1992 \n");		
	for (;;) {				
		put_string("\r\n::");				
		get_line('\r');				
		process();	
	}
}

/* sysinit initializes the hardware/software */
sysinit() {		
extern int com_port;		
#define C8255_CTL	0x303		
#define MEM_H	0x313		
com_port = 1;		
cominit(9600);		
init_el();	/* init the 82c455 chip */		
init_rtc();	/* init real time clock */		
init_tone();	/* init the tone output */
 }

long cmd[10];

process() {  /* branches to function */		
int	change(), display(), exit();		
int	fill(), input(), nec(), nop(), output();		
int	i, j, k, m;		
BYTE	c;		
static int (*table[])() = {			
nop,	/* A */	nop,	/* B */	nop,	/* C */			
display,	/* D */	nop,	/* E */	fill,	/* F */			
nop,	/* G */	nop,	/* H */	input,	/* I */			
nop,	/* J */	nop,	/* K */	nop,	/* L */			
nop,	/* M */	nop,	/* N */	output,	/* O */			
nop,	/* P */	nop,	/* Q */	nop,	/* R */			
change,	/* S */	nop,	/* T */	nop,	/* U */			
nop,	/* V */	nop,	/* W */	exit,	/* X */			
nop,	/* Y */	nop,	/* Z */ };		
if (isalpha(buf[0])) {				
j = strlen(buf);				
cmd[0] = 0;				
for (i = 0, k = 1; i < j; ) {						
if (!isalnum(buf[++i])) continue;						
cmd[k] = 0;						
m = NO;						
while ((buf[i] >= 'A' && buf[i] <= 'F')								
|| (buf[i] >= '0' && buf[i] <= '9')) {								
c = buf[i++] - '0';									
if (c > 9) c = c + '0' - 'A' + 10;								
cmd[k] = cmd[k] * 16 + c;								
m = YES;			
}						
if (m = YES) k++;		
}				
cmd[0] = k - 1;				
(*table[buf[0] - 'A'])();	
} else if (buf[0] == '?')			
help();
}

help() { /* lists commands */		
int i, length;		
static char *ptr[] = {				
"D: Display Memory	D Start,Length",				
"F: Fill Memory	F Start,Length,Char",				
"I: Input Port	I Port",				
"O: Output Port	O Port,Byte",				
"S: Set Memory	S Start",	
};		

length = sizeof(ptr) / sizeof(char *);		
for (i = 0; i < length; i++) {				
crlf();				
put_string(ptr[i]);		
}
}

nop() { /* a no-op function. */		
put_string("Function isn't available\n");
}

display() {  /* displays memory. */		
static unsigned long start = 0;		
static long length = 0x80;		
int i;		
long cnt, s;		
char buf[17];		
BYTE b;		

if (cmd[0] >= 1)			
start=((cmd[1]>>4) <<16)+(cmd[1]&0x0f);		
if (cmd[0] >= 2) length = cmd[2];		
for (i=0,cnt=0;cnt<length;start++,cnt++) {				
if (see_if_char() == YES) return;				
if (i == 0) {						
crlf();						
put_hex_address(start);						
i = 16;				
}				
if (i == 8) put_string(" -");				
put_char(' ');				
put_hex_byte(b = peekb(start));				
i-;				
if (isprint(b)) buf[15 - i] = b;				
else buf[15 - i] = '.';				
if (i == 0) {						
buf[16] = '\0';						
put_string("   ");						
put_string(buf);				
}		
}		
crlf();
}

/* fills a range of memory with a character.*/
fill() {		
unsigned long start;		
WORD length;		
BYTE b;		

if (cmd[0] >= 1)			
start = ((cmd[1]>>4)<<16)+(cmd[1]&0x0f);		
else return;		
if (cmd[0] >= 2) length = cmd[2];		
else return;		
if (cmd[0] >= 3) b = cmd[3];		
else return;		
while (length-) {		
pokeb(start++, b);	
}
}

/* crlf sends a CR then a LF. */
crlf() {	
put_string("\r\n");
}

/* changes a byte of memory. */
change() {		
int i;		
unsigned long start;		
BYTE b, c;		
if (cmd[0] >= 1)			
start =((cmd[1]>>4)<<16)+(cmd[1]&0x0f);		
else return;		
do {				
put_hex_byte(b = peekb(start));				
put_char('-');				
c = get_hex_byte(&b);				
pokeb(start++, b);				
if (c == '\r') return;				
put_char(' ');		
} while (1 == 1);
}

/* input read an input port.*/
input() {		
int i;		
WORD port;		
BYTE b, c;			

if (cmd[0] >= 1) port = (WORD)(cmd[1]);		
else return;		
put_hex_byte(inportb(port));
}

/* output sets an output port to be set.*/
output() {		
int i;		
WORD port;		
BYTE b, c;		

if (cmd[0] >= 1) port = (WORD)(cmd[1]);		
else return;		
if (cmd[0] >= 2) b = (BYTE)cmd[2];		
else return;		
outportb(port, b);		
put_hex_word(port);		
put_string(" <= ");		
put_hex_byte(b);
}

get_hex_byte(b) /* reads a hex byte.*/	
BYTE *b;
{		
BYTE c;		
int first = YES;			

for (;;) {				
put_char(c = wait_for_char());				
c = toupper(c);				
if ((c>='A'&&c<='F')||(c>='0'&&c<='9')) {						
if (first == YES) {								
*b = 0;	/* initialize accumulator */								
first = NO;						
}						
c = c - '0';						
if (c > 9) c += ('0' - 'A' + 10);						
*b = *b * 16 + c;				
}				
else return c;		
}
}

put_hex_address(h) /* outputs a address.*/	
unsigned long h;
{		
put_hex_word((WORD)(h >> 16));		
put_char(':');		
put_hex_word((WORD)h);
}

put_hex_word(h) /* outputs a hex word.*/	
WORD h;
{		
put_hex_byte(h >> 8);		
put_hex_byte(h);
}

put_hex_byte(h) /* outputs a byte in hex.*/	
BYTE h;
{		
put_hex_digit(h >> 4);		
put_hex_digit(h);
}

put_hex_digit(h) /* outputs a hex digit.*/	
BYTE h;
{		
if ((h &= 0x0f) > 9) h += 7;		
put_char(h + '0');
}


About Us | What We Do | SSM | MiCOS | Search | Designs | Articles

Copyright © 1998-2014 SLTF Consulting, a division of SLTF Marine LLC. All rights reserved.