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}