[][src]Module imxrt_uart_log::dma

Asynchronous logging using DMA

This logger is slightly more complicated to set up. To log data,

  1. Configure a UART peripheral with baud rates, parities, inversions, etc.
  2. Select a DMA channel. Take note of the DMA channel number.
  3. Call poll() in either your DMA channel's interrupt handler, or throughout your event loop.
  • If you're calling poll() in a DMA channel's interrupt handler, unmask the interrupt via an unsafe call to cortex_m::interrupt::unmask(), and also enable interrupt on completion.
  1. Call init with all of
  • a UART transfer half,
  • a DMA channel
  • a logging configuration
  1. Use the macros from the log crate to write data.

Optionally, you may specify your own DMA buffer. See the BYOB feature to learn about user-supplied DMA buffers.

Use-cases

  • Infrequently logging smaller messages with minimal delay
  • Logging where the responsiveness of the logging implementation may not be critical

Implementation

The implementation minimizes the time it takes to call and return from a log macro. The caller incurs the time it takes to perform any string interpolation and a copy into a circular buffer. Both string interpolation and copying happen in a critical section. Then,

  1. if there is no active DMA transfer, the logger schedules a DMA transfer, and returns.
  2. if there is a completed DMA transfer, the logger finalizes the transfer. The logger appends the new log message to the enqueued contents in the circular buffer. The logger schedules a DMA transfer, and returns.
  3. if there is an active DMA transfer, the logger enqueues the new message in the circular buffer, and returns immediately.

The implementation schedules new transfers when the current transfer is complete, there are log messages in the circular buff, and you either

  • call poll(), or
  • log another message (see 2. above)

By default, the implementation relies on a 2KiB statically-allocated circular buffer. If you saturate the buffer before the next transfer is scheduled, the data that cannot be copied into the buffer will be dropped. Either keep messages small, or keep messages infrequent, to avoid circular buffer saturation.

Tips

Interrupt priorities

To improve logging responsiveness, consider changing your DMA channel's interrupt priority. This may be helpful when frequently logging from interrupts. If your DMA channel's interrupt priority is greater than your other interrupt priorities, poll() is more likely to be called, which will mean more data sent over serial.

Flush the async logger

To guarantee that a transfer completes, use poll() while waiting for an Idle return:

use imxrt_uart_log::dma::{poll, Poll};

log::error!("Send message and wait for the transfer to finish");
while Poll::Idle != poll() {}

Note that this will flush all contents from the async logger, so you will also wait for any previously-scheduled transfers to complete.

Responsiveness

If you do not care about logging responsiveness, or if you're OK with "bursty" logging, you do not need to call poll(). The logging implementation will finalize and schedule transfers when it detects that a previous transfer is complete. For example,

log::info!("what's");   // Immediately schedules DMA transfer
log::info!("the");      // Enqueues message, but not transferred
// Some time later, when "what's" transfer completes...
log::info!("weather?"); // Sends both "the" and "weather?"

will immediately log "what's". The second message "the" is written to the circular buffer, and it will be scheduled to transfer when you write "weather?".

If the time between "the" and "weather?" is large, and you'd like to receive "the" before "weather" is written, add one or or more poll() calls in between the two calls. The most responsive async logger will call poll() in the DMA channel's interrupt handler, which will run as soon as a transfer completes.

Example

In this example, we select DMA channel 7 to use for logging transfers. We implement the DMA7_DMA23 interrupt to service DMA transfers. We need to unmask the DMA7_DMA23 interrupt for proper operation. See the comments for more information.

use imxrt_hal::ral::interrupt;

// Assume that DMA7_DMA23 is registered in the vector table
fn DMA7_DMA23() {
    imxrt_uart_log::dma::poll();
}

let mut peripherals = imxrt_hal::Peripherals::take().unwrap();

let (_, ipg_hz) = peripherals.ccm.pll1.set_arm_clock(
    imxrt_hal::ccm::PLL1::ARM_HZ,
    &mut peripherals.ccm.handle,
    &mut peripherals.dcdc,
);

let mut dma_channels = peripherals.dma.clock(&mut peripherals.ccm.handle);
let mut channel = dma_channels[7].take().unwrap();
// Enable interrupt generation when the DMA transfer completes
channel.set_interrupt_on_completion(true);
// Don't forget to unmask the interrupt!
unsafe {
    cortex_m::peripheral::NVIC::unmask(interrupt::DMA7_DMA23);
}

let uarts = peripherals.uart.clock(
    &mut peripherals.ccm.handle,
    imxrt_hal::ccm::uart::ClockSelect::OSC,
    imxrt_hal::ccm::uart::PrescalarSelect::DIVIDE_1,
);
let uart = uarts
    .uart2
    .init(
        peripherals.iomuxc.ad_b1.p02,
        peripherals.iomuxc.ad_b1.p03,
        115_200,
    )
    .unwrap();

let (tx, _) = uart.split();
imxrt_uart_log::dma::init(tx, channel, Default::default()).unwrap();

// At this point, you may use log macros to write data.
log::info!("Hello world!");

BYOB

"Bring Your Own Buffer" (BYOB) is an optional, compile-time feature that affects the DMA logging API. If you enable the "byob" feature, you indicate that you will statically allocate a circular DMA buffer, rather than relying on the default DMA buffer. You may supply the buffer to init().

BYOB is useful if you want to control either the size or placement of the DMA buffer. You're responsible for following the alignment requirements. See the i.MX RT HAL's DMA documentation for more details on DMA buffers.

Enums

Poll

A poll()ing result

Functions

init

Initialize the DMA-based logger with a UART transfer half and a DMA channel

poll

Drives DMA-based logging