Week 6
Embedded Programming
- Browse through the data sheet for your microcontroller
- Compare the performance and development workflows for other architectures
Embedded Programming
For the group assignment this week, we have to compare the performance and development workflow for different architectures and look into the differente datasheets of the microcontrollers found at FabLab Puebla.
- Architectures -
Architectures are basically families of chips, "architecture" refers to the fundamental design and structure of the computing core and its instruction set. Think of it as the blueprint that dictates how a microcontroller or processor is built and how it processes information. To simplify it imagine you're building a house. The architecture of the house dictates the overall design, how rooms are laid out, what materials are used, and how different parts of the house connect with each other. Similarly, in microcontrollers and processors, the architecture defines how the device is designed internally, how it performs computations, and how different components within it communicate and work together.
Here are a list of architectures found in the different chips found in FabLab Puebla
Architecture | Pros | Cons | Main Applications | Common Microcontrollers |
---|---|---|---|---|
AVR (8-bit) | Low power consumption, Simple to program, Good community support | Limited processing power, Fewer advanced peripherals | Hobbyist projects, Simple embedded systems | ATmega328, ATmega32u4, ATtiny45, ATtiny44, ATtiny412, ATtiny1614 |
ARM Cortex-M0+ | Energy efficient, Wide range of tools, High performance for 32-bit | More complex to program than AVR, Higher cost | Low power applications, IoT devices, Wearables | SAMD21, RP2040 |
RISC-V (32-bit) | Open source architecture, Scalable, Flexible | Less mature ecosystem, Fewer ready-to-use libraries | IoT, Educational purposes, Research projects | ESP-32C3 |
Xtensa (LX6/LX7) | Highly customizable, Good for DSP applications, Integrated Wi-Fi and Bluetooth | Proprietary, Requires licensing for commercial use | Connected devices, Smart home, Audio processing | ESP32-S3, ESP32-WROOM |
Tensilica L106 (32-bit) | Low cost, Integrated Wi-Fi, Good SDK support | Single core, Less powerful than newer ESP32 models | IoT, Budget Wi-Fi projects | ESP8266 |
ARM Cortex-M4F | High performance, Floating point unit, Energy efficient | More expensive than simpler cores, Complex for beginners | High-performance embedded systems, Industrial control, Advanced IoT devices | Nucleo-L432KC, nRF52840 |
- MCUs -
Here we have all the chips that can be used and found so that we can compare them in the Lab:
- Comparison -
Looking at the quantity of avilable MCUs one can only wonder, how can i choose the right one, well for starters we can see the previous tables and look for the function I need be it Bluetooth, USB, etc. Another way too look at it is to see the dimensions and think if it works inside of the project i'm building. But on of the most important ones are performance. Every architecture works differently so we're gonne do a couple of tests to compare them.
Arithmetic Operations
One thing that's kinda obvious is that when you're working with an 8-bit microcontroller, you're kinda stuck with numbers between 0 and 255. If you wanna go negative, you can only go from -127 to 128. But if you're trying to deal with bigger numbers, you've gotta split them up and juggle them across different parts, which can get a bit tricky since the microcontroller itself isn't gonna help you out with that.If you bump up to a 32-bit microcontroller, you're in for a treat because now you can play with huge numbers, all the way up to over 4 billion (just to keep it simple, that's for unsigned numbers; signed numbers are a whole other story). So let's start with a couple of test with code given to us by our instructor.
void setup() {
Serial.begin(9600);
while (!Serial); // Wait for the serial port to open
// Determine the bit depth of commonly used data types
Serial.print("Bit Depth - byte: ");
Serial.println(sizeof(byte) * 8);
Serial.print("Bit Depth - int: ");
Serial.println(sizeof(int) * 8);
Serial.print("Bit Depth - long: ");
Serial.println(sizeof(long) * 8);
Serial.print("Bit Depth - float: ");
Serial.println(sizeof(float) * 8);
Serial.print("Bit Depth - double: ");
Serial.println(sizeof(double) * 8);
//Overwrite Timer1 Configs to work without prescale at 16 MHz
TCCR1B = (1 CS10);
// Test arithmetic operations using hardware timers
unsigned long endTime1,endTime2,endTime3;
int testValue = 12345678910; // A large number for 32-bit, but will overflow on 8-bit
int result;
//Start Timer at 0
TCNT1=0;
result = testValue + testValue;
endTime1 = TCNT1;//Timestamp Addition
result = testValue * testValue;
endTime2 = TCNT1;; //Timestamp Multiplication
result = testValue / 2;
endTime3 = TCNT1; //Timestamp Division
//Print Times per operation
Serial.print("Addition Time (nanoseconds): ");
Serial.println(endTime1*62.5);
Serial.print("Multiplication Time (nanoseconds): ");
Serial.println((endTime2*62.5)-(endTime1*62.5));
Serial.print("Division Time (nanoseconds): ");
Serial.println((endTime3*62.5)-(endTime2*62.5));
}
void loop() {
// No need to repeat the measurements
}
- Cheat Sheet -
As an added bonus we decided to do a Cheat sheet of commonly used commands for Embedded Programming both for python and for c++
Common Syntax Rules
Variables and Data Types
Python: Python simplifies variable usage and data type declaration. Variables are dynamically typed, which means the interpreter infers the data type based on the assigned value, making Python very flexible and easy to use for developers.
C++: In C++, variables must be declared with an explicit data type before they are used. This static typing adds to the language's robustness, enabling compile-time type checking and optimization that can lead to more efficient code execution.
# Variables
x = 10 # int
y = 20.5 # float
name = "Alice" # str
is_valid = True # bool
# No explicit type declaration needed.
// Variables
int x = 10;
float y = 20.5;
std::string name = "Alice";
bool is_valid = true;
// Type must be explicitly declared.
Control Structures
Python: Python's control structures are designed for readability and simplicity. The use of indentation to define block scopes makes Python code very clean and easy to understand, even for beginners.
C++: C++ employs braces ({}) to define the scope of control structures, providing a familiar syntax for those with experience in C or similar languages. It supports a wide range of control flow statements, allowing for complex decision-making and looping with efficiency.
# If-Else
if x > 10:
print("Greater")
elif x == 10:
print("Equal")
else:
print("Lesser")
# Loops
for i in range(5): # For loop
print(i)
i = 0
while i < 5: # While loop
print(i)
i += 1
// If-Else
if (x > 10) {
std::cout << "Greater" << std::endl;
} else if (x == 10) {
std::cout << "Equal" << std::endl;
} else {
std::cout << "Lesser" << std::endl;
}
// Loops
for (int i = 0; i < 5; i++) { // For loop
std::cout << i << std::endl;
}
int i = 0;
while (i < 5) { // While loop
std::cout << i << std::endl;
i++;
}
Functions
Python: Functions in Python are defined using the def keyword. Python supports a variety of function arguments and allows for easy definition of default, keyword, and variable-length arguments, facilitating flexible function calls.
C++: C++ functions must specify the return type explicitly. They provide a powerful feature set, including overloading, default parameters, and templates, which allow for writing generic and reusable code components.
# Defining a function
def add_numbers(a, b):
return a + b
# Calling a function
result = add_numbers(5, 3)
// Defining a function
int addNumbers(int a, int b) {
return a + b;
}
// Calling a function
int result = addNumbers(5, 3);
Comments
Python: Python supports single-line comments initiated with # and multi-line comments enclosed within triple quotes, which can also be used for docstrings in functions, classes, and modules.
C++: ++ uses // for single-line comments and /* */ for multi-line comments. This convention is consistent with many programming languages derived from or inspired by C, making it familiar to a broad audience.
# This is a single-line comment.
'''
This is a
multi-line comment.
'''
// This is a single-line comment.
/*
This is a
multi-line comment.
*/
Basic GPIO use
There's three basic commands for basic GPIO(General Purpose Input Output) these area:
- Initializing a Pin This is like setting up a pin on your microcontroller to either listen in or shout out. When you initialize a pin as an Input, it's like giving it ears to listen for signals coming in. When you set it as an Output, it's like giving it a mouth to send out signals.
- Reading a Pin This means checking what the pin is hearing. Is it a high signal (1), like someone shouting "Yes!"? Or is it a low signal (0), more like a whisper of "No"? It's all about figuring out what message the pin is receiving.
- Writing a Pin This is about sending a message out. When you write to a pin set as an Output, you're telling it to either shout out a "Yes!" (send a high signal or 1) or whisper a "No" (send a low signal or 0). If you mention writing to a pin set as an Input, that's a mix-up; you don't "write" or send messages through ears. You only listen with input pins, and you talk (write signals) with output pins.
# Importar las librerías necesarias
import board
import digitalio
# Initialize pins
# Initialize as output
led = digitalio.DigitalInOut(board.GP25)
led.direction = digitalio.Direction.OUTPUT
# Initialize as input
button = digitalio.DigitalInOut(board.GP14)
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.DOWN
# Write a pin
led.value = True # Establecer el valor en ALTO
led.value = False # Establecer el valor en BAJO
# Read a pin
button_value = button.value
//Define Pins
#define led 25
#define button 27
//Initilize pins
pinMode(led, OUTPUT);
pinMode(button, INPUT);
//Write a Pin
digitalWrite(led,HIGH);//Set Value HIGH
digitalWrite(led,LOW); //Set Value LOW
//Read a Pin
digitalRead(button);
# Import necessary libraries
from machine import Pin
# Initialize pins
# Initialize as output
led = Pin(25, Pin.OUT) # Assuming GP25 corresponds to pin 25
# Initialize as input with pull-down resistor
button = Pin(14, Pin.IN, Pin.PULL_DOWN) # Assuming GP14 corresponds to pin 14
# Write to a pin
led.value(1) # Set the value to HIGH
led.on()
led.value(0) # Set the value to LOW
led.off()
led.toggle() # Toggles the value of pin
# Read from a pin
button_value = button.value()
Analog
Analog readings are all about measuring how much of something is happening. Unlike digital signals, which are like flipping a light switch on or off (just two states), analog signals can vary smoothly across a wide range of values. Think of it like a dimmer switch for your room's lights, where you can adjust the brightness to any level between totally off and super bright.
import board
import analogio
adc = analogio.AnalogIn(board.A0) # Initialize A0 as analog Input
sensorValue = adc.value # Read and save an analog reading
#define analogPin = A0; // Set pin analogPin keyword to A0
int sensorValue = analogRead(analogPin); // Read analogValue
from machine import ADC
#Creating an ADC Object
adc = ADC(Pin(ADC_PIN))
#Configuring ADC Resolution (Optional)
adc = ADC(Pin(ADC_PIN))
adc.width(ADC.WIDTH_12BIT) # Set ADC resolution to 12 bits
#Reading Analog Values
value = adc.read()
Timers
Imagine you're cooking a complicated dish that requires you to keep track of different cooking times for various ingredients. You'd likely use a timer for each one to make sure everything comes out perfectly. In the world of microcontrollers, timers serve a similar purpose, helping you manage when and how often certain pieces of code run. This can be anything from blinking an LED without using delay(), to triggering sensor readings at regular intervals, or even debouncing a button press.
Timers can operate in various modes, like counting up to a specific value, counting down, or repeating actions at set intervals. They're fundamental in moving beyond simple, linear code execution to creating more complex, efficient, and interactive applications.
import time
time.sleep(1) # Delays for 1 second
//Delays for 1 second
delay(1000);
import board
import time
# Record the start time
start_time = time.monotonic()
interval = 1 # Time interval in seconds
while True:
# Check the current time
current_time = time.monotonic()
# If the current time is greater than start time + interval
if current_time - start_time >= interval:
# Do something
# Reset the start time
start_time = current_time
unsigned long previousMillis = 0;
// Interval for delay
const long interval = 1000;
void loop(){
//updates time passed
unsigned long currentMillis = millis();
//checks If the current time is greater than start time + interval
if (currentMillis - previousMillis >= interval) {
//does something
// Reset the start time
previousMillis = currentMillis;
}
}
from machine import Timer
def timer_callback(t):
print("Timer event!")
# Create and start a periodic timer
timer = Timer(0)
timer.init(mode=Timer.PERIODIC, period=2000, callback=timer_callback)
Printing
In both Arduino (C++) and CircuitPython, printing is typically used to send messages back to the computer over USB, which can be viewed in a serial monitor (in the Arduino IDE) or a serial console (for CircuitPython in the Mu Editor). This capability is incredibly helpful for debugging purposes, to display sensor readings, or to show program status messages.
import time
while True:
#Print Messages
print("Hello, world!")
time.sleep(1) # Pause for a second
#print Variables
temperature = 25.3 # Example temperature
print (temperature)
#Print as Formatted String Literal
print(f"The temperature is {temperature} degrees Celsius.")
void setup() {
// Start serial communication at 9600 bauds per second
Serial.begin(9600);
}
void loop() {
// Print a message to the serial monitor
Serial.print("Hello, world!");
// Print a message to the serial monitor with endl
Serial.println("Hello, world!");
// Print a variable to monitor
Serial.println(counter);
// Print in binary, HEX and OCT are also available
Serial.println(counter, BIN);
delay(1000); // Wait for a second
}
PWM
Think of PWM like blinking your eyes. If you blink really fast, it kind of seems like your eyes are open all the time, just a bit less bright. If you blink slowly, it feels like your eyes are mostly closed. PWM works by turning something on and off at a super fast rate, and by changing how long it stays on versus how long it stays off, you can simulate "in-between" states.
Here's the lowdown on how it works:
- Frequency: This is how fast the PWM signal flips between on and off. Imagine tapping your finger on a table rapidly—that's high frequency. Tapping slowly is low frequency. Most of the time, you set the frequency once and forget about it because what really matters is the next part.
- Duty Cycle: This is the real magic of PWM. It's all about the ratio of "on" time to the total cycle time (which is the "on" time plus the "off" time). If the duty cycle is 50%, the signal is on half the time and off half the time. If it's 10%, it's only on a tiny bit of the time, so it seems like it's barely on at all.
import board
import pwmio
# Create a PWMOut object on an LED pin
pwm = pwmio.PWMOut(board.D5, duty_cycle=0, frequency=5000,)
# Change Duty Cycle Value to max 16 bit
pwm.duty_cycle = 65535
# Change Duty Cycle Value to max 16 bit
pwm.duty_cycle = 0
#define pwm = 5; // PWM on pin 5
//set pin 5 as an OUTPUT
pinMode(pwm,OUTPUT);
//writes the duty cycle with value between 0 and 255
int value=127;
analogWrite(pwm,value);
from machine import Pin, PWM
# Create a PWM object in a pin
pwm = PWM(Pin(5)) # IN RP2040 the led is in 5
pwm.freq(5000) # Configure frequency at 5000 Hz
# by asigning a value to x you can change the duty cycle
pwm.duty(x)
Interrupt
Imagine your computer or a tiny brain in a gadget (microcontroller) is busy doing its thing, like playing a video or controlling lights, and doesn't want to be bothered checking every second if you're pressing a button or if something else needs attention. An "interrupt" is like a quick tap on the shoulder—it tells the device to immediately pause what it's doing, check what's needed (like turning a light on with a button press), and then go right back to what it was doing before. This way, the device can react instantly to important stuff without having to constantly stop and check, making it much more efficient.
'''
Define the callback This function will be called
when the interrupt occurs.
'''
def callback(p):
# Code to execute when interrupt occurs
from machine import Pin
# Initialize pin
pin = Pin(pin_number, Pin.IN, Pin.PULL_UP)
#Initialize the pin input, attach an interrupt to it using Pin.irq()
pin.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=callback)
void setup() {
pinMode(2, INPUT_PULLUP); // Set the button pin
/*
Attach an interrupt to a pin
ISR: Interrupt Service Routine - the function to call when the
interrupt occurs.
mode: Determines when the interrupt should be triggered.
Common modes include CHANGE, RISING, FALLING, and LOW.
*/
attachInterrupt(digitalPinToInterrupt(2), interrupt, FALLING);
}
void loop() {
// Main code here
}
void interrupt() {
// Code to execute when interrupt occurs
}
Finally for some fun and color here are some example codes to use the neopixel with
- Arduino be sure to install the library from library manager
- MicroPython with it's library
- CircuitPython be sure to download the bundle and install the needed libraries
Finally as an added bonus here are the UF files needed for MicroPython and for CircuitPython.