Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ target = "riscv32imc-esp-espidf"
[target.'cfg(target_os = "espidf")']
linker = "ldproxy"
runner = "espflash flash --monitor"
rustflags = [ "--cfg", "espidf_time64"]
rustflags = [
"--cfg", "espidf_time64",
"-C", "force-frame-pointers=yes"
]

[unstable]
build-std = ["std", "panic_abort"]
Expand All @@ -13,6 +16,7 @@ build-std = ["std", "panic_abort"]
MCU = "esp32c3"
ESP_IDF_VERSION = "v5.5.3"
ESP_IDF_TOOLS_INSTALL_DIR = "workspace"
RUST_BACKTRACE = "1"

# Uncomment this if you have moved the target dir by setting CARGO_TARGET_DIR or similar.
# Required for now due to embuild limitations.
Expand Down
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,13 @@ cfor = "1.1.0"

heapless = "0.9.3" # (::esp-idf-svc)

time = { version = "0.3.55", features = ["default", "serde"] }
time = { version = "0.3.55", features = ["default", "serde", "formatting", "macros"] }

embassy-time = { version = "0.5", features = ["generic-queue-8"] }

paste = "1.0.15"

itertools = "0.13.0"

[build-dependencies]
embuild = "0.33"
4 changes: 4 additions & 0 deletions ns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env python3

import time
print(time.time_ns() - (time.timezone - time.daylight * 3600) * 1_000_000_000)
16 changes: 8 additions & 8 deletions pin_allocations.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
0: Voltage measurement
1: Current measurement
0:
1: Stepper UART RX + TX
2: Onboard ARGB LED
3: Leak detection
4: SD SPI CLK
5: SD SPI MOSI
4: SD SPI MOSI
5: SD SPI CLK
6: SD SPI MISO
7: SD SPI CS (has onboard LED)
8: I2C SCL
9: Onboard button
10: I2C SDA
18: Stepper DIR
19: Stepper STEP
20: Stepper UART RX
21: Stepper UART TX
18: [USB]
19: [USB]
20: Stepper DIR
21: Stepper STEP
12 changes: 9 additions & 3 deletions sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Rust often needs a bit of an extra main task stack size compared to C (the default is 3K)
# You might have to increase this further if you allocate large stack variables in the main task
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
# Because we immediately spawn a new thread, the main thread memory usage is pretty small.
# I believe that it tops out at 1332 bytes
CONFIG_ESP_MAIN_TASK_STACK_SIZE=1400

# Increase a bit these stack sizes as they are also a bit too small by default
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
Expand All @@ -10,3 +10,9 @@ CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=4096
# that allocate large stack variables; or better yet - use
# `std::thread::Builder::new().stack_size(XXX)` for spawning
CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=4096

# Note that you need to prepend `CONFIG_` to the names you see in the Kconfig listings

CONFIG_ESP_SYSTEM_USE_FRAME_POINTER=y
CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y
CONFIG_FREERTOS_USE_TRACE_FACILITY=y
24 changes: 24 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use crate::charter::Charter;

use serde::{Deserialize, Serialize};

use heapless::String as HeaplessString;

#[derive(Deserialize, Serialize, Debug)]
pub struct SystemConfig {
pub wifi_ssid: HeaplessString<32>,
pub wifi_pass: HeaplessString<64>,
pub charter: Charter,
pub profiling: bool,
}

impl Default for SystemConfig {
fn default() -> Self {
SystemConfig {
wifi_ssid: "ESP".try_into().unwrap(), // Will never fail; less than character limit
wifi_pass: "floatware".try_into().unwrap(), // ditto
charter: Default::default(),
profiling: false,
}
}
}
227 changes: 227 additions & 0 deletions src/debugging.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
//! Some useful functions:
//! esp_get_free_heap_size()
//! esp_get_free_heap_size()
//! esp_get_minimum_free_heap_size()
//! heap_caps_get_total_size(caps)
//! heap_caps_get_free_size(caps)
//! heap_caps_get_minimum_free_size(caps)
//! heap_caps_get_largest_free_block(caps)
//! heap_caps_get_info(&mut multi_heap_info_t, caps)

use crate::{get_float_thread_handle, prelude::*, tasks::i2c::get_i2c_thread_handle};

use std::{
collections::HashMap,
ffi::CStr,
future::{poll_fn, Future},
num::Saturating,
pin::pin,
ptr::null_mut,
sync::RwLock,
time::{Duration, Instant}
};

use esp_idf_svc::sys::{
eTaskState,
eTaskState_eBlocked,
eTaskState_eDeleted,
eTaskState_eInvalid,
eTaskState_eReady,
eTaskState_eRunning,
eTaskState_eSuspended,
uxTaskGetNumberOfTasks,
uxTaskGetStackHighWaterMark,
uxTaskGetSystemState,
xTASK_STATUS,
TaskHandle_t
};

use serde::Serialize;

////////////////////////////////////////////////////////////////////////////////

#[derive(Debug, Serialize)]
pub struct TaskInfo {
#[serde(skip_serializing)]
pub handle: TaskHandle_t,
pub name: String,
pub task_number: u32,
pub state: TaskState,
pub priority: u32,
pub run_time_counter: u32,
pub stack_high_water_mark: u32,
}

impl From<xTASK_STATUS> for TaskInfo {
fn from(task: xTASK_STATUS) -> Self {
let name = String::from(match task.xHandle {
h if h == get_float_thread_handle() => "<float-thread>",
h if h == get_i2c_thread_handle () => "<i2c-thread>",
_ => unsafe { CStr::from_ptr(task.pcTaskName) }.to_str()
.unwrap_or("<utf8-error-in-task-name>")
});

Self {
handle: task.xHandle,
name,
task_number: task.xTaskNumber,
state: task.eCurrentState.into(),
priority: task.uxCurrentPriority,
run_time_counter: task.ulRunTimeCounter,
stack_high_water_mark: task.usStackHighWaterMark,
}
}
}

#[derive(Debug, Serialize)]
pub enum TaskState {
Running,
Ready,
Blocked,
Suspended,
Deleted,
Invalid
}

impl From<eTaskState> for TaskState {
fn from(value: eTaskState) -> Self {
#[allow(non_upper_case_globals)]
match value {
eTaskState_eRunning => Self::Running,
eTaskState_eReady => Self::Ready,
eTaskState_eBlocked => Self::Blocked,
eTaskState_eSuspended => Self::Suspended,
eTaskState_eDeleted => Self::Deleted,
eTaskState_eInvalid | _ => Self::Invalid,
}
}
}

////////////////////////////////////////////////////////////////////////////////

/// Return the minimum recorded amount of stack space (in bytes) remaining for the
/// provided task handle, or the caller thread if null.
#[inline] pub fn high_water_mark(handle: TaskHandle_t) -> u32 {
not_unsafe! { uxTaskGetStackHighWaterMark(handle) }
}

/// Return the minimum recorded amount of stack space (in bytes) remaining for the
/// caller thread. Don't call this from an interrupt.
#[inline] pub fn high_water_mark_caller() -> u32 { high_water_mark(null_mut()) }

/// Returns a list of [xTASK_STATUS] objects, one for each existing FreeRTOS task.
///
/// This function takes a long time to call.
///
/// Note that in the extremely unlikely circumstance that a task is created between
/// the time the number of tasks is measured and the vector is populated, the
/// underlying C function will error and this function will return an empty vector.
fn get_task_status_list() -> Vec<xTASK_STATUS> {
let num_tasks = not_unsafe! { uxTaskGetNumberOfTasks() };

let mut buf = Vec::with_capacity(num_tasks as _);

unsafe {
let populated = uxTaskGetSystemState(buf.as_mut_ptr(), num_tasks, null_mut());
buf.set_len(populated as _);
}
buf
}

pub fn iter_tasks() -> impl Iterator<Item = TaskInfo> {
get_task_status_list().into_iter().map(TaskInfo::from)
}

pub fn get_tasks() -> Vec<TaskInfo> {
let mut vec = iter_tasks().collect::<Vec<_>>();
vec.sort_by_key(|task_info| task_info.task_number);
vec
}

fn print_task_info() {
info!("{:#?}", get_tasks().as_slice());
}

////////////////////////////////////////////////////////////////////////////////

#[derive(Default, Debug, Serialize)]
pub struct FutureStats {
polls: Saturating<u32>,
total_time: Duration,
longest_time: Duration,
}

pub type ProfilingData = HashMap<&'static str, RwLock<FutureStats>>;

static mut PROFILING_DATA: Option<ProfilingData> = None;

/// If profiling is enabled, return a reference to the profiling data map.
#[inline] pub fn get_profiling_data() -> Option<&'static mut ProfilingData> {
unsafe { PROFILING_DATA.as_mut() }
}

#[inline] fn profiling_enabled() -> bool {
unsafe { PROFILING_DATA.is_some() }
}

/// Enable profiling by populating the profiling data hashmap.
pub fn enable_profiling() {
unsafe {
if let None = PROFILING_DATA {
PROFILING_DATA = Some(Default::default());
}
}
}

/// Wrap polling calls to the passed future and profile how long they take.
///
/// If the provided stats struct is None, then the provided future is transparently
/// executed and no profiling will be performed.
pub async fn profile<F: Future>(future: F, stats: Option<&mut RwLock<FutureStats>>) -> F::Output {
if let Some(stats) = stats {
// Profiling enabled
let mut future = pin!(future);
let mut lock_ok = true;

poll_fn(|cx| {
let start = Instant::now();

let result = future.as_mut().poll(cx);

let elapsed = start.elapsed();

if lock_ok {
match stats.get_mut() {
Ok(current) => {
current.polls += 1;
current.total_time = current.total_time.saturating_add(elapsed);
current.longest_time = current.longest_time.max(elapsed);
}
Err(poison) => {
error!("Profiling lock poisoned: {poison:?}");
lock_ok = false;
},
}
}

result
}).await
} else {
// Profiling disabled
future.await
}

}

/// Wrap the passed future in a profiler and return it. If profiling is not enabled,
/// then the future will be executed normally.
#[macro_export]
macro_rules! profile {
($name:ident($($arg:tt)*)) => {{
let stats = crate::debugging::get_profiling_data().and_then(|map|
map.try_insert(stringify!($name), Default::default()).ok()
);

crate::debugging::profile($name($($arg)*), stats)
}};
}
Loading
Loading