Skip to content

Console Driver Guide #33

Description

@rusty1968

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

  1. Blocking vs Non-blocking: embedded_io is typically blocking, while Hubris expects non-blocking operations. Implement timeout-based adaptation.

  2. Error Handling: embedded_io uses ErrorKind enum, adapt to Hubris error patterns.

  3. Buffer Management: embedded_io uses slice-based I/O, adapt to byte-oriented Hubris patterns.

  4. Interrupt Integration: Bridge vendor HAL interrupts to Hubris notification system.

Common Pitfalls

  1. Clock Configuration: Ensure UART clock is properly configured through sys driver
  2. Pin Multiplexing: Correctly configure GPIO alternate functions
  3. Interrupt Priority: Set appropriate interrupt priorities to avoid conflicts
  4. Buffer Sizes: Size buffers appropriately for your use case
  5. Flow Control: Test with and without hardware flow control
  6. Baud Rate Calculation: Verify baud rate calculations are correct for your clock frequency

Debugging Tips

  1. Use ringbuf: Extensively log UART operations for debugging
  2. Logic Analyzer: Use logic analyzer to verify signal timing
  3. Loopback Testing: Test with TX connected to RX for basic functionality
  4. Interrupt Verification: Verify interrupts are firing correctly
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationrfcRequest For Comments - a discussion topic

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions