feat: add display UI with splash screen, dashboard, and RSSI sparkline

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>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-03-29 20:42:54 -06:00
1 parent 7064110293
commit 3102fc747a
17624 files changed
+4564928 -38

No files matched your search

+1
View File
@@ -0,0 +1 @@
{"files":{".cargo_vcs_info.json":"96605b7306f87ca94b37b6e04eb23bf98393390b85c4d8fc210901beef13f265","CHANGELOG.md":"079617c5c2d2ff2aaf597f5b8b36b96ebe4375a6cec253b4f7368a9021b5dbfe","Cargo.toml":"3070964015d592fcbcab9a9c9e62e29d7b1c8fb90986bab0cababdf48e15d2c3","Cargo.toml.orig":"33a3670161a8c426406b06e4f87b9afefc9f3a603ddb12ab3b478a7db4a7a8ac","README.md":"81be4ce9892ea6bec014ebe1e5608c583c06db7dd081eddeb40e79cf8a2262ce","src/lib.rs":"27a6803089997d08c438b568921a5cbfa0a3f6ddeb0c123ac168692fcec60ba7","src/result.rs":"4da48ac78ef99cc4bdf861c1b5f0a764b95bd134db538e38d1ce442cfb3aab07"},"package":"8188909339ccc0c68cfb5a04648313f09621e8b87dc03095454f1a11f6c5d436"}
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "f73533a7d5663fc7ea85d715d828ca9983683761"
},
"path_in_vcs": "riscv-pac"
}
+35
View File
@@ -0,0 +1,35 @@
# Change Log
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
## [v0.2.0] - 2024-10-19
### Added
- Add `result` module for `Error` and `Result` types
- Add `ExceptionNumber` trait.
- Classify interrupt numbers in `CoreInterruptNumber` and `ExternalInterruptNumber`.
- Added simple tests to illustrate how to implement all the provided traits.
### Changed
- All traits now work with `usize` data type.
## [v0.1.1] - 2024-02-15
- Fix crates.io badge links
## [v0.1.0] - 2024-01-14
### Added
- Add `InterruptNumber`, `PriorityNumber`, and `HartIdNumber` traits.
### Changed
- Update `README.md`
+51
View File
@@ -0,0 +1,51 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.60"
name = "riscv-pac"
version = "0.2.0"
authors = ["The RISC-V Team <risc-v@teams.rust-embedded.org>"]
build = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Low level access to RISC-V processors"
documentation = "https://docs.rs/riscv-pac"
readme = "README.md"
keywords = [
"riscv",
"register",
"peripheral",
]
categories = [
"embedded",
"hardware-support",
"no-std",
]
license = "ISC"
repository = "https://github.com/rust-embedded/riscv"
[package.metadata.docs.rs]
default-target = "riscv64imac-unknown-none-elf"
targets = [
"riscv32i-unknown-none-elf",
"riscv32imc-unknown-none-elf",
"riscv32imac-unknown-none-elf",
"riscv64imac-unknown-none-elf",
"riscv64gc-unknown-none-elf",
]
[lib]
name = "riscv_pac"
path = "src/lib.rs"
+40
View File
@@ -0,0 +1,40 @@
[![crates.io](https://img.shields.io/crates/d/riscv-pac.svg)](https://crates.io/crates/riscv-pac)
[![crates.io](https://img.shields.io/crates/v/riscv-pac.svg)](https://crates.io/crates/riscv-pac)
# `riscv-pac`
> Target-specific traits to be implemented by PACs
This project is developed and maintained by the [RISC-V team][team].
## [Documentation](https://docs.rs/crate/riscv-pac)
## Minimum Supported Rust Version (MSRV)
This crate is guaranteed to compile on stable Rust 1.60 and up. It *might*
compile with older versions but that may change in any new patch release.
## License
Copyright 2023-2024 [RISC-V team][team]
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
## Code of Conduct
Contribution to this crate is organized under the terms of the [Rust Code of
Conduct][CoC], the maintainer of this crate, the [RISC-V team][team], promises
to intervene to uphold that code of conduct.
[CoC]: CODE_OF_CONDUCT.md
[team]: https://github.com/rust-embedded/wg#the-risc-v-team
+289
View File
@@ -0,0 +1,289 @@
#![no_std]
pub mod result;
use result::Result;
/// Trait for enums of target-specific exception numbers.
///
/// This trait should be implemented by a peripheral access crate (PAC) on its enum of available
/// exceptions for a specific device. Alternatively, the `riscv` crate provides a default
/// implementation for the RISC-V ISA. Each variant must convert to a `usize` of its exception number.
///
/// # Safety
///
/// * This trait must only be implemented on the `riscv` crate or on a PAC of a RISC-V target.
/// * This trait must only be implemented on enums of exceptions.
/// * Each enum variant must represent a distinct value (no duplicates are permitted),
/// * Each enum variant must always return the same value (do not change at runtime).
/// * All the exception numbers must be less than or equal to `MAX_EXCEPTION_NUMBER`.
/// * `MAX_EXCEPTION_NUMBER` must coincide with the highest allowed exception number.
pub unsafe trait ExceptionNumber: Copy {
/// Highest number assigned to an exception.
const MAX_EXCEPTION_NUMBER: usize;
/// Converts an exception to its corresponding number.
fn number(self) -> usize;
/// Tries to convert a number to a valid exception.
fn from_number(value: usize) -> Result<Self>;
}
/// Trait for enums of target-specific interrupt numbers.
///
/// This trait should be implemented by a peripheral access crate (PAC) on its enum of available
/// interrupts for a specific device. Alternatively, the `riscv` crate provides a default
/// implementation for the RISC-V ISA. Each variant must convert to a `usize` of its interrupt number.
///
/// # Safety
///
/// * This trait must only be implemented on the `riscv` crate or on a PAC of a RISC-V target.
/// * This trait must only be implemented on enums of interrupts.
/// * Each enum variant must represent a distinct value (no duplicates are permitted),
/// * Each enum variant must always return the same value (do not change at runtime).
/// * All the interrupt numbers must be less than or equal to `MAX_INTERRUPT_NUMBER`.
/// * `MAX_INTERRUPT_NUMBER` must coincide with the highest allowed interrupt number.
pub unsafe trait InterruptNumber: Copy {
/// Highest number assigned to an interrupt source.
const MAX_INTERRUPT_NUMBER: usize;
/// Converts an interrupt source to its corresponding number.
fn number(self) -> usize;
/// Tries to convert a number to a valid interrupt.
fn from_number(value: usize) -> Result<Self>;
}
/// Marker trait for enums of target-specific core interrupt numbers.
///
/// Core interrupts are interrupts are retrieved from the `mcause` CSR. Usually, vectored mode is
/// only available for core interrupts. The `riscv` crate provides a default implementation for
/// the RISC-V ISA. However, a PAC may override the default implementation if the target has a
/// different interrupt numbering scheme (e.g., ESP32C3).
///
/// # Safety
///
/// Each enum variant must represent a valid core interrupt number read from the `mcause` CSR.
pub unsafe trait CoreInterruptNumber: InterruptNumber {}
/// Marker trait for enums of target-specific external interrupt numbers.
///
/// External interrupts are interrupts caused by external sources (e.g., GPIO, UART, SPI).
/// External interrupts are **not** retrieved from the `mcause` CSR.
/// Instead, RISC-V processors have a single core interrupt for all external interrupts.
/// An additional peripheral (e.g., PLIC) is used to multiplex the external interrupts.
///
/// # Safety
///
/// Each enum variant must represent a valid external interrupt number.
pub unsafe trait ExternalInterruptNumber: InterruptNumber {}
/// Trait for enums of priority levels.
///
/// This trait should be implemented by a peripheral access crate (PAC) on its enum of available
/// priority numbers for a specific device. Each variant must convert to a `usize` of its priority level.
///
/// # Safety
///
/// * This trait must only be implemented on a PAC of a RISC-V target.
/// * This trait must only be implemented on enums of priority levels.
/// * Each enum variant must represent a distinct value (no duplicates are permitted).
/// * Each enum variant must always return the same value (do not change at runtime).
/// * All the priority level numbers must be less than or equal to `MAX_PRIORITY_NUMBER`.
/// * `MAX_PRIORITY_NUMBER` must coincide with the highest allowed priority number.
pub unsafe trait PriorityNumber: Copy {
/// Number assigned to the highest priority level.
const MAX_PRIORITY_NUMBER: usize;
/// Converts a priority level to its corresponding number.
fn number(self) -> usize;
/// Tries to convert a number to a valid priority level.
fn from_number(value: usize) -> Result<Self>;
}
/// Trait for enums of HART identifiers.
///
/// This trait should be implemented by a peripheral access crate (PAC) on its enum of available
/// HARTs for a specific device. Each variant must convert to a `usize` of its HART ID number.
///
/// # Safety
///
/// * This trait must only be implemented on a PAC of a RISC-V target.
/// * This trait must only be implemented on enums of HART IDs.
/// * Each enum variant must represent a distinct value (no duplicates are permitted),
/// * Each anum variant must always return the same value (do not change at runtime).
/// * All the HART ID numbers must be less than or equal to `MAX_HART_ID_NUMBER`.
/// * `MAX_HART_ID_NUMBER` must coincide with the highest allowed HART ID number.
pub unsafe trait HartIdNumber: Copy {
/// Highest number assigned to a context.
const MAX_HART_ID_NUMBER: usize;
/// Converts a HART ID to its corresponding number.
fn number(self) -> usize;
/// Tries to convert a number to a valid HART ID.
fn from_number(value: usize) -> Result<Self>;
}
#[cfg(test)]
mod test {
use super::*;
use crate::result::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Exception {
E1 = 1,
E3 = 3,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Interrupt {
I1 = 1,
I2 = 2,
I4 = 4,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Priority {
P0 = 0,
P1 = 1,
P2 = 2,
P3 = 3,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum HartId {
H0 = 0,
H1 = 1,
H2 = 2,
}
unsafe impl ExceptionNumber for Exception {
const MAX_EXCEPTION_NUMBER: usize = Self::E3 as usize;
#[inline]
fn number(self) -> usize {
self as _
}
#[inline]
fn from_number(number: usize) -> Result<Self> {
match number {
1 => Ok(Exception::E1),
3 => Ok(Exception::E3),
_ => Err(Error::InvalidVariant(number)),
}
}
}
unsafe impl InterruptNumber for Interrupt {
const MAX_INTERRUPT_NUMBER: usize = Self::I4 as usize;
#[inline]
fn number(self) -> usize {
self as _
}
#[inline]
fn from_number(number: usize) -> Result<Self> {
match number {
1 => Ok(Interrupt::I1),
2 => Ok(Interrupt::I2),
4 => Ok(Interrupt::I4),
_ => Err(Error::InvalidVariant(number)),
}
}
}
unsafe impl PriorityNumber for Priority {
const MAX_PRIORITY_NUMBER: usize = Self::P3 as usize;
#[inline]
fn number(self) -> usize {
self as _
}
#[inline]
fn from_number(number: usize) -> Result<Self> {
match number {
0 => Ok(Priority::P0),
1 => Ok(Priority::P1),
2 => Ok(Priority::P2),
3 => Ok(Priority::P3),
_ => Err(Error::InvalidVariant(number)),
}
}
}
unsafe impl HartIdNumber for HartId {
const MAX_HART_ID_NUMBER: usize = Self::H2 as usize;
#[inline]
fn number(self) -> usize {
self as _
}
#[inline]
fn from_number(number: usize) -> Result<Self> {
match number {
0 => Ok(HartId::H0),
1 => Ok(HartId::H1),
2 => Ok(HartId::H2),
_ => Err(Error::InvalidVariant(number)),
}
}
}
#[test]
fn check_exception_enum() {
assert_eq!(Exception::E1.number(), 1);
assert_eq!(Exception::E3.number(), 3);
assert_eq!(Exception::from_number(0), Err(Error::InvalidVariant(0)));
assert_eq!(Exception::from_number(1), Ok(Exception::E1));
assert_eq!(Exception::from_number(2), Err(Error::InvalidVariant(2)));
assert_eq!(Exception::from_number(3), Ok(Exception::E3));
assert_eq!(Exception::from_number(4), Err(Error::InvalidVariant(4)));
}
#[test]
fn check_interrupt_enum() {
assert_eq!(Interrupt::I1.number(), 1);
assert_eq!(Interrupt::I2.number(), 2);
assert_eq!(Interrupt::I4.number(), 4);
assert_eq!(Interrupt::from_number(0), Err(Error::InvalidVariant(0)));
assert_eq!(Interrupt::from_number(1), Ok(Interrupt::I1));
assert_eq!(Interrupt::from_number(2), Ok(Interrupt::I2));
assert_eq!(Interrupt::from_number(3), Err(Error::InvalidVariant(3)));
assert_eq!(Interrupt::from_number(4), Ok(Interrupt::I4));
assert_eq!(Interrupt::from_number(5), Err(Error::InvalidVariant(5)));
}
#[test]
fn check_priority_enum() {
assert_eq!(Priority::P0.number(), 0);
assert_eq!(Priority::P1.number(), 1);
assert_eq!(Priority::P2.number(), 2);
assert_eq!(Priority::P3.number(), 3);
assert_eq!(Priority::from_number(0), Ok(Priority::P0));
assert_eq!(Priority::from_number(1), Ok(Priority::P1));
assert_eq!(Priority::from_number(2), Ok(Priority::P2));
assert_eq!(Priority::from_number(3), Ok(Priority::P3));
assert_eq!(Priority::from_number(4), Err(Error::InvalidVariant(4)));
}
#[test]
fn check_hart_id_enum() {
assert_eq!(HartId::H0.number(), 0);
assert_eq!(HartId::H1.number(), 1);
assert_eq!(HartId::H2.number(), 2);
assert_eq!(HartId::from_number(0), Ok(HartId::H0));
assert_eq!(HartId::from_number(1), Ok(HartId::H1));
assert_eq!(HartId::from_number(2), Ok(HartId::H2));
assert_eq!(HartId::from_number(3), Err(Error::InvalidVariant(3)));
}
}
+58
View File
@@ -0,0 +1,58 @@
use core::fmt;
/// Convenience alias for the [Result](core::result::Result) type for the library.
pub type Result<T> = core::result::Result<T, Error>;
/// Represents error variants for the library.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Error {
/// Attempted out-of-bounds access.
IndexOutOfBounds {
index: usize,
min: usize,
max: usize,
},
/// Invalid field value.
InvalidFieldValue {
field: &'static str,
value: usize,
bitmask: usize,
},
/// Invalid value of a register field that does not match any known variants.
InvalidFieldVariant { field: &'static str, value: usize },
/// Invalid value.
InvalidValue { value: usize, bitmask: usize },
/// Invalid value that does not match any known variants.
InvalidVariant(usize),
/// Unimplemented function or type.
Unimplemented,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IndexOutOfBounds { index, min, max } => write!(
f,
"out-of-bounds access, index: {index}, min: {min}, max: {max}"
),
Self::InvalidFieldValue {
field,
value,
bitmask,
} => write!(
f,
"invalid {field} field value: {value:#x}, valid bitmask: {bitmask:#x}",
),
Self::InvalidFieldVariant { field, value } => {
write!(f, "invalid {field} field variant: {value:#x}")
}
Self::InvalidValue { value, bitmask } => {
write!(f, "invalid value: {value:#x}, valid bitmask: {bitmask:#x}",)
}
Self::InvalidVariant(value) => {
write!(f, "invalid variant: {value:#x}")
}
Self::Unimplemented => write!(f, "unimplemented"),
}
}
}