#Day1 — Bare-Metal MCU: Intro to registers

bhuvaneshwari kanagaraj
6 min readJul 6, 2024

--

Hi All, Bhuvaneshwari here! I’m excited to share that I’m taking up a 9-day challenge to learn bare-metal coding from scratch. Are you ready to take up this challenge with me? Here’s what you’ll need:

  1. Consistency
  2. Dedication
  3. Interest

These are the essential ingredients for this journey. This is the first post in the series, and today, we will be writing the hardest blink program ever in your life.

Why Did I Switch to Coding back to AVR?

I initially faced significant challenges and struggled to understand the code. This led me to continue my bare-metal programming with the AVR microcontroller. However, I decided to push myself further by tackling the STM32.

What is Bare-Metal Programming?

Bare-metal programming is a low-level type of programming that directly interacts with the hardware, without using an operating system or abstraction layer. It involves writing code that communicates directly with the system’s components and takes into account their specific details.

Throughout this blog series, I’ll be referencing this datasheet extensively.

We have two types of memory

Program Memory:
This is where your program is stored.

Data Memory:
This is where your temporary data is stored.

Often, you will notice many empty spaces in Data Memory. These spaces are registers, which are specific locations in RAM that serve special purposes for the microcontroller.

Now that we understand the memory structure, let’s dive into Arduino bare metal programming. We’ll utilize Pin 13 on the Arduino, which has a built-in LED.

To begin, let’s take a careful look at the Arduino pinout diagram and proceed to write the code.

If you’ve ever wondered about the different ports like PORT B, PORT C, and PORT D on your Arduino, these refer to specific groups of digital I/O pins grouped together for easier control and management.

PORT B:

  • Pins: This port typically corresponds to digital pins 8 through 13 on an Arduino Uno.
  • Usage: You can manipulate PORT B to control these pins simultaneously or individually by setting or clearing specific bits within the PORTB register.
  • Example: PORTB |= (1 << PB0); would set digital pin 8 high.

What does this PB5 mean?

It is PORT B and Bit 5.

PORT C:

  • Pins: This port corresponds to the analog pins A0 through A5 on an Arduino Uno.
  • Usage: Although these pins are primarily used for analog input, they can also be used as digital I/O pins, and PORT C allows direct control over these pins.
  • Example: PORTC &= ~(1 << PC1); would set analog pin A1 (used as a digital pin) low.

PORT D:

  • Pins: This port corresponds to digital pins 0 through 7 on an Arduino Uno.
  • Usage: PORT D can be used to control these pins directly, useful for tasks that require fast and efficient pin manipulation.
  • Example: PORTD ^= (1 << PD2); would toggle digital pin 2.

Okay now let's write code.

void setup() {

pinMode(LED_BUILTIN, OUTPUT);
}

void loop(){

PORTB = 32;
delay(1000);
PORTB = 0;
delay(1000);
}

The objective here is to move away from using digitalRead and digitalWritefunctions entirely.

I conducted a quick experiment to compare the normal Arduino code and the bare metal approach for toggling pin 9. Below is a rephrased explanation and the code I used, which I copied from ChatGPT:

// Define the output pin for the PWM signal
const int pwmPin = 3; // Change this to any PWM-capable pin

void setup() {
// Set the PWM pin as an output
pinMode(pwmPin, OUTPUT);
}

void loop() {
// Generate a sinusoidal PWM signal
for (int i = 0; i < 256; i++) {
int val = 128 + 127 * sin(i * 2 * PI / 256); // Sinusoidal calculation
analogWrite(pwmPin, val); // Output the PWM signal
delay(10); // Adjust delay as needed for the frequency
}
}

Here is the measurement of Frequency; Its 491 Hz.

Okay Now let's take the bare metal code into comparison;

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <math.h>

#define PWM_PIN 3 // PWM output pin (OC2B) on Arduino Uno

volatile uint8_t dutyCycle = 128; // Initial duty cycle for PWM (50%)

void setupPWM() {
// Set PWM pin (OC2B) as output
DDRD |= (1 << PD3);

// Timer/Counter2 Configuration for PWM
TCCR2A |= (1 << COM2B1) | (1 << WGM20) | (1 << WGM21); // Non-inverting PWM mode, Fast PWM
TCCR2B |= (1 << CS20); // No prescaler, running at F_CPU (16 MHz)

OCR2A = 255; // Set TOP (max value) for Timer/Counter2
}

void setPWMDutyCycle(uint8_t duty) {
OCR2B = duty; // Set PWM duty cycle (0-255)
}

void generateSinWave() {
const float amplitude = 127.5; // Amplitude of sinusoidal waveform (0-255)
const float frequency = 1.0; // Frequency of sinusoidal waveform in Hz

while (1) {
for (int i = 0; i < 256; i++) {
uint8_t val = amplitude + amplitude * sin(2 * M_PI * frequency * i / 256);
setPWMDutyCycle(val);
_delay_ms(10); // Adjust delay as needed for frequency control
}
}
}

int main() {
setupPWM(); // Initialize PWM on OC2B pin (D3)

generateSinWave(); // Start generating sinusoidal waveform

return 0;
}

Here is the measurement of Frequency; it is 62.5kHz.

Okay, now the question is can we get rid of pinMode?

Yes, it’s Possible!!

Page Number 72
  1. PORTB: This register controls the output state of the pins. Writing to PORTB sets the pins to HIGH or LOW.
  2. DDRB: This register sets the data direction of the pins. Writing to DDRB configures the pins as INPUT or OUTPUT.
  3. PINB: This register reads the current state of the pins. Reading PINB returns the current pin levels.

Okay we know D13 is PB5and if we want to make it a DDRB .

Changing the code correspondingly.

void setup() {

DDRB = 32; // Its sets PB5 as High
//pinMode(LED_BUILTIN, OUTPUT);
}

void loop(){

PORTB = 32;
delay(1000);
PORTB = 0;
delay(1000);
}

Next, let’s get rid of the delay. Will an empty for loop work? Let’s give it a try.

void setup() {

DDRB = 32; // Its sets PB5 as High
//pinMode(LED_BUILTIN, OUTPUT);
}

void loop(){

PORTB = 32;
for (long i =0; i<1000000; i++){}
PORTB = 0;
for (long i =0; i<1000000; i++){}
delay(1000);
}

The Arduino compiler is quite sophisticated. It optimizes the code, so it might ignore an empty loop to improve speed.

To prevent this optimization, we need to trick the compiler into performing an actual task.

void setup() {

DDRB = 32; // Its sets PB5 as High
//pinMode(LED_BUILTIN, OUTPUT);
}

void loop(){

PORTB = 32;
for (long i =0; i<1000000; i++){
PORTB = 32;
}
PORTB = 0;
for (long i =0; i<1000000; i++){
PORTB = 0;
}
}

Our compiler doesn’t handle registers directly. Since register manipulation involves direct access, the compiler executes the process without any optimization or questioning.

See you all in the next blog post.

--

--