Sunday, 12 October 2014

Oshonsoft AVR simulator software serial gotcha (update: AVR isr gotcha)

A while back, I asked Vladimir Soso - the guy behind the Oshonsoft series of compilers and simulators for PIC and AVR microcontrollers - if he had considered changing his software serial implementation, to include a "timeout" option.

At the time, the Oshonsoft project had gone quiet, and no updates were forthcoming, so I moved over to SourceBoost C and started re-writing some of my own "libraries" of code from the PIC Simulator IDE sources.

Then, not so long ago, the whole Oshonsoft came back to life! And in it, a few updates - including implementing some of the suggestions made by users, such as my software serial idea.

Without a timeout, any time an instruction like:
SERIN rx, 9600, b
is met, the code hangs, waiting for a full byte to be received into the buffer. If no byte arrives, the code simply stops at this instruction, until one has been received.

With the latest updates, the compiler supports two new DEFINE directives, which allow you to say "if this interrupt event occurs, stop waiting for an incoming serial byte and get on with the  rest of the code". For example, we can use

Define SERIN_TIMEOUT_REG = TIFR
Define SERIN_TIMEOUT_BIT = 2

which tells the mcu that if the Timer1 overflow interrupt flag is set, the software serial read function should be aborted. But implementing this includes a couple of gotchas, which can be quite tricky to get to the bottom of, so - for future reference - here goes:

The idea behind our test code is to send some data over serial, wait for a couple of seconds, and if nothing is received back from the host, to load some default values (and if serial data is received, to use this to set up the initialisation routine). It wasn't possible to use the hardware UART (so interrupt driven serial communication wasn't possible) to I needed some way to say "start listening for serial data, but after x number of seconds, give up and do this instead".
The new Oshonsoft routines provide exactly this functionality, if coupled with the timer1 module (set to run on a /256 prescaler, so, at 8Mhz, it takes about 2 seconds to rollover).

The first gotcha (and this kept us guessing for a few hours) is that any unhandled interrupt can cause an AVR microcontroller to reset. So if - as I did - you enable the timer1 overflow interrupt, but don't write a timer1 overflow interrupt "vector" (i.e. the code to handle the interrupt) then the mcu will reset when the interrupt fires.

Frustratingly, it's not always immediate - so there's little correlation between the interrupt firing and the mcu resetting. I spent hours checking and double-checking the pull-up resistor on the reset line and the fuse settings (watch dog, brown out etc) during programming in the mistaken belief that something else could be causing the mcu to reset.

So if you don't have an interrupt routine, the mcu resets.
The obvious solution is to write an interrupt handler.
So I did.

This stopped the mcu from resetting, but the serial-in function didn't abort as it should. I put the equivalent of "trace statements" in the interrupt handler (made the LED flash a number of times, at a particular rate) to make sure that it was firing correctly - and it was - but this didn't help.

The answer, in this case, was simply to disable the interrupt-set flag in  the TIMSK register. This is the flag that says "when timer1 rolls over, fire an interrupt". The ability for the SerIn routine to monitor the state of the OVF1 flag is built in - it doesn't need an "external" interrupt handler to make it abort.

In fact, if an interrupt handler is written for the event that has been designated to abort the SerIn routine, it seems to disable the ability of that interrupt flag to force the abort. So in this case, the answer was to set up the timer1 as a free-running counter

'reset timer1 to zero
TCNT1H = 0x00
TCNT1L = 0x00

'set timer1 to overflow after about 2 seconds
TCCR1A = 0x00  'user timer1 in normal/counter mode
TCCR1B = 0x04  'use /256 prescaler at 8Mhz ~= 2sec

but specifically unset the flag (usually used) to create an interrupt event:
'disable timer1 interrupts
TIMSK.TOIE1 = 0

With these in place, it was a simple task to test that the abort routine was working as it should:
High LED
Serin rx, 9600, i
TCCR1B = 0x00  'turn off timer1
Low LED

With these amendments, the code started to work as it should. Here's the full listing. It doesn't actually do much by itself, but provides a starting point for using interrupts to abort a software-serial read.


declarations:

     Dim i As Byte
     Symbol tx = PORTA.5
     Symbol rx = PORTA.5
   
     Define SERIN_TIMEOUT_REG = TIFR
     Define SERIN_TIMEOUT_BIT = 2

init:

     'make all pins inputs
     ConfigPin PORTG = Input
     ConfigPin PORTF = Input
     ConfigPin PORTE = Input
     ConfigPin PORTD = Input
     ConfigPin PORTC = Input
     ConfigPin PORTB = Input
     ConfigPin PORTA = Input
   
     'enable pull-ups on all input pins
     '(in avr you do this by setting the output register
     'while putting the pin in an input state)
     PORTG = 0x0ff
     PORTF = 0xff
     PORTE = 0xff
     PORTD = 0xff
     PORTC = 0xff
     PORTB = 0xff
     PORTA = 0xff

     'disable the pull-up resistor on the
     'serial tx/rx duplexed line
     PORTA.5 = 0 'pin 46
         
     'just to show the chip is doing something!
     ConfigPin PG.0 = Output
     For i = 1 To 5
           High PG.0
           WaitMs 100
           Low PG.0
           WaitMs 100
     Next i
         
     'send a message back to the host
     ConfigPin PORTA.5 = Output
     Serout tx, 9600, "Ready"
     WaitMs 50
   
     'disconnect the serial line (put it in input
     'mode with no pull-up makes it high-impedance)
     ConfigPin PORTA.5 = Input
   
     'reset timer1 to zero
     TCNT1H = 0x00
     TCNT1L = 0x00

     'disable timer1 interrupts
     '(an interrupt vector stops the serin from aborting)
     TIMSK.TOIE1 = 0
               
     'set timer1 to overflow after about 2 seconds
     TCCR1A = 0x00 'user timer1 in normal/counter mode
     TCCR1B = 0x04 'use /256 prescaler at 8Mhz ~= 2sec
   
     'now try reading some serial data in, set to time out
     'when timer1 rolls over. If this works, we'll change the
     'speed of the flashing LED. If it doesn't, the speed
     'will stay at about one flash every 2 seconds
     High PG.0
     Serin rx, 9600, i
     TCCR1B = 0x00 'turn off timer1
     Low PG.0

loop:
     'do nothing
     High PG.0
     WaitMs 500
     Low PG.0
     WaitMs 500
Goto loop
End