stm32l4xx-hal

repository·master·Indexed 41 days ago

https://github.com/stm32-rs/stm32l4xx-hal

Hardware abstraction layer for the STM32L4 family of microcontrollers, providing high-level, type-safe Rust APIs for interacting with hardware peripherals. Version 0.7.1 includes support for CRC configuration, DMA transfer management with DMAFrame and Transfer APIs, and flash memory programming including page erasure and native u64 writes.

Tokens
14.7K
Snippets
60
Records
73
Agent score
51%

What's inside stm32l4xx-hal

  1. Configure the RTC with RtcConfig

    master

    The RtcConfig struct allows you to define the clock source, wakeup clock source, and prescaler factors for the Real-Time Clock. You can use a builder-like pattern to configure these settings before initializing the Rtc instance.

    Available clock sources (RtcClockSource):

    • NoClock (0b00)
    • LSE (0b01): Low Speed External oscillator
    • LSI (0b10): Low Speed Internal oscillator
    • HSE (0b11): High Speed External oscillator divided by 32

    Available wakeup clock sources (RtcWakeupClockSource):

    • RtcClkDiv16 (0b000)
    • RtcClkDiv8 (0b001)
    • RtcClkDiv4 (0b010)
    • RtcClkDiv2 (0b011)
    • CkSpre (0b100): Typically 1 Hz clock.
    let config = RtcConfig::default()
        .clock_config(RtcClockSource::LSE)
        .async_prescaler(127)
        .sync_prescaler(255)
        .wakeup_clock_config(RtcWakeupClockSource::CkSpre);
    
    // Use config to initialize Rtc...
  2. DMA Transfer Traits

    master

    The HAL uses several traits to define how different DMA configurations behave. Depending on your use case, you will implement or call methods defined in these traits:

    TraitPurpose
    ReadDma<B, RS>Performs a single DMA read from peripheral to memory using buffer B with word type RS.
    WriteDma<B, TS>Performs a single DMA write from memory to peripheral using buffer B with word type TS.
    TransferDma<B, TS>Performs a simultaneous bidirectional DMA transfer between memory and peripheral.
    CircReadDma<B, RS>Provides circular DMA reading capabilities from peripheral to memory.

    These traits are the entry points for starting transfers like .read(), .write(), .transfer(), or .circ_read().

  3. Configure UART pin configurations and modes

    master

    The Pins<USART> trait is used by the HAL to detect the capabilities of your pin configuration, such as whether hardware flow control, RS485 (DEM) mode, or half-duplex mode is enabled. You can provide different tuples of pins to satisfy different hardware requirements:

    • Standard Rx+Tx: Use a tuple (Tx, Rx). Sets FLOWCTL, DEM, and HALF_DUPLEX to false.
    • Half-Duplex (Tx only): Use a tuple (Tx,) where Tx implements TxHalfDuplexPin. Sets HALF_DUPLEX to true.
    • Hardware Flow Control: Use a tuple (Tx, Rx, Rts, Cts) where pins implement TxPin, RxPin, RtsDePin, and CtsPin. Sets FLOWCTL to true.
    • RS485 Mode (DEM): Use a tuple (Tx, Rx, De) where De implements RtsDePin. Sets DEM to true.
    // Example conceptual usage of pin tuples for different modes
    // Note: Actual instantiation depends on specific peripheral and pin types
    
    // Standard mode
    let pins = (tx_pin, rx_pin);
    
    // Half-duplex mode
    let pins = (tx_pin,);
    
    // Hardware flow control mode
    let pins = (tx_pin, rx_pin, rts_pin, cts_pin);
    
    // RS485 mode
    let pins = (tx_pin, rx_pin, de_pin);
  4. Representing flash pages with FlashPage

    master

    The FlashPage struct represents a specific region of flash memory. In this HAL, each flash page corresponds to a region of 2048 bytes. Because the flash controller can only perform erase operations on a page basis, you must use FlashPage when calling erase methods.

    // FlashPage is a wrapper around a usize representing the page index
    let page = FlashPage(0);
  5. How FlashProgramming manages locking

    master

    The FlashProgramming struct uses the RAII (Resource Acquisition Is Initialization) pattern to manage the flash controller's lock state.

    When you call unlock_flash, you receive a FlashProgramming instance. When this instance goes out of scope and is dropped, it automatically calls an internal lock() method to secure the flash registers. This ensures that the flash controller is not left in an unlocked, vulnerable state if an error occurs or when programming is complete.

  6. Program Flash memory

    master

    To program the flash memory, you must first unlock the flash registers using the KEYR register. This returns a FlashProgramming object which provides an interface for erasing and writing data. The FlashProgramming object automatically locks the flash controller when it is dropped.

    Workflow

    1. Constrain the FLASH peripheral: Use FlashExt::constrain() on the raw FLASH peripheral to get a Parts struct containing the necessary registers.
    2. Unlock: Call keyr.unlock_flash(&mut sr, &mut cr) to obtain a FlashProgramming instance.
    3. Erase: Use erase_page(page) to clear a specific page.
    4. Write: Use write(address, data) for byte-oriented writes or write_native(address, data) for high-performance u64 writes.

    Warning: erase_all_pages() will erase the entire flash, including the currently running program. This should only be called if the program is running from RAM.

    fn program_region(mut flash: flash::Parts) -> Result<(), flash::Error> {
        // Unlock the flashing module
        let mut prog = flash.keyr.unlock_flash(&mut flash.sr, &mut flash.cr)?;
    
        let page = flash::FlashPage(5);
    
        // Perform the erase and programming operation
        prog.erase_page(page)?;
        let data = [
            0x1111_1112_1113_1114,
            0x2221_2222_2223_2224,
            0x3331_3332_3333_3334,
        ];
        prog.write_native(page.to_address(), &data)?;
    
        // Check result
        let addr = page.to_address() as *const u64;
        assert!(unsafe { core::ptr::read(addr) } == data[0]);
        assert!(unsafe { core::ptr::read(addr.offset(1)) } == data[1]);
        assert!(unsafe { core::ptr::read(addr.offset(2)) } == data[2]);
    
        Ok(())
    }
  7. Configure and freeze the system clocks

    master

    The CFGR (Clock Configuration Register) builder allows you to define the desired clock tree for your STM32L4 device. Once configured, you must call .freeze() to apply the settings.

    Note: .freeze() requires mutable references to the ACR (Flash Access Control Register) and Pwr (Power Control) peripherals to adjust flash latency and unlock the backup domain if necessary.

    Common configuration methods include:

    • .sysclk(freq): Sets the system (core) frequency.
    • .hclk(freq): Sets the AHB bus frequency.
    • .pclk1(freq) / .pclk2(freq): Sets APB bus frequencies.
    • .msi(range): Sets the Multi-Speed Internal clock frequency using MsiFreq.
    • .hse(freq, bypass, css): Configures the High-Speed External clock.
    • .lse(bypass, css): Configures the Low-Speed External clock.
    • .hsi48(bool): Enables the 48 MHz HSI clock for USB/RNG/SDMMC.
    • .sysclk_with_pll(freq, pll_config): Sets the system frequency using a specific PLL configuration.
    let clocks = rcc.cfgr
        .sysclk(80.MHz()
        .hclk(80.MHz())
        .pclk1(40.MHz())
        .pclk2(80.MHz())
        .freeze(&mut flash.acr, &mut pwr);
  8. Handle DMA transfer completion and errors

    master

    When using DMA for serial communication, you can use FrameReader and FrameSender to manage the lifecycle of data frames.

    FrameSender (Transmitting)

    • send(frame): Starts a new DMA transfer with the provided buffer. Returns Err(frame) if a transfer is already in progress.
    • transfer_complete_interrupt(): Should be called inside the DMA Transfer Complete interrupt. It clears the interrupt and returns the Option<BUFFER> (the frame used for the transfer) so you can reuse or free it.
    • ongoing_transfer(): Returns true if a transmission is currently active.

    FrameReader (Receiving)

    FrameReader handles incoming data and can be triggered by different interrupt types:

    • transfer_complete_interrupt(next_frame): Called when the DMA transfer finishes.
    • character_match_interrupt(next_frame): Called when a specific character is matched (e.g., in USART).
    • receiver_timeout_interrupt(next_frame): Called when a receiver timeout occurs.

    All these methods return the BUFFER that was being used for the previous transfer, allowing for continuous frame rotation.

    // Example conceptual flow for a Sender
    if let Err(old_frame) = sender.send(new_frame) { 
        // Handle busy state
    }
    
    // Inside DMA Interrupt
    if let Some(completed_frame) = sender.transfer_complete_interrupt() {
        // Process completed_frame
    }
  9. Configure the target MCU via Cargo features

    master

    The stm32l4xx-hal crate requires you to enable a specific feature corresponding to your microcontroller in your Cargo.toml. If no valid feature is enabled, the crate will fail to compile with a compile_error!.

    Supported features include:

    • L4x1 series: stm32l431, stm32l451, stm32l471
    • L4x2 series: stm32l412, stm32l422, stm32l432, stm32l442, stm32l452, stm32l462
    • L4x3 series: stm32l433, stm32l443
    • L4x5 series: stm32l475
    • L4x6 series: stm32l476, stm32l486, stm32l496, stm32l4a6
    • L4+ series: stm32l4r9, stm32l4s9 (Note: HAL integration for these is currently slim)
    [dependencies]
    stm32l4xx-hal = {
        version = "0.7.1",
        features = ["stm32l432"]
    }
  10. Manage Low Power Timer interrupts and events

    master

    The LowPowerTimer supports two interrupt events:

    1. Event::CompareMatch: Occurs when the counter matches the compare value.
    2. Event::AutoReloadMatch: Occurs when the counter reaches the auto-reload value (the counter is then reset to 0).

    Workflow for handling interrupts:

    1. Enable listening: Call .listen(event) to enable interrupts for a specific event.
    2. Check trigger status: In your interrupt handler or main loop, call .is_event_triggered(event) to check if the event occurred.
    3. Clear flag: If is_event_triggered returns true, you must call .clear_event_flag(event) to prevent the interrupt from firing continuously.
    4. Disable listening: Call .unlisten(event) to stop receiving interrupts for that event.
    // Enable compare match interrupt
    lptim1.listen(Event::CompareMatch);
    
    // In an interrupt handler or polling loop:
    if lptim1.is_event_triggered(Event::CompareMatch) {
        lptim1.clear_event_flag(Event::CompareMatch);
        // Handle the event...
    }
  11. Configure and use the CRC peripheral

    master

    To use the CRC peripheral, you must first constrain it using the CrcExt::constrain method, which enables the peripheral's power via the AHB1 bus. Once constrained, you use a builder pattern on the returned Config object to set the polynomial, initial value, and bit reversal settings. Calling .freeze() applies these settings and returns a Crc instance.

    With the Crc instance, you can feed data using .feed(&[u8]) and retrieve the calculation result using .result() (which resets the peripheral) or .peek_result() (which does not reset it).

    let crc = dp.CRC.constrain(&mut rcc.ahb1);
    
    // Lets use the CRC-16-CCITT polynomial
    let mut crc = crc.polynomial(crc::Polynomial::L16(0x1021)).freeze();
    
    let data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
    crc.feed(&data);
    
    let result = crc.result();
    assert!(result == 0x78cb);
  12. Constrain the RCC peripheral

    master

    To use the Reset and Clock Control (RCC) abstractions, you must first constrain the raw RCC peripheral. This converts the raw register access into a structured Rcc object that manages bus access and clock configuration.

    Use the constrain() method provided by the RccExt trait on the RCC peripheral.

    let rcc = dp.RCC.constrain();