Firmware

Hello, World!

Let’s print some messages over the USB connection.

void setup() {
    USBSerial.begin(9600);
}

void loop() {
    USBSerial.println("Hello, World!");
    delay(1000);
}

This prints Hello, World! every second. But how can we see it?

If you are using the Arduino IDE, you can open the serial monitor, set the baudrate1 to 9600, and watch the hello’s flow in!

Another way we can read these messages is with Python!

Python has a library called pyserial.

To install it in your active Python environment, simply run:

python -m pip install pyserial

…in your terminal.

ℹ️
You can run python -m serial to determine the name of the port your DevBoard is on.

You can now open a Python file or REPL and write/run the following code:

from serial import Serial, SerialException

with Serial('/your/port', 9600) as ser:
    while True:
        print(ser.readline().decode())

.readline() accumulates bytes until the newline (\n ) byte is received.

We use .decode() because .readline() returns bytes which can be decoded into a string.

Serial LED

Ok so we can send bytes from the DevBoard to our computer, but what about the other way ‘round?

Let’s try to control an LED from our computer. To do this, we need to send messages the other way.

We first set up receiving bytes over serial on the DevBoard:

Interrupts

An interrupt is an event driven signal that runs code.

In our case, an event we care to handle is if we receive a byte over serial.

Luckily, this event is available to us, it’s called ARDUINO_HW_CDC_RX_EVENT.

Wow, what it’s trying to say is that if the serial port’s receive buffer is not empty, this event will be triggered.

So let’s define a function we want to be called when that event is triggered (we receive a byte):

void on_receive(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { ... }

The signature of this function is defined by the type esp_event_handler_t. You can refer to Espressif’s documentation to see more.

Then, we register the interrupt with the USBSerial peripheral in our setup() like so:

void setup() {
    pinMode(LED, OUTPUT);

    // register "on_receive" as callback for RX event
    USBSerial.onEvent(ARDUINO_HW_CDC_RX_EVENT, on_receive);
    USBSerial.begin(9600);
}

Ok, we also configure an LED to be an output, great.

Oh, and we correspond the byte received event to our function, nice!

Serial

So, what should be in our on_receive function?

Well, the first thing we need to do is get the data from the serial port’s buffer:

// read one byte
char state { USBSerial.read() };

We consider each byte received to be the target LED state (sent by the computer).

Then we need to do some validation, we know the LED can only be set to LOW, or HIGH, so we need to check the received byte is equal to either of those:

// guard byte is valid LED state
if (!(state == LOW || state == HIGH)) {
    // invalid byte received
    // what else should we do?
    return;
}

If we find the byte to be valid, we proceed to updating the LED:

// update LED with valid state
digitalWrite(LED, state);

Ok, this is pretty good, let’s hop back over to Python.

Let’s try sending the byte 0x1 and see what happens:

with Serial('/your/port', 9600) as ser:
    ser.write(bytes([0x1]))
    input() # keep port open to see the LED turn on

The LED should turn on!

Ok! This is cool! But…

Validation

What if we send an invalid byte? How could the app know? It should know, right?

Validation is an important consideration when developing communication systems. So let’s add it.

Let’s create two more constants at the top of our file:

const int LED { 17 };

// add these
const char S_OK { 0xaa };
const char S_ERR { 0xff };

We can send back one of these depending on the validity of the received data.

Let’s go back and update on_receive:

void on_receive(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
    // read one byte
    char state { USBSerial.read() };

    // guard byte is valid LED state
    if (!(state == LOW || state == HIGH)) {
        // invalid byte received
        // report error
        USBSerial.write(S_ERR);
        return;
    }

    // update LED with valid state
    digitalWrite(LED, state);
    USBSerial.write(S_OK);
}

Now whenever the app sends an LED state, it should expect a confirmation response.

You can try this in Python:

with Serial('/your/port', 9600) as ser:
    ser.write(bytes([0x1]))
    assert ser.read() == bytes([0xaa])

    ser.write(bytes([0x0]))
    assert ser.read() == bytes([0xaa])

    ser.write(bytes([0x2]))
    assert ser.read() == bytes([0xff])

If an error is raised, one of these assertions failed!

Loop?

What happened to loop()? What do we need to put in there?

Well…

void loop() { }

Nothing!

Our code is completely interrupt driven, so the loop function need not be populated :)

At this point, we have completed half of the system!

graph LR
    subgraph "DevBoard"
        subgraph "Front End"
            A2(LED)
        end

        subgraph "Back End"
            B2(Serial)
            C2(Callbacks)
            D2(Logic)
        end

        B2 --> C2 --> D2 --> A2
        D2 --> B2
    end

  1. The baudrate is the rate at which the signal on the wire can change in “baud’s” per second. ↩︎