8051 Coding and Debugging

by Olaf Pfeiffer, Embedded Systems Academy, Inc., based on the article "Real-Time 8051 Systems - An Overview" by Mike Beach, Hitex UK

Using assembly and sporadic status messages sent to the serial port to develop and debug an 8051 based application is not exactly "state-of-the-art". This article shows some of the benefits engineers get by using the right tools.

Introduction

A microcontroller-based embedded controller device is an everyday building block for many turnkey products from data loggers to engine management systems. Although many different microcontroller architectures are available, one of the most often used microcontrollers are still the 8-bit 8051 derivatives. The never-ending popularity of the 8051 architecture is based on several facts:

  1. Well known and established architecture
    • It's around since more then 15 years
    • It will be around for at least another 15 years
  2. Excellent tools available from Assembler to Compiler to Simulator/Debugger and In-Circuit Emulator
    • Assemblers, Compilers and Simulator Debuggers from Keil, Raisonance and others
    • A free C compiler version limited to 4k of code is available from Raisonance: www.51xa-tools.com
    • Wide variety of In-Circuit Emulators from entry-level to high-end
  3. Parts available from many different vendors
    • 8051 derivatives are manufactured by about 20 different semiconductor companies
    • "Standard" 8051 derivatives are pin-compatible in-between different vendors
  4. Wide variety of derivatives available
    • Philips Semiconductors offers the broadest range of 8051 derivatives from very small, affordable 20-pin (or smaller) devices to complex parts with many I/O points and peripheral features
  5. Upgrade paths available
    • Most 8051 chip manufacturers work on "faster" 8051 devices - either by just increasing speed or by decreasing the number of machine cycles required by one instruction cycle (standard is 12)
    • Philips Semiconductors announced the 51MX with an extended address range of 8MB
    • The Philips XA architecture is a 16-bit upgrade of the 8051. The Raisonance compiler mentioned above, is a compiler for both the 8051 and the Philips XA

Many applications only require a few IO ports and some memory for the program. The code is either stored on-chip in a masked ROM, OTP or Flash - or externally in a Flash or EPROM. The typical application is invariably real-time in nature. This means that software run times are bound by the necessity to process input data derived from real world events - calculations based on values read from on-chip timers is an obvious example; processing data received from a serial port is another.

Generally, devices like the 8051 have an OTP or Flash containing the code and possibly an external RAM, although this is often not the case - frequently, just the 128 or 256 bytes on the internal chip are available. OTP is normally used for cost reasons; masked ROMs are relatively uncommon because of the masking cost and engineers' reluctance to cast their software in concrete! Flash gives the option to change software after release if bugs are discovered or a change in specification is required.

8051's are also commonly used as communications controllers, either via on chip I2C/serial interfaces, CAN or external UART devices. With a fairly decent output compare/input capture system on variants like the 8xC552 and 8xC554, programs are often making timing measurements between events, using, in the case of these devices 16-bit timer 2. Calculations based on the time between captures typically follow in the course of frequency/speed measurements etc..Any impact on the processor operation caused by debugging, for instance, is going to render the results invalid.

With any real time system, whether it is during the development phase when the software is first being coded up, or in any final pre-release software quality assessment phase, the system can only properly be tested dynamically in real time.

Overall, the fact that these systems' inputs are dependent on real world events or real interrupts means that any testing or debugging which prejudices the system behavior must, by definition, invalidate any data. In addition, the loss of real time execution inevitably alters the interaction between say, interrupt and background routines so that critical regions may never be found. This is really all part of the more fundamental debate as to whether static analysis of real time systems is actually a valid thing to do at all. Certainly to date, this has been the conventional approach but recent defense standard requirements for software production do actually require some sort of dynamic testing.

The Increasing Sophistication Of Microcontroller Software

Traditionally, program development in the particular case of the 8051 processor, has been done in assembler and so complicated calculations have tended to be excluded. This is because firstly, the 8051 is not a particularly good number-cruncher, and secondly, the amount of work involved in coding up maths routines in assembler. However, with the increasing use of C, the relatively simple 8051 instruction set becomes rather remote to the programmer and the tendency is to do more complicated things, simply because C makes it so painless to do it. For instance, a 32 by 16 divide in assembler is a very major piece of software, whereas in C it would constitute maybe just one source line.

Naturally, programmers get easily carried away with this and produce more complicated code than before. Most significantly, from the debugging/testing point of view, real time programs can now be viewed as a series of readable C statements, where the logic and function of the program are the main issues, rather than how the instruction set has been coerced into carrying out the program's specification.

Of course, when it comes to testing and debugging, the engineers producing these programs will tend to think only in terms of C statements and real program variables, unlike the assembler programmer, who being so close to the machine, will treat variables as the inhabitants of esoteric and largely anonymous registers.

Pseudo-Static Testing Methods

Sub-routines or even small functional blocks such as a calculation are commonly tested statically; the program may be run to the beginning of the calculation, some input values loaded up, and the program released to run in real time. At the end of the section to be tested, execution is broken and the result(s) extracted and checked for correctness.

This is a perfectly valid method, provided that the input values are totally independent of real world events or time, and that program operation could not possibly be upset by interrupts. Another extension to this problem is that once the program or software is working to a reasonable standard, often the programmer needs to get a feel for what the thing actually does - in simple terms, play around with the software's inputs while the program is running. This often reveals unexpected operation when say an input goes to full scale. Again conventionally, because of the way in which the 8051 and probably most other microcontrollers are constructed, the contents of any RAM locations/variables are totally invisible during program execution. While they can be made visible with some advanced development systems by setting breakpoints on write accesses to certain variables and looking at the data, real time operation is inevitably lost in the very act of extracting the data. Even if the emulator can carry on automatically after the breakpoint, at least several hundred microseconds are lost while the emulator actually retrieves the value.

Many simpler emulators will not even do this basic task. For much of the time with this sort of software development, the programmer has to simply keep going through the listings, doing a "what if" exercise on every variable, possibly extending to a '"what if" variable = such and such, and interrupt occurs at a certain point' etc. All the potential permutations of variable values, program paths and interrupt times makes the testing of this sort of real time software almost impossible.

The Link Between Hardware And Coding Approach

With applications coded in assembler, the memory model that is used is really very much embedded into the software and reflects fully the physical memory configuration of the hardware. For instance, if the programmer is going to use an external RAM chip, a conscious effort must be made to use MOVX @DPTR instructions when moving data around. If the program is written using solely the internal memory, to convert it to use external RAM (should more space be required) would be a very major undertaking - all the move indirect/direct instructions would have to be converted to include the loading of DPTR and the use of MOVX @DPTR.

Such an exercise is almost impossible given a program of any size, or at the very least extremely laborious. Really in this sense, the software is as rigid and inflexible as the hardware design. If the software is designed to use only internal RAM, it is almost as difficult to convert it to use external RAM as it is to change the PCB design to add the RAM chip to the hardware in the first place.
Coding For A Variety Of Memory Models

A new possibility has been opened up in this area for engineers using C compilers such as the ones from Keil or Raisonance. These compilers will support many different memory models - the memory model being determined by just a software switch at the compilation stage. It now becomes extremely simple to change the memory model assumed by the software during development. The memory model used is no longer implicit in the nature of the program instructions used by the programmer.

An example of how this memory model switch works is as follows: if the SMALL model is selected, the compiler will assume that no external RAM exists and all function parameters, variables etc. will be located within the internal directly or indirectly addressable RAM segments of the microcontroller's memory. The large model, if selected as a global model, will cause all variables and parameter passing areas to be forced into external RAM, where they will be addressed by the MOVX @DPTR instruction.

The stack, as has to be the case with the 8051, always remains in internal memory. Typically in expanded 8051 systems, the external RAM will be used for parameter passing and variables, constituting the LARGE model for the C compiler. The stack now has a free run of the internal memory. There is, unfortunately, a penalty in going to the large model, as within the basic 8031 there is only one DPTR that can be used for moving data in and out of external memory devices, so any addressing of an external variable will require the loading of the DPTR and then the moving of the data to the target location, via the accumulator. There is a COMPACT model which will actually allow paged memory to be used. This is actually slightly better in that external memory movements are made with respect to one of the eight general purpose R0 - R7 registers. This, in actual fact, does not require the constant loading and unloading of the DPTR and so probably represents the best way of using external RAM.

Variable Characteristics In Assembler And C Programs

The upshot of all this is that a program which is initially written for the SMALL memory model can at the flick of a software switch be recompiled to produce a totally different set of 8051 instructions that can now make use of external RAM. This, as has been said, has not been possible with assembler - assembler programs carry their memory model with them at all times and it cannot change. With the C compiler route, the memory model may be changed at will. This ease of conversion opens up an interesting new debugging/ testing possibility that has been well explored by the authors.

Testing Real Time 8051 C Systems - in LARGE memory model

Depending on the 8051 derivative and in-circuit emulation system choosen, on-the-fly access to internal memory areas such as SFR (Special Function Registers) or internal RAM will be limited. Some derivatives can be set into an emulation mode, where some pins are multiplexed and provide the in-circuit emulator with information about the internally accessed memory. While running, on-the-fly monitoring of internal variables is possible in real-time with such systems. Unfortunately, this feature is not available on "regular" 8051 derivatives.

However, with the previously stated testing problems and memory model points in mind, here is a solution to the real-time debugging problem of accessing internal memory on 8051 systems on-the-fly.

When a module or function is first being coded up, it needs to be compiled using the LARGE model, regardless of the final model to be used - normally determined by the hardware design. This has the effect of pushing all the variables into external RAM. During the debugging phase, the variables will remain in external RAM. Assuming that the compiler will produce code of identical functionality, regardless of model used, the programmer is left in the happy position of having all the variables and parameters of the new code sitting in external RAM rather than buried within the on-chip RAM.

The debugging via an in-circuit emulator is done at C source level, so that the user will single-step C lines or trace lines of C and will basically only view the program in terms of the C source lines originally created using his text editor. Essentially, just the logic of the program is actually being tested here, not the way that the 8051 is being driven by the compiler's opcodes.

As has been said, many 8051 systems have no external RAM, its all on-chip. However, if external code memory is used, port 0 and 2 are available as address and data bus. Having program variables located in external RAM therefore may appear unwise. However, using the in-circuit emulator's own memory, this absent RAM can be easily created. The program will therefore run using RAM within the emulator. Even though this external area is not actually part of the final design, providing the emulator is attached, the RAM is available and any LARGE/COMPACT model C programs can make use of it and run correctly.

Ultimately, the whole point of doing this is that the high-end in-circuit emulators will actually let you recover the values of variables located in external RAM, in real time. High-end in-circuit emulators also use dual-ported memory: the RAM mapped into the data space is dual-ported. This means that the 8051 can access the RAM, and in between cycles, the emulator's own hardware can get in and recover specific variables. Once extracted from the 8051 system, their values can either be reported to the user on the controlling PC screen, or directed into the emulator's trace buffer - it doesn't really matter which.

In effect what we have got now is the ability to recover C variables or program variables in real time "on the fly", using the real 8051 hardware. The only respect in which it differs from the proper hardware is that it has got the in-circuit emulator supplying the external RAM, where no RAM chip position actually exists on the board.

A Simple Example

The value of this technique is best illustrated by the following real-time testing example:

The speed of a rotating shaft is to be measured in real time as a scaled value between 0 and 10000 rpm and from this a variety of other calculations performed. The problem here is a typical 8051 task; an interrupt is generated by an input capture pin every 180 degrees of shaft rotation. Every time a negative transition occurs on the capture input 0, the TIMER2 counter value is latched into an input capture register. This is done every 180 degrees.

The input capture service routine picks up this value, subtracts it from the last value and works out the time in TIMER2 counts between successive interrupts or 180 degree markers on the shaft. Knowing the time between interrupts, it is possible to work out the rotational speed.

The calculation to be performed in the service routine every 180 degrees of shaft rotation is:

  1. time_for_180 = abs(time_this_180 - time_last_180) ;
  2. speed = 382500 / (time_for_180) ;

Where 0 to 255 represents 0 to 10000 rpm and the numerator is a constant that assumes a 2us clock period.
With the emulator configured correctly, the values of time_for_180 and speed are displayed on the screen with the program running in real time. Clearly, with the input values being dependent on real time events, any interference would cause an error in the measure times. By the same token, any corruption of the result by other interrupt routines will now be obvious - the result on the screen will be wrong. Now, with the interrupt rate constant, the output from the calculation will be constant. The calculation can now be checked from the screen values by using a pocket calculator...

Conventionally, this calculation could only be tested statically - influence from other interrupts could only be done by a "what if" exercise on the source listings and logical bugs might ultimately only show up as faulty operation at a port pin.

In addition, being able to see the results of the C calculation change in real time as, in this case, the output of a signal generator is altered, is very reassuring to the programmer.

Having verified the correct operation of the function, it can now be recompiled using the small model. Assuming that the compiler produces functionally identical code regardless of model, the function can be considered debugged.

Another problem which can be partially solved by this method is, for instance, buffering timer values and incoming serial port buffer contents into a visible external RAM location. Thus the mystery of a timer's value on entry to an interrupt routine can now be solved.

Some Points of Limitation

The following points should be noted:

  1. For situations where the emulator supplies the external RAM chip, it should be noted that port 0 and 2 are needed as address and data bus, also #RD and #WR must be reserved. None of them can be used as IO port pins.
  2. If the output data being observed changes too quickly for a satisfactory display on the emulator's host PC screen, it could be channeled into the trace buffer for later examination. Real time operation is again not prejudiced.
  3. There is only one drawback with this method - in very high speed, very time critical systems, the extra runtime caused by using external variables might prevent the required operating speed from being reached. A partial solution to this is to compile using the final SMALL model but force only specific variables into the accessible external RAM by declaring them as XDATA. Here they can be observed for out of range values etc. during real time operation.

Summary

To summarize: with the need to test real time systems in real time to get valid results, program variables will have to be made visible so that they can be checked. Being pragmatic about the side effects of this, unless the silicon manufacturer allows the internal memory/register file to be accessed, there is always going to be some overhead involved in retrieving program data on the fly. However, the method outlined goes as far towards the goal of real time dynamic software testing as is likely to be achieved for 8051 family microcontrollers.

The Link Between Hardware And Coding Approach

With applications coded in assembler, the memory model that is used is really very much embedded into the software and reflects fully the physical memory configuration of the hardware. For instance, if the programmer is going to use an external RAM chip, a conscious effort must be made to use MOVX @DPTR instructions when moving data around. If the program is written using solely the internal memory, to convert it to use external RAM (should more space be required) would be a very major undertaking - all the move indirect/direct instructions would have to be converted to include the loading of DPTR and the use of MOVX @DPTR.

Such an exercise is almost impossible given a program of any size, or at the very least extremely laborious. Really in this sense, the software is as rigid and inflexible as the hardware design. If the software is designed to use only internal RAM, it is almost as difficult to convert it to use external RAM as it is to change the PCB design to add the RAM chip to the hardware in the first place.

Coding For A Variety Of Memory Models

A new possibility has been opened up in this area for engineers using C compilers such as the ones from Keil or Raisonance. These compilers will support many different memory models - the memory model being determined by just a software switch at the compilation stage. It now becomes extremely simple to change the memory model assumed by the software during development. The memory model used is no longer implicit in the nature of the program instructions used by the programmer.

An example of how this memory model switch works is as follows: if the SMALL model is selected, the compiler will assume that no external RAM exists and all function parameters, variables etc. will be located within the internal directly or indirectly addressable RAM segments of the microcontroller's memory. The large model, if selected as a global model, will cause all variables and parameter passing areas to be forced into external RAM, where they will be addressed by the MOVX @DPTR instruction.

The stack, as has to be the case with the 8051, always remains in internal memory. Typically in expanded 8051 systems, the external RAM will be used for parameter passing and variables, constituting the LARGE model for the C compiler. The stack now has a free run of the internal memory. There is, unfortunately, a penalty in going to the large model, as within the basic 8031 there is only one DPTR that can be used for moving data in and out of external memory devices, so any addressing of an external variable will require the loading of the DPTR and then the moving of the data to the target location, via the accumulator. There is a COMPACT model which will actually allow paged memory to be used. This is actually slightly better in that external memory movements are made with respect to one of the eight general purpose R0 - R7 registers. This, in actual fact, does not require the constant loading and unloading of the DPTR and so probably represents the best way of using external RAM.

Variable Characteristics In Assembler And C Programs

The upshot of all this is that a program which is initially written for the SMALL memory model can at the flick of a software switch be recompiled to produce a totally different set of 8051 instructions that can now make use of external RAM. This, as has been said, has not been possible with assembler - assembler programs carry their memory model with them at all times and it cannot change. With the C compiler route, the memory model may be changed at will. This ease of conversion opens up an interesting new debugging/ testing possibility that has been well explored by the authors.