panic_persist/
lib.rs

1//! # `panic-persist`
2//!
3//! Writes panic messages to a section of RAM
4//!
5//! This crate contains an implementation of `panic_fmt` that logs panic messages to a region of
6//! RAM defined by the user, so that these messages can be retrieved on next boot, and handled
7//! outside of panic context, by sending to a logging interface, writing to flash, etc.
8//!
9//! After logging the message to RAM, the device will be soft-reset automatically.
10//!
11//! Unlike other methods this allows to discover the panic reason post-mortem using normal program
12//! control flow.
13//!
14//! Currently this crate was only tested on ARM Cortex-M architecture but should be easily portable
15//! to other platforms as required.
16//!
17//! ## Usage
18//!
19//! ### Add a section to your linker script
20//!
21//! You will need to reserve a section of RAM to be used to persist messages. This section must be
22//! large enough to hold the 8 byte header, as well as any panic messages you would like to persist.
23//! If there is not suitable space in the section, the panic message will be truncated.
24//!
25//! This section should be outside of any other sections, to prevent program initialization from
26//! zeroing or otherwise modifying these sections on boot.
27//!
28//! `memory.x` file before modification:
29//!
30//! ``` ignore
31//! MEMORY
32//! {
33//!   /* NOTE K = KiBi = 1024 bytes */
34//!   FLASH : ORIGIN  = 0x00000000, LENGTH = 512K
35//!   RAM : ORIGIN    = 0x20000000, LENGTH = 64K
36//! }
37//! ```
38//!
39//! `memory.x` file after modification to hold a 1K region:
40//!
41//! ``` ignore
42//! MEMORY
43//! {
44//!   /* NOTE K = KiBi = 1024 bytes */
45//!   FLASH : ORIGIN  = 0x00000000, LENGTH = 512K
46//!   RAM : ORIGIN    = 0x20000000, LENGTH = 63K
47//!   PANDUMP: ORIGIN = 0x2000FC00, LENGTH = 1K
48//! }
49//!
50//! _panic_dump_start = ORIGIN(PANDUMP);
51//! _panic_dump_end   = ORIGIN(PANDUMP) + LENGTH(PANDUMP);
52//! ```
53//!
54//!
55//! ### Program Usage Example
56//!
57//! ``` ignore
58//! #![no_std]
59//!
60//! use panic_persist as _;
61//!
62//! #[entry]
63//! fn main() -> ! {
64//!     // Normal board setup...
65//!
66//!     // Check if there was a panic message, if so, send to UART
67//!     if let Some(msg) = get_panic_message_bytes() {
68//!         board.uart.write(msg);
69//!     }
70//!
71//!     // ...
72//! }
73//! ```
74//!
75//! ## Features
76//!
77//! There are a few optional features, `utf8` and `custom-panic-handler`.
78//!
79//! ### utf8
80//!
81//! This allows the panic message to be returned
82//! as a `&str` rather than `&[u8]`, for easier printing. As this requires the ability
83//! to validate the UTF-8 string (to ensure it wasn't truncated mid-character), it may
84//! increase code size usage, and is by default off.
85//!
86//! ### custom-panic-handler
87//!
88//! This disables the panic handler from this library so that any user can implement their own.
89//! To persist panic messages, the function `report_panic_info` is made available;
90//!
91//! ```rust
92//! // My custom panic implementation
93//! #[panic_handler]
94//! fn panic(info: &PanicInfo) -> ! {
95//!     // ...
96//!     panic_persist::report_panic_info(info);
97//!     // ...
98//! }
99//! ```
100//!
101//! ### min-panic
102//!
103//! This prints a smaller, line-number-only message, in order to reduce space needed when persisting panics, at a loss of some context.
104
105#![allow(clippy::empty_loop)]
106#![deny(missing_docs)]
107#![deny(warnings)]
108#![no_std]
109
110use core::cmp::min;
111use core::fmt::Write;
112use core::mem::size_of;
113use core::panic::PanicInfo;
114
115struct Ram {
116    offset: usize,
117}
118
119/// Internal Write implementation to output the formatted panic string into RAM
120impl core::fmt::Write for Ram {
121    fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> {
122        // Obtain panic region start and end from linker symbol _panic_dump_start and _panic_dump_end
123        extern "C" {
124            static mut _panic_dump_start: u8;
125            static mut _panic_dump_end: u8;
126        }
127
128        // Get the data about the string that is being written now
129        let data = s.as_bytes();
130        let len = data.len();
131
132        // Obtain info about the panic dump region
133        let start_ptr = unsafe { &mut _panic_dump_start as *mut u8 };
134        let end_ptr = unsafe { &mut _panic_dump_end as *mut u8 };
135        let max_len = end_ptr as usize - start_ptr as usize;
136        let max_len_str = max_len - size_of::<usize>() - size_of::<usize>();
137
138        // If we have written the full length of the region, we can't write any
139        // more. This could happen with multiple writes with this implementation
140        if self.offset >= max_len_str {
141            return Ok(());
142        }
143
144        // We should write the size of the string, or the amount of space
145        // we have remaining, whichever is less
146        let str_len = min(max_len_str - self.offset, len);
147
148        unsafe {
149            // Write the magic word for later detection
150            start_ptr.cast::<usize>().write_unaligned(0x0FACADE0);
151
152            // For now, skip writing the length...
153
154            // Write the string to RAM
155            core::ptr::copy(
156                data.as_ptr() as *mut u8,
157                start_ptr.offset(8).offset(self.offset as isize),
158                str_len,
159            );
160
161            // Increment the offset so later writes will be appended
162            self.offset += str_len;
163
164            // ... and now write the current offset (or total size) to the size location
165            start_ptr
166                .offset(4)
167                .cast::<usize>()
168                .write_unaligned(self.offset);
169        };
170
171        Ok(())
172    }
173}
174
175/// Get the panic message from the last boot, if any.
176/// This method may possibly not return valid UTF-8 if the message
177/// was truncated before the end of a full UTF-8 character. Care must
178/// be taken before treating this as a proper &str.
179///
180/// If a message existed, this function will only return the value once
181/// (subsequent calls will return None)
182pub fn get_panic_message_bytes() -> Option<&'static [u8]> {
183    // Obtain panic region start and end from linker symbol _panic_dump_start and _panic_dump_end
184    extern "C" {
185        static mut _panic_dump_start: u8;
186        static mut _panic_dump_end: u8;
187    }
188
189    let start_ptr = unsafe { &mut _panic_dump_start as *mut u8 };
190
191    if 0x0FACADE0 != unsafe { core::ptr::read_unaligned(start_ptr.cast::<usize>()) } {
192        return None;
193    }
194
195    // Clear the magic word to prevent this message from "sticking"
196    // across multiple boots
197    unsafe {
198        start_ptr.cast::<usize>().write_unaligned(0x00000000);
199    }
200
201    // Obtain info about the panic dump region
202    let end_ptr = unsafe { &mut _panic_dump_end as *mut u8 };
203    let max_len = end_ptr as usize - start_ptr as usize;
204    let max_len_str = max_len - size_of::<usize>() - size_of::<usize>();
205
206    let len = unsafe { core::ptr::read_unaligned(start_ptr.offset(4).cast::<usize>()) };
207
208    if len > max_len_str {
209        return None;
210    }
211
212    // TODO: This is prooooooooobably undefined behavior
213    let byte_slice = unsafe { core::slice::from_raw_parts(start_ptr.offset(8), len) };
214
215    Some(byte_slice)
216}
217
218/// Get the panic message from the last boot, if any. If any invalid
219/// UTF-8 characters occur, the message will be truncated before the
220/// first error.
221///
222/// If a message existed, this function will only return the value once
223/// (subsequent calls will return None)
224#[cfg(feature = "utf8")]
225pub fn get_panic_message_utf8() -> Option<&'static str> {
226    let bytes = get_panic_message_bytes()?;
227
228    use core::str::from_utf8;
229
230    match from_utf8(bytes) {
231        Ok(stir) => Some(stir),
232        Err(utf_err) => {
233            match from_utf8(&bytes[..utf_err.valid_up_to()]) {
234                Ok(stir) => Some(stir),
235                Err(_) => {
236                    // This shouldn't be possible...
237                    None
238                }
239            }
240        }
241    }
242}
243
244/// Report the panic so the message is persisted.
245///
246/// This function is used in custom panic handlers.
247#[cfg(feature = "custom-panic-handler")]
248pub fn report_panic_info(info: &PanicInfo) {
249    writeln!(Ram { offset: 0 }, "{}", info).ok();
250}
251
252#[cfg(not(feature = "custom-panic-handler"))]
253#[panic_handler]
254fn panic(info: &PanicInfo) -> ! {
255    cortex_m::interrupt::disable();
256
257    #[cfg(feature = "min-panic")]
258    if let Some(location) = info.location() {
259        writeln!(Ram { offset: 0 }, "Panicked at {}", location).ok();
260    } else {
261        writeln!(Ram { offset: 0 }, "Panic occured!").ok();
262    }
263
264    #[cfg(not(feature = "min-panic"))]
265    writeln!(Ram { offset: 0 }, "{}", info).ok();
266
267    cortex_m::peripheral::SCB::sys_reset();
268}