UART Console Integration Guide for Hubris
This guide provides detailed instructions for integrating a UART-based console for a new microcontroller in the Hubris ecosystem, assuming the vendor HAL provides embedded_io blocking APIs.
Overview
Hubris uses blocking task semantics with interrupt-driven I/O - tasks block waiting for interrupt notifications, then perform immediate hardware operations. The system supports two main architectural approaches: library-style (direct hardware access) and driver task (IPC-based).
Architecture Decision: Library vs Driver Task
Library Approach
- When to use: Direct console access, high-performance requirements, single client
- Examples:
drv/stm32h7-usart (library), task/uartecho
- Pros: Lower latency, direct hardware access, simpler integration
- Cons: Less isolation, single-owner constraint
Driver Task Approach (Recommended)
- When to use: Multiple clients, need isolation, system services
- Examples:
drv/stm32fx-usart, drv/lpc55-usart
- Pros: Better isolation, multiple clients, cleaner separation
- Cons: Higher latency due to IPC, more complex
Step-by-Step Implementation
1. Create HAL Abstraction Layer
First, create a HAL abstraction that bridges embedded_io to Hubris patterns:
// drv/{mcu}-usart/src/lib.rs
#![no_std]
use embedded_io::{Read, Write, ErrorType};
pub struct Usart {
uart: VendorUart, // Your vendor's UART implementation
}
impl Usart {
pub fn turn_on(
sys: &Sys,
uart_instance: VendorUart,
peripheral: Peripheral,
pins: &[(PinSet, Alternate)],
clock_hz: u32,
baud_rate: u32,
hardware_flow_control: bool,
) -> Self {
// Enable clocks and configure peripheral
sys.enable_clock(peripheral);
sys.leave_reset(peripheral);
// Configure UART with vendor HAL
uart_instance.configure(baud_rate, /* other params */);
// Configure GPIO pins
for &(mask, alternate) in pins {
sys.gpio_configure_alternate(
mask,
OutputType::PushPull,
Speed::Low,
Pull::None,
alternate,
);
}
// Enable interrupts
uart_instance.enable_rx_interrupt();
Self { uart: uart_instance }
}
// Hubris-style non-blocking interface
pub fn try_tx_push(&self, byte: u8) -> bool {
// Adapt embedded_io Write to non-blocking
match self.uart.write(&[byte]) {
Ok(_) => true,
Err(_) => false, // FIFO full or other error
}
}
pub fn try_rx_pop(&self) -> Option<u8> {
let mut buf = [0u8; 1];
match self.uart.read(&mut buf) {
Ok(1) => Some(buf[0]),
_ => None, // No data available
}
}
pub fn check_and_clear_rx_overrun(&self) -> bool {
// Check vendor-specific overrun status
self.uart.check_and_clear_overrun_error()
}
pub fn enable_rx_interrupt(&self) {
self.uart.enable_interrupt(RxInterrupt);
}
pub fn enable_tx_fifo_empty_interrupt(&self) {
self.uart.enable_interrupt(TxEmptyInterrupt);
}
pub fn disable_tx_fifo_empty_interrupt(&self) {
self.uart.disable_interrupt(TxEmptyInterrupt);
}
}
2. Handle embedded_io to Hubris Interface Translation
Create an adapter layer to translate between embedded_io blocking APIs and Hubris interrupt-driven patterns:
// Key consideration: embedded_io is blocking, but Hubris uses interrupt-driven immediate checks
impl Usart {
fn try_tx_push(&self, byte: u8) -> bool {
// Check if TX FIFO has space - immediate hardware status check
if !self.uart.tx_fifo_full() {
// Use blocking write since we know FIFO has space
match self.uart.write(&[byte]) {
Ok(_) => true,
Err(_) => false,
}
} else {
false // FIFO full, caller will re-enable TX interrupt and block
}
}
fn try_rx_pop(&self) -> Option<u8> {
// Check if RX FIFO has data - immediate hardware status check
if !self.uart.rx_fifo_empty() {
let mut buf = [0u8; 1];
match self.uart.read(&mut buf) {
Ok(1) => Some(buf[0]),
_ => None,
}
} else {
None // No data available
}
}
}
3. Create Console Task
Based on the task/uartecho pattern, create your console task:
// task/{mcu}-console/src/main.rs
#![no_std]
#![no_main]
use drv_{mcu}_usart as drv_usart;
use drv_usart::Usart;
use heapless::Deque;
use ringbuf::*;
use userlib::*;
task_slot!(SYS, sys);
#[derive(Debug, Clone, Copy, PartialEq)]
enum UartLog {
Tx(u8),
TxFull,
Rx(u8),
RxOverrun,
}
ringbuf!(UartLog, 64, UartLog::Rx(0));
const BUF_LEN: usize = 256; // Larger buffer for console
enum ConsoleState {
EchoChar(u8),
FlushLine,
ProcessCommand,
}
#[export_name = "main"]
fn main() -> ! {
let uart = configure_uart_device();
let mut line_buf = Deque::<u8, BUF_LEN>::new();
let mut console_state = None;
sys_irq_control(notifications::USART_IRQ_MASK, true);
loop {
sys_recv_notification(notifications::USART_IRQ_MASK);
// Handle TX state machine
while let Some(state) = console_state.take() {
match state {
ConsoleState::EchoChar(byte) => {
if uart.try_tx_push(byte) {
ringbuf_entry!(UartLog::Tx(byte));
} else {
console_state = Some(ConsoleState::EchoChar(byte));
uart.enable_tx_fifo_empty_interrupt();
break;
}
}
ConsoleState::FlushLine => {
// Implement line processing logic
process_command_line(&mut line_buf);
}
ConsoleState::ProcessCommand => {
// Handle command processing
// This is where you'd integrate with your system's command interface
}
}
}
// Handle RX
if uart.check_and_clear_rx_overrun() {
ringbuf_entry!(UartLog::RxOverrun);
}
while let Some(byte) = uart.try_rx_pop() {
ringbuf_entry!(UartLog::Rx(byte));
match byte {
b'\r' | b'\n' => {
// Line complete - process command
console_state = Some(ConsoleState::ProcessCommand);
uart.enable_tx_fifo_empty_interrupt();
break;
}
b'\x08' | b'\x7f' => {
// Backspace/Delete
if let Some(_) = line_buf.pop_back() {
// Echo backspace sequence
console_state = Some(ConsoleState::EchoChar(b'\x08'));
}
}
byte if byte.is_ascii_graphic() || byte == b' ' => {
// Printable character
if line_buf.push_back(byte).is_ok() {
console_state = Some(ConsoleState::EchoChar(byte));
}
}
_ => {
// Ignore other control characters
}
}
}
sys_irq_control(notifications::USART_IRQ_MASK, true);
}
}
fn configure_uart_device() -> Usart {
use drv_usart::device;
use drv_usart::vendor_hal_api::*;
const CLOCK_HZ: u32 = 100_000_000; // Adjust for your MCU
const BAUD_RATE: u32 = 115_200;
// Pin configuration - adapt to your MCU
const PINS: &[(PinSet, Alternate)] = &[
(Port::A.pin(9).and_pin(10), Alternate::UART), // TX, RX
];
// Get UART peripheral instance from vendor HAL
let uart_instance = vendor_hal::Uart::new(/* params */);
let peripheral = Peripheral::Uart1; // Adjust for your MCU
Usart::turn_on(
&Sys::from(SYS.get_task_id()),
uart_instance,
peripheral,
PINS,
CLOCK_HZ,
BAUD_RATE,
false, // hardware_flow_control
)
}
fn process_command_line(line_buf: &mut Deque<u8, BUF_LEN>) {
// Convert buffer to string and process commands
let mut cmd_str = heapless::String::<BUF_LEN>::new();
while let Some(byte) = line_buf.pop_front() {
if cmd_str.push(byte as char).is_err() {
break;
}
}
// Process command (integrate with your system's command handler)
match cmd_str.trim() {
"help" => {
// Send help text
}
"status" => {
// Send system status
}
cmd => {
// Handle unknown command
}
}
}
include!(concat!(env!("OUT_DIR"), "/notifications.rs"));
4. Configure Cargo.toml
# drv/{mcu}-usart/Cargo.toml
[package]
name = "drv-{mcu}-usart"
version = "0.1.0"
edition = "2021"
[dependencies]
embedded-io = "0.6"
{vendor-hal} = "0.1" # Your vendor's HAL crate
drv-stm32xx-sys-api = { path = "../stm32xx-sys-api" } # Adapt for your sys API
userlib = { path = "../../sys/userlib" }
[features]
default = []
uart1 = []
uart2 = []
# Add features for different UART instances
# task/{mcu}-console/Cargo.toml
[package]
name = "task-{mcu}-console"
version = "0.1.0"
edition = "2021"
[dependencies]
drv-{mcu}-usart = { path = "../../drv/{mcu}-usart" }
heapless = "0.8"
userlib = { path = "../../sys/userlib" }
ringbuf = { path = "../../lib/ringbuf" }
[features]
default = ["uart1"]
uart1 = ["drv-{mcu}-usart/uart1"]
uart2 = ["drv-{mcu}-usart/uart2"]
baud_rate_115_200 = []
baud_rate_3M = []
hardware_flow_control = []
5. Application Configuration
Add to your application's TOML file:
# app/{your-app}/app.toml
[tasks.console]
name = "task-{mcu}-console"
priority = 3
max-sizes = { flash = 16384, ram = 8192 }
features = ["uart1", "baud_rate_115_200"]
uses = ["uart1", "gpioa"]
interrupts = {"uart1.irq" = "usart-irq"}
task-slots = ["sys"]
[tasks.sys]
name = "drv-{mcu}-sys"
# ... sys configuration
# If using library approach, no separate UART driver task needed
# If using driver task approach:
[tasks.uart_driver]
name = "drv-{mcu}-usart"
priority = 2
max-sizes = { flash = 8192, ram = 4096 }
features = ["uart1"]
uses = ["uart1", "gpioa"]
interrupts = {"uart1.irq" = "usart-irq"}
task-slots = ["sys"]
6. Integration Considerations
Interrupt Handling
// Ensure proper interrupt configuration in your build.rs
// or app configuration
fn configure_interrupts() {
// Map hardware UART IRQ to task notification
// This is typically handled by the build system
}
Memory Management
// Static allocation for embedded systems
static mut UART_BUFFER: [u8; 1024] = [0; 1024];
// Use ringbuf for logging and debugging
ringbuf!(ConsoleLog, 128, ConsoleLog::Info(""));
Error Handling
impl Usart {
pub fn handle_errors(&self) -> Result<(), UartError> {
// Check for frame errors, parity errors, etc.
if self.uart.has_frame_error() {
self.uart.clear_frame_error();
return Err(UartError::FrameError);
}
if self.uart.has_parity_error() {
self.uart.clear_parity_error();
return Err(UartError::ParityError);
}
Ok(())
}
}
Build and Test
Building
# Build the console task
cargo xtask build app/{your-app}/app.toml console
# Build complete application
cargo xtask dist app/{your-app}/app.toml
# Flash and test
cargo xtask flash app/{your-app}/app.toml
Testing
# Connect serial terminal (minicom, screen, etc.)
minicom -D /dev/ttyUSB0 -b 115200
# Test basic echo
# Type characters and verify echo
# Test command processing
help
status
# Test buffer limits
# Type long lines to test buffer handling
# Test error conditions
# Test overrun scenarios if possible
Advanced Features
Command Processing Integration
// Integrate with system command handlers
use task_jefe::Command;
fn process_system_command(cmd: &str) -> Response {
match cmd {
"reset" => {
// Trigger system reset
jefe::reset_system()
}
"tasks" => {
// List running tasks
jefe::list_tasks()
}
_ => Response::UnknownCommand
}
}
Hardware Flow Control
impl Usart {
pub fn configure_flow_control(&self, enable: bool) {
if enable {
self.uart.enable_hardware_flow_control();
// Configure CTS/RTS pins
self.configure_flow_control_pins();
}
}
}
Multiple Baud Rate Support
#[cfg(feature = "baud_rate_115_200")]
const BAUD_RATE: u32 = 115_200;
#[cfg(feature = "baud_rate_3M")]
const BAUD_RATE: u32 = 3_000_000;
#[cfg(feature = "baud_rate_9600")]
const BAUD_RATE: u32 = 9_600;
Key Differences from embedded-hal
-
Blocking vs Non-blocking: embedded_io is typically blocking, while Hubris expects non-blocking operations. Implement timeout-based adaptation.
-
Error Handling: embedded_io uses ErrorKind enum, adapt to Hubris error patterns.
-
Buffer Management: embedded_io uses slice-based I/O, adapt to byte-oriented Hubris patterns.
-
Interrupt Integration: Bridge vendor HAL interrupts to Hubris notification system.
Common Pitfalls
- Clock Configuration: Ensure UART clock is properly configured through sys driver
- Pin Multiplexing: Correctly configure GPIO alternate functions
- Interrupt Priority: Set appropriate interrupt priorities to avoid conflicts
- Buffer Sizes: Size buffers appropriately for your use case
- Flow Control: Test with and without hardware flow control
- Baud Rate Calculation: Verify baud rate calculations are correct for your clock frequency
Debugging Tips
- Use ringbuf: Extensively log UART operations for debugging
- Logic Analyzer: Use logic analyzer to verify signal timing
- Loopback Testing: Test with TX connected to RX for basic functionality
- Interrupt Verification: Verify interrupts are firing correctly
- Clock Verification: Ensure UART peripheral clock is enabled
Conclusion
This guide provides a solid foundation for integrating UART console functionality with modern embedded_io-based vendor HALs in the Hubris ecosystem. Adapt the specific details to your target microcontroller's requirements and vendor HAL API.
UART Console Integration Guide for Hubris
This guide provides detailed instructions for integrating a UART-based console for a new microcontroller in the Hubris ecosystem, assuming the vendor HAL provides
embedded_ioblocking APIs.Overview
Hubris uses blocking task semantics with interrupt-driven I/O - tasks block waiting for interrupt notifications, then perform immediate hardware operations. The system supports two main architectural approaches: library-style (direct hardware access) and driver task (IPC-based).
Architecture Decision: Library vs Driver Task
Library Approach
drv/stm32h7-usart(library),task/uartechoDriver Task Approach (Recommended)
drv/stm32fx-usart,drv/lpc55-usartStep-by-Step Implementation
1. Create HAL Abstraction Layer
First, create a HAL abstraction that bridges
embedded_ioto Hubris patterns:2. Handle embedded_io to Hubris Interface Translation
Create an adapter layer to translate between
embedded_ioblocking APIs and Hubris interrupt-driven patterns:3. Create Console Task
Based on the
task/uartechopattern, create your console task:4. Configure Cargo.toml
5. Application Configuration
Add to your application's TOML file:
6. Integration Considerations
Interrupt Handling
Memory Management
Error Handling
Build and Test
Building
Testing
Advanced Features
Command Processing Integration
Hardware Flow Control
Multiple Baud Rate Support
Key Differences from embedded-hal
Blocking vs Non-blocking:
embedded_iois typically blocking, while Hubris expects non-blocking operations. Implement timeout-based adaptation.Error Handling:
embedded_iousesErrorKindenum, adapt to Hubris error patterns.Buffer Management:
embedded_iouses slice-based I/O, adapt to byte-oriented Hubris patterns.Interrupt Integration: Bridge vendor HAL interrupts to Hubris notification system.
Common Pitfalls
Debugging Tips
Conclusion
This guide provides a solid foundation for integrating UART console functionality with modern
embedded_io-based vendor HALs in the Hubris ecosystem. Adapt the specific details to your target microcontroller's requirements and vendor HAL API.