5. Embedded programming

Our goal this week

Write a program for a microcontroller, and simulate its operation to interact (with local input &/or output devices) and communicate (with remote wired or wireless connections).

To achieve that, we're being exposed to an obscene amount of information about microcontrollers and embedded programming.

Introduction to microcontroller IDEs

Leo Kuipers, from Waag's 2024 Fab Academy, gave us an intro to microcontrollers, Arduino's IDE, platformIO, microPython, and a lot more.

But first, some background: Lightyears ago, in the 70s, Code C was invented (platformIO supports C/C++). Then, in 2004-05, some Italians invented Arduino (which is an IDE -- amongst many other things -- which is based around C).

"The project goal was to create simple, low cost tools for creating digital projects by non-engineers."

MCUs

Ideas and terms

There were many new terms being thrown at us, and I managed to catch a few that seemed noteworthy. There were also a few key considerations that exist in the world of microprocessors, so I've spelled a few out below.

A small microcontroller has 8kb of memory. How many 1s and 0s can that actually fit? Not many. Which is why professional embedded programmers don't use the same kind of bootstrapping that we do -- which is relatively clunky / inefficient. But, to be able to write really efficient code isn't something we're going to be able to do after one day of learning about microprocessors.

Microprocessors are groups of transistors arranged in specific circuit groups that perform different functions. They're wired in such a way that they can perform one, or a few types of tasks. Therefore, certain pins have certain capabilities / functions — and you’ll only know if you read the Data Sheet. Microcontroller decisions depend on how many I/O ports you need. Input / Output.

For example, the DAC block can convert digital signals (1s and 0s) to an analog signals (0V-5V). ADC converts, for example, temperature readings - voltage, into binary. ADC is always related to voltage. Analog signal is always referring to a certain voltage. DAC is the other way around. Converting binary into voltage.

And now, some terms to understand:

Ground (GND): I was trying to understand Ground (GND). Someone on Reddit gave the example of a waterpark. Water starts at the top of the slide, flows down the slide, but the water stays in the system. The ground is the pool. The leftover water / electrons flow to the pool / ground.

Electrons are "pumped" around a circuit. The battery doesn't create new electrons, it just gives energy to existing ones. Batteries run out because they lose chemical energy to "pump" the electrons around the circuit.

MCU - micro controller unit: MCU can handle one program at a time — one main loop. Whereas operating systems have many programs running on top of them.

An integrated development environment (IDE) is a software application that provides comprehensive facilities for software development: - Visual Studio Code - platformIO - etc

Compiler — translates human understandable code into binary.

Data bus - Everything is connected to the Bus (basically the data highway). The width of this "highway" (8-bit, 16-bit, 32-bit, etc.) determines how much data can move simultaneously, directly affecting the system's performance.

Registers are high-speed storage locations that temporarily hold data for immediate processing.

NB. Make sure to add at least one LED to your project.

Seeed Studio XIAO RP2040

Seeed Studio XIAO RP2040

Seeed Studio XIAO RP2040

  • Powered by Raspberry Pi 2040 chip, dual-core operating up to 133 MHz, equipped 264KB of SRAM, and 2MB of onboard flash memory
  • Ultra-small Design: 21 x 17.8mm
  • Multiple Development Interfaces: 2x buttons, 11x digital / 4x analog pins, 1x I2C interface, 1x UART port, 1x SPI port, and 1x SWD Bonding pad interface
  • Multiple Develop Platform: Support Arduino / Micropython / CircuitPython development

RP2040 data sheet

Arduino intro

The Seeed RP2040 comes with a pre-installed boot loader that allows for easy programming and firmware updates. And ensures that you can’t easilt "Brick" the device.

For Arduino compatibility, the RP2040 uses a specialized core (aka software package) called "arduino-pico" developed by Earle Philhower. This core allows RP2040-based boards to be programmed using the Arduino IDE and language.

Arduino

/*
To make comments
*/

// will also comment out whatever comes after it — until the end of the line

Delay is always in milliseconds 1000 = 1 second.

To reset the microcontroller: Hold B, press R while holding B, let go of R and then B.

LED write HIGH turns RGB lights off and LOW turns them on. This is specific to XIAO’s Seeed RP2040 RGB pins. Make sure to RTFM about an MCUs settings!

Leo setup

To get started with coding our microprocessors we downloaded, installed, and opened Arduino's IDE.

Our XIAO RP2040 isn't immediately recognized by the IDE, so we have to input a code within the preferences so that we can get properly set up.

Setup

This is the code:

Setup 2

Inputting the above initiates a download that will allow our RP2040's to work with Arduino's IDE.

Setup 3

Below is the same screen as above, but this time our MCU is showing.

Setup 4

With all of that out of the way, we can start to write code for our MCUs. When programming a micro controller, it's normal to use example code — "that’s how everyone starts". Both Leo and Neil said this. We selected the example code called "Blink".

When writing code for a microcontroller using Arduino, you have to have a setup() and a loop() function.

The setup function will only run once. Then the loop starts and keeps running.

In the first exercise, we made Pin 17, Pin 16, and Pin 25 outputs. You can see which pin is assigned by hovering over the words. Leo also gave us a cheat sheet for the RP2040, which I've added below.

Define pins

RP2040 pins

Cool fact, USB normally gives out 5V, but USB C is backwards compatible, meaning it speaks to whatever is on the other end and regulates the current.

After learning about the basics of coding in Arduino and getting the MCU to blink, I decided to venture out on my own and make the MCU blink in a 4-7-8 pattern. 4-7-8 is a simple yet powerful technique to regulate the nervous system and aid mental and physical well-being. Below was the first successful code that I had written. I expanded on this idea in my future efforts, but, by midway through Thursday, this was all I had.

/*
4-7-8 breathing technique
*/

void setup() {
  // initialize digital pin PIN_LED_X as an output.
  pinMode(PIN_LED_R, OUTPUT);
  pinMode(PIN_LED_G, OUTPUT);
  pinMode(PIN_LED_B, OUTPUT);
  digitalWrite(PIN_LED_R, HIGH);
  digitalWrite(PIN_LED_G, HIGH);
  digitalWrite(PIN_LED_B, HIGH); // turns the LED off -- first configuration 
}

void loop() {
  digitalWrite(PIN_LED_R, LOW);     
  delay(4000);                      
  digitalWrite(PIN_LED_R, HIGH);
  digitalWrite(PIN_LED_B, LOW);     
  delay(7000);   
  digitalWrite(PIN_LED_B, HIGH);  
  digitalWrite(PIN_LED_G, LOW);     
  delay(8000); 
  digitalWrite(PIN_LED_G, HIGH);                 
}

Pulse width modulation

PWM - pulse width modulation (on / off pulse) means modulating the width of the pulse and the influence that that has on an LED's perceived brightness.

For example, 10 milliseconds on 10 milliseconds off — our eyes blend that together and it looks like the light is at 50% strength.

Vary different combinations of on/off over the same time. Any kind of light that dims is a PWM.

PWM

PWM 20ms

PWM 19-1

Neopixel

The white thing in the middle of the chip is a neopixel. It's like a smart LED. We downloaded a library in order to better control the neopixel.

There are some necessary steps to take when setting up a neopixel.

Define

There are libraries that have been created to improve how to handle the neopixel (I believe there are libraries for lots of aspects of coding with Arduino). You have to include a library in your code with the #include. The library we included is called FastLED.

#include <FastLED.h>

Make sure to check if there’s an "enable pin". XIAO’s neopixel’s have an enable pin that needs to be set to on. Do that in setup() and not define.

 pinMode(11, OUTPUT);
 digitalWrite(11, HIGH);

FastLED has documentation online, and that's where we found the CRGB library (ie. colors).

NB. To comment out a few lines of code in VSC: cmd + /

MicroPython

Leo had some issues with Thonny, an IDE for microPython, so I didn't plan on using it much. Still, he gave us some information, which I'll add below.

  • UF2 file is a firmware file that we can mount onto our MCU
  • Drag and drop and it disappears
  • Running microPython means there is an OS on the microprocessor
  • If the REPL tab is showing, then it is a sign that everything is set up correctly

The last lesson for the day was from Henk. He showed us how to use the Wokwi logic analyzer -- which is a tool that you can add to a simulation that tracks the performance of the model. When the simulation has run for a bit, download the VCB file and view it with PulseView.

Dimming the neopixel

In setup(), to dim the neopixel:

FastLED.setBrightness(brightness); // brightness value from 0-255

In loop():

leds[i].fadeLightBy(brightness); // higher values = dimmer light
void loop() {
leds[0] =CRGB::Purple;
brightness = brightness + fadeAmount;

  if(brightness <= 0 || brightness >= 255) {
    fadeAmount = -fadeAmount;
  }

if (brightness >= 255) {
  FastLED.setBrightness(255);
}
else if (brightness <= 0) {
  FastLED.setBrightness(0);
}
else {
  FastLED.setBrightness(brightness);
}
FastLED.show();
delay(30); // hold on to the brightness for a little while
}

Friday - coding project

I arrived on Friday hoping to code The Matrix and was unpleasantly surprised at how hard it was to create three different if{} functions in order to get the RGB lights to switch at 4, 7, and 11 seconds.

I watched this video on how to get Arduino to "multi-task" and it gave me some helpful nudges in the right direction, but I still need to hone my understanding of a few important concepts before I can watch random videos and know what's going on.

Serial.print(), Baud rate, and <= && >=

Unfortunately, I lost the original code that I had been building, but it wasn't working anyways, so maybe it's for the better. Henk had encouraged me to use the Serial.print() function to have the microprocessor communicate with me what it was counting -- because I was using the millis() function to count elapsed time. So, I did that. However, there wasn't any output in the Serial Monitor.

Upon further research, I needed to add this to my setup function:

void setup() {
    Serial.begin(9600);  // Initialize serial communication at 9600 baud rate
}

That way the microprocessor and my laptop would communicate on the same baud rate (9600). With that initiated, I was able to view what the microprocessor was "saying" via the Serial Monitor. I did that by adding a line of code in the void loop() that printed what the MCU was counting: Serial.print(currentTime);.

The printed messages helped me confirm that the script was running (and counting) properly, which meant that there was something wrong in the code.

After reviewing everything thorougly, I decided to question my understanding of the greater than, less than signs. Something we learned in grade 3, and is something I still struggle with today. Which one is which?! <``>?

It turns out that my if statement needed to be fixed. I had been hoping to say "in-between" with the following code: (currentTime > 4000 || currentTime < 11000)".

But, as I learned, that's not correct. It meant that the part in my code which activated the Green light stayed TRUE because after the 4,000th millisecond, the currentTime stayed greater than 4000.

So, I changed the formula to this, and it worked: (currentTime >= 4000 && currentTime < 11000).

An important step in this process was reverting back to the beginning. I stripped the code to it's most basic elements and continuously tested it along the way -- I then added more complicated elements and tested those.

First Spiral

And so, with that update, the First Spiral was complete:

void setup() {
  Serial.begin(9600);
  pinMode(PIN_LED_R, OUTPUT);
  pinMode(PIN_LED_G, OUTPUT);
  pinMode(PIN_LED_B, OUTPUT);
  digitalWrite(PIN_LED_R, HIGH);
  digitalWrite(PIN_LED_G, HIGH);
  digitalWrite(PIN_LED_B, HIGH);
}

void loop() {
    unsigned long currentTime = millis();  // Move this inside loop
    if (currentTime < 4000) {
      digitalWrite(PIN_LED_R, LOW);
      digitalWrite(PIN_LED_G, HIGH);
      digitalWrite(PIN_LED_B, HIGH);
    }
    else if (currentTime >= 4000 && currentTime < 11000) {
      digitalWrite(PIN_LED_R, HIGH);
      digitalWrite(PIN_LED_G, LOW);
      digitalWrite(PIN_LED_B, HIGH);
    }
    else if (currentTime >= 11000 && currentTime <= 19000) {
      digitalWrite(PIN_LED_R, HIGH);
      digitalWrite(PIN_LED_G, HIGH);
      digitalWrite(PIN_LED_B, LOW);
    }
    Serial.println(currentTime);
}

Restarting the loop

My next goal was to get the timer to restart after 19,000 milliseconds. This is where the YouTube video I had previously watched about multi-tasking came back to help me. In that video, he creates an equation that uses currentTime and elapsedTime. In order to get the color change loop to start afresh, I needed to to figure out how to reset the currentTime variable.

I added what’s below as an attempt to try and get the restart to happen.

 else if (currentTime > 19000) {
        digitalWrite(PIN_LED_B, HIGH);
        currentTime = startTime;
      }

It didn’t work. But at least the blue light (at the end of the sequence) did turn off. So that’s a good start. Then it just went back to red and stayed there. It stayed there because I’ve set currentTime = startTime and startTime was set to equal 0. That's why the light just stayed stuck on the red if-loop.

AND FINALLY IT WORKED! Below are two excerpts from the code that worked that help explain why it did. The trick was to create an equation where the if-statement outcomes were based on elapsedTime, rather than currentTime.

unsigned long startTime = 0;
unsigned long currentTime;
unsigned long elapsedTime;

void loop() {
    currentTime = millis();
    elapsedTime = currentTime - startTime;
      .
      .
      .
    else if (elapsedTime >= 11000 && elapsedTime <= 19000) {
        digitalWrite(PIN_LED_R, HIGH);
        digitalWrite(PIN_LED_G, HIGH);
        digitalWrite(PIN_LED_B, LOW);
      }
    else if (currentTime > 19000) {
        digitalWrite(PIN_LED_B, HIGH);
        startTime = currentTime;
      }
}

What was quite central understanding why the last part worked was learning what the millis() function actually does. * It counts up from 0 when Arduino starts * Increments every millisecond * Returns an unsigned long integer * Continues counting until it reaches about 50 days, then rolls over to 0

So, currentTime was always going to keep growing. And my initial equation was trying to make the time that had elapsed zero, but that doesn't really make sense, because time had actually passed and it was being counted.

The final equation set the startTime function equal to currentTime. That would make the elapsedTime function equal 0 -- and the loop could start again at elapsedTime = 0.

Insert neopixel code

The next step was to insert the 4-7-8 breathing code I had made on Thursday, using the neopixel. When I begun working on Friday, I realized that I should probably start with baby steps and try to fit my more complicated code into a simpler structure.

I begun by defining and setting up the neopixel, and adding a FastLED.show() into each existing if() statement.

#define RGB_LED_ENABLE 11 // on Node 11 -- this enables the neopixel
#define RGB_DATA_PIN 12
#define NUM_LEDS 1

CRGB leds[NUM_LEDS];
void setup() {
  pinMode(RGB_LED_ENABLE, OUTPUT);
  digitalWrite(RGB_LED_ENABLE, HIGH); 
  FastLED.addLeds<NEOPIXEL, RGB_DATA_PIN>(leds, NUM_LEDS);
}
leds[0] =CRGB::Red;
FastLED.show();

That worked, so then I added the brightness fade code that I had made before. That also worked! I was on a roll.

But the brightness code didn't fade up at the rate that I had hoped. I tried creating an equations, and it didn't break the code worked, but it wasn’t right, the brighness change was way too quick!

Here's how the main bits of that equation looked looked, but keep in mind that it didn't work.

int maxBrightness = 255; // Maximum brightness
unsigned long fadeDuration = 4000; // Total time for fade in milliseconds
int fadeRate; // Amount to change brightness each loop iteration

unsigned long startTime = 0;
unsigned long currentTime;
unsigned long elapsedTime;

int brightness = 0; // integer (signed vs. unsigned)
int fadeAmount = 10;

void setup() {
  FastLED.setBrightness(brightness); // brightness value from 0-255
  fadeRate = maxBrightness / (fadeDuration / 30); // Calculate fade amount based on time
}

void loop() { 
    currentTime = millis();
    elapsedTime = currentTime - startTime;
    Serial.println(elapsedTime);

      if (elapsedTime < 4000) {
        leds[0] =CRGB::Red;
        FastLED.setBrightness(30);

        brightness = brightness + fadeAmount;

        if (brightness <= 0 || brightness >= 255) {
          fadeAmount = -fadeAmount; 
        }
        if (brightness >= 255) {
          FastLED.setBrightness(255);
        }
        else if (brightness <= 0) {
          FastLED.setBrightness(0);
        }
        else {
          FastLED.setBrightness(brightness);
        }
        FastLED.show();
        delay(fadeRate); // hold on to the brightness for a little while
      }
}

The map() function

After looking for some solutions online, I eventually learned about the map() function. The Arduino map() function is a tool for transforming values from one range to another. It ensures that the output value maintains the same relative position in the new range as the input value had in the original range.

map(value, fromLow, fromHigh, toLow, toHigh)

It meant that the value of the brightness would increase steadily over the time period of 0 - 3999 milliseconds.

        leds[0] =CRGB::Red;
        brightness = map(elapsedTime, 0, 3999, 0, 200);
        FastLED.setBrightness(brightness);
        FastLED.show();

...and it can also be inverted -- make the light dim over a set period of time.

        leds[0] =CRGB::Blue;
        brightness = map(elapsedTime, 11000, 19000, 200, 0);
        FastLED.setBrightness(brightness);
        FastLED.show();

With this new, beautiful map() tool, I had all I needed to get the code to get the MCU to behave how I wanted.

Second spiral

The final code for Friday was this:

#include<Arduino.h>
#include<FastLED.h>

#define RGB_LED_ENABLE 11 // on Node 11 -- this enables the neopixel
#define RGB_DATA_PIN 12
#define NUM_LEDS 1

unsigned long startTime = 0;
unsigned long currentTime;
unsigned long elapsedTime;

CRGB leds[NUM_LEDS]; // Definethearray of leds
int brightness; // integer (signed vs. unsigned)

void setup() {
  Serial.begin(9600); // initialize serial communication at 9600 bits per second

  pinMode(RGB_LED_ENABLE, OUTPUT);
  digitalWrite(RGB_LED_ENABLE, HIGH); // enable RGB LED

  FastLED.addLeds<NEOPIXEL, RGB_DATA_PIN>(leds, NUM_LEDS);
  FastLED.setBrightness(brightness); // brightness value from 0-255
}

void loop() { 
    currentTime = millis();
    elapsedTime = currentTime - startTime;
    Serial.println(elapsedTime);
      if (elapsedTime < 4000) {
        leds[0] =CRGB::Red;
        brightness = map(elapsedTime, 0, 3999, 0, 200); // map elapsedTime from 0 to 3999; to output of 0 to 200
        FastLED.setBrightness(brightness);
        FastLED.show();
      }
      else if (elapsedTime >= 4000 && elapsedTime < 11000) {
        leds[0] =CRGB::Green;
        FastLED.setBrightness(200);
        FastLED.show();
      }
      else if (elapsedTime >= 11000 && elapsedTime <= 19000) {
        leds[0] =CRGB::Blue;
        brightness = map(elapsedTime, 11000, 19000, 200, 0);
        FastLED.setBrightness(brightness);
        FastLED.show();
      }
      else if (currentTime > 19000) {
        startTime = currentTime; // reset the timer
      }
}

Hero shot