Saturday, 19 November 2011

A Dual-Rate UART Implemented in Software Using Atmel ATTiny MCUs

I've started designing a wireless interface using Bluetooth that, for technical reasons, requires the endpoints to communicate at a nonstandard baud rate. I found these neat-o Bluetooth-to-serial adapters on the net [look around, this is not the cheapest place, don't pay more than $8 USD or so for them and be sure to verify they have the HC-03 or HC-05 firmware that supports master -and- slave modes] that have a convenient AT-command set interface for configuration, so you can pair them up without any in-depth knowledge of Bluetooth, and a transparent mode that then acts like the modems of old... but they only support the standard rates you see with COM ports on PCs (9600/19200/38400 and so on). Bummer.

So.. it seemed to me that it should be possible to add a controller that, depending on the communication direction (this is a unidirectional link), would do an up- or down-conversion by sitting in between the BT interface and the target endpoint.

No hardware UART I've heard of supports a split data rate on the Tx and Rx lines, so a hardware UART or USART was not in the running. Software UART implementations from Atmel's app notes only do half-duplex while this required something that could shuffle data both ways in order to do the rate conversion.

For full-duplex software UARTs there is a handy library out there which includes one -- the Procyon AVRlib -- but it was written for AVR mega MCUs, and didn't have any facility for asymmetric baud rates. My application only needed a few pins so I wanted to use the ATTiny which has a somewhat different interrupt and timer register set as compared to the ATmega series. Moreover, the ATTiny25/45/85 have two independent hardware timers -- it looked perfectly feasible to implement a software UART that had asymmetric baud rates on the Tx and Rx sides, so why not roll my own software UART. Shouldn't be too hard, right?

Here's the general concept:

[A]----low-rate---->|SW-UART-Rx|---->|SW-UART-Tx|----high-rate---->|BT Master| >>>

and (for the other side)

>>> |BT Slave|----high-rate---->|SW-UART-Rx|---->|SW-UART-Tx|----low-rate---->[B]

As long as your endpoints [A] and [B] use a lower bitrate than whatever the Bluetooth-to-serial modules are configured for while in transparent mode, no special buffering of serial data should be required as [A] and [B] limit the total end-to-end bitrate to that of [A] and [B] themselves.

It took a bit of head-scratching with the oscilloscope attached but I managed to get a full-duplex software UART going on the ATTiny85, with no external hardware required. Symmetric or dual-bitrates are supported. I've tested the symmetric config at 38400 using puTTY and a USB-to-serial interface and all looks good so far. No real reason why the split rate shouldn't work, as each of Tx and Rx use their own unique timer...

I'll post code as soon as I have a spare moment.. gotta eat lunch now :p

[Update] Download source:


  1. Just wanted to tell you, you're my hero. thanks for this.

    1. Glad someone could use it! The project I originally wrote it for changed, such that I didn't actually need it any more. Let me know if you find bugs, I'll try to fix them. I'm sure I'll use it for something else, someday...

  2. Hi dangerRuss, I'm a bit a noob, so that's why I have problems with the following. I've connected a 11.0592 Mhz crystal to the attiny85.

    And now I don't know if I still can use the same values for txBaudrateDiv and txBaudrateval (same goes for the rx type vars)

    so what should the method swuartSetBaudRate look like incase of an external crystal of a 11.0592Mhz Crystal?

    Hope you can provide me with an answer.

    1. Hi, yes you would need to adjust the constants in the source code to account for your external crystal. The code currently assumes an 8MHz clock.

      As you said, in swuart.c, the cases in function swuartSetBaudRate() need to account for CPUCLK. The equation is: (CPUCLK / DIV / BPSRATE), where DIV is chosen (eg., CLK/8) so that the txBaudRateVal < 256.

      So if you wanted to derive a UART clock of 38400bps, using an 11.0592MHz crystal, start by calculating assuming a clock divisor of 1, eg: 11059200 / 1 / 38400 = 288. 288 is too big for the 8-bit txBaudRateVal, so let's choose txBaudRateDiv = CLK/8 (2). 288 / 8 = 36 (with 0% error, perfect).

      In your case, this would mean for, say, 38400bps:
      txBaudRateDiv = 2; /* clk/8 */
      txBaudRAteVal = 36;

      I probably should have come up with some kind of macro that determines the constants here at compile time but the complication is that txBaudRateDiv must be chosen such that the resulting txBaudRateVal is < 256. It could be done with runtime logic I suppose but that would take up extra code space.

  3. Hi DangerRuss,

    Thanks for this great piece of software. I am writing a protocol converter to fit my gps with faulty nmea sequence to a nikon camera. For this I need to send data at 4800 baud/sec. and receive 9600 baud/sec. I tried to add code to swuart.c to handle 4800 baud rate, but unfortunately I only get garbage with my serial terminal. On 9600 baud/sec everything is ok.
    here are my attempts to achieve 4800 baud:

    107 switch(txBaudRate) {
    108 case 4800:
    109 txBaudRateDiv = 3; /* clk/64 */
    110 txBaudRateVal = 26;
    111 break;

    107 switch(txBaudRate) {
    108 case 4800:
    109 txBaudRateDiv = 2; /* clk/8 */
    110 txBaudRateVal = 208;
    111 break;

    Checking the bit times using scope seems to be OK, but I always get garbage. Should I change anything else to get it work?


    1. Hello again! Please ignore my previous post. Finally I found the solution. At 4800 baud/sec I need to delay for at least 3 ms after each call to swuartTransmitByte() since the interrupt routine is still transmitting the previous byte. After inserting those delays the comminication works correctly.

    2. Hey Dándor, great to hear you got it working. It sounds like there might be a bug in my lib to do with detecting an already in-progress Tx... I will have to dig out the code and my board and look.

      I haven't done any work with it in over a year to be honest :) I'm great at starting, but not finishing, little projects like this.

  4. Hi DangerRuss,

    As I see in the code there is check for busy-ness. Maybe the "hi state" before the start bit is too short to detect the start of the start bit...