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}