# Created bsp_can module

This commit is contained in:
Dmitry Akimov 2026-03-30 13:29:17 +03:00
parent 94458e80f9
commit ae90c303e1
26 changed files with 2164 additions and 28 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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)

18
bsp/can/CMakeLists.txt Normal file
View file

@ -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
$<$<BOOL:${BUILD_TESTS_HOST}>:${CMAKE_CURRENT_SOURCE_DIR}/mocks>)
target_link_libraries(
bsp_can
PUBLIC bsp_status
PRIVATE bsp_tick utils sdk_flexcan $<$<BOOL:${BUILD_TESTS_HOST}>:fff>)

925
bsp/can/PLAN.md Normal file
View file

@ -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 <stdint.h>
#include <stdbool.h>
#include <stddef.h>
/* ── Константы ── */
#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 | Назначение |
|-------|-----------------------------------|
| 05 | TX (отправка, round-robin) |
| 621 | RX с индивидуальными фильтрами |
| 2231 | Резерв |
**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 <stdint.h>
#include <stdbool.h>
#include <stddef.h>
/* Минимальные типы для компиляции 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 <bitrate> → OK / ERR
CAN_SEND <id> <dlc> <hex> → OK / ERR
CAN_RECV <timeout_ms><id> <dlc> <hex> / TIMEOUT
CAN_FILTER <id> <mask> → 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 <string.h>
#include <stdio.h>
#include <stdlib.h>
#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 <bitrate> */
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 <id_hex> <dlc> <data_hex> */
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 <timeout_ms> */
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];
/* формат: <id_hex> <dlc> <data_hex> */
bsp_uart_host_write_str(buf);
bsp_uart_host_write_str("\r\n");
} else {
bsp_uart_host_write_str("TIMEOUT\r\n");
}
}
/* CAN_FILTER <index> <id_hex> <mask_hex> <ext:0|1> */
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} $<TARGET_FILE:${TARGET_NAME}>
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 алиас
```
---
## Чеклист реализации
```
Этап 12: 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)
```

248
bsp/can/README.md Normal file
View file

@ -0,0 +1,248 @@
# bsp_can
Приём и передача CAN 2.0 фреймов через FlexCAN2 (трансивер SN65HVD230D, разъём P1 контакты 34).
Применяется для обмена с управляющими модулями по 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` |

91
bsp/can/include/bsp/can.h Normal file
View file

@ -0,0 +1,91 @@
#ifndef CAN_H_
#define CAN_H_
#include "bsp/status.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
/* ── Константы ── */
#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_ */

47
bsp/can/mocks/can_mocks.c Normal file
View file

@ -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 *);

56
bsp/can/mocks/can_mocks.h Normal file
View file

@ -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 */

487
bsp/can/src/can.c Normal file
View file

@ -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 <stdbool.h>
#include <string.h>
/* ══════════════════════════════════════════════════════════════════════
* Приватные константы
* */
/** Периферийный блок 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;
}

View file

@ -25,12 +25,10 @@
/**
* @brief Коды возврата BSP-модулей.
*
* Соглашение:
* @note
* BSP_OK операция завершена успешно.
* BSP_ERR_* операция завершена с ошибкой; подробности в имени кода.
*
* Значения фиксированы и не меняются: могут сохраняться в NVM / передаваться
* по протоколу.
*/
typedef enum bsp_status_e
{

View file

@ -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);
}
}
}

View file

@ -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`

View file

@ -18,6 +18,7 @@ target_link_libraries(
PRIVATE bsp_board
bsp_led
bsp_tick
bsp_can
bsp_uart_host
bsp_boot_xip
bsp_opto

View file

@ -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)
{

View file

@ -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})

View file

View file

View file

@ -0,0 +1,32 @@
#pragma once
/**
* Stub fsl_common.h для host-тестов.
* Содержит базовые типы NXP SDK используемые в:
* fsl_flexcan.h (stub)
*
* В реальном SDK тянет CMSIS device header на хосте не нужен.
*/
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
/* ── 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,
};

View file

@ -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);

View file

@ -1,2 +1,3 @@
add_subdirectory(host_uart)
add_subdirectory(hil_opto)
add_subdirectory(hil_can)

View file

View file

View file

@ -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

0
tools/hil/test_can.py Normal file
View file

View file

@ -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
# ---------------------------------------------------------------------------