1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
//! # `panic-persist`
//!
//! Writes panic messages to a section of RAM
//!
//! This crate contains an implementation of `panic_fmt` that logs panic messages to a region of
//! RAM defined by the user, so that these messages can be retrieved on next boot, and handled
//! outside of panic context, by sending to a logging interface, writing to flash, etc.
//!
//! After logging the message to RAM, the device will be soft-reset automatically.
//!
//! Unlike other methods this allows to discover the panic reason post-mortem using normal program
//! control flow.
//!
//! Currently this crate was only tested on ARM Cortex-M architecture but should be easily portable
//! to other platforms as required.
//!
//! ## Usage
//!
//! ### Add a section to your linker script
//!
//! You will need to reserve a section of RAM to be used to persist messages. This section must be
//! large enough to hold the 8 byte header, as well as any panic messages you would like to persist.
//! If there is not suitable space in the section, the panic message will be truncated.
//!
//! This section should be outside of any other sections, to prevent program initialization from
//! zeroing or otherwise modifying these sections on boot.
//!
//! `memory.x` file before modification:
//!
//! ``` ignore
//! MEMORY
//! {
//!   /* NOTE K = KiBi = 1024 bytes */
//!   FLASH : ORIGIN  = 0x00000000, LENGTH = 512K
//!   RAM : ORIGIN    = 0x20000000, LENGTH = 64K
//! }
//! ```
//!
//! `memory.x` file after modification to hold a 1K region:
//!
//! ``` ignore
//! MEMORY
//! {
//!   /* NOTE K = KiBi = 1024 bytes */
//!   FLASH : ORIGIN  = 0x00000000, LENGTH = 512K
//!   RAM : ORIGIN    = 0x20000000, LENGTH = 63K
//!   PANDUMP: ORIGIN = 0x2000FC00, LENGTH = 1K
//! }
//!
//! _panic_dump_start = ORIGIN(PANDUMP);
//! _panic_dump_end   = ORIGIN(PANDUMP) + LENGTH(PANDUMP);
//! ```
//!
//!
//! ### Program Usage Example
//!
//! ``` ignore
//! #![no_std]
//!
//! use panic_persist as _;
//!
//! #[entry]
//! fn main() -> ! {
//!     // Normal board setup...
//!
//!     // Check if there was a panic message, if so, send to UART
//!     if let Some(msg) = get_panic_message_bytes() {
//!         board.uart.write(msg);
//!     }
//!
//!     // ...
//! }
//! ```
//!
//! ## Features
//!
//! There are a few optional features, `utf8` and `custom-panic-handler`.
//!
//! ### utf8
//!
//! This allows the panic message to be returned
//! as a `&str` rather than `&[u8]`, for easier printing. As this requires the ability
//! to validate the UTF-8 string (to ensure it wasn't truncated mid-character), it may
//! increase code size usage, and is by default off.
//!
//! ### custom-panic-handler
//!
//! This disables the panic handler from this library so that any user can implement their own.
//! To persist panic messages, the function `report_panic_info` is made available;
//!
//! ```rust
//! // My custom panic implementation
//! #[panic_handler]
//! fn panic(info: &PanicInfo) -> ! {
//!     // ...
//!     panic_persist::report_panic_info(info);
//!     // ...
//! }
//! ```
//!
//! ### min-panic
//!
//! This prints a smaller, line-number-only message, in order to reduce space needed when persisting panics, at a loss of some context.

#![allow(clippy::empty_loop)]
#![deny(missing_docs)]
#![deny(warnings)]
#![no_std]

use core::cmp::min;
use core::fmt::Write;
use core::mem::size_of;
use core::panic::PanicInfo;

struct Ram {
    offset: usize,
}

/// Internal Write implementation to output the formatted panic string into RAM
impl core::fmt::Write for Ram {
    fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> {
        // Obtain panic region start and end from linker symbol _panic_dump_start and _panic_dump_end
        extern "C" {
            static mut _panic_dump_start: u8;
            static mut _panic_dump_end: u8;
        }

        // Get the data about the string that is being written now
        let data = s.as_bytes();
        let len = data.len();

        // Obtain info about the panic dump region
        let start_ptr = unsafe { &mut _panic_dump_start as *mut u8 };
        let end_ptr = unsafe { &mut _panic_dump_end as *mut u8 };
        let max_len = end_ptr as usize - start_ptr as usize;
        let max_len_str = max_len - size_of::<usize>() - size_of::<usize>();

        // If we have written the full length of the region, we can't write any
        // more. This could happen with multiple writes with this implementation
        if self.offset >= max_len_str {
            return Ok(());
        }

        // We should write the size of the string, or the amount of space
        // we have remaining, whichever is less
        let str_len = min(max_len_str - self.offset, len);

        unsafe {
            // Write the magic word for later detection
            start_ptr.cast::<usize>().write_unaligned(0x0FACADE0);

            // For now, skip writing the length...

            // Write the string to RAM
            core::ptr::copy(
                data.as_ptr() as *mut u8,
                start_ptr.offset(8).offset(self.offset as isize),
                str_len,
            );

            // Increment the offset so later writes will be appended
            self.offset += str_len;

            // ... and now write the current offset (or total size) to the size location
            start_ptr
                .offset(4)
                .cast::<usize>()
                .write_unaligned(self.offset);
        };

        Ok(())
    }
}

/// Get the panic message from the last boot, if any.
/// This method may possibly not return valid UTF-8 if the message
/// was truncated before the end of a full UTF-8 character. Care must
/// be taken before treating this as a proper &str.
///
/// If a message existed, this function will only return the value once
/// (subsequent calls will return None)
pub fn get_panic_message_bytes() -> Option<&'static [u8]> {
    // Obtain panic region start and end from linker symbol _panic_dump_start and _panic_dump_end
    extern "C" {
        static mut _panic_dump_start: u8;
        static mut _panic_dump_end: u8;
    }

    let start_ptr = unsafe { &mut _panic_dump_start as *mut u8 };

    if 0x0FACADE0 != unsafe { core::ptr::read_unaligned(start_ptr.cast::<usize>()) } {
        return None;
    }

    // Clear the magic word to prevent this message from "sticking"
    // across multiple boots
    unsafe {
        start_ptr.cast::<usize>().write_unaligned(0x00000000);
    }

    // Obtain info about the panic dump region
    let end_ptr = unsafe { &mut _panic_dump_end as *mut u8 };
    let max_len = end_ptr as usize - start_ptr as usize;
    let max_len_str = max_len - size_of::<usize>() - size_of::<usize>();

    let len = unsafe { core::ptr::read_unaligned(start_ptr.offset(4).cast::<usize>()) };

    if len > max_len_str {
        return None;
    }

    // TODO: This is prooooooooobably undefined behavior
    let byte_slice = unsafe { core::slice::from_raw_parts(start_ptr.offset(8), len) };

    Some(byte_slice)
}

/// Get the panic message from the last boot, if any. If any invalid
/// UTF-8 characters occur, the message will be truncated before the
/// first error.
///
/// If a message existed, this function will only return the value once
/// (subsequent calls will return None)
#[cfg(feature = "utf8")]
pub fn get_panic_message_utf8() -> Option<&'static str> {
    let bytes = get_panic_message_bytes()?;

    use core::str::from_utf8;

    match from_utf8(bytes) {
        Ok(stir) => Some(stir),
        Err(utf_err) => {
            match from_utf8(&bytes[..utf_err.valid_up_to()]) {
                Ok(stir) => Some(stir),
                Err(_) => {
                    // This shouldn't be possible...
                    None
                }
            }
        }
    }
}

/// Report the panic so the message is persisted.
///
/// This function is used in custom panic handlers.
#[cfg(feature = "custom-panic-handler")]
pub fn report_panic_info(info: &PanicInfo) {
    writeln!(Ram { offset: 0 }, "{}", info).ok();
}

#[cfg(not(feature = "custom-panic-handler"))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    cortex_m::interrupt::disable();

    #[cfg(feature = "min-panic")]
    if let Some(location) = info.location() {
        writeln!(Ram { offset: 0 }, "Panicked at {}", location).ok();
    } else {
        writeln!(Ram { offset: 0 }, "Panic occured!").ok();
    }

    #[cfg(not(feature = "min-panic"))]
    writeln!(Ram { offset: 0 }, "{}", info).ok();

    cortex_m::peripheral::SCB::sys_reset();
}