luminous Home   Feature   Space   Science   Atheism   Paranonsense   Technology   Offsite   About   Search
Home : Technology : An AVR-based Robot

Download all of the Eagle schematics and AVR Studio project files as a zip file (142KB).


AVR-based Robot Software


Basic Services

There are a set of basic services which must be set up before any serious code runs on the AVR. These include setting up the stack, calling reset routines, and running the main loop. It also involves defining the relationship between the logical functions of the ports and registers and their physical equivalents.

ROBOTDEF.INC

This file defines a series of macros which make the physical I/O locations and other values more meaningful.

;-----------------------------------------------------------------
;
; Name:         RobotDef.inc
; Title:        Defines for robot I/O, etc.
; Version:      1.0
; Last updated: 2001.03.10
; Target:       AT90S2313
;
;-----------------------------------------------------------------
;
; DESCRIPTION
;
; Defines for robot I/O
;
; Bit numbers begin with b
; Bitmasks begin with m
; Registers begin with r
; Other values are all uppercase
;
;-----------------------------------------------------------------

.include "2313def.inc"

;-----------------------------------------------------------------
;
; Robot specific port designations.
;
;-----------------------------------------------------------------
;
; Port D
;

These are the bit designations for port D, which is connected to the front and back bump switches, and the left, right and back eye drives. These bit numbers are used in turn to define mask values which have a '1' bit in the appropriate position, and some compound masks for 'all bump switches' and 'all eye drives'. The data direction register on Port D will be outputs only for the eye drives, the others are inputs or are used for the UART.

; UART on pins D0, D1

; Bit #s
.equ    bBUMPF          =PIND2
.equ    bBUMPB          =PIND3
.equ    bEYEDRIVEL      =PIND4
.equ    bEYEDRIVER      =PIND5
.equ    bEYEDRIVEB      =PIND6

; Mask values
.equ    mBUMPF          =(1<<bBUMPF)
.equ    mBUMPB          =(1<<bBUMPB)
.equ    mEYEDRIVEL      =(1<<bEYEDRIVEL)
.equ    mEYEDRIVER      =(1<<bEYEDRIVER)
.equ    mEYEDRIVEB      =(1<<bEYEDRIVEB)

.equ    mBUMPS          =(mBUMPF|mBUMPB)
.equ    mEYEDRIVES      =(mEYEDRIVEL|mEYEDRIVER|mEYEDRIVEB)

.equ    mDDRD           =mEYEDRIVES

;-----------------------------------------------------------------
;
; Port B
;

Again, these are bit designations, this time for port B, which is connected to the analog to digital converter RC network, the IRED drive, and the motor drives. Also again, these are used to construct masks for convenience's sake. Two DDR values are given, one for reading from the RC network, and one for discharging it.

; Analog comparator on B0, B1

; Bit #s
.equ    bRCREAD         =PINB0
.equ    bANALOGIN       =PINB1
.equ    bRCDRIVE        =PINB2
.equ    bIREDDRIVE      =PINB3
.equ    bMOTORLB        =PINB4
.equ    bMOTORLA        =PINB5
.equ    bMOTORRA        =PINB6
.equ    bMOTORRB        =PINB7

; Mask values
.equ    mRCREAD         =(1<<bRCREAD)
.equ    mANALOGIN       =(1<<bANALOGIN)
.equ    mRCDRIVE        =(1<<bRCDRIVE)
.equ    mIREDDRIVE      =(1<<bIREDDRIVE)
.equ    mMOTORLA        =(1<<bMOTORLA)
.equ    mMOTORLB        =(1<<bMOTORLB)
.equ    mMOTORRB        =(1<<bMOTORRB)
.equ    mMOTORRA        =(1<<bMOTORRA)

.equ    mMOTORS         =(mMOTORLA|mMOTORLB|mMOTORRA|mMOTORRB)

.equ    mDDRBREAD       =(mRCDRIVE|mIREDDRIVE|mMOTORS)
.equ    mDDRDISCHARGE   =(mDDRBREAD|mRCREAD)

;-----------------------------------------------------------------
;
; General purpose registers

The registers are assigned particular roles. rRETURN is a general purpose return value from a subroutine call. rSCRATCHn and rISCRATCHn are just general scratch registers, for use in the main loop and the interrupt routines respectively. I have saved the bother of pushing the status register during an interrupt routine by dedicating a register to that task, rISREGSAVE. rTEMPn and rITEMPn are high registers which are used for temporary values, differing from the scratch registers in that they can operate with immediate values. rPARAMn are registers used mainly for passing parameters to subroutines, and rLOOPI and rLOOPJ are two loop counters. The X, Y and Z registers retain their usual designations.

.def    rRETURN         =r0     ; return value from functions

.def    rSCRATCH        =r7
.def    rSCRATCH2       =r8
.def    rSCRATCH3       =r9
.def    rSCRATCH4       =r10

.def    rISCRATCH       =r11    ; for use in interrupt routines
.def    rISCRATCH2      =r12    ; ditto
.def    rISCRATCH3      =r13    ; ditto
.def    rISCRATCH4      =r14    ; ditto

.def    rISREGSAVE      =r15    ; sreg saved here in interrupts

.def    rTEMP           =r16
.def    rTEMP2          =r17

.def    rITEMP          =r18    ; for use in interrupt routines
.def    rITEMP2         =r19    ; again, for use in interrupt routines

.def    rPARAM1         =r20    ; parameter 1 for function calls
.def    rPARAM2         =r21    ; parameter 2 for function calls

.def    rLOOPI          =r22    ; loop counters
.def    rLOOPJ          =r23    ; by tradition, I is outer loop, J inner

;-----------------------------------------------------------------
;
; Macros

I noticed that the LDS and STS instructions (to store to/from locations in RAM) take up 2 words of space, but that an indexed load via a register will only take 1. Since code space is going to be tight in the 2313, as it only has 1024 words of flash memory, I have dedicated the Y register to always point to the start of the RAM, so I can access the first 64 bytes of RAM using these LDY and STY macros.

; LDY   r,addr
.macro  LDY
        ldd @0,Y+(@1-RAMSTART)
.endmacro

; STY   addr,r
.macro  STY
        std Y+(@0-RAMSTART),@1
.endmacro

.listmac

These simple debugging macros helped me debug the monitor routines but are now unused.

; DEBUG displayed_value

.macro  DEBUG
        push    rTEMP
        ldi     rTEMP,($FF-@0)
        out     PORTB,rTEMP
        pop     rTEMP
.endmacro

; DEBUG displayed_register

.macro  DEBUGR
        push    rTEMP
        mov     rTEMP,@0
        com     rTEMP
        out     PORTB,rTEMP
        pop     rTEMP
.endmacro

;-----------------------------------------------------------------

Finally, the clock rate for the processor, which is used later to work out the UART baud rate register value.

.equ    ROBOT_CLOCKRATE =4000000

;-----------------------------------------------------------------

ROBOT.ASM

This file performs initial setup and runs the main loop.

;-----------------------------------------------------------------
;
; Name:         Robot.asm
; Title:        Basic setup and services for robot.
; Version:      1.0
; Last updated: 2001.03.11
; Target:       AT90S2313
;
;-----------------------------------------------------------------
;
; DESCRIPTION
;
; Simple robot systems integration testbed.
; 
;-----------------------------------------------------------------

.nolist
.include "robotdef.inc"
.list

;-----------------------------------------------------------------
;
; Data

First, we define any data required for this module. This is just a tick count which is incremented by the main timer 0 interrupt.

.dseg

robot_tickcount:
        .byte   2               ; tick count

;-----------------------------------------------------------------
;
; Interrupt service vectors

Interrupts are routed to the correct places. Unused interrupts just have a reti instruction.

.cseg
.org 0
        rjmp robot_reset        ; Reset vector

        reti                    ; INT0 vector
        reti                    ; INT1 vector

        rjmp eye_capture        ; T/C1 capture vector
        reti                    ; T/C1 compare match vector
        rjmp eye_overflow       ; T/C1 overflow vector

        rjmp robot_tick         ; T/C0 overflow vector

        rjmp uart_receive       ; UART receive service
        reti                    ; UART data reg empty service
        reti                    ; UART transmit service

        reti                    ; Analog comparator service

;-----------------------------------------------------------------
;
; Start of actual code.
;
;-----------------------------------------------------------------
;
; Reset vector - generic system.
;

The reset vector goes to some initial setup code. This prepares the way for executing the main loop.

; Before calling reset functions for each subsystem, it sets up
; the following:
;
; 1. RAM set to all 0's
; 2. Y -> RAMSTART
; 3. Sleep mode enabled, t0, t1 on rising edge
; 4. 2kHz interrupt on timer 0
; 5. XH, YH, ZH all 0

First, we must always set the stack pointer.

robot_reset:
        ldi     rTEMP,RAMEND    ; Set initial stack ptr location at ram end
        out     SPL,rTEMP

Since the RAM and EEPROM are less than 256 bytes in size, we can set the X, Y and Z high registers to 0 and forget about them.

        clr     XH
        clr     YH
        clr     ZH

Now we clear the RAM, since this is cheaper than explicitly setting most of the data values to 0. This loop also leaves Y pointing at the start of RAM, which allows us to use the LDY and STY macros defined in ROBOTDEF.INC.

        ldi     YL,RAMEND+1     ; Just past end since we are predecrementing.

robot_ramclear:
        st      -Y,YH           ; clear
        cpi     YL,RAMSTART     ; done?
        brne    robot_ramclear  ; no.. go round again. Y-> RAMSTART at end

Timer 0 is set up as an approximately 2kHz timer, and enabled. The sleep mode is enabled also.

        ; set up timer0

        ldi     rTEMP,(1<<TOIE0) ; timer interrupt mask 0 enable
        out     TIMSK,rTEMP     

        ldi     rTEMP,$02       ; clock prescaler for T0 = clk/8 (02)
        out     TCCR0,rTEMP     

        ; set sleep mode

        ldi     rTEMP,$2A       ; enable sleep mode, interrupt t0 and t1 on falling edge only
        out     MCUCR,rTEMP

Each module can publish a reset routine - these are all called at this point to initialize each one.

        ; now call all the reset functions

        rcall   reset_uart
        rcall   reset_eyes
        rcall   reset_state
        rcall   reset_monitor

In addition to the reset routines, we now do some additional setup. This should by rights be in the EYES.ASM module, now that I come to review the code. This calibrates the eyes.

        ; and, go!

        sei                     ; enable interrupts and off we go!
        
        ldi     rLOOPI,16
        rcall   calibrate_eyes_counted  ; skip first few calibration cycles
        
        ldi     zl,low(robot_calibrate*2)
        ldi     zh,high(robot_calibrate*2)
        rcall   uart_writestringlf

        rcall   calibrate_eyes          ; and then actually calibrate the eyes after we've settled

        ldi     zl,low(robot_calibrate_done*2)
        ldi     zh,high(robot_calibrate_done*2)
        rcall   uart_writestringlf

Now we arrive at the main loop. This sleeps until something happens, then calls routines to update each of the modules, and goes round again forever.

robot_mainloop: 
        sleep                   ; sleep till next interrupt

        rcall   update_monitor  ; update monitor
        rcall   update_state    ; update state machine

        rjmp    robot_mainloop  ; Infinite loop - never terminates

;-----------------------------------------------------------------
;
; Timer 0 vector - free running 2kHz counter.

Timer 0's interrupt routine just increments the tick counter and calls 'tick' routines in the modules that need an interrupt-driven update.

robot_tick:
        in      rISREGSAVE,SREG

        ldy     rISCRATCH,robot_tickcount
        inc     rISCRATCH       
        sty     robot_tickcount,rISCRATCH
        
        rcall   state_tick
        rcall   update_eyes

        out     SREG,rISREGSAVE
        reti

;-----------------------------------------------------------------
;
; Messages

Here we just define some debug messages to be sent to the UART.

robot_calibrate:
        .db     "Calibrating eyes...",$00

robot_calibrate_done:
        .db     "...done",$00

;-----------------------------------------------------------------
;
; Include other subsystems

And this includes all the other modules.

.include "eyes.asm"
.include "uart.asm"
.include "state.asm"
.include "monitor.asm"

;-----------------------------------------------------------------

Eyes

The eyes are abstracted from a series of analog inputs to a simple set of on/off bits by these routines.

EYES.ASM

This file contains all the code for dealing with the eyes.

;-----------------------------------------------------------------
;
; Name:         eyes.asm
; Title:        Eye circuit services for robot
; Version:      1.0
; Last updated: 2001.03.10
; Target:       AT90S2313
;
;-----------------------------------------------------------------
;
; DESCRIPTION
;

I really went to town on the comments in this file...

; The eye circuit referred to above is a photodiode operating
; in zero bias mode (cathode to ground) connected to a two stage
; simple transistor amplifier, which is in turn attached to the
; negative input of the comparator. The circuit diagram for this can
; be summarized as follows:
;
; Photodiode cathode to ground, anode through 100nF capacitor to
; base of T1. T1 is a BC639, base also through 150k to collector, 
; emitter to ground, collector to 30k to drive pin. Collector of 
; T1 decoupled to next stage with 100nF capacitor. 
;
; Pins EYEDRIVEL,R and B (PD4-6) supply the power to drive the 
; first stage amplifiers for the photodiodes.
;
; All three first stage outputs are commoned onto a 1k resistor
; into the base of the next stage, T2, also a BC639, base also
; through 30k to collector, collector through 6k8 to positive
; supply. Collector also feeds into inverting input of on-chip
; comparator.
;
; Simple R-C ADC (3k into 10nF) is driven from RCDRIVE (PB2) and
; the junction is connected to the noninverting input of the
; on-chip comparator (RCREAD).
;
; Timer 1 is used in input capture mode to capture a positive edge on
; the comparator.
;
; Pin IREDDRIVE (PB3) is used to drive an IRED (high=on) through
; another pair of transistors (to amplify current) which is then 
; used to take alternate on/off light/dark readings which are then
; totalled as a difference over several cycles.
;
; PUBLISHED FUNCTIONS
;
; reset_eyes
;
; Resets this module. Called on reset. Sets I/O settings, T1
; compare mode and count rate, discharges RC network.
;
;
; update_eyes
;
; Called on a 2kHz counter to actually run the sampling.
;
; The IRED is run at 50% duty during the sampling. Input counts are
; accumulated using successive addition and subtraction to implement
; a simple high-pass filter which resonates at around 1kHz (which is
; not coincidentally the IRED drive frequency).
;
; This filter effects an average over many cycles (64 in this example)
; to help eliminate random noise, which should add and subtract from
; the accumulator evenly.
;
;
; read_eyes
;
; Reads eye states. Returns R0 as a bitvector, with bit 0 indicating
; EyeL state, bit 1 EyeR, and bit 2 EyeB. Whether the eye is "on" or
; not is determined by the thresholds set by the calibration cycle.
;
; Tests suggest that a measurable signal can be obtained from objects
; between 10 and 20cm away from the eye, depending on size, reflectivity,
; etc.
;
;
; calibrate_eyes
;
; Calibrates the eyes over a few cycles, setting the threshold of each
; eye to a sensible value.
;
; For a real-world app, it would be a good idea to run a calibration
; phase to avoid false readings. This should read the eye in a quies-
; cent state (nothing in view) and get estimates of the peak, minimum
; and typical counts from the integrator/filter. A sensible threshold
; can then be established for triggering the device.
;
; 
;-----------------------------------------------------------------
;
; Defines

When we are reading the eyes, we take a certain number of samples (EYE_SAMPLES), which are in phase with the IRED drive, and then switch eyes. After this we need to wait for EYE_REST samples before sampling the eye again, to allow the amplifier to start up and stabilise - remember that we are switching eyes by switching the power to the first stage of the photodiode amplifier.

.equ    EYE_SAMPLES             =$40    ; number of samples taken for reading
.equ    EYE_REST                =$10    ; number of cycles to wait after switching eyes
.equ    EYE_CALIBRATECOUNT      =$40    ; number of cycle to calibrate eye

;-----------------------------------------------------------------
;
; Variables

.dseg

eye_count:      .byte   1       ; interrupt counter
eye_active:     .byte   1       ; active eye
eye_state:      .byte   1       ; eye drive mask
eye_ired:       .byte   1       ; ired drive state

eye_accuml:     .byte   1       ; accumulator for dsp-style filter calculation
eye_accumh:     .byte   1

eye_valuel:     .byte   1       ; accumulator is saturated to lie between
eye_valuer:     .byte   1       ; +00 and +FF during transfer.
eye_valueb:     .byte   1

eye_thresholdll: .byte  1       ; low thresholds set by calibrate_eye
eye_thresholdrl: .byte  1       ; these are also used to store the MAXIMUM value
eye_thresholdbl: .byte  1       ; encountered during calibration.

eye_thresholdlh: .byte  1       ; high thresholds set by calibrate_eye
eye_thresholdrh: .byte  1       ; paradoxically, these are also used to store
eye_thresholdbh: .byte  1       ; the MINIMUM value encountered during calibration

eyes_triggered: .byte   1       ; current trigger state for each eye

;-----------------------------------------------------------------
;
; Code
;
; Y is assumed to point to the RAM.
; RAM is assumed to be cleared to 0's on reset.

.cseg

;-----------------------------------------------------------------
;
; reset_eyes

This is just the reset routine for the eye module. This sets up the initial state for the eye drive pins, the current eye, and sets up and enables the ADC and timer 1, which we will be using to actually do the sampling.

reset_eyes:
        ; discharge at start

        ldi     rTEMP,(EYE_SAMPLES+EYE_REST)    ; count + recovery time
        sty     eye_count,rTEMP

        ldi     rTEMP,mEYEDRIVEL        ; Initial photodiode drive mask
        sty     eye_state,rTEMP

        rcall   eye_drive
                
        ; set up interrupt masks to enable timer 1 interrupt for timeout
        ; and input capture.

        in      rTEMP,TIMSK
        ori     rTEMP, (1<<TOIE1)|(1<<TICIE)
        out     TIMSK,rTEMP

        ; set up analog comparator mode to allow input capture

        ldi     rTEMP,$07       ; ACIC enable, rising edge
        out     ACSR,rTEMP

        ret

;-----------------------------------------------------------------
;
; update_eyes
; Called from within the timer routine on 2kHz timer 0.

This is called at 2kHz, and the first part just decides whether we are sampling or resting. We count down eye-count from EYE_SAMPLES+EYE_REST - if the count is above EYE_SAMPLES, then we're still resting. If it's negative, then we've expired and need to transfer the digital filter accumulator into the appropriate eye value location. Otherwise, we're doing a sample.

update_eyes:
        ldy     rITEMP,eye_count        ; tick counter
        dec     rITEMP
        sty     eye_count,rITEMP
        brmi    update_eyes_resetcount

        cpi     rITEMP,EYE_SAMPLES      ; still resting?
        brcc    update_eyes_skipcount

This is the sample routine. We set the start count for timer 1 to FF00 (i.e. 256 counts before overflowing), set the RC network to start ramping up in voltage, and start timer 1. The actual reading happens on the interrupts from timer 1.

        ser     rITEMP                  ; start count = $FF00
        clr     rITEMP2
        out     TCNT1H,rITEMP
        out     TCNT1L,rITEMP2

        ldi     rITEMP,mDDRBREAD
        out     DDRB,rITEMP             ; set data direction for reading
        sbi     PORTB,bRCDRIVE          ; drive RC network, all other bits unchanged

        sbic    ACSR,ACO                ; already triggered? skip if ok
        rjmp    update_eyes_premature   ; premature timeout!

        ldi     rITEMP, $41             ; start counter1, no predivider
        out     TCCR1B,rITEMP

update_eyes_skipcount:
        ret

update_eyes_premature:
        ldy     rITEMP2,eye_ired
        ldi     rITEMP,$08              ; toggle IRED drive after reading
        eor     rITEMP2,rITEMP
        sty     eye_ired,rITEMP2

        ret

Here we've finished reading so we clamp the eye readings into the range 00..FF, and store it in one of the three eye value variables. We also advance to driving the next eye.

update_eyes_resetcount:
        ; Store accumulator value in appropriate eye.

        clr     rITEMP2

        ldy     rISCRATCH,eye_accumh
        and     rISCRATCH,rISCRATCH
        brmi    eye_valuefound

        ser     rITEMP2
        brne    eye_valuefound

        ldy     rITEMP2,eye_accuml

eye_valuefound:
        mov     rISCRATCH2,XL

        ldi     XL,eye_valuel           ; address of eye
        ldy     rISCRATCH,eye_active    ; +active eye index
        add     XL,rISCRATCH
        st      X,rITEMP2

        mov     XL,rISCRATCH2
        
        ldy     rITEMP,eye_state        ; advance to next eye
        cpi     rITEMP,mEYEDRIVEB
        breq    eye_state_reset

        inc     rISCRATCH
        add     rITEMP,rITEMP
        rjmp    eye_next_done

eye_state_reset:
        clr     rISCRATCH
        ldi     rITEMP,mEYEDRIVEL

eye_next_done:
        sty     eye_active,rISCRATCH
        sty     eye_state,rITEMP

        clr     rISCRATCH
        sty     eye_accumh,rISCRATCH
        sty     eye_accuml,rISCRATCH    
        sty     eye_ired,rISCRATCH      

        ldi     rITEMP,(EYE_SAMPLES+EYE_REST)   ; count + recovery time
        sty     eye_count,rITEMP
        
        rjmp    eye_drive

;-----------------------------------------------------------------
;
; Timer1 vectors - capture and accumulate value.

This is the overflow interrupt for timer 1. If the timer has overflowed, then the RC network has not reached the input voltage in the allotted time and we assign a limit value of FF to the value read.

eye_overflow:
        in      rISREGSAVE,SREG

        ldi     rITEMP, $40             ; stop counter1 - timed out
        out     TCCR1B,rITEMP

        clr     rISCRATCH
        com     rISCRATCH               ; $FF
        rjmp    eye_setvalue

This is what normally happens - the RC voltage has surpassed the input voltage from the eye preamplifier, and has triggered an input capture event. This copies the current value of timer 1 to the ICR1 registers in I/O space. We can now use this to work out how long it took to get there and thus get a reasonable guess at the voltage.

eye_capture:
        in      rISREGSAVE,SREG

        ldi     rITEMP, $40             ; stop counter1
        out     TCCR1B,rITEMP

        in      rISCRATCH2,ICR1H        ; unused, but required for semantics of 16 bit read.
        in      rISCRATCH ,ICR1L

Whatever the value, it is alternatively added and subtracted from a 16-bit accumulator. This is so that noise and DC levels will cancel out, but any signal in phase with the IRED drive (which is also driven from the same timer 0 interrupt which starts the sampling process) will actually increase in magnitude.

eye_setvalue:
        clr     rISCRATCH2

        ldy     rISCRATCH3,eye_accuml
        ldy     rISCRATCH4,eye_accumh

        ldy     rITEMP2,eye_ired
        and     rITEMP2,rITEMP2
        brne    eye_subtract

        add     rISCRATCH3,rISCRATCH
        adc     rISCRATCH4,rISCRATCH2   ; 0
        rjmp    eye_add

eye_subtract:
        sub     rISCRATCH3,rISCRATCH
        sbc     rISCRATCH4,rISCRATCH2   ; 0

eye_add:
        rcall   eye_drive

        ldi     rITEMP,mIREDDRIVE       ; toggle IRED drive after reading
        eor     rITEMP2,rITEMP          ; IRED state still in rITEMP2
        sty     eye_ired,rITEMP2

        sty     eye_accuml,rISCRATCH3
        sty     eye_accumh,rISCRATCH4

        out     SREG,rISREGSAVE
        reti

;-----------------------------------------------------------------
;
; eye_drive
; Common routine to reset ADC RC network
; PD drive value passed in register DRIVE

eye_drive:
        in      rITEMP,PORTD
        cbr     rITEMP,mEYEDRIVES
        ldy     rISCRATCH,eye_state
        or      rITEMP,rISCRATCH
        out     PORTD,rITEMP            ; and output drive values

        in      rITEMP,PORTB
        cbr     rITEMP,(mANALOGIN|mRCDRIVE|mIREDDRIVE)
        ldy     rISCRATCH,eye_ired
        or      rITEMP,rISCRATCH
        out     PORTB,rITEMP

        ldi     rITEMP,mDDRDISCHARGE    ; outputs on B0, B2, B3-B7
        out     DDRB,rITEMP
        ret

;-----------------------------------------------------------------
;
; read_eyes
; Read eye state and return as bit vector. This uses the low and high
; thresholds, together with the current state, as hysteresis.

read_eyes:
        ldi     rPARAM1,$04             ; mask for eye 2 to start
        clr     rRETURN
        ldi     ZL,eye_valuel+3         ; one past end of value array
        
read_eyes_loop:
        ld      rSCRATCH,-Z
        ldd     rSCRATCH2,Z+(eye_thresholdll-eye_valuel)        ; low threshold

        ldy     rTEMP,eyes_triggered
        and     rTEMP,rPARAM1           ; check if this eye triggered already
        brne    read_eyes_hysteresis    ; yes - continue to use low threshold
        
        ldd     rSCRATCH2,Z+(eye_thresholdlh-eye_valuel)        ; else use high one instead

read_eyes_hysteresis:
        cp      rSCRATCH,rSCRATCH2
        brcs    read_eyes_low
        
        or      rRETURN,rPARAM1         ; higher than selected threshold so set

read_eyes_low:
        lsr     rPARAM1                 ; finished all eyes?
        brne    read_eyes_loop          ; not yet
        
        sty     eyes_triggered,rRETURN  ; store for next time
        ret

;-----------------------------------------------------------------
;
; calibrate_eyes
; Waits for EYE_CALIBRATECOUNT full eye cycles  and records
; min and max for each eye, which are then used to set the thresholds
; for sensing

calibrate_eyes:
        ldi     rLOOPI,EYE_CALIBRATECOUNT
        
calibrate_eyes_counted:
        clr     rTEMP
        sty     eye_thresholdll,rTEMP   ; maxima = minvalue to start with
        sty     eye_thresholdrl,rTEMP
        sty     eye_thresholdbl,rTEMP
        
        ser     rTEMP
        sty     eye_thresholdlh,rTEMP   ; minima = maxvalue to start with
        sty     eye_thresholdrh,rTEMP
        sty     eye_thresholdbh,rTEMP

calibrate_eyes_mainloop:        
calibrate_eyes_wait02:
        ldy     rTEMP2,eye_active
        cpi     rTEMP2,$02
        brne    calibrate_eyes_wait02

calibrate_eyes_wait00:
        ldy     rTEMP2,eye_active
        cpi     rTEMP2,$00
        brne    calibrate_eyes_wait00

        ; compare each value against min and max and adjust
        ; this is done with a predecrement on Z therefore we
        ; start z one beyond the end of the value array
        ; we can also easily check z for end condition which
        ; eliminates the need for a separate loop counter
        
        ldi     ZL,eye_valuel+3
        
calibrate_eyes_minimax:
        ld      rSCRATCH,-Z
        ldd     rSCRATCH2,Z+(eye_thresholdll-eye_valuel)        ; max
        ldd     rSCRATCH3,Z+(eye_thresholdlh-eye_valuel)        ; min
        
        cp      rSCRATCH,rSCRATCH2
        brcs    calibrate_eyes_notmax
        
        std     Z+(eye_thresholdll-eye_valuel),rSCRATCH

calibrate_eyes_notmax:
        cp      rSCRATCH,rSCRATCH3
        brcc    calibrate_eyes_notmin
        
        std     Z+(eye_thresholdlh-eye_valuel),rSCRATCH

calibrate_eyes_notmin:
        cpi     ZL,eye_valuel
        brne    calibrate_eyes_minimax
        
        dec     rLOOPI
        brne    calibrate_eyes_mainloop
        
        ; postprocess min/max into thresholds
        ; loop works the same way as for the minimax calculation

        ldi     ZL,eye_thresholdll+3
        
Having found the minimum and maximum values for each eye, we can now set the thresholds based on them. There are several different variants of this, some of which are commented out. For the half-difference threshold, which I found OK, we set the "off" threshold to max, and the "on" threshold to max plus half the difference between max and min.

calibrate_eyes_threshold:
        ld      rSCRATCH,-Z             ; max
        ldd     rSCRATCH2,Z+(eye_thresholdlh-eye_thresholdll)   ; min
        
        sub     rSCRATCH2,rSCRATCH      ; min-max = -(max-min)
        
;       Half difference threshold

        asr     rSCRATCH2               ; (min-max)/2
        sub     rSCRATCH,rSCRATCH2      ; max-(min-max)/2 = max+(max-min)/2

;       Single difference threshold
;
;       sub     rSCRATCH,rSCRATCH2      ; max-(min-max) = max+(max-min)

;       Double difference threshold
;
;       sub     rSCRATCH,rSCRATCH2      ; max-(min-max) = max+(max-min)
;       sub     rSCRATCH,rSCRATCH2      ; max+(max-min)*2
        
        std     Z+(eye_thresholdlh-eye_thresholdll), rSCRATCH   ; store back again

        cpi     ZL,eye_thresholdll
        brne    calibrate_eyes_threshold

        ret

;-----------------------------------------------------------------

RS232

The monitor uses some basic RS232 services to communicate with a host PC for debugging.

RS232.ASM

This file contains all the code for dealing with the UART.

;-----------------------------------------------------------------
;
; Name:         uart.asm
; Title:        AVR Uart services
; Version:      1.0
; Last updated: 2001.02.03
; Target:       AT90Sxxxx (All devices)
;
;-----------------------------------------------------------------
;
; DESCRIPTION
;
; UART service which uses interrupts to read characters
; from the serial port and buffer them, but which handles trans-
; mission synchronously, without interrupts.
; 
;-----------------------------------------------------------------

.dseg

These declarations define the buffer which will be used for storing incoming characters from the UART. uart_rhead and uart_rtail are the adresses in the buffer of the head (where a new character will be written by the UART) and tail (where a character will be read from by the program) of a circular buffer. The number of characters currently in the buffer is uart_rsize, and any errors are reported with a non-0 value in uart_rerror.

uart_rhead:     .byte   1
uart_rtail:     .byte   1
uart_rsize:     .byte   1
uart_rerror:    .byte   1

The buffer itself needs to be a specific size and at a specific position.

.equ    UART_BUFSIZE    =$20    ; must be power of 2
.equ    UART_BUFFER     =$A0    ; must be on a UART_BUFSIZE boundary

And here's the standard calculation of baud rate register values.

.equ    UART_BAUDRATE   =9600
.equ    UART_BAUDK      =(ROBOT_CLOCKRATE/(16*UART_BAUDRATE))-1

;-----------------------------------------------------------------

.cseg

;-----------------------------------------------------------------
;
; Reset vector - setup

The reset routine just sets up the head and tail, and enables the UART for interrupt-driven receive and polled transmit. It also outputs a welcome message. Remember that addresses in flash should be multiplied by 2! At the end it restores the zero value in ZH, since other routines may rely on it.

reset_uart:
        ldi     rTEMP,UART_BUFFER
        sty     uart_rhead,rTEMP
        sty     uart_rtail,rTEMP

        ldi     rTEMP,UART_BAUDK
        out     UBRR,rTEMP      ; load baudrate
        
        ldi     rTEMP,(1<<TXEN)|(1<<RXEN)|(1<<RXCIE)    ; enable transmit/receive, enable rx int.
        out     UCR,rTEMP

        ldi     ZH,high(Message*2)      ; haha! Word address
        ldi     ZL,low(Message*2)
        rcall   uart_writestringlf

        clr     ZH
        ret

;-----------------------------------------------------------------
;
; Receive vector - buffer input

This is the receive interrupt vector. It checks for errors and then writes the character into the buffer and updates the head pointer.

uart_receive:
        in      rISREGSAVE,SREG
        mov     rISCRATCH4,XL

        sbic    USR,FE          ; check for
        rjmp    uart_rxerr_fe   ; framing error

        ldy     rITEMP,uart_rsize
        cpi     rITEMP,UART_BUFSIZE
        breq    uart_rxerr_bf   ; buffer full

        inc     rITEMP
        sty     uart_rsize,rITEMP

        ldy     XL,uart_rhead
        in      rITEMP,UDR
        st      X+,rITEMP

        andi    XL,(UART_BUFSIZE-1)
        ori     XL,UART_BUFFER
        sty     uart_rhead,XL

        sbic    USR,OR          ; check for
        rjmp    uart_rxerr_or   ; overrun

uart_rxend:
        mov     XL,rISCRATCH4
        out     SREG,rISREGSAVE
        reti

uart_rxerr_fe:
uart_rxerr_bf:
        in      rITEMP,UDR              ; dummy read to clear RxC
uart_rxerr_or:
        ldi     rITEMP,1                ; overflow, framing error
        sty     uart_rerror,rITEMP

        rjmp    uart_rxend

;-----------------------------------------------------------------
;
; Read from receiver buffer char=rRETURN, wait until character ready

This reads a character from the buffer. In this version, it first checks to see if any characters are ready. If not, then it will loop until one comes in before falling through to read it.

uart_readcharwait:
        rcall   uart_charready
        breq    uart_readcharwait

This reads a character from the buffer. If no characters are ready, then it will perform incorrectly.

uart_readchar:
        push    XL
        
        ldy     XL,uart_rtail
        ld      rRETURN,x+
        andi    XL,(UART_BUFSIZE-1)
        ori     XL,UART_BUFFER
        sty     uart_rtail,XL

        cli
        ldy     rSCRATCH,uart_rsize
        dec     rSCRATCH
        sty     uart_rsize,rSCRATCH
        sei

        pop     XL

        ret
        
;-----------------------------------------------------------------
;
; Ask if a character is ready from the UART (eq=no, ne=yes)

This is simple to find out - if there is a character, then the number of characters in the buffer is non-0.

uart_charready:
        ldy     rSCRATCH,uart_rsize
        and     rSCRATCH,rSCRATCH
        ret     

;-----------------------------------------------------------------
;
; Write to transmitter a string followed by a cr/lf

uart_writestringlf:
        rcall   uart_writestring

        ; falls through to uart_writecrlf...

;-----------------------------------------------------------------
;
; Write to transmitter a cr/lf pair

uart_writecrlf:
        ldi     rPARAM1,$0D
        rcall   uart_writechar
        ldi     rPARAM1,$0A

        ; falls through to uart_writechar again...

;-----------------------------------------------------------------
;
; Write to transmitter char=rPARAM1

This is a simple, polled write character routine. It waits until the UDRE bit is set (UART Data Register Empty) which indicates that it is safe to transmit another character.

uart_writechar:
        sbis    USR,UDRE
        rjmp    uart_writechar
        out     UDR,rPARAM1
        ret

;-----------------------------------------------------------------
;
; Write to transmitter hex value=rPARAM1

This converts the value in rPARAM1 into two hex digits (0..9, A..F) and outputs them to the serial port.

uart_writehex:
        push    rPARAM1
        swap    rPARAM1
        rcall   uart_writehexit
        pop     rPARAM1
        
uart_writehexit:
        andi    rPARAM1,$0F
        subi    rPARAM1,-48
        cpi     rPARAM1,$3A
        brcs    uart_writechar
        subi    rPARAM1,-7
        rjmp    uart_writechar

;-----------------------------------------------------------------
;
; Write 0-terminated string in Flash at Z to transmitter

uart_writestring:
        lpm                             ; r0,Z
        adiw    ZL,1
        mov     rPARAM1,r0
        and     rPARAM1,rPARAM1
        breq    uart_writestringend

        rcall   uart_writechar

        rjmp    uart_writestring

uart_writestringend:
        ret                             ; NB: Shared with uart_writestring
        
;-----------------------------------------------------------------
;
; Write 0-terminated string in RAM at Z to transmitter, with crlf
; if reqd.
;
; - - - UNUSED - - - - - - - - - - - - - - - - - - - - - - - - - -
;
;uart_writestringram:
;       clr     zh
;uart_writestringramloop:
;       ld      rPARAM1,z+
;       and     rPARAM1,rPARAM1
;       breq    uart_writestringend
;
;       rcall   uart_writechar
;
;       rjmp    uart_writestringramloop
;
;uart_writestringramlf:
;       rcall   uart_writestringram
;       rjmp    uart_writecrlf
;
; - - - UNUSED - - - - - - - - - - - - - - - - - - - - - - - - - -

;-----------------------------------------------------------------
;
; Constant Data

;-----------------------------------------------------------------
;
; Message

Message:
        .db     $0d,$0a,"UART Ready",$00

;-----------------------------------------------------------------

State Machine

The main personality of the robot is based on a state machine description stored in the EEPROM.

STATE.ASM

This file defines the state machine interpreter.

;-----------------------------------------------------------------
;
; Name:         state.asm
; Title:        State machine for running the robot
; Version:      1.0
; Last updated: 2001.03.17
; Target:       AT90S2313
;
;-----------------------------------------------------------------
;
; DESCRIPTION
;

Once again, comments'R'us.

; The high-level behavior of the robot is determined by a state
; machine, which specifies the response to several stimuli.
;
; The states are written into the EEPROM, either by serial
; programming, or via the separate robot monitor subsystem.
;
; Each state is stored as a compressed description, as follows:
;
; Byte 0:
;       Bits 7-4, motor output. 00 = stop, 01=reverse,
;                 10=forward, 11=random state, for each motor
;       Bit  3    reserved
;       Bits 0-2, timeout value, approximately...
;                 0000 = 1/8 second
;                 0001 = 1/4 second
;                 0010 = 1/2 second
;                 0011 =   1 second
;                 0100 =   2 seconds
;                 0101 =   4 seconds
;                 0110 =   8 seconds
;                 0111 =  16 seconds
;
; Byte 1..n:
;       Bits 7-4: Destination state for event 2n-2
;       Bits 3-0: Destination state for event 2n-1
;
;       Destination states are specified relative to this state:
;       0000 = +0 (self transition)
;       0001 = +1
;       ...
;       0111 = +7
;       1000 = +0 (no transition - event inactive)
;       1001 = -7
;       ...
;       1111 = -1

The difference between a self transition (0000) and a no transition (1000) is that a self transition resets the timer associated with this state, but a no transition does not. Also, a transition off either end of the state table wraps back to the other end. So in the common 32-state case, a +1 transition from state 31 will give state 0, and a -1 from state 0 will give state 31.

;
; Initial state is always state 0
; States are numbered 0-9, A-Z for convenience.
; Maximum number of states is set by available EEPROM space (128B)
; 
;-----------------------------------------------------------------
;
; Defines

These define how many events there are, and the offsets to the motor byte and the start of the responses to events within a particular state block. STATE_SIZE is the total number of bytes in a state block - for 6 events, this works out as 4.

.equ    STATE_NUMEVENTS =$06    ; timeout, eyes1-3, bump1-2

.equ    STATE_MOTOR     =$00
.equ    STATE_RESPONSE  =$01
.equ    STATE_SIZE      =$01+(STATE_NUMEVENTS+1)/2

Using the above, we calculate how many state blocks will fit in the eeprom, and how much space they actually take.

.equ    STATE_COUNT     =(128/STATE_SIZE)
.equ    STATE_SPACE     =(STATE_SIZE*STATE_COUNT)

Some masks which will divide up the motor byte into the correct parts.

.equ    STATE_mMOTORL   =(mMOTORLA|mMOTORLB)
.equ    STATE_mMOTORR   =(mMOTORRA|mMOTORRB)
.equ    STATE_mMOTORS   =mMOTORS
.equ    STATE_mTIMEOUT  =$07

Some masks which will divide up the trigger byte into the correct parts.

.equ    mEVENT_TIMEOUT  =$01
.equ    mEVENT_BUMPF    =$02
.equ    mEVENT_BUMPB    =$04
.equ    mEVENT_EYEL     =$08
.equ    mEVENT_EYER     =$10
.equ    mEVENT_EYEB     =$20

;-----------------------------------------------------------------
;
; Macros
;
; Multiply register by state size.
; May use rSCRATCH as temporary register
; use: mulss rN
; rN <- rN * STATE_SIZE

.macro  mulss
        lsl     @0
        lsl     @0
.endmacro

;-----------------------------------------------------------------
;
; Variables

.dseg

state_current:  .byte   1       ; pointer to current state block
state_ticklo:   .byte   1       ; tick count for timeout
state_tickhi:   .byte   1       ; tick count for timeout
state_events:   .byte   1       ; bitvector for triggered events

state_randoma:  .byte   1       ; state for random number generator
state_randomb:  .byte   1
state_randomc:  .byte   1

state_lastbump: .byte   1       ; last bump switch state
state_lasteye:  .byte   1       ; last eye proximity state

;-----------------------------------------------------------------
;
; Code
;
; Y is assumed to point to the RAM.
; RAM is assumed to be cleared to 0's on reset.

.cseg

;-----------------------------------------------------------------
;
; reset_state
;
; Reset state machine, update random number generator, etc.

reset_state:
        ldi     rTEMP,(mEVENT_BUMPF|mEVENT_BUMPB)
        
        sty     state_lastbump,rTEMP    ; and store last bump state

        ;Init the random number generator 
        
        sty     state_randoma,rTEMP     ;since a 00,00,00 state will not 
        sty     state_randomb,rTEMP     ;progress.
        sty     state_randomc,rTEMP     ;

        ; set pullups on port D for bump sensors

        sbi     PORTD,bBUMPF    
        sbi     PORTD,bBUMPB

        clr     zl
        
        rcall   state_load              ; load the first state
        
        in      rTEMP2,PORTB            ; but output a zero motor state to I/O port.
        cbr     rTEMP2,mMOTORS
        out     PORTB,rTEMP2
        
        ret
        
;-----------------------------------------------------------------
;
; state_load
;
; Load the current state and reinitialize state machine for
; this state. New current state pointer passed in in Z.

Within the state machine, current states are stored and manipulated as pointers. This saves us doing multiplies all the time. Here, we are setting a new state so we need to read and set the motor states...

state_load:
        sty     state_current,zl
        
        rcall   state_readee
        mov     rTEMP,rRETURN
        mov     rTEMP2,rRETURN
        
        andi    rTEMP,STATE_mMOTORL
        cpi     rTEMP,STATE_mMOTORL
        brne    state_loadml
        
        push    rTEMP2
        rcall   state_random
        rcall   state_random
        andi    rTEMP,STATE_mMOTORL
        pop     rTEMP2
        
state_loadml:
        
        andi    rTEMP2,STATE_mMOTORR
        cpi     rTEMP2,STATE_mMOTORR
        brne    state_loadmr
        
        push    rTEMP
        rcall   state_random
        rcall   state_random
        add     rTEMP,rTEMP     ; need to shift this
        add     rTEMP,rTEMP     ; else we get same 2 bits as above
        andi    rTEMP,STATE_mMOTORR
        mov     rTEMP2,rTEMP
        pop     rTEMP
        
state_loadmr:
        or      rTEMP,rTEMP2
        
        ; output motor state to I/O port.
        
        in      rTEMP2,PORTB
        cbr     rTEMP2,mMOTORS
        or      rTEMP2,rTEMP
        out     PORTB,rTEMP2

...and set the new timeout value for this state. This is (1<<TIMEOUT)+16 ticks of the main state machine clock.

        ; load tick count
        
        ldi     rTEMP,1
        rcall   state_readee
        mov     rTEMP2,rRETURN
        andi    rTEMP2,STATE_mTIMEOUT
        rjmp    state_loadloopstart
        
state_loadloop:
        add     rTEMP,rTEMP
        dec     rTEMP2
        
state_loadloopstart:
        brne    state_loadloop
        
        ldi     rTEMP2,$10              ; leeway
        sty     state_ticklo,rTEMP2
        sty     state_tickhi,rTEMP

        ret
        
;-----------------------------------------------------------------
;
; update_state
;
; Update current state from bits in state_events. Bit 0 = event 0,
; etc. Also reads eyes.

This runs through each possible event source in turn, and if an event has happened it will trigger a transition to a new state.

update_state:
        rcall   read_eyes
        
        ; Uncomment this block to use eye *transitions* rather than states
        ; to trigger events.
        ;
        ; ldy   rTEMP,state_lasteye
        ; sty   state_lasteye,rRETURN
        ;
        ; eor   rTEMP,rRETURN           ; catch transitions
        
        mov     rTEMP,rRETURN
        
        lsl     rTEMP
        lsl     rTEMP
        lsl     rTEMP                   ; shift up to proper place
        
        ldy     rPARAM1,state_events    ; get events
        or      rPARAM1,rTEMP           ; or in the eyes
        sty     state_events,rPARAM1    ; and store
        
        ;
        ; Now actually check all the bits in rPARAM1 in turn
        ;
        
        ldi     rPARAM2,$01             ; mask to clear bit later
        ldy     zl,state_current
        ldi     rLOOPI,STATE_NUMEVENTS-1
        
update_state_loop:
        inc     zl
        rcall   state_readee

        sbrc    rPARAM1,0
        rcall   update_state_now        ; update state with HIGH nybble
        
        lsr     rPARAM1
        lsl     rPARAM2
        
        dec     rLOOPI
        brmi    update_state_exit

        swap    rRETURN

        sbrc    rPARAM1,0
        rcall   update_state_now        ; update state with HIGH nybble

        lsr     rPARAM1
        lsl     rPARAM2

        dec     rLOOPI
        brpl    update_state_loop

update_state_exit:
        ret
        
update_state_now:
        ; update state according to high nybble of rRETURN.     
        
        ; clear event bit
        
        ldy     rTEMP,state_events
        com     rPARAM2
        and     rTEMP,rPARAM2
        com     rPARAM2
        sty     state_events,rTEMP

        ; is this a null transition? if so, continue.

        mov     rTEMP,rRETURN
        andi    rTEMP,$F0
        cpi     rTEMP,$80
        breq    update_state_notrans    ; null transition - skip
        
        ; otherwise, we have a real event causing a transition
        
        ldy     zl,state_current        ; get current state for later on, in case we do a self transition
        mov     rLOOPJ,rLOOPI           ; save this for later too

        clr     rLOOPI                  ; no more loops
        
        cpi     rTEMP,$00
        breq    update_state_selftrans  ; self transition - short cut, no uart output

        ; sign extend difference
        
        asr     rTEMP
        asr     rTEMP
        asr     rTEMP
        asr     rTEMP
        
        mulss   rTEMP           ; get difference
        add     zl,rTEMP        ; and add
        brpl    update_state_nneg
        
        subi    zl,-STATE_SPACE ; adjust if -ve.
        rjmp    state_load
        
update_state_nneg:
        cpi     zl,STATE_SPACE
        brcs    update_state_novr
        
        subi    zl,STATE_SPACE
        
update_state_novr:

        ;
        ; Write triggered event to UART, and continue
        ;
        
If a state change happens, then a debugging message is sent to the UART. This is of the form: "C:E->N" where C is the current state, E is the event triggered, and N is the new state. States are numbered from 0..9 then from A..Z (or less far if fewer states are available).

        push    zl
        ldy     rPARAM1,state_current
        rcall   monitor_statetochar
        rcall   uart_writechar

        ldi     rPARAM1,':'
        rcall   uart_writechar

        ldi     rPARAM1,STATE_NUMEVENTS-1+'0'
        sub     rPARAM1,rLOOPJ
        rcall   uart_writechar

        ldi     rPARAM1,'-'
        rcall   uart_writechar
        ldi     rPARAM1,'>'
        rcall   uart_writechar
        mov     rPARAM1,zl
        rcall   monitor_statetochar
        rcall   uart_writechar

        rcall   uart_writecrlf
        pop     zl

update_state_selftrans:
        rjmp    state_load

update_state_notrans:
        ; push  rPARAM1
        ; rcall uart_writecrlf
        ; pop   rPARAM1
        ret
                
;-----------------------------------------------------------------
;
; state_tick
;
; Tick counter for state machine timeout values, etc.

state_tick:
        ldy     rISCRATCH,state_ticklo
        dec     rISCRATCH
        sty     state_ticklo,rISCRATCH

        brne    state_tickend
        
        ; 1/8 s basic tick. Sample the bump switches.
        
        in      rITEMP2,GIFR            ; read IFR for bumps
        out     GIFR,rITEMP2            ; and reset set bits
        
        ; uncomment next block to allow events triggered on
        ; bump switch TRANSITIONS, not levels.

        ; ldy   rITEMP,state_lastbump
        ; sty   state_lastbump,rITEMP2
        
        ; eor   rITEMP2,rITEMP          ; catch TRANSITIONS not LEVELS
        
        swap    rITEMP2
        lsr     rITEMP2
        andi    rITEMP2,(mEVENT_BUMPF|mEVENT_BUMPB)

        ldy     rITEMP,state_events
        or      rITEMP,rITEMP2          ; indicate events
        
        ; now check for timeout
        
        ldy     rISCRATCH,state_tickhi
        dec     rISCRATCH

        sty     state_tickhi,rISCRATCH

        brne    state_notimeout
        
        ori     rITEMP,mEVENT_TIMEOUT   ; indicate events
        
state_notimeout:
        sty     state_events,rITEMP     ; and put event status back
        
state_tickend:
        ret

;-----------------------------------------------------------------
;
; state_readee
;
; Read from eeprom, address Z, return in rRETURN

state_readee:
        sbic    EECR,EEWE       ; check not writing
        rjmp    state_readee
        
        out     EEAR,zl
        sbi     EECR,EERE
        in      rRETURN,EEDR
        ret

;-----------------------------------------------------------------
;
; state_random
;
; Random number generator
; Maximal length 19 bit shift register sequence, adapted from the
; "getting started" notes of Dave van Horn. Thanks, Dave!

state_random:
        ;  rSCRATCH3    rSCRATCH2    rSCRATCH
        ;22222111 11111110 00000000
        ;43210987 65432109 87654321
        
        ldy     rSCRATCH,state_randoma
        ldy     rSCRATCH2,state_randomb
        ldy     rSCRATCH3,state_randomc

        mov     rTEMP,rSCRATCH          ;Make copy
        mov     rTEMP2,rSCRATCH3        ;Make copy

        rol     rSCRATCH                ;Shift the bits D7->Carry
        rol     rSCRATCH2               ;Carry->D0 D7->Carry
        rol     rSCRATCH3               ;Carry->D0 D7->Carry, but that will be fixed.

        ;Now we have to Xor the bits to see what
        ;goes in to bit 1

        rol     rTEMP2                  ;Move bit 19->20
        andi    rTEMP2,$08              ;Important that D1,0 be zero

        andi    rTEMP,$13               ;Mask out irrelevants, protecting bit 19
        or      rTEMP2,rTEMP            ;Get bits 5->21 2->18 1->17

        andi    rTEMP2,$18              ;Nuke bits 18,17
        lsr     rTEMP2                  ;19->19 5->20
        lsr     rTEMP2                  ;19->18 5->19
        lsr     rTEMP2                  ;19->17 5->18

        eor     rTEMP2,rTEMP            ;First xor, result in rTEMP2 (5x2 in 02 and 19x1 in 01)
        mov     rTEMP,rTEMP2            ;Make a copy
        ror     rTEMP                   ;Move the 5x2 result to 01
        andi    rTEMP2,$01              ;Mask off everything else
        andi    rTEMP,$01               ;in both
        eor     rTEMP2,rTEMP            ;
        brne    state_random_1          ;If one, do that, else 

state_random_0: 
        ;Set a zero in the lsb of the low byte
        mov     rTEMP,rSCRATCH          ;Get the low byte
        andi    rTEMP,$FE               ;Make the LSB zero
        mov     rSCRATCH,rTEMP          ;Put it back
        rjmp    state_random_exit       ;Bye bye

state_random_1: 
        ;Set a one in the lsb of the low byte
        mov     rTEMP,rSCRATCH          ;Get the low byte
        ori     rTEMP,$01               ;Make the LSB one
        mov     rSCRATCH,rTEMP          ;Put it back

state_random_exit:
        sty     state_randoma,rSCRATCH
        sty     state_randomb,rSCRATCH2
        sty     state_randomc,rSCRATCH3

        mov     rRETURN,rSCRATCH        ; and return it

        ret

;-----------------------------------------------------------------

;-----------------------------------------------------------------
;
; Default state machine
; Derived from program loaded into monitor as in the comments

.eseg
.org    0
                                ;  +------------------------ state number
                                ;  |  ++-------------------- motor state
                                ;  |  ||  +----------------- timeout value
                                ;  |  ||  |                  Next state...
                                ;  |  ||  |   +------------- ...on timeout
                                ;  |  ||  |   | ++---------- ...on bump switches, fore or back
                                ;  |  ||  |   | || +++------ ...on eye activation, left, right or back
                                ;  |  ||  |   | || |||
        .db     $F5,$13,$25,$42 ; s0  ??  5   1 32 542     ; exploring  4s, run away
        .db     $F5,$62,$14,$31 ; s1  ??  5   7 32 542     ; exploring  4s, run away
        .db     $93,$E1,$83,$28 ; s2  FF  3   0 3- 54-     ; shy forward
        .db     $63,$D8,$F8,$8F ; s3  RR  3   0 -2 --2     ; shy backward
        .db     $23,$FF,$EF,$8E ; s4  R0  3   3 32 3-2     ; reverse left
        .db     $43,$EE,$D8,$ED ; s5  0R  3   3 32 -32     ; reverse right
        .db     $F5,$AD,$CF,$EC ; s6  ??  5   0 32 542     ; exploring  4s, run away
        .db     $F5,$92,$14,$31 ; s7  ??  5   0 98 BA8     ; exploring  4s; aggressive!
        .db     $63,$E1,$83,$28 ; s8  RR  3   6 9- BA-     ; attack backward, then run away
        .db     $93,$D8,$F8,$8F ; s9  FF  3   6 -8 --8     ; attack forward, then run away
        .db     $13,$FF,$EF,$8E ; sA  F0  3   9 98 9-8     ; attack right, then forward, then run away
        .db     $83,$EE,$D8,$ED ; sB  0F  3   9 98 -98     ; attack left, then forward, then run away
        
Examination of the above machine reveals why the thing was so cowardly on the day. It only has one aggressive state, from which any and all transitions lead to nonaggressive states! This was done to avoid it getting stuck in a state where it continually attacked a wall or some other obstacle, but in reality it proved to be far too cautious.

;-----------------------------------------------------------------

Debug Monitor

In order to aid debugging and allow the personality to be changed using only a RS232 terminal, I included a simple debug monitor.

MONITOR.ASM

This file defines the debug monitor.

;-----------------------------------------------------------------
;
; Name:         monitor.asm
; Title:        Robot monitor service
; Version:      1.0
; Last updated: 2001.03.19
; Target:       AT90S2313
;
;-----------------------------------------------------------------
;
; DESCRIPTION
;
; Monitor service which runs from the serial port.
; 
; This implements a simple monitor for influencing the behavior
; of the robot and loading eeprom data tables, etc.
;
; Command set
;
; ;     
;       Comment - rest of line ignored
;
; 0..7
;       Trigger event 0..7 (6,7 reserved for now)
;
; d<s>  
;       Display state <s>
;
; s<s> mm t s s s s s s
;       mm=motor state (0,F,R,?)
;       t=timeout 0-7
;       s=destination state
;
; States are numbered 0-9, A-Z (or a-z)
;
; Execution of the state machine for the robot is suspended
; until completion of a monitor command with trailing crlf
; 
;-----------------------------------------------------------------

.dseg

Thes variables define the state of the monitor. The selected state is in monitor_select, and the state block being constructed is in monitor_sblock. The state block is only loaded into the eeprom when fully constructed, to prevent errors causing too much havoc.

monitor_select: .byte   1       ; selected state
monitor_sblock: .byte   4       ; state block

;-----------------------------------------------------------------

.cseg

;-----------------------------------------------------------------
;
; reset_monitor
; Reset monitor status. Should be called after reset_uart.

reset_monitor:
        ldi     zl,low(monitor_welcome*2)
        ldi     zh,high(monitor_welcome*2)
monitor_exitmessage:
        rcall   uart_writestringlf
        clr     zh
        ret
        
;-----------------------------------------------------------------
;
; update_monitor
; Main monitor routine.

This just sits and listens for characters on the UART. If nothing has been received, it goes straight back to the main loop.

update_monitor:
        rcall   uart_charready
        brne    update_monitor_charready
        ret

update_monitor_charready:
        rcall   monitor_readchar
        
First, we check for digits 0-7 for event triggering.

        cpi     rPARAM1,'0'
        brlt    update_monitor_notdigit
        cpi     rPARAM1,'8'
        brlt    update_monitor_event

Check for carriage returns or comments. We can't use the character literal ';' because even though it's in single quotes it still signals the start of a comment.

update_monitor_notdigit:
        cpi     rPARAM1,$0d
        breq    update_monitor_comment
        cpi     rPARAM1,$0a
        breq    update_monitor_comment
        cpi     rPARAM1,$3B                     ; semicolon - can't use ';' because it starts a comment. Bleah.
        breq    update_monitor_comment

OK, so now we check for the main commands d (for display) and s (for state load). If it's not one of them, we emit a * as an error marker and hop into the error routine.

        cpi     rPARAM1,'d'
        breq    update_monitor_displaystate
        
        cpi     rPARAM1,'s'
        breq    update_monitor_setstate

        ldi     rPARAM1,'*'
        rcall   uart_writechar
        
        rcall   monitor_ignoreline_charread
        rjmp    update_monitor_error
        
For comments, we just ignore the rest of the line.

update_monitor_comment:
        rcall   monitor_ignoreline_charread
        rjmp    update_monitor_exit
        
For event triggers, we work out which event should be triggered and write the appropriate bit into the state machine's events variable. To the state machine, this is indistingushable from a real event.

update_monitor_event:
        ldi     rTEMP,$01
update_monitor_event_loop:
        cpi     rPARAM1,'0'
        breq    update_monitor_event_set
        lsl     rTEMP
        dec     rPARAM1
        rjmp    update_monitor_event_loop
update_monitor_event_set:
        ldy     rTEMP2,state_events
        or      rTEMP2,rTEMP
        sty     state_events,rTEMP2
        rjmp    update_monitor_exit

This displays the current contents of a particular state block in the same format as it was entered. The next character after the d is taken to be the state block number and the rest of the line is ignored. Only when an end-of-line character is received is the actual state transmitted to the RS232 port.

update_monitor_displaystate:
        rcall   monitor_readchar
        rcall   monitor_ignoreline      ; ignore rest of line before continuing
        
        rcall   monitor_chartostate
        
        rcall   uart_writecrlf
        
update_monitor_showstate:
        mov     zl,rRETURN
        rcall   state_readee            ; get motor bits etc.
        
        mov     rTEMP,rRETURN
        ldi     rPARAM1,'0'
        andi    rTEMP,STATE_mMOTORL     ; motor bits
        breq    update_monitor_motorl

        ldi     rPARAM1,'R'
        cpi     rTEMP,mMOTORLA
        breq    update_monitor_motorl
                
        ldi     rPARAM1,'F'
        cpi     rTEMP,mMOTORLB
        breq    update_monitor_motorl
        
        ldi     rPARAM1,'?'
update_monitor_motorl:
        rcall   uart_writechar
        
        mov     rTEMP,rRETURN
        ldi     rPARAM1,'0'
        andi    rTEMP,STATE_mMOTORR     ; motor bits
        breq    update_monitor_motorr

        ldi     rPARAM1,'R'
        cpi     rTEMP,mMOTORRA
        breq    update_monitor_motorr
                
        ldi     rPARAM1,'F'
        cpi     rTEMP,mMOTORRB
        breq    update_monitor_motorr
        
        ldi     rPARAM1,'?'
update_monitor_motorr:
        rcall   uart_writechar
        
        ldi     rPARAM1,' '
        rcall   uart_writechar
        
        mov     rPARAM1,rRETURN
        rcall   uart_writehexit
        
        ldi     rPARAM1,' '
        rcall   uart_writechar
        
        ldi     rLOOPI,3

update_monitor_ds_loop:
        inc     zl
        rcall   state_readee
        
        mov     rPARAM2,rRETURN
        rcall   update_monitor_display_dest
        swap    rPARAM2
        rcall   update_monitor_display_dest

        dec     rLOOPI
        brne    update_monitor_ds_loop
        
        rjmp    update_monitor_exit


This dsets the current contents of a particular state block. The next character after the s is taken to be the state block number and the rest of the line is used to determine what the state block contents are. The format of this information is shown in the comment at the top of the file. Only when an end-of-line character is received is the actual state block created, and if there are any errors then a * is emitted at the error position along with an error message and the state block is not loaded into the eeprom.

update_monitor_setstate:
        rcall   monitor_readchar        ; read state character
        
        rcall   monitor_chartostate     ; convert to pointer
        mov     zl,rRETURN
        cpi     zl,EEEND
        brlt    update_monitor_ss_ok1

        ;
        ; Note: This is interleaved here in order that it's not too far away from
        ; the code that uses it.
        ;
        
update_monitor_ss_error:        
        ldi     rPARAM1,'*'
        rcall   uart_writechar
        rcall   monitor_ignoreline      ; ignore rest of line before continuing
        rjmp    update_monitor_error

update_monitor_ss_ok1:
        sty     monitor_select,zl       ; store state pointer
        
        rcall   monitor_readchar        ; read first motor character
        clr     rPARAM2                 ; rPARAM2 = first state block byte
        
        cpi     rPARAM1,'0'
        breq    update_monitor_ss_r

        ori     rPARAM2,mMOTORLA
        cpi     rPARAM1,'R'
        breq    update_monitor_ss_r
        cpi     rPARAM1,'r'
        breq    update_monitor_ss_r

        ori     rPARAM2,mMOTORLB
        cpi     rPARAM1,'?'
        breq    update_monitor_ss_r

        cbr     rPARAM2,mMOTORLA
        cpi     rPARAM1,'F'
        breq    update_monitor_ss_r
        cpi     rPARAM1,'f'
        brne    update_monitor_ss_error

update_monitor_ss_r:
        rcall   monitor_readchar        ; read 2nd motor character

        cpi     rPARAM1,'0'
        breq    update_monitor_ss_t

        ori     rPARAM2,mMOTORRA
        cpi     rPARAM1,'R'
        breq    update_monitor_ss_t
        cpi     rPARAM1,'r'
        breq    update_monitor_ss_t

        ori     rPARAM2,mMOTORRB
        cpi     rPARAM1,'?'
        breq    update_monitor_ss_t

        cbr     rPARAM2,mMOTORRA
        cpi     rPARAM1,'F'
        breq    update_monitor_ss_t
        cpi     rPARAM1,'f'
        brne    update_monitor_ss_error

update_monitor_ss_t:
        rcall   monitor_readchar        ; read timeout character
        
        subi    rPARAM1,'0'
        brcs    update_monitor_ss_error
        cpi     rPARAM1,8
        brge    update_monitor_ss_error
        
        or      rPARAM2,rPARAM1
        
        ldi     xl,monitor_sblock       ; point to state block to fill in
        st      x+,rPARAM2              ; store in parameter block
        
        clr     rPARAM2

        ldi     rLOOPI,6                ; now read 6 state characters
        
update_monitor_ss_loop:
        rcall   monitor_readchar        ; read state character
        
        ldi     rTEMP,$08
        
        cpi     rPARAM1,'-'             ; null transition?
        breq    update_monitor_ss_null
        
        rcall   monitor_chartostate     ; get destination state
        mov     rTEMP,rRETURN           ; get destination state
        ldy     rTEMP2,monitor_select   ; get current state
        sub     rTEMP,rTEMP2            ; subtract
        asr     rTEMP
        asr     rTEMP                   ; /4 = state number
        subi    rTEMP,-16               ; add 16 to make the compares easier
        cpi     rTEMP,(16-7)
        brlt    update_monitor_ss_error2 ; too far back - error
        cpi     rTEMP,(16+8)
        brlt    update_monitor_ss_ok2

update_monitor_ss_error2:
        rjmp    update_monitor_ss_error ; too far forward - error
        
update_monitor_ss_ok2:
        andi    rTEMP,$0F               ; bottom nybble only

update_monitor_ss_null:
        or      rPARAM2,rTEMP           ; or in the state
        
        mov     rTEMP,rLOOPI            ; get current loop counter
        andi    rTEMP,$01               ; odd or even?
        breq    update_monitor_ss_even
        
        st      x+,rPARAM2              ; store state

        clr     rPARAM2                 ; and clear ready for next loop
        
        rjmp    update_monitor_ss_next

update_monitor_ss_even:
        swap    rPARAM2                 ; swap nybble up into top of state

update_monitor_ss_next:
        dec     rLOOPI
        brne    update_monitor_ss_loop
        
        ;
        ; Here, we should have a complete state block. Write it to the eeprom.
        ;
        
        ldy     zl,monitor_select
        ldi     xl,monitor_sblock
        ldi     rLOOPI,4                ; 4 bytes to write to eeprom
        
update_monitor_ss_write:
        ld      rPARAM1,x+
        rcall   monitor_write_eeprom    ; write rPARAM1 to eeprom at Z
        inc     zl

        dec     rLOOPI
        brne    update_monitor_ss_write

        rcall   monitor_ignoreline      ; ignore rest of line before continuing
        rcall   uart_writecrlf
        rjmp    update_monitor_exit

update_monitor_error:
        ldi     zl,low(monitor_errormsg*2)
        ldi     zh,high(monitor_errormsg*2)
        rjmp    monitor_exitmessage     

update_monitor_exit:
        ldi     zl,low(monitor_exitmsg*2)
        ldi     zh,high(monitor_exitmsg*2)
        rjmp    monitor_exitmessage
        
;-----------------------------------------------------------------
;
; update_monitor_display_dest
; Takes zl = current state, rPARAM2 (high bits) = transition code

update_monitor_display_dest:
        mov     rPARAM1,rPARAM2
        andi    rPARAM1,$F0
        cpi     rPARAM1,$80
        brne    update_monitor_dd_non0
        ldi     rPARAM1,'-'
        rjmp    uart_writechar

update_monitor_dd_non0:
        asr     rPARAM1
        asr     rPARAM1
        add     rPARAM1,zl
        rcall   monitor_statetochar
        rjmp    uart_writechar
        
;-----------------------------------------------------------------
;
; monitor_ignoreline
; Ignore the rest of the line. Call monitor_ignoreline_charread if
; the first character of the rest of the line has already been read.
; Preserves rPARAM1

monitor_ignoreline_charread:
        push    rPARAM1
        rjmp    monitor_ignoreline_charread2

monitor_ignoreline:
        push    rPARAM1
        
monitor_ignoreline_loop:
        rcall   monitor_readchar

monitor_ignoreline_charread2:
        cpi     rPARAM1,$0D
        breq    monitor_ignoreline_exit
        cpi     rPARAM1,$0A
        brne    monitor_ignoreline_loop
        
monitor_ignoreline_exit:
        pop     rPARAM1
        ret

;-----------------------------------------------------------------
;
; monitor_chartostate
; Convert incoming character in rPARAM1 to state pointer in rRETURN

monitor_chartostate:
        cpi     rPARAM1,'A'
        brlt    monitor_chartostate_2

        andi    rPARAM1,$FF-$20         ; assume caps
        subi    rPARAM1,7
        
monitor_chartostate_2:
        subi    rPARAM1,'0'
        mov     rRETURN,rPARAM1
        lsl     rRETURN
        lsl     rRETURN
        ret
        
;-----------------------------------------------------------------
;
; monitor_statetochar
; Convert state pointer in rPARAM1 to character in rPARAM1

monitor_statetochar:
        lsr     rPARAM1
        lsr     rPARAM1
        
        cpi     rPARAM1,10
        brlt    monitor_statetochar_2
        
        subi    rPARAM1,-7
        
monitor_statetochar_2:
        subi    rPARAM1,-'0'
        ret

;-----------------------------------------------------------------
;
; monitor_readchar
; Read character with echo back to host. Character is in BOTH rPARAM1
; and rRETURN. Skips spaces

monitor_readchar:
        rcall   uart_readcharwait
        mov     rPARAM1,rRETURN
        rcall   uart_writechar
        cpi     rPARAM1,' '
        breq    monitor_readchar
        ret

;-----------------------------------------------------------------
;
; monitor_write_eeprom
; Write rPARAM1 to eeprom at Z

monitor_write_eeprom:
        sbic    EECR,EEWE               ;if EEWE not clear
        rjmp    monitor_write_eeprom    ;    wait more

        out     EEAR,zl         ; output address
        out     EEDR,rPARAM1    ; output data
        
        cli                     ; critical section
        sbi     EECR,EEMWE      ;   set master write enable, remove if 1200 is used     
        sbi     EECR,EEWE       ;   set EEPROM Write strobe
                                ;   This instruction takes 4 clock cycles since
                                ;   it halts the CPU for two clock cycles
        sei                     ; end critical section
        
        ret

;-----------------------------------------------------------------
;
; Constant Data

;-----------------------------------------------------------------
;
; Message

monitor_welcome:
        .db     "Monitor ready",$00
monitor_errormsg:
        .db     $0d,$0a,"Error at *",$00
monitor_exitmsg:
        .db     " OK",$00

;-----------------------------------------------------------------

Build Process

All of these files should be loaded into an AVR studio project, with ROBOT.ASM being the main target file. The build process should complete with no errors or warnings, and is then ready to be uploaded into the 2313 using whatever programming hardware is appropriate. I use a simple dongle and PonyProg, although I have just taken delivery of Atmel's STK500 starter kit which can also act as a serial ISP programmer so I will be looking at that in some detail later on.