Friday, December 21, 2012

ATtiny Software Serial TX in Assembly

ATtiny13 in one of my eeZee Tiny boards
Assembly language is way more fun than I remember. I implemented a very rudimentary software serial transmit on an ATtiny13. Really just to see if I could.

Receive is harder. I didn't do that because I don't really need it right now. If you dare me I might do it. But for now the communication is in one direction.

TTL Serial Protocol

Most typically, serial communication involves 8 data bits, no parity, and one stop bit (8N1). When sending TTL level (5V and 0V) serial, the idle state of the line is 5V. A start bit, 0V, is sent as the first bit, followed by the LSB of the data byte up through the MSB. After this, the stop bit is sent, a 5V signal.

I started with the code that would send the data byte. I planned to use a timer interrupt handler for timing the bits, but as an initial baby step, I just created a loop that would shift bits out one at a time.

I used r16 as the 1-byte buffer. The data placed in here is shifted out. A counter register, r17, counts the number of bits to send. It's set to 8 to begin with

.include "tn13Adef.inc" ; definitions for ATtiny13A
.def txreg = r16 ; transmit 'buffer'
.def bitcnt = r17 ; bit counter
.equ TXPIN = PB0 ; TX pin is PB0
.cseg
.org 0x00
rjmp start ; executed after reset

start:
ldi r16, 0x6D ; 'm' 
ldi r17, 8 ; put 8 in counter register
txloop:
lsr r16 ; shift off the next bit, LSB first
brcs Send1 ; if it is 1 (C=1) then send 1
Send0:
cbi PORTB, TXPIN ; send a 0=low
rjmp BitDone
Send1:
sbi PORTB, TXPIN ; send a 1=high
BitDone:
dec bitcnt ; bitcnt--
breq start ; if bitcnt == 0, start over
rjmp txloop ; else do the next bit

I used the Simulator debugger platform to watch the code and make sure it did what I wanted it to. In AVRStudio 4, choose Select platform and device... from the Debug menu and select Simulator and your device.


Next I added the logic to send the start bit. Instead of setting r17 to 8 I set it to 9 and then added special logic to send the start bit as the first bit.


start:
ldi r16, 0x6D ; 'm' 
ldi r17, 9 ; put 9 in counter register
txloop:
cpi r17, 9 ; if r17 == 9 (first bit)
breq Send0 ; send start bit (low)

lsr r16 ; shift off the next bit, LSB first
brcs Send1 ; if it is 1 (C=1) then send 1
Send0:
cbi PORTB, TXPIN ; send a 0=low
rjmp BitDone
Send1:
sbi PORTB, TXPIN ; send a 1=high
BitDone:
dec bitcnt ; bitcnt--
breq start ; if bitcnt == 0, start over
rjmp txloop ; else do the next bit


Finally I added logic to send the stop bit after the last data bit is sent or when bitcnt == 0. The stop bit is a high signal. Then, I reworked the code to run within an interrupt service routine.

  • The routine sends a stop bit (high/idle) whenever bitcnt == 0 or on bitcnt == 1
  • To send data, load the character into txreg then set bitcnt to 10: 8 data bits, 1 start bit, 1 stop bit
  • The routine will send a start bit then shift out 8 bits of data, then a stop bit



Timing

Tuned Delay Loop

A simple way to deal with timing is to implement precision delay loops. Here's a link to my latest version that simply pauses after setting a bit and pauses in between bytes. Works great.


I used this delay calculator. In this tool under Wine I could not generate a loop based on time input, but I could generate a loop by calculating cycles. A delay of 1000 cycles at 9.6MHz will give a 9600bps output assuming the chip's internal oscillator is correct. After tuning the loop, my Saleae Logic analyzer tells me the bitstream was clocking in at 4800.77Hz while sending out "U" (binary: 01010101) repeatedly. That's plenty close.

Timing With Timer

In the original version which I can't find now, the transmit routine runs off a timer interrupt handler. The ATtiny13 has one timer and you can set up an interrupt to occur on compare match.

In other words, you put the timer in Clear on Timer Compare (CTC) mode and then set the compare register OCR0A to some number, and when the timer counter TCNT0 reaches that number, it fires an interrupt and then resets the counter. This is the method to schedule interrupts to occur at fixed intervals.
  • Divide the clock by some value, 1, 8, 64, 256 or 1024 to prescale the timer
  • Select CTC mode and set OCR0A as a second divider
  • For a 4800Hz interrupt frequency from a 4.8MHz clock use /8 and /125
I set up the timer, put in the code to transmit a byte. And it didn't quite work. The bit pattern looked right on the oscilloscope, so I pulled out my newly purchased Open Bench Logic Sniffer.

4347 baud is nowhere close! No wonder this didn't work.
It told me the data was right but the baud rate was incorrect, 4347bps instead of 4800bps! I had to change the OCR0A from 125 to 112 to get 4826bps, then the correct data appeared on my serial terminal program. Will it be close enough to work with the transmitter? I'll test that later.

Close enough for the PC serial terminal to get the right data
The ATtiny13A is running off an internal 4.8MHz oscillator that's roughly calibrated at the factory. I'm finding that the oscillator frequency is very temperature sensitive. As the chip will be braving the Colorado elements, I'll have to hope I can find a more stable clock source.

Time Delay

Here's a nice tool for calculating and creating a fixed time delay routine

http://bretm.home.comcast.net/~bretm/avrdelay.html

That's it for now. I'll post more about the sensor project as I progress.

No comments:

Post a Comment