diff --git a/CMakePresets.json b/CMakePresets.json
index 7409241..3c2f1f5 100644
--- a/CMakePresets.json
+++ b/CMakePresets.json
@@ -147,6 +147,7 @@
"uart_host_mock_example",
"test_ring_buffer",
"test_cli",
+ "test_prio_queue",
"test_timeout_pattern",
"uart_host_mock_example"
]
@@ -162,6 +163,7 @@
"test_bsp_button",
"test_bsp_can",
"test_cli",
+ "test_prio_queue",
"uart_host_mock_example",
"test_ring_buffer",
"test_timeout_pattern",
diff --git a/bsp/button/README.md b/bsp/button/README.md
index 0f8ee89..d667710 100644
--- a/bsp/button/README.md
+++ b/bsp/button/README.md
@@ -59,7 +59,7 @@ bsp_button_init();
bsp_button_poll();
/* В основном цикле: */
-if (bsp_button_get_event_pressed(BSP_BUTTON_1)) {
+if (**bsp_button_get_event_pressed(BSP_BUTTON_1)**) {
/* однократное срабатывание по нажатию */
}
diff --git a/bsp/display/CMakeLists.txt b/bsp/display/CMakeLists.txt
new file mode 100644
index 0000000..e69de29
diff --git a/bsp/display/README.md b/bsp/display/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/bsp/opto/include/bsp/opto.h b/bsp/opto/include/bsp/opto.h
index a31d9a7..e0b98a8 100644
--- a/bsp/opto/include/bsp/opto.h
+++ b/bsp/opto/include/bsp/opto.h
@@ -198,6 +198,7 @@ extern "C"
* Должна вызываться из main loop на каждой итерации. НЕ вызывать из ISR.
* Каналы MODE_PROTO пропускаются — для них используется коллбэк из ISR.
*/
+ //FIXME: poll
void bsp_opto_process(void);
/**
diff --git a/firmware/test/arch.svg b/firmware/test/arch.svg
new file mode 100644
index 0000000..da527f6
--- /dev/null
+++ b/firmware/test/arch.svg
@@ -0,0 +1,268 @@
+
diff --git a/tests/host/CMakeLists.txt b/tests/host/CMakeLists.txt
index d92e7af..50db4e8 100644
--- a/tests/host/CMakeLists.txt
+++ b/tests/host/CMakeLists.txt
@@ -173,3 +173,15 @@ add_host_test(
${CMAKE_SOURCE_DIR}/bsp/usb_cdc/include
MOCKS
${BSP_MOCKS_DIR})
+
+# -----------------------------------------------------------------------------
+# utils/prio_queue
+# -----------------------------------------------------------------------------
+add_host_test(
+ NAME
+ test_prio_queue
+ SOURCES
+ prio_queue/test_prio_queue.c
+ ${PROJECT_SOURCE_DIR}/utils/prio_queue/prio_queue.c
+ INCLUDES
+ ${PROJECT_SOURCE_DIR}/utils)
diff --git a/tests/host/prio_queue/test_prio_queue.c b/tests/host/prio_queue/test_prio_queue.c
new file mode 100644
index 0000000..a29773d
--- /dev/null
+++ b/tests/host/prio_queue/test_prio_queue.c
@@ -0,0 +1,601 @@
+/**
+ * @file test_prio_queue.c
+ * @brief Host unit-тесты для prio_queue (Категория A — без NXP SDK).
+ *
+ * Фреймворк: Unity.
+ * Запуск: ctest --preset host-debug-test -R test_prio_queue -V
+ *
+ * Проверяемый инвариант:
+ * - peek()/remove_at(0) всегда отдаёт элемент с наивысшим приоритетом.
+ * - При равных приоритетах — FIFO (первым вошёл, первым вышел).
+ */
+
+#include "prio_queue/prio_queue.h"
+#include "unity.h"
+
+#include
+#include
+#include
+/* ── Тестовые типы ─────────────────────────────────────────────────────── */
+
+typedef struct
+{
+ int priority;
+ const char *name;
+} task_t;
+
+/* cmp: меньший priority → ближе к голове (выше приоритет) */
+static int task_cmp(const void *p_a, const void *p_b)
+{
+ int pa = ((const task_t *) p_a)->priority;
+ int pb = ((const task_t *) p_b)->priority;
+ return (pa < pb) ? -1 : (pa > pb) ? 1 : 0;
+}
+
+/* ── Общие фикстуры ────────────────────────────────────────────────────── */
+
+#define CAPACITY 8U
+
+static task_t s_storage[CAPACITY];
+static prio_queue_t s_q;
+
+void setUp(void)
+{
+ memset(s_storage, 0, sizeof(s_storage));
+ prio_queue_init(&s_q, s_storage, CAPACITY, sizeof(task_t), task_cmp);
+}
+
+void tearDown(void)
+{
+}
+
+/* ══════════════════════════════════════════════════════════════════════════
+ * Группа: init
+ * ══════════════════════════════════════════════════════════════════════════ */
+
+void test_init_size_is_zero(void)
+{
+ TEST_ASSERT_EQUAL_UINT8(0U, prio_queue_size(&s_q));
+}
+
+void test_init_peek_returns_null(void)
+{
+ TEST_ASSERT_NULL(prio_queue_peek(&s_q));
+}
+
+void test_init_at_returns_null_when_empty(void)
+{
+ TEST_ASSERT_NULL(prio_queue_at(&s_q, 0U));
+}
+
+/* ══════════════════════════════════════════════════════════════════════════
+ * Группа: insert_order — правильная сортировка
+ * ══════════════════════════════════════════════════════════════════════════ */
+
+void test_insert_single_element(void)
+{
+ task_t t = { .priority = 5, .name = "t5" };
+ pq_status_t ret = prio_queue_insert(&s_q, &t);
+
+ TEST_ASSERT_EQUAL(PQ_OK, ret);
+ TEST_ASSERT_EQUAL_UINT8(1U, prio_queue_size(&s_q));
+}
+
+void test_insert_two_ascending_order_peek_is_higher(void)
+{
+ /* Вставляем менее приоритетный первым */
+ task_t low = { .priority = 10, .name = "low" };
+ task_t high = { .priority = 1, .name = "high" };
+
+ prio_queue_insert(&s_q, &low);
+ prio_queue_insert(&s_q, &high);
+
+ const task_t *p_top = (const task_t *) prio_queue_peek(&s_q);
+ TEST_ASSERT_NOT_NULL(p_top);
+ TEST_ASSERT_EQUAL_STRING("high", p_top->name);
+}
+
+void test_insert_descending_input_peek_is_still_highest(void)
+{
+ task_t a = { .priority = 3, .name = "a" };
+ task_t b = { .priority = 2, .name = "b" };
+ task_t c = { .priority = 1, .name = "c" };
+
+ /* Вставляем в убывающем порядке приоритета */
+ prio_queue_insert(&s_q, &a);
+ prio_queue_insert(&s_q, &b);
+ prio_queue_insert(&s_q, &c);
+
+ const task_t *p_top = (const task_t *) prio_queue_peek(&s_q);
+ TEST_ASSERT_NOT_NULL(p_top);
+ TEST_ASSERT_EQUAL_STRING("c", p_top->name);
+}
+
+void test_insert_ascending_input_order_preserved(void)
+{
+ task_t a = { .priority = 1, .name = "a" };
+ task_t b = { .priority = 2, .name = "b" };
+ task_t c = { .priority = 3, .name = "c" };
+
+ prio_queue_insert(&s_q, &a);
+ prio_queue_insert(&s_q, &b);
+ prio_queue_insert(&s_q, &c);
+
+ /* Проверяем полный порядок */
+ const task_t *p0 = (const task_t *) prio_queue_at(&s_q, 0U);
+ const task_t *p1 = (const task_t *) prio_queue_at(&s_q, 1U);
+ const task_t *p2 = (const task_t *) prio_queue_at(&s_q, 2U);
+
+ TEST_ASSERT_EQUAL_STRING("a", p0->name);
+ TEST_ASSERT_EQUAL_STRING("b", p1->name);
+ TEST_ASSERT_EQUAL_STRING("c", p2->name);
+}
+
+/* ══════════════════════════════════════════════════════════════════════════
+ * Группа: fifo — FIFO при равных приоритетах
+ * ══════════════════════════════════════════════════════════════════════════ */
+
+void test_fifo_two_equal_priority_first_in_first_out(void)
+{
+ task_t first = { .priority = 2, .name = "first" };
+ task_t second = { .priority = 2, .name = "second" };
+
+ prio_queue_insert(&s_q, &first);
+ prio_queue_insert(&s_q, &second);
+
+ const task_t *p_top = (const task_t *) prio_queue_peek(&s_q);
+ TEST_ASSERT_NOT_NULL(p_top);
+ TEST_ASSERT_EQUAL_STRING("first", p_top->name);
+}
+
+void test_fifo_three_equal_priority_order_preserved(void)
+{
+ task_t a = { .priority = 5, .name = "a" };
+ task_t b = { .priority = 5, .name = "b" };
+ task_t c = { .priority = 5, .name = "c" };
+
+ prio_queue_insert(&s_q, &a);
+ prio_queue_insert(&s_q, &b);
+ prio_queue_insert(&s_q, &c);
+
+ const task_t *p0 = (const task_t *) prio_queue_at(&s_q, 0U);
+ const task_t *p1 = (const task_t *) prio_queue_at(&s_q, 1U);
+ const task_t *p2 = (const task_t *) prio_queue_at(&s_q, 2U);
+
+ TEST_ASSERT_EQUAL_STRING("a", p0->name);
+ TEST_ASSERT_EQUAL_STRING("b", p1->name);
+ TEST_ASSERT_EQUAL_STRING("c", p2->name);
+}
+
+void test_fifo_mixed_priorities_equal_group_fifo(void)
+{
+ /* Вставляем: high(1), eq_a(2), eq_b(2), low(3) */
+ task_t high = { .priority = 1, .name = "high" };
+ task_t eq_a = { .priority = 2, .name = "eq_a" };
+ task_t eq_b = { .priority = 2, .name = "eq_b" };
+ task_t low = { .priority = 3, .name = "low" };
+
+ prio_queue_insert(&s_q, &high);
+ prio_queue_insert(&s_q, &eq_a);
+ prio_queue_insert(&s_q, &eq_b);
+ prio_queue_insert(&s_q, &low);
+
+ /* Ожидаемый порядок: high, eq_a, eq_b, low */
+ const task_t *p0 = (const task_t *) prio_queue_at(&s_q, 0U);
+ const task_t *p1 = (const task_t *) prio_queue_at(&s_q, 1U);
+ const task_t *p2 = (const task_t *) prio_queue_at(&s_q, 2U);
+ const task_t *p3 = (const task_t *) prio_queue_at(&s_q, 3U);
+
+ TEST_ASSERT_EQUAL_STRING("high", p0->name);
+ TEST_ASSERT_EQUAL_STRING("eq_a", p1->name);
+ TEST_ASSERT_EQUAL_STRING("eq_b", p2->name);
+ TEST_ASSERT_EQUAL_STRING("low", p3->name);
+}
+
+/* ══════════════════════════════════════════════════════════════════════════
+ * Группа: full_eviction — статусы при заполнении
+ * ══════════════════════════════════════════════════════════════════════════ */
+
+static void fill_queue_with_priority(int priority)
+{
+ for (uint8_t i = 0U; i < CAPACITY; i++)
+ {
+ task_t t = { .priority = priority, .name = "filler" };
+ prio_queue_insert(&s_q, &t);
+ }
+}
+
+void test_full_returns_pq_ok_until_capacity(void)
+{
+ for (uint8_t i = 0U; i < CAPACITY; i++)
+ {
+ task_t t = { .priority = (int) i, .name = "t" };
+ pq_status_t ret = prio_queue_insert(&s_q, &t);
+ TEST_ASSERT_EQUAL(PQ_OK, ret);
+ }
+ TEST_ASSERT_EQUAL_UINT8(CAPACITY, prio_queue_size(&s_q));
+}
+
+void test_full_higher_priority_returns_pq_evicted(void)
+{
+ fill_queue_with_priority(10);
+
+ task_t better = { .priority = 1, .name = "better" };
+ pq_status_t ret = prio_queue_insert(&s_q, &better);
+
+ TEST_ASSERT_EQUAL(PQ_EVICTED, ret);
+ /* Размер не изменился */
+ TEST_ASSERT_EQUAL_UINT8(CAPACITY, prio_queue_size(&s_q));
+}
+
+void test_full_lower_priority_returns_pq_full(void)
+{
+ fill_queue_with_priority(1);
+
+ task_t worst = { .priority = 100, .name = "worst" };
+ pq_status_t ret = prio_queue_insert(&s_q, &worst);
+
+ TEST_ASSERT_EQUAL(PQ_FULL, ret);
+ TEST_ASSERT_EQUAL_UINT8(CAPACITY, prio_queue_size(&s_q));
+}
+
+void test_full_equal_priority_to_all_returns_pq_full(void)
+{
+ /* Все элементы с prio=5; новый тоже prio=5 → FIFO: новый менее приоритетен */
+ fill_queue_with_priority(5);
+
+ task_t same = { .priority = 5, .name = "same" };
+ pq_status_t ret = prio_queue_insert(&s_q, &same);
+
+ TEST_ASSERT_EQUAL(PQ_FULL, ret);
+}
+
+/* ══════════════════════════════════════════════════════════════════════════
+ * Группа: evict_correct — правильный элемент вытесняется
+ * ══════════════════════════════════════════════════════════════════════════ */
+
+void test_evict_removes_least_prioritized_element(void)
+{
+ /* Очередь: [1, 2, 3]; вставляем 2 → вытесняется 3 */
+ task_t small_q_storage[3];
+ memset(small_q_storage, 0, sizeof(small_q_storage));
+
+ prio_queue_t small_q;
+ prio_queue_init(&small_q, small_q_storage, 3U, sizeof(task_t), task_cmp);
+
+ task_t a = { .priority = 1, .name = "a" };
+ task_t b = { .priority = 2, .name = "b" };
+ task_t c = { .priority = 3, .name = "c" };
+
+ prio_queue_insert(&small_q, &a);
+ prio_queue_insert(&small_q, &b);
+ prio_queue_insert(&small_q, &c);
+
+ task_t d = { .priority = 2, .name = "d" };
+ pq_status_t ret = prio_queue_insert(&small_q, &d);
+
+ TEST_ASSERT_EQUAL(PQ_EVICTED, ret);
+
+ /* c(3) должна была вытесниться; d(2) должна быть внутри */
+ bool found_d = false;
+ bool found_c = false;
+
+ for (uint8_t i = 0U; i < prio_queue_size(&small_q); i++)
+ {
+ const task_t *p_elem = (const task_t *) prio_queue_at(&small_q, i);
+ if (strcmp(p_elem->name, "d") == 0)
+ {
+ found_d = true;
+ }
+ if (strcmp(p_elem->name, "c") == 0)
+ {
+ found_c = true;
+ }
+ }
+
+ TEST_ASSERT_TRUE(found_d);
+ TEST_ASSERT_FALSE(found_c);
+}
+
+void test_evict_top_still_correct_after_eviction(void)
+{
+ task_t small_q_storage[2];
+ memset(small_q_storage, 0, sizeof(small_q_storage));
+
+ prio_queue_t small_q;
+ prio_queue_init(&small_q, small_q_storage, 2U, sizeof(task_t), task_cmp);
+
+ task_t lo = { .priority = 10, .name = "lo" };
+ task_t hi = { .priority = 1, .name = "hi" };
+ task_t best = { .priority = 0, .name = "best" };
+
+ prio_queue_insert(&small_q, &lo);
+ prio_queue_insert(&small_q, &hi);
+ /* best вытесняет lo */
+ prio_queue_insert(&small_q, &best);
+
+ const task_t *p_top = (const task_t *) prio_queue_peek(&small_q);
+ TEST_ASSERT_NOT_NULL(p_top);
+ TEST_ASSERT_EQUAL_STRING("best", p_top->name);
+}
+
+/* ══════════════════════════════════════════════════════════════════════════
+ * Группа: peek — стабильность указателя и edge-cases
+ * ══════════════════════════════════════════════════════════════════════════ */
+
+void test_peek_null_when_empty(void)
+{
+ TEST_ASSERT_NULL(prio_queue_peek(&s_q));
+}
+
+void test_peek_does_not_remove_element(void)
+{
+ task_t t = { .priority = 3, .name = "t" };
+ prio_queue_insert(&s_q, &t);
+
+ prio_queue_peek(&s_q);
+ prio_queue_peek(&s_q);
+
+ TEST_ASSERT_EQUAL_UINT8(1U, prio_queue_size(&s_q));
+}
+
+void test_peek_pointer_into_internal_storage(void)
+{
+ task_t t = { .priority = 7, .name = "stored" };
+ prio_queue_insert(&s_q, &t);
+
+ const task_t *p_top = (const task_t *) prio_queue_peek(&s_q);
+ TEST_ASSERT_EQUAL_STRING("stored", p_top->name);
+ TEST_ASSERT_EQUAL_INT(7, p_top->priority);
+}
+
+/* ══════════════════════════════════════════════════════════════════════════
+ * Группа: remove_at — корректность удаления и сдвига
+ * ══════════════════════════════════════════════════════════════════════════ */
+
+void test_remove_at_first_element(void)
+{
+ task_t a = { .priority = 1, .name = "a" };
+ task_t b = { .priority = 2, .name = "b" };
+ task_t c = { .priority = 3, .name = "c" };
+
+ prio_queue_insert(&s_q, &a);
+ prio_queue_insert(&s_q, &b);
+ prio_queue_insert(&s_q, &c);
+
+ prio_queue_remove_at(&s_q, 0U);
+
+ TEST_ASSERT_EQUAL_UINT8(2U, prio_queue_size(&s_q));
+ const task_t *p_top = (const task_t *) prio_queue_peek(&s_q);
+ TEST_ASSERT_EQUAL_STRING("b", p_top->name);
+}
+
+void test_remove_at_middle_element(void)
+{
+ task_t a = { .priority = 1, .name = "a" };
+ task_t b = { .priority = 2, .name = "b" };
+ task_t c = { .priority = 3, .name = "c" };
+
+ prio_queue_insert(&s_q, &a);
+ prio_queue_insert(&s_q, &b);
+ prio_queue_insert(&s_q, &c);
+
+ prio_queue_remove_at(&s_q, 1U); /* удаляем b */
+
+ TEST_ASSERT_EQUAL_UINT8(2U, prio_queue_size(&s_q));
+
+ const task_t *p0 = (const task_t *) prio_queue_at(&s_q, 0U);
+ const task_t *p1 = (const task_t *) prio_queue_at(&s_q, 1U);
+ TEST_ASSERT_EQUAL_STRING("a", p0->name);
+ TEST_ASSERT_EQUAL_STRING("c", p1->name);
+}
+
+void test_remove_at_last_element(void)
+{
+ task_t a = { .priority = 1, .name = "a" };
+ task_t b = { .priority = 2, .name = "b" };
+
+ prio_queue_insert(&s_q, &a);
+ prio_queue_insert(&s_q, &b);
+
+ prio_queue_remove_at(&s_q, 1U);
+
+ TEST_ASSERT_EQUAL_UINT8(1U, prio_queue_size(&s_q));
+ const task_t *p_top = (const task_t *) prio_queue_peek(&s_q);
+ TEST_ASSERT_EQUAL_STRING("a", p_top->name);
+}
+
+void test_remove_at_out_of_bounds_is_nop(void)
+{
+ task_t t = { .priority = 1, .name = "t" };
+ prio_queue_insert(&s_q, &t);
+
+ prio_queue_remove_at(&s_q, 5U); /* idx >= count — должна быть NOP */
+
+ TEST_ASSERT_EQUAL_UINT8(1U, prio_queue_size(&s_q));
+}
+
+void test_remove_at_on_empty_queue_is_nop(void)
+{
+ prio_queue_remove_at(&s_q, 0U); /* не должно упасть */
+ TEST_ASSERT_EQUAL_UINT8(0U, prio_queue_size(&s_q));
+}
+
+/* ══════════════════════════════════════════════════════════════════════════
+ * Группа: at — доступ по индексу
+ * ══════════════════════════════════════════════════════════════════════════ */
+
+void test_at_returns_null_for_out_of_bounds(void)
+{
+ task_t t = { .priority = 1, .name = "t" };
+ prio_queue_insert(&s_q, &t);
+
+ TEST_ASSERT_NULL(prio_queue_at(&s_q, 1U));
+ TEST_ASSERT_NULL(prio_queue_at(&s_q, 99U));
+}
+
+void test_at_returns_correct_element(void)
+{
+ task_t a = { .priority = 1, .name = "a" };
+ task_t b = { .priority = 2, .name = "b" };
+
+ prio_queue_insert(&s_q, &a);
+ prio_queue_insert(&s_q, &b);
+
+ task_t *p_elem = (task_t *) prio_queue_at(&s_q, 1U);
+ TEST_ASSERT_NOT_NULL(p_elem);
+ TEST_ASSERT_EQUAL_STRING("b", p_elem->name);
+}
+
+/* ══════════════════════════════════════════════════════════════════════════
+ * Группа: size — счётчик
+ * ══════════════════════════════════════════════════════════════════════════ */
+
+void test_size_increments_on_insert(void)
+{
+ for (uint8_t i = 0U; i < 4U; i++)
+ {
+ task_t t = { .priority = (int) i, .name = "t" };
+ prio_queue_insert(&s_q, &t);
+ TEST_ASSERT_EQUAL_UINT8((uint8_t) (i + 1U), prio_queue_size(&s_q));
+ }
+}
+
+void test_size_decrements_on_remove(void)
+{
+ task_t a = { .priority = 1, .name = "a" };
+ task_t b = { .priority = 2, .name = "b" };
+
+ prio_queue_insert(&s_q, &a);
+ prio_queue_insert(&s_q, &b);
+
+ prio_queue_remove_at(&s_q, 0U);
+ TEST_ASSERT_EQUAL_UINT8(1U, prio_queue_size(&s_q));
+
+ prio_queue_remove_at(&s_q, 0U);
+ TEST_ASSERT_EQUAL_UINT8(0U, prio_queue_size(&s_q));
+}
+
+void test_size_unchanged_on_pq_full(void)
+{
+ fill_queue_with_priority(5);
+ TEST_ASSERT_EQUAL_UINT8(CAPACITY, prio_queue_size(&s_q));
+
+ task_t worst = { .priority = 100, .name = "worst" };
+ prio_queue_insert(&s_q, &worst);
+
+ TEST_ASSERT_EQUAL_UINT8(CAPACITY, prio_queue_size(&s_q));
+}
+
+/* ══════════════════════════════════════════════════════════════════════════
+ * Группа: stress — последовательность вставок/удалений
+ * ══════════════════════════════════════════════════════════════════════════ */
+
+void test_stress_insert_all_remove_all_in_order(void)
+{
+ /* Вставляем 8 элементов с приоритетами 8..1 (убывающими) */
+ for (int i = (int) CAPACITY; i >= 1; i--)
+ {
+ task_t t = { .priority = i, .name = "x" };
+ prio_queue_insert(&s_q, &t);
+ }
+
+ /* Вытаскиваем по одному: каждый раз первый должен быть минимальным приоритетом */
+ for (int expected = 1; expected <= (int) CAPACITY; expected++)
+ {
+ const task_t *p_top = (const task_t *) prio_queue_peek(&s_q);
+ TEST_ASSERT_NOT_NULL(p_top);
+ TEST_ASSERT_EQUAL_INT(expected, p_top->priority);
+ prio_queue_remove_at(&s_q, 0U);
+ }
+
+ TEST_ASSERT_EQUAL_UINT8(0U, prio_queue_size(&s_q));
+ TEST_ASSERT_NULL(prio_queue_peek(&s_q));
+}
+
+void test_stress_fifo_across_removes(void)
+{
+ /* Вставляем поочерёдно prio=1 (a, b, c) и между ними prio=2 */
+ task_t a = { .priority = 1, .name = "a" };
+ task_t mid = { .priority = 2, .name = "mid" };
+ task_t b = { .priority = 1, .name = "b" };
+ task_t c = { .priority = 1, .name = "c" };
+
+ prio_queue_insert(&s_q, &a);
+ prio_queue_insert(&s_q, &mid);
+ prio_queue_insert(&s_q, &b);
+ prio_queue_insert(&s_q, &c);
+
+ /* Порядок: a(1), b(1), c(1), mid(2) */
+ const char *expected_names[] = { "a", "b", "c", "mid" };
+
+ for (uint8_t i = 0U; i < 4U; i++)
+ {
+ const task_t *p_top = (const task_t *) prio_queue_peek(&s_q);
+ TEST_ASSERT_NOT_NULL(p_top);
+ TEST_ASSERT_EQUAL_STRING(expected_names[i], p_top->name);
+ prio_queue_remove_at(&s_q, 0U);
+ }
+}
+
+/* ══════════════════════════════════════════════════════════════════════════
+ * main
+ * ══════════════════════════════════════════════════════════════════════════ */
+
+int main(void)
+{
+ UNITY_BEGIN();
+
+ /* init */
+ RUN_TEST(test_init_size_is_zero);
+ RUN_TEST(test_init_peek_returns_null);
+ RUN_TEST(test_init_at_returns_null_when_empty);
+
+ /* insert_order */
+ RUN_TEST(test_insert_single_element);
+ RUN_TEST(test_insert_two_ascending_order_peek_is_higher);
+ RUN_TEST(test_insert_descending_input_peek_is_still_highest);
+ RUN_TEST(test_insert_ascending_input_order_preserved);
+
+ /* fifo */
+ RUN_TEST(test_fifo_two_equal_priority_first_in_first_out);
+ RUN_TEST(test_fifo_three_equal_priority_order_preserved);
+ RUN_TEST(test_fifo_mixed_priorities_equal_group_fifo);
+
+ /* full_eviction */
+ RUN_TEST(test_full_returns_pq_ok_until_capacity);
+ RUN_TEST(test_full_higher_priority_returns_pq_evicted);
+ RUN_TEST(test_full_lower_priority_returns_pq_full);
+ RUN_TEST(test_full_equal_priority_to_all_returns_pq_full);
+
+ /* evict_correct */
+ RUN_TEST(test_evict_removes_least_prioritized_element);
+ RUN_TEST(test_evict_top_still_correct_after_eviction);
+
+ /* peek */
+ RUN_TEST(test_peek_null_when_empty);
+ RUN_TEST(test_peek_does_not_remove_element);
+ RUN_TEST(test_peek_pointer_into_internal_storage);
+
+ /* remove_at */
+ RUN_TEST(test_remove_at_first_element);
+ RUN_TEST(test_remove_at_middle_element);
+ RUN_TEST(test_remove_at_last_element);
+ RUN_TEST(test_remove_at_out_of_bounds_is_nop);
+ RUN_TEST(test_remove_at_on_empty_queue_is_nop);
+
+ /* at */
+ RUN_TEST(test_at_returns_null_for_out_of_bounds);
+ RUN_TEST(test_at_returns_correct_element);
+
+ /* size */
+ RUN_TEST(test_size_increments_on_insert);
+ RUN_TEST(test_size_decrements_on_remove);
+ RUN_TEST(test_size_unchanged_on_pq_full);
+
+ /* stress */
+ RUN_TEST(test_stress_insert_all_remove_all_in_order);
+ RUN_TEST(test_stress_fifo_across_removes);
+
+ return UNITY_END();
+}
\ No newline at end of file
diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt
index 16dd8a2..7122100 100644
--- a/utils/CMakeLists.txt
+++ b/utils/CMakeLists.txt
@@ -7,7 +7,8 @@
# Не зависит от конкретного железа (нет fsl_*, CMSIS, FreeRTOS). 2. Используется
# более чем в одном месте проекта.
-add_library(utils STATIC ring_buffer/ring_buffer.c log/log.c)
+add_library(utils STATIC ring_buffer/ring_buffer.c log/log.c
+ prio_queue/prio_queue.c)
# PUBLIC: любой таргет, слинкованный с utils, автоматически получает корень
# utils/ в include path → #include "ring_buffer/ring_buffer.h" #include "log.h"
diff --git a/utils/prio_queue/README.md b/utils/prio_queue/README.md
new file mode 100644
index 0000000..6dea0a0
--- /dev/null
+++ b/utils/prio_queue/README.md
@@ -0,0 +1,92 @@
+# util/prio_queue
+
+Отсортированный массив с фиксированной ёмкостью — generic приоритетная очередь
+с вытеснением.
+
+**Типичное использование:** очередь задач, событий или медиадорожек с приоритетами,
+где число элементов заранее известно и невелико (≤ ~32). При полном буфере новый
+элемент с более высоким приоритетом вытесняет наименее приоритетный.
+
+| Параметр | Значение |
+|---|---|
+| Элемент | любой тип, задаётся через `item_size` |
+| Ёмкость | любая, задаётся при `prio_queue_init`; фиксирована на всё время жизни |
+| Порядок | определяется `cmp`-функцией пользователя (аналог `qsort`) |
+| Вставка | O(n) сдвиг; оптимально при n ≤ 32 |
+| Peek-top | O(1) |
+| Thread-safety | нет; при использовании из нескольких контекстов — внешняя синхронизация |
+| Зависимости | ``, ``, `` |
+
+## Быстрый старт
+
+```c
+#include "prio_queue/prio_queue.h"
+
+typedef struct { int priority; const char *name; } task_t;
+
+/* Comparator: меньший priority → ближе к голове */
+static int task_cmp(const void *a, const void *b) {
+ int pa = ((const task_t *)a)->priority;
+ int pb = ((const task_t *)b)->priority;
+ return (pa < pb) ? -1 : (pa > pb) ? 1 : 0;
+}
+
+static task_t storage[8];
+static prio_queue_t q;
+
+/* Инициализация */
+prio_queue_init(&q, storage, 8, sizeof(task_t), task_cmp);
+
+/* Вставка */
+task_t t = { .priority = 2, .name = "send_data" };
+pq_status_t st = prio_queue_insert(&q, &t);
+/* st == PQ_OK — вставлен
+ st == PQ_EVICTED — вставлен, наименее приоритетный вытеснен
+ st == PQ_FULL — отклонён, новый элемент наименее приоритетен из всех */
+
+/* Чтение верхнего элемента без удаления */
+const task_t *top = prio_queue_peek(&q);
+if (top != NULL) { /* обработать top */ }
+
+/* Удаление верхнего элемента после обработки */
+prio_queue_remove_at(&q, 0);
+```
+
+## Политика вытеснения при полном буфере
+
+```
+Очередь полна [A(1) B(2) C(3)], вставляем D(2):
+ → D приоритетнее C(3) → C вытесняется → [A(1) B(2) D(2)] PQ_EVICTED
+
+Очередь полна [A(1) B(2) C(3)], вставляем E(5):
+ → E менее приоритетен чем все → отклоняется PQ_FULL
+```
+
+## Comparator
+
+Сигнатура идентична `qsort`:
+
+```c
+typedef int (*pq_cmp_fn)(const void *a, const void *b);
+```
+
+| Возврат | Смысл |
+|---|---|
+| `< 0` | `a` стоит перед `b` (a приоритетнее) |
+| `> 0` | `b` стоит перед `a` |
+| `0` | равнозначны; порядок вставки сохраняется |
+
+## API
+
+```c
+void prio_queue_init(prio_queue_t *q, void *buf, uint8_t capacity,
+ size_t item_size, pq_cmp_fn cmp);
+
+pq_status_t prio_queue_insert(prio_queue_t *q, const void *item);
+const void *prio_queue_peek(const prio_queue_t *q);
+uint8_t prio_queue_size(const prio_queue_t *q);
+void *prio_queue_at(prio_queue_t *q, uint8_t idx);
+void prio_queue_remove_at(prio_queue_t *q, uint8_t idx);
+```
+
+Тесты: `tests/host/test_prio_queue.c`
diff --git a/utils/prio_queue/prio_queue.c b/utils/prio_queue/prio_queue.c
new file mode 100644
index 0000000..562e5f9
--- /dev/null
+++ b/utils/prio_queue/prio_queue.c
@@ -0,0 +1,111 @@
+/*
+ * prio_queue.c
+ */
+
+#include "prio_queue.h"
+
+#include
+
+/* ---------- вспомогательные ---------- */
+
+static void *pq_at(prio_queue_t *p_q, uint8_t i)
+{
+ return (uint8_t *) p_q->buf + (size_t) i * p_q->item_size;
+}
+
+static const void *pq_at_const(const prio_queue_t *p_q, uint8_t i)
+{
+ return (const uint8_t *) p_q->buf + (size_t) i * p_q->item_size;
+}
+
+/* ---------- публичный API ---------- */
+
+void prio_queue_init(prio_queue_t *p_q, void *p_buf, uint8_t capacity, size_t item_size,
+ pq_cmp_fn p_cmp)
+{
+ p_q->buf = p_buf;
+ p_q->capacity = capacity;
+ p_q->count = 0;
+ p_q->item_size = item_size;
+ p_q->cmp = p_cmp;
+ memset(p_buf, 0, (size_t) capacity * item_size);
+}
+
+pq_status_t prio_queue_insert(prio_queue_t *p_q, const void *p_item)
+{
+ /* Найти позицию вставки: первый элемент, перед которым новый стоит в очереди.
+ cmp(existing, new) > 0 означает: existing менее приоритетен, вставляем перед ним. */
+ uint8_t insert_pos = p_q->count;
+ for (uint8_t i = 0; i < p_q->count; i++)
+ {
+ if (p_q->cmp(pq_at(p_q, i), p_item) > 0)
+ {
+ insert_pos = i;
+ break;
+ }
+ }
+
+ if (p_q->count < p_q->capacity)
+ {
+ /* Есть свободное место — сдвигаем вправо и вставляем */
+ for (uint8_t i = p_q->count; i > insert_pos; i--)
+ {
+ memcpy(pq_at(p_q, i), pq_at(p_q, i - 1), p_q->item_size);
+ }
+ memcpy(pq_at(p_q, insert_pos), p_item, p_q->item_size);
+ p_q->count++;
+ return PQ_OK;
+ }
+
+ /* Буфер полон */
+ if (insert_pos == p_q->capacity)
+ {
+ /* Новый элемент наименее приоритетен — отклоняем */
+ return PQ_FULL;
+ }
+
+ /* Вытесняем последний (наименее приоритетный), сдвигаем, вставляем */
+ for (uint8_t i = p_q->capacity - 1U; i > insert_pos; i--)
+ {
+ memcpy(pq_at(p_q, i), pq_at(p_q, i - 1), p_q->item_size);
+ }
+ memcpy(pq_at(p_q, insert_pos), p_item, p_q->item_size);
+ return PQ_EVICTED;
+}
+
+const void *prio_queue_peek(const prio_queue_t *p_q)
+{
+ if (p_q->count == 0U)
+ {
+ return NULL;
+ }
+ return pq_at_const(p_q, 0);
+}
+
+uint8_t prio_queue_size(const prio_queue_t *p_q)
+{
+ return p_q->count;
+}
+
+void *prio_queue_at(prio_queue_t *p_q, uint8_t idx)
+{
+ if (idx >= p_q->count)
+ {
+ return NULL;
+ }
+ return pq_at(p_q, idx);
+}
+
+void prio_queue_remove_at(prio_queue_t *p_q, uint8_t idx)
+{
+ if (idx >= p_q->count)
+ {
+ return;
+ }
+ for (uint8_t i = idx; i < p_q->count - 1U; i++)
+ {
+ memcpy(pq_at(p_q, i), pq_at(p_q, i + 1), p_q->item_size);
+ }
+ memset(pq_at(p_q, p_q->count - 1U), 0, p_q->item_size);
+ p_q->count--;
+}
\ No newline at end of file
diff --git a/utils/prio_queue/prio_queue.h b/utils/prio_queue/prio_queue.h
new file mode 100644
index 0000000..f3a77da
--- /dev/null
+++ b/utils/prio_queue/prio_queue.h
@@ -0,0 +1,70 @@
+/*
+ * prio_queue.h
+ *
+ * Generic sorted array с фиксированной ёмкостью.
+ * Не знает ничего про аудио, FreeRTOS или FatFS.
+ *
+ * Порядок сортировки задаётся comparator-функцией:
+ * cmp(a, b) < 0 → a стоит перед b (a приоритетнее)
+ * cmp(a, b) > 0 → b стоит перед a
+ * cmp(a, b) == 0 → равнозначны (порядок вставки сохраняется)
+ */
+
+#ifndef PRIO_QUEUE_H_
+#define PRIO_QUEUE_H_
+
+#include
+#include
+
+typedef int (*pq_cmp_fn)(const void *a, const void *b);
+
+typedef enum
+{
+ PQ_OK, /*!< Вставка прошла успешно */
+ PQ_EVICTED, /*!< Вставка прошла, наименее приоритетный элемент вытеснён */
+ PQ_FULL, /*!< Очередь полна, новый элемент менее приоритетен чем все */
+} pq_status_t;
+
+typedef struct
+{
+ void *buf; /*!< Указатель на внешний буфер элементов */
+ uint8_t capacity; /*!< Максимальное число элементов */
+ uint8_t count; /*!< Текущее число элементов */
+ size_t item_size; /*!< Размер одного элемента в байтах */
+ pq_cmp_fn cmp; /*!< Функция сравнения */
+} prio_queue_t;
+
+/*!
+ * @brief Инициализация. Буфер @buf должен быть выделен вызывающей стороной
+ * и иметь размер не менее @capacity * @item_size байт.
+ */
+void prio_queue_init(prio_queue_t *p_q, void *p_buf, uint8_t capacity, size_t item_size,
+ pq_cmp_fn p_cmp);
+
+/*!
+ * @brief Вставить элемент с сохранением сортировки.
+ * При полном буфере и более высоком приоритете — вытесняет последний.
+ */
+pq_status_t prio_queue_insert(prio_queue_t *p_q, const void *p_item);
+
+/*!
+ * @brief Вернуть указатель на элемент с наивысшим приоритетом (index 0).
+ * NULL если очередь пуста.
+ */
+const void *prio_queue_peek(const prio_queue_t *p_q);
+
+/*! Текущее число элементов. */
+uint8_t prio_queue_size(const prio_queue_t *p_q);
+
+/*!
+ * @brief Изменяемый доступ к элементу по индексу.
+ * NULL если @idx >= count.
+ */
+void *prio_queue_at(prio_queue_t *p_q, uint8_t idx);
+
+/*!
+ * @brief Удалить элемент по индексу со сдвигом остальных влево.
+ */
+void prio_queue_remove_at(prio_queue_t *p_q, uint8_t idx);
+
+#endif /* PRIO_QUEUE_H_ */
\ No newline at end of file