8096 Based Digital Storage Oscilloscope

Eric Horton
Dedicated Computer Systems

Summary
In this article the development of a digital storage scope using a micro-controller with on board A/D Converter is described. Software coding is provided to demonstrate the use of 'C' as well as the combination of 'C' with assembly language to improve the operating frequency range. A PC is used for display of the captured waveform.

The best method to demonstrate the capabilities of the high performance 16-bit 8096/80C196 micro-controller, is with a useful working example for which it is well suited. This article describes the design of digital storage oscilloscope requiring a minimum of external hardware components utilizing the on-chip peripherals of 8096. The programming of a micro-controller system using the C language is also described. In one of the programs some assembly language is mixed with the C language to provide increased program execution speed in specific sections of the application code. An important goal of projects such as this, is to minimize the time required to go from concept to prototype testing. The combination of a highly integrated micro-controller and a high level language like C can be instrumental in reaching this goal.

Storage Oscilloscope
The oscilloscope process begins by sampling an analog input at regular timed intervals and converting each measurement to eight bit digital values which are temporarily stored on the micro-controller board. After all the samples have been collected the resulting block of data is uploaded to a PC via a serial link. The PC displays the data graphically on its' screen. This design is very representative of typical data acquisition systems where a uniform time series of samples is gathered and then uploaded to another system.

The variations in the design of these system designs are often in the format of the trigger logic and the rate at which the samples are taken. Some systems begin sampling immediately following power-up, while others may wait for a specific time before starting to sample, and others must wait until a specific trigger condition is satisfied before beginning to collect samples.

A/D Converter
The 8096 analog to digital section contains a 10-bit successive approximation converter which outputs its results in two data registers, one is a byte-wide register AD_RESULT_HI, which holds the most significant 8-bits and the other AD_RESULT_LO contains the two least significant bits. The analog input section of this micro-controller contains a sample-and-hold circuit and an eight channel multiplexer, both these are integrated onto the chip to minimizing the external analog circuitry.

Reference Voltage
The A to D reference voltage power supply is shared between the analog and digital section of the A to D unit and must be have operating range between 4.5 to 5.5 volts. In a typical application this voltage is provided by a separate low noise power supply. The resulting voltage resolution per bit is 5 millivolts when the reference voltage is 5.12 volts. The precision of the reference voltage is less critical when using ratiometric analog devices such as potentiometers, strain gauges, or when the analog resolution is reduced to 8-bits. Any analog input quantities must be between zero and this reference voltage level, therefore AC inputs are normally offset by a voltage of one half the reference level.

Conversion Rate
Upon completion of its' conversion, The A to D unit sets an interrupt request flag in the INT_PENDING register. This flag is tested by the program while waiting to store the results of each measurement. The A to D conversion time is dependent upon the state time of the particular member of the 8096/80C196 family being used, this timing is also dependent upon the clock crystal frequency selected. The 8096BH version operating at 12 MHz requires 22 microseconds to perform a 10-bit conversion, the 80C196KC version is able to short cycle the A to D converter and produce an 8-bit result in 9.8 microseconds when operating at 16 MHz. The conversion times and resulting sampling frequencies dictate the upper frequency resolution of the input signal being viewed. In order to clearly see the details of a particular waveform typically requires a minimum of ten samples to be taken during each cycle of the highest frequency to be viewed. Therefore the upper viewing limit of a 12 MHz BH system is approximately 5 KHz. This sampling requirement for viewing data obviously differs from the Nyquist criteria of two samples per cycle required to reproduce the original signal.

This project also uses the graphics capabilities of a PC to display the measured waveforms. The common graphics resolution available on PC include EGA at ( 640 x 350 ), Hercules graphics with ( 720 x 348 ) and VGA at ( 640 x 480 ). The selection of 8-bit values resulting in 256 pixels of vertical resolution is a reasonable choice and keeps the software simple and fast. To fully utilize the available horizontal resolution a total of 640 samples is chosen to match the screen width.

The high speed output (HSO) section of the 8096 is used to control the precise timing of each data sample. This section is designed to perform several different timing or counting functions with very little involvement of the CPU. For this particular application the HSO is programmed to initiate the sampling and the analog conversion at a precise time.

Timer1 is a 16-bit timer being clocked each state time as generated by the on-chip crystal oscillator, for a 8096BH at 12 Mhz it is incremented each 2 microseconds. A 16-bit value is written to the HSO_TIME register which is the input to the content addressable memory. When a value in the content addressable memory matches the value of TIMER1 then the associated command in register HSO_COMMAND is executed.

For this application the selected command starts the A to D process. A uniform time series of samples can therefore be acquired by writing values to the HSO_TIME with each subsequent value increasing by the amount of the sample interval expressed in TIMER1 ticks. The calculation of the next setting for HSO_TIME is performed using unsigned 16-bit math which correctly compensates for TIMER1 overflowing from FFFF hex to 0000 at the end of its cycle. The largest sample interval possible using this method corresponds to a full timer cycle of 131 milliseconds for 12 MHz.

These examples are written in small C which is a subset of full C , small C does not directly support structures, unions or floating-point math which are not always needed in embedded system applications where the 8096 is most effective. Very compact and efficient code for the 8096 micro-controller is produced by this compiler. The special function registers which control the on-chip peripherals can be programmed using C with this compiler. These registers are referenced as external variables using labels which match those found in a file called ' startcx.asm '. By selecting clear variable labels and writing with clear and logic code, the C language can be reasonably self-documenting. A single line of C code normally generates four or more lines of assembly code which helps to reduce programming time in C when compared to using assembly language.

Software

Program #1 is a simple oscilloscope program which is written totally in C. The included files "conio.asm" and "hexio.asm" provide the character serial communication functions putchar(), getchar(), and hexidecimal characters communication putbyte(), getbyte() and getword(). All of these functions are used to communicate with the PC . The various commands, data masks and constants are defined once and then may be referred to by name alone in the code. The several special function registers required for this program are referred to as ' extern ' variables. Most of the global variables used in this program are located in the internal data RAM of the 8096. The array ' sample[] ' needed to hold the 640 data samples is located in the off-chip RAM of the development board. The memory location for the array is directed by the included file called "data.mem".

This C program only executes the oscilloscope task and therefore remains in the ' while(1) ' loop forever. The 'ZZ' characters which are transmitted to the PC provide the synchronizing handshake between the PC and the 8096. After the PC replies with a ' Y ' character and the new settings for the sample interval and input triggering level the 8096 begins testing the input signal. Following a valid trigger, the data samples are collected and send to the PC, the ' ZZ ' is then send once again. The 'ZZ' is not a valid hexidecimal character and can be easily detected by the PC as the sync characters.

The A to D interface is handled by two functions, 'adc_now()' returns an immediate data measurement and saves the first array element and the function adc_collect() which collects the timed data samples and places each in the array ' sample[] '. The function 'adc_now' also calculates the value of the variable 'timex' in expectation of the trigger condition being satisfied at any time. This variable will be loaded into the HSO_TIME register by the other function adc_collect() as it gathers each measurement and calculates the next value for 'timex' .

When the array is completely filled the data is then transmitted as hexidecimal characters to the PC and the loop begins again. The minimum interval of time between samples for this program written completely in C is 160 micro-seconds. Some of the assembly code as produced by the compiler is shown in program #1A.

The program #1B is part of the code for the PC, it is written in compiled basic, the plotting of the data and detection of the sync characters "ZZ" is shown in this section of code. The BASIC command line-(X,Y) is used to join each data point to the previous point. Because the origin (0,0) of the screen display is located at the upper left corner of the screen in BASIC the value of the data is subtracted from a constant vertical offset.

Although the compiler produces efficient code, an increase in code speed can be obtained by writing the sensitive sections of the code in assembly language and calling these sections from the C code mainline. The timing critical sections of this program are the functions which interface with the A to D converter. Program #2 is a very similar to the first program except these A to D functions are written completely in assembly language decreasing the minimum sampling interval to 40 micro-seconds. The function data_record() does not return to the mainline in C until the array is filled with data. Most of the lines of assembly code match lines of C code in the functions of the first program.

Beyond capturing and retaining the record of a single measurement, another of the strengths of a digital storage oscilloscope is its' ability to capture and display the input signal which was present previous to the trigger event. Program #3 continuously samples and stores the data in the array while testing the logic for a valid trigger condition. The first 200 samples are captured before the program begins to test for a valid trigger level. The array ' sample[] ' operates as a rotating circular file during the time before the trigger event and as the remaining data samples are collected. The element count for the array at which the trigger occurred is saved as the variable ' point ' to allow the data to be transmitted to the PC in the proper order, beginning with the data collect before the trigger was valid. The rest of this program is very much like the previous two examples.

All of these example programs require less than 2K bytes for the storage of the compiled program code. The DCS96 development system used for this project contains a 32K RAM on board and supports an additional bank-switched 128K RAM. By simply increasing the size of the array 'sample[]' a very long record of the input signal can be captured. The resulting data record can represent 50 to 250 consecutive PC screens of 640 samples of one waveform measurement. Further efforts in the PC programming can provide storage and retrieval of sample records as data files which can be compared by multiple traces or mathematical calculations.

It is hoped this article will inspire more interest in the new generation of highly integrated 16 bit microcontrollers such as Intel's 8096/80C196 (referred in this article), Motorola's HC16 and other emerging 16/32 bit microcontrollers as well as the use of high level languages in the programming of embedded systems.


/* mcjscope.c*/
#include "startcx.asm"         /* mcjscope.c */
#include "conio.asm"
#include "hexio.asm"

#define start_adc_now   8     /* adc command for channel 0 */
#define go_from_hso     0     /* adc channel 0 started via hso */
#define adc_start       0xF   /* start adc via hso timer */
#define mask_adc_int    0xFD  /* clear  bit 1  */
#define adc_int_bit     2     /* adc bit in INT_PENDING  */
#define size            640   /* data record size in bytes */

extern unsigned char  AD_COMMAND, AD_RES_HI, HSO_COMMAND, INT_PENDING;
extern unsigned int   TIMER1, HSO_TIME ;

unsigned int interval, count, timex ;
unsigned char trigger_level ;
#include "data.mem"          /*  locates the array sample[] with 640  */
unsigned char sample[size] ; /*     elements in the off-chip memory   */

main()
  {
  while(1)               /*   this scope program loops forever   */
    {
    putchar('Z') ;  putchar('Z') ;  /* transmit 'ZZ'  to the PC  */
    while ( getchar() != 'Y' ) ;    /* waits for  'Y ' from PC   */
    interval = getint() ;           /* get time between samples  */
    trigger_level = getbyte() ;     /* get signal trigger level  */

    while ( adc_now() > trigger_level ) ;  /* wait till below    */
    while ( adc_now() < trigger_level ) ;  /* then above trigger */

    for ( count=1 ; count <= size ; count ++ ) adc_collect () ;
    for ( count=0 ; count <= size ; count ++ ) putbyte( sample[count] ) ;
    }
  }
adc_now()
  {
  INT_PENDING = 0 ;
  AD_COMMAND = start_adc_now ;
  while ( ! INT_PENDING & adc_int_bit ) ;  /* wait for adc to finish */
  sample[0] = AD_RES_HI ;
  timex = TIMER1 + interval ;
  return ( AD_RES_HI );
  }
adc_collect ()
  {
  INT_PENDING = 0 ;
  AD_COMMAND = go_from_hso ;
  HSO_COMMAND = adc_start ;
  HSO_TIME = timex ;
  timex = timex + interval ;         /* calculate next HSO_TIME value */
  while( ! INT_PENDING & adc_int_bit ) ;
  sample[count] = AD_RES_HI ;        /* collect next data sample      */
  return ;
  }

/* mcjscope.asm */
;    interval = getint() ;           /* get time between samples  */
    CLR RCX
    LCALL getint
    ST RAX,interval
;    trigger_level = getbyte() ;     /* get signal trigger level  */
    CLR RCX
    LCALL getbyte
    STB RAL,trigger_level

;    while ( adc_now() > trigger_level ) ;  /* wait till below    */
L_6:
    CLR RCX
    LCALL adc_now
    LD RBX,RAX
    LDBZE RAX,trigger_level
    LCALL C_ugt
    OR RAX,RAX
    JNE *+5
    LJMP L_7
    LJMP L_6
L_7:
;    while ( adc_now() < trigger_level ) ;  /* then above trigger */
L_8:
    CLR RCX
    LCALL adc_now
    LD RBX,RAX
    LDBZE RAX,trigger_level
    LCALL C_ult
    OR RAX,RAX
    JNE *+5
    LJMP L_9
    LJMP L_8
 L_9:
;    for ( count=1 ; count <= size ; count ++ ) adc_collect () ;
    LD RAX,#1
    ST RAX,count
L_12:
    LD RBX,count
    LD RAX,#640
    LCALL C_ule
    OR RAX,RAX
    JNE *+5
    LJMP L_11
    LJMP L_13
L_10:
    LD RAX,count
    INC RAX
    ST RAX,count
    DEC RAX
    LJMP L_12
L_13:
    CLR RCX
    LCALL adc_collect
    LJMP L_10
L_11:


*******  mcjbas.txt  *******

690 PRINT #1,"Y";      ' send a ' Y ' for handshaking with the 8096

700 PRINT#1,I$;        ' send interval value to the 8096 as hex string
705 PRINT#1,T$;        ' send trigger level also to the 8096 board

710 Y$=INPUT$(2,#1)    ' receive the first data sample as a hex byte
720 Y=VAL("&H"+Y$)     ' convert the hex data to decimal for plotting
730 PSET(XMIN,YMAX-Y)  ' the first sample is a single point at time 0

740 FOR I= 1 TO 640    ' receive the rest of the data in correct order
760 Y$=INPUT$(2,#1)
765 IF Y$="ZZ" THEN GOTO 810 ' check for sync character each time
770 Y=VAL("&H"+Y$)
780 LINE -(XMIN+I,YMAX-Y) ' connect each data point to form the plot
790 NEXT I

/*mcjscopa.c  mixed with assembly code*/

#include "startcx.asm"     
#include "conio.asm"
#include "hexio.asm"
#define size 640   /* number of data samples  */

/* the following values are being defined for the assembler */
#asm
.equ adc_bit,       1
.equ mask_adc,      0xFD
.equ start_adc_now, 8
.equ go_from_hso,   0
.equ adc_start,     0x0F
.equ size,          640
#endasm

unsigned int  interval, count, timex ;
unsigned char trigger_level ;

#include "data.mem"
unsigned char sample[size] ;

main()
{
while(1)
  {
  putchar('Z') ;  putchar('Z') ;
  while ( getchar() != 'Y' ) ;
  interval = getint() ;
  trigger_level = getbyte() ;

  count = 0 ;
  data_record () ; /*  the complete triggering and data collection  */
                   /*  process is written in assembly language      */

  for (count=0;count<=size;count++)  putbyte ( sample[count] ) ;
  }
}


 #asm
 data_record:
 lcall adc_now
 cmpb AD_RES_HI, trigger_level
 jc data_record                ; wait until signal below trigger level
 lo_to_hi:
 lcall adc_now
 cmpb AD_RES_HI, trigger_level
 jnc lo_to_hi                  ; wait until signal above trigger level
 adc_loop:
 ldb INT_PENDING, 0            ; only using channel 0
 ldb AD_COMMAND, #go_from_hso
 ldb HSO_COMMAND, #adc_start
 ld HSO_TIME, timex
 add timex, interval
 adc_wait:
 jbc INT_PENDING, adc_bit, adc_wait
 stb AD_RES_HI, sample [count]+ ;  collect next sample
 cmp count, #size               ;  test if array is full
 jne adc_loop
 ret

 adc_now:
 ldb INT_PENDING, 0
 ldb AD_COMMAND, #start_adc_now
 adcn_wait:
 jbc INT_PENDING, adc_bit, adcn_wait
 add timex, TIMER1, interval
 ret

 #endasm
#include "startcx.asm"   /*  mcjscopy.c  */
#include "conio.asm"
#include "hexio.asm"
#define go_from_hso     0     /* adc channel 0 started via hso */
#define adc_start       0xF   /* start adc via hso timer */
#define mask_adc_int    0xFD  /* clear  bit 1  */
#define adc_int_bit     2     /* adc bit in INT_PENDING  */
#define size 640     /* data record size in bytes */
#define pre_trigger 200   /* samples to be displayed before trigger */
#define post_trigger  ( size - pre_trigger )

extern unsigned char  AD_COMMAND, AD_RES_HI, HSO_COMMAND
extern unsigned char INT_PENDING;
extern unsigned int   TIMER1, HSO_TIME;
char   no_trigger ;
int    count, num, point ;
unsigned int   interval, timex ;
unsigned char  trigger_level ;
#include "data.mem"
unsigned char  sample[size] ;

main()
{while(1)
  {
  no_trigger = 1 ;
  putchar('Z') ; putchar('Z') ;
  while ( getchar() != 'Y' ) ;
  interval = getint() ;
  trigger_level = getbyte() ;
  timex = TIMER1 + interval ;
     /* collect the first samples without testing the trigger  */
  for (count = 0 ; count <= pre_trigger ; count++ ) adc_collect() ;

  while ( no_trigger )  /*  collect samples while waiting to trigger */
    {
    if (count > size )  count =0 ;  /*  array wraps around itself  */
    if ( adc_collect() > trigger_level )  no_trigger = 0 ;
    count++ ;
    }
    point = count - 1 ;       /* establish the point of triggering */
    for (num= 0 ; num <= post_trigger ; num++ )
      {                       /* collect the rest of the samples */
      if ( count > size ) count = 0 ;
      adc_collect () ;
      count++ ;
      }
    count = point - pre_trigger ;           /* calculate the start of  */
    if ( count < 0 ) count = size + count ; /* the pre-trigger data    */
    for (num = 0 ; num <= size ; num++ )
      {
      if (count > size ) count= 0 ;   /* transmit the data block to    */
      putbyte( sample[count] ) ;      /*  the PC in correct sequence   */
      count++ ;
      }
   }
}
 adc_collect ()
   {
   INT_PENDING = 0 ;
   AD_COMMAND = go_from_hso ;
   HSO_COMMAND = adc_start ;
   HSO_TIME = timex ;
   timex = timex + interval ;
   while( ! INT_PENDING & adc_int_bit ) ;
   sample[count] = AD_RES_HI ;
   return ( AD_RES_HI ) ;
   }

© 2009 Micro Control Journal. All rights reserved.