Using the Leonardo as USB to serial converter.

Arduino Leonardo used as usb to serial converter.

The green board in the picture above carries an atmega 328p with a Duemilanove bootloader. The Leonardo works as an USB to serial converter and is used to upload sketches to the atmega328p, and to communicate serially with the sketches.

For serial communications and power, the green board has the common 6 pin header you also find on e.g. the Arduino Pro or the Sanguino:

Target processor      Pin                           Serial to usb converter
(Atmega 328p)         Header                        (Leonardo)
GND   ---------------|1|--------------------------- GND
                     |2|                           
5V    <--------------|3|--------------------------- 5V
RX    <--- 1K -------|4|--------------------------- TX
TX    ---- 1K -------|5|--------------------------> RX
RESET <----||--------|6|--------------------------- DTR_PIN (here pin 13 4)
  |       100nF    
 10K
  |
 5V

The DTR pin is used by the Arduino IDE to automatically trigger a reset of the target processor, when it uploads a sketch. The header is meant to be used with an usb to serial converter like e.g. Sparkfun’s  “FTDI simple break out board”. This post is about a sketch and a few hacks in the arduino core that allows you to use the Leonardo  for this purpose.

UPDATE (September 2015): Thanks to Matthijs Kooiman’s work (see Expose CDC settings to sketch #3343), it is now possible to have this functionality without having to hack the arduino core.
Since long before the Leonardo was released (!), Paul Stoffregen has a more optimized sketch. He posted an updated version here: https://github.com/arduino/Arduino/pull/3343#issuecomment-115045979. You need Arduino 1.6.6.

I leave the rest of this post around  as people have used it and come back here. But I believe it is now obsolete, even for work on older cores. You have to modify those cores anyway and therefore I think it is better to add the Serial_.dtr() and Serial_.baud() methods to the old core and rewrite the sketch below without the callbacks. The callbacks below run in interrupt mode. It is difficult to guarantee such a callback is synchronized correctly with normal code. E.g. a flaw in the sketch below is that the loop() function may read a corrupted value of variable ‘baud’. In practice this will not cause a problem as in the next pass through loop(), the baud rate will be set correctly. But this illustrates it is simpler to avoid callbacks under interrupt…
____________________________________

Here is the sketch:
(again: it makes more sense to use the sketch from the link above).

/*
  leo_usb2serial
  Allows to use an Arduino Leonardo as an usb to serial converter.
 */
static long baud = 57600;
static long newBaud = baud;

// this pin will output the DTR signal (as set by the pc)
#define DTR_PIN 13

#define LINESTATE_DTR  1

void lineCodingEvent(long baud, byte databits, byte parity, byte charFormat)
{
  newBaud = baud;
}

void lineStateEvent(unsigned char linestate)
{
  if(linestate & LINESTATE_DTR)
    digitalWrite(DTR_PIN, HIGH);
  else
    digitalWrite(DTR_PIN, LOW);
}

void setup() {
  pinMode(DTR_PIN, OUTPUT);
  digitalWrite(DTR_PIN, LOW);
  Serial.begin(baud);
  Serial1.begin(baud);
}

void loop() {

  // Set the new baud rate
  if(newBaud != baud) {
    baud = newBaud;
    Serial1.end();
    Serial1.begin(baud);
  }

  // copy from virtual serial line to uart and vice versa
  if (Serial.available()) {
    char c = (char)Serial.read();
    Serial1.write(c);
  }
  if (Serial1.available()) {
    char c = (char)Serial1.read();
    Serial.write(c);
  }
}

The sketch’s main job is to forward everything that is received over USB (Serial) onto the real uart (Serial1), and vice versa. That is what happens at the end of loop().

Another task the sketch has to accomplish is to update the uart’s baudrate, whenever the pc changes the baud rate of the virtual com port. When this happens, the arduino core calls lineCodingEvent(baud,...). This routine runs under interrupt so we must take care not to spend too much time in it. Therefore the new baud rate is recorded in newBaud and the actual work is done from loop():

 if(newBaud != baud) {
    baud = newBaud;
    Serial1.end();
    Serial1.begin(baud);
  }

The last thing is to make the DTR signal available on one of the digital pins. Whenever the pc sets or clears DTR of the virtual com port, the arduino core calls lineStateEvent(). This routine also runs under interrupt, but the only thing to do is to adjust the level of the DTR_PIN. Pin 13 is used as DTR_PIN, this way one can observe the led to see what the Arduino IDE does with the DTR signal.

Now we get to the hacking part. Neither lineCodingEvent() nor lineStateEvent() are part of the arduino core. I plan to submit a change request for this, it looks a useful feature to me. However, it is not intrusive to add this feature manually to the core. Besides the changes discussed in my previous post are also needed for this to work, otherwise serial buffer overruns will happen.

Locate the CDC.cpp file and add the lines printed in bold:

void WEAK lineCodingEvent(long baud, byte databits, byte parity, byte charFormat)
{
}

void WEAK lineStateEvent(byte linestate)
{
}

bool WEAK CDC_Setup(Setup& setup)
{
        u8 r = setup.bRequest;
        u8 requestType = setup.bmRequestType;

        if (REQUEST_DEVICETOHOST_CLASS_INTERFACE == requestType)
        {
                if (CDC_GET_LINE_CODING == r)
                {
                        USB_SendControl(0,(void*)&_usbLineInfo,7);
                        return true;
                }
        }

        if (REQUEST_HOSTTODEVICE_CLASS_INTERFACE == requestType)
        {
                if (CDC_SET_LINE_CODING == r)
                {
                        USB_RecvControl((void*)&_usbLineInfo,7);
                        lineCodingEvent(_usbLineInfo.dwDTERate,
                                        _usbLineInfo.bDataBits,
                                        _usbLineInfo.bParityType,
                                        _usbLineInfo.bCharFormat);
                        return true;
                }

                if (CDC_SET_CONTROL_LINE_STATE == r)
                {
                        _usbLineInfo.lineState = setup.wValueL;

                        lineStateEvent(_usbLineInfo.lineState);

                        // auto-reset into the bootloader is triggered when the port, already 
                        // open at 1200 bps, is closed.  this is the signal to start the watchdog
                        // with a relatively long period so it can finish housekeeping tasks
                        // like servicing endpoints before the sketch ends
                        if (1200 == _usbLineInfo.dwDTERate) {
                                // We check DTR state to determine if host port is open (bit 0 of lineState).
                                if ((_usbLineInfo.lineState & 0x01) == 0) {
                                        *(uint16_t *)0x0800 = 0x7777;
                                        wdt_enable(WDTO_120MS);
                                } else {
                                        // Most OSs do some intermediate steps when configuring ports and DTR can
                                        // twiggle more than once before stabilizing.
                                        // To avoid spurious resets we set the watchdog to 250ms and eventually
                                        ...

First, weak symbols are add for lineCodingEvent() and lineStateEvent(). This makes sure that if a sketch does not provide one of the functions, the linker will use these dummy ones.

Then, it suffices to call the functions at the right place. CDC_Setup() is called whenever the Leonardo receives a “control message” , related to the CDC (Communication Device Class) protocol. Line coding and line state are set via such control messages. In the above snippet, the event functions are called after the messages are parsed.

I tried out the sketch by downloading the asciiTable sample using the IDE. The target atmega328p gets autoreset and the baudrate updated to 57600, which is what the Duemilanove bootloader uses. When the download completed, I opened the serial monitor and set it to 9600 baud. Upon reset of the atmega328p, I received the expected output from the asciiTable sketch.

10 thoughts on “Using the Leonardo as USB to serial converter.

  1. David

    Great info and I have used this on a number of occasions. I would like to suggest or ask that instead of crossing out the original, just make note at the top that for 1.6.6 and later the core hack is not needed and give the links and note that the rest of the article is for older or specialized cores only. I think there will be older cores needed for processors which are not supported by the main core (ATmega32U2, ATmega16U4, etc.) and your original post in its original form is very handy for hacking in those old cores.

    Reply
      1. David

        Thanks. Your statements “it is now obsolete, even for work on older cores. You have to modify those cores anyway and therefore I think it is better to add the Serial_.dtr() and Serial_.baud() methods to the old core” interest me a great deal. But, I was not able to understand enough about this new method to successfully update my core in this way. I would like to, but having difficulty with that. I am not able to fully understand the discussion in that GitHub issue “Expose CDC settings to sketch.” Your original article is more clear to me because it shows all in one place a sketch which gets the USB to serial job done, and it explains very clearly where to go in the core to make the necessary modifications. If you have time and can expand upon how the better solution works or how it would be applied to another core, I would really appreciate it. This CDC stuff is something I have a lot of difficulty understanding.

      2. petervho Post author

        It embodies less than you think: just copy the Serial_::baud() and Serial_::dtr() implementations from the new CDC.cpp file into yours, they are a few lines of code each.
        Also add their declarations to class Serial_, in USBAPI.h.

        This allows you to write the original sketch without the lineStateEvent() and lineCodingEvent()

        E.g. for the baud rate:

        if (baud != Serial.baud()) {
        baud = Serial.baud();
        Serial1.end();
        Serial1.begin(baud);
        }

        And similar for dtr, have a look at Paul’s sketch for inspiration, but that sketch also uses availableForWrite() which is more optimal but not strictly needed.

  2. petervho Post author

    Did you select the Leonardo as board? It definitely still has Serial1 in 1.6.6.
    B.t.w. in 1.6.6 it is better to use Paul Stoffregen’s sketch (see link above).

    Reply

Leave a comment