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/config.toml":"6afd1a04a09cf72bd9cc9cc288c3ff336617d1ff52e72805c5b1d5dd373a470d",".cargo_vcs_info.json":"68b407dd60d82da62ab64110a554843660dbb9622cfafbd70d4dc56bd832b716","CHANGELOG.md":"1d3f2be7946af51f1884b1e2dd196f94dd22506ab04c4d1918949cb831505eea","Cargo.toml":"970dce61afbb969ba7724f8c8a5b70c8fb994a4d2b2ee79ce04427280ae12b5e","Cargo.toml.orig":"fe16338dbf9466d17efd4a0401401e13c803ebde9aefea51ca7334ed6c55e247","README.md":"fecf2f597b3a86eccc6345e1920e35ee22b884db137277ed5ac6aec728208c92","build.rs":"9386dd12a6619c367dc2993cf611487df8d0ad18ac1de585f94cc920a9353692","src/defmt.rs":"4440a40cee2d06f2840ff717e8c12014f027902756c5ee288273901aa26913ee","src/lib.rs":"17b29d5af997bea3475d5552b3797436571b5f6651b4d79bb5f7d74e51d8674b","src/logger.rs":"bbb6f8e22a72e56e134ca51eaf906ad9d04b14588f8b6d8c03d824f2b7fd5304"},"package":"ee38e87bc7e303c299047c0e9bcd0f8ccca7c7e70d1fd78bbb565db14f33beb6"}
+2
View File
@@ -0,0 +1,2 @@
[unstable]
build-std = ["alloc", "core"]
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "d655fe665faf2ff2d64d4debb7852b003594d3c1"
},
"path_in_vcs": "esp-println"
}
+63
View File
@@ -0,0 +1,63 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
### Changed
### Fixed
### Removed
## 0.12.0 - 2024-10-10
### Changed
- Replace environment variables `ESP_LOGLEVEL` and `ESP_LOGFILTER` with just one environment variable: `ESP_LOG` (#2291)
## 0.11.0 - 2024-08-29
### Added
- Made `esp_println::Printer::write_bytes` public (#1812)
## 0.10.0 - 2024-07-15
### Added
- Add `auto` feature to auto-detect Serial-JTAG/UART communication (#1658)
### Changed
- `auto` is the default communication method (#1658)
- Add the `links` field to Cargo.toml so that only one version of the package can be included (#1761)
## 0.9.1 - 2024-03-11
### Changed
- Un-pinned the defmt package's version number
## 0.9.0 - 2024-02-07
### Added
- Add support for ESP32-P4
### Removed
- Remove ESP 8266 support
## 0.8.0 - 2023-12-21
### Removed
- Remove RTT and defmt-raw support
[Unreleased]: https://github.com/esp-rs/esp-hal/commits/main/esp-println?since=2024-10-10
+88
View File
@@ -0,0 +1,88 @@
# 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.76.0"
name = "esp-println"
version = "0.12.0"
build = "build.rs"
links = "esp-println"
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Provides `print!` and `println!` implementations various Espressif devices"
readme = "README.md"
license = "MIT OR Apache-2.0"
repository = "https://github.com/esp-rs/esp-hal"
[package.metadata.docs.rs]
cargo-args = [
"-Z",
"build-std=core",
]
default-target = "riscv32imc-unknown-none-elf"
features = ["esp32c3"]
[lib]
name = "esp_println"
path = "src/lib.rs"
[dependencies.critical-section]
version = "1.1.3"
optional = true
[dependencies.defmt]
version = "0.3.8"
optional = true
[dependencies.log]
version = "0.4.22"
optional = true
[dependencies.portable-atomic]
version = "1.9.0"
optional = true
default-features = false
[build-dependencies.esp-build]
version = "0.1.0"
[build-dependencies.log]
version = "0.4.22"
[features]
auto = ["dep:portable-atomic"]
colors = []
critical-section = ["dep:critical-section"]
default = [
"critical-section",
"colors",
"auto",
]
defmt-espflash = [
"dep:defmt",
"defmt?/encoding-rzcobs",
]
esp32 = []
esp32c2 = []
esp32c3 = []
esp32c6 = []
esp32h2 = []
esp32p4 = []
esp32s2 = []
esp32s3 = []
jtag-serial = ["dep:portable-atomic"]
log = ["dep:log"]
no-op = []
uart = []
+123
View File
@@ -0,0 +1,123 @@
# esp-println
[![Crates.io](https://img.shields.io/crates/v/esp-println?labelColor=1C2C2E&color=C96329&logo=Rust&style=flat-square)](https://crates.io/crates/esp-println)
[![docs.rs](https://img.shields.io/docsrs/esp-println?labelColor=1C2C2E&color=C96329&logo=rust&style=flat-square)](https://docs.rs/esp-println)
![MSRV](https://img.shields.io/badge/MSRV-1.76-blue?labelColor=1C2C2E&style=flat-square)
![Crates.io](https://img.shields.io/crates/l/esp-println?labelColor=1C2C2E&style=flat-square)
[![Matrix](https://img.shields.io/matrix/esp-rs:matrix.org?label=join%20matrix&labelColor=1C2C2E&color=BEC5C9&logo=matrix&style=flat-square)](https://matrix.to/#/#esp-rs:matrix.org)
A library that provides `print!`, `println!`, `dbg!` implementations and
logging capabilities for Espressif devices.
- Supports all Espressif ESP32 family devices.
- Supports different communication methods:
- UART (Default)
- JTAG-Serial (Only available in ESP32-C3, ESP32-C6, ESP32-H2, ESP32-S3)
- No-op: Turns printing into a no-op
- Supports [`defmt`] backend
# Usage
```toml
esp-println = { version = "0.11.0", features = ["esp32c2"] }
```
or `cargo add esp-println --features esp32c2`
It's important to specify your target device as feature.
Then in your program:
```rust
use esp_println::println;
```
You can now `println!("Hello world")` as usual.
# Features
- There is one feature for each supported target: `esp32`, `esp32c2`,
`esp32c3`, `esp32c6`, `esp32h2`, `esp32s2`, and `esp32s3`.
- One of these features must be enabled.
- Only one of these features can be enabled at a time.
- There is one feature for each supported communication method: `uart`, `jtag-serial` and `auto`.
- Only one of these features can be enabled at a time.
- `no-op`: Don't print anything.
- `log`: Enables logging using [`log` crate].
- `colors`: Enable colored logging.
- Only effective when using the `log` feature.
- `critical-section`: Enables critical sections.
- `defmt-espflash`: This is intended to be used with [`espflash`], see `-L/--log-format` argument
of `flash` or `monitor` subcommands of `espflash` and `cargo-espflash`. Uses [rzCOBS] encoding
and adds framing.
## Default Features
By default, we use the `auto`, `critial-section` and `colors` features.
Which means that it will auto-detect if it needs to print to the UART or JTAG-Serial, use critical sections and output
messages will be colored.
If we want to use a communication method that is not `auto`, the default
one, we need to [disable the default features].
## Logging
With the feature `log` activated you can initialize a simple logger like this
```rust
init_logger(log::LevelFilter::Info);
```
There is a default feature `colors` which enables colored log output.
Additionally, you can use
```rust
init_logger_from_env();
```
In this case the following environment variables are used:
- `ESP_LOG` log messages you want to show, similar to `RUST_LOG`. RegEx is not supported. e.g. `warn,test::foo=info,test::foo::bar=debug`
If this simple logger implementation isn't sufficient for your needs, you can implement your own logger on top of `esp-println`. See [Implementing a Logger section log documentaion]
## `defmt`
Using the `defmt-espflash` feature, `esp-println` will install a `defmt` global logger. The logger will
output to the same data stream as `println!()`, and adds framing bytes so it can be used even with
other, non-`defmt` output. Using the `defmt-espflash` feature automatically uses the [rzCOBS] encoding and does
not allow changing the encoding.
Follow the [`defmt` book's setup instructions] on how to
set up `defmt`. Remember, the global logger is already installed for you by `esp-println`!
Please note that `defmt` does _not_ provide MSRV guarantees with releases, and as such we are not able to make any MSRV guarantees when this feature is enabled. For more information refer to the MSRV section of `defmt`'s README:
https://github.com/knurling-rs/defmt?tab=readme-ov-file#msrv
[`defmt`]: https://github.com/knurling-rs/defmt
[`log` crate]: https://github.com/rust-lang/log
[rzCOBS]: https://github.com/Dirbaio/rzcobs
[`espflash`]: https://github.com/esp-rs/espflash
[disable the default features]: https://doc.rust-lang.org/cargo/reference/features.html#the-default-feature
[Implementing a Logger section log documentaion]: https://docs.rs/log/0.4.17/log/#implementing-a-logger
[`defmt` book's setup instructions]: https://defmt.ferrous-systems.com/setup
# Troubleshooting linker errors
If you experience linker errors, make sure you have _some_ reference to `esp_println` in your code.
If you don't use `esp_println` directly, you'll need to add e.g. `use esp_println as _;` to your
import statements. This ensures that the global logger will not be removed by the compiler.
# License
Licensed under either of:
- Apache License, Version 2.0 ([LICENSE-APACHE](../LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](../LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
# Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in
the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without
any additional terms or conditions.
+187
View File
@@ -0,0 +1,187 @@
use std::{env, path::Path};
use esp_build::assert_unique_used_features;
fn main() {
// Ensure that only a single chip is specified
assert_unique_used_features!(
"esp32", "esp32c2", "esp32c3", "esp32c6", "esp32h2", "esp32p4", "esp32s2", "esp32s3"
);
// Ensure that only a single communication method is specified
assert_unique_used_features!("jtag-serial", "uart", "auto");
// Ensure that, if the `jtag-serial` communication method feature is enabled,
// a compatible chip feature is also enabled.
if cfg!(feature = "jtag-serial")
&& !(cfg!(feature = "esp32c3")
|| cfg!(feature = "esp32c6")
|| cfg!(feature = "esp32h2")
|| cfg!(feature = "esp32p4")
|| cfg!(feature = "esp32s3"))
{
panic!(
"The `jtag-serial` feature is only supported by the ESP32-C3, ESP32-C6, ESP32-H2, ESP32-P4, and ESP32-S3"
);
}
// Ensure that, if the `colors` is used with `log`.`
if cfg!(feature = "colors") && !cfg!(feature = "log") {
println!(
"cargo:warning=The `colors` feature is only effective when using the `log` feature"
);
}
if std::env::var("ESP_LOGLEVEL").is_ok() || std::env::var("ESP_LOGFILTER").is_ok() {
panic!("`ESP_LOGLEVEL` and `ESP_LOGFILTER` is not supported anymore. Please use `ESP_LOG` instead.");
}
generate_filter_snippet();
#[cfg(target_os = "windows")]
println!("cargo:rustc-cfg=host_is_windows");
println!("cargo:rerun-if-env-changed=ESP_LOG");
println!("cargo:rustc-check-cfg=cfg(host_is_windows)");
}
fn generate_filter_snippet() {
let out_dir = env::var_os("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("log_filter.rs");
let filter = env::var("ESP_LOG");
let snippet = if let Ok(filter) = filter {
let res = parse_spec(&filter);
if !res.errors.is_empty() {
panic!("Error parsing `ESP_LOG`: {:?}", res.errors);
} else {
let max = res
.directives
.iter()
.map(|v| v.level)
.max()
.unwrap_or(log::LevelFilter::Off);
let max = match max {
log::LevelFilter::Off => "Off",
log::LevelFilter::Error => "Error",
log::LevelFilter::Warn => "Warn",
log::LevelFilter::Info => "Info",
log::LevelFilter::Debug => "Debug",
log::LevelFilter::Trace => "Trace",
};
let mut snippet = String::new();
snippet.push_str(&format!(
"pub(crate) const FILTER_MAX: log::LevelFilter = log::LevelFilter::{};",
max
));
snippet
.push_str("pub(crate) fn is_enabled(level: log::Level, _target: &str) -> bool {");
for directive in res.directives {
let level = match directive.level {
log::LevelFilter::Off => "Off",
log::LevelFilter::Error => "Error",
log::LevelFilter::Warn => "Warn",
log::LevelFilter::Info => "Info",
log::LevelFilter::Debug => "Debug",
log::LevelFilter::Trace => "Trace",
};
if let Some(name) = directive.name {
snippet.push_str(&format!(
"if _target.starts_with(\"{}\") && level <= log::LevelFilter::{} {{ return true; }}",
&name, level
));
} else {
snippet.push_str(&format!(
"if level <= log::LevelFilter::{} {{ return true; }}",
level
));
}
}
snippet.push_str(" false");
snippet.push('}');
snippet
}
} else {
"pub(crate) const FILTER_MAX: log::LevelFilter = log::LevelFilter::Off; pub(crate) fn is_enabled(_level: log::Level, _target: &str) -> bool { true }".to_string()
};
std::fs::write(&dest_path, &snippet).unwrap();
}
#[derive(Default, Debug)]
struct ParseResult {
pub(crate) directives: Vec<Directive>,
pub(crate) errors: Vec<String>,
}
impl ParseResult {
fn add_directive(&mut self, directive: Directive) {
self.directives.push(directive);
}
fn add_error(&mut self, message: String) {
self.errors.push(message);
}
}
#[derive(Debug)]
struct Directive {
pub(crate) name: Option<String>,
pub(crate) level: log::LevelFilter,
}
/// Parse a logging specification string (e.g:
/// `crate1,crate2::mod3,crate3::x=error/foo`) and return a vector with log
/// directives.
fn parse_spec(spec: &str) -> ParseResult {
let mut result = ParseResult::default();
let mut parts = spec.split('/');
let mods = parts.next();
if let Some(m) = mods {
for s in m.split(',').map(|ss| ss.trim()) {
if s.is_empty() {
continue;
}
let mut parts = s.split('=');
let (log_level, name) =
match (parts.next(), parts.next().map(|s| s.trim()), parts.next()) {
(Some(part0), None, None) => {
// if the single argument is a log-level string or number,
// treat that as a global fallback
match part0.parse() {
Ok(num) => (num, None),
Err(_) => (log::LevelFilter::max(), Some(part0)),
}
}
(Some(part0), Some(""), None) => (log::LevelFilter::max(), Some(part0)),
(Some(part0), Some(part1), None) => {
if let Ok(num) = part1.parse() {
(num, Some(part0))
} else {
result.add_error(format!("invalid logging spec '{part1}'"));
continue;
}
}
_ => {
result.add_error(format!("invalid logging spec '{s}'"));
continue;
}
};
result.add_directive(Directive {
name: name.map(|s| s.to_owned()),
level: log_level,
});
}
}
result
}
+89
View File
@@ -0,0 +1,89 @@
//! defmt global logger implementation.
// Implementation taken from defmt-rtt, with a custom framing prefix
#[cfg(feature = "critical-section")]
use critical_section::RestoreState;
use super::PrinterImpl;
/// Global logger lock.
#[cfg(feature = "critical-section")]
static mut TAKEN: bool = false;
#[cfg(feature = "critical-section")]
static mut CS_RESTORE: RestoreState = RestoreState::invalid();
static mut ENCODER: defmt::Encoder = defmt::Encoder::new();
#[defmt::global_logger]
pub struct Logger;
unsafe impl defmt::Logger for Logger {
fn acquire() {
#[cfg(feature = "critical-section")]
unsafe {
// safety: Must be paired with corresponding call to release(), see below
let restore = critical_section::acquire();
// safety: accessing the `static mut` is OK because we have acquired a critical
// section.
if TAKEN {
panic!("defmt logger taken reentrantly")
}
// safety: accessing the `static mut` is OK because we have acquired a critical
// section.
TAKEN = true;
// safety: accessing the `static mut` is OK because we have acquired a critical
// section.
CS_RESTORE = restore;
}
// If not disabled, write a non-UTF8 sequence to indicate the start of a defmt
// frame. We need this to distinguish defmt frames from other data that
// might be written to the printer.
do_write(&[0xFF, 0x00]);
// safety: accessing the `static mut` is OK because we have acquired a critical
// section.
unsafe { ENCODER.start_frame(do_write) }
}
unsafe fn release() {
// safety: accessing the `static mut` is OK because we have acquired a critical
// section.
ENCODER.end_frame(do_write);
Self::flush();
#[cfg(feature = "critical-section")]
{
// We don't need to write a custom end-of-frame sequence because:
// - using `defmt`, the rzcobs encoding already includes a terminating zero
// - using `defmt-raw`, we don't add any additional framing data
// safety: accessing the `static mut` is OK because we have acquired a critical
// section.
TAKEN = false;
// safety: accessing the `static mut` is OK because we have acquired a critical
// section.
let restore = CS_RESTORE;
// safety: Must be paired with corresponding call to acquire(), see above
critical_section::release(restore);
}
}
unsafe fn flush() {
PrinterImpl::flush();
}
unsafe fn write(bytes: &[u8]) {
// safety: accessing the `static mut` is OK because we have acquired a critical
// section.
ENCODER.write(bytes, do_write);
}
}
fn do_write(bytes: &[u8]) {
PrinterImpl::write_bytes_assume_cs(bytes)
}
+483
View File
@@ -0,0 +1,483 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/46717278")]
#![allow(rustdoc::bare_urls)]
#![no_std]
#[cfg(feature = "defmt-espflash")]
pub mod defmt;
#[cfg(feature = "log")]
pub mod logger;
/// Prints to the selected output, with a newline.
#[cfg(not(feature = "no-op"))]
#[macro_export]
macro_rules! println {
($($arg:tt)*) => {{
{
use core::fmt::Write;
writeln!($crate::Printer, $($arg)*).ok();
}
}};
}
/// Prints to the selected output.
#[cfg(not(feature = "no-op"))]
#[macro_export]
macro_rules! print {
($($arg:tt)*) => {{
{
use core::fmt::Write;
write!($crate::Printer, $($arg)*).ok();
}
}};
}
/// Prints to the configured output, with a newline.
#[cfg(feature = "no-op")]
#[macro_export]
macro_rules! println {
($($arg:tt)*) => {{}};
}
/// Prints to the configured output.
#[cfg(feature = "no-op")]
#[macro_export]
macro_rules! print {
($($arg:tt)*) => {{}};
}
/// Prints and returns the value of a given expression for quick and dirty
/// debugging.
// implementation adapted from `std::dbg`
#[macro_export]
macro_rules! dbg {
// NOTE: We cannot use `concat!` to make a static string as a format argument
// of `eprintln!` because `file!` could contain a `{` or
// `$val` expression could be a block (`{ .. }`), in which case the `println!`
// will be malformed.
() => {
$crate::println!("[{}:{}]", ::core::file!(), ::core::line!())
};
($val:expr $(,)?) => {
// Use of `match` here is intentional because it affects the lifetimes
// of temporaries - https://stackoverflow.com/a/48732525/1063961
match $val {
tmp => {
$crate::println!("[{}:{}] {} = {:#?}",
::core::file!(), ::core::line!(), ::core::stringify!($val), &tmp);
tmp
}
}
};
($($val:expr),+ $(,)?) => {
($($crate::dbg!($val)),+,)
};
}
/// The printer that is used by the `print!` and `println!` macros.
pub struct Printer;
impl core::fmt::Write for Printer {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
Printer::write_bytes(s.as_bytes());
Ok(())
}
}
impl Printer {
/// Writes a byte slice to the configured output.
pub fn write_bytes(bytes: &[u8]) {
with(|| {
PrinterImpl::write_bytes_assume_cs(bytes);
PrinterImpl::flush();
})
}
}
#[cfg(feature = "jtag-serial")]
type PrinterImpl = serial_jtag_printer::Printer;
#[cfg(feature = "uart")]
type PrinterImpl = uart_printer::Printer;
#[cfg(feature = "auto")]
type PrinterImpl = auto_printer::Printer;
#[cfg(all(
feature = "auto",
any(
feature = "esp32c3",
feature = "esp32c6",
feature = "esp32h2",
feature = "esp32p4", // as mentioned in 'build.rs'
feature = "esp32s3"
)
))]
mod auto_printer {
use super::with;
use crate::{
serial_jtag_printer::Printer as PrinterSerialJtag,
uart_printer::Printer as PrinterUart,
};
pub struct Printer;
impl Printer {
fn use_jtag() -> bool {
// Decide if serial-jtag is used by checking SOF interrupt flag.
// SOF packet is sent by the HOST every 1ms on a full speed bus.
// Between two consecutive ticks, there will be at least 1ms (selectable tick
// rate range is 1 - 1000Hz).
// We don't reset the flag - if it was ever connected we assume serial-jtag is
// used
#[cfg(feature = "esp32c3")]
const USB_DEVICE_INT_RAW: *const u32 = 0x60043008 as *const u32;
#[cfg(feature = "esp32c6")]
const USB_DEVICE_INT_RAW: *const u32 = 0x6000f008 as *const u32;
#[cfg(feature = "esp32h2")]
const USB_DEVICE_INT_RAW: *const u32 = 0x6000f008 as *const u32;
#[cfg(feature = "esp32p4")]
const USB_DEVICE_INT_RAW: *const u32 = unimplemented!();
#[cfg(feature = "esp32s3")]
const USB_DEVICE_INT_RAW: *const u32 = 0x60038000 as *const u32;
const SOF_INT_MASK: u32 = 0b10;
unsafe { (USB_DEVICE_INT_RAW.read_volatile() & SOF_INT_MASK) != 0 }
}
pub fn write_bytes_assume_cs(bytes: &[u8]) {
if Self::use_jtag() {
with(|| {
PrinterSerialJtag::write_bytes_assume_cs(bytes);
})
} else {
with(|| {
PrinterUart::write_bytes_assume_cs(bytes);
})
}
}
pub fn flush() {
if Self::use_jtag() {
with(|| {
PrinterSerialJtag::flush();
})
} else {
with(|| {
PrinterUart::flush();
})
}
}
}
}
#[cfg(all(
feature = "auto",
not(any(
feature = "esp32c3",
feature = "esp32c6",
feature = "esp32h2",
feature = "esp32p4",
feature = "esp32s3"
))
))]
mod auto_printer {
// models that only have UART
pub type Printer = crate::uart_printer::Printer;
}
#[cfg(all(
any(feature = "jtag-serial", feature = "auto"),
any(
feature = "esp32c3",
feature = "esp32c6",
feature = "esp32h2",
feature = "esp32p4",
feature = "esp32s3"
)
))]
mod serial_jtag_printer {
use portable_atomic::{AtomicBool, Ordering};
pub struct Printer;
#[cfg(feature = "esp32c3")]
const SERIAL_JTAG_FIFO_REG: usize = 0x6004_3000;
#[cfg(feature = "esp32c3")]
const SERIAL_JTAG_CONF_REG: usize = 0x6004_3004;
#[cfg(any(feature = "esp32c6", feature = "esp32h2"))]
const SERIAL_JTAG_FIFO_REG: usize = 0x6000_F000;
#[cfg(any(feature = "esp32c6", feature = "esp32h2"))]
const SERIAL_JTAG_CONF_REG: usize = 0x6000_F004;
#[cfg(feature = "esp32p4")]
const SERIAL_JTAG_FIFO_REG: usize = 0x500D_2000;
#[cfg(feature = "esp32p4")]
const SERIAL_JTAG_CONF_REG: usize = 0x500D_2004;
#[cfg(feature = "esp32s3")]
const SERIAL_JTAG_FIFO_REG: usize = 0x6003_8000;
#[cfg(feature = "esp32s3")]
const SERIAL_JTAG_CONF_REG: usize = 0x6003_8004;
/// A previous wait has timed out. We use this flag to avoid blocking
/// forever if there is no host attached.
static TIMED_OUT: AtomicBool = AtomicBool::new(false);
fn fifo_flush() {
let conf = SERIAL_JTAG_CONF_REG as *mut u32;
unsafe { conf.write_volatile(0b001) };
}
fn fifo_full() -> bool {
let conf = SERIAL_JTAG_CONF_REG as *mut u32;
unsafe { conf.read_volatile() & 0b010 == 0b000 }
}
fn fifo_write(byte: u8) {
let fifo = SERIAL_JTAG_FIFO_REG as *mut u32;
unsafe { fifo.write_volatile(byte as u32) }
}
fn wait_for_flush() -> bool {
const TIMEOUT_ITERATIONS: usize = 50_000;
// Wait for some time for the FIFO to clear.
let mut timeout = TIMEOUT_ITERATIONS;
while fifo_full() {
if timeout == 0 {
TIMED_OUT.store(true, Ordering::Relaxed);
return false;
}
timeout -= 1;
}
true
}
impl Printer {
pub fn write_bytes_assume_cs(bytes: &[u8]) {
if fifo_full() {
// The FIFO is full. Let's see if we can progress.
if TIMED_OUT.load(Ordering::Relaxed) {
// Still wasn't able to drain the FIFO. Let's assume we won't be able to, and
// don't queue up more data.
// This is important so we don't block forever if there is no host attached.
return;
}
// Give the fifo some time to drain.
if !wait_for_flush() {
return;
}
} else {
// Reset the flag - we managed to clear our FIFO.
TIMED_OUT.store(false, Ordering::Relaxed);
}
for &b in bytes {
if fifo_full() {
fifo_flush();
// Wait for the FIFO to clear, we have more data to shift out.
if !wait_for_flush() {
return;
}
}
fifo_write(b);
}
}
pub fn flush() {
fifo_flush();
}
}
}
#[cfg(all(any(feature = "uart", feature = "auto"), feature = "esp32"))]
mod uart_printer {
const UART_TX_ONE_CHAR: usize = 0x4000_9200;
pub struct Printer;
impl Printer {
pub fn write_bytes_assume_cs(bytes: &[u8]) {
for &b in bytes {
unsafe {
let uart_tx_one_char: unsafe extern "C" fn(u8) -> i32 =
core::mem::transmute(UART_TX_ONE_CHAR);
uart_tx_one_char(b)
};
}
}
pub fn flush() {}
}
}
#[cfg(all(any(feature = "uart", feature = "auto"), feature = "esp32s2"))]
mod uart_printer {
pub struct Printer;
impl Printer {
pub fn write_bytes_assume_cs(bytes: &[u8]) {
// On ESP32-S2 the UART_TX_ONE_CHAR ROM-function seems to have some issues.
for chunk in bytes.chunks(64) {
for &b in chunk {
unsafe {
// write FIFO
(0x3f400000 as *mut u32).write_volatile(b as u32);
};
}
// wait for TX_DONE
while unsafe { (0x3f400004 as *const u32).read_volatile() } & (1 << 14) == 0 {}
unsafe {
// reset TX_DONE
(0x3f400010 as *mut u32).write_volatile(1 << 14);
}
}
}
pub fn flush() {}
}
}
#[cfg(all(
any(feature = "uart", feature = "auto"),
not(any(feature = "esp32", feature = "esp32s2"))
))]
mod uart_printer {
trait Functions {
const TX_ONE_CHAR: usize;
const CHUNK_SIZE: usize = 32;
fn tx_byte(b: u8) {
unsafe {
let tx_one_char: unsafe extern "C" fn(u8) -> i32 =
core::mem::transmute(Self::TX_ONE_CHAR);
tx_one_char(b);
}
}
fn flush();
}
struct Device;
#[cfg(feature = "esp32c2")]
impl Functions for Device {
const TX_ONE_CHAR: usize = 0x4000_005C;
fn flush() {
// tx_one_char waits for empty
}
}
#[cfg(feature = "esp32c3")]
impl Functions for Device {
const TX_ONE_CHAR: usize = 0x4000_0068;
fn flush() {
unsafe {
const TX_FLUSH: usize = 0x4000_0080;
const GET_CHANNEL: usize = 0x4000_058C;
let tx_flush: unsafe extern "C" fn(u8) = core::mem::transmute(TX_FLUSH);
let get_channel: unsafe extern "C" fn() -> u8 = core::mem::transmute(GET_CHANNEL);
const G_USB_PRINT_ADDR: usize = 0x3FCD_FFD0;
let g_usb_print = G_USB_PRINT_ADDR as *mut bool;
let channel = if *g_usb_print {
// Flush USB-JTAG
3
} else {
get_channel()
};
tx_flush(channel);
}
}
}
#[cfg(feature = "esp32s3")]
impl Functions for Device {
const TX_ONE_CHAR: usize = 0x4000_0648;
fn flush() {
unsafe {
const TX_FLUSH: usize = 0x4000_0690;
const GET_CHANNEL: usize = 0x4000_1A58;
let tx_flush: unsafe extern "C" fn(u8) = core::mem::transmute(TX_FLUSH);
let get_channel: unsafe extern "C" fn() -> u8 = core::mem::transmute(GET_CHANNEL);
const G_USB_PRINT_ADDR: usize = 0x3FCE_FFB8;
let g_usb_print = G_USB_PRINT_ADDR as *mut bool;
let channel = if *g_usb_print {
// Flush USB-JTAG
4
} else {
get_channel()
};
tx_flush(channel);
}
}
}
#[cfg(any(feature = "esp32c6", feature = "esp32h2"))]
impl Functions for Device {
const TX_ONE_CHAR: usize = 0x4000_0058;
fn flush() {
unsafe {
const TX_FLUSH: usize = 0x4000_0074;
const GET_CHANNEL: usize = 0x4000_003C;
let tx_flush: unsafe extern "C" fn(u8) = core::mem::transmute(TX_FLUSH);
let get_channel: unsafe extern "C" fn() -> u8 = core::mem::transmute(GET_CHANNEL);
tx_flush(get_channel());
}
}
}
#[cfg(feature = "esp32p4")]
impl Functions for Device {
const TX_ONE_CHAR: usize = 0x4FC0_0054;
fn flush() {
unsafe {
const TX_FLUSH: usize = 0x4FC0_0074;
const GET_CHANNEL: usize = 0x4FC0_0038;
let tx_flush: unsafe extern "C" fn(u8) = core::mem::transmute(TX_FLUSH);
let get_channel: unsafe extern "C" fn() -> u8 = core::mem::transmute(GET_CHANNEL);
tx_flush(get_channel());
}
}
}
pub struct Printer;
impl Printer {
pub fn write_bytes_assume_cs(bytes: &[u8]) {
for chunk in bytes.chunks(Device::CHUNK_SIZE) {
for &b in chunk {
Device::tx_byte(b);
}
Device::flush();
}
}
pub fn flush() {}
}
}
#[inline]
fn with<R>(f: impl FnOnce() -> R) -> R {
#[cfg(feature = "critical-section")]
return critical_section::with(|_| f());
#[cfg(not(feature = "critical-section"))]
f()
}
+69
View File
@@ -0,0 +1,69 @@
use super::println;
#[cfg(not(host_is_windows))]
include!(concat!(env!("OUT_DIR"), "/log_filter.rs"));
#[cfg(host_is_windows)]
include!(concat!(env!("OUT_DIR"), "\\log_filter.rs"));
/// Initialize the logger with the given maximum log level.
///
/// `ESP_LOG` environment variable will still be honored if set.
pub fn init_logger(level: log::LevelFilter) {
unsafe {
log::set_logger_racy(&EspLogger).unwrap();
log::set_max_level_racy(level);
}
}
/// Initialize the logger from the `ESP_LOG` environment variable.
pub fn init_logger_from_env() {
unsafe {
log::set_logger_racy(&EspLogger).unwrap();
log::set_max_level_racy(FILTER_MAX);
}
}
struct EspLogger;
impl log::Log for EspLogger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
let level = metadata.level();
let target = metadata.target();
is_enabled(level, target)
}
#[allow(unused)]
fn log(&self, record: &log::Record) {
if !self.enabled(&record.metadata()) {
return;
}
const RESET: &str = "\u{001B}[0m";
const RED: &str = "\u{001B}[31m";
const GREEN: &str = "\u{001B}[32m";
const YELLOW: &str = "\u{001B}[33m";
const BLUE: &str = "\u{001B}[34m";
const CYAN: &str = "\u{001B}[35m";
#[cfg(feature = "colors")]
let color = match record.level() {
log::Level::Error => RED,
log::Level::Warn => YELLOW,
log::Level::Info => GREEN,
log::Level::Debug => BLUE,
log::Level::Trace => CYAN,
};
#[cfg(feature = "colors")]
let reset = RESET;
#[cfg(not(feature = "colors"))]
let color = "";
#[cfg(not(feature = "colors"))]
let reset = "";
println!("{}{} - {}{}", color, record.level(), record.args(), reset);
}
fn flush(&self) {}
}