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 ) ;
}
|