Development Notes - Hayasen Crate
Crate Structure
Module Organization
The crate follows a modular design pattern optimized for embedded systems:
hayasen/
├── src/
│ ├── lib.rs # Main library entry point
│ ├── error.rs # Unified error types
│ ├── functions.rs # Function registry system
│ └── mpu9250.rs # MPU9250 sensor implementation
├── examples/ # Usage examples
└── tests/ # Integration tests
Key Architectural Decisions
1. Feature-Gated Compilation
#![allow(unused)] fn main() { // lib.rs #[cfg(feature = "mpu9250")] pub mod mpu9250; #[cfg(feature = "mpu9250")] pub use crate::mpu9250::*; }
Rationale: The crate uses conditional compilation to include only the sensors you need, reducing binary size and compilation time. This is crucial for resource-constrained embedded systems.
Usage:
[dependencies]
hayasen = { version = "x.x.x", features = ["mpu9250"] }
2. Prelude Pattern
#![allow(unused)] fn main() { pub mod prelude { pub use crate::error::Error; #[cfg(feature = "mpu9250")] pub use crate::mpu9250::*; pub use embedded_hal::i2c::I2c; } }
Purpose: Provides a convenient way to import commonly used types and traits. Users can simply use hayasen::prelude::*;
to get everything they need.
3. Convenience Layer (mpu9250_hayasen
)
#![allow(unused)] fn main() { pub mod mpu9250_hayasen { // Simplified API for common operations pub fn create_default<I2C, E>(i2c: I2C, address: u8) -> Result<mpu9250::Mpu9250<I2C>, Error<E>> pub fn read_all<I2C, E>(sensor: &mut mpu9250::Mpu9250<I2C>) -> Result<(f32, [f32; 3], [f32; 3]), Error<E>> } }
Benefits:
- Reduces boilerplate for common operations
- Provides sensible defaults for new users
- Maintains access to low-level API for advanced users
Driver Architecture
Core Driver Structure
#![allow(unused)] fn main() { pub struct Mpu9250<I2C> { i2c: I2C, // Owned I2C peripheral address: u8, // Device I2C address accel_scale: f32, // Cached scaling factor for acceleration gyro_scale: f32, // Cached scaling factor for angular velocity } }
Design Principles:
- Ownership Model: The driver takes ownership of the I2C peripheral to prevent conflicts
- Zero-Cost Abstractions: Scale factors are pre-calculated and cached to avoid runtime divisions
- Type Safety: Generic over I2C type for platform independence
State Management
The driver maintains minimal state to reduce memory footprint:
- Scale factors are calculated once during configuration
- No internal buffering or filtering (left to user applications)
- Stateless register operations for maximum flexibility
no_std
Compatibility
Core Requirements
#![allow(unused)] #![no_std] #![no_main] // For applications, not the library itself fn main() { }
Memory Management
No Dynamic Allocation
#![allow(unused)] fn main() { // ✓ Stack-allocated arrays let mut buffer = [0u8; 6]; // ✗ Avoid heap allocation // let mut buffer = vec![0u8; 6]; // This would require std }
Fixed-Size Buffers
All I2C operations use compile-time sized buffers:
#![allow(unused)] fn main() { pub fn read_accel_raw(&mut self) -> Result<[i16; 3], Error<E>> { let mut buffer = [0u8; 6]; // Fixed size, stack allocated // ... } }
Core Library Dependencies
The crate only depends on core
and embedded-hal
:
#![allow(unused)] fn main() { use core::fmt::{Debug, Formatter, Result}; // ✓ Core library use embedded_hal::i2c::I2c; // ✓ Hardware abstraction // ✗ Avoid std dependencies // use std::vec::Vec; // use std::collections::HashMap; }
Floating Point Considerations
Target Compatibility
#![allow(unused)] fn main() { // The crate uses f32 for sensor data conversion let temperature = (raw as f32) / 340.0 + 36.53; }
Notes:
- Uses
f32
for better performance on 32-bit ARM Cortex-M - All calculations are optimized for embedded floating-point units
- For targets without FPU, consider using fixed-point arithmetic wrapper
Alternative for No-FPU Targets
#![allow(unused)] fn main() { // Example fixed-point implementation (not included in crate) pub fn read_acceleration_fixed(&mut self) -> Result<[i32; 3], Error<E>> { let raw = self.read_accel_raw()?; // Scale to milligee (1/1000 g) using integer math let scale_factor = (self.accel_scale * 1000.0) as i32; Ok([ (raw[0] as i32 * scale_factor) / 1000, (raw[1] as i32 * scale_factor) / 1000, (raw[2] as i32 * scale_factor) / 1000, ]) } }
Const Generics and Compile-Time Optimization
#![allow(unused)] fn main() { // Register addresses are compile-time constants const WHO_AM_I: u8 = 0x75; const ACCEL_XOUT_H: u8 = 0x3B; // Enums are zero-cost abstractions #[derive(Debug, Clone, Copy, PartialEq)] pub enum AccelRange { Range2G, // Compiles to simple integer values Range4G, Range8G, Range16G, } }
Safety Considerations
I2C Communication Safety
Transaction Atomicity
#![allow(unused)] fn main() { pub fn read_accel_raw(&mut self) -> Result<[i16; 3], Error<E>> { let mut buffer = [0u8; 6]; self.i2c.write(self.address, &[ACCEL_XOUT_H])?; // Register pointer self.i2c.read(self.address, &mut buffer)?; // Read data // ... process buffer } }
Critical Points:
- Atomic Operations: Each register read is a complete write-then-read transaction
- Error Propagation: I2C errors are immediately propagated using
?
operator - Buffer Safety: Fixed-size buffers prevent overflow issues
Address Validation
#![allow(unused)] fn main() { pub fn verify_identity(&mut self) -> Result<(), Error<E>> { let mut buffer = [0u8]; self.i2c.write(self.address, &[WHO_AM_I])?; self.i2c.read(self.address, &mut buffer)?; if buffer[0] != WHO_AM_I_VALUE { return Err(Error::NotDetected); // Explicit validation } Ok(()) } }
Safety Features:
- Device Verification: Always check WHO_AM_I register before operation
- Type Safety: Rust's type system prevents incorrect address usage
- Explicit Error States: Clear error types for different failure modes
Memory Safety
Buffer Bounds Checking
#![allow(unused)] fn main() { // Safe array access - Rust prevents buffer overruns at compile time let x = ((buffer[0] as i16) << 8) | buffer[1] as i16; // Safe indexing let y = ((buffer[2] as i16) << 8) | buffer[3] as i16; // Bounds checked let z = ((buffer[4] as i16) << 8) | buffer[5] as i16; // Compile-time verified }
Integer Overflow Protection
#![allow(unused)] fn main() { // Safe conversion with explicit casting let x = raw[0] as f32 * self.accel_scale; // Explicit type conversion // For temperature calculation, use checked arithmetic in critical applications pub fn read_temperature_celsius_safe(&mut self) -> Result<f32, Error<E>> { let raw = self.read_temp_raw()?; let temp_f64 = (raw as f64) / 340.0 + 36.53; // Higher precision intermediate Ok(temp_f64 as f32) } }
Register Access Safety
Read-Modify-Write Operations
#![allow(unused)] fn main() { pub fn wake_up(&mut self) -> Result<(), Error<E>> { let mut buffer = [0u8]; self.i2c.write(self.address, &[PWR_MGMT_1])?; // Read current value self.i2c.read(self.address, &mut buffer)?; let new_config = buffer[0] & 0xBF; // Clear sleep bit safely self.i2c.write(self.address, &[PWR_MGMT_1, new_config])?; // Write back Ok(()) } }
Safety Measures:
- Atomic RMW: Complete read-modify-write sequence
- Bit Masking: Safe bit manipulation using explicit masks
- State Preservation: Only modify intended bits, preserve others
Error Handling Strategy
Comprehensive Error Types
#![allow(unused)] fn main() { #[derive(Clone, PartialEq, Eq)] pub enum Error<E> { I2c(E), // Underlying I2C errors InvalidData, // Data validation failures NotDetected, // Device not found/responding ConfigError, // Configuration parameter errors SensorSpecific(&'static str), // Sensor-specific error messages } }
Error Recovery Patterns
#![allow(unused)] fn main() { // Recommended error handling pattern fn safe_sensor_operation() -> Result<(), Error<YourI2cError>> { let mut retry_count = 0; const MAX_RETRIES: u8 = 3; loop { match sensor.read_acceleration() { Ok(data) => return Ok(process_data(data)), Err(Error::I2c(_)) if retry_count < MAX_RETRIES => { retry_count += 1; delay_ms(10); // Brief delay before retry continue; }, Err(e) => return Err(e), // Propagate non-recoverable errors } } } }
Concurrency Safety
Single-Threaded Design
#![allow(unused)] fn main() { // The driver requires &mut self for all operations impl<I2C, E> Mpu9250<I2C> where I2C: I2c<Error = E> { pub fn read_acceleration(&mut self) -> Result<[f32; 3], Error<E>> // ^^^^ Exclusive access required } }
Implications:
- Thread Safety: Not
Send
orSync
by default - I2C peripherals are typically not thread-safe - Exclusive Access: Prevents concurrent access to I2C bus
- RAII Pattern: Resource cleanup handled by Rust's ownership system
Interrupt Safety
#![allow(unused)] fn main() { // For interrupt-driven applications fn interrupt_safe_reading() { // Disable interrupts during I2C transaction if required by your platform critical_section::with(|_cs| { let result = sensor.read_acceleration(); // Process result... }); } }
Performance Considerations
Zero-Copy Design
#![allow(unused)] fn main() { // Direct array return - no heap allocation pub fn read_accel_raw(&mut self) -> Result<[i16; 3], Error<E>> { // Returns stack-allocated array directly } }
Minimal Register Access
#![allow(unused)] fn main() { // Efficient register reading - single transaction for multiple axes let mut buffer = [0u8; 6]; // Read all 6 bytes (3 axes × 2 bytes) at once self.i2c.write(self.address, &[ACCEL_XOUT_H])?; self.i2c.read(self.address, &mut buffer)?; }
Development Workflow
Testing Strategy
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; // Mock I2C for unit testing struct MockI2c { expected_writes: Vec<Vec<u8>>, read_responses: Vec<Vec<u8>>, } impl I2c for MockI2c { type Error = (); fn write(&mut self, _addr: u8, data: &[u8]) -> Result<(), Self::Error> { // Verify expected write operations Ok(()) } fn read(&mut self, _addr: u8, buffer: &mut [u8]) -> Result<(), Self::Error> { // Return mock data Ok(()) } } #[test] fn test_sensor_initialization() { let mock_i2c = MockI2c::new(); let mut sensor = Mpu9250::new(mock_i2c, 0x68); // Test initialization logic... } } }
Documentation Guidelines
#![allow(unused)] fn main() { /// Reads accelerometer data in g-force units /// /// # Returns /// /// Array of [X, Y, Z] acceleration values in g-force units. /// The scaling is automatically applied based on the configured range. /// /// # Errors /// /// Returns `Error::I2c` if communication fails. /// /// # Example /// /// ```no_run /// use hayasen::prelude::*; /// /// let mut sensor = Mpu9250::new(i2c, 0x68); /// sensor.initialize_sensor(AccelRange::Range2G, GyroRange::Range250Dps)?; /// let accel = sensor.read_acceleration()?; /// println!("Acceleration: {:?}", accel); /// ``` pub fn read_acceleration(&mut self) -> Result<[f32; 3], Error<E>> }
Platform Integration Notes
Embedded HAL Compatibility
#![allow(unused)] fn main() { // Generic over any I2C implementation impl<I2C, E> Mpu9250<I2C> where I2C: I2c<Error = E> // Uses embedded-hal trait { // Platform-agnostic implementation } }
Benefits:
- Platform Independence: Works with any embedded-hal compliant I2C driver
- Type Safety: Compile-time verification of I2C compatibility
- Error Propagation: Preserves underlying platform error types
Memory Layout Considerations
#![allow(unused)] fn main() { #[repr(C)] struct SensorData { temperature: f32, // 4 bytes accel: [f32; 3], // 12 bytes gyro: [f32; 3], // 12 bytes } // Total: 28 bytes - predictable layout }
Safety Patterns
Resource Management
#![allow(unused)] fn main() { // RAII pattern ensures I2C is properly released impl<I2C> Drop for Mpu9250<I2C> { fn drop(&mut self) { // I2C peripheral is automatically returned when Mpu9250 is dropped // No explicit cleanup required due to Rust's ownership system } } }
Initialization Safety
#![allow(unused)] fn main() { pub fn initialize_sensor(&mut self, accel_range: AccelRange, gyro_range: GyroRange) -> Result<(), Error<E>> { self.verify_identity()?; // Always verify device first self.configure_power()?; // Ensure proper power state self.setup_accelerometer(accel_range)?; // Configure before use self.setup_gyroscope(gyro_range)?; // Configure before use Ok(()) } }
Safety Chain:
- Device identity verification prevents wrong device communication
- Power configuration ensures device is ready
- Sensor configuration sets known state before operation
Register Access Patterns
#![allow(unused)] fn main() { // Safe register write pattern fn write_register(&mut self, register: u8, value: u8) -> Result<(), Error<E>> { // Always write register address followed by data self.i2c.write(self.address, &[register, value])?; Ok(()) } // Safe register read pattern fn read_register(&mut self, register: u8) -> Result<u8, Error<E>> { let mut buffer = [0u8; 1]; self.i2c.write(self.address, &[register])?; // Set register pointer self.i2c.read(self.address, &mut buffer)?; // Read data Ok(buffer[0]) } }
Error Safety Guarantees
Fail-Fast Design
#![allow(unused)] fn main() { // Sensor operations fail immediately on error pub fn read_acceleration(&mut self) -> Result<[f32; 3], Error<E>> { let raw = self.read_accel_raw()?; // Fail fast on I2C error // Only proceed if raw read succeeded let x = raw[0] as f32 * self.accel_scale; // ... } }
Error Propagation Chain
#![allow(unused)] fn main() { // Error chain: I2C Error -> Driver Error -> Application Error Hardware I2C Error ↓ (From<E> implementation) Driver Error<E> ↓ (? operator) Application Error Handling }
Integration Patterns
Dependency Injection
#![allow(unused)] fn main() { // The driver doesn't create its own I2C - it's injected let i2c = platform_specific_i2c_setup(); let sensor = Mpu9250::new(i2c, address); // Dependency injection }
Benefits:
- Testability with mock I2C implementations
- Platform flexibility
- Resource sharing control
Builder Pattern Extension
#![allow(unused)] fn main() { // Example extension for complex configurations impl<I2C, E> Mpu9250<I2C> where I2C: I2c<Error = E> { pub fn builder(i2c: I2C, address: u8) -> SensorBuilder<I2C> { SensorBuilder::new(i2c, address) } } pub struct SensorBuilder<I2C> { sensor: Mpu9250<I2C>, configured: bool, } impl<I2C, E> SensorBuilder<I2C> where I2C: I2c<Error = E> { pub fn with_accel_range(mut self, range: AccelRange) -> Result<Self, Error<E>> { self.sensor.setup_accelerometer(range)?; Ok(self) } pub fn build(self) -> Result<Mpu9250<I2C>, Error<E>> { if !self.configured { return Err(Error::ConfigError); } Ok(self.sensor) } } }
Debugging and Development
Register Debug Utilities
#![allow(unused)] fn main() { #[cfg(debug_assertions)] impl<I2C, E> Mpu9250<I2C> where I2C: I2c<Error = E> { pub fn dump_registers(&mut self) -> Result<(), Error<E>> { let registers = [WHO_AM_I, PWR_MGMT_1, ACCEL_CONFIG, GYRO_CONFIG]; for ® in ®isters { match self.read_register(reg) { Ok(value) => println!("Register 0x{:02X}: 0x{:02X}", reg, value), Err(e) => println!("Failed to read register 0x{:02X}: {:?}", reg, e), } } Ok(()) } } }
Compile-Time Configuration Validation
#![allow(unused)] fn main() { // Use const assertions for compile-time validation const _: () = { assert!(WHO_AM_I_VALUE == 0x71, "Incorrect WHO_AM_I value for MPU9250"); }; }
Performance Optimization Notes
Batch Operations
#![allow(unused)] fn main() { // Reading all axes in single I2C transaction is more efficient pub fn read_accel_raw(&mut self) -> Result<[i16; 3], Error<E>> { let mut buffer = [0u8; 6]; // Read all 3 axes at once // More efficient than 3 separate 2-byte reads } }
Cache-Friendly Access Patterns
#![allow(unused)] fn main() { // Scale factors are cached to avoid repeated calculations impl<I2C, E> Mpu9250<I2C> { pub fn setup_accelerometer(&mut self, range: AccelRange) -> Result<(), Error<E>> { let (config_value, scale) = match range { AccelRange::Range2G => (0x00, 2.0 / 32768.0), // Pre-calculated // ... }; self.accel_scale = scale; // Cache for future use Ok(()) } } }
Future Development Considerations
Extensibility
- Modular Design: Easy to add new sensor support via feature flags
- Trait Abstractions: Common sensor operations could be abstracted into traits
- Async Support: Could be extended for async I2C operations
Version Compatibility
#![allow(unused)] fn main() { // Use semantic versioning for breaking changes // Major: Breaking API changes // Minor: New features, backwards compatible // Patch: Bug fixes only }
The crate is designed with embedded systems' constraints in mind, prioritizing safety, performance, and resource efficiency while maintaining a clean, intuitive API.