Lazzerex

Explore
Backend, Systems & Web Development

Home Articles The Secret Behind Codes And Hardware

The Secret Behind Codes And Hardware

H. S. N. Bình -- views

  • Programming

Have you ever wondered how a simple line of code can make an LED blink or control a motor? As a developer, I've often marveled at this seemingly magical transformation from abstract instructions to physical actions in the real world.

Cover image for The Secret Behind Codes And Hardware

Recently, I came across an excellent video by Artful Bytes that demystifies this fundamental concept, and it completely changed how I think about the relationship between software and hardware. Below each section will also contain some code demonstration if you’re interested.

The Mystery That Started It All

The journey begins with a simple question that many developers never think to ask: How does something as abstract as code control something as physical as the pixels on your screen or the components in your devices? While most of us are content with high-level abstractions, understanding the underlying mechanism reveals a surprisingly elegant solution that powers everything from microcontrollers to laptops.

Article image

The Universal Mechanism: Memory-Mapped I/O

At its core, the answer lies in a concept called memory-mapped I/O. Despite its technical-sounding name, the principle is remarkably straightforward: your code communicates with hardware through memory addresses, just like it would with regular data storage.

Think of memory as a vast array of numbered boxes, each capable of holding a value. In high-level languages like Python or JavaScript, you rarely interact with these boxes directly. However, if you've ever worked with pointers in C, you've already encountered this concept. Pointers are simply addresses that let you access these memory locations directly.

Here's where it gets interesting: while most of these memory boxes connect to regular storage like RAM or flash memory, some are wired directly to hardware components. These special addresses form what we call memory-mapped I/O regions.

Article image

From Theory to Practice: Seeing It in Action

To truly appreciate this concept, let's examine how it works in practice, following the demonstrations that Artful Bytes presented in their video.

The Arduino Example

Consider the classic Arduino "Blink" program. On the surface, you see friendly functions like digitalWrite() and pinMode(). However, beneath these convenient abstractions lies the raw truth: direct memory operations.

When you strip away the helper functions, the code becomes remarkably simple:

  • One memory write to configure the pin as an output
  • Two memory writes to toggle the pin on and off

The magic happens when you write specific values to predetermined memory addresses. These addresses, documented in the microcontroller's datasheet, are directly connected to the hardware pins that control your LED.

High-level Arduino code:

                  void setup() {
  pinMode(13, OUTPUT);
}

void loop() {
  digitalWrite(13, HIGH);
  delay(1000);
  digitalWrite(13, LOW);
  delay(1000);
}
                

Direct memory-mapped I/O equivalent:

                  // Direct memory manipulation - Arduino Uno
// Configure pin 13 as output
*(volatile uint8_t*)0x24 |= (1 << 5);  // Set bit 5 in DDRB register

void loop() {
  // Turn LED on
  *(volatile uint8_t*)0x25 |= (1 << 5);   // Set bit 5 in PORTB register
  delay(1000);
  
  // Turn LED off
  *(volatile uint8_t*)0x25 &= ~(1 << 5);  // Clear bit 5 in PORTB register
  delay(1000);
}
                

Scaling Up: The STM32 Trimmer

The same principle applies to more complex devices. In the Artful Bytes video's trimmer example using an STM32 microcontroller, what appears to be sophisticated motor control software ultimately reduces to memory operations:

  • Configuration writes to set up the pins
  • Timer peripheral configuration through memory writes
  • Reading from specific addresses to check button states
  • Enabling and disabling motor signals through targeted memory writes

Importantly, memory-mapped I/O works bidirectionally. Just as you can write to memory addresses to control outputs, you can read from specific addresses to gather input from sensors, buttons, or other hardware components.

High-level HAL code:

                  // Initialize GPIO and Timer
HAL_GPIO_Init(MOTOR_GPIO_Port, &GPIO_InitStruct);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);

// Main loop
while (1) {
  if (HAL_GPIO_ReadPin(BUTTON_GPIO_Port, BUTTON_Pin)) {
    __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 500);
  } else {
    __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 0);
  }
}
                

Direct memory-mapped I/O equivalent:

                  // STM32F4 direct register access
#define GPIOA_BASE    0x40020000
#define TIM1_BASE     0x40010000
#define RCC_BASE      0x40023800

// Enable clocks
*(volatile uint32_t*)(RCC_BASE + 0x30) |= (1 << 0);  // Enable GPIOA clock
*(volatile uint32_t*)(RCC_BASE + 0x44) |= (1 << 0);  // Enable TIM1 clock

// Configure GPIO pins
*(volatile uint32_t*)(GPIOA_BASE + 0x00) |= (0x2 << 16); // PA8 alternate function
*(volatile uint32_t*)(GPIOA_BASE + 0x00) &= ~(0x3 << 0); // PA0 input

// Configure Timer
*(volatile uint32_t*)(TIM1_BASE + 0x2C) = 999;        // Set ARR
*(volatile uint32_t*)(TIM1_BASE + 0x34) = 0;          // Set CCR1

// Main loop
while (1) {
  // Read button state
  if (*(volatile uint32_t*)(GPIOA_BASE + 0x10) & (1 << 0)) {
    *(volatile uint32_t*)(TIM1_BASE + 0x34) = 500;    // Set duty cycle
  } else {
    *(volatile uint32_t*)(TIM1_BASE + 0x34) = 0;      // Stop motor
  }
}
                

Beyond Microcontrollers: The Thermal Printer

Even seemingly complex devices like thermal receipt printers operate on this same principle. Rather than relying on specialized libraries, you can control such devices through direct memory operations:

  • Configure serial communication with just three memory writes
  • Send formatting commands and text data byte by byte
  • All through targeted memory access patterns

Using library:

                  #include "printer_lib.h"

void print_receipt() {
  printer_init();
  printer_set_font(FONT_MEDIUM);
  printer_print_line("RECEIPT");
  printer_feed_lines(2);
  printer_print_line("Total: $10.50");
}
                

Direct memory-mapped I/O equivalent:

                  // Nordic nRF52 UART direct control
#define UART_BASE     0x40002000
#define GPIO_BASE     0x50000000

// Configure UART pins and settings
*(volatile uint32_t*)(UART_BASE + 0x508) = 6;         // TX pin
*(volatile uint32_t*)(UART_BASE + 0x50C) = 8;         // RX pin
*(volatile uint32_t*)(UART_BASE + 0x524) = 0x01D7E000; // 115200 baud
*(volatile uint32_t*)(UART_BASE + 0x500) = 1;         // Enable UART// Data to send
uint8_t print_data[] = {
  0x1B, 0x40,           // Initialize printer
  0x1B, 0x21, 0x08,     // Set font
  'R', 'E', 'C', 'E', 'I', 'P', 'T', '\n',
  '\n', '\n',
  'T', 'o', 't', 'a', 'l', ':', ' ', '$', '1', '0', '.', '5', '0', '\n'
};

// Send data
for (int i = 0; i < sizeof(print_data); i++) {
  // Wait for TX ready
  while (!(*(volatile uint32_t*)(UART_BASE + 0x118) & 1));
  
  // Send byte
  *(volatile uint32_t*)(UART_BASE + 0x51C) = print_data[i];
}
                

The Underlying Mechanism

Understanding how this works requires a brief look under the hood of a microcontroller. When you write to regular memory, the process is straightforward: the CPU puts an address on the address bus, places data on the data bus, and signals the RAM to store the value.

Memory-mapped I/O follows an almost identical process, with one crucial difference: an address decoder acts like a sophisticated routing system. This decoder examines each memory address and determines whether it should go to regular memory or to specific hardware components. When an address corresponds to a hardware register, the decoder enables that particular hardware block instead of the RAM.

This elegant design means that from the CPU's perspective, controlling hardware is indistinguishable from accessing regular memory. There are no special instructions or complex protocols – just standard memory operations that happen to trigger real-world actions.

The Universal Truth: Even Your Laptop

Article image

Perhaps the most striking demonstration of memory-mapped I/O's universality comes from applying it to everyday devices. Even your laptop's screen brightness control, typically adjusted through a graphical interface, can be controlled through direct memory writes.

While modern operating systems restrict direct memory access for security reasons, it's still possible (with the right permissions and kernel modifications) to write values directly to memory addresses that control hardware functions. This isn't practical for everyday use, but it powerfully demonstrates that the same fundamental mechanism governs everything from simple microcontrollers to complex computer systems.

GUI method (conceptual):

                  # Using system tools
xrandr --output eDP-1 --brightness 0.7
                

Direct memory access method:

                  # Enable direct memory access (requires root and kernel modifications)# WARNING: This can damage your system if done incorrectly# Using devmem tool to write to memory-mapped registers# Find the brightness control register (varies by system)
sudo devmem 0xFED90000 32 0x80000000  # Example address and value# Or using C code with /dev/mem
                
                  // Direct memory access in C (Linux)
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int control_brightness(uint32_t brightness_value) {
    int mem_fd = open("/dev/mem", O_RDWR | O_SYNC);
    if (mem_fd == -1) return -1;
    
    // Map physical memory to virtual address space
    void* mapped_base = mmap(0, 4096, PROT_READ | PROT_WRITE, 
                           MAP_SHARED, mem_fd, 0xFED90000);
    
    if (mapped_base == MAP_FAILED) {
        close(mem_fd);
        return -1;
    }
    
    // Write to the brightness control register
    *(volatile uint32_t*)mapped_base = brightness_value;
    
    // Clean up
    munmap(mapped_base, 4096);
    close(mem_fd);
    return 0;
}
                

Why This Matters

Memory-mapped I/O represents what we might call the "fundamental theorem of embedded systems." Just as calculus connects differentiation and integration through its fundamental theorem, memory-mapped I/O creates a unified bridge between the software and hardware worlds.

This understanding provides several key insights:

Simplicity in Complexity: No matter how sophisticated a system appears, hardware control often reduces to manipulating bits in memory locations.

Portability: Because the CPU treats I/O operations like regular memory access, code can be more easily ported between different systems.

Unified Programming Model: Developers can use the same mental model for both data manipulation and hardware control.

Memory Access Comparison

Regular memory access:

                  // Writing to RAM
int global_variable = 42;
*(int*)0x20000000 = global_variable;  // Store in RAM// Reading from RAM
int value = *(int*)0x20000000;        // Read from RAM
                

Memory-mapped I/O access:

                  // Writing to hardware register
*(volatile uint32_t*)0x40020014 = 0x00000100;  // Turn on LED// Reading from hardware register
uint32_t button_state = *(volatile uint32_t*)0x40020010;  // Read GPIO input
                

Address Decoder Logic (Conceptual)

                  // Simplified representation of what happens in hardware
void address_decoder(uint32_t address, uint32_t data, bool write_enable) {
    if (address >= 0x20000000 && address < 0x20020000) {
        // RAM range
        if (write_enable) {
            ram_write(address, data);
        }
    }
    else if (address >= 0x40020000 && address < 0x40020400) {
        // GPIO range
        if (write_enable) {
            gpio_write(address, data);
        }
    }
    else if (address >= 0x40010000 && address < 0x40010400) {
        // Timer range
        if (write_enable) {
            timer_write(address, data);
        }
    }
    // ... more hardware blocks
}
                

The Broader Implications

While memory-mapped I/O isn't the only mechanism for software-hardware communication (interrupts, DMA, and port-mapped I/O also exist), it's by far the most common and often works in conjunction with these other methods.

This knowledge also explains certain aspects of embedded programming that might otherwise seem arbitrary:

  • Why C remains dominant in embedded systems
  • Why pointers are so prevalent in hardware-related code
  • Why the volatile keyword exists (it prevents compiler optimizations that assume memory won't change unexpectedly)
                  // Without volatile - compiler might optimize away
int *led_register = (int*)0x40020014;
*led_register = 1;  // Turn on LED
*led_register = 0;  // Turn off LED - might be optimized out!// With volatile - compiler preserves all memory accesses
volatile int *led_register = (volatile int*)0x40020014;
*led_register = 1;  // Turn on LED
*led_register = 0;  // Turn off LED - guaranteed to execute
                

Conclusion

The next time you write code that interacts with hardware, whether it's blinking an LED, reading a sensor, or controlling a motor, remember that beneath all the abstractions and convenient libraries lies this elegant truth: you're simply writing values to special memory addresses that happen to be connected to the physical world.

As Artful Bytes beautifully demonstrated in their video, understanding memory-mapped I/O doesn't just satisfy curiosity, it opens up a world of possibilities. Once you realize that hardware is "just memory," you begin to see that with the right knowledge and tools, you truly can control almost anything.


This article was inspired by the excellent educational content from Artful Bytes. I highly recommend checking out their channel for more deep dives into embedded systems and the fascinating intersection of software and hardware.

This blog post is inspired by this video: https://youtu.be/sp3mMwo3PO0?si=XHwur-tnpKa4HYLT


Read more at: Lazzerex’s Blog

Source:  Published Notion page