move callback_list, parse_buffer, prepend_buffer, static_vector, timer_queue into util; util links pico_stdlib for timer_queue

This commit is contained in:
Ian Gulliver
2026-05-01 10:50:15 -07:00
parent bcddb14acf
commit cc1448d6a2
6 changed files with 1 additions and 0 deletions

View File

@@ -1,2 +1,3 @@
add_library(util INTERFACE)
target_include_directories(util INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(util INTERFACE pico_stdlib)

83
util/callback_list.h Normal file
View File

@@ -0,0 +1,83 @@
#pragma once
#include <utility>
// Fixed-capacity doubly-linked list with an embedded node array and intrusive
// free list. Used for callback registries (frame callbacks, timers) needing
// O(1) insert/remove with stable handles, without heap in IRQ-adjacent paths.
template <typename T, int N>
struct callback_list {
struct node {
T value;
node* prev = nullptr;
node* next = nullptr;
};
node nodes[N];
node* free_head = &nodes[0];
node* head = nullptr;
callback_list() {
for (int i = 0; i < N - 1; i++) nodes[i].next = &nodes[i + 1];
nodes[N - 1].next = nullptr;
}
bool empty() const { return head == nullptr; }
node* insert(T value) {
if (!free_head) return nullptr;
node* n = free_head;
free_head = n->next;
n->value = std::move(value);
n->prev = nullptr;
n->next = head;
if (head) head->prev = n;
head = n;
return n;
}
template <typename Less>
node* insert_sorted(T value, Less&& less) {
if (!free_head) return nullptr;
node* n = free_head;
free_head = n->next;
n->value = std::move(value);
if (!head || less(n->value, head->value)) {
n->prev = nullptr;
n->next = head;
if (head) head->prev = n;
head = n;
return n;
}
node* cur = head;
while (cur->next && !less(n->value, cur->next->value))
cur = cur->next;
n->prev = cur;
n->next = cur->next;
if (cur->next) cur->next->prev = n;
cur->next = n;
return n;
}
void remove(node* n) {
if (!n) return;
if (n->prev) n->prev->next = n->next;
else head = n->next;
if (n->next) n->next->prev = n->prev;
n->value = T{};
n->next = free_head;
n->prev = nullptr;
free_head = n;
}
node* front() { return head; }
template <typename Fn>
void for_each(Fn&& fn) {
node* cur = head;
while (cur) {
node* next = cur->next;
fn(cur);
cur = next;
}
}
};

35
util/parse_buffer.h Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <span>
// Bounds-checked sequential reader for byte streams. consume<T>() returns a
// typed pointer into the underlying buffer or nullptr on underflow. Used to
// walk packet headers in order without UB and without copies.
class parse_buffer {
const uint8_t* m_data;
size_t m_remaining;
public:
parse_buffer(std::span<const uint8_t> data)
: m_data(data.data()), m_remaining(data.size()) {}
template <typename T>
const T* consume() {
if (m_remaining < sizeof(T)) return nullptr;
auto* p = reinterpret_cast<const T*>(m_data);
m_data += sizeof(T);
m_remaining -= sizeof(T);
return p;
}
bool skip(size_t len) {
if (m_remaining < len) return false;
m_data += len;
m_remaining -= len;
return true;
}
std::span<const uint8_t> remaining() const { return {m_data, m_remaining}; }
size_t remaining_size() const { return m_remaining; }
};

41
util/prepend_buffer.h Normal file
View File

@@ -0,0 +1,41 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <span>
// Buffer that grows in both directions from a midpoint. Payload is appended at
// the back; each protocol layer then prepends its header in reverse order
// (UDP, then IPv4, then Ethernet) without moving payload bytes. Lets the
// network stack build outbound frames bottom-up with no copies or scratch.
template <size_t N>
class prepend_buffer {
uint8_t m_buf[N];
size_t m_start = N / 2;
size_t m_end = N / 2;
public:
template <typename T>
T* prepend() {
m_start -= sizeof(T);
return reinterpret_cast<T*>(m_buf + m_start);
}
uint8_t* append(size_t len) {
uint8_t* p = m_buf + m_end;
m_end += len;
return p;
}
void append_copy(std::span<const uint8_t> data) {
memcpy(append(data.size()), data.data(), data.size());
}
uint8_t* payload_ptr() { return m_buf + m_end; }
uint8_t* data() { return m_buf + m_start; }
const uint8_t* data() const { return m_buf + m_start; }
size_t size() const { return m_end - m_start; }
std::span<const uint8_t> span() const { return {data(), size()}; }
};

36
util/static_vector.h Normal file
View File

@@ -0,0 +1,36 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
// Fixed-capacity contiguous container: std::vector's interface with std::array's
// storage. Used for response payloads sized at parse time but bounded at compile
// time, where heap allocation is unavailable or undesirable.
template <typename T, size_t Capacity>
class static_vector {
T m_data[Capacity];
size_t m_size = 0;
public:
void push_back(const T &v) {
if (m_size < Capacity) m_data[m_size++] = v;
}
void clear() { m_size = 0; }
size_t size() const { return m_size; }
size_t capacity() const { return Capacity; }
bool full() const { return m_size >= Capacity; }
bool empty() const { return m_size == 0; }
T *data() { return m_data; }
const T *data() const { return m_data; }
T &operator[](size_t i) { return m_data[i]; }
const T &operator[](size_t i) const { return m_data[i]; }
T *begin() { return m_data; }
T *end() { return m_data + m_size; }
const T *begin() const { return m_data; }
const T *end() const { return m_data + m_size; }
};

66
util/timer_queue.h Normal file
View File

@@ -0,0 +1,66 @@
#pragma once
#include "pico/time.h"
#include "callback_list.h"
struct timer_entry {
absolute_time_t when;
void (*fn)() = nullptr;
};
using timer_handle = callback_list<timer_entry, 16>::node*;
// Deadline-sorted callback list with a single hardware alarm armed at the
// head. The alarm IRQ sets a flag; run() drains expired entries on the main
// loop, so callbacks execute in normal context rather than interrupt context.
struct timer_queue {
callback_list<timer_entry, 16> list;
alarm_id_t alarm = -1;
volatile bool irq_pending = false;
timer_handle schedule(absolute_time_t when, void (*fn)()) {
auto* n = list.insert_sorted({when, fn},
[](const timer_entry& a, const timer_entry& b) {
return absolute_time_diff_us(b.when, a.when) < 0;
});
arm();
return n;
}
timer_handle schedule_ms(uint32_t ms, void (*fn)()) {
return schedule(make_timeout_time_ms(ms), fn);
}
bool cancel(timer_handle h) {
if (!h) return false;
list.remove(h);
arm();
return true;
}
void run() {
if (!irq_pending) return;
irq_pending = false;
while (auto* n = list.front()) {
if (absolute_time_diff_us(get_absolute_time(), n->value.when) > 0) break;
auto fn = n->value.fn;
list.remove(n);
fn();
}
arm();
}
bool empty() const { return list.empty(); }
private:
static int64_t alarm_cb(alarm_id_t, void* user_data) {
static_cast<timer_queue*>(user_data)->irq_pending = true;
return 0;
}
void arm() {
if (alarm >= 0) cancel_alarm(alarm);
alarm = -1;
if (auto* n = list.front())
alarm = add_alarm_at(n->value.when, alarm_cb, this, false);
}
};