# Created bsp_can module
This commit is contained in:
parent
94458e80f9
commit
ae90c303e1
26 changed files with 2164 additions and 28 deletions
|
|
@ -13,6 +13,7 @@
|
||||||
"actboy168.tasks",
|
"actboy168.tasks",
|
||||||
"bierner.markdown-preview-github-styles",
|
"bierner.markdown-preview-github-styles",
|
||||||
"cheshirekow.cmake-format",
|
"cheshirekow.cmake-format",
|
||||||
|
"codeium.codeium",
|
||||||
"cschlosser.doxdocgen",
|
"cschlosser.doxdocgen",
|
||||||
"dan-c-underwood.arm",
|
"dan-c-underwood.arm",
|
||||||
"davidanson.vscode-markdownlint",
|
"davidanson.vscode-markdownlint",
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,7 @@
|
||||||
"test_bsp_led",
|
"test_bsp_led",
|
||||||
"test_log",
|
"test_log",
|
||||||
"test_bsp_opto",
|
"test_bsp_opto",
|
||||||
|
"uart_host_mock_example",
|
||||||
"test_ring_buffer",
|
"test_ring_buffer",
|
||||||
"test_timeout_pattern",
|
"test_timeout_pattern",
|
||||||
"uart_host_mock_example"
|
"uart_host_mock_example"
|
||||||
|
|
@ -154,6 +155,7 @@
|
||||||
"targets": [
|
"targets": [
|
||||||
"test_bsp_led",
|
"test_bsp_led",
|
||||||
"test_log",
|
"test_log",
|
||||||
|
"uart_host_mock_example",
|
||||||
"test_bsp_opto",
|
"test_bsp_opto",
|
||||||
"test_ring_buffer",
|
"test_ring_buffer",
|
||||||
"test_timeout_pattern",
|
"test_timeout_pattern",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ add_subdirectory(led)
|
||||||
add_subdirectory(tick)
|
add_subdirectory(tick)
|
||||||
add_subdirectory(uart_host)
|
add_subdirectory(uart_host)
|
||||||
add_subdirectory(opto)
|
add_subdirectory(opto)
|
||||||
|
add_subdirectory(can)
|
||||||
# Подавляем предупреждения при компиляции собственных .c файлов библиотеки
|
# Подавляем предупреждения при компиляции собственных .c файлов библиотеки
|
||||||
target_compile_options(bsp_board PRIVATE -w)
|
target_compile_options(bsp_board PRIVATE -w)
|
||||||
|
|
||||||
|
|
|
||||||
18
bsp/can/CMakeLists.txt
Normal file
18
bsp/can/CMakeLists.txt
Normal 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
925
bsp/can/PLAN.md
Normal 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 | Назначение |
|
||||||
|
|-------|-----------------------------------|
|
||||||
|
| 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 <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 алиас
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Чеклист реализации
|
||||||
|
|
||||||
|
```
|
||||||
|
Этап 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)
|
||||||
|
```
|
||||||
248
bsp/can/README.md
Normal file
248
bsp/can/README.md
Normal file
|
|
@ -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` |
|
||||||
91
bsp/can/include/bsp/can.h
Normal file
91
bsp/can/include/bsp/can.h
Normal 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
47
bsp/can/mocks/can_mocks.c
Normal 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
56
bsp/can/mocks/can_mocks.h
Normal 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
487
bsp/can/src/can.c
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -25,12 +25,10 @@
|
||||||
/**
|
/**
|
||||||
* @brief Коды возврата BSP-модулей.
|
* @brief Коды возврата BSP-модулей.
|
||||||
*
|
*
|
||||||
* Соглашение:
|
* @note
|
||||||
* BSP_OK — операция завершена успешно.
|
* BSP_OK — операция завершена успешно.
|
||||||
* BSP_ERR_* — операция завершена с ошибкой; подробности в имени кода.
|
* BSP_ERR_* — операция завершена с ошибкой; подробности в имени кода.
|
||||||
*
|
*
|
||||||
* Значения фиксированы и не меняются: могут сохраняться в NVM / передаваться
|
|
||||||
* по протоколу.
|
|
||||||
*/
|
*/
|
||||||
typedef enum bsp_status_e
|
typedef enum bsp_status_e
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ typedef struct
|
||||||
bool initialised;
|
bool initialised;
|
||||||
} opto_module_t;
|
} 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++)
|
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)
|
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);
|
GPIO_DisableInterrupts(OPTO_GPIO_BASE, mask);
|
||||||
|
|
||||||
/* Вызвать коллбэк прямо из ISR */
|
/* Вызвать коллбэк прямо из 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 */
|
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++)
|
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] = {
|
static const uint8_t K_PINS[BSP_OPTO_CH_COUNT] = {
|
||||||
[BSP_OPTO_CH_IN1] = OPTO_IN1_PIN,
|
[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++)
|
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)
|
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);
|
EnableIRQ(GPIO1_Combined_16_31_IRQn);
|
||||||
|
|
||||||
s_opto.initialised = true;
|
g_s_opto.initialised = true;
|
||||||
return BSP_OK;
|
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)
|
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 ||
|
if (input_channel >= BSP_OPTO_CH_COUNT || !g_s_opto.channels[input_channel].enabled ||
|
||||||
s_opto.channels[input_channel].mode == BSP_OPTO_MODE_PROTO)
|
g_s_opto.channels[input_channel].mode == BSP_OPTO_MODE_PROTO)
|
||||||
{
|
{
|
||||||
return BSP_OPTO_STATE_INACTIVE;
|
return BSP_OPTO_STATE_INACTIVE;
|
||||||
}
|
}
|
||||||
return s_opto.channels[input_channel].confirmed_state;
|
return g_s_opto.channels[input_channel].confirmed_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Взвод MODE_PROTO для следующего пакета ──────────────────────────────── */
|
/* ── Взвод MODE_PROTO для следующего пакета ──────────────────────────────── */
|
||||||
|
|
@ -260,7 +260,7 @@ void bsp_opto_proto_arm(bsp_opto_ch_t ch)
|
||||||
return;
|
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)
|
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)
|
void bsp_opto_process(void)
|
||||||
{
|
{
|
||||||
if (!s_opto.initialised)
|
if (!g_s_opto.initialised)
|
||||||
{
|
{
|
||||||
return;
|
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++)
|
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) */
|
/* Пропускаем: выключен, нет события, или proto-канал (обрабатывается в ISR) */
|
||||||
if (!p_ch->enabled || !p_ch->pending || p_ch->mode == BSP_OPTO_MODE_PROTO)
|
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;
|
uint32_t elapsed = now - p_ch->last_edge_ms;
|
||||||
if (elapsed < s_opto.debounce_ms)
|
if (elapsed < g_s_opto.debounce_ms)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -307,9 +307,9 @@ void bsp_opto_process(void)
|
||||||
{
|
{
|
||||||
p_ch->confirmed_state = new_state;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,27 +147,28 @@ uv run --directory tools/hil pytest test_opto.py -v --no-load
|
||||||
|
|
||||||
### Зелёный прогон
|
### Зелёный прогон
|
||||||
|
|
||||||
```
|
```bash
|
||||||
18 passed in 13.4s
|
18 passed in 13.4s
|
||||||
```
|
```
|
||||||
|
|
||||||
### Типичные ошибки и их причины
|
### Типичные ошибки и их причины
|
||||||
|
|
||||||
**`TimeoutError: Прошивка не отправила READY`**
|
#### `TimeoutError: Прошивка не отправила READY`
|
||||||
|
|
||||||
Прошивка не запустилась. Возможные причины:
|
Прошивка не запустилась. Возможные причины:
|
||||||
|
|
||||||
- ELF не пересобран после изменений — `just build::build-hil`
|
- ELF не пересобран после изменений — `just build::build-hil`
|
||||||
- MCU не получает питание — `just host::m5-power on`, проверить RLY1
|
- MCU не получает питание — `just host::m5-power on`, проверить RLY1
|
||||||
- Неверный порт в `.env` — `HIL_VCOM_PORT`
|
- Неверный порт в `.env` — `HIL_VCOM_PORT`
|
||||||
- MCU-Link занят GDB-сервером — закрыть `just host::debug-server`
|
- MCU-Link занят GDB-сервером — закрыть `just host::debug-server`
|
||||||
|
|
||||||
**`M5 agent не отвечает`**
|
#### `M5 agent не отвечает`
|
||||||
|
|
||||||
- Агент не задеплоен — `just host::m5-deploy`
|
- Агент не задеплоен — `just host::m5-deploy`
|
||||||
- M5 завис — отключить и подключить USB, повторить деплой
|
- M5 завис — отключить и подключить USB, повторить деплой
|
||||||
- Неверный порт — `HIL_M5_PORT` в `.env`
|
- Неверный порт — `HIL_M5_PORT` в `.env`
|
||||||
|
|
||||||
**Тест читает INACTIVE вместо ACTIVE (или наоборот)**
|
#### Тест читает INACTIVE вместо ACTIVE (или наоборот)
|
||||||
|
|
||||||
- Провод подключён не к тому реле — сверить [HIL_BENCH.md](HIL_BENCH.md)
|
- Провод подключён не к тому реле — сверить [HIL_BENCH.md](HIL_BENCH.md)
|
||||||
- `agent.py` изменился но не задеплоен — `just host::m5-deploy`
|
- `agent.py` изменился но не задеплоен — `just host::m5-deploy`
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ target_link_libraries(
|
||||||
PRIVATE bsp_board
|
PRIVATE bsp_board
|
||||||
bsp_led
|
bsp_led
|
||||||
bsp_tick
|
bsp_tick
|
||||||
|
bsp_can
|
||||||
bsp_uart_host
|
bsp_uart_host
|
||||||
bsp_boot_xip
|
bsp_boot_xip
|
||||||
bsp_opto
|
bsp_opto
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
#include "board.h"
|
#include "board.h"
|
||||||
|
#include "bsp/can.h"
|
||||||
#include "bsp/led.h"
|
#include "bsp/led.h"
|
||||||
#include "bsp/opto.h"
|
#include "bsp/opto.h"
|
||||||
#include "bsp/tick.h"
|
#include "bsp/tick.h"
|
||||||
|
|
@ -46,12 +47,22 @@ int main(void)
|
||||||
{
|
{
|
||||||
const uint16_t DELAY_MS = 10;
|
const uint16_t DELAY_MS = 10;
|
||||||
const uint32_t UART_BAUDRATE = 115200;
|
const uint32_t UART_BAUDRATE = 115200;
|
||||||
|
const uint32_t CAN_BAUDRATE = 125000;
|
||||||
board_hw_init();
|
board_hw_init();
|
||||||
bsp_led_init();
|
bsp_led_init();
|
||||||
bsp_tick_init();
|
bsp_tick_init();
|
||||||
bsp_uart_host_init(UART_BAUDRATE);
|
bsp_uart_host_init(UART_BAUDRATE);
|
||||||
log_uart_init();
|
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 = {
|
bsp_opto_config_t opto_cfg = {
|
||||||
.callbacks = { on_opto_change, on_opto_change, NULL },
|
.callbacks = { on_opto_change, on_opto_change, NULL },
|
||||||
.modes = { BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_LEVEL },
|
.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_opto_init(&opto_cfg);
|
||||||
bsp_led_on(LED_APP);
|
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)
|
while (1)
|
||||||
{
|
{
|
||||||
bsp_opto_process();
|
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)
|
if (g_is_in1_activated)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -126,3 +126,20 @@ add_host_test(
|
||||||
${PROJECT_SOURCE_DIR}/bsp/common/include
|
${PROJECT_SOURCE_DIR}/bsp/common/include
|
||||||
MOCKS
|
MOCKS
|
||||||
${BSP_MOCKS_DIR})
|
${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})
|
||||||
|
|
|
||||||
0
tests/host/can/test_bsp_can.c
Normal file
0
tests/host/can/test_bsp_can.c
Normal file
0
tests/host/mocks/clock_config.h
Normal file
0
tests/host/mocks/clock_config.h
Normal file
32
tests/host/mocks/fsl_common.h
Normal file
32
tests/host/mocks/fsl_common.h
Normal 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,
|
||||||
|
};
|
||||||
182
tests/host/mocks/fsl_flexcan.h
Normal file
182
tests/host/mocks/fsl_flexcan.h
Normal 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);
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
add_subdirectory(host_uart)
|
add_subdirectory(host_uart)
|
||||||
add_subdirectory(hil_opto)
|
add_subdirectory(hil_opto)
|
||||||
|
add_subdirectory(hil_can)
|
||||||
|
|
|
||||||
0
tests/target/hil_can/CMakeLists.txt
Normal file
0
tests/target/hil_can/CMakeLists.txt
Normal file
0
tests/target/hil_can/main.c
Normal file
0
tests/target/hil_can/main.c
Normal file
|
|
@ -64,7 +64,7 @@ _CFG = {
|
||||||
"aw_addr": 0x59,
|
"aw_addr": 0x59,
|
||||||
"can_tx": 42,
|
"can_tx": 42,
|
||||||
"can_rx": 43,
|
"can_rx": 43,
|
||||||
"can_baud": 500_000,
|
"can_baud": 125_000,
|
||||||
}
|
}
|
||||||
|
|
||||||
_RST_PIN = 3 # G3_PHY_RST → нога сброса AW9523B
|
_RST_PIN = 3 # G3_PHY_RST → нога сброса AW9523B
|
||||||
|
|
|
||||||
0
tools/hil/test_can.py
Normal file
0
tools/hil/test_can.py
Normal file
|
|
@ -36,11 +36,11 @@ from conftest import uart_cmd
|
||||||
# process() цикл ~10 мс (CLI_RX_TIMEOUT)
|
# process() цикл ~10 мс (CLI_RX_TIMEOUT)
|
||||||
# UART round-trip ~2 мс
|
# UART round-trip ~2 мс
|
||||||
# Итого минимум ~33 мс. Берём с запасом × 3.
|
# Итого минимум ~33 мс. Берём с запасом × 3.
|
||||||
RELAY_ON_S = 0.150 # ждать после включения реле
|
RELAY_ON_S = 0.1 # ждать после включения реле
|
||||||
|
|
||||||
# После выключения нужно убедиться что INACTIVE подтверждён дебаунсом.
|
# После выключения нужно убедиться что INACTIVE подтверждён дебаунсом.
|
||||||
# Берём чуть больше чем RELAY_ON_S — дебаунс работает симметрично.
|
# Берём чуть больше чем RELAY_ON_S — дебаунс работает симметрично.
|
||||||
RELAY_OFF_S = 0.150
|
RELAY_OFF_S = 0.1
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue