Skip to main content

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 as frontend; // <-- 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 = imxrt_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 = frontend::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(
257    missing_docs,
258    unsafe_op_in_unsafe_fn,
259    clippy::undocumented_unsafe_blocks,
260    clippy::missing_safety_doc
261)]
262
263#[cfg(feature = "defmt")]
264pub mod defmt;
265#[cfg(feature = "log")]
266pub mod log;
267
268#[cfg(feature = "lpuart")]
269mod lpuart;
270
271#[cfg(feature = "usbd")]
272mod usbd;
273#[cfg(feature = "usbd")]
274pub use usbd::{UsbdConfig, UsbdConfigBuilder};
275
276/// Interrupt configuration.
277///
278/// If interrupts are enabled, you're responsible for registering the ISR
279/// associated with the peripheral. See the crate-level documentation to
280/// understand how this affects each logging backend.
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282pub enum Interrupts {
283    /// Peripheral interrupts are disabled.
284    Disabled,
285    /// Peripheral interrupts are enabled.
286    Enabled,
287}
288
289fn try_write_producer<const N: usize>(
290    buffer: &[u8],
291    producer: &mut bbqueue::Producer<'_, N>,
292) -> Result<(), bbqueue::Error> {
293    fn write_grant<'a, const N: usize>(
294        bytes: &'a [u8],
295        prod: &mut bbqueue::Producer<'_, N>,
296    ) -> Result<&'a [u8], bbqueue::Error> {
297        let mut grant = prod.grant_max_remaining(bytes.len())?;
298        let grant_len = grant.len();
299        grant.copy_from_slice(&bytes[..grant_len]);
300        grant.commit(grant_len);
301        Ok(&bytes[grant_len..])
302    }
303
304    // Either (1) write all of s into the buffer, (2) fill up the back of the buffer,
305    // or (3) fill up as much as you can until you hit old data.
306    let buffer = write_grant::<N>(buffer, producer)?;
307
308    // Non-empty for (2) and (3).
309    if !buffer.is_empty() {
310        // This could either fail, or the grant could be smaller than the (remaining)
311        // string. In the latter case, we drop data.
312        write_grant::<N>(buffer, producer)?;
313    }
314
315    Ok(())
316}
317
318/// An error indicating the logger is already set.
319///
320/// This could happen because
321///
322/// - you've already initialized a logger provided by this package
323/// - you're using the `log` package, and something else has registered
324///   the dynamic logger
325pub struct AlreadySetError<R> {
326    /// Holds the peripherals and other state provided to the
327    /// initialization routine.
328    pub resources: R,
329}
330
331impl<R> AlreadySetError<R> {
332    fn new(resources: R) -> Self {
333        Self { resources }
334    }
335}
336
337impl<R> core::fmt::Debug for AlreadySetError<R> {
338    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
339        f.write_str("logger already set")
340    }
341}
342
343include!(concat!(env!("OUT_DIR"), "/config.rs"));
344use config::BUFFER_SIZE;
345
346static BUFFER: bbqueue::BBBuffer<BUFFER_SIZE> = bbqueue::BBBuffer::new();
347type Consumer = bbqueue::Consumer<'static, { crate::BUFFER_SIZE }>;
348
349/// The poller drives the logging process.
350///
351/// You're expected to periodically call [`poll()`](Self::poll) to asynchronously
352/// move log messages from memory to your peripheral. `poll()` never
353/// blocks on I/O or data.
354///
355/// `Poller` logically owns static, mutable state that's allocated behind
356/// this package's API. The specific state depends on the selected backend.
357/// Since it manages static, mutable state, there can only be one instance
358/// of a `Poller` in any program.
359pub struct Poller {
360    inner: Inner,
361}
362
363// Safety: it's OK to move this across execution contexts.
364// Poller is !Send, so the same object cannot be safely accessed
365// across these execution contexts.
366unsafe impl Send for Poller {}
367
368impl Poller {
369    fn new<B: Into<Inner>>(backend: B) -> Self {
370        Poller {
371            inner: backend.into(),
372        }
373    }
374
375    /// Drive the logging process.
376    ///
377    /// If log messages are available, and if there is no active transfer,
378    /// `poll()` initiates a new transfer. It also manages the state of the
379    /// backend peripheral. There's no guarantee on how many bytes are sent
380    /// in each transfer.
381    #[inline]
382    pub fn poll(&mut self) {
383        self.inner.poll();
384    }
385}
386
387enum Inner {
388    #[cfg(feature = "lpuart")]
389    Lpuart(&'static mut lpuart::Backend),
390    #[cfg(feature = "usbd")]
391    Usbd(&'static mut usbd::Backend),
392}
393
394#[cfg(feature = "lpuart")]
395impl From<&'static mut lpuart::Backend> for Inner {
396    fn from(backend: &'static mut lpuart::Backend) -> Self {
397        Inner::Lpuart(backend)
398    }
399}
400
401#[cfg(feature = "usbd")]
402impl From<&'static mut usbd::Backend> for Inner {
403    fn from(backend: &'static mut usbd::Backend) -> Self {
404        Inner::Usbd(backend)
405    }
406}
407
408impl Inner {
409    fn poll(&mut self) {
410        match self {
411            #[cfg(feature = "lpuart")]
412            Self::Lpuart(backend) => backend.poll(),
413            #[cfg(feature = "usbd")]
414            Self::Usbd(backend) => backend.poll(),
415        }
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::try_write_producer;
422    use bbqueue::BBBuffer;
423
424    #[test]
425    fn write_producer_simple() {
426        let bb = BBBuffer::<4>::new();
427        let (mut prod, mut cons) = bb.try_split().unwrap();
428        try_write_producer(&[1, 2, 3], &mut prod).unwrap();
429        assert_eq!(cons.read().unwrap().buf(), &[1, 2, 3]);
430    }
431
432    #[test]
433    fn write_producer_lost_data() {
434        let bb = BBBuffer::<5>::new();
435        let (mut prod, mut cons) = bb.try_split().unwrap();
436        prod.grant_exact(2).unwrap().commit(2);
437        cons.read().unwrap().release(1);
438        assert!(try_write_producer(&[1, 2, 3, 4], &mut prod).is_err());
439        assert_eq!(cons.read().unwrap().buf(), &[0, 1, 2, 3]);
440    }
441
442    #[test]
443    fn write_producer_wrap_around() {
444        let bb = BBBuffer::<5>::new();
445        let (mut prod, mut cons) = bb.try_split().unwrap();
446        prod.grant_exact(3).unwrap().commit(3);
447        cons.read().unwrap().release(2);
448        try_write_producer(&[1, 2, 3, 4], &mut prod).unwrap();
449        let grant = cons.split_read().unwrap();
450        let (bck, fnt) = grant.bufs();
451        assert_eq!(bck, &[0, 1, 2]);
452        // Looks like BBBuffer uses an extra element to differentiate start / end points,
453        // so we lost data without any error. That's OK.
454        assert_eq!(fnt, &[3]);
455    }
456
457    #[test]
458    fn default_configs() {
459        assert_eq!(crate::config::USB_BULK_MPS, 512);
460        assert_eq!(crate::config::USB_SPEED, imxrt_usbd::Speed::High);
461        assert_eq!(crate::config::BUFFER_SIZE, 1024);
462    }
463}