The ADC0808 A/D Converter

The A/D Converter we will use is the ADC0808 by National Semiconductor. This converter is 8 bits and has a microprocessor bus interface and single +5 power. It also has an 8 input analog multiplexer. The interface requires 6 lines in addition to the data bus. These are the 3 mux input select address lines and 3 control lines. The 3 mux address lines are connected to data bus bits 0 through 2. So that a nibble (0 - 7) written to the A/D will select one of the eight inputs. There is a bus output enable line for reading the A/D and a End of Convert line that signals the host that the conversion is complete. Lastly there are 2 more lines that are tied together and connected as 1 to the host. This is the Start/ALE line. By placing a nibble (0 - 7) on the data bus and setting the Start/ALE line to a one, the input mux selection is written to the A/D. When this line is taken back low, the conversion process starts. The EOC line is driven low. Within a few milliseconds, the conversion is complete and the EOC line is driven high. The OE line is set high and the result is read from the A/D data bus. The OE line is set low, disabling the A/D output data bus. This completes one cycle of the A/D conversion process.

There are 2 other lines that are tied, one each to the Vcc and Gnd pins. These are the +Vref and -Vref pins. The A/D conversion is done based on the ratio of the input to the Vref pins. In other words, if +Vref is +5.00 volts, then each bit is worth 5.00/256 or 19.53 mv (millivolts). The more accurate voltage for +Vref ( in terms of measuring a voltage) would be 5.12 which would yeild a 20 mv / bit resolution. We could then just multiply the value of the conversion by 20 mv and have the actual voltage on the input, accurate to + or - 20 mv. This would result in a 5.1 volt full scale reading if the binary value is FFh or 255d). So full scale is 1 bit less than +Vref. Also +Vref can't be more positive than Vcc and -Vref can't be more negative than ground. So to have a 5.12 volt +Vref, you would have to have a +5.12 volt Vcc.

In this prototype, I just tie the Vref to Vcc and the -Vref to Gnd and take the loss of accuracy in favor of simplicity. I use 20 mv for the value of each bit which results in an error of about 2.35%. In most logic families, it doesn't hurt to use a Vcc of 5.12 instead of 5.00, and can actually be up to 5.25 volts in most cases without damage. So if you wanted better accuracy you could make your system power voltage adjustable, and set it to 5.120 volts.

If you are measuring the position of a potentiometer, with either end tied to Vcc and Gnd and the wiper as an input, then it doesn't matter what the +Vref value actually is, since you are now looking for a ratio, not an actual value. You want to know the POSITION of the wiper, relative to either end, not it's actual resistance or voltage. This is what the ADC0808 was designed to work best with and it's called ratiometric.

Here are the steps in order and what each does. Initially the EOC line is high, the OE line is low and the Start/ALE line is low:

1. A mux selection (00h) is placed on the A/D bus
2. The Start/ALE line is set high.
3. The Start/ALE line is set low
4. The EOC line is monitored until high
5. The OE line is set high
6. The converted results are read from the A/D
7. The OE line is set low.

Line 1 - Bits 0 thru 2 are the mux select lines. So to select, say input 0, a 00h is placed on the bus. To select input 7, a 07h is placed on the bus.

Line 2 - Setting the Start/ALE line high, strobes in the mux select to the A/D. At this point what ever voltage is being applied to input 0 is now connected to the A/D converter input.

Line 3 - Resetting this line starts the converter converting. Shortly after this the EOC line goes low, indicating busy.

Line 4 - The EOC line is monitored until it goes high, indicating conversion complete.

Line 5 - The OE line enables the A/D output data bus when it is high and disables it when it is low. So setting the line high here enables the A/D output bus.

Line 6 - The converted value is read into the DS5000.

Line 7 - The A/D output bus is disabled (tri stated, or turned off), and the conversion process is complete.

Tri State Bus

To digress just a moment, I haven't done that for a while, I need to discuss a general point about buses and tying several things to the same point. If I have 2 regular switches and I tie their contacts together (parallel) so that I can monitor both switches with two points, if both switches are open, then the output is open. If one of the switches is closed, the output is closed. If the other switch is now closed, no change is seen on the output, since the output was already closed by the first switch. The point here is that when you tie two things together, there can be a conflict between the two things, if they both try to signal at the same time.

There are a couple of ways around this problem. One is to put each thing on a separate connection. This solution requires a lot of hardware to work. The DS5000 only has about 24 pins that you can connect something to. While this may seem like a lot of connection points, if you wanted to connect the display to its own lines, and the A/D to its own lines, you would need 11 for the display and 11 for the A/D. That's already 22 and you haven't connected a single switch yet.

The other approach is to use a tri state data bus. In normal logic, you have two possible states, a 0 and a 1. Tri state adds a third level, called off. Off means just that. When the bus is disabled, it is in the high impedance (high resistance) state, or turned off, for all practical purposes, disconnected. While you still have the original limitation, you can't be reading from both at the same time, you can share the bus between the display and the A/D by reading each at a different time, one after the other.

This is what we do. There are 8 pins on each of the DS5000, the display, and the A/D chips that are all tied together, in a seemingly impossible situation. But the connections are only mechanical. Inside each chip are another set of switches that connect the mechanical pins of the chip, with the logic inside. If these switches are open, the chip is, in effect, disconnected.

There are other lines, the 3 mux select lines on the A/D that could have required 3 more lines, had we not tied these to the data bus. These are inputs, not outputs, so the problem of conflict doesn't enter in here. Adding these lines to the data bus have no practical effect on the data bus operation. Since normally the display and the A/D chips have their bus drivers turned off (boy it gets harder to keep separate, logic buses and riding buses, we have logic bus drivers now). We simply use the bus for different things at different times.

At the time we are placing the 00h on the bus to select input 0 on the A/D, the Start/ALE line is what makes use of the bus at this time. It strobes the 00h into the A/D chip. The display chip is being a display and could care less that the A/D is using the bus. A more elemental way of looking at the problem is that on a bus, the problem of conflict arises when more than one device is trying to TALK on the bus at the same time. The problem of conflict never comes from how many "listeners" there are on the bus.

So the necessary evil that comes along with buses are control lines. In our case 3 lines for the display and 3 lines for the A/D. So far, we've saved ourselves 11 connections that would normally have to be made with separate lines, the 3 mux select lines and the 8 data bus lines from the A/D. We have used 14 lines to make 25 connections between the DS5000 and the display and A/D chips. We will ultimately add a keyboard to the system that will add 1 new control line, and use all the data bus lines. Then we will have used 15 lines to make 34 connections.

Buses are terrific things, but they do have their drawbacks. Any time one of the chips that have bus drivers fails in a way to leave drivers stuck on, through some hardware failure, or software glitch, none of the other devices on the bus can communicate over the bus. In essence, there is a loudmouth, screaming at the top of his lungs, in a small, crowded room, so that no other communications can take place in the room. It's a sacrifice made with all due diligence and forethought. If the bus "breaks" you fix it.

Now for a little about p0 on the DS5000. We are using p0 as the peripheral data bus for the display, A/D, and eventually a keyboard. Any time we (the DS5000) writes to p0, we must follow the operation with an instruction to write all 1's to p0. This leaves p0 in it's tri state condition. The outputs of the DS5000 are high enough resistance that other outputs can override the DS5000 outputs. But this only works if all the outputs are high (hence the pull up resistor), and some one wants to bring one low. It won't work if the line is low and some one wants to bring it high. It will stay low (or worse, burn up a chip!). It's like the switch analogy. If both switches are closed, and you open one, the output is still closed by the other switch, so no change occurred on the output. Both switches have to be open for any one switch to communicate.

P0 is also unique amoung the DS5000 ports in that is is an open collector output, as opposed to totem pole. In my design here, the display has enough of a pullup on it's inputs, that it pulls up p0 so that no external pullups are required. If you don't use a display, to be safe in any case, you should pull up all 8 lines with a 10K resistor to Vcc. I don't show any on my schematic, and, indeed, I don't use any on my prototype either. The other DS5000 ports are totem pole outputs, that don't require any pullups.

Back to the ADC0808. Here is the program as it will exist in our system software. The bit adec is the EOC (end of convert) pin of the ADC0808. The bit adst is the Start/ALE input pins of the ADC0808. Bit adoe is the A/D output enable pin.

adcnv:
wait2:   jnb   adec,wait2  ;wait until EOC is a one
         setb  adoe        ;enable A/D output bus
         mov   a,p0        ;get converted value
         clr   adoe        ;disable A/D output bus
         mov   a,#h'00     ;set mux select to input 0
         mov   p0,a        ;put mux select on the bus
         setb  adst        ;set Start/ALE to a one (this strobes in the address)
         clr   adst        ;clear Start/ALE to a zero (this starts the conversion)
         mov   p0,#h'ff    ;set p0 to tri state bus

This example is only converting input 0 and never converts the other 7 inputs. Otherwise it is exactly as it will be in the final software. This pretty well covers the display and the A/D converter.

Now for something on how our system is going to operate. As I've mentioned in another lesson, there are two basic ways to have events signaled to the system. One is to poll all the devices, getting their status. Another is to have an output, like EOC on the A/D chip generate an interrupt. We could have done that ourselves. But the DS5000 only has 2 external interrupt inputs. One of these I like to dedicate to a "Break" switch, for trouble shooting purposes. So without adding any additional hardware to expand these, I only have one external interrupt to use for all our interfacing needs. Not much to work with.

For our needs, this is quite sufficient. What we will do is to exercise the third option, and that is to have a combination interrupt driven polled response. The X-10 computer interface module has an output for the host, that is an optic coupled, 60 Hz square wave, in sync with the 120 VAC power. We will double this to 120 Hz, for reasons I'll explain when we get into X-10 interfacing. The parts U2, D9, and R20, along with the two loose wires from the power supply that we haven't connected yet, are used to make a substitute for this feature until we actually connect the TW523 interface. These parts also make an optic coupled, 60 Hz square wave output, synched to the 120 VAC power.

In either case, the system operation is identical. With this arrangement, we will get an int0 interrupt 120 times a second. This is exactly the rate needed to accommodate the X-10 interface, with the least hardware. During each iteration of this interrupt, several things are going to happen. First the A/D is serviced, then the display is serviced, and eventually a keyboard will be serviced. Also the X-10 is serviced.

The other interrupt that could be happening regularly is the RS-232 serial port. When a character is received on the serial port, an interrupt is generated that says that the receive buffer is full. It is the responsibility of the program to get to the serial buffer and read the contents before the next character comes in. If you don't, chaos will most surely erupt.

The DS5000 takes 12 clock cycles to make an instruction cycle. This makes the 11.0592 MHz clock turn into 921,600 instruction times or cycles per second.

With 9600 as the baud rate, there are 960 instruction cycles between characters, if characters were coming at the maximum rate, or worst case. There are 7680 instruction times between each 120 Hz interrupt. Since we never disable any interrupts after startup, and an interrupt can occur and be serviced, while inside another service routine, there shouldn't be any problems.

The things to consider when doing much of the software inside an interrupt routine, is how much of the total supply of instruction times will be used up by the interrupt, and will this be too many to allow the operating loop to have some crunch time. The thing to remember, there are a finite number of instruction times in each second of time. In our case, there are 921,600 instruction times in each second. We are going to have two different pieces of software running all the time, one the Operating Loop, another the 120 Hz interrupt routine, along with any characters sent or received on the RS-232 link.

One thing that I like to do is to calculate shortest and longest execution times for all the different routines, and keep a list of them, to reference when trouble shooting. This takes a while to compile, but it is priceless if it's needed.

Back to our system. All the polling of devices on the tri state peripheral data bus will be done during the 120 Hz interrupt routine. The operating loop will not directly communicate with these devices. This communication will take place through the interrupt routine, using flags and buffers.

The external data ram is used for the buffers needed to make all this work. The DS5000 has only 128 internal ram locations, which run out fast with large buffer sizes. But we have "more ram than we can shake a stick at", in external data memory.

The display has a buffer here that is 128 bytes long. If the operating system wants to display something, it is written to this buffer. During the 120 Hz interrupt routine, one line of the display is refreshed. So in 4 iterations of the 120 Hz routine, the display is completely updated. Or another way of saying it, the display is updated at a rate of 30 Hz. I first tried to update the entire display each 120 Hz cycle, but the next interrupt would occur before I finished servicing the previous interrupt, in other words, I ran out of time. It took longer than 1/120th of a second to update the entire display. I chose 1 line per cycle, for a fast real time display rate and less overhead on the host.

During each iteration of the 120 Hz interrupt, one point is converted by the A/D converter. In 8 iterations of the 120 Hz routine, all the inputs will have been converted. Another way of saying this is that the A/D points are scanned at a rate of 15 Hz, or 15 times a second. The 8 readings are stored in a buffer that is read by anyone wanting to know a reading. The reason I chose this approach is that the A/D is in the process of converting a point, almost all the time. You hardly have to say anything to him, he just works. By doing it this way, there is very little overhead to scanning the A/D points into the system memory. The A/D is left mostly waiting around for that poky old DS5000 to get around to giving him his next job.Of course that inconsiderate ungrateful A/D doesn't realize that the trusty DS5000 is taking care of a lot more than just A/D.

While all this A/D and Display work is goin' on, there are X-10 commands to send and receive, PC commands to be honored, at the earliest possible opportunity. In addition a security system that can turn on an X-10 device (horn for instance) to wake the dead when needed. A real time clock keeps up with scheduled X-10 events and watches for scheduled tasks to do, like start the coffee pot, or lower the thermostat, turn off the kids TV maybe.

I am looking for a good Phone interface that would allow the system to be expanded to the phone system, without going through the UL and FCC listing process to get an acceptance sticker. When I do, we will add that to our arsenal. I know the devices exist, I just have to get one and start playing with it. With that we can add dial in control and status features. With a couple of other chips, we can add voice control, 900 Mhz RF communications and 28.8 Modem interface.

Other cool interfaces would be a sonic range finder, synthesized voice playback, IDE hard drive interface, WIRELESS BRAIN WAVE TRANSCEIVER!!....... Maybe we're goin' a little too fer here, partner. I don't think so.

I'll let you in on a little secret. One of my pet projects (that's those you never get to) is to get a model airplane (10 ft wing span) and mount one of those little digital camera's in the nose, have a 900 Mhz radio link with video down link and stick, throttle, flaps, and rudder controls, and have, on the safe end of this scheme, a chair with a monitor and joysticks to watch and control the plane. I love simulators but none are really like flying yet, with real time frame rates and infinite graphics. This would be really cool, and fun to fly. You could have an air speed indicator and GPS to keep your course and maintain safe operating distance from the tower. If the plane got too far away, the GPS would guide the plane back to the tower and go into a slow bank around the tower, 100 ft off the ground.

Of course if you got that to work, the better one would be a glider, with a small engine to get it launched. You would need a gravity meter, to detect up and down drafts and maybe a level indicator, along with an altimeter and real time audio from plane. You could fly this from your car, sitting on a hill.

Another might be a sailboat, 8 ft long or so, with motorized jib and main, tiller, camera's, tilt meter, water speed, and movable ballast. Something on the order of an America's Cup boat.

Another would be some kind of an air boat, or hovercraft. The ultimate, in my mind, is an autonomous submarine that could be piloted from a boat above. I'm not sure how to do the link between the sub and the surface. I've never played with communicating through water. But I'll bet it can be done.

Well, enough of imagination for now. To digress another moment, I need to speak about certain "tricks" to help in writing software drivers, or interrupt service routines. There is a method of smoothing out digital or analog inputs to the system. For digital inputs, switches, contacts, etc., the process is called "debouncing".

The term debounce comes from the dry contact world. It is to remove the bounce from a contact. If you drop a ball to the floor, it doesn't stick to the floor the first time it contacts it. It bounces back up and then down again and then up, each time getting closer to sticking to the floor. After several bounces, the ball comes to rest on the floor. You started with the ball not touching the floor, and then several bounces later, the ball is touching the floor.

The same thing happens in a relay, switch, or any other contacts that come together with a force driving them together. In the case of the ball, gravity was driving the ball together with the floor. In a relay, the energized coil, creating an electromagnet, drives (pulls) the contacts together. The same thing happens in the relay or switch as did with the ball. The contacts bounce together several times before they touch continuously.

To debounce a contact, the input is sampled several times, and only then is it recognized as having changed. To hang a cap on a noisy line is somewhat the analog electronic equivalent of debouncing the line. The cap eats up spikes, hopefully, cleaning up the line.

I normally write a routine that scans all the inputs at once. Usually I arrange the hardware so that all the inputs are in one port. Then I read from that port, masking out any bits that aren't inputs, and then comparing the current status with the previous status, when you scanned it last.

You are looking for a change between the previous, or old status, and the current status. As the micro is powering up, the inputs are read and stored as old status, to be used for all the succeeding scans of the inputs.

I always have an operating loop, that continuously scans the inputs, in a never ending loop, and then jumping out to a routine associated with a particular input, when it is sensed and debounced. After the routine is finished, I return to the loop, to stay there, until another input has changed.

Here is a generic input scan routine, that will read and debounce the 8 inputs in port 3 of the DS5000. This code uses a location for old status (ostat), a location for current status (cstat), a location for debounced changes (dbchg), and 8 locations starting at the label dbctr that are the 8 debounce counters, one for each input. All this code is shown in the proper sequence, even though there is descriptive text in between each part.
 

This code reads the inputs and stores it as old status.

        mov     a,p3               ;read current status
        mov     ostat,p3           ;and store it as old status

This is the start of the operating loop. This code reads the inputs and compares them to the old status, generating any changes as a 1's. If there isn't a change on an input, that bit will be a 0.

begin:  mov     a,p3               ;read current status
        mov     cstat,a            ;and store as current status
        xrl     a,ostat            ;generate changes

This code goes thru all 8 inputs and either increments the associated debounce counter if there is a change, or clears the counter if there isn't a change.

        mov     r0,#dbctr          ;set to start of debounce counters
        mov     r3,#h'08           ;set for 8 debounce counters
n0002:  rrc     a                  ;rotate bit of change into carry
        jnc     inc00              ;if bit is a 0 (no change) then goto inc00
        inc     @r0                ;increment debounce counter
n0003:  inc     r0                 ;step counter pointer
        djnz    r3,n0002           ;if not last then n0002
        sjmp    c0002              ;goto c0002
inc00:  mov     @r0,#h'00          ;zero debounce counter
        sjmp    n0003              ;goto n0003

This code goes thru all 8 debounce counters to see if any have reached a count of 16. If one has reached 16, that bit is set in the debounced changes byte (dbchg) and the debounce counter is cleared. Otherwise the bit in the debounced changes byte will be zero and the counter left untouched.

c0002:  mov     r0,#dbctr          ;get start of debounce counters
        mov     r3,#h'08           ;set to check 8 status inputs
n0004:  mov     a,@r0              ;get debounce counter
        cjne    a,#h'10,onexx      ;if count not = 16 then goto onexx
        setb    c                  ;set carry flag
        mov     @r0,#h'00          ;zero debounce counter
twoxx:  mov     a,dbchg            ;get debounced changes
        rrc     a                  ;and shift in
        mov     dbchg,a            ;into storage
        inc     r0                 ;step debounce counter pointer
        djnz    r3,n0004           ;if not last then n0004
        sjmp    e0000              ;goto e0000
onexx:  clr     c                  ;clear carry
        sjmp    twoxx              ;goto twoxx

What you have now is a byte (dbchg) representing any changes that have been debounced. There is more that one way to proceed from here, but here's one way. I am only showing the code for the first 3 bits (bit 0 thru bit 2) but it's the same for the rest.

e0000:  mov     a,dbchg            ;get debounced changes
        jnb     acc.0,e0001        ;if bit 0=0, goto next input
        ljmp    rout0              ;otherwise goto routine 0
e0001:  mov     a,dbchg            ;get debounced changes
        jnb     acc.1,e0002        ;if bit 1=0, goto next input
        ljmp    rout1              ;otherwise goto routine 1
e0002:  mov     a,dbchg            ;get debounced changes
        jnb     acc.2,endlp        ;if bit 2=0, goto next input
        ljmp    rout2              ;otherwise goto routine 2
endlp:  ljmp    begin              ;end of operating loop

This code represents the routine that will be executed as a result of an input change on input 0. The nop represents all the code that you would have for servicing input 0.

rout0:  nop                        ;this represents the routine

This code complements the old status bit 0. This makes the old status equal to the current status, so that when we return to the scan loop, no changes will be detected until bit 0 changes states again.

        mov     a,ostat            ;get old status and complement
        xrl     a,#h'01            ;old status bit 0
        mov     ostat,a            ;and store it
        ljmp    e0001              ;done, goto next input

This code is identical to rout0 except for this being for bit 1.

rout1:  nop                        ;your code
        mov     a,ostat            ;complement old status
        xrl     a,#h'02            ;bit 1
        mov     ostat,a            ;and store it
        ljmp    e0002              ;done, goto next input

This code is identical to rout0 except for this being for bit 2.

rout2:  nop                        ;your code
        mov     a,ostat            ;complement old status
        xrl     a,#h'04            ;bit 2
        mov     ostat,a            ;and store it
        ljmp    endlp              ;done, goto end of loop

So what all this has done is to debounce an input and execute some routine to service it. A change on an input has to be there for 16 CONSECUTIVE scans before it is recognized as valid. If an input doesn't have a change, it's debounce counter is always cleared. When a counter has reached 16, it is cleared and a bit is set in the debounced changes byte to reflect that that bit has changed and is valid.

Next, the routine for that bit is executed and at the end of this routine, the old status for this bit is complemented, making it the same as the current status. Then the next input is checked. Now when this bit is scanned and checked against the old status on the next scan, there won't be a change generated for this bit until it changes states again for 16 CONSECUTIVE scans.

This not only debounces a contact, but increases the noise immunity on any input. This should eliminate, altogether, any problems with noise on your inputs. I used a count of 16 but you can use any count value up to 255 or down to 1.

The comparable thing for analog inputs is called averaging. There are many formula's for performing the averaging, but I use a simple one, that probably has an official name, but it escapes me right now.

What I normally do is to have a memory location for each analog input, containing the current value used by the operating system. As each input is scanned and converted, the new value is added to the old value and divided by two, and then stored back in the memory location. This has the effect of filtering and smoothing the input, at a minimal software impact. This isn't the most complicated way of filtering, or the most accurate and responsive, but it is very simple and takes very few clock cycles to execute.

Here is an example of this. This uses port 3 as the a/d value and temp as the current reading.

filter:  mov a,p0           ;get the conversion value
         add a,temp         ;add it to the current value
         rra                ;divide it by two
         mov temp,a         ;store as current

This code reads port 0 for the value. That value is then added to the current value in memory. This result is divided by two, by shifting right one bit position. This is stored in memory as the current value. Depending on the impedance of the input, I sometimes also hang a .1 uf capacitor to ground at the chip input, to add some extra filtering.

At this point I am placing the final software source file for this course, mostly for those who are wishing to progress at a faster pace. I spent several months debugging this code and refining it. I only ask that if you use it in whole, or in part, for a commercial product, that you keep me in mind. In the next lesson we will talk about X-10 and home control over existing power lines and the interface we will use to connect to the TW523 X-10 Power Line Computer Interface. This interface provides an isolated connection to the power line, that all but eliminates any transients from getting into the micro. It is opto isolated in both directions and provides over 1000 volts of isolation.

My home page is http://www.hkrmicrop.com/personal/index.html .

On to lesson 16.