π Data Transmission Between CPU and Peripheral Devices via I/O Methods
MMIO (Memory-Mapped I/O)
MMIO (Memory-Mapped I/O) is a way for the central processing unit (CPU) to interact with peripheral hardware devices (e.g. UART
, timers
, GPIO
, SPI
, etc.) by "sticking" the registers of these devices to specific addresses in the CPU memory.
These addresses in the CPU do not store regular data, but allow reading and writing to the device control registers.
This means that the peripheral device can be accessed in the same way as regular memory - via regular memory read/write instructions (LDR
, STR
, MOV
, etc.). at specific addresses.
How does it work inside CPU?
- The CPU has a single address bus, which is used to access both RAM and MMIO devices.
- When the CPU accesses an address that belongs to the MMIO area:
- In this case, the control logic understands that this is not ordinary memory, but a device register.
- Signals go to the periphery, not to RAM/ROM.
π§· Key features
- It is necessary to work through
volatile
, so that the compiler does not optimize access to the device. - Addresses and devices are specified by the platform manufacturer (e.g.
ARM Cortex-M
,x86
)
Characteristic | volatile |
atomic |
mutex |
---|---|---|---|
Scenario | Read/write MMIO registers Check flag or register status | Interthread counter, ready flag Safe logical condition (compare, increment) | Work with buffer, list, structure |
Ensures visibility of changes | β Yes (to the compiler) | β Yes (across threads/cores) | β Yes |
Ensures atomicity | β No | β
Yes (e.g., fetch_add ) |
β Yes (via locking) |
Protects from data races | β No | β Yes (on primitive types) | β Yes (on any resource) |
Blocks other threads | β No | β No | β Yes |
Supports complex operations | β No | β οΈ Limited (e.g., arithmetic, flags) | β Yes (conditions, buffers, etc.) |
Use in Embedded | β MMIO (memory-mapped I/O) | β Counters, flags, semaphores | β RTOS, critical sections |
Rust usage | ptr::read_volatile , PAC |
core::sync::atomic::* |
critical-section , Mutex<T> |
Performance | π Fast | β‘ Fast | π’ Slower (due to blocking) |
πΈ volatile
Compilers like to optimize code: if a variable doesn't change in the current context, they can not re-read it.
- The device can change the register itself
- The variable can change in another context
volatile
tells the compiler: "don't optimize access to this variable - read/write it each time, because it can change at any time outside the current code."
- It is only used with MMIO.
- Rust doesn't have a built-in
volatile
keyword, but provides functionality viacore::ptr
andvolatile-safe
types:
use core::ptr::{read_volatile, write_volatile};
// unsafe is required for any direct volatile access
let ptr = 0x4000_0000 as *mut u32;
// Write the value without buffering
unsafe {
write_volatile(ptr, 0x1234);
}
// Read the value with optimizations disabled
let value = unsafe {
read_volatile(ptr)
};
πΈ atomic
- Provides atomic access to data (e.g. counters, flags).
- Suitable for thread synchronization on multiprocessor systems.
- Can be used without locks (lock-free).
- AtomicBool, AtomicU8, AtomicUsize, etc.
use core::sync::atomic::{AtomicBool, Ordering};
static READY: AtomicBool = AtomicBool::new(false);
fn main() {
READY.store(true, Ordering::Release);
if READY.load(Ordering::Acquire) {
// safe to read
}
}
πΈ mutex
- Provides mutual exclusion.
- Suitable for large objects consisting of many variables or structures.
- More commonly used in multitasking systems (
RTOS
,OS
). - In embedded:
- critical-section (interrupt lock),
- Mutex
from embassy, ββRTIC, or spin::Mutex without an OS.
use critical_section::Mutex;
use core::cell::RefCell;
static SHARED: Mutex<RefCell<Option<u32>>> = Mutex::new(RefCell::new(None));
fn example() {
critical_section::with(|cs| {
*SHARED.borrow(cs).borrow_mut() = Some(42);
});
}
Debugging firmware
Classic software development is always inside the cycle of write - run - check - fix.
In classic systems, debugging is simplified by built-in logs, breakpoints and step-by-step code execution directly in the IDE.
When developing programs for microcontrollers, debugging is significantly more complicated since the code is written on a PC and executed on a device with a completely different architecture.
Obtaining debug information, setting breakpoints, accessing registers and local variables require a special environment and integration of tools, and the process itself becomes much less obvious.
π In this article I want to describe the standard process when debugging software running on a microcontroller and describe the tools that play a key role in this.
I2C (Inter-Integrated Circuit)
SPI (Serial Peripheral Interface)
UART (Universal Asynchronous Receiver / Transmitter)
Serial Protocols
Digital systems are based on the concept of bits, in addition to using bits they often have to transfer them back and forth usually between two components (MCU and Sensor, LCD etc).
π All the different methods of bit transfer can be divided into 2 categories - Parallel
and Serial
transfer
Embedded π¦ crates ecosystem
Review of the ecosystem of crates used for programming microcontrollers in Rust
In general, developing programs for MCU is aimed at obtaining some information from the surrounding world, performing necessary calculations with this data and interacting back. Using Rust gives a huge freedom in choosing the level of abstraction at which the developer can interact with the MCU.
π In this article I'll take a closer look at these abstraction levels and some other useful crates.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BSP (Board Support Package) β ββΆ Specific board (pins, display, LED, etc.)
β ββ Peripheral configuration, pins, displays β
βββββββββββββββββββββββββββββββββββββββββββββ€
β Embedded HAL β ββΆ Traits for Cross-Platform Compatibility
β ββ Common Interfaces (digital::OutputPin) β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β HAL (Hardware Abstraction Layer) β ββΆ Simplified management of timers, GPIO, UART
β ββ Implementation of embedded-hal traits via PAC
βββββββββββββββββββββββββββββββββββββββββββββ€
β PAC (Peripheral Access Crate) β ββΆ SVD-based API generation
β ββ Direct access to registers, type-safe
βββββββββββββββββββββββββββββββββββββββββββββ€
β SVD (System View Description) β ββΆ XML description of all registers, their bits and fields
β ββ Basis for PAC autogeneration (via svd2rust)
ββββββββββββββββββββββββββββββββββββββββββββββ€
β Register Management (Low Level) β ββΆ Working directly with addresses and registers via unsafe
β ββ Working with MMIO, volatile, bitmasks β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
π§ͺ RP2040 Pico W Lab
Experiments, Prototypes & Notes with the Raspberry Pi Pico W
This repository contains my projects, experiments, and notes using the Raspberry Pi Pico W, built on the RP2040 microcontroller with integrated Wi-Fi.
It serves as a playground for testing ideas, learning embedded concepts, and building small prototypes.