Lightweight software UART -> custom serial


Software UART to custom serial on an AtTiny45, and then going to the Peggy2 board

7 months ago, in the middle of the winter, it all started with this…

I remember spending around a month playing with the Peggy2 LED board, and after managing to solder all the 625 LEDs (this is no easy task ! 🙂 ) and fix some initial hardware issues, I was happy that it was finally working… the whole “chain” that is:

  1. the PSP would connect to Twitter and download my latest tweets
  2. it would then send the content via IrDA link to my newly created device
  3. then the AtMego328 of the Peggy2 would read that and then display it

To be more precise, it was “kind of” working, as the display was blinking quite a lot, due to all the unnecessary delays introduced by checking if there’s any new serial data. Also, the PSP had to keep sending the same information again and again, just to make sure it would reach the Peggy2.

I guess one can’t really escape the 80/20 rule, and after having enjoyed the initial proof of concept there was to be some “serious” work necessary if I wanted it to work correctly and look “professional”…

1st attempt – use a software serial based on interrupts

The 1st obvious solution to the “blinking” problem was to try and use interrupts rather than simply try to read serial data every now and then and then time out if nothing was available.

As you all know, this is really baaaad, as you spend so much time checking for nothing, and then when the data finally comes, it’s enough that you’re slightly late and you won’t be able to get it. My solution was working however, as the PSP sent the same string multiple times,  as a continuous array of concatenated strings separated by a special character.

I’ve updated the NewSoftSerial library from Arduiniana (thanks Mikal !) so that it takes 2 extra parameters:

  1. an end message tag
  2. a callback method

The idea is simple: the library uses interrupts to receive the serial data in the background, and when it encounters your special character it calls the callback method. This way then and only then you start reading from the buffers and do whatever you need with the data…. sweeeeeet…

Here’s the Arduino code:

#include <Peggy2.h>
#include <PeggyWriter.h>
#include <NewSoftSerial.h>

Peggy2 mPeggyFrame;
PeggyWriter mPeggyWriter;
PeggyScroller mPeggyScroller;

#define RX_PIN 15  // A1
#define TX_PIN 14 // A0
#define SPEED_BPS 9600
#define START_MSG_TAG '#'
#define END_MSG_TAG '%'
#define MSG_MAX_SIZE 100

volatile boolean mSerialWaiting = false;
char mDisplayText[MSG_MAX_SIZE];

NewSoftSerial mySerial(RX_PIN, TX_PIN, END_MSG_TAG, callbackSoftSerial);

void setup(){
//Serial.begin(115200);    // Beware this breaks the NewSofSerial if used !
// Call this once to init the hardware:
mPeggyFrame.HardwareInit();

mPeggyWriter.drawCharacterSequence("INIT", &mPeggyFrame, 3, 10);
//mPeggyScroller.init(&mPeggyFrame, &mPeggyWriter, 10, "INIT    ");

mySerial.begin(SPEED_BPS);
}

void loop(){
mPeggyFrame.RefreshAll(1);

if(mSerialWaiting){
if(readSoftwareSerial() > 0){
mPeggyFrame.Clear();
mPeggyWriter.drawCharacterSequence((char*)mDisplayText, &mPeggyFrame, 3, 10);
// mPeggyScroller.init(&mPeggyFrame, &mPeggyWriter, 10, (char*)strRead);
}

mSerialWaiting = false;
}
}

int readSoftwareSerial(){
if(! mySerial.available()) return -1;
int _char, i=0;
_char = mySerial.read();
if(_char >= 0){
while(_char != START_MSG_TAG && _char >= 0)  _char = mySerial.read();
_char = mySerial.read();
while(_char >= 0 && _char != END_MSG_TAG && i < 100){
mDisplayText[i] = _char;
i++;
_char = mySerial.read();
}

if(i==0){
mDisplayText[i] = 'E';
i++;
}

mDisplayText[i] = NULL; // end of string
}

return i;
}

void callbackSoftSerial(){
mSerialWaiting = true;
}

Now, this is all very nice, clean and quick in theory, but for some reason the display kept blinking !!!

It turned out that the IrDA device I created kept firing “fake” signals over the wires hence triggering the interrupts and making the display blink.

After roughly 1 week of frustration around this, I decided it was time to try something else…

2nd attempt – use an extra uC

It sounds counter intuitive and wasteful (and now at the end I can confirm it is…) but the only thing I could think of to get rid of the blinking due to the fake signals, was to have another small uC that would deal with the UART from the IrDA device and then, only when it has checked that it’s valid data and has extracted the relevant part of the string (between start and end tags), send only the relevant information to the Peggy2.

Preparing the AtTiny45 for work...

So there are 2 communications to be had:

  1. serial UART from the IrDA device to the new uC
  2. custom serial from the new uC to the Peggy2

Does it feel “wrong” ? Having to add a microcontroller that reads serial on one side and outputs serial on the other end… yes it does…

Anyhow, given that I was already making a compromise by adding extra hardware, I thought “it has to be something small and clean”… so here comes my “preferred” uC, the AtTiny45… it’s amazing how much power it concentrates in such a small package… I love it !

Now the problem with the AtTiny45 (and 25 or 85) is that it does NOT have any hardware serial interfaces… it has this USI interface, which, if my understanding is correct, is some sort of building bricks for other serial protocols so that you don’t write everything in software… but it felt quite tricky…

And the reason is because it is… a couple of weeks of frustration went by, during which I tried all the UART examples from Atmel : the AVR411, then AVR307 and finally the AVR305… what a pain…

I finally converted the AVR305 example to GCC assembler and mixed it with some C code in the main file (wow, so may things that I learnt, I guess experience is really “what you get when you don’t get what you want”… plus frustration obviously… 🙂 ) and here’s some nice code that actually works :


#define F_CPU 8000000UL
#include <avr/io.h>
#include <util/delay.h>
#define BUFFER_SIZE 50

uint8_t _buffer[BUFFER_SIZE];

/* SERIAL UART */

extern void initUART();
extern void forever();
extern void sendCharUART();
extern void rcvCharUART();

volatile uint8_t charToSendUART;
volatile uint8_t charReceivedUART;

#define START_MSG_TAG '#'
#define END_MSG_TAG '%'

/* END SERIAL UART */

/* CUSTOM SERIAL */

#define CUSTOM_SERIAL_PIN PB2
#define LED_PIN PB5
#define PORT PORTB
#define DDR	DDRB
#define PIN PINB
#define BYTE_SIZE = 8

const unsigned long T0 = 500; 	// in microseconds
const unsigned long T1 = 1500; //(T0 * 3);
const unsigned long START = 4000; //(T0 * 8);
const unsigned long END = 2500; //(T0 * 5);
const unsigned long BETWEEN_BITS = 250; //(T0 / 2);

void customSerialSendStart(){
// START pulse
PORT |= _BV(CUSTOM_SERIAL_PIN);   // high
_delay_us(START);
PORT &= ~_BV(CUSTOM_SERIAL_PIN);  // low
_delay_us(BETWEEN_BITS);
}

void customSerialSendEnd(){
// START pulse
PORT |= _BV(CUSTOM_SERIAL_PIN);   // high
_delay_us(END);
PORT &= ~_BV(CUSTOM_SERIAL_PIN);  // low
_delay_us(BETWEEN_BITS);
}

void customSerialSendByte(unsigned char data){
for(unsigned char i = 0; i < 8; i++){
PORT |= _BV(CUSTOM_SERIAL_PIN); // high
if(data & 0x01) _delay_us(T1);
else _delay_us(T0);
PORT &= ~_BV(CUSTOM_SERIAL_PIN);  // low
_delay_us(BETWEEN_BITS);
data >>= 1;  // get the next most significant bit
}
}

void initCustomSerial(){
// CUSTOM SERIAL EMISSION
// make SERIAL pin output
DDR |= _BV(CUSTOM_SERIAL_PIN);

// SERIAL PORT start LOW
PORT &= ~_BV(CUSTOM_SERIAL_PIN);

// make LED pin output
DDR |= _BV(LED_PIN);
PORT &= ~_BV(LED_PIN);  // heartbeat OFF
}

/* END CUSTOM SERIAL */

// VERY CAREFUL ! BEcause we use some registries in assembly r16 - r19 and GCC seems to put his vars in there, these have to be global !
uint8_t byteCount = 0;
uint8_t i = 0;

int main(void) {
initUART();
initCustomSerial();

while(1){
while(charReceivedUART != START_MSG_TAG) rcvCharUART(); // wait until we get a start
PORT |= _BV(LED_PIN); // heartbeat ON

// start storing data
rcvCharUART();

while((charReceivedUART != END_MSG_TAG) && (byteCount < BUFFER_SIZE)){
if(charReceivedUART != START_MSG_TAG){
_buffer[byteCount] = charReceivedUART;
byteCount ++;
}else{
byteCount = 0;
}

rcvCharUART();
}

// the message is all in memory, send it on the other side

customSerialSendStart();
for(i=0; i < byteCount; i++) customSerialSendByte(_buffer[i]);
customSerialSendEnd();

byteCount = 0;

PORT &= ~_BV(LED_PIN);  // heartbeat OFF

_delay_ms(100);
}
}

and the “lovely” assembly code (please do notice that there is no syntax highlighting, simply because the WordPress’  “sourcecode” tag doesn’t provide for assembly… you can’t get geekier than that ! 🙂 ) – based on AVR305 from Atmel:


;****************************
;*
;* Title		: Half Duplex Interrupt Driven Software UART
;*
;* Based on AVR305 from Atmel, translated to GCC assembler and with a couple of extra functions added.
;*
;***************************************************************************

#include <avr/io.h>

;***** Pin definitions
#define RxD	PB0
#define TxD	PB1
#define PORT _SFR_IO_ADDR(PORTB)
#define DDR	_SFR_IO_ADDR(DDRB)
#define PIN	_SFR_IO_ADDR(PINB)

;***** Global register variables
#define bitcnt	r16
#define temp	r17
#define Txbyte	r18
#define Rxbyte	r19

.extern charToSendUART					;external variable
.extern charReceivedUART				;external variable
;***************************************************************************

;*
;* "putchar"
;*
;* This subroutine transmits the byte stored in the "Txbyte" register
;* The number of stop bits used is set with the sb constant
;*
;* Number of words	:14 including return
;* Number of cycles	:Depens on bit rate
;* Low registers used	:None
;* High registers used	:2 (bitcnt,Txbyte)
;* Pointers used	:None
;*
;***************************************************************************

#define		sb	1		;Number of stop bits (1, 2, ...)

putchar:	ldi	bitcnt,9+sb	;1+8+sb (sb is # of stop bits)
com	Txbyte		;Inverte everything
sec			;Start bit

putchar0:	brcc	putchar1	;If carry set
cbi	PORT,TxD	;    send a '0'
rjmp	putchar2	;else

putchar1:	sbi	PORT,TxD	;    send a '1'
nop

putchar2:	rcall UART_delay	;One bit delay

rcall UART_delay
lsr	Txbyte		;Get next bit
dec	bitcnt		;If not all bit sent
brne	putchar0	;   send next
;else
ret			;   return

;***************************************************************************
;*
;* "getchar"
;*
;* This subroutine receives one byte and returns it in the "Rxbyte" register
;*
;* Number of words	:14 including return
;* Number of cycles	:Depens on when data arrives
;* Low registers used	:None
;* High registers used	:2 (bitcnt,Rxbyte)
;* Pointers used	:None
;*
;***************************************************************************

getchar:	ldi 	bitcnt,9	;8 data bit + 1 stop bit

getchar1:	sbic 	PIN,RxD	;Wait for start bit
rjmp 	getchar1
rcall UART_delay	;0.5 bit delay

getchar2:	rcall UART_delay	;1 bit delay
rcall UART_delay
clc			;clear carry
sbic 	PIN,RxD	;if RX pin high
sec			;
dec 	bitcnt		;If bit is stop bit
breq 	getchar3	;   return
;else
ror 	Rxbyte		;   shift bit into Rxbyte
rjmp 	getchar2	;   go get next

getchar3:	ret

;***************************************************************************
;*
;* "UART_delay"
;*
;* This delay subroutine generates the required delay between the bits when
;* transmitting and receiving bytes. The total execution time is set by the
;* constant "b":
;*
;*	3·b + 7 cycles (including rcall and ret)
;*
;* Number of words	:4 including return
;* Low registers used	:None
;* High registers used	:1 (temp)
;* Pointers used	:None
;*
;***************************************************************************

; Some b values: 	(See also table in Appnote documentation)
;
; 1 MHz crystal:
;   9600 bps - b=14
;  19200 bps - b=5
;  28800 bps - b=2

#define b	135  ; 8MHz   9600bps

UART_delay:	ldi	temp,b

UART_delay1:	dec	temp
brne	UART_delay1
ret

;***** TESTProgram Execution Starts Here

.global forever
forever:
rcall	getchar
mov	Txbyte, Rxbyte
rcall	putchar		;Echo received char
rjmp	forever

.global initUART
initUART:
sbi	PORT,TxD	;Init port pins
sbi	DDR,TxD
ldi	Txbyte,12	;Clear terminal
rcall	putchar
ret

.global sendCharUART
sendCharUART:
lds Txbyte, charToSendUART			;load variable to r18
rcall putchar
ret

.global rcvCharUART
rcvCharUART:
rcall getchar
sts charReceivedUART, Rxbyte
ret

There are some “tricks” here too (like the fact that I can’t use local variables in the main file, as GCC will use the same registers for them that are already used in the assembly file, and this will lead to some very obscure results… don’t ask how many frustrating hours I spent on this 🙂 ) but the overall code looks nice.

As you can see I’m using here a very simple bespoke serial protocol, and obviously the Peggy2 has to know about it and have the exact same timings:

CustomSerial file

const int T = 500;        // in microseconds
const int DELTA = T / 2;    // in microseconds
const int T0_MIN = T - DELTA;
const int T0_MAX = T + DELTA;
const int T1_MIN = 3 * T - DELTA;
const int T1_MAX = 3 * T + DELTA;
const int START_MIN = 8 * T - DELTA;
const int START_MAX = 8 * T + DELTA;
const int END_MIN = 5 * T - DELTA;
const int END_MAX = 5 * T + DELTA;

unsigned long _pulseStart;
unsigned long _pulse;
unsigned int _pulseCount;

volatile unsigned long _pulses[MSG_MAX_SIZE * 8];
unsigned int MAX_PULSES = (MSG_MAX_SIZE - 1)  * 8;

void CustomSerial_InterruptHandler(int state){
if(state == HIGH){
_pulseStart = micros();
}else{
_pulse = micros() - _pulseStart; // in microSecs
if(_pulse > START_MIN && _pulse < START_MAX){
//start counting pulses
_pulseCount = 0;
}else if((_pulse > END_MIN && _pulse < END_MAX) || (_pulseCount == MAX_PULSES)){
//END of message if END pulse OR too many chars -> message ready
_serialWaiting = (_pulseCount > 0 && (_pulseCount % 8 == 0)); // make sure it's multiple of 8
}else if(_pulseCount >= 0){
//continue counting pulses
_pulses[_pulseCount] = _pulse;
_pulseCount ++;
}
}
}

int CustomSerial_getBytes(char* msg){
int byteCount = _pulseCount / 8;
int i = 0; int currByte;
while(i < byteCount){
currByte = getByte(i * 8);
if(currByte < 0) return currByte;
msg[i] = currByte;
i ++;
}

msg[i] = NULL; // end of string
return byteCount;
}

//Convert pulses to integer
int getByte(int pos){
int result = 0;
int currentBit;

for(int i = pos; i < pos + 8; i++) {
currentBit = getBit(_pulses[i]);
if(currentBit < 0) return currentBit;
if(currentBit == 1) result += (1 << (i - pos));
}

return result;
}

int getBit(int pulseSize){
if(pulseSize < 0) return -2;
if((pulseSize > T0_MIN) && (pulseSize < T0_MAX)) return 0;
if((pulseSize > T1_MIN) && (pulseSize < T1_MAX)) return 1;
return -1;
}

and the main file controlling the Peggy2:

#include <Peggy2.h>
#include <PeggyWriter.h>

Peggy2 _peggy;
PeggyWriter _peggyWriter;
PeggyScroller _peggyScroller;

#define RX_PIN 15  // A1
#define END_MSG_TAG '%'
#define MSG_MAX_SIZE 30
#define READCHAR_TIMEOUT 2

volatile boolean _serialWaiting = false;

char _displayText[MSG_MAX_SIZE + 1];

void setup(){
// Call this once to init the hardware:
_peggy.HardwareInit();
_peggyWriter.drawCharacterSequence("INIT", &_peggy, 0, 0);
//_peggyScroller.init(&_peggyFrame, &_peggyWriter, 10, "INIT  ");
attachRXInterrupt();
}

void loop(){
_peggy.RefreshAll(10);

if(_serialWaiting){
_peggy.Clear();
int count = CustomSerial_getBytes(_displayText);
if(count > 0){
_peggyWriter.drawCharacterSequence((char*)_displayText, &_peggy, 0, 0);
//_peggyScroller.init(&_peggyFrame, &_peggyWriter, 10, "YOYO  ");
}else if (count == -1){
_peggyWriter.drawCharacterSequence("E", &_peggy, 3, 10);
}else if (count == -2){
_peggyWriter.drawCharacterSequence("EE", &_peggy, 3, 10);
}else{
_peggyWriter.drawCharacterSequence("EEE", &_peggy, 3, 10);
}
_serialWaiting = false;
}
}

/* Config INTERRUPT on the RX_PIN */
// RX_PIN is D15 = A1 corresponding to PORT 1, PCMSK1 bit 1

#define PCINT_PORT 1
#define PCINT_BIT 1

void attachRXInterrupt(){
// set the mask
PCMSK1 |= 0x01 << PCINT_BIT;

// enable the interrupt
PCICR |= 0x01 << PCINT_PORT;
}

void detachRXInterrupt() {
PCICR &= ~(0x01 << PCINT_PORT);
}

ISR(PCINT1_vect) {
if(! _serialWaiting) CustomSerial_InterruptHandler(digitalRead(RX_PIN));
}

HOWEVER, after an entire Saturday spend on writing and testing this, something very strange happened: it was working OK when connected to the PC, sending through UART at 9600 to the AtTiny45 and then the data being forwarded to the Peggy2, BUT it won’t work (or very randomly) when connected to the IrDA device… !

Again, how frustrating… after (luckily only) a couple of hours I realised that it might have to do with the power supply : the IrDA device and the AtTiny45 were both powered directly from the Peggy2, which seems to use quite a lot of power for all those huge LEDs. And sure enough, I still don’t know exactly what was happening (I can only assume it has to do with some power drops generated by the Peggy2 when lighting up plenty of LEDs at once – this is confirmed by the huuuge capacitor it has, and also by issues with programming the AtTiny45 when more than 20-30% of the LEDs were lit) this was the reason !

Conclusion

I switched the AtTiny and IrDA device to another power supply (sharing the ground of course)  and wow… it was working… what a relief…

But then almost immediately I realised that this might actually have been the reason why I was getting “fake” signals on the line in the first place! And again, sure enough the solution from the 1st attempt actually works like a charm, as long as the VCC for the IrDA device comes from somewhere “clean”.

And now, even worse/better, it turns out the IrDA device doesn’t need the Vcc wire (don’t ask me why, I did build it, but don’t pretend to understand all the subtleties…) it’s enough to connect the GND, Rx and Tx and everything works PERFECTLY !!!

So finally after more than a week of work/frustration/learning I realise that the whole AtTiny45 idea is nice, but useless… as useless as it felt from the beginning… 🙂

P.S. wow, what a post, this must be my longest ever…

2 Responses to Lightweight software UART -> custom serial

  1. Peter says:

    Hi , in wich compiler did you compile the assembler program of serial assembly.
    “Half Duplex Interrupt Driven Software UART;”
    I does’nt see any interupt handler , what does it mean in title above
    I’ve tryed it in avrstudio but it give two errors like:

    AVRASM: AVR macro assembler 2.1.42 (build 1796 Sep 15 2009 10:48:36)
    Copyright (C) 1995-2009 ATMEL Corporation

    D:\SerialUart_eXamples\SerialUart.asm(9): error: Cannot find include file: avr/io.h
    D:\SerialUart_eXamples\SerialUart.asm(24): error: Invalid directive: ‘.extern’

    Assembly failed, 2 errors, 0 warnings
    grtz
    Peter

    • trandi says:

      Hi Peter,

      To be honest I can barely remember anything about this project beyond what’s in the post… 🙂
      I’m pretty sure I was using “AVR Studio 4” and simply created an assembly project.

      Sorry for not being able to help more…
      Dan

Leave a comment