core Library
core Library
Level 17 — Embedded & Systems Programming The foundational, dependency-free subset of the Rust Standard Library — containing essential types (
Option,Result,Iterator), language traits (Copy,Clone,Send), and intrinsics that require zero operating system support and zero heap memory allocations.
Exercise 3: Bare-Metal Fixed-Point Mathematics and Bit Manipulation (core::num)
Scenario:
In an embedded motor speed controller (#![no_std]), floating-point hardware is disabled to conserve power. Motor velocity and acceleration must be calculated using 32-bit fixed-point arithmetic (Q16.16 format) provided by core::num and bitwise bit manipulation methods (leading_zeros, rotate_left, saturating_add).
- Implement
FixedPoint16wrappingi32. - Implement fixed-point multiplication and saturating addition using
coremethods. - Include unit tests with assertions (
assert_eq!) verifying fixed-point math and overflow saturation.
Answer
Implementation
#![no_std]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FixedPoint16(pub i32); // Q16.16 fixed-point format
impl FixedPoint16 {
pub const SCALE: i32 = 65536; // 2^16
pub fn from_int(val: i32) -> Self {
Self(val.saturating_mul(Self::SCALE))
}
pub fn to_int(self) -> i32 {
self.0 / Self::SCALE
}
pub fn add(self, rhs: Self) -> Self {
Self(self.0.saturating_add(rhs.0))
}
pub fn mul_q16(self, rhs: Self) -> Self {
let product = (self.0 as i64).saturating_mul(rhs.0 as i64);
Self((product >> 16) as i32)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fixed_point_math() {
let a = FixedPoint16::from_int(10);
let b = FixedPoint16::from_int(5);
let sum = a.add(b);
assert_eq!(sum.to_int(), 15);
let prod = a.mul_q16(b);
assert_eq!(prod.to_int(), 50);
}
}
Technical Explanation
- Zero-Dependency Math:
core::numprovides saturating arithmetic (saturating_add,saturating_mul) without requiring OS or standard library support. - Fixed-Point Scaling: Shift arithmetic (
>> 16) maintains fixed-point precision within 32-bit registers. - No-Std Safety: Pure
coremath executes safely across any microcontroller CPU target.
1. Prerequisites
allocLibrary — The heap-allocating extension built on top ofcore.
2. Term Category
Rust Core Foundation (dependency-free platform-agnostic library): core is the minimal core of the Rust Standard Library. It is implicitly embedded inside std (std::option::Option is re-exported from core::option::Option). In #![no_std] environments, core is always present and guarantees zero OS syscalls and zero dynamic memory allocations.
3. Explanation
(1) Design Motivation — "Why did we design this?"
To enable portable code sharing between high-level web services and low-level bare-metal hardware:
- Language primitives (
bool,i32,f64, slices&[T]) need math operations. - Data structures need error handling (
Result<T, E>) and optionality (Option<T>). - Formatting needs byte string formatting (
core::fmt).
Rust strictly separated core from std:
core: Contains everything that can run on a bare microprocessor without an OS or heap manager.std: Extendscorewith OS capabilities (Files, Sockets, Threads, System Allocator).
(2) Code Examples
Pure core Functions in #![no_std]
#![no_std]
use core::cmp::max;
use core::mem::size_of;
pub fn calculate_max_size<T>() -> usize {
let base_size = size_of::<T>();
max(base_size, 8)
}
pub fn safe_divide(numerator: u32, denominator: u32) -> Option<u32> {
if denominator == 0 {
None
} else {
Some(numerator / denominator)
}
}
4. Common Mistakes & Pitfalls
Mistake 2: Attempting to Use std::vec::Vec or std::string::String Directly from core
The mistake: Expecting core to contain heap-allocated collections like Vec or HashMap.
Why it's wrong: core is completely platform-agnostic and contains zero heap-allocation abstractions.
Fix: Import heap types from alloc or use fixed-capacity stack arrays in core.
Mistake 3: Invoking OS-Dependent Threading or Filesystem APIs Inside core Code
The mistake: Expecting thread spawning or file I/O to be supported in core.
Why it's wrong: core targets bare-metal hardware where operating system primitives do not exist.
Fix: Restrict core code to pure algorithms, data formatting, and pointer manipulation.
Mistake 1: Assuming core Performs Heap Allocations
The mistake: Expecting core::vec::Vec or core::string::String to exist in core.
Why it's wrong: core requires zero heap allocation capabilities. Heap types live in alloc.
5. Practice Exercises
Exercise 1: Zero-Allocation core Slice & Iterator Processing
Scenario: Implement a #![no_std] binary payload parser fn extract_packet<'a>(buffer: &'a [u8], magic_header: &[u8]) -> Result<(&'a [u8], &'a [u8]), &'static str> using only core::slice and core::result::Result. The function must locate a frame delimited by magic_header, extract a 2-byte big-endian payload length header, and return a tuple (payload, remaining_bytes) without allocating heap memory or using std.
Answer
Implementation
#![no_std]
/// Parse a frame: [magic_header] [len_u16_be] [payload...] [remaining...]
pub fn extract_packet<'a>(
buffer: &'a [u8],
magic_header: &[u8],
) -> Result<(&'a [u8], &'a [u8]), &'static str> {
if magic_header.is_empty() || buffer.len() < magic_header.len() + 2 {
return Err("Buffer too short or invalid magic header");
}
// Locate magic header position using core::slice::windows
let header_pos = buffer
.windows(magic_header.len())
.position(|window| window == magic_header)
.ok_or("Magic header not found")?;
let payload_start = header_pos + magic_header.len();
if buffer.len() < payload_start + 2 {
return Err("Truncated header length field");
}
// Read 2-byte big-endian length using core slice indexing and primitive byte conversion
let len_bytes: [u8; 2] = [buffer[payload_start], buffer[payload_start + 1]];
let payload_len = u16::from_be_bytes(len_bytes) as usize;
let data_start = payload_start + 2;
let data_end = data_start + payload_len;
if buffer.len() < data_end {
return Err("Incomplete payload frame");
}
let payload = &buffer[data_start..data_end];
let remaining = &buffer[data_end..];
Ok((payload, remaining))
}
pub fn test_extract_packet() {
let raw_stream = [
0xAA, 0xBB, // Noise
0xDE, 0xAD, // Magic Header
0x00, 0x04, // Length (4 bytes big-endian)
0x01, 0x02, 0x03, 0x04, // Payload
0xFF, 0xFE, // Remaining stream data
];
let magic = [0xDE, 0xAD];
let result = extract_packet(&raw_stream, &magic);
assert!(result.is_ok());
let (payload, remaining) = result.unwrap();
assert_eq!(payload, &[0x01, 0x02, 0x03, 0x04]);
assert_eq!(remaining, &[0xFF, 0xFE]);
}
Technical Explanation
- Uses
core::slice::windowsandIterator::positionto search for subslice patterns without standard library string or vector helpers. u16::from_be_bytesconverts fixed 2-byte slices directly into numeric lengths in#![no_std].- Zero-copy lifetime propagation (
'a) ensures extracted payload slices borrow directly from the input buffer without allocation.
Exercise 2: Custom core::fmt::Display & Zero-Heap Telemetry Formatter
Scenario: Implement core::fmt::Display for a telemetry sensor diagnostic enumeration enum TelemetryStatus { Nominal { voltage_mv: u16 }, Degraded { err_code: u8 }, Critical(&'static str) } using core::fmt::Formatter. Write a test function using a static fixed-size core::fmt::Write adapter buffer to print formatted status messages in a #![no_std] environment.
Answer
Implementation
#![no_std]
use core::fmt::{self, Write};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TelemetryStatus {
Nominal { voltage_mv: u16 },
Degraded { err_code: u8 },
Critical(&'static str),
}
impl fmt::Display for TelemetryStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TelemetryStatus::Nominal { voltage_mv } => {
write!(f, "STATUS: NOMINAL ({} mV)", voltage_mv)
}
TelemetryStatus::Degraded { err_code } => {
write!(f, "STATUS: DEGRADED (Err Code: 0x{:02X})", err_code)
}
TelemetryStatus::Critical(reason) => {
write!(f, "STATUS: CRITICAL ({})", reason)
}
}
}
}
// Fixed-capacity buffer implementing core::fmt::Write for no_std testing
pub struct ArrayString<const N: usize> {
buf: [u8; N],
len: usize,
}
impl<const N: usize> ArrayString<N> {
pub const fn new() -> Self {
Self { buf: [0; N], len: 0 }
}
pub fn as_str(&self) -> &str {
core::str::from_utf8(&self.buf[..self.len]).unwrap_or("")
}
}
impl<const N: usize> Write for ArrayString<N> {
fn write_str(&mut self, s: &str) -> fmt::Result {
let bytes = s.as_bytes();
if self.len + bytes.len() > N {
return Err(fmt::Error);
}
self.buf[self.len..self.len + bytes.len()].copy_from_slice(bytes);
self.len += bytes.len();
Ok(())
}
}
pub fn test_telemetry_formatting() {
let status = TelemetryStatus::Degraded { err_code: 0x1F };
let mut out = ArrayString::<64>::new();
write!(out, "{}", status).expect("Formatting failed");
assert_eq!(out.as_str(), "STATUS: DEGRADED (Err Code: 0x1F)");
}
Technical Explanation
- Custom domain data structures implement
core::fmt::Displayusingwrite!macro provided entirely bycore. - Implement
core::fmt::Writeon a stack-allocated byte array buffer (ArrayString<N>), enabling string formatting in embedded software without depending onstd::string::Stringor heap allocation.
6. Related Terms
allocLibrary — The heap-allocating extension built on top ofcore.- The Rust Standard Library (
std) — Related concept: The Rust Standard Library (std).
7. Key Takeaways
coreis the dependency-free foundation of Rust, requiring zero OS support and zero heap memory.- Provides
Option,Result,Iterator,Copy,Clone,mem,fmt, and primitive math. - Always available in all Rust compilation targets.