Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
342 lines
9.0 KiB
Rust
342 lines
9.0 KiB
Rust
#![allow(rustdoc::bare_urls, unused_macros)]
|
|
#![cfg_attr(target_arch = "xtensa", feature(asm_experimental_arch))]
|
|
#![doc = include_str!("../README.md")]
|
|
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/46717278")]
|
|
#![no_std]
|
|
|
|
#[cfg(feature = "defmt")]
|
|
use defmt as _;
|
|
#[cfg(feature = "println")]
|
|
use esp_println as _;
|
|
|
|
const MAX_BACKTRACE_ADDRESSES: usize = 10;
|
|
|
|
#[cfg(feature = "colors")]
|
|
const RESET: &str = "\u{001B}[0m";
|
|
#[cfg(feature = "colors")]
|
|
const RED: &str = "\u{001B}[31m";
|
|
|
|
#[cfg(feature = "defmt")]
|
|
macro_rules! println {
|
|
("") => {
|
|
// Do nothing if the string is just a space
|
|
};
|
|
($($arg:tt)*) => {
|
|
defmt::error!($($arg)*);
|
|
};
|
|
}
|
|
|
|
#[cfg(all(feature = "println", not(feature = "defmt")))]
|
|
macro_rules! println {
|
|
($($arg:tt)*) => {
|
|
esp_println::println!($($arg)*);
|
|
};
|
|
}
|
|
|
|
#[allow(unused, unused_variables)]
|
|
fn set_color_code(code: &str) {
|
|
#[cfg(feature = "println")]
|
|
{
|
|
println!("{}", code);
|
|
}
|
|
}
|
|
|
|
#[cfg_attr(target_arch = "riscv32", path = "riscv.rs")]
|
|
#[cfg_attr(target_arch = "xtensa", path = "xtensa.rs")]
|
|
pub mod arch;
|
|
|
|
#[cfg(feature = "panic-handler")]
|
|
#[panic_handler]
|
|
fn panic_handler(info: &core::panic::PanicInfo) -> ! {
|
|
pre_backtrace();
|
|
|
|
#[cfg(feature = "colors")]
|
|
set_color_code(RED);
|
|
|
|
println!("");
|
|
println!("====================== PANIC ======================");
|
|
|
|
#[cfg(not(feature = "defmt"))]
|
|
println!("{}", info);
|
|
|
|
#[cfg(feature = "defmt")]
|
|
println!("{}", defmt::Display2Format(info));
|
|
|
|
println!("");
|
|
println!("Backtrace:");
|
|
println!("");
|
|
|
|
let backtrace = crate::arch::backtrace();
|
|
#[cfg(target_arch = "riscv32")]
|
|
if backtrace.iter().filter(|e| e.is_some()).count() == 0 {
|
|
println!("No backtrace available - make sure to force frame-pointers. (see https://crates.io/crates/esp-backtrace)");
|
|
}
|
|
for addr in backtrace.into_iter().flatten() {
|
|
#[cfg(all(feature = "colors", feature = "println"))]
|
|
println!("{}0x{:x}", RED, addr - crate::arch::RA_OFFSET);
|
|
|
|
#[cfg(not(all(feature = "colors", feature = "println")))]
|
|
println!("0x{:x}", addr - crate::arch::RA_OFFSET);
|
|
}
|
|
|
|
#[cfg(feature = "colors")]
|
|
set_color_code(RESET);
|
|
|
|
#[cfg(feature = "semihosting")]
|
|
semihosting::process::abort();
|
|
|
|
#[cfg(not(feature = "semihosting"))]
|
|
halt();
|
|
}
|
|
|
|
#[cfg(all(feature = "exception-handler", target_arch = "xtensa"))]
|
|
#[no_mangle]
|
|
#[link_section = ".rwtext"]
|
|
unsafe fn __user_exception(cause: arch::ExceptionCause, context: arch::Context) {
|
|
pre_backtrace();
|
|
|
|
#[cfg(feature = "colors")]
|
|
set_color_code(RED);
|
|
|
|
// Unfortunately, a different formatter string is used
|
|
#[cfg(not(feature = "defmt"))]
|
|
esp_println::println!("\n\nException occurred '{}'", cause);
|
|
|
|
#[cfg(feature = "defmt")]
|
|
defmt::error!("\n\nException occurred '{}'", cause);
|
|
|
|
println!("{:?}", context);
|
|
|
|
let backtrace = crate::arch::backtrace_internal(context.A1, 0);
|
|
for e in backtrace {
|
|
if let Some(addr) = e {
|
|
println!("0x{:x}", addr);
|
|
}
|
|
}
|
|
println!("");
|
|
println!("");
|
|
println!("");
|
|
|
|
#[cfg(feature = "colors")]
|
|
set_color_code(RESET);
|
|
|
|
#[cfg(feature = "semihosting")]
|
|
semihosting::process::abort();
|
|
|
|
#[cfg(not(feature = "semihosting"))]
|
|
halt();
|
|
}
|
|
|
|
#[cfg(all(feature = "exception-handler", target_arch = "riscv32"))]
|
|
#[export_name = "ExceptionHandler"]
|
|
fn exception_handler(context: &arch::TrapFrame) -> ! {
|
|
pre_backtrace();
|
|
|
|
let mepc = context.pc;
|
|
let code = context.mcause & 0xff;
|
|
let mtval = context.mtval;
|
|
|
|
#[cfg(feature = "colors")]
|
|
set_color_code(RED);
|
|
|
|
if code == 14 {
|
|
println!("");
|
|
println!(
|
|
"Stack overflow detected at 0x{:x} called by 0x{:x}",
|
|
mepc, context.ra
|
|
);
|
|
println!("");
|
|
} else {
|
|
let code = match code {
|
|
0 => "Instruction address misaligned",
|
|
1 => "Instruction access fault",
|
|
2 => "Illegal instruction",
|
|
3 => "Breakpoint",
|
|
4 => "Load address misaligned",
|
|
5 => "Load access fault",
|
|
6 => "Store/AMO address misaligned",
|
|
7 => "Store/AMO access fault",
|
|
8 => "Environment call from U-mode",
|
|
9 => "Environment call from S-mode",
|
|
10 => "Reserved",
|
|
11 => "Environment call from M-mode",
|
|
12 => "Instruction page fault",
|
|
13 => "Load page fault",
|
|
14 => "Reserved",
|
|
15 => "Store/AMO page fault",
|
|
_ => "UNKNOWN",
|
|
};
|
|
|
|
println!(
|
|
"Exception '{}' mepc=0x{:08x}, mtval=0x{:08x}",
|
|
code, mepc, mtval
|
|
);
|
|
#[cfg(not(feature = "defmt"))]
|
|
println!("{:x?}", context);
|
|
|
|
#[cfg(feature = "defmt")]
|
|
println!("{:?}", context);
|
|
|
|
let backtrace = crate::arch::backtrace_internal(context.s0 as u32, 0);
|
|
if backtrace.iter().filter(|e| e.is_some()).count() == 0 {
|
|
println!("No backtrace available - make sure to force frame-pointers. (see https://crates.io/crates/esp-backtrace)");
|
|
}
|
|
for addr in backtrace.into_iter().flatten() {
|
|
#[cfg(all(feature = "colors", feature = "println"))]
|
|
println!("{}0x{:x}", RED, addr - crate::arch::RA_OFFSET);
|
|
|
|
#[cfg(not(all(feature = "colors", feature = "println")))]
|
|
println!("0x{:x}", addr - crate::arch::RA_OFFSET);
|
|
}
|
|
}
|
|
|
|
println!("");
|
|
println!("");
|
|
println!("");
|
|
|
|
#[cfg(feature = "colors")]
|
|
set_color_code(RESET);
|
|
|
|
#[cfg(feature = "semihosting")]
|
|
semihosting::process::abort();
|
|
|
|
#[cfg(not(feature = "semihosting"))]
|
|
halt();
|
|
}
|
|
|
|
// Ensure that the address is in DRAM and that it is 16-byte aligned.
|
|
//
|
|
// Based loosely on the `esp_stack_ptr_in_dram` function from
|
|
// `components/esp_hw_support/include/esp_memory_utils.h` in ESP-IDF.
|
|
//
|
|
// Address ranges can be found in `components/soc/$CHIP/include/soc/soc.h` as
|
|
// `SOC_DRAM_LOW` and `SOC_DRAM_HIGH`.
|
|
fn is_valid_ram_address(address: u32) -> bool {
|
|
if (address & 0xF) != 0 {
|
|
return false;
|
|
}
|
|
|
|
#[cfg(feature = "esp32")]
|
|
if !(0x3FFA_E000..=0x4000_0000).contains(&address) {
|
|
return false;
|
|
}
|
|
|
|
#[cfg(feature = "esp32c2")]
|
|
if !(0x3FCA_0000..=0x3FCE_0000).contains(&address) {
|
|
return false;
|
|
}
|
|
|
|
#[cfg(feature = "esp32c3")]
|
|
if !(0x3FC8_0000..=0x3FCE_0000).contains(&address) {
|
|
return false;
|
|
}
|
|
|
|
#[cfg(feature = "esp32c6")]
|
|
if !(0x4080_0000..=0x4088_0000).contains(&address) {
|
|
return false;
|
|
}
|
|
|
|
#[cfg(feature = "esp32h2")]
|
|
if !(0x4080_0000..=0x4085_0000).contains(&address) {
|
|
return false;
|
|
}
|
|
|
|
#[cfg(feature = "esp32p4")]
|
|
if !(0x4FF0_0000..=0x4FFC_0000).contains(&address) {
|
|
return false;
|
|
}
|
|
|
|
#[cfg(feature = "esp32s2")]
|
|
if !(0x3FFB_0000..=0x4000_0000).contains(&address) {
|
|
return false;
|
|
}
|
|
|
|
#[cfg(feature = "esp32s3")]
|
|
if !(0x3FC8_8000..=0x3FD0_0000).contains(&address) {
|
|
return false;
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
#[cfg(all(
|
|
any(
|
|
not(any(feature = "esp32", feature = "esp32p4", feature = "esp32s3")),
|
|
not(feature = "halt-cores")
|
|
),
|
|
not(feature = "custom-halt")
|
|
))]
|
|
#[allow(unused)]
|
|
fn halt() -> ! {
|
|
loop {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "custom-halt")]
|
|
fn halt() -> ! {
|
|
extern "Rust" {
|
|
fn custom_halt() -> !;
|
|
}
|
|
unsafe { custom_halt() }
|
|
}
|
|
|
|
// TODO: Enable `halt` function for `esp32p4` feature once implemented
|
|
#[cfg(all(any(feature = "esp32", feature = "esp32s3"), feature = "halt-cores"))]
|
|
#[allow(unused)]
|
|
fn halt() -> ! {
|
|
#[cfg(feature = "esp32")]
|
|
mod registers {
|
|
pub(crate) const OPTIONS0: u32 = 0x3ff48000;
|
|
pub(crate) const SW_CPU_STALL: u32 = 0x3ff480ac;
|
|
}
|
|
|
|
#[cfg(feature = "esp32p4")]
|
|
mod registers {
|
|
pub(crate) const SW_CPU_STALL: u32 = 0x50115200;
|
|
}
|
|
|
|
#[cfg(feature = "esp32s3")]
|
|
mod registers {
|
|
pub(crate) const OPTIONS0: u32 = 0x60008000;
|
|
pub(crate) const SW_CPU_STALL: u32 = 0x600080bc;
|
|
}
|
|
|
|
let sw_cpu_stall = registers::SW_CPU_STALL as *mut u32;
|
|
|
|
#[cfg(feature = "esp32p4")]
|
|
unsafe {}
|
|
|
|
#[cfg(not(feature = "esp32p4"))]
|
|
unsafe {
|
|
// We need to write the value "0x86" to stall a particular core. The write
|
|
// location is split into two separate bit fields named "c0" and "c1", and the
|
|
// two fields are located in different registers. Each core has its own pair of
|
|
// "c0" and "c1" bit fields.
|
|
|
|
let options0 = registers::OPTIONS0 as *mut u32;
|
|
|
|
options0.write_volatile(options0.read_volatile() & !(0b1111) | 0b1010);
|
|
|
|
sw_cpu_stall.write_volatile(
|
|
sw_cpu_stall.read_volatile() & !(0b111111 << 20) & !(0b111111 << 26)
|
|
| (0x21 << 20)
|
|
| (0x21 << 26),
|
|
);
|
|
}
|
|
|
|
loop {}
|
|
}
|
|
|
|
#[cfg(not(feature = "custom-pre-backtrace"))]
|
|
#[allow(unused)]
|
|
fn pre_backtrace() {}
|
|
|
|
#[cfg(feature = "custom-pre-backtrace")]
|
|
fn pre_backtrace() {
|
|
extern "Rust" {
|
|
fn custom_pre_backtrace();
|
|
}
|
|
unsafe { custom_pre_backtrace() }
|
|
}
|