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');
}
Adapted from an article that appeared in Personal Engineering & Instrumentation
News.
Return to the article index.
|