From ae90c303e18e52f8e2635dbbd8b480fe386aff56 Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Mon, 30 Mar 2026 13:29:17 +0300 Subject: [PATCH] # Created bsp_can module --- .devcontainer/devcontainer.json | 1 + CMakePresets.json | 2 + bsp/CMakeLists.txt | 1 + bsp/can/CMakeLists.txt | 18 + bsp/can/PLAN.md | 925 ++++++++++++++++++++++++++++ bsp/can/README.md | 248 ++++++++ bsp/can/include/bsp/can.h | 91 +++ bsp/can/mocks/can_mocks.c | 47 ++ bsp/can/mocks/can_mocks.h | 56 ++ bsp/can/src/can.c | 487 +++++++++++++++ bsp/common/include/bsp/status.h | 4 +- bsp/opto/src/opto.c | 34 +- docs/testing/hil/HIL_HOW_TO.md | 9 +- firmware/test/CMakeLists.txt | 1 + firmware/test/main.c | 30 +- tests/host/CMakeLists.txt | 17 + tests/host/can/test_bsp_can.c | 0 tests/host/mocks/clock_config.h | 0 tests/host/mocks/fsl_common.h | 32 + tests/host/mocks/fsl_flexcan.h | 182 ++++++ tests/target/CMakeLists.txt | 1 + tests/target/hil_can/CMakeLists.txt | 0 tests/target/hil_can/main.c | 0 tools/hil/m5/agent.py | 2 +- tools/hil/test_can.py | 0 tools/hil/test_opto.py | 4 +- 26 files changed, 2164 insertions(+), 28 deletions(-) create mode 100644 bsp/can/CMakeLists.txt create mode 100644 bsp/can/PLAN.md create mode 100644 bsp/can/README.md create mode 100644 bsp/can/include/bsp/can.h create mode 100644 bsp/can/mocks/can_mocks.c create mode 100644 bsp/can/mocks/can_mocks.h create mode 100644 bsp/can/src/can.c create mode 100644 tests/host/can/test_bsp_can.c create mode 100644 tests/host/mocks/clock_config.h create mode 100644 tests/host/mocks/fsl_common.h create mode 100644 tests/host/mocks/fsl_flexcan.h create mode 100644 tests/target/hil_can/CMakeLists.txt create mode 100644 tests/target/hil_can/main.c create mode 100644 tools/hil/test_can.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bfc616f..4a95bfa 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,6 +13,7 @@ "actboy168.tasks", "bierner.markdown-preview-github-styles", "cheshirekow.cmake-format", + "codeium.codeium", "cschlosser.doxdocgen", "dan-c-underwood.arm", "davidanson.vscode-markdownlint", diff --git a/CMakePresets.json b/CMakePresets.json index 003da12..b3b05dd 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -142,6 +142,7 @@ "test_bsp_led", "test_log", "test_bsp_opto", + "uart_host_mock_example", "test_ring_buffer", "test_timeout_pattern", "uart_host_mock_example" @@ -154,6 +155,7 @@ "targets": [ "test_bsp_led", "test_log", + "uart_host_mock_example", "test_bsp_opto", "test_ring_buffer", "test_timeout_pattern", diff --git a/bsp/CMakeLists.txt b/bsp/CMakeLists.txt index 1e553ef..65f2e0b 100644 --- a/bsp/CMakeLists.txt +++ b/bsp/CMakeLists.txt @@ -13,6 +13,7 @@ add_subdirectory(led) add_subdirectory(tick) add_subdirectory(uart_host) add_subdirectory(opto) +add_subdirectory(can) # Подавляем предупреждения при компиляции собственных .c файлов библиотеки target_compile_options(bsp_board PRIVATE -w) diff --git a/bsp/can/CMakeLists.txt b/bsp/can/CMakeLists.txt new file mode 100644 index 0000000..84606c4 --- /dev/null +++ b/bsp/can/CMakeLists.txt @@ -0,0 +1,18 @@ +if(BUILD_TESTS_HOST) + set(CAN_HOST_SOURCES mocks/can_mocks.c) +else() + + set(CAN_HOST_SOURCES src/can.c) +endif() + +add_library(bsp_can STATIC ${CAN_HOST_SOURCES}) + +target_include_directories( + bsp_can + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include + $<$:${CMAKE_CURRENT_SOURCE_DIR}/mocks>) + +target_link_libraries( + bsp_can + PUBLIC bsp_status + PRIVATE bsp_tick utils sdk_flexcan $<$:fff>) diff --git a/bsp/can/PLAN.md b/bsp/can/PLAN.md new file mode 100644 index 0000000..547d519 --- /dev/null +++ b/bsp/can/PLAN.md @@ -0,0 +1,925 @@ +# План разработки bsp_can + +## Обзор + +Единый BSP-модуль `bsp/can/` для работы с FlexCAN2 (CAN1) на MIMXRT1052. +Одна шина, один трансивер SN65HVD230D. Модуль не зависит от FreeRTOS — +интеграция с RTOS делается через callback на стороне приложения. + +**Приоритеты первой итерации (в порядке реализации):** + +1. Базовый TX/RX (send + polling receive) +2. Фильтрация по ID (STD + EXT) +3. HIL-тесты (M5Stack + MicroPython CAN) +4. Host-тесты (fff mocks) +5. Callback-механизм для FreeRTOS (вторая итерация) + +--- + +## Архитектура: один модуль — два паттерна использования + +``` +bsp/can/ ← единственный модуль, без FreeRTOS + include/bsp/can.h ← публичный API + src/bsp_can.c ← реализация поверх fsl_flexcan + mocks/bsp_can_mock.h ← fff-мок для host-тестов app-кода (будущее) + +firmware/test/ ← bare-metal: bsp_can_receive() polling +firmware/tft_app/ ← FreeRTOS: callback → xQueueSendFromISR +``` + +Почему один модуль работает в обоих контекстах: + +- `bsp_can_receive()` — блокирующий polling с таймаутом (bare-metal, HIL) +- `bsp_can_register_rx_callback()` — вызов из ISR, не блокируется (FreeRTOS) +- Вызывающий код выбирает один из двух механизмов, модуль не знает о контексте + +--- + +## Привязка к железу + +Из схемы (лист 3 + лист 5): + +| Сигнал | Пин MCU | GPIO_AD_B0 | Функция | +|-----------|-----------------|--------------|--------------| +| CAN_TX | GPIO_AD_B0_14 | H14 | FLEXCAN2_TX | +| CAN_RX | GPIO_AD_B0_15 | L10 | FLEXCAN2_RX | + +Трансивер: SN65HVD230D (U9), CAN_H/CAN_L через RCLAMP0524PA (VD12). +Терминация: R30 120Ω + J1 (jumper для подключения). + +> **Важно:** на RT1052 CAN1 в SDK = FlexCAN2 периферия (base `CAN2`). +> Нумерация в SDK сдвинута: `CAN1` base → не используется, `CAN2` base → наша шина. + +--- + +## Этап 1 — Публичный API: `bsp/can/include/bsp/can.h` + +```c +#pragma once + +#include "bsp/common.h" +#include +#include +#include + +/* ── Константы ── */ + +#define BSP_CAN_DATA_MAX_LEN 8U +#define BSP_CAN_FILTER_MAX 16U /* MB6..MB21 под RX-фильтры */ + +/* ── Типы ── */ + +typedef struct bsp_can_frame_s { + uint32_t id; /* 11-bit STD или 29-bit EXT */ + uint8_t dlc; /* 0..8 */ + bool is_extended; /* false = STD, true = EXT */ + bool is_remote; /* RTR */ + uint8_t data[BSP_CAN_DATA_MAX_LEN]; +} bsp_can_frame_t; + +typedef struct bsp_can_config_s { + uint32_t bitrate; /* например 500000 */ +} bsp_can_config_t; + +/** + * Callback из ISR-контекста. + * Реализация НЕ ДОЛЖНА блокироваться. + * Типичное использование: xQueueSendFromISR(). + */ +typedef void (*bsp_can_rx_callback_t)(const bsp_can_frame_t *p_frame, + void *p_user_ctx); + +/* ── Init / Deinit ── */ + +bsp_status_t bsp_can_init(const bsp_can_config_t *p_config); +void bsp_can_deinit(void); + +/* ── Фильтрация ── */ + +/** + * Настроить RX-фильтр на конкретный Message Buffer. + * + * @param index 0 .. BSP_CAN_FILTER_MAX-1 + * @param id CAN ID для фильтрации + * @param mask битовая маска (1 = проверять, 0 = игнорировать) + * @param is_extended true = 29-bit EXT, false = 11-bit STD + */ +bsp_status_t bsp_can_set_filter(uint8_t index, + uint32_t id, + uint32_t mask, + bool is_extended); + +/** Принимать все фреймы (сброс всех фильтров). */ +bsp_status_t bsp_can_accept_all(void); + +/* ── TX ── */ + +/** + * Отправить CAN-фрейм. Блокируется до завершения или таймаута. + * + * @param p_frame фрейм для отправки + * @param timeout_ms таймаут в мс (0 = без ожидания) + * @return BSP_OK, BSP_ERR_TIMEOUT, BSP_ERR_PARAM + */ +bsp_status_t bsp_can_send(const bsp_can_frame_t *p_frame, + uint32_t timeout_ms); + +/* ── RX: polling ── */ + +/** + * Принять CAN-фрейм (polling). Блокируется до приёма или таймаута. + * + * @param p_frame буфер для принятого фрейма + * @param timeout_ms таймаут в мс (0 = проверить и вернуться) + * @return BSP_OK, BSP_ERR_TIMEOUT + */ +bsp_status_t bsp_can_receive(bsp_can_frame_t *p_frame, + uint32_t timeout_ms); + +/* ── RX: callback (для FreeRTOS bridge) ── */ + +/** + * Зарегистрировать callback для приёма из ISR. + * При регистрации callback, polling через bsp_can_receive() отключается. + * + * @param callback функция-обработчик (NULL = отключить callback) + * @param p_user_ctx пользовательский контекст, передаётся в callback + */ +bsp_status_t bsp_can_register_rx_callback(bsp_can_rx_callback_t callback, + void *p_user_ctx); +``` + +--- + +## Этап 2 — Реализация: `bsp/can/src/bsp_can.c` + +Ключевые решения по реализации: + +**Message Buffers (MB) FlexCAN2:** + +| MB | Назначение | +|-------|-----------------------------------| +| 0–5 | TX (отправка, round-robin) | +| 6–21 | RX с индивидуальными фильтрами | +| 22–31 | Резерв | + +**Polling vs Callback — взаимоисключающие:** + +```c +static bsp_can_rx_callback_t s_rx_callback; +static void *s_rx_user_ctx; +static volatile bool s_use_callback; + +/* Внутренний ISR-обработчик */ +static void can_rx_isr_handler(const bsp_can_frame_t *p_frame) +{ + if (s_use_callback && s_rx_callback != NULL) { + s_rx_callback(p_frame, s_rx_user_ctx); + } else { + /* положить в internal ring buffer для polling */ + ring_buffer_write(&s_rx_ring, p_frame, sizeof(*p_frame)); + } +} +``` + +**Внутренний RX ring buffer** для polling-режима — переиспользуем `utils/ring_buffer/`. + +**SDK-функции, которые будем вызывать (нужны для мока):** + +``` +FLEXCAN_Init() +FLEXCAN_Deinit() +FLEXCAN_SetTimingConfig() +FLEXCAN_SetRxMbConfig() +FLEXCAN_SetTxMbConfig() +FLEXCAN_SetRxIndividualMask() +FLEXCAN_TransferSendBlocking() /* для TX */ +FLEXCAN_TransferReceiveBlocking() /* для RX polling */ +FLEXCAN_EnableMbInterrupts() +FLEXCAN_DisableMbInterrupts() +FLEXCAN_ReadRxMb() +``` + +--- + +## Этап 3 — CMake: `bsp/can/CMakeLists.txt` + +```cmake +add_library(bsp_can STATIC + src/bsp_can.c +) + +target_include_directories(bsp_can + PUBLIC include + PRIVATE ${BSP_GENERATED_DIR} +) + +target_link_libraries(bsp_can + PUBLIC bsp_common + PRIVATE bsp_tick # для таймаутов + ring_buffer # внутренний RX буфер +) +``` + +Подключить в `bsp/CMakeLists.txt`: + +```cmake +add_subdirectory(can) +``` + +--- + +## Этап 4 — Host-тесты + +### 4.1 Stub-хедер: `tests/host/mocks/fsl_flexcan.h` + +```c +#pragma once +#include +#include +#include + +/* Минимальные типы для компиляции bsp_can.c на хосте */ + +typedef struct { uint32_t reserved[256]; } CAN_Type; + +typedef enum { + kStatus_Success = 0, + kStatus_Fail = 1, + kStatus_FLEXCAN_RxOverflow = 100, +} status_t; + +typedef enum { + kFLEXCAN_FrameFormatStandard = 0, + kFLEXCAN_FrameFormatExtend = 1, +} flexcan_frame_format_e; + +typedef enum { + kFLEXCAN_FrameTypeData = 0, + kFLEXCAN_FrameTypeRemote = 1, +} flexcan_frame_type_e; + +typedef struct { + uint32_t baudRate; +} flexcan_config_s; + +typedef struct { + uint32_t propSeg; + uint32_t phaseSeg1; + uint32_t phaseSeg2; + uint32_t rJumpwidth; + uint32_t preDivider; +} flexcan_timing_config_s; + +typedef struct { + volatile uint32_t id; + volatile uint32_t cs; + volatile uint8_t data[8]; + volatile uint8_t dlc; +} flexcan_frame_s; + +typedef struct { + uint32_t id; + flexcan_frame_format_e format; + flexcan_frame_type_e type; +} flexcan_rx_mb_config_s; + +typedef struct { + flexcan_frame_s *p_frame; + uint8_t mb_idx; +} flexcan_mb_transfer_s; + +/* Сигнатуры — реализации предоставляет fff */ +void FLEXCAN_Init(CAN_Type *p_base, const flexcan_config_s *p_config, + uint32_t src_clk); +void FLEXCAN_Deinit(CAN_Type *p_base); +void FLEXCAN_GetDefaultConfig(flexcan_config_s *p_config); +void FLEXCAN_SetRxMbConfig(CAN_Type *p_base, uint8_t mb_idx, + const flexcan_rx_mb_config_s *p_config, + bool enable); +void FLEXCAN_SetTxMbConfig(CAN_Type *p_base, uint8_t mb_idx, bool enable); +void FLEXCAN_SetRxIndividualMask(CAN_Type *p_base, uint8_t mb_idx, + uint32_t mask); +status_t FLEXCAN_TransferSendBlocking(CAN_Type *p_base, uint8_t mb_idx, + flexcan_frame_s *p_frame); +status_t FLEXCAN_ReadRxMb(CAN_Type *p_base, uint8_t mb_idx, + flexcan_frame_s *p_frame); +void FLEXCAN_EnableMbInterrupts(CAN_Type *p_base, uint32_t mask); +void FLEXCAN_DisableMbInterrupts(CAN_Type *p_base, uint32_t mask); +uint32_t FLEXCAN_GetStatusFlags(CAN_Type *p_base); +void FLEXCAN_ClearStatusFlags(CAN_Type *p_base, uint32_t mask); +``` + +### 4.2 Тестовый файл: `tests/host/can/test_bsp_can.c` + +```c +#include "unity.h" +#include "fff.h" + +DEFINE_FFF_GLOBALS; + +#include "fsl_flexcan.h" + +/* fff-фейки */ +FAKE_VOID_FUNC(FLEXCAN_Init, CAN_Type *, const flexcan_config_s *, uint32_t); +FAKE_VOID_FUNC(FLEXCAN_Deinit, CAN_Type *); +FAKE_VOID_FUNC(FLEXCAN_GetDefaultConfig, flexcan_config_s *); +FAKE_VOID_FUNC(FLEXCAN_SetRxMbConfig, CAN_Type *, uint8_t, + const flexcan_rx_mb_config_s *, bool); +FAKE_VOID_FUNC(FLEXCAN_SetTxMbConfig, CAN_Type *, uint8_t, bool); +FAKE_VOID_FUNC(FLEXCAN_SetRxIndividualMask, CAN_Type *, uint8_t, uint32_t); +FAKE_VALUE_FUNC(status_t, FLEXCAN_TransferSendBlocking, CAN_Type *, uint8_t, + flexcan_frame_s *); +FAKE_VALUE_FUNC(status_t, FLEXCAN_ReadRxMb, CAN_Type *, uint8_t, + flexcan_frame_s *); + +#include "bsp/can.h" + +void setUp(void) +{ + RESET_FAKE(FLEXCAN_Init); + RESET_FAKE(FLEXCAN_Deinit); + RESET_FAKE(FLEXCAN_GetDefaultConfig); + RESET_FAKE(FLEXCAN_SetRxMbConfig); + RESET_FAKE(FLEXCAN_SetTxMbConfig); + RESET_FAKE(FLEXCAN_SetRxIndividualMask); + RESET_FAKE(FLEXCAN_TransferSendBlocking); + RESET_FAKE(FLEXCAN_ReadRxMb); + FFF_RESET_HISTORY(); +} + +void tearDown(void) { } + +/* ── Init ── */ + +void test_init_calls_flexcan_init(void) +{ + bsp_can_config_t cfg = { .bitrate = 500000U }; + bsp_status_t status = bsp_can_init(&cfg); + + TEST_ASSERT_EQUAL(BSP_OK, status); + TEST_ASSERT_EQUAL(1, FLEXCAN_Init_fake.call_count); +} + +void test_init_null_config_returns_err(void) +{ + TEST_ASSERT_EQUAL(BSP_ERR_PARAM, bsp_can_init(NULL)); +} + +void test_deinit_calls_flexcan_deinit(void) +{ + bsp_can_config_t cfg = { .bitrate = 500000U }; + bsp_can_init(&cfg); + bsp_can_deinit(); + + TEST_ASSERT_EQUAL(1, FLEXCAN_Deinit_fake.call_count); +} + +/* ── TX ── */ + +void test_send_valid_frame(void) +{ + bsp_can_config_t cfg = { .bitrate = 500000U }; + bsp_can_init(&cfg); + + FLEXCAN_TransferSendBlocking_fake.return_val = kStatus_Success; + + bsp_can_frame_t frame = { + .id = 0x123, .dlc = 8, .is_extended = false, .is_remote = false, + .data = {0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04} + }; + + TEST_ASSERT_EQUAL(BSP_OK, bsp_can_send(&frame, 100U)); + TEST_ASSERT_EQUAL(1, FLEXCAN_TransferSendBlocking_fake.call_count); +} + +void test_send_null_frame_returns_err(void) +{ + bsp_can_config_t cfg = { .bitrate = 500000U }; + bsp_can_init(&cfg); + + TEST_ASSERT_EQUAL(BSP_ERR_PARAM, bsp_can_send(NULL, 100U)); +} + +void test_send_dlc_over_8_returns_err(void) +{ + bsp_can_config_t cfg = { .bitrate = 500000U }; + bsp_can_init(&cfg); + + bsp_can_frame_t frame = { .id = 0x123, .dlc = 9 }; + TEST_ASSERT_EQUAL(BSP_ERR_PARAM, bsp_can_send(&frame, 100U)); +} + +/* ── Фильтры ── */ + +void test_set_filter_std(void) +{ + bsp_can_config_t cfg = { .bitrate = 500000U }; + bsp_can_init(&cfg); + + bsp_status_t s = bsp_can_set_filter(0, 0x123, 0x7FFU, false); + TEST_ASSERT_EQUAL(BSP_OK, s); + TEST_ASSERT_GREATER_THAN(0, FLEXCAN_SetRxMbConfig_fake.call_count); + TEST_ASSERT_GREATER_THAN(0, FLEXCAN_SetRxIndividualMask_fake.call_count); +} + +void test_set_filter_index_out_of_range(void) +{ + bsp_can_config_t cfg = { .bitrate = 500000U }; + bsp_can_init(&cfg); + + TEST_ASSERT_EQUAL(BSP_ERR_PARAM, + bsp_can_set_filter(BSP_CAN_FILTER_MAX, 0x123, 0x7FF, false)); +} + +void test_accept_all_configures_all_mb(void) +{ + bsp_can_config_t cfg = { .bitrate = 500000U }; + bsp_can_init(&cfg); + + TEST_ASSERT_EQUAL(BSP_OK, bsp_can_accept_all()); +} + +int main(void) +{ + UNITY_BEGIN(); + + RUN_TEST(test_init_calls_flexcan_init); + RUN_TEST(test_init_null_config_returns_err); + RUN_TEST(test_deinit_calls_flexcan_deinit); + RUN_TEST(test_send_valid_frame); + RUN_TEST(test_send_null_frame_returns_err); + RUN_TEST(test_send_dlc_over_8_returns_err); + RUN_TEST(test_set_filter_std); + RUN_TEST(test_set_filter_index_out_of_range); + RUN_TEST(test_accept_all_configures_all_mb); + + return UNITY_END(); +} +``` + +### 4.3 Регистрация: `tests/host/CMakeLists.txt` + +```cmake +add_host_test( + NAME test_bsp_can + SOURCES can/test_bsp_can.c + ${PROJECT_SOURCE_DIR}/bsp/can/src/bsp_can.c + ${PROJECT_SOURCE_DIR}/utils/ring_buffer/ring_buffer.c + INCLUDES + ${PROJECT_SOURCE_DIR}/bsp/can/include + ${PROJECT_SOURCE_DIR}/bsp/common/include + ${PROJECT_SOURCE_DIR}/utils/ring_buffer + MOCKS + ${BSP_MOCKS_DIR} +) +``` + +### 4.4 `CMakePresets.json` — добавить в `host-debug-build` + +```json +"targets": [ + "test_bsp_led", + "test_log", + "test_bsp_opto", + "test_ring_buffer", + "test_timeout_pattern", + "uart_host_mock_example", + "test_bsp_can" +] +``` + +--- + +## Этап 5 — HIL-тесты + +### 5.1 Инфраструктура на стороне хоста: M5Stack CAN + +M5Stack StamPLC с CAN-модулем (TJA1050 или MCP2515) работает как +CAN-адаптер. Связь host ↔ M5Stack через USB CDC (MicroPython). + +**Агент на M5Stack** (`tools/hil/m5/can_agent.py` — MicroPython): + +``` +Команды через serial: + CAN_INIT → OK / ERR + CAN_SEND → OK / ERR + CAN_RECV / TIMEOUT + CAN_FILTER → OK / ERR +``` + +**Python-обёртка** (`tools/hil/m5/can_bus.py`): + +```python +class M5CanBus: + """CAN-адаптер через M5Stack serial.""" + + def __init__(self, port: str, baudrate: int = 115200): + self.ser = serial.Serial(port, baudrate, timeout=2.0) + + def init_can(self, bitrate: int = 500_000) -> None: ... + def send(self, can_id: int, data: bytes, + is_extended: bool = False) -> None: ... + def recv(self, timeout_ms: int = 1000) -> CanFrame | None: ... + def set_filter(self, can_id: int, mask: int) -> None: ... +``` + +### 5.2 C-прошивка: `tests/target/can/main.c` + +```c +#include "board.h" +#include "bsp/led.h" +#include "bsp/tick.h" +#include "bsp/uart_host.h" +#include "bsp/can.h" +#include +#include +#include + +#define CLI_BAUD_RATE 115200U +#define CLI_LINE_MAX 128U +#define CLI_RX_TIMEOUT 100U + +static size_t cli_read_line(uint8_t *p_buf, size_t max_len) { /* как в шаблоне */ } + +/* Вспомогательные функции парсинга */ +static bool parse_hex_bytes(const char *p_hex, uint8_t *p_out, uint8_t len) { ... } +static void format_hex_bytes(const uint8_t *p_data, uint8_t len, char *p_out) { ... } + +static void cli_process_line(const char *p_line) +{ + if (strncmp(p_line, "PING", 4U) == 0) { + bsp_uart_host_write_str("PONG\r\n"); + } + /* CAN_INIT */ + else if (strncmp(p_line, "CAN_INIT ", 9U) == 0) { + uint32_t bitrate = (uint32_t)strtoul(p_line + 9, NULL, 10); + bsp_can_config_t cfg = { .bitrate = bitrate }; + bsp_status_t s = bsp_can_init(&cfg); + bsp_uart_host_write_str(s == BSP_OK ? "OK\r\n" : "ERR\r\n"); + } + /* CAN_SEND */ + else if (strncmp(p_line, "CAN_SEND ", 9U) == 0) { + /* парсинг id, dlc, data из строки */ + bsp_can_frame_t frame = { /* заполнить */ }; + bsp_status_t s = bsp_can_send(&frame, 500U); + bsp_uart_host_write_str(s == BSP_OK ? "OK\r\n" : "ERR\r\n"); + } + /* CAN_RECV */ + else if (strncmp(p_line, "CAN_RECV ", 9U) == 0) { + uint32_t timeout = (uint32_t)strtoul(p_line + 9, NULL, 10); + bsp_can_frame_t frame; + if (bsp_can_receive(&frame, timeout) == BSP_OK) { + char buf[64]; + /* формат: */ + bsp_uart_host_write_str(buf); + bsp_uart_host_write_str("\r\n"); + } else { + bsp_uart_host_write_str("TIMEOUT\r\n"); + } + } + /* CAN_FILTER */ + else if (strncmp(p_line, "CAN_FILTER ", 11U) == 0) { + /* парсинг index, id, mask, is_extended */ + bsp_status_t s = bsp_can_set_filter(/* ... */); + bsp_uart_host_write_str(s == BSP_OK ? "OK\r\n" : "ERR\r\n"); + } + /* CAN_ACCEPT_ALL */ + else if (strncmp(p_line, "CAN_ACCEPT_ALL", 14U) == 0) { + bsp_status_t s = bsp_can_accept_all(); + bsp_uart_host_write_str(s == BSP_OK ? "OK\r\n" : "ERR\r\n"); + } + else if (p_line[0] != '\0') { + bsp_uart_host_write_str("ERR_UNKNOWN\r\n"); + } +} + +int main(void) +{ + board_hw_init(); + bsp_tick_init(); + bsp_led_init(); + bsp_uart_host_init(CLI_BAUD_RATE); + bsp_led_on(LED_HEARTBEAT); + + while (bsp_uart_host_rx_available() == 0U) { + bsp_uart_host_write_str("READY\r\n"); + bsp_delay(200U); + } + + static uint8_t s_line_buf[CLI_LINE_MAX]; + for (;;) { + size_t len = cli_read_line(s_line_buf, sizeof(s_line_buf)); + if (len > 0U) { + cli_process_line((const char *)s_line_buf); + } + } +} +``` + +### 5.3 CMake: `tests/target/can/CMakeLists.txt` + +```cmake +set(TARGET_NAME test_hil_can) + +add_executable(${TARGET_NAME} + main.c + ${BSP_GENERATED}/clock_config.c + ${BSP_STARTUP_FILE} + ${BSP_SYSCALLS_FILE} +) + +target_link_options(${TARGET_NAME} PRIVATE + -T${CMAKE_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_ram.ld + -Wl,--gc-sections + -Wl,--print-memory-usage + -Wl,-Map=${CMAKE_CURRENT_BINARY_DIR}/${TARGET_NAME}.map +) + +target_link_libraries(${TARGET_NAME} PRIVATE + bsp_boot_ram + bsp_board + bsp_led + bsp_tick + bsp_uart_host + bsp_can +) + +add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_SIZE} $ + COMMENT "Size: ${TARGET_NAME}" +) +``` + +### 5.4 Подключить в `tests/target/CMakeLists.txt` + +```cmake +add_subdirectory(host_uart) +add_subdirectory(can) # ← добавить +``` + +### 5.5 `CMakePresets.json` — добавить в `target-debug-build` + +```json +"targets": [ + "test_host_uart", + "test_hil_opto", + "test_hil_can" +] +``` + +### 5.6 Pytest: `tools/hil/test_can.py` + +```python +"""test_can.py — HIL тесты bsp_can.""" +import pytest +import time +from conftest import uart_cmd +from m5.can_bus import M5CanBus + + +@pytest.fixture(scope="module") +def can_bus(request) -> M5CanBus: + """CAN-адаптер на стороне хоста (M5Stack).""" + port = request.config.getoption("--m5-port") + bus = M5CanBus(port) + bus.init_can(500_000) + yield bus + bus.close() + + +class TestCanBasic: + + @pytest.fixture(autouse=True) + def _setup(self, loaded_can, uart, can_bus): + self.ser = uart + self.can = can_bus + # Инициализировать CAN на MCU + assert uart_cmd(self.ser, "CAN_INIT 500000") == "OK" + assert uart_cmd(self.ser, "CAN_ACCEPT_ALL") == "OK" + + @pytest.mark.smoke + def test_ping(self): + """Базовая проверка UART-канала.""" + assert uart_cmd(self.ser, "PING") == "PONG" + + def test_mcu_tx_host_rx(self): + """MCU отправляет фрейм, M5 принимает.""" + assert uart_cmd(self.ser, "CAN_SEND 123 8 DEADBEEF01020304") == "OK" + frame = self.can.recv(timeout_ms=1000) + assert frame is not None + assert frame.id == 0x123 + assert frame.dlc == 8 + assert frame.data == bytes.fromhex("DEADBEEF01020304") + + def test_host_tx_mcu_rx(self): + """M5 отправляет фрейм, MCU принимает.""" + self.can.send(0x456, bytes.fromhex("AABBCCDD"), is_extended=False) + resp = uart_cmd(self.ser, "CAN_RECV 1000") + assert resp != "TIMEOUT" + # Парсинг ответа: "456 4 AABBCCDD" + parts = resp.split() + assert int(parts[0], 16) == 0x456 + assert int(parts[1]) == 4 + + def test_loopback_echo(self): + """M5 отправляет → MCU принимает → MCU отправляет назад → M5 принимает.""" + self.can.send(0x100, bytes.fromhex("01020304")) + # MCU должен принять + resp = uart_cmd(self.ser, "CAN_RECV 1000") + assert resp != "TIMEOUT" + # MCU отправляет эхо + assert uart_cmd(self.ser, "CAN_SEND 100 4 01020304") == "OK" + echo = self.can.recv(timeout_ms=1000) + assert echo is not None + assert echo.id == 0x100 + + +class TestCanFilter: + + @pytest.fixture(autouse=True) + def _setup(self, loaded_can, uart, can_bus): + self.ser = uart + self.can = can_bus + assert uart_cmd(self.ser, "CAN_INIT 500000") == "OK" + + def test_filter_accepts_matching_id(self): + """Фильтр 0x200/0x7FF — принимает 0x200.""" + assert uart_cmd(self.ser, "CAN_FILTER 0 200 7FF 0") == "OK" + self.can.send(0x200, bytes(4)) + resp = uart_cmd(self.ser, "CAN_RECV 1000") + assert resp != "TIMEOUT" + assert resp.startswith("200 ") + + def test_filter_rejects_non_matching_id(self): + """Фильтр 0x200/0x7FF — отвергает 0x300.""" + assert uart_cmd(self.ser, "CAN_FILTER 0 200 7FF 0") == "OK" + self.can.send(0x300, bytes(4)) + resp = uart_cmd(self.ser, "CAN_RECV 500") + assert resp == "TIMEOUT" + + def test_filter_mask_partial(self): + """Маска 0x7F0 — принимает 0x201..0x20F.""" + assert uart_cmd(self.ser, "CAN_FILTER 0 200 7F0 0") == "OK" + self.can.send(0x205, bytes(2)) + resp = uart_cmd(self.ser, "CAN_RECV 1000") + assert resp != "TIMEOUT" + + def test_accept_all_after_filter(self): + """accept_all сбрасывает фильтры.""" + assert uart_cmd(self.ser, "CAN_FILTER 0 200 7FF 0") == "OK" + assert uart_cmd(self.ser, "CAN_ACCEPT_ALL") == "OK" + self.can.send(0x300, bytes(4)) + resp = uart_cmd(self.ser, "CAN_RECV 1000") + assert resp != "TIMEOUT" + + +class TestCanExtended: + + @pytest.fixture(autouse=True) + def _setup(self, loaded_can, uart, can_bus): + self.ser = uart + self.can = can_bus + assert uart_cmd(self.ser, "CAN_INIT 500000") == "OK" + assert uart_cmd(self.ser, "CAN_ACCEPT_ALL") == "OK" + + def test_ext_id_tx_rx(self): + """Отправка/приём EXT ID (29-bit).""" + assert uart_cmd(self.ser, + "CAN_SEND 1ABCDEF0 4 AABBCCDD") == "OK" + frame = self.can.recv(timeout_ms=1000) + assert frame is not None + assert frame.id == 0x1ABCDEF0 + assert frame.is_extended is True +``` + +### 5.7 Фикстура загрузки: `tools/hil/conftest.py` + +```python +@pytest.fixture(scope="module") +def loaded_can(request: pytest.FixtureRequest) -> None: + _load_elf( + request, + Path(cfg.BUILD_DIR) / "tests/target/can/test_hil_can.elf", + ) +``` + +### 5.8 `just/host.just` — алиасы + +```just +[doc('Запустить HIL-тест CAN')] +[group('hil')] +hil-can: + HIL_BUILD_DIR={{_hil_build}} \ + uv run --directory {{HIL_DIR}} pytest test_can.py -v +``` + +--- + +## Этап 6 (будущее) — Callback для FreeRTOS + +Не входит в первую итерацию. Заглушка в API уже есть (`bsp_can_register_rx_callback`). +Реализация в `firmware/tft_app/`: + +```c +static QueueHandle_t s_can_queue; + +static void can_isr_to_queue(const bsp_can_frame_t *p_frame, void *p_ctx) +{ + BaseType_t higher_woken = pdFALSE; + xQueueSendFromISR(s_can_queue, p_frame, &higher_woken); + portYIELD_FROM_ISR(higher_woken); +} + +void can_task(void *p_param) +{ + s_can_queue = xQueueCreate(16, sizeof(bsp_can_frame_t)); + bsp_can_config_t cfg = { .bitrate = 500000U }; + bsp_can_init(&cfg); + bsp_can_register_rx_callback(can_isr_to_queue, NULL); + + bsp_can_frame_t rx; + for (;;) { + if (xQueueReceive(s_can_queue, &rx, portMAX_DELAY) == pdTRUE) { + /* dispatch по rx.id */ + } + } +} +``` + +--- + +## Файловая карта изменений + +``` +НОВЫЕ ФАЙЛЫ: + bsp/can/CMakeLists.txt + bsp/can/include/bsp/can.h + bsp/can/src/bsp_can.c + tests/host/can/test_bsp_can.c + tests/host/mocks/fsl_flexcan.h + tests/target/can/main.c + tests/target/can/CMakeLists.txt + tools/hil/test_can.py + tools/hil/m5/can_agent.py (MicroPython на M5Stack) + tools/hil/m5/can_bus.py (Python-обёртка) + +ИЗМЕНЕНИЯ В СУЩЕСТВУЮЩИХ ФАЙЛАХ: + bsp/CMakeLists.txt ← add_subdirectory(can) + tests/host/CMakeLists.txt ← add_host_test(... test_bsp_can ...) + tests/target/CMakeLists.txt ← add_subdirectory(can) + CMakePresets.json ← test_bsp_can в host-debug-build + ← test_hil_can в target-debug-build + tools/hil/conftest.py ← loaded_can фикстура + just/host.just ← hil-can алиас +``` + +--- + +## Чеклист реализации + +``` +Этап 1–2: BSP модуль + [ ] bsp/can/include/bsp/can.h — публичный API + [ ] bsp/can/src/bsp_can.c — реализация (FlexCAN2) + [ ] bsp/can/CMakeLists.txt — библиотека + [ ] bsp/CMakeLists.txt — add_subdirectory(can) + +Этап 3: pin_mux + [ ] bsp/generated/pin_mux.* — добавить FLEXCAN2 TX/RX в .mex + +Этап 4: Host-тесты + [ ] tests/host/mocks/fsl_flexcan.h — stub + [ ] tests/host/can/test_bsp_can.c — тесты + [ ] tests/host/CMakeLists.txt — add_host_test + [ ] CMakePresets.json — host-debug-build targets + [ ] just build::test-host — зелёный прогон + +Этап 5: HIL-тесты + [ ] tests/target/can/main.c — C-прошивка с CLI + [ ] tests/target/can/CMakeLists.txt — сборка + [ ] tests/target/CMakeLists.txt — add_subdirectory + [ ] CMakePresets.json — target-debug-build targets + [ ] tools/hil/m5/can_agent.py — MicroPython агент + [ ] tools/hil/m5/can_bus.py — Python-обёртка + [ ] tools/hil/test_can.py — pytest + [ ] tools/hil/conftest.py — loaded_can фикстура + [ ] just/host.just — hil-can алиас + [ ] just build::build-hil — сборка target-прошивки + [ ] just host::hil-can — зелёный прогон +``` + +--- + +## Порядок работы + +```bash +# 1. Создать bsp/can (API + реализация) +# 2. Добавить pin_mux для FLEXCAN2 в .mex → перегенерировать +# 3. Host-тесты: stub + test + CMake → just build::test-host +# 4. HIL C-прошивка: target/can → just build::build-hil +# 5. M5Stack CAN-агент: залить can_agent.py +# 6. HIL pytest: test_can.py → just host::hil-can +# 7. Интегрировать в firmware/test (CLI-команды CAN) +``` \ No newline at end of file diff --git a/bsp/can/README.md b/bsp/can/README.md new file mode 100644 index 0000000..f482260 --- /dev/null +++ b/bsp/can/README.md @@ -0,0 +1,248 @@ +# bsp_can + +Приём и передача CAN 2.0 фреймов через FlexCAN2 (трансивер SN65HVD230D, разъём P1 контакты 3–4). + +Применяется для обмена с управляющими модулями по CAN-шине: приём команд, +отправка откликов. Протоколы на стороне приложения — разный DLC, STD/EXT ID. + +--- + +## Архитектура + +```bash + bsp_can_send() ← blocking polling + таймаут + ↓ +[FlexCAN2 TX MB1] → SN65HVD230D → CAN bus + +[CAN bus] → SN65HVD230D → [FlexCAN2 RX MB2..17] + ↓ + poll_rx_mailboxes() ← опрос флагов MB + ↓ + ring_buffer ← внутренний FIFO + ↓ + bsp_can_receive() ← polling + таймаут +``` + +- **TX** — blocking polling с таймаутом. `bsp_can_send()` записывает фрейм + в TX MB и ждёт флага завершения. Worst case при 500 kbit/s — ~260 мкс на фрейм. +- **RX** — polling. `bsp_can_receive()` опрашивает все активные RX MB, + складывает найденные фреймы во внутренний ring buffer, возвращает первый + доступный. Без прерываний. +- **Singleton** — один экземпляр, одна CAN-шина. + +--- + +## Распределение Message Buffers + +| MB | Назначение | +|-------|-----------------------------------------------------| +| 0 | Зарезервирован (ERR005829 workaround: inactive TX) | +| 1 | TX — отправка фреймов | +| 2..17 | RX — до 16 индивидуальных фильтров | + +ERR005829 — errata FlexCAN на i.MX RT1050/1052: при гонке TX/RX арбитража +MB 0 может зависнуть. Workaround: MB 0 всегда `kFLEXCAN_TxMbInactive`. + +--- + +## Быстрый старт + +```c +#include "bsp/can.h" + +/* После board_hw_init() + bsp_tick_init(): */ +bsp_can_config_t cfg = { .bitrate = 500000U }; +bsp_can_init(&cfg); + +/* Принимать всё */ +bsp_can_accept_all(); + +/* TX — blocking, таймаут 500 мс */ +bsp_can_frame_t tx = { + .id = 0x123, .dlc = 4, .is_extended = false, + .data = {0xDE, 0xAD, 0xBE, 0xEF} +}; +bsp_can_send(&tx, 500); + +/* RX — polling, таймаут 100 мс */ +bsp_can_frame_t rx; +if (bsp_can_receive(&rx, 100) == BSP_OK) { + /* обработать rx.id, rx.data[0..rx.dlc-1] */ +} +``` + +--- + +## Фильтрация + +Каждый фильтр занимает один RX MB. Максимум 16 фильтров (`BSP_CAN_FILTER_MAX`). + +```c +/* Принимать только STD ID 0x200 с точным совпадением */ +bsp_can_set_filter(0, 0x200, 0x7FF, false); + +/* Принимать STD ID 0x300..0x30F (маска 0x7F0, младшие 4 бита игнорируются) */ +bsp_can_set_filter(1, 0x300, 0x7F0, false); + +/* Принимать EXT ID 0x1ABCDEF0 с точным совпадением */ +bsp_can_set_filter(2, 0x1ABCDEF0, 0x1FFFFFFF, true); + +/* Сбросить фильтры — принимать всё (STD + EXT) */ +bsp_can_accept_all(); +``` + +`bsp_can_accept_all()` настраивает два MB: один для STD (маска 0), один +для EXT (маска 0). Все остальные MB деактивируются. + +--- + +## Блокирующее поведение + +### TX: `bsp_can_send()` + +Блокирующий вызов. Записывает фрейм в TX MB через `FLEXCAN_WriteTxMb()`, +затем ждёт флага завершения с polling `FLEXCAN_GetMbStatusFlags()`. +Возвращается по одному из условий: + +| Условие | Возврат | +|---------|---------| +| Фрейм успешно отправлен | `BSP_OK` | +| TX MB занят предыдущей передачей | `BSP_ERR_BUSY` | +| Истёк `timeout_ms` | `BSP_ERR_TIMEOUT` | +| Невалидные параметры | `BSP_ERR_PARAM` | + +При 500 kbit/s максимальное время отправки одного фрейма — ~260 мкс. +Для bare-metal и FreeRTOS-задачи это приемлемо. + +### RX: `bsp_can_receive()` + +Polling с таймаутом. Обходит все активные RX MB, читает готовые фреймы +во внутренний ring buffer, пытается извлечь один фрейм: + +| Условие | Возврат | +|---------|---------| +| Фрейм найден (из буфера или MB) | `BSP_OK` | +| Истёк `timeout_ms` | `BSP_ERR_TIMEOUT` | +| Невалидные параметры | `BSP_ERR_PARAM` | + +```c +/* Неблокирующий опрос — timeout_ms = 0 */ +if (bsp_can_receive(&rx, 0) == BSP_OK) { /* есть фрейм */ } + +/* Ожидание с таймаутом */ +bsp_can_receive(&rx, 1000); /* ждать до 1 секунды */ +``` + +--- + +## Callback-механизм (заглушка) + +В первой итерации `bsp_can_register_rx_callback()` возвращает +`BSP_ERR_NOT_SUPPORTED`. API заложен для будущей интеграции с FreeRTOS: + +```c +/* Будущее использование в firmware/tft_app: */ +static void can_isr_to_queue(const bsp_can_frame_t *p_frame, void *p_ctx) +{ + /* xQueueSendFromISR(...) */ +} + +bsp_can_register_rx_callback(can_isr_to_queue, NULL); +``` + +При реализации callback включит прерывания на RX MB. ISR читает фрейм +и вызывает callback напрямую. Callback **не должен блокироваться** — только +атомарные операции (флаг, очередь). Polling через `bsp_can_receive()` +отключается при активном callback. + +--- + +## FreeRTOS + +Модуль не зависит от FreeRTOS и работает в обоих контекстах: + +| Контекст | TX | RX | +|----------|----|----| +| bare-metal (`firmware/test`, HIL) | `bsp_can_send()` — blocking polling | `bsp_can_receive()` — polling | +| FreeRTOS (`firmware/tft_app`) | `bsp_can_send()` — из задачи | `bsp_can_receive()` — из задачи с `timeout_ms` | + +Для FreeRTOS с минимальной латентностью — будущий callback + `xQueueSendFromISR()`. +Polling с `timeout_ms = 10` из задачи подходит для протоколов с интервалом > 10 мс. + +--- + +## Подключение + +```cmake +# bsp/CMakeLists.txt +add_subdirectory(can) + +# firmware/test/CMakeLists.txt +target_link_libraries(firmware_test PRIVATE + bsp_board + bsp_tick + bsp_can +) +``` + +--- + +## Тестирование + +### Host unit-тесты (тестирование логики bsp_can) + +Категория **B** — модуль вызывает NXP SDK (`fsl_flexcan.h`). +SDK-функции мокаются через fff в тестовом файле. +Stubs: `fsl_flexcan.h`, `fsl_common.h`, `clock_config.h` в `tests/host/mocks/`. + +```cmake +add_host_test( + NAME test_bsp_can + SOURCES can/test_bsp_can.c + ${PROJECT_SOURCE_DIR}/bsp/can/src/bsp_can.c + ${PROJECT_SOURCE_DIR}/utils/ring_buffer/ring_buffer.c + INCLUDES + ${PROJECT_SOURCE_DIR}/bsp/can/include + ${PROJECT_SOURCE_DIR}/bsp/common/include + ${PROJECT_SOURCE_DIR}/utils/ring_buffer + MOCKS + ${BSP_MOCKS_DIR} +) +``` + +### Humble Object (тестирование потребителей bsp_can) + +Модуль предоставляет fff-заглушки в `bsp/can/mocks/`: + +```c +#include "fff.h" +DEFINE_FFF_GLOBALS; + +#include "bsp/can.h" +#include "can_mock.h" + +void setUp(void) { CAN_MOCK_RESET_ALL(); } + +void test_protocol_sends_response(void) { + bsp_can_send_fake.return_val = BSP_OK; + /* ... вызываем protocol_handle_frame() ... */ + TEST_ASSERT_EQUAL(1, bsp_can_send_fake.call_count); +} +``` + +### HIL-тесты + +C-прошивка `tests/target/can/` с CLI через UART + pytest `tools/hil/test_can.py`. +CAN-адаптер на стороне хоста — M5Stack с CAN-модулем. + +--- + +## Зависимости + +| Зависимость | Тип | Описание | +|-------------|-----|----------| +| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API | +| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаутов | +| `ring_buffer` | PRIVATE | Внутренний RX FIFO | +| `sdk_flexcan` | PRIVATE | `fsl_flexcan.h` — FlexCAN2 SDK драйвер | +| `clock_config` | PRIVATE | `BOARD_BOOTCLOCKRUN_CAN_CLK_ROOT` | diff --git a/bsp/can/include/bsp/can.h b/bsp/can/include/bsp/can.h new file mode 100644 index 0000000..2ce4eaa --- /dev/null +++ b/bsp/can/include/bsp/can.h @@ -0,0 +1,91 @@ +#ifndef CAN_H_ +#define CAN_H_ + +#include "bsp/status.h" + +#include +#include +#include + +/* ── Константы ── */ + +#define BSP_CAN_DATA_MAX_LEN 8U +#define BSP_CAN_FILTER_MAX 16U /* MB6..MB21 под RX-фильтры */ + +/* ── Типы ── */ + +typedef struct bsp_can_frame_s +{ + uint32_t id; /* 11-bit STD или 29-bit EXT */ + uint8_t dlc; /* 0..8 */ + bool is_extended; /* false = STD, true = EXT */ + bool is_remote; /* RTR */ + uint8_t data[BSP_CAN_DATA_MAX_LEN]; +} bsp_can_frame_t; + +typedef struct bsp_can_config_s +{ + uint32_t bitrate; +} bsp_can_config_t; + +/** + * Callback из ISR-контекста. + * Реализация НЕ ДОЛЖНА блокироваться. + * Типичное использование: xQueueSendFromISR(). + */ +typedef void (*bsp_can_rx_callback_t)(const bsp_can_frame_t *p_frame, void *p_user_ctx); + +/* ── Init / Deinit ── */ + +bsp_status_t bsp_can_init(const bsp_can_config_t *p_config); +void bsp_can_deinit(void); + +/* ── Фильтрация ── */ + +/** + * Настроить RX-фильтр на конкретный Message Buffer. + * + * @param index 0 .. BSP_CAN_FILTER_MAX-1 + * @param id CAN ID для фильтрации + * @param mask битовая маска (1 = проверять, 0 = игнорировать) + * @param is_extended true = 29-bit EXT, false = 11-bit STD + */ +bsp_status_t bsp_can_set_filter(uint8_t index, uint32_t can_id, uint32_t mask, bool is_extended); + +/** Принимать все фреймы (сброс всех фильтров). */ +bsp_status_t bsp_can_accept_all(void); + +/* ── TX ── */ + +/** + * Отправить CAN-фрейм. Блокируется до завершения или таймаута. + * + * @param p_frame фрейм для отправки + * @param timeout_ms таймаут в мс (0 = без ожидания) + * @return BSP_OK, BSP_ERR_TIMEOUT, BSP_ERR_PARAM + */ +bsp_status_t bsp_can_send(const bsp_can_frame_t *p_frame, uint32_t timeout_ms); + +/* ── RX: polling ── */ + +/** + * Принять CAN-фрейм (polling). Блокируется до приёма или таймаута. + * + * @param p_frame буфер для принятого фрейма + * @param timeout_ms таймаут в мс (0 = проверить и вернуться) + * @return BSP_OK, BSP_ERR_TIMEOUT + */ +bsp_status_t bsp_can_receive(bsp_can_frame_t *p_frame, uint32_t timeout_ms); + +/* ── RX: callback (для FreeRTOS bridge) ── */ + +/** + * Зарегистрировать callback для приёма из ISR. + * При регистрации callback, polling через bsp_can_receive() отключается. + * + * @param callback функция-обработчик (NULL = отключить callback) + * @param p_user_ctx пользовательский контекст, передаётся в callback + */ +bsp_status_t bsp_can_register_rx_callback(bsp_can_rx_callback_t p_callback, void *p_user_ctx); + +#endif /* CAN_H_ */ \ No newline at end of file diff --git a/bsp/can/mocks/can_mocks.c b/bsp/can/mocks/can_mocks.c new file mode 100644 index 0000000..8eec5dd --- /dev/null +++ b/bsp/can/mocks/can_mocks.c @@ -0,0 +1,47 @@ +/** + * @file can_mocks.c + * @brief fff-заглушки bsp_can для host unit-тестов (Humble Object). + * + * Подключается вместо bsp_can.c при BUILD_TESTS_HOST. + * Не требует SDK, FlexCAN, ring buffer. + * + * Использование в тесте: + * + * #include "fff.h" + * DEFINE_FFF_GLOBALS; + * + * #include "bsp/can.h" + * #include "can_mocks.h" + * + * void setUp(void) { CAN_MOCK_RESET_ALL(); } + * void tearDown(void) {} + * + * void test_something(void) { + * bsp_can_send_fake.return_val = BSP_OK; + * // ... вызываем код под тестом ... + * TEST_ASSERT_EQUAL(1, bsp_can_send_fake.call_count); + * } + */ + +#include "can_mocks.h" + +/* -------------------------------------------------------------------------- */ +/* Определения fff-заглушек */ +/* -------------------------------------------------------------------------- */ + +/* Init / Deinit */ +DEFINE_FAKE_VALUE_FUNC(bsp_status_t, bsp_can_init, const bsp_can_config_t *); +DEFINE_FAKE_VOID_FUNC(bsp_can_deinit); + +/* Фильтрация */ +DEFINE_FAKE_VALUE_FUNC(bsp_status_t, bsp_can_set_filter, uint8_t, uint32_t, uint32_t, bool); +DEFINE_FAKE_VALUE_FUNC(bsp_status_t, bsp_can_accept_all); + +/* TX */ +DEFINE_FAKE_VALUE_FUNC(bsp_status_t, bsp_can_send, const bsp_can_frame_t *, uint32_t); + +/* RX: polling */ +DEFINE_FAKE_VALUE_FUNC(bsp_status_t, bsp_can_receive, bsp_can_frame_t *, uint32_t); + +/* RX: callback */ +DEFINE_FAKE_VALUE_FUNC(bsp_status_t, bsp_can_register_rx_callback, bsp_can_rx_callback_t, void *); \ No newline at end of file diff --git a/bsp/can/mocks/can_mocks.h b/bsp/can/mocks/can_mocks.h new file mode 100644 index 0000000..f6b6fe2 --- /dev/null +++ b/bsp/can/mocks/can_mocks.h @@ -0,0 +1,56 @@ +/** + * @file can_mocks.h + * @brief Объявления fff-заглушек bsp_can. + * + * Включать только в test-файлах, не в продакшн-коде. + * + * Порядок include в тест-файле: + * 1. fff.h + * 2. DEFINE_FFF_GLOBALS; + * 3. bsp/can.h ← публичный API (сигнатуры функций) + * 4. can_mock.h ← FAKE_* структуры + */ + +#ifndef CAN_MOCKS_H +#define CAN_MOCKS_H + +#include "bsp/can.h" +#include "fff.h" + +/* -------------------------------------------------------------------------- */ +/* Объявления заглушек (DEFINE_ живут в can_mocks.c) */ +/* -------------------------------------------------------------------------- */ + +/* Init / Deinit */ +DECLARE_FAKE_VALUE_FUNC(bsp_status_t, bsp_can_init, const bsp_can_config_t *); +DECLARE_FAKE_VOID_FUNC(bsp_can_deinit); + +/* Фильтрация */ +DECLARE_FAKE_VALUE_FUNC(bsp_status_t, bsp_can_set_filter, uint8_t, uint32_t, uint32_t, bool); +DECLARE_FAKE_VALUE_FUNC(bsp_status_t, bsp_can_accept_all); + +/* TX */ +DECLARE_FAKE_VALUE_FUNC(bsp_status_t, bsp_can_send, const bsp_can_frame_t *, uint32_t); + +/* RX: polling */ +DECLARE_FAKE_VALUE_FUNC(bsp_status_t, bsp_can_receive, bsp_can_frame_t *, uint32_t); + +/* RX: callback */ +DECLARE_FAKE_VALUE_FUNC(bsp_status_t, bsp_can_register_rx_callback, bsp_can_rx_callback_t, void *); + +/* -------------------------------------------------------------------------- */ +/* Удобный макрос: сброс всех заглушек в setUp() */ +/* -------------------------------------------------------------------------- */ + +/* clang-format off */ +#define CAN_MOCK_RESET_ALL() \ + RESET_FAKE(bsp_can_init); \ + RESET_FAKE(bsp_can_deinit); \ + RESET_FAKE(bsp_can_set_filter); \ + RESET_FAKE(bsp_can_accept_all); \ + RESET_FAKE(bsp_can_send); \ + RESET_FAKE(bsp_can_receive); \ + RESET_FAKE(bsp_can_register_rx_callback) +/* clang-format on */ + +#endif /* CAN_MOCKS_H */ \ No newline at end of file diff --git a/bsp/can/src/can.c b/bsp/can/src/can.c new file mode 100644 index 0000000..e6578d8 --- /dev/null +++ b/bsp/can/src/can.c @@ -0,0 +1,487 @@ +/** + * @file bsp_can.c + * @brief BSP CAN — единый модуль для bare-metal и FreeRTOS. + * + * Использует FlexCAN2 (CAN2 base) на MIMXRT1052. + * Пины: GPIO_AD_B0_14 (FLEXCAN2_TX), GPIO_AD_B0_15 (FLEXCAN2_RX). + * Трансивер: SN65HVD230D. + * + * Распределение Message Buffers: + * MB 0 — Зарезервирован (ERR005829: inactive TX для workaround) + * MB 1 — TX (отправка) + * MB 2..17 — RX с индивидуальными фильтрами (BSP_CAN_FILTER_MAX = 16) + * + * Режим приёма: + * - Polling (по умолчанию): bsp_can_receive() опрашивает все RX MB + * и складывает найденные фреймы во внутренний ring buffer. + * - Callback (заглушка): bsp_can_register_rx_callback() возвращает + * BSP_ERR_NOT_SUPPORTED в первой итерации. + */ + +#include "bsp/can.h" + +#include "bsp/tick.h" +#include "clock_config.h" +#include "fsl_flexcan.h" +#include "ring_buffer/ring_buffer.h" + +#include +#include + +/* ══════════════════════════════════════════════════════════════════════ + * Приватные константы + * ══════════════════════════════════════════════════════════════════════ */ + +/** Периферийный блок FlexCAN2 (на RT1052 CAN1-пины → CAN2 base). */ +#define BSP_CAN_BASE CAN2 + +/** Источник тактирования FlexCAN — смотри Config Tools. */ +#define BSP_CAN_CLK_SRC kFLEXCAN_ClkSrcPeri +#define BSP_CAN_CLK_FREQ_HZ BOARD_BOOTCLOCKRUN_CAN_CLK_ROOT + +/* ERR005829: MB0 зарезервирован как inactive TX. */ +#define RESERVED_MB_IDX 0U +#define TX_MB_IDX 1U +#define RX_MB_FIRST 2U +#define RX_MB_LAST (RX_MB_FIRST + BSP_CAN_FILTER_MAX - 1U) /* 17 */ +#define MAX_MB_NUM (RX_MB_LAST + 1U) /* 18 */ + +/** Максимальная допустимая скорость CAN 2.0 */ +#define BSP_CAN_BITRATE_MAX 1000000U +/** + * Размер внутреннего ring buffer для polling-приёма (в байтах). + * Один фрейм сериализуется в sizeof(bsp_can_frame_t) байт. + * 256 байт ≈ 16 фреймов по 16 байт — достаточно для типичного polling. + */ +#define RX_RING_SIZE 256U + +/* ══════════════════════════════════════════════════════════════════════ + * Приватное состояние модуля + * ══════════════════════════════════════════════════════════════════════ */ + +static bool g_s_initialized = false; + +/** Битовая маска активных RX MB (бит i → MB (RX_MB_FIRST + i) настроен). */ +static uint32_t g_s_rx_mb_active_mask; + +/* Ring buffer для polling-приёма */ +static uint8_t g_s_rx_ring_storage[RX_RING_SIZE]; +static ring_buffer_desc_t g_s_rx_ring; + +/* ══════════════════════════════════════════════════════════════════════ + * Вспомогательные функции + * ══════════════════════════════════════════════════════════════════════ */ + +/** + * Конвертировать bsp_can_frame_t → flexcan_frame_t для TX. + * + * Ключевой момент: SDK хранит ID уже сдвинутым через FLEXCAN_ID_STD/EXT, + * а data — в big-endian word format (byte0 = MSB of dataWord0). + */ + +// NOLINT(cppcoreguidelines-avoid-magic-numbers) +static void frame_to_sdk(const bsp_can_frame_t *p_bsp, flexcan_frame_t *p_sdk) +{ + (void) memset(p_sdk, 0, sizeof(*p_sdk)); + + if (p_bsp->is_extended) + { + p_sdk->id = FLEXCAN_ID_EXT(p_bsp->id); + p_sdk->format = (uint8_t) kFLEXCAN_FrameFormatExtend; + } + else + { + p_sdk->id = FLEXCAN_ID_STD(p_bsp->id); + p_sdk->format = (uint8_t) kFLEXCAN_FrameFormatStandard; + } + + p_sdk->type = + p_bsp->is_remote ? (uint8_t) kFLEXCAN_FrameTypeRemote : (uint8_t) kFLEXCAN_FrameTypeData; + p_sdk->length = p_bsp->dlc; + + /* Упаковка data[0..7] → dataWord0/dataWord1 (big-endian byte order). */ + p_sdk->dataByte0 = p_bsp->data[0]; + p_sdk->dataByte1 = p_bsp->data[1]; + p_sdk->dataByte2 = p_bsp->data[2]; + p_sdk->dataByte3 = p_bsp->data[3]; + p_sdk->dataByte4 = p_bsp->data[4]; + p_sdk->dataByte5 = p_bsp->data[5]; + p_sdk->dataByte6 = p_bsp->data[6]; + p_sdk->dataByte7 = p_bsp->data[7]; +} + +/** + * Конвертировать flexcan_frame_t → bsp_can_frame_t после RX. + */ +static void frame_from_sdk(const flexcan_frame_t *p_sdk, + bsp_can_frame_t *p_bsp) // NOLINT(cppcoreguidelines-avoid-magic-numbers) +{ + (void) memset(p_bsp, 0, sizeof(*p_bsp)); + + if ((uint8_t) kFLEXCAN_FrameFormatExtend == p_sdk->format) + { + p_bsp->id = p_sdk->id >> CAN_ID_EXT_SHIFT; + p_bsp->is_extended = true; + } + else + { + p_bsp->id = p_sdk->id >> CAN_ID_STD_SHIFT; + p_bsp->is_extended = false; + } + + p_bsp->is_remote = ((uint8_t) kFLEXCAN_FrameTypeRemote == p_sdk->type); + p_bsp->dlc = (p_sdk->length <= BSP_CAN_DATA_MAX_LEN) ? p_sdk->length : BSP_CAN_DATA_MAX_LEN; + + p_bsp->data[0] = p_sdk->dataByte0; + p_bsp->data[1] = p_sdk->dataByte1; + p_bsp->data[2] = p_sdk->dataByte2; + p_bsp->data[3] = p_sdk->dataByte3; + p_bsp->data[4] = p_sdk->dataByte4; + p_bsp->data[5] = p_sdk->dataByte5; + p_bsp->data[6] = p_sdk->dataByte6; + p_bsp->data[7] = p_sdk->dataByte7; +} + +/** + * Опросить все активные RX MB и сложить принятые фреймы в ring buffer. + * + * @return количество принятых фреймов за этот вызов + */ +static uint32_t poll_rx_mailboxes(void) +{ + uint32_t received = 0U; + + for (uint8_t i = 0U; i < BSP_CAN_FILTER_MAX; i++) + { + if ((g_s_rx_mb_active_mask & (1U << i)) == 0U) + { + continue; + } + + uint8_t mb_idx = RX_MB_FIRST + i; + + /* Проверяем флаг готовности MB. */ + uint64_t mb_flag = (uint64_t) 1U << mb_idx; + if (FLEXCAN_GetMbStatusFlags(BSP_CAN_BASE, mb_flag) == 0U) + { + continue; + } + + /* Читаем фрейм из MB. */ + flexcan_frame_t sdk_frame; + status_t sdk_status = FLEXCAN_ReadRxMb(BSP_CAN_BASE, mb_idx, &sdk_frame); + + /* Очищаем флаг. */ + FLEXCAN_ClearMbStatusFlags(BSP_CAN_BASE, mb_flag); + + if ((sdk_status == kStatus_Success) || (sdk_status == kStatus_FLEXCAN_RxOverflow)) + { + bsp_can_frame_t bsp_frame; + frame_from_sdk(&sdk_frame, &bsp_frame); + + /* Сериализуем фрейм побайтово в ring buffer. */ + ring_buffer_write(&g_s_rx_ring, (const uint8_t *) &bsp_frame, sizeof(bsp_frame)); + received++; + } + } + + return received; +} + +/** + * Попробовать извлечь один фрейм из ring buffer. + * + * @return true если фрейм получен + */ +static bool try_dequeue_frame(bsp_can_frame_t *p_frame) +{ + if (ring_buffer_count(&g_s_rx_ring) < sizeof(bsp_can_frame_t)) + { + return false; + } + + size_t read = ring_buffer_read(&g_s_rx_ring, (uint8_t *) p_frame, sizeof(*p_frame)); + return (read == sizeof(*p_frame)); +} + +/* ══════════════════════════════════════════════════════════════════════ + * Публичный API + * ══════════════════════════════════════════════════════════════════════ */ + +/* ── Init / Deinit ───────────────────────────────────────────────── */ + +bsp_status_t bsp_can_init(const bsp_can_config_t *p_config) +{ + if (p_config == NULL) + { + return BSP_ERR_PARAM; + } + if (p_config->bitrate == 0U || p_config->bitrate > BSP_CAN_BITRATE_MAX) + { + return BSP_ERR_PARAM; + } + + /* Если уже инициализирован — сначала деинициализируем. */ + if (g_s_initialized) + { + bsp_can_deinit(); + } + + /* Инициализация ring buffer. */ + ring_buffer_init(&g_s_rx_ring, g_s_rx_ring_storage, RX_RING_SIZE); + + /* Конфигурация FlexCAN. */ + flexcan_config_t flexcan_cfg; + FLEXCAN_GetDefaultConfig(&flexcan_cfg); + + flexcan_cfg.clkSrc = BSP_CAN_CLK_SRC; + flexcan_cfg.bitRate = p_config->bitrate; + flexcan_cfg.maxMbNum = MAX_MB_NUM; + flexcan_cfg.enableIndividMask = true; + flexcan_cfg.disableSelfReception = true; + flexcan_cfg.enableLoopBack = false; + + /* + * Рассчитать оптимальные timing-параметры (propSeg, phaseSeg1/2, rJumpwidth) + * для заданного bitrate и частоты тактирования. + * FLEXCAN_Init() сама НЕ вызывает эту функцию — она только подгоняет + * prescaler по уже заполненному timingConfig. Без явного расчёта + * timing останется дефолтным (под 1 Mbit/s) и sample point будет неверным. + */ + flexcan_timing_config_t timing_cfg; + (void) memset(&timing_cfg, 0, sizeof(timing_cfg)); + + if (!FLEXCAN_CalculateImprovedTimingValues(BSP_CAN_BASE, p_config->bitrate, BSP_CAN_CLK_FREQ_HZ, + &timing_cfg)) + { + return BSP_ERR_PARAM; + } + + (void) memcpy(&flexcan_cfg.timingConfig, &timing_cfg, sizeof(timing_cfg)); + + FLEXCAN_Init(BSP_CAN_BASE, &flexcan_cfg, BSP_CAN_CLK_FREQ_HZ); + + /* MB1 — рабочий TX. */ + FLEXCAN_SetTxMbConfig(BSP_CAN_BASE, TX_MB_IDX, true); + + g_s_rx_mb_active_mask = 0U; + g_s_initialized = true; + + return BSP_OK; +} + +void bsp_can_deinit(void) +{ + if (!g_s_initialized) + { + return; + } + + FLEXCAN_Deinit(BSP_CAN_BASE); + + g_s_rx_mb_active_mask = 0U; + ring_buffer_reset(&g_s_rx_ring); + g_s_initialized = false; +} + +/* ── Фильтрация ──────────────────────────────────────────────────── */ + +bsp_status_t bsp_can_set_filter(uint8_t index, uint32_t can_id, uint32_t mask, bool is_extended) +{ + if (!g_s_initialized) + { + return BSP_ERR_PARAM; + } + if (index >= BSP_CAN_FILTER_MAX) + { + return BSP_ERR_PARAM; + } + + uint8_t mb_idx = RX_MB_FIRST + index; + + /* Конфигурация RX MB. */ + flexcan_rx_mb_config_t rx_mb_cfg; + rx_mb_cfg.type = kFLEXCAN_FrameTypeData; + + if (is_extended) + { + rx_mb_cfg.format = kFLEXCAN_FrameFormatExtend; + rx_mb_cfg.id = FLEXCAN_ID_EXT(can_id); + } + else + { + rx_mb_cfg.format = kFLEXCAN_FrameFormatStandard; + rx_mb_cfg.id = FLEXCAN_ID_STD(can_id); + } + + FLEXCAN_SetRxMbConfig(BSP_CAN_BASE, mb_idx, &rx_mb_cfg, true); + + /* Установить индивидуальную маску. + * SDK ожидает маску в том же формате, что и ID в MB. + * Для STD: FLEXCAN_RX_MB_STD_MASK(mask, rtr=0, ide=1) + * ide=1 означает: проверять бит IDE (отвергать EXT-фреймы). + * Для EXT: FLEXCAN_RX_MB_EXT_MASK(mask, rtr=0, ide=1) + */ + uint32_t sdk_mask; + if (is_extended) + { + sdk_mask = FLEXCAN_RX_MB_EXT_MASK(mask, 0U, 1U); + } + else + { + sdk_mask = FLEXCAN_RX_MB_STD_MASK(mask, 0U, 1U); + } + + FLEXCAN_SetRxIndividualMask(BSP_CAN_BASE, mb_idx, sdk_mask); + + g_s_rx_mb_active_mask |= (1U << index); + + return BSP_OK; +} + +bsp_status_t bsp_can_accept_all(void) +{ + if (!g_s_initialized) + { + return BSP_ERR_PARAM; + } + + /* + * Настраиваем один RX MB (index 0) с маской 0x000 + * (все биты игнорируются — принимает любой ID). + * Деактивируем остальные. + */ + + /* Деактивировать все ранее настроенные RX MB. */ + for (uint8_t i = 0U; i < BSP_CAN_FILTER_MAX; i++) + { + if ((g_s_rx_mb_active_mask & (1U << i)) != 0U) + { + FLEXCAN_SetRxMbConfig(BSP_CAN_BASE, RX_MB_FIRST + i, NULL, false); + } + } + + g_s_rx_mb_active_mask = 0U; + + /* Настроить MB1 на приём всех STD-фреймов. */ + flexcan_rx_mb_config_t rx_mb_cfg; + rx_mb_cfg.format = kFLEXCAN_FrameFormatStandard; + rx_mb_cfg.type = kFLEXCAN_FrameTypeData; + rx_mb_cfg.id = FLEXCAN_ID_STD(0U); + + FLEXCAN_SetRxMbConfig(BSP_CAN_BASE, RX_MB_FIRST, &rx_mb_cfg, true); + FLEXCAN_SetRxIndividualMask(BSP_CAN_BASE, RX_MB_FIRST, 0U); + + g_s_rx_mb_active_mask = 1U; /* Только index 0 активен. */ + + /* Настроить MB2 на приём всех EXT-фреймов. */ + rx_mb_cfg.format = kFLEXCAN_FrameFormatExtend; + rx_mb_cfg.id = FLEXCAN_ID_EXT(0U); + + FLEXCAN_SetRxMbConfig(BSP_CAN_BASE, RX_MB_FIRST + 1U, &rx_mb_cfg, true); + FLEXCAN_SetRxIndividualMask(BSP_CAN_BASE, RX_MB_FIRST + 1U, 0U); + + g_s_rx_mb_active_mask |= (1U << 1U); /* index 0 и 1 активны. */ + + /* Очистить ring buffer — старые данные неактуальны. */ + ring_buffer_reset(&g_s_rx_ring); + + return BSP_OK; +} + +/* ── TX ──────────────────────────────────────────────────────────── */ + +bsp_status_t bsp_can_send(const bsp_can_frame_t *p_frame, uint32_t timeout_ms) +{ + if (!g_s_initialized) + { + return BSP_ERR_PARAM; + } + if (p_frame == NULL) + { + return BSP_ERR_PARAM; + } + if (p_frame->dlc > BSP_CAN_DATA_MAX_LEN) + { + return BSP_ERR_PARAM; + } + + flexcan_frame_t sdk_frame; + frame_to_sdk(p_frame, &sdk_frame); + + /* Записать фрейм в TX MB. */ + status_t wr_status = FLEXCAN_WriteTxMb(BSP_CAN_BASE, TX_MB_IDX, &sdk_frame); + if (wr_status != kStatus_Success) + { + return BSP_ERR_BUSY; + } + + /* Ждать завершения передачи с таймаутом. */ + uint64_t tx_flag = (uint64_t) 1U << TX_MB_IDX; + uint32_t start_ms = bsp_tick_get_ms(); + + while (FLEXCAN_GetMbStatusFlags(BSP_CAN_BASE, tx_flag) == 0U) + { + uint32_t elapsed = bsp_tick_get_ms() - start_ms; + if (elapsed >= timeout_ms) + { + return BSP_ERR_TIMEOUT; + } + } + + /* Очистить флаг завершения. */ + FLEXCAN_ClearMbStatusFlags(BSP_CAN_BASE, tx_flag); + + return BSP_OK; +} + +/* ── RX: polling ─────────────────────────────────────────────────── */ + +bsp_status_t bsp_can_receive(bsp_can_frame_t *p_frame, uint32_t timeout_ms) +{ + if (!g_s_initialized) + { + return BSP_ERR_PARAM; + } + if (p_frame == NULL) + { + return BSP_ERR_PARAM; + } + + /* Сначала проверяем — может, в ring buffer уже есть фрейм. */ + if (try_dequeue_frame(p_frame)) + { + return BSP_OK; + } + + uint32_t start_ms = bsp_tick_get_ms(); + + for (;;) + { + /* Опросить все активные MB, сложить в ring buffer. */ + poll_rx_mailboxes(); + + /* Попробовать извлечь фрейм. */ + if (try_dequeue_frame(p_frame)) + { + return BSP_OK; + } + + /* Проверить таймаут. */ + uint32_t elapsed = bsp_tick_get_ms() - start_ms; + if (elapsed >= timeout_ms) + { + return BSP_ERR_TIMEOUT; + } + } +} + +/* ── RX: callback (заглушка) ─────────────────────────────────────── */ + +bsp_status_t bsp_can_register_rx_callback(bsp_can_rx_callback_t p_callback, void *p_user_ctx) +{ + (void) p_callback; + (void) p_user_ctx; + + return BSP_ERR_NOT_SUPPORTED; +} \ No newline at end of file diff --git a/bsp/common/include/bsp/status.h b/bsp/common/include/bsp/status.h index de7e457..acefb42 100644 --- a/bsp/common/include/bsp/status.h +++ b/bsp/common/include/bsp/status.h @@ -25,12 +25,10 @@ /** * @brief Коды возврата BSP-модулей. * - * Соглашение: + * @note * BSP_OK — операция завершена успешно. * BSP_ERR_* — операция завершена с ошибкой; подробности в имени кода. * - * Значения фиксированы и не меняются: могут сохраняться в NVM / передаваться - * по протоколу. */ typedef enum bsp_status_e { diff --git a/bsp/opto/src/opto.c b/bsp/opto/src/opto.c index 71c4276..4ead2f4 100644 --- a/bsp/opto/src/opto.c +++ b/bsp/opto/src/opto.c @@ -62,7 +62,7 @@ typedef struct bool initialised; } opto_module_t; -static opto_module_t s_opto; +static opto_module_t g_s_opto; /* ── Вспомогательные функции ─────────────────────────────────────────────── */ @@ -113,7 +113,7 @@ void GPIO1_Combined_16_31_IRQHandler(void) // NOLINT(readability-identifier-nami for (bsp_opto_ch_t ch = (bsp_opto_ch_t) 0U; ch < BSP_OPTO_CH_COUNT; ch++) { - opto_ch_state_t *p_ch = &s_opto.channels[ch]; + opto_ch_state_t *p_ch = &g_s_opto.channels[ch]; if (!p_ch->enabled) { @@ -137,9 +137,9 @@ void GPIO1_Combined_16_31_IRQHandler(void) // NOLINT(readability-identifier-nami GPIO_DisableInterrupts(OPTO_GPIO_BASE, mask); /* Вызвать коллбэк прямо из ISR */ - if (s_opto.callbacks[ch] != NULL) + if (g_s_opto.callbacks[ch] != NULL) { - s_opto.callbacks[ch](ch, OPTO_PIN_TO_STATE(p_ch->raw_pin)); + g_s_opto.callbacks[ch](ch, OPTO_PIN_TO_STATE(p_ch->raw_pin)); } } else /* BSP_OPTO_MODE_LEVEL */ @@ -169,9 +169,9 @@ bsp_status_t bsp_opto_init(const bsp_opto_config_t *p_config) /* Сохранить конфигурацию */ for (bsp_opto_ch_t ch = (bsp_opto_ch_t) 0U; ch < BSP_OPTO_CH_COUNT; ch++) { - s_opto.callbacks[ch] = p_config->callbacks[ch]; + g_s_opto.callbacks[ch] = p_config->callbacks[ch]; } - s_opto.debounce_ms = p_config->debounce_ms; + g_s_opto.debounce_ms = p_config->debounce_ms; static const uint8_t K_PINS[BSP_OPTO_CH_COUNT] = { [BSP_OPTO_CH_IN1] = OPTO_IN1_PIN, @@ -187,7 +187,7 @@ bsp_status_t bsp_opto_init(const bsp_opto_config_t *p_config) for (bsp_opto_ch_t ch = (bsp_opto_ch_t) 0U; ch < BSP_OPTO_CH_COUNT; ch++) { - opto_ch_state_t *p_ch = &s_opto.channels[ch]; + opto_ch_state_t *p_ch = &g_s_opto.channels[ch]; if (ch == BSP_OPTO_CH_RS && !p_config->rs_as_gpio) { @@ -235,7 +235,7 @@ bsp_status_t bsp_opto_init(const bsp_opto_config_t *p_config) EnableIRQ(GPIO1_Combined_16_31_IRQn); - s_opto.initialised = true; + g_s_opto.initialised = true; return BSP_OK; } @@ -243,12 +243,12 @@ bsp_status_t bsp_opto_init(const bsp_opto_config_t *p_config) bsp_opto_state_t bsp_opto_read(bsp_opto_ch_t input_channel) { - if (input_channel >= BSP_OPTO_CH_COUNT || !s_opto.channels[input_channel].enabled || - s_opto.channels[input_channel].mode == BSP_OPTO_MODE_PROTO) + if (input_channel >= BSP_OPTO_CH_COUNT || !g_s_opto.channels[input_channel].enabled || + g_s_opto.channels[input_channel].mode == BSP_OPTO_MODE_PROTO) { return BSP_OPTO_STATE_INACTIVE; } - return s_opto.channels[input_channel].confirmed_state; + return g_s_opto.channels[input_channel].confirmed_state; } /* ── Взвод MODE_PROTO для следующего пакета ──────────────────────────────── */ @@ -260,7 +260,7 @@ void bsp_opto_proto_arm(bsp_opto_ch_t ch) return; } - opto_ch_state_t *p_ch = &s_opto.channels[ch]; + opto_ch_state_t *p_ch = &g_s_opto.channels[ch]; if (!p_ch->enabled || p_ch->mode != BSP_OPTO_MODE_PROTO) { @@ -276,7 +276,7 @@ void bsp_opto_proto_arm(bsp_opto_ch_t ch) void bsp_opto_process(void) { - if (!s_opto.initialised) + if (!g_s_opto.initialised) { return; } @@ -285,7 +285,7 @@ void bsp_opto_process(void) for (bsp_opto_ch_t ch = (bsp_opto_ch_t) 0U; ch < BSP_OPTO_CH_COUNT; ch++) { - opto_ch_state_t *p_ch = &s_opto.channels[ch]; + opto_ch_state_t *p_ch = &g_s_opto.channels[ch]; /* Пропускаем: выключен, нет события, или proto-канал (обрабатывается в ISR) */ if (!p_ch->enabled || !p_ch->pending || p_ch->mode == BSP_OPTO_MODE_PROTO) @@ -294,7 +294,7 @@ void bsp_opto_process(void) } uint32_t elapsed = now - p_ch->last_edge_ms; - if (elapsed < s_opto.debounce_ms) + if (elapsed < g_s_opto.debounce_ms) { continue; } @@ -307,9 +307,9 @@ void bsp_opto_process(void) { p_ch->confirmed_state = new_state; - if (s_opto.callbacks[ch] != NULL) + if (g_s_opto.callbacks[ch] != NULL) { - s_opto.callbacks[ch](ch, new_state); + g_s_opto.callbacks[ch](ch, new_state); } } } diff --git a/docs/testing/hil/HIL_HOW_TO.md b/docs/testing/hil/HIL_HOW_TO.md index 26e9255..07c661f 100644 --- a/docs/testing/hil/HIL_HOW_TO.md +++ b/docs/testing/hil/HIL_HOW_TO.md @@ -147,27 +147,28 @@ uv run --directory tools/hil pytest test_opto.py -v --no-load ### Зелёный прогон -``` +```bash 18 passed in 13.4s ``` ### Типичные ошибки и их причины -**`TimeoutError: Прошивка не отправила READY`** +#### `TimeoutError: Прошивка не отправила READY` Прошивка не запустилась. Возможные причины: + - ELF не пересобран после изменений — `just build::build-hil` - MCU не получает питание — `just host::m5-power on`, проверить RLY1 - Неверный порт в `.env` — `HIL_VCOM_PORT` - MCU-Link занят GDB-сервером — закрыть `just host::debug-server` -**`M5 agent не отвечает`** +#### `M5 agent не отвечает` - Агент не задеплоен — `just host::m5-deploy` - M5 завис — отключить и подключить USB, повторить деплой - Неверный порт — `HIL_M5_PORT` в `.env` -**Тест читает INACTIVE вместо ACTIVE (или наоборот)** +#### Тест читает INACTIVE вместо ACTIVE (или наоборот) - Провод подключён не к тому реле — сверить [HIL_BENCH.md](HIL_BENCH.md) - `agent.py` изменился но не задеплоен — `just host::m5-deploy` diff --git a/firmware/test/CMakeLists.txt b/firmware/test/CMakeLists.txt index 62c9cba..f84af92 100644 --- a/firmware/test/CMakeLists.txt +++ b/firmware/test/CMakeLists.txt @@ -18,6 +18,7 @@ target_link_libraries( PRIVATE bsp_board bsp_led bsp_tick + bsp_can bsp_uart_host bsp_boot_xip bsp_opto diff --git a/firmware/test/main.c b/firmware/test/main.c index 34f8082..9dda0c7 100644 --- a/firmware/test/main.c +++ b/firmware/test/main.c @@ -1,5 +1,6 @@ #include "board.h" +#include "bsp/can.h" #include "bsp/led.h" #include "bsp/opto.h" #include "bsp/tick.h" @@ -46,12 +47,22 @@ int main(void) { const uint16_t DELAY_MS = 10; const uint32_t UART_BAUDRATE = 115200; + const uint32_t CAN_BAUDRATE = 125000; board_hw_init(); bsp_led_init(); bsp_tick_init(); bsp_uart_host_init(UART_BAUDRATE); log_uart_init(); + LOG_I("BOOT", "firmware_test started, tick=%lu", (unsigned long) bsp_tick_get_ms()); + + bsp_can_config_t can_cfg = { .bitrate = CAN_BAUDRATE }; + bsp_status_t status = bsp_can_init(&can_cfg); + if (status != BSP_OK) + { + LOG_E("CAN", "CAN init failed:%d", can_cfg.bitrate); + } + bsp_opto_config_t opto_cfg = { .callbacks = { on_opto_change, on_opto_change, NULL }, .modes = { BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_LEVEL }, @@ -61,11 +72,28 @@ int main(void) }; bsp_opto_init(&opto_cfg); bsp_led_on(LED_APP); - LOG_I("BOOT", "firmware_test started, tick=%lu", (unsigned long) bsp_tick_get_ms()); + + status = bsp_can_accept_all(); + if (status != BSP_OK) + { + LOG_E("CAN", "CAN init failed:%d", can_cfg.bitrate); + } + else + { + LOG_I("CAN", "CAN init OK"); + } + + bsp_can_frame_t rx_frame; while (1) { bsp_opto_process(); + + if (bsp_can_receive(&rx_frame, 0) == BSP_OK) + { + LOG_D("CAN", "RX: ID=0x%08X, DATA=%02X %02X %02X %02X", rx_frame.id, rx_frame.data[0], + rx_frame.data[1], rx_frame.data[2], rx_frame.data[3]); + } // Контроль включения if (g_is_in1_activated) { diff --git a/tests/host/CMakeLists.txt b/tests/host/CMakeLists.txt index 09571a3..d01b642 100644 --- a/tests/host/CMakeLists.txt +++ b/tests/host/CMakeLists.txt @@ -126,3 +126,20 @@ add_host_test( ${PROJECT_SOURCE_DIR}/bsp/common/include MOCKS ${BSP_MOCKS_DIR}) + +# ----------------------------------------------------------------------------- +# Тест кода, использующего bsp_can через мок +# ---------------------------------------------------------------------------- +add_host_test( + NAME + test_bsp_can + SOURCES + can/test_bsp_can.c + ${PROJECT_SOURCE_DIR}/bsp/can/src/can.c + ${PROJECT_SOURCE_DIR}/utils/ring_buffer/ring_buffer.c + INCLUDES + ${PROJECT_SOURCE_DIR}/bsp/can/include + ${PROJECT_SOURCE_DIR}/bsp/common/include + ${PROJECT_SOURCE_DIR}/utils/ring_buffer + MOCKS + ${BSP_MOCKS_DIR}) diff --git a/tests/host/can/test_bsp_can.c b/tests/host/can/test_bsp_can.c new file mode 100644 index 0000000..e69de29 diff --git a/tests/host/mocks/clock_config.h b/tests/host/mocks/clock_config.h new file mode 100644 index 0000000..e69de29 diff --git a/tests/host/mocks/fsl_common.h b/tests/host/mocks/fsl_common.h new file mode 100644 index 0000000..799d900 --- /dev/null +++ b/tests/host/mocks/fsl_common.h @@ -0,0 +1,32 @@ +#pragma once + +/** + * Stub fsl_common.h для host-тестов. + * Содержит базовые типы NXP SDK используемые в: + * fsl_flexcan.h (stub) + * + * В реальном SDK тянет CMSIS device header — на хосте не нужен. + */ + +#include +#include +#include +#include + +/* ── status_t ── */ + +typedef int32_t status_t; + +enum +{ + kStatusGroup_Generic = 0, + kStatusGroup_FLEXCAN = 5, +}; + +#define MAKE_STATUS(group, code) ((int32_t) ((int32_t) (group) * 100 + (int32_t) (code))) + +enum +{ + kStatus_Success = 0, + kStatus_Fail = 1, +}; \ No newline at end of file diff --git a/tests/host/mocks/fsl_flexcan.h b/tests/host/mocks/fsl_flexcan.h new file mode 100644 index 0000000..36b368a --- /dev/null +++ b/tests/host/mocks/fsl_flexcan.h @@ -0,0 +1,182 @@ +#pragma once + +/** + * Stub fsl_flexcan.h для host-тестов. + * Содержит типы, макросы и сигнатуры используемые в: + * bsp/can/src/bsp_can.c + * + * fff предоставляет реализации через FAKE_* в тестовом файле. + * + * Оригинал: sdk/devices/MIMXRT1052/drivers/fsl_flexcan.h + */ + +#include "fsl_common.h" + +/* ── CAN peripheral ──────────────────────────────────────────────────────── */ + +typedef struct +{ + volatile uint32_t MCR; + volatile uint32_t CTRL1; + volatile uint32_t CTRL2; + volatile uint32_t TIMER; + volatile uint32_t ESR1; + volatile uint32_t IMASK1; + volatile uint32_t IFLAG1; + uint32_t reserved[64]; +} CAN_Type; + +#ifndef CAN2 +static CAN_Type g_stub_can2; +#define CAN2 (&g_stub_can2) +#endif + +/* ── ID shift/mask constants (from CMSIS device header) ──────────────────── */ + +#define CAN_ID_STD_SHIFT 18U +#define CAN_ID_STD_MASK (0x1FFC0000U) +#define CAN_ID_EXT_SHIFT 0U +#define CAN_ID_EXT_MASK (0x1FFFFFFFU) + +/* ── ID and mask helper macros (identical to original SDK) ───────────────── */ + +#define FLEXCAN_ID_STD(id) (((uint32_t) (((uint32_t) (id)) << CAN_ID_STD_SHIFT)) & CAN_ID_STD_MASK) + +#define FLEXCAN_ID_EXT(id) \ + (((uint32_t) (((uint32_t) (id)) << CAN_ID_EXT_SHIFT)) & (CAN_ID_EXT_MASK | CAN_ID_STD_MASK)) + +#define FLEXCAN_RX_MB_STD_MASK(id, rtr, ide) \ + (((uint32_t) ((uint32_t) (rtr) << 31) | (uint32_t) ((uint32_t) (ide) << 30)) | \ + FLEXCAN_ID_STD(id)) + +#define FLEXCAN_RX_MB_EXT_MASK(id, rtr, ide) \ + (((uint32_t) ((uint32_t) (rtr) << 31) | (uint32_t) ((uint32_t) (ide) << 30)) | \ + FLEXCAN_ID_EXT(id)) + +/* ── Transfer status codes ───────────────────────────────────────────────── */ + +enum +{ + kStatus_FLEXCAN_TxBusy = MAKE_STATUS(kStatusGroup_FLEXCAN, 0), + kStatus_FLEXCAN_TxIdle = MAKE_STATUS(kStatusGroup_FLEXCAN, 1), + kStatus_FLEXCAN_RxBusy = MAKE_STATUS(kStatusGroup_FLEXCAN, 3), + kStatus_FLEXCAN_RxIdle = MAKE_STATUS(kStatusGroup_FLEXCAN, 4), + kStatus_FLEXCAN_RxOverflow = MAKE_STATUS(kStatusGroup_FLEXCAN, 5), +}; + +/* ── Enums ───────────────────────────────────────────────────────────────── */ + +typedef enum _flexcan_frame_format +{ + kFLEXCAN_FrameFormatStandard = 0x0U, + kFLEXCAN_FrameFormatExtend = 0x1U, +} flexcan_frame_format_t; + +typedef enum _flexcan_frame_type +{ + kFLEXCAN_FrameTypeData = 0x0U, + kFLEXCAN_FrameTypeRemote = 0x1U, +} flexcan_frame_type_t; + +typedef enum _flexcan_clock_source +{ + kFLEXCAN_ClkSrcOsc = 0x0U, + kFLEXCAN_ClkSrcPeri = 0x1U, + kFLEXCAN_ClkSrc0 = 0x0U, + kFLEXCAN_ClkSrc1 = 0x1U, +} flexcan_clock_source_t; + +/* ── Structs ─────────────────────────────────────────────────────────────── */ + +typedef struct _flexcan_frame +{ + struct + { + uint32_t timestamp : 16; + uint32_t length : 4; + uint32_t type : 1; + uint32_t format : 1; + uint32_t : 1; + uint32_t idhit : 9; + }; + struct + { + uint32_t id : 29; + uint32_t : 3; + }; + union + { + struct + { + uint32_t dataWord0; + uint32_t dataWord1; + }; + struct + { + uint8_t dataByte3; + uint8_t dataByte2; + uint8_t dataByte1; + uint8_t dataByte0; + uint8_t dataByte7; + uint8_t dataByte6; + uint8_t dataByte5; + uint8_t dataByte4; + }; + }; +} flexcan_frame_t; + +typedef struct _flexcan_timing_config +{ + uint16_t preDivider; + uint8_t rJumpwidth; + uint8_t phaseSeg1; + uint8_t phaseSeg2; + uint8_t propSeg; +} flexcan_timing_config_t; + +typedef struct _flexcan_config +{ + uint32_t bitRate; + flexcan_clock_source_t clkSrc; + uint8_t maxMbNum; + bool enableLoopBack; + bool enableTimerSync; + bool enableIndividMask; + bool disableSelfReception; + bool enableListenOnlyMode; + bool enableRemoteRequestFrameStored; + flexcan_timing_config_t timingConfig; +} flexcan_config_t; + +typedef struct _flexcan_rx_mb_config +{ + uint32_t id; + flexcan_frame_format_t format; + flexcan_frame_type_t type; +} flexcan_rx_mb_config_t; + +/* ── Function signatures — реализуются через fff ─────────────────────────── */ + +void FLEXCAN_GetDefaultConfig(flexcan_config_t *p_config); + +bool FLEXCAN_CalculateImprovedTimingValues(CAN_Type *p_base, uint32_t bitrate, uint32_t src_clk_hz, + flexcan_timing_config_t *p_timing_cfg); + +void FLEXCAN_Init(CAN_Type *p_base, const flexcan_config_t *p_config, uint32_t src_clk_hz); + +void FLEXCAN_Deinit(CAN_Type *p_base); + +void FLEXCAN_SetTxMbConfig(CAN_Type *p_base, uint8_t mb_idx, bool enable); + +void FLEXCAN_SetRxMbConfig(CAN_Type *p_base, uint8_t mb_idx, + const flexcan_rx_mb_config_t *p_rx_mb_config, bool enable); + +void FLEXCAN_SetRxIndividualMask(CAN_Type *p_base, uint8_t mask_idx, uint32_t mask); + +status_t FLEXCAN_WriteTxMb(CAN_Type *p_base, uint8_t mb_idx, const flexcan_frame_t *p_tx_frame); + +status_t FLEXCAN_ReadRxMb(CAN_Type *p_base, uint8_t mb_idx, flexcan_frame_t *p_rx_frame); + +uint64_t FLEXCAN_GetMbStatusFlags(CAN_Type *p_base, uint64_t mask); + +void FLEXCAN_ClearMbStatusFlags(CAN_Type *p_base, uint64_t mask); \ No newline at end of file diff --git a/tests/target/CMakeLists.txt b/tests/target/CMakeLists.txt index 2bcb5ae..cd58b2b 100644 --- a/tests/target/CMakeLists.txt +++ b/tests/target/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(host_uart) add_subdirectory(hil_opto) +add_subdirectory(hil_can) diff --git a/tests/target/hil_can/CMakeLists.txt b/tests/target/hil_can/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/target/hil_can/main.c b/tests/target/hil_can/main.c new file mode 100644 index 0000000..e69de29 diff --git a/tools/hil/m5/agent.py b/tools/hil/m5/agent.py index c1e90e3..4bd2994 100644 --- a/tools/hil/m5/agent.py +++ b/tools/hil/m5/agent.py @@ -64,7 +64,7 @@ _CFG = { "aw_addr": 0x59, "can_tx": 42, "can_rx": 43, - "can_baud": 500_000, + "can_baud": 125_000, } _RST_PIN = 3 # G3_PHY_RST → нога сброса AW9523B diff --git a/tools/hil/test_can.py b/tools/hil/test_can.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/hil/test_opto.py b/tools/hil/test_opto.py index 3e391f3..c40eaf5 100644 --- a/tools/hil/test_opto.py +++ b/tools/hil/test_opto.py @@ -36,11 +36,11 @@ from conftest import uart_cmd # process() цикл ~10 мс (CLI_RX_TIMEOUT) # UART round-trip ~2 мс # Итого минимум ~33 мс. Берём с запасом × 3. -RELAY_ON_S = 0.150 # ждать после включения реле +RELAY_ON_S = 0.1 # ждать после включения реле # После выключения нужно убедиться что INACTIVE подтверждён дебаунсом. # Берём чуть больше чем RELAY_ON_S — дебаунс работает симметрично. -RELAY_OFF_S = 0.150 +RELAY_OFF_S = 0.1 # ---------------------------------------------------------------------------