imxrt_log/
lib.rs

1//! Logging extensions for i.MX RT processors.
2//!
3//! `imxrt-log` supports two logging frontends:
4//!
5//! - [`defmt`][defmt-docs] for efficient logging.
6//! - [`log`][log-docs] for text-based logging.
7//!
8//! See the [`defmt`] and [`log`] modules for more information.
9//!
10//! `imxrt-log` builds upon the `imxrt-hal` hardware abstraction layer (HAL)
11//! and provides two peripheral backends:
12//!
13//! - LPUART with DMA
14//! - USB serial (CDC) device
15//!
16//! Mix and match these frontends and backends to integrate logging into your
17//! i.MX RT processor. To understand the differences of each frontend, see
18//! the package documentation. Read on to learn about building this package,
19//! and to understand the differences in each backend.
20//!
21//! # Building
22//!
23//! Given its dependency on [`imxrt-hal`][hal-docs], this package has the same build
24//! requirements as `imxrt-hal`. To learn how to build this package, consult the
25//! HAL documentation. Essentially, if you can build the HAL, you can build this
26//! package.
27//!
28//! This package uses [`critical-section`](https://crates.io/crates/critical-section)
29//! to ensure safe concurrent access to the log producer. In order for this
30//! to build, you must select a correct critical section implementation for your
31//! system. See the `critical-section` documentation for more information.
32//!
33//! # Design
34//!
35//! Logging frontends place log frames in a circular buffer. Compile- and run-time
36//! filters prevent log message formatting and copies. For more frontend
37//! design details, see the documentation of each frontend module.
38//!
39//! Backends read from this circular buffer and asynchronously transfer data out
40//! of memory. Backends may buffer data as part of their implementation.
41//!
42//! The circular buffer is the limiting resource. Once you initialize a logger
43//! with a frontend-backend combination, you cannot initialize any other loggers.
44//!
45//! # Backend usage
46//!
47//! The LPUART and USB backends provide a consistent interface to drive logging.
48//! After initializing a front and backend pair, you receive a [`Poller`] object.
49//! In order to move log messages, you must occasionally call `poll()` on the poller
50//! object. Each `poll()` call either does nothing, or drives the asynchronous
51//! transfer of log messages from your peripheral.
52//!
53//! The API allows you to enable or disable interrupts that fire when a transfer
54//! completes. Depending on the backend, the interrupt may periodically trigger.
55//! If the interrupt periodically triggers, you can use the interrupt to occasionally
56//! call `poll()`.
57//!
58//! The backends have some behavioral and performance differences. They're also
59//! initialized differently. The next section describes these differences.
60//!
61//! ## LPUART with DMA
62//!
63//! The LPUART with DMA implementation transports log messages over LPUART using
64//! DMA transfers. In summary,
65//!
66//! - Initialize your LPUART before initializing the logger.
67//! - If you enable interrupts, define your interrupt handlers.
68//! - Bring your own timer to call `poll()`.
69//! - It uses less memory than USB.
70//!
71//! _Initialization_. The logging initialiation routine requires an LPUART
72//! object from `imxrt-hal`. Configure your `Lpuart` object with baud rates,
73//! parity bits, etc. before supplying it to the logging initialization routine.
74//!
75//! The initialization routine also requires a DMA channel. Any DMA channel will
76//! do. The implementation fully configures the DMA channel, so there is no need for
77//! you to configure the channel.
78//!
79//! _Interrupts_. If you enable interrupts (see [`Interrupts`]), the DMA channel
80//! asserts its interrupt when each transfer completes. You must call `poll()`
81//! to clear the interrupt. The implementation does not touch LPUART interrupts.
82//!
83//! _Timers_. The interrupts enabled by the LPUART backend cannot periodically
84//! trigger. Therefore, you are responsible for periodically calling `poll()`.
85//! Consider using a PIT or GPT timer from `imxrt-hal` to help with this, or
86//! consider calling `poll()` in a software loop.
87//!
88//! _Buffer management_. The implementation performs DMA transfers directly out
89//! of the log message buffer. This means that there is no intermediate buffer
90//! for log messages. The implementation frees the log messages from the circular
91//! buffer once the transfer completes.
92//!
93//! ## USBD
94//!
95//! The USB device implementation transports log messages over USB by presenting
96//! a serial (USB CDC) class to a USB host. In summary,
97//!
98//! - Simply provide USB register blocks to the logger initialization routine.
99//! - If you enable interrupts, define your interrupt handles.
100//! - You might not need your own timer.
101//! - It uses more memory than LPUART.
102//!
103//! _Initialization_. The logging initialization routine handles all peripheral
104//! configuration. You simply provide the USB register block instances; `imxrt-hal`
105//! can help with this.
106//!
107//! By default, the initialization routine configures a high-speed USB device with
108//! a 512 byte bulk endpoint max packet size. You can change these settings with
109//! build-time environment variables, discussed later.
110//!
111//! _Interrupts_. If you enable interrupts (see [`Interrupts`]), the USB device
112//! controller asserts its interrupt when each transfer completes. It also enables
113//! a USB-managed timer to periodically trigger the interrupt. You must call `poll()`
114//! to clear these interrupt conditions.
115//!
116//! _Timers_. If you enable interrupts, the associated USB interrupt periodically
117//! fires. You can use this to periodically call `poll()` without using any other
118//! timer or software loop.
119//!
120//! The timer has a default interval. You can configure this interval through each
121//! logger initialization routine.
122//!
123//! If you do not enable interrupts, you're responsible for periodically calling
124//! `poll()`. See the LPUART _timers_ discussion for recommendations.
125//!
126//! _Buffer management_. The implementation copies data out of the circular buffer
127//! and places it in an intermediate transfer buffer. Once this copy completes, the
128//! implementation frees the log frames from the circular buffer, and starts the
129//! USB transfer from this intermediate buffer. The requirement for the intermediate
130//! buffer is a USB driver implementation detail that increases this backend's memory
131//! requirements.
132//!
133//! # Examples
134//!
135//! It's easiest to use the USB backend because it has a built-in timer, and the
136//! implementation handles all peripheral initialization. The example below shows
137//! an interrupt-driven USB logger. It uses `imxrt-hal` APIs to prepare the logger.
138//!
139//! ```no_run
140//! use imxrt_log::defmt; // <-- Change 'defmt' to 'log' to change the frontend.
141//! use imxrt_hal as hal;
142//! use imxrt_ral as ral;
143//!
144//! use ral::interrupt;
145//! #[cortex_m_rt::interrupt]
146//! fn USB_OTG1() {
147//!     static mut POLLER: Option<imxrt_log::Poller> = None;
148//!     if let Some(poller) = POLLER.as_mut() {
149//!         poller.poll();
150//!     } else {
151//!         let poller = initialize_logger().unwrap();
152//!         *POLLER = Some(poller);
153//!         // Since we enabled interrupts, this interrupt
154//!         // handler will be called for USB traffic and timer
155//!         // events. These are handled by poll().
156//!     }
157//! }
158//!
159//! /// Initialize a USB logger.
160//! ///
161//! /// Returns `None` if any USB peripheral instance is taken,
162//! /// or if initialization fails.
163//! fn initialize_logger() -> Option<imxrt_log::Poller> {
164//!     let usb_instances = hal::usbd::Instances {
165//!         usb: unsafe { ral::usb::USB1::instance() },
166//!         usbnc: unsafe { ral::usbnc::USBNC1::instance() },
167//!         usbphy: unsafe { ral::usbphy::USBPHY1::instance() },
168//!     };
169//!     // Initialize the logger, and ensure that it triggers interrupts.
170//!     let poller = defmt::usbd(usb_instances, imxrt_log::Interrupts::Enabled).ok()?;
171//!     Some(poller)
172//! }
173//!
174//! // Elsewhere in your code, configure USB clocks. Then, pend the USB_OTG1()
175//! // interrupt so that it fires and initializes the logger.
176//! # || -> Option<()> {
177//! let mut ccm = unsafe { ral::ccm::CCM::instance() };
178//! let mut ccm_analog = unsafe { ral::ccm_analog::CCM_ANALOG::instance() };
179//! hal::ccm::analog::pll3::restart(&mut ccm_analog);
180//! hal::ccm::clock_gate::usb().set(&mut ccm, hal::ccm::clock_gate::ON);
181//!
182//! cortex_m::peripheral::NVIC::pend(interrupt::USB_OTG1);
183//! // Safety: interrupt handler is self contained and safe to unmask.
184//! unsafe { cortex_m::peripheral::NVIC::unmask(interrupt::USB_OTG1) };
185//! # Some(()) }().unwrap();
186//!
187//! // After the USB device enumerates and configures, you're ready for
188//! // logging.
189//! ::defmt::info!("Hello world!");
190//! ```
191//!
192//! For an advanced example that uses RTIC, see the `rtic_logging` example
193//! maintained in the `imxrt-hal` repository. This example lets you easily explore
194//! all frontend-backend combinations, and it works on various i.MX RT development
195//! boards.
196//!
197//! # Package configurations
198//!
199//! You can configure this package at compile time.
200//!
201//! - Binary configurations use feature flags.
202//! - Variable configurations use environment variables set during compilation.
203//!
204//! The table below describes the package feature flags. Default features make it
205//! easy for you to use all package features. To reduce dependencies, disable this
206//! package's default features, then selectively enable frontends and backends.
207//!
208//! | Feature flag | Description                         | Enabled by default? |
209//! | ------------ | ----------------------------------- | ------------------- |
210//! | `defmt`      | Enable the `defmt` logging frontend |        Yes          |
211//! | `log`        | Enable the `log` logging frontend   |        Yes          |
212//! | `lpuart`     | Enable the LPUART backend           |        Yes          |
213//! | `usbd`       | Enable the USB device backend       |        Yes          |
214//!
215//! This package isn't particularly interesting without a frontend-backend combination,
216//! so this configuration is not supported. Any features not listed above are considered
217//! an implementation detail and may change without notice.
218//!
219//! Environment variables provide additional configuration hooks. The table below
220//! describes the supported configuration variables and their effects on the build.
221//!
222//! | Environment variable           | Description                                               | Default value | Accepted values           |
223//! | ------------------------------ | --------------------------------------------------------- | ------------- | ------------------------- |
224//! | `IMXRT_LOG_USB_BULK_MPS`       | Bulk endpoint max packet size, in bytes.                  |     512       | One of 8, 16, 32, 64, 512 |
225//! | `IMXRT_LOG_USB_SPEED`          | Specify a high (USB2) or full (USB 1.1) speed USB device. |    HIGH       | Either `HIGH` or `FULL`   |
226//! | `IMXRT_LOG_BUFFER_SIZE`        | Specify the log message buffer size, in bytes.            |    1024       | An integer power of two   |
227//!
228//! Note:
229//!
230//! - `IMXRT_LOG_USB_*` are always permitted. If `usbd` is disabled, then `IMXRT_LOG_USB_*`
231//!    configurations do nothing.
232//! - If `IMXRT_LOG_USB_SPEED=FULL`, then `IMXRT_LOG_USB_BULK_MPS` cannot be 512. On the other hand,
233//!   if `IMXRT_LOG_USB_SPEED=HIGH`, then `IMXRT_LOG_USB_BULK_MPS` must be 512.
234//! - Both `IMXRT_LOG_USB_BULK_MPS` and `IMXRT_LOG_BUFFER_SIZE` affect internally-managed buffer
235//!   sizes. If space is tight, reduces these numbers to reclaim memory.
236//!
237//! # Limitations
238//!
239//! Although it uses `critical-section`, this logging package may not be designed for immediate
240//! use in a multi-core system, like the i.MX RT 1160 and 1170 MCUs. Notably, there's no critical
241//! section implementation for these processors that would ensure safe, shared access to the log
242//! producer across the cores. Furthermore, its not yet clear how to build embedded Rust applications
243//! for these systems.
244//!
245//! Despite these limitations, it may be possible to use this package on multi-core MCUs, but you need
246//! to treat them as two single-core MCUs. Specifically, you would need to build two binaries -- one for
247//! each core, each having separate memory regions for data -- and each core would need to use its own,
248//! distinct peripheral for transport. Then, select a single-core `critical-section` implementation,
249//! like the one provided by `cortex-m`.
250//!
251//! [defmt-docs]: https://defmt.ferrous-systems.com
252//! [hal-docs]: https://docs.rs/imxrt-hal
253//! [log-docs]: https://docs.rs/log/0.4/log/
254
255#![no_std]
256#![warn(missing_docs, unsafe_op_in_unsafe_fn)]
257
258#[cfg(feature = "defmt")]
259pub mod defmt;
260#[cfg(feature = "log")]
261pub mod log;
262
263#[cfg(feature = "lpuart")]
264mod lpuart;
265
266#[cfg(feature = "usbd")]
267mod usbd;
268#[cfg(feature = "usbd")]
269pub use usbd::{UsbdConfig, UsbdConfigBuilder};
270
271/// Interrupt configuration.
272///
273/// If interrupts are enabled, you're responsible for registering the ISR
274/// associated with the peripheral. See the crate-level documentation to
275/// understand how this affects each logging backend.
276#[derive(Debug, Clone, Copy, PartialEq, Eq)]
277pub enum Interrupts {
278    /// Peripheral interrupts are disabled.
279    Disabled,
280    /// Peripheral interrupts are enabled.
281    Enabled,
282}
283
284fn try_write_producer<const N: usize>(
285    buffer: &[u8],
286    producer: &mut bbqueue::Producer<'_, N>,
287) -> Result<(), bbqueue::Error> {
288    fn write_grant<'a, const N: usize>(
289        bytes: &'a [u8],
290        prod: &mut bbqueue::Producer<'_, N>,
291    ) -> Result<&'a [u8], bbqueue::Error> {
292        let mut grant = prod.grant_max_remaining(bytes.len())?;
293        let grant_len = grant.len();
294        grant.copy_from_slice(&bytes[..grant_len]);
295        grant.commit(grant_len);
296        Ok(&bytes[grant_len..])
297    }
298
299    // Either (1) write all of s into the buffer, (2) fill up the back of the buffer,
300    // or (3) fill up as much as you can until you hit old data.
301    let buffer = write_grant::<N>(buffer, producer)?;
302
303    // Non-empty for (2) and (3).
304    if !buffer.is_empty() {
305        // This could either fail, or the grant could be smaller than the (remaining)
306        // string. In the latter case, we drop data.
307        write_grant::<N>(buffer, producer)?;
308    }
309
310    Ok(())
311}
312
313/// An error indicating the logger is already set.
314///
315/// This could happen because
316///
317/// - you've already initialized a logger provided by this package
318/// - you're using the `log` package, and something else has registered
319///   the dynamic logger
320pub struct AlreadySetError<R> {
321    /// Holds the peripherals and other state provided to the
322    /// initialization routine.
323    pub resources: R,
324}
325
326impl<R> AlreadySetError<R> {
327    fn new(resources: R) -> Self {
328        Self { resources }
329    }
330}
331
332impl<R> core::fmt::Debug for AlreadySetError<R> {
333    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
334        f.write_str("logger already set")
335    }
336}
337
338include!(concat!(env!("OUT_DIR"), "/config.rs"));
339use config::BUFFER_SIZE;
340
341static BUFFER: bbqueue::BBBuffer<BUFFER_SIZE> = bbqueue::BBBuffer::new();
342type Consumer = bbqueue::Consumer<'static, { crate::BUFFER_SIZE }>;
343
344/// The poller drives the logging process.
345///
346/// You're expected to periodically call [`poll()`](Self::poll) to asynchronously
347/// move log messages from memory to your peripheral. `poll()` never
348/// blocks on I/O or data.
349///
350/// `Poller` logically owns static, mutable state that's allocated behind
351/// this package's API. The specific state depends on the selected backend.
352/// Since it manages static, mutable state, there can only be one instance
353/// of a `Poller` in any program.
354pub struct Poller {
355    _marker: core::marker::PhantomData<*mut ()>,
356    vtable: PollerVTable,
357}
358
359// Safety: it's OK to move this across execution contexts.
360// Poller is !Send, so the same object cannot be safely accessed
361// across these execution contexts.
362unsafe impl Send for Poller {}
363
364impl Poller {
365    fn new(vtable: PollerVTable) -> Self {
366        Poller {
367            _marker: core::marker::PhantomData,
368            vtable,
369        }
370    }
371
372    /// Drive the logging process.
373    ///
374    /// If log messages are available, and if there is no active transfer,
375    /// `poll()` initiates a new transfer. It also manages the state of the
376    /// backend peripheral. There's no guarantee on how many bytes are sent
377    /// in each transfer.
378    #[inline]
379    pub fn poll(&mut self) {
380        unsafe { (self.vtable.poll)() };
381    }
382}
383
384struct PollerVTable {
385    poll: unsafe fn(),
386}
387
388#[cfg(test)]
389mod tests {
390    use super::try_write_producer;
391    use bbqueue::BBBuffer;
392
393    #[test]
394    fn write_producer_simple() {
395        let bb = BBBuffer::<4>::new();
396        let (mut prod, mut cons) = bb.try_split().unwrap();
397        try_write_producer(&[1, 2, 3], &mut prod).unwrap();
398        assert_eq!(cons.read().unwrap().buf(), &[1, 2, 3]);
399    }
400
401    #[test]
402    fn write_producer_lost_data() {
403        let bb = BBBuffer::<5>::new();
404        let (mut prod, mut cons) = bb.try_split().unwrap();
405        prod.grant_exact(2).unwrap().commit(2);
406        cons.read().unwrap().release(1);
407        assert!(try_write_producer(&[1, 2, 3, 4], &mut prod).is_err());
408        assert_eq!(cons.read().unwrap().buf(), &[0, 1, 2, 3]);
409    }
410
411    #[test]
412    fn write_producer_wrap_around() {
413        let bb = BBBuffer::<5>::new();
414        let (mut prod, mut cons) = bb.try_split().unwrap();
415        prod.grant_exact(3).unwrap().commit(3);
416        cons.read().unwrap().release(2);
417        try_write_producer(&[1, 2, 3, 4], &mut prod).unwrap();
418        let grant = cons.split_read().unwrap();
419        let (bck, fnt) = grant.bufs();
420        assert_eq!(bck, &[0, 1, 2]);
421        // Looks like BBBuffer uses an extra element to differentiate start / end points,
422        // so we lost data without any error. That's OK.
423        assert_eq!(fnt, &[3]);
424    }
425
426    #[test]
427    fn default_configs() {
428        assert_eq!(crate::config::USB_BULK_MPS, 512);
429        assert_eq!(crate::config::USB_SPEED, imxrt_usbd::Speed::High);
430        assert_eq!(crate::config::BUFFER_SIZE, 1024);
431    }
432}