Lenonardo vs. UNO (and others) - bootloader fails on one, not the other

First of, I hope this is the right group. This is a basic issue but it's definitely boot-loader related.

I think I'm missing something fundamental here. I needed to take some Arduino's to a user group (maker-lab in making) to introduce basic Arduino and other maker electronics. Unfortunately my old UNOs were all used in projects so I decided to add new ones, and found the "new" Leonardo. I've never used that before, and thought it was time for an upgrade. I only needed something basic so it would work perfectly for the talk.

I love how things are labeled better - it was great for demos, and a basic demo with NEOPIXEL and a few other things just worked. Happy presenter - and I had hoped the story ends here. But unfortunately it doesn't.

I also had a TM1638 (https://www.amazon.com/gp/product/B07CJS3184) for the same purpose - to illustrate a few basic principles. I hacked up some VERY ugly code, and the whole thing is easy to control. That is, from my UNO that's the same. Even a NANO, it works great - but when I upload the same code to the Leonardo, the boot-loader fails and to the computer, there's no longer a device attached. Nothing in the sketch works - I have to use a USBTiny to write a new boot-loader to the board (which actually requires a bit of a work-around, but I don't think that's relevant here), and with that done, I can get "blinky" just fine. Uploading basic sketches like Blinky doesn't kill the Leonardo. So far, JUST my little project does.

That same project uploads fine on a UNO. But my little "blinky" isn't running unless I leave out initialization of my code. So there's definitely a bug, but I'm not understanding why the bootloader on the Leonardo works "differently". I'll like to understand the difference - perhaps there's a feature here I can take advantage of?

I'm lost in regards to two things:

  1. How in the world do I debug what's going on? There's no JTAG etc. so how do you figure out why the USB interface just stops?
  2. How can an extremely simple sketch that basically just initializes a few digital ports (I've commented everything else out!) die like this?

Please post your full sketch.

If possible, you should always post code directly in the forum thread as text using code tags:

  • Do an Auto Format (Tools > Auto Format in the Arduino IDE or Ctrl + B in the Arduino Web Editor) on your code. This will make it easier for you to spot bugs and make it easier for us to read.
  • In the Arduino IDE or Arduino Web Editor, click on the window that contains your sketch code.
  • Press "Ctrl + A". This will select all the text.
  • Press "Ctrl + C". This will copy the selected text to the clipboard.
  • In a forum reply here, click on the reply field.
  • Click the </> button on the forum toolbar. This will add the forum's code tags markup to your reply.
  • Press "Ctrl + V". This will paste the sketch between the code tags.
  • Move the cursor outside of the code tags before you add any additional text to your reply.
  • Repeat the above process if your sketch has multiple tabs.

This will make it easy for anyone to look at it, which will increase the likelihood of you getting help.

If the sketch is longer than the 9000 characters maximum allowed by the forum, then it's OK to add it as an attachment. After clicking the "Reply" button, you will see an "Attachments and other settings" link.

When your code requires a library that's not included with the Arduino IDE please post a link (using the chain links icon on the forum toolbar to make it clickable) to where you downloaded that library from or if you installed it using Library Manger (Sketch > Include Library > Manage Libraries in the Arduino IDE or Libraries > Library Manager in the Arduino Web Editor) then say so and state the full name of the library.

I didn't post the code on purpose - the question I raise is unrelated to it. I am trying to understand why Leonardo and UNO acts differently to the same sketch. I don't want the code to distract from my question. That said, I do have a reduced (simplified) code set that doesn't do anything other than initializing the library object and I've simplified the class to have the bare necessaries for initialization:

ino/sketch:

#include "TMTEST.h"

TMTEST board(5, 6, 7);

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
}

int st = HIGH;
void loop() {
  // board.updateDisplay();
  delay(100);
  digitalWrite(LED_BUILTIN, st);
  st = (st == HIGH) ? LOW : HIGH;
}

TMTEST.h (placed in the libraries folder):

#ifndef TMTEST_h
#define TMTEST_h

#include "Arduino.h"

class TMTEST
{
  public:
    TMTEST(int _strobePin, int _clockPin, int _dataPin);

    void updateDisplay();

  private:
    void init(int _strobePin, int _clockPin, int _dataPin);

    enum mode_t { WRITE_MODE, READ_MODE } unitMode;

    int strobePin, clockPin, dataPin;

    byte intensity;
    byte ledOut[16]; // All 16 addressable LED/7Segment units
    bool dirty;

    bool keyStatus[8];

    void setWriteMode();

    void setReadMode();

    void sendByte(byte b);

    void sendCommand(byte cmd);

    void initUnit(byte brightness);

    void offUnit();

    void singleAddressMode();

    void startAddressMode();

    void readButtons();

    void sendAdrValue(byte adr, byte val);

    void blank();
};
#endif

TMTEST.cpp

#include "TMTEST.h"
#include "Arduino.h"

void TMTEST::sendByte(byte b) {
  shiftOut(dataPin, clockPin, LSBFIRST, b);
}

void TMTEST::sendCommand(byte cmd) {
  digitalWrite(strobePin, LOW); //set the strobe low so it'll accept instruction
  sendByte(cmd);
  digitalWrite(strobePin, HIGH); //set the strobe low so it'll accept instruction
}

void TMTEST::initUnit(byte brightness) {
  byte cmd = 0x88 | (brightness & 0x07);
  sendCommand(cmd);
  Serial.println("initUnit");
}

void TMTEST::offUnit() {
  sendCommand(0x80);
}

void TMTEST::singleAddressMode() {
  sendCommand(0x44);
}

void TMTEST::startAddressMode() {
  sendCommand(0x40);
}

void TMTEST::updateDisplay() {
  if (dirty) {
    dirty = false;
    startAddressMode();
    sendByte(0x00); // Start address
    for (int i = 0; i < 16; i++) {
      sendByte(ledOut[i]);
    }
    digitalWrite(strobePin, HIGH); //set the strobe low so it'll accept instruction
  }
}

TMTEST::TMTEST(int _strobePin, int _clockPin, int _dataPin) {
  Serial.begin(115200);
  Serial.println("Hello there!");
  delay(400);
  strobePin = _strobePin;
  clockPin = _clockPin;
  dataPin = _dataPin;
  dirty = true;
  intensity = 0x03;

  delay(500);
}

Again, this code is not supposed to produce anything but a flashing LED. On both the UNO and the Leonardo, the LED doesn't flash with THIS code (if I initialize the object in setup() it does flash on the UNO) - but on the Leonardo, I lose the board/USB and I have to fully reset using USBTiny with a new boot-loader.

bit4man:
I am trying to understand why Leonardo and UNO acts differently to the same sketch.

The Uno's primary microcontroller is the ATmega328P. The ATmega328P does not have USB capabilities so in order to allow uploading to the Uno over USB Arduino added a separate USB to TTL serial adapter chip. On the official Uno and clones, this is the ATmega16U2. On Uno derivatives, it's usually the CH340. That chip is what creates a virtual serial port on your computer. This means that no matter how screwy the code is that you run on the Uno, it will never mess up the USB functionality.

The Leonardo's ATmega32U4 microcontroller has native USB so Arduino was able to avoid the need for a separate USB chip. That's all handled right on the same microcontroller where you sketch is running. It's possible for your sketch code to interfere with that USB code you don't see and "brick" the Leonardo. That's what happens with your code. It turns out it's those delays in the TMTEST constructor that did it. I don't have a specific explanation for why this is, but it's a bad idea to do anything like that in a constructor, including those calls to Serial.begin() and Serial.print(). The reason is that the constructor code runs before the initialization code that configures the hardware and the USB. So you see, the code IS related to your question!

bit4man:
I have to use a USBTiny to write a new boot-loader to the board

That's not necessary. The bootloader code is not affected by your sketch code and the bootloader has its own USB code. The tricky thing is that the way the bootloader is normally activated when you upload to the Leonardo is by the Arduino IDE opening a 1200 baud serial connection to the virtual serial port created by the USB code running in your sketch. There is some code in the sketch program that sees that 1200 baud connection as a signal to reset the microcontroller to activate the bootloader code so that the upload can start. Since your sketch broke the USB code that creates the virtual serial port, there is no way for the Arduino IDE to send the reset signal. This means you need to manually reset the board at just the right time during the upload:

Start the upload.

Watch the black console window at the bottom of the Arduino IDE window until you see something like this:

Sketch uses 3462 bytes (12%) of program storage space. Maximum is 28672 bytes.
Global variables use 149 bytes (5%) of dynamic memory, leaving 2411 bytes for local variables. Maximum is 2560 bytes.

Immediately press and release the reset button on the Leonardo. This will activate the bootloader and the upload will finish successfully.

pert:
The Uno's primary microcontroller is the ATmega328P. The ATmega328P does not have USB capabilities so in order to allow uploading to the Uno over USB Arduino added a separate USB to TTL serial adapter chip. On the official Uno and clones, this is the ATmega16U2. On Uno derivatives, it's usually the CH340. That chip is what creates a virtual serial port on your computer. This means that no matter how screwy the code is that you run on the Uno, it will never mess up the USB functionality.

Thanks - now I realize I'd read that somewhere before but never added it up to explain what I was seeing. Thanks!

pert:
That's what happens with your code. It turns out it's those delays in the TMTEST constructor that did it. I don't have a specific explanation for why this is, but it's a bad idea to do anything like that in a constructor, including those calls to Serial.begin() and Serial.print(). The reason is that the constructor code runs before the initialization code that configures the hardware and the USB. So you see, the code IS related to your question!

I knew the code wasn't perfect - that's why there are delay's in the constructor - they were added AFTER I saw the issues. But I must have done other things too, as removing them definitely starts the "blinky". And with the TM1638 connected, I now see the test LED turn on too - that never happened before.

I'm still curious how to debug this stuff. All I could see was that my setup() didn't complete, and worse I couldn't send serial "I'm here" since the object initializes before the setup gets to the initialization of Serial. For a very long time I didn't try the sketch on the UNO so I simply had a sketch and a Leonardo and it went dead right after I wrote to it. I've never used JTAG pins - I know what they are for, but regardless they're not present on these boards. So I'll still hunt down ideas to debug, but for now I need to thank you for telling me to remove my debug as it was what caused the "halt".

The Serial on a Leonardo works different from the Uno.

If you want to see the first message, you will need to wait till the connection is properly established. Add the below after the Serial.begin().

while(!Serial);

Note that this will block if the board is not connected to a PC.

You can also bring a 32U4 based board to a near grinding halt by printing too much; this will happen when using Serial.print/println/write and the board is disconnected after while(!Serial) is executed. Use Serial.available() to test if the message that you want to print will fit in the TX buffer.