Saturday, January 11, 2014

AVR half-duplex software UART supporting single pin operation

Many of the ATtiny MCUs have no hardware UART limited number of pins.  Arduino tiny cores uses the TinyDebugSerial class which is output only, so using serial input requires extra code.  I have written a small software serial UART which can share a single pin for Rx & Tx when a simple external circuit is used.

To understand the circuit, a tutorial on TTL serial communication may help.  When the TTL serial adapter is not transmitting, the voltage from the Tx pin keeps Q1 turned on, and the AVR pin (Tx/Rx) will sense a high voltage, indicating idle state.  When the AVR transmits a 0, with Q1 on, Rx will get pulled low indicating a 0.  R1 ensures current flow through the base of Q1 is kept below 1mA.  When the AVR transmits a 1, Rx will no longer be pulled low, and Rx will return to high state.  When the serial adapter is transmitting a 0, D1 will allow it to pull the AVR pin low.  With no base current, Q1 will be turned off, and the Rx line on the serial adapter will be disconnected from the transmission.

I've written the serial code to work as an Arduino library.  Compiled it uses just 62 bytes of flash, and does not require any RAM (there is no buffering).  As it is written in AVR assembly, it will support very high baud rates - up to 460.8kbps at 16Mhz.  The default baud rate of 115.2kbps is defined in BasicSerial3.h, and can be changed with the BAUD_RATE define.  The library defaults to use PB5 for both Tx and Rx.  It can be changed with the UART_Tx and UART_Rx defines in BasicSerial3.S.  Here's an example sketch to that uses it:
#include <BasicSerial3.h>

// sketch to test Serial

void setup()
{
}

void serOut(const char* str)
{
   while (*str) TxByte (*str++);
}

void loop(){
  byte c;
  serOut("Serial echo test\n\r");
  while ( c = RxByte() ){
    TxByte(c);
  }
  delay(1000);
}

Here's a screen shot of putty running the example:

56 comments:

  1. Amazingly simple and clever solution :-) Thanks for sharing.

    ReplyDelete
  2. Absolutely great! Thanks for sharing!

    ReplyDelete
  3. Wow! This is awesome. I will try this with my DigiSparks!

    ReplyDelete
    Replies
    1. I tested it on a DigiSpark going to a PL-2303HX USB-serial adapter. It's great for the digisparks, trinkets, and other USB-connected ATtiny85s since 2 of their pins are crippled by being permanantly wired to the USB D+ & D-.

      Delete
  4. Great idea!

    Thanks Ralph

    ReplyDelete
  5. Quick question Ralph,

    Would it work to connect for instance two Arduino Uno?

    Thanks
    Max

    ReplyDelete
    Replies
    1. It should be even easier connecting two arduinos, with both of them running the half-duplex UART code. They could be both connected directly. At most you might need an external pullup resistor on the Tx/Rx line

      Delete
    2. Hi Ralph,

      Thanks for the quick answer!

      Interesting... I thought about a direct connection tx/rx to tx/rx but I didn't think it was possible. I expected it required some software tweaking or some extra interface circuit.

      Max

      Delete
    3. Hi Ralph,

      Just to be sure I understand...

      Let's assume that two Arduinos (e.g. Uno) are connected using a single tx/rx line with a pull-up resistor. Is the pin used in the Arduinos defined as UART_Tx and UART_Rx in BasicSerial3.S ?

      Thanks
      Max

      Delete
  6. Can this be used for a one line Interrupt / Enable?

    ReplyDelete
    Replies
    1. Not exactly sure what you're asking, but if you mean can the RxByte code used in an ISR, it can with a few code changes. You'd need to remove the cli instruction and add the typical ISR code to save/restore SREG & the used registers (r22 & r24).

      Delete
  7. I could this be used for gps data use?

    ReplyDelete
    Replies
    1. It can be used with any ttl serial interface.

      Delete
  8. Does not compile with Arduino 1.5.2 or 1.5.5.

    In function `loop':
    undefined reference to `TxTimedByte'
    undefined reference to `RxTimedByte'

    ReplyDelete
    Replies
    1. I used 1.04, and 1.05 should work the same.
      When 1.5.x goes out of beta, I'll try it.

      Delete
  9. Clever trick! thank you to share it. I have added your article to the last embedded systems weekly issue http://embedsysweekly.com/issue13-microcontroller-8051-pic/
    I hope you'll like how I presented it.

    ReplyDelete
    Replies
    1. Good, though your English is not quite perfect.

      Delete
  10. Hi Ralph,

    is it possible to use the code without sharing a single pin or does it cause any problems??

    Thanks for sharing your code!

    ReplyDelete
    Replies
    1. It should work fine Johann - just change the defines for the tx and rx pins.
      If you're looking for something you can use with plain AVR code instead of Arduino, I have a similar soft UART here:
      https://code.google.com/p/picoboot/source/browse/trunk/BBUart.S

      Delete
    2. Hey, would this work with lets say Attiny13A? I don't need anything fast - 57600 or even 28800 bps will be more than enough - I have tried your BBUart.S but avr-gcc complains about this code.

      Delete
    3. Someone on the Arduino forums said it worked on the tiny13 for him:
      http://forum.arduino.cc/index.php?topic=207467.0
      I'll have a better version of the UART finished soon, along with a decent Makefile. It will work by defining BAUDRATE in the application code (i.e. where your main is) so the uart code doesn't have to be modified.

      Delete
    4. Ok so adding:

      #include
      #define UART_Port (PORTB-0x20)

      helped - all ASM errors are gone now. The only problem I have now is that somehow my main.c code is unable to see TxByte - even if there is extern directive pointing that it's defined in ASM (I want to use TxByte only as I need some UART just to debug ADC values sometimes and using terminal seems to be the best way).

      Delete
    5. This comment has been removed by the author.

      Delete
    6. This comment has been removed by the author.

      Delete
    7. OK I have tried TXDELAY 25 (as it was said on the forums) - I get garbage on my screen regardless of the speed I set in minicom (115200, 57600, etc) different garbage for different speeds - Tx pin of tiny13 is connected directly to Rx of my USB serial converter.

      Delete
    8. I looked at the tiny13 datasheet, and it says, "The device is shipped with CKSEL = “10”, SUT = “10”, and CKDIV8 programmed." That means 9.6Mhz/8 = 1.2Mhz. That's going to be to slow for 115.2, and even 57.6 would be tricky. At 38.4kbps there's 31.25 cycles per bit, so 8 for the delay counter would give a total of 31 cycles per bit (<1% timing error) if you use the arduino-compatible version of the code (linked above). If you're using a modified version of BBUart, (the one that calls delay3Cycle), TXDELAY should be 6, which would give a total of 32 cycles and 2.4% timing error. Most UARTs can handle up to 5% timing error.

      Delete
    9. Thanks Ralph - unfortunately this didn't help - I tried both 1.2Mhz and 9.6MHz - no luck - still garbage - but at the moment I power this via USB - so I'll do some proper stabilization and filtering via external battery bank to see if this helps. I'll also try to add decoupling capacitor and will let you know :) - thanks for all help and this brilliant tiny code.

      Delete
    10. My bad - this is tiny13a - this comes with 9.6MHz by default - but it should be no different to tiny13 except it's less power hungry.

      Delete
    11. So what is the formula for TXDELAY in BBUart.S? I might have to try few different speeds to see if any of them will work at all.

      Delete
    12. If you're using the latest version with the nop after the out, its:
      Its ((BAUDRATE/F_CPU) -15) /3).
      The nop is there just to make the TxByte and RxByte timing in sync, so if you're only using TxByte there's no benefit to the nop. Without it subtract 14 cycles from BAUDRATE/F_CPU instead of 15.

      If you have a logic analyzer you could check the output timing. If you don't, you could use the audio input on a computer as a low-speed scope to do the same thing. If you can wait a few days I'll be writing an article on wiring a 3.5mm jack and about which software I like.

      Delete
    13. Thanks - all I need is TX from AVR to PC - that way I can see for ex. ADC values and anything else I desire - I'm happy to wait for any extra solution that might help - no logic analyzer at hand :(

      Delete
    14. Hmmm BAUDRATE/F_CPU gives me very small number :) so I assume it should be F_CPU/BAUDRATE?

      Also I use BBUart.S from the link you left in comments and I can't see nop after out at the end of TXLoop.

      Also I had to change brne delay3Cycle to brne Delay3Cycle - as I was getting an error (avr-gcc linux - is of course case sensitive)

      Delete
    15. This afternoon I finished the post on using the sound card as a logic analyzer.
      You're correct about F_CPU/BAUDRATE. You can see the forumula from the macros in BBUart.h. I've done a few updates to the BBUart code, but if you use the matching BBUart.h you'll get the right timing.
      For the version I think you're using, TXDELAY should work out to 23 for 115.2kbps @9.6Mhz (with 0.4% timing error). The 25 that you tried would have been close enough that it would see characters but they would be corrupted.
      In the most recent version I've tweaked things a bit and added the code for echo test (uart_echo.c).
      https://code.google.com/p/nerdralph/source/browse/#svn%2Ftrunk%2Favr

      Delete
    16. This comment has been removed by the author.

      Delete
    17. Still no luck - the only thing I can say is that my rs-usb converter (based on pl2303) is playing important role here.
      Looks like you're Windows user - so I can't use your latest entry tips on sound card logic analyzer :(

      Delete
    18. I do most of my development on Linux, and use Windows for client-side stuff. Under Linux any sound recorder like KDE has would do the trick for using the audio input as a logic analyzer.

      Delete
    19. Just follow-up - the problem was with usb-serial cable - my pl2303 is not good. I visited my friend who has FTDI cable and surprise surprise - it WORKS just fine! - thanks for help :)

      Delete
  11. This comment has been removed by the author.

    ReplyDelete
  12. sorry, i get
    avr-gcc: error: unrecognized command line option '-assembler-with-cpp'
    any idea?

    ReplyDelete
    Replies
    1. You should have at least version 4.3.2 of avr-gcc. Check it with avr-gcc --version.

      Delete
    2. ok, thanks ralph!
      if i use digispark-arduino-1.0.4 ide instead of arduino-1.0.5, it compile well!!!
      please could you suggest the appropriate diodes and resistance for use with attiny85 in 8 mhz 3.3V mode or 16 mhz mode 5V mode?
      best regards, pescadito

      Delete
    3. That's surprising. I'll try to see what the difference is with the 1.0.5 arduino ide.
      I used a 1n4148 diode, and also tested it with a schottky diode; just about any diode should work with both 3.3 & 5v. For the resistor anything from 4.7K to 47K should work - I did most of my testing with a 10K resistor.

      Delete
  13. thank ralph, very kind of you!!!
    i use digispark-arduino-1.0.4,micronucleous, 1n4148 and 222a and i work, downloading for digispark 16 mhz work better in 5v, but with few errors if 3.3v is provided (rx char is printed corrupted),
    but them, in the loop, any sentence after TxByte(c); doesn' t execute every cicle,...only execute..sometime........
    i'm referint to
    while ( c = RxByte() ){
    TxByte(c);
    serOut(".....\n\r");
    }
    any idea?

    ReplyDelete
  14. I can think of a couple possibilities:
    1) if you're using an ATtinyx5 on a breadboard, make sure you have a decoupling capacitor (anything between 0.1 and 1uF). I've had problems go away when I used a 0.1uF cap.
    2) Try dropping the baud rate to 57600 in BasicSerial3.h
    #define BAUD_RATE 57600
    The macros to calculate the bit delays are only within a few cycles of the correct timing. In my own code I calculate the delays by hand to get the best timing. For example, at 115200kbps each bit is 8.681 us long. At 8Mhz, that's 69.444 cycles. The delay loop is 3 cycles per itteration, and if the delay calculation macro has a rounding error the timing could be off by 4 or 5 cycles per bit.

    ReplyDelete
  15. thank again ralph!!
    i tryed a 0.1 uF ceramic cap bewten vcc and gnd, also a 1F electrolytic cap and also 57600 and rx/tx worked well
    but proble is that any sentence after TxByte(c); in the while is not executed!!!
    i try to add support for some characters commands in rx but without result
    it's a software problem or a hardware problem? another idea?
    best regards, pescadito

    ReplyDelete
    Replies
    1. wooo, it seems that's a voltage problem, to reduce space i was testing without a regulator, then i tested 3 differents, one return tx gabarage, other not execute some sentece, last do both without problem: was a regulable booster with a 3.4V output.....

      Delete
    2. Good to hear you figured it out.

      Delete
  16. Seems I'm still unable to make it work. Regardless of voltage filtering or decoupling I'm getting garbage :( Will try with Attiny85 :) - anyway I'm out of options now.
    I can't wait for your code update (I presume based on timer) I hope it will fit tiny 1k flash :) including some of my code ;)

    ReplyDelete
  17. Hi there,
    I want to use this configuration for communication between two microcontroller by single wire communication.I want to implement this just in reverse way .Do you think its possible?

    ReplyDelete
    Replies
    1. Between 2 MCUs should work fine - then you wouldn't need the circuit to combine tx/rx onto one wire.

      Delete
  18. Hi,
    Looks like a compact and tidy library, perfect for communicating between two MCUs over a single line.
    I am fairly experienced on the Arduino platform and now trying to make two attiny85 (@8MHz) talk to one another (right now, just one way is just fine).

    Here is my setup:
    With the BasicSerial3 library, the header is modified as:
    #define UART_Port (PORTB-0x20)
    #define UART_Tx 4
    #define UART_Rx 4
    to use the attiny's pin 4.

    In the code, i have a loop which sends out a string "A" using:
    serOut("A");
    //every two seconds.

    On the second tiny, I have a simple code which (is supposed to) receive the string on pin 4 again (pin 4 of each MCU connected by a single line) and blink an led:
    while ( c = RxByte() ){
    //blink an LED to let me know a that a byte has been received
    }

    I am stuck at this point when I am not able to get it working. I know there is something simple being missed out, but unable to figure it out!
    Any pointers on what might be going wrong?

    ReplyDelete
    Replies
    1. You shouldn't subtract 32 from the port address, unless you remove the following line too:
      #define __SFR_OFFSET 0

      Delete
  19. Hi Ralph,
    Thanks for your reply.
    I checked with the following:
    #define UART_Port (PORTB4) // to use IO pin 4 on the ATTiny85
    #define UART_Tx 4 // should i use PB4 instead of just 4 here?
    #define UART_Rx 4

    I tried the same code hooked on to a Mega with physical connection (from i/o pin 4 of the tiny) to digital pin 4 on the Mega
    and trying to print the characters received thru:
    while ( c = RxByte() ){
    Serial.print(c);
    }

    Sitll no luck.
    Any idea?

    ReplyDelete
    Replies
    1. I'd try each end independently with a USB-ttl adapter. Most of them have LEDs for RX, so you'll know if any data is flowing. If you have a multimeter you can also check that the line is high when no data is being transmitted (and after the first byte is transmitted since the port is set to output mode in the TxByte code).

      Delete
  20. I have used this library on a ATtiny85 with succes
    For testing purpose I used 2 pins (3 and 4) as tx and rx
    The Tiny was running @8MHz and baudrate 57600.
    I think that using two pins (atleast for testing) makes it eayer, not so much that can go wrong

    ReplyDelete