Analog I/O 2

Why Another Analog IO Tutorial?

The last tutorial taught how to get rudimentary analog input on the PIC and communicate it in a very basic way to a computer. The techniques in that tutorial, however, don't allow for using more than one sensor, or inputting it into a standard terminal. This here tutorial will build on what we did previously to enable better formatting of the output. It will also help us utilize the full 10 bit resolution of the ADC. Lastly (though I probably won't present them in this order) I will introduce the VERY useful world of macros.

Formatting

The first thing we need to do is format text as ASCII. ASCII is a set of numeric codes that represent print characters. Look at http://www.asciitable.com for the table of ASCII definitions. If you take a look at the table you can see that the numbers all start at 0x30 and go sequentially from there. This makes it very easy to convert a decimal number to its ASCII representation, you just add 0x30.

Macros

I'll use this opportunity (converting decimal numbers to ASCII) to introduce macros. Macros are what you're invoking every time you call an instruction: movf is a macro, so is call, so are all the other instructions. When you invoke a macro the compiler just replaces the invocation with the macro instructions. Think of macros as a kind of shorthand--rather than rewriting a procedure every time you just invoke a name that implies the procedure. Macros are different from subroutines in that the compiler will replace each invocation with the text of the macro, making the amount of memory used proportional to the number of times the macro is called, whereas a subroutine only goes into memory once, since it's basically a kind of goto statement. The advantage of macros is that you can pass them argument, while subroutines must assume the placement of the operands--which is usually W, but when more operands are needed, they must be stored in other variables.

Macros are easy. They start with the name of the macro in the first column, followed by the word MACRO and then a list of the arguments, delimited by commas. The macro is ended by the endm statement and in between goes the procedure. When you need to operate on one of the arguments you just use its name as a variable. Here's a simple macro that adds 0x30 to a number x and stores the result back in x. This is all we need to convert a number to its ascii equivalent. Note that the arguments are read as memory addresses, like pointers, rather than having their values copied into local variables.

ENCDA MACRO x
                movlw   0x30                    ; convert to 0-9 in ASCII
                addwf   x, F
        endm

Now any time you want to encode a number into its ASCII representation you call ENCDA number and the number is encoded and stored in itself.

Now Really. Formatting

Now that you can easily ASCII encode a number, you need to be able to convert the two registers ADRESL and ADRESH (the ADC results) into a decimal number, because that's what we want to see on the other end. Luckily there's a subroutine that does just this in the Microchip Picdem 2 Plus Demo Board. This assumes that there are 5 holder variables already created: Tenk, Thou, Hund, Tens, Ones. It also assumes that ADRESL has been moved to a variable NumL, and ADRESH has been moved to NumH. Here it is:

; Binary (16-bit) to BCD. From Microchip PICDEM 2 PLUS DEMO BOARD
; xxx = highest possible result
bin16_bcd:
                ; Takes number in NumH:NumL
                ; Returns decimal in TenK:Thou:Hund:Tens:Ones
                swapf   NumH,W
                andlw   0x0F
                addlw   0xF0
                movwf   Thou
                addwf   Thou,F
                addlw   0xE2
                movwf   Hund
                addlw   0x32
                movwf   Ones
                movf    NumH,W
                andlw   0x0F
                addwf   Hund,F
                addwf   Hund,F
                addwf   Ones,F
                addlw   0xE9
                movwf   Tens
                addwf   Tens,F
                addwf   Tens,F
                swapf   NumL,W
                andlw   0x0F
                addwf   Tens,F
                addwf   Ones,F
                rlcf    Tens,F
                rlcf    Ones,F
                comf    Ones,F
                rlcf    Ones,F
                movf    NumL,W
                andlw   0x0F
                addwf   Ones,F
                rlcf    Thou,F
                movlw   0x07
                movwf   TenK
                movlw   0x0A ; Ten
Lb1:
                decf    Tens,F
                addwf   Ones,F
                btfss   STATUS,C
                bra     Lb1
Lb2:
                decf    Hund,F
                addwf   Tens,F
                btfss   STATUS,C
                bra     Lb2
Lb3:
                decf    Thou,F
                addwf   Hund,F
                btfss   STATUS,C
                bra     Lb3
Lb4:
                decf    TenK,F
                addwf   Thou,F
                btfss   STATUS,C
                bra     Lb4
                return

I made sure to clearly mark the assumptions it makes, which I think is a good idea any time a subroutine uses a variable beside W.

Previously we just put ADRESL in W, which only allows us to play with the least significant 8 bits of the A/D conversion. Since bin16_bcd assumes NumL and NumH, let's change our ADC_Get function to put ADRESH and ADRESL in NumH and NumL, so the whole sub looks like this:

; Gets the analog value of a pin and puts it in NumH and NumL
ADC_Get
                bsf             ADCON0, GO      ; start conversion
adc_wait                                        ; don't move on until the conversion is done.
                btfsc   ADCON0, DONE
                goto    adc_wait
                movff   ADRESH, NumH
                movff   ADRESL, NumL
                return

Transmitting the Data

Transmitting the data is pretty simple. We'll send the TenK digit, then the Thou digit, etc. We also want to label each number put out so that we could have more than one number being transmitted and PD could separate them with a [route] object. We also want an End Of Line (EOL) character so delineate the end of each individual transmission and display the nubmers in a column in hyperterminal. Finally, we want to add a character that delineates the tag for route from the number itself. Here's the variable definitions for the delimeter, EOL, and bin16_bcd:

; Define Constants
DELIMETER       equ 0x02
EOL                     equ 0x03

; Define variables
char            equ 0x04
NumH            equ 0x05
NumL            equ 0x06
TenK            equ 0x07
Thou            equ 0x08
Hund            equ 0x09
Tens            equ 0x0A
Ones            equ 0x0B

; initialize variables and constants
        movlw   ' '
        movwf   DELIMETER
        movlw   '\n'
        movwf   EOL

Now we'll write a macro that sends a number to PD (or hyperterminal). It takes 6 arguments: the tag character and 5 digits.

Send_arg MACRO tag, dig1, dig2, dig3, dig4, dig5
                movf    tag, W
                call    UART_Put
                movf    DELIMETER, W
                call    UART_Put
                movf    dig1, W
                call    UART_Put
                movf    dig2, W
                call    UART_Put
                movf    dig3, W
                call    UART_Put
                movf    dig4, W
                call    UART_Put
                movf    dig5, W
                call    UART_Put
                movf    EOL, W
                call    UART_Put
        endm

Finishing Up

Our main loop will get the A/D conversion, convert it into decimal, encode that into ASCII, and send the result. Here's the final code:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; psio-1.asm
;       Dumps values of pins to the UART for reading from Pure Data.
;
;       by Ian Smith-Heisters
;               November 18th, 2004:    Started
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

        title "Parallel Serial IO"
        List p=18f452,f=inhx32
        #include <p18f452.inc>

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;The config aliases are already defined in the 18f452 header file.
        __CONFIG    _CONFIG1H, _OSCS_ON_1H & _XT_OSC_1H ; External Clock on OSC1 & OSC2
        __CONFIG    _CONFIG2L, _BOR_ON_2L & _BORV_20_2L & _PWRT_OFF_2L ; Brown out reset on at 2.0V, no power-up timer
        __CONFIG    _CONFIG2H, _WDT_OFF_2H & _WDTPS_128_2H ; watchdog off, postscaler count to 128
        __CONFIG    _CONFIG3H, _CCP2MX_ON_3H ; CCP2 pin Mux enabled. What is this?
        __CONFIG    _CONFIG4L, _STVR_ON_4L & _LVP_ON_4L & _DEBUG_OFF_4L ; Stack under/overflow reset on, LVP on, debug off
        __CONFIG    _CONFIG5L, _CP0_OFF_5L & _CP1_OFF_5L & _CP2_OFF_5L & _CP3_OFF_5L ; all protection off
        __CONFIG    _CONFIG5H, _CPB_OFF_5H & _CPD_OFF_5H
        __CONFIG    _CONFIG6L, _WRT0_OFF_6L & _WRT1_OFF_6L & _WRT2_OFF_6L & _WRT3_OFF_6L
        __CONFIG    _CONFIG6H, _WRTC_OFF_6H & _WRTB_OFF_6H & _WRTD_OFF_6H
        __CONFIG    _CONFIG7L, _EBTR0_OFF_7L & _EBTR1_OFF_7L & _EBTR2_OFF_7L & _EBTR3_OFF_7L
        __CONFIG    _CONFIG7H, _EBTRB_OFF_7H

        org     0x0000

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Variable Definitions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Define Constants
COUNT1          equ     0x00
COUNT2          equ     0x01
DELIMETER       equ 0x02
EOL                     equ 0x03

; Define variables
char            equ 0x04
NumH            equ 0x05
NumL            equ 0x06
TenK            equ 0x07
Thou            equ 0x08
Hund            equ 0x09
Tens            equ 0x0A
Ones            equ 0x0B

; initialize variables and constants
        movlw   ' '
        movwf   DELIMETER
        movlw   '\n'
        movwf   EOL

; Set port D to all output
        clrf    TRISD

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Macro Definitions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Send_arg MACRO tag, dig1, dig2, dig3, dig4, dig5
                movf    tag, W
                call    UART_Put
                movf    DELIMETER, W
                call    UART_Put
                movf    dig1, W
                call    UART_Put
                movf    dig2, W
                call    UART_Put
                movf    dig3, W
                call    UART_Put
                movf    dig4, W
                call    UART_Put
                movf    dig5, W
                call    UART_Put
                movf    EOL, W
                call    UART_Put
        endm

ENCDA MACRO x
                movlw   0x30                    ; convert to 0-9 in ASCII
                addwf   x, F
        endm

; call initialization subs
        call    UART_Init
        call    ADC_Init
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Main loop
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Main
                movlw           'a'
                movwf           char
                call            ADC_Get

                call            bin16_bcd       ; assumes NumH, NumL, TenK, Thou, Hund, Tens, Ones
                ENCDA           TenK            ; coerce into ASCII encoding
                ENCDA           Thou
                ENCDA           Hund
                ENCDA           Tens
                ENCDA           Ones

                Send_arg        char, TenK, Thou, Hund, Tens, Ones

                incf            char, F         ; increment char this is pointless now, but will be useful for
                                                                ; using more than one ADC pin.
                goto            Main

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Subroutine Definitions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
                #include <UART.inc>
; Initialize analog input
ADC_Init
                ; Set port A to AN0 input
                bsf             TRISA, AN0
                ; Set port A to be an analog input see pp.183 and 184 in the datasheet
                movlw   B'00000001'
                movwf   ADCON0
                movlw   B'11001110'
                movwf   ADCON1
                return

; A basic delay, used for flashing LEDs.
Delay
                setf    COUNT1
                setf    COUNT2
Loop1   decfsz  COUNT1, F
                goto    Loop1
                setf    COUNT1
                decfsz  COUNT2, F
                goto    Loop1
                return

; Gets the analog value of a pin and puts it in NumH and NumL
ADC_Get
                bsf             ADCON0, GO      ; start conversion
adc_wait                                        ; don't move on until the conversion is done.
                btfsc   ADCON0, DONE
                goto    adc_wait
                movff   ADRESH, NumH
                movff   ADRESL, NumL
                return

; Binary (16-bit) to BCD. From Microchip PICDEM 2 PLUS DEMO BOARD
; xxx = highest possible result
bin16_bcd:
                ; Takes number in NumH:NumL
                ; Returns decimal in TenK:Thou:Hund:Tens:Ones
                swapf   NumH,W
                andlw   0x0F
                addlw   0xF0
                movwf   Thou
                addwf   Thou,F
                addlw   0xE2
                movwf   Hund
                addlw   0x32
                movwf   Ones
                movf    NumH,W
                andlw   0x0F
                addwf   Hund,F
                addwf   Hund,F
                addwf   Ones,F
                addlw   0xE9
                movwf   Tens
                addwf   Tens,F
                addwf   Tens,F
                swapf   NumL,W
                andlw   0x0F
                addwf   Tens,F
                addwf   Ones,F
                rlcf    Tens,F
                rlcf    Ones,F
                comf    Ones,F
                rlcf    Ones,F
                movf    NumL,W
                andlw   0x0F
                addwf   Ones,F
                rlcf    Thou,F
                movlw   0x07
                movwf   TenK
                movlw   0x0A ; Ten
Lb1:
                decf    Tens,F
                addwf   Ones,F
                btfss   STATUS,C
                bra     Lb1
Lb2:
                decf    Hund,F
                addwf   Tens,F
                btfss   STATUS,C
                bra     Lb2
Lb3:
                decf    Thou,F
                addwf   Hund,F
                btfss   STATUS,C
                bra     Lb3
Lb4:
                decf    TenK,F
                addwf   Thou,F
                btfss   STATUS,C
                bra     Lb4
                return

        end

That should be enough to get you started. It's easy to add more sensors and give them different tags so they can be transmitted in parallel. Building the sensors themselves is up to you.