Перенесён из репозитория protocan-transport, который подключался сабмодулем в CAN_to_RS485. Кадрирование AA 55 с CRC16 поверх любого байтового потока (RS485, RS232, USB CDC), разбор 29-битного идентификатора, общее адресное пространство регистров и каталог с подпиской на поток значений для SETGUI. Состояние живёт в структурах вызывающего, поэтому в одной прошивке поднимается сколько угодно независимых каналов. Порт STM32F4 (USART + DMA) в комплекте. Хостовые тесты test_transport и test_gui проходят.
446 lines
14 KiB
C
446 lines
14 KiB
C
/**
|
||
* Тесты библиотеки. Платформенных зависимостей нет, собирается любым C99:
|
||
*
|
||
* clang -std=c99 -Wall -Wextra -Iinclude tests/test_transport.c src/pcan_*.c -o test
|
||
*/
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
#include "protocan_transport.h"
|
||
|
||
static int g_fail;
|
||
|
||
#define CHECK(cond) \
|
||
do { \
|
||
if (!(cond)) { \
|
||
printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \
|
||
g_fail++; \
|
||
} \
|
||
} while (0)
|
||
|
||
/* -- CRC ------------------------------------------------------------------- */
|
||
|
||
static void test_crc(void)
|
||
{
|
||
/* Контрольное значение CRC-16/CCITT-FALSE. */
|
||
CHECK(pcan_crc16((const uint8_t *)"123456789", 9) == 0x29B1U);
|
||
}
|
||
|
||
/* -- Кадр ------------------------------------------------------------------ */
|
||
|
||
static void test_encode(void)
|
||
{
|
||
pcan_frame_t f;
|
||
memset(&f, 0, sizeof f);
|
||
f.seq = 1U;
|
||
f.flags = PCAN_FLAG_IDE;
|
||
f.id = 0x1234567UL;
|
||
f.dlc = 2U;
|
||
f.data[0] = 0xAAU;
|
||
f.data[1] = 0xBBU;
|
||
|
||
uint8_t raw[PCAN_FRAME_MAX];
|
||
size_t n = pcan_frame_encode(&f, raw, sizeof raw);
|
||
|
||
/* Эталон согласован с gui/transport.py. */
|
||
static const uint8_t want[] = {
|
||
0xAA, 0x55, 0x08, 0x01, 0x01, 0x67, 0x45, 0x23, 0x01, 0xAA, 0xBB,
|
||
0xFE, 0x14
|
||
};
|
||
CHECK(n == sizeof want);
|
||
CHECK(memcmp(raw, want, sizeof want) == 0);
|
||
|
||
/* Маленький буфер должен отвергаться, а не портить память. */
|
||
CHECK(pcan_frame_encode(&f, raw, 4U) == 0U);
|
||
}
|
||
|
||
static void test_encode_max(void)
|
||
{
|
||
pcan_frame_t f;
|
||
memset(&f, 0, sizeof f);
|
||
f.seq = 255U;
|
||
f.flags = PCAN_FLAG_IDE | PCAN_FLAG_DIR;
|
||
f.id = 0x1FFFFFFFUL;
|
||
f.dlc = 8U;
|
||
for (int i = 0; i < 8; i++) {
|
||
f.data[i] = (uint8_t)(0xF0 + i);
|
||
}
|
||
|
||
uint8_t raw[PCAN_FRAME_MAX];
|
||
size_t n = pcan_frame_encode(&f, raw, sizeof raw);
|
||
|
||
static const uint8_t want[] = {
|
||
0xAA, 0x55, 0x0E, 0xFF, 0x05, 0xFF, 0xFF, 0xFF, 0x1F,
|
||
0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0x6D, 0x0B
|
||
};
|
||
CHECK(n == sizeof want);
|
||
CHECK(n == PCAN_FRAME_MAX);
|
||
CHECK(memcmp(raw, want, sizeof want) == 0);
|
||
}
|
||
|
||
static void test_roundtrip(void)
|
||
{
|
||
pcan_frame_t f;
|
||
memset(&f, 0, sizeof f);
|
||
f.flags = PCAN_FLAG_IDE | PCAN_FLAG_DIR;
|
||
f.id = 0x0ABCDEFUL;
|
||
f.dlc = 5U;
|
||
memcpy(f.data, "HELLO", 5);
|
||
|
||
uint8_t raw[PCAN_FRAME_MAX];
|
||
size_t n = pcan_frame_encode(&f, raw, sizeof raw);
|
||
|
||
pcan_parser_t p;
|
||
pcan_parser_init(&p);
|
||
pcan_frame_t out;
|
||
memset(&out, 0, sizeof out);
|
||
int got = 0;
|
||
for (size_t i = 0; i < n; i++) {
|
||
if (pcan_parser_push(&p, raw[i], &out)) {
|
||
got++;
|
||
}
|
||
}
|
||
CHECK(got == 1);
|
||
CHECK(out.id == f.id);
|
||
CHECK(out.dlc == f.dlc);
|
||
CHECK(out.flags == f.flags);
|
||
CHECK(memcmp(out.data, "HELLO", 5) == 0);
|
||
/* Хвост за DLC должен быть обнулён, а не содержать прошлый кадр. */
|
||
CHECK(out.data[5] == 0U && out.data[6] == 0U && out.data[7] == 0U);
|
||
CHECK(p.stats.frames == 1U);
|
||
}
|
||
|
||
static void test_resync(void)
|
||
{
|
||
pcan_frame_t f;
|
||
memset(&f, 0, sizeof f);
|
||
f.flags = PCAN_FLAG_IDE;
|
||
f.id = 0x100UL;
|
||
f.dlc = 1U;
|
||
f.data[0] = 0x42U;
|
||
|
||
uint8_t raw[PCAN_FRAME_MAX];
|
||
size_t n = pcan_frame_encode(&f, raw, sizeof raw);
|
||
|
||
/* Мусор, оборванная сигнатура и битый LEN перед настоящим кадром. */
|
||
static const uint8_t junk[] = { 0x00, 0xFF, 0xAA, 0xAA, 0x55, 0x03, 0x11, 0xAA };
|
||
|
||
pcan_parser_t p;
|
||
pcan_parser_init(&p);
|
||
pcan_frame_t out;
|
||
int got = 0;
|
||
for (size_t i = 0; i < sizeof junk; i++) {
|
||
if (pcan_parser_push(&p, junk[i], &out)) {
|
||
got++;
|
||
}
|
||
}
|
||
for (size_t i = 0; i < n; i++) {
|
||
if (pcan_parser_push(&p, raw[i], &out)) {
|
||
got++;
|
||
}
|
||
}
|
||
CHECK(got == 1);
|
||
CHECK(out.id == 0x100UL);
|
||
CHECK(out.data[0] == 0x42U);
|
||
CHECK(p.stats.bad_len == 1U);
|
||
}
|
||
|
||
static void test_bad_crc(void)
|
||
{
|
||
pcan_frame_t f;
|
||
memset(&f, 0, sizeof f);
|
||
f.flags = PCAN_FLAG_IDE;
|
||
f.id = 0x55UL;
|
||
f.dlc = 3U;
|
||
|
||
uint8_t raw[PCAN_FRAME_MAX];
|
||
size_t n = pcan_frame_encode(&f, raw, sizeof raw);
|
||
raw[n - 1U] ^= 0xFFU;
|
||
|
||
pcan_parser_t p;
|
||
pcan_parser_init(&p);
|
||
pcan_frame_t out;
|
||
int got = 0;
|
||
for (size_t i = 0; i < n; i++) {
|
||
if (pcan_parser_push(&p, raw[i], &out)) {
|
||
got++;
|
||
}
|
||
}
|
||
CHECK(got == 0);
|
||
CHECK(p.stats.crc_errors == 1U);
|
||
}
|
||
|
||
/* -- Идентификатор --------------------------------------------------------- */
|
||
|
||
static void test_id(void)
|
||
{
|
||
pcan_id_t id = { 0x1234U, PCAN_MSG_GAS, 2U, 1U,
|
||
PCAN_ROUTE_FROM_DEVICE, PCAN_PRIORITY_STANDARD };
|
||
uint32_t raw = pcan_id_pack(&id);
|
||
CHECK(raw == 0x19231234UL);
|
||
|
||
pcan_id_t back;
|
||
pcan_id_unpack(raw, &back);
|
||
CHECK(back.msg_body == id.msg_body);
|
||
CHECK(back.msg_type == id.msg_type);
|
||
CHECK(back.device_id == id.device_id);
|
||
CHECK(back.device_type == id.device_type);
|
||
CHECK(back.route == id.route);
|
||
CHECK(back.priority == id.priority);
|
||
|
||
/* Раскладки MsgBody из protocan.h */
|
||
CHECK(pcan_body_broadcast(3U, 0U) == 0x0030U);
|
||
CHECK(pcan_body_typed(4U, 17U) == 0x4011U);
|
||
CHECK(pcan_body_modbus(0x100U, 3U) == 0x1003U);
|
||
CHECK(pcan_body_error(0U, 0xFFU) == 0x00FFU);
|
||
}
|
||
|
||
/* -- Кольцевой буфер ------------------------------------------------------- */
|
||
|
||
static void test_ring(void)
|
||
{
|
||
uint8_t storage[16];
|
||
pcan_ring_t rb;
|
||
|
||
CHECK(pcan_ring_init(&rb, storage, 16U));
|
||
/* Размер не степень двойки - отказ, а не тихая порча индексов. */
|
||
pcan_ring_t bad;
|
||
CHECK(!pcan_ring_init(&bad, storage, 15U));
|
||
|
||
CHECK(pcan_ring_free(&rb) == 15U);
|
||
CHECK(pcan_ring_write(&rb, (const uint8_t *)"0123456789", 10U));
|
||
CHECK(pcan_ring_count(&rb) == 10U);
|
||
/* Не влезает - не должно попасть ни одного байта. */
|
||
CHECK(!pcan_ring_write(&rb, (const uint8_t *)"ABCDEF", 6U));
|
||
CHECK(pcan_ring_count(&rb) == 10U);
|
||
CHECK(rb.dropped == 6U);
|
||
|
||
/* Линейный участок для DMA. */
|
||
const uint8_t *ptr = NULL;
|
||
uint16_t lin = pcan_ring_linear(&rb, &ptr);
|
||
CHECK(lin == 10U);
|
||
CHECK(ptr == storage);
|
||
pcan_ring_consume(&rb, lin);
|
||
CHECK(pcan_ring_empty(&rb));
|
||
|
||
/* Заворот: линейный участок обязан обрываться на конце буфера. */
|
||
CHECK(pcan_ring_write(&rb, (const uint8_t *)"ABCDEFGH", 8U));
|
||
lin = pcan_ring_linear(&rb, &ptr);
|
||
CHECK(lin == 6U); /* от tail=10 до конца буфера (16) */
|
||
CHECK(ptr == &storage[10]);
|
||
pcan_ring_consume(&rb, lin);
|
||
lin = pcan_ring_linear(&rb, &ptr);
|
||
CHECK(lin == 2U);
|
||
CHECK(ptr == storage);
|
||
}
|
||
|
||
/* -- Общее адресное пространство ------------------------------------------- */
|
||
|
||
static uint16_t s_holding[8];
|
||
static uint16_t s_computed_calls;
|
||
|
||
static pcan_gas_status_t computed_read(const pcan_gas_region_t *r,
|
||
uint16_t offset, uint16_t *value)
|
||
{
|
||
(void)r;
|
||
s_computed_calls++;
|
||
*value = (uint16_t)(0xA000U + offset);
|
||
return PCAN_GAS_OK;
|
||
}
|
||
|
||
static const pcan_gas_region_t s_regions[] = {
|
||
{ 0x0000U, 8U, s_holding, NULL, NULL, 0U, NULL, "holding" },
|
||
{ 0xFF00U, 4U, NULL, computed_read, NULL, PCAN_GAS_RDONLY, NULL, "diag" },
|
||
};
|
||
|
||
static const pcan_gas_map_t s_map = { s_regions, 2U };
|
||
|
||
static void test_gas(void)
|
||
{
|
||
CHECK(pcan_gas_map_validate(&s_map));
|
||
|
||
/* Перекрытие регионов должно отвергаться. */
|
||
static const pcan_gas_region_t overlap[] = {
|
||
{ 0x0000U, 8U, s_holding, NULL, NULL, 0U, NULL, NULL },
|
||
{ 0x0004U, 8U, s_holding, NULL, NULL, 0U, NULL, NULL },
|
||
};
|
||
const pcan_gas_map_t bad = { overlap, 2U };
|
||
CHECK(!pcan_gas_map_validate(&bad));
|
||
|
||
uint16_t v = 0U;
|
||
CHECK(pcan_gas_write(&s_map, 0x0002U, 0x1234U) == PCAN_GAS_OK);
|
||
CHECK(pcan_gas_read(&s_map, 0x0002U, &v) == PCAN_GAS_OK);
|
||
CHECK(v == 0x1234U);
|
||
CHECK(s_holding[2] == 0x1234U);
|
||
|
||
/* Дырка в карте. */
|
||
CHECK(pcan_gas_read(&s_map, 0x0100U, &v) == PCAN_GAS_NO_REG);
|
||
/* Регион только для чтения. */
|
||
CHECK(pcan_gas_write(&s_map, 0xFF00U, 1U) == PCAN_GAS_READ_ONLY);
|
||
/* Колбэк вместо памяти. */
|
||
CHECK(pcan_gas_read(&s_map, 0xFF02U, &v) == PCAN_GAS_OK);
|
||
CHECK(v == 0xA002U);
|
||
CHECK(s_computed_calls == 1U);
|
||
|
||
/* Блок обрывается на первом адресе вне карты. */
|
||
uint16_t block[8];
|
||
CHECK(pcan_gas_read_block(&s_map, 0x0006U, block, 8U) == 2U);
|
||
}
|
||
|
||
static void test_gas_frames(void)
|
||
{
|
||
for (int i = 0; i < 8; i++) {
|
||
s_holding[i] = (uint16_t)(0x1000 + i);
|
||
}
|
||
|
||
/* Запрос на чтение: GAS, DLC = 0, адрес в MsgBody. */
|
||
pcan_id_t id = { 0x0002U, PCAN_MSG_GAS, 2U, 1U,
|
||
PCAN_ROUTE_FROM_PM, PCAN_PRIORITY_STANDARD };
|
||
pcan_frame_t req;
|
||
memset(&req, 0, sizeof req);
|
||
req.id = pcan_id_pack(&id);
|
||
req.flags = PCAN_FLAG_IDE;
|
||
req.dlc = 0U;
|
||
|
||
pcan_frame_t rsp;
|
||
memset(&rsp, 0, sizeof rsp);
|
||
CHECK(pcan_gas_handle(&s_map, &req, &rsp));
|
||
CHECK(rsp.dlc == 8U); /* 4 регистра */
|
||
|
||
pcan_id_t rid;
|
||
pcan_id_unpack(rsp.id, &rid);
|
||
CHECK(rid.msg_type == PCAN_MSG_GAS);
|
||
CHECK(rid.msg_body == 0x0002U);
|
||
CHECK(rid.route == PCAN_ROUTE_FROM_DEVICE);
|
||
|
||
/* Регистры лежат младшим байтом вперёд - как в SETCAN. */
|
||
CHECK(rsp.data[0] == 0x02U && rsp.data[1] == 0x10U);
|
||
uint16_t regs[4];
|
||
CHECK(pcan_gas_frame_to_regs(&rsp, regs, 4U) == 4U);
|
||
CHECK(regs[0] == 0x1002U && regs[3] == 0x1005U);
|
||
|
||
/* Запись: GAS с данными, ответа быть не должно. */
|
||
pcan_frame_t wr;
|
||
memset(&wr, 0, sizeof wr);
|
||
id.msg_body = 0x0000U;
|
||
wr.id = pcan_id_pack(&id);
|
||
wr.flags = PCAN_FLAG_IDE;
|
||
wr.dlc = 4U;
|
||
wr.data[0] = 0xEFU; wr.data[1] = 0xBEU;
|
||
wr.data[2] = 0xADU; wr.data[3] = 0xDEU;
|
||
CHECK(!pcan_gas_handle(&s_map, &wr, &rsp));
|
||
CHECK(s_holding[0] == 0xBEEFU);
|
||
CHECK(s_holding[1] == 0xDEADU);
|
||
|
||
/* Чтение по адресу вне карты не должно порождать ответ. */
|
||
id.msg_body = 0x0500U;
|
||
req.id = pcan_id_pack(&id);
|
||
CHECK(!pcan_gas_handle(&s_map, &req, &rsp));
|
||
}
|
||
|
||
/* -- Канал связи ----------------------------------------------------------- */
|
||
|
||
static uint8_t s_line[256];
|
||
static size_t s_line_len;
|
||
static size_t s_line_space = sizeof s_line;
|
||
|
||
static size_t fake_write(void *ctx, const uint8_t *data, size_t len)
|
||
{
|
||
(void)ctx;
|
||
if ((s_line_len + len) > s_line_space) {
|
||
return 0U; /* всё-или-ничего */
|
||
}
|
||
memcpy(&s_line[s_line_len], data, len);
|
||
s_line_len += len;
|
||
return len;
|
||
}
|
||
|
||
static size_t fake_space(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
return s_line_space - s_line_len;
|
||
}
|
||
|
||
static int s_rx_count;
|
||
static pcan_frame_t s_rx_last;
|
||
|
||
static void on_frame(const pcan_frame_t *f, void *user)
|
||
{
|
||
(void)user;
|
||
s_rx_count++;
|
||
s_rx_last = *f;
|
||
}
|
||
|
||
static void test_link(void)
|
||
{
|
||
pcan_io_t io = { fake_write, fake_space, NULL };
|
||
pcan_link_t link;
|
||
CHECK(pcan_link_init(&link, &io, on_frame, NULL));
|
||
|
||
s_line_len = 0U;
|
||
s_rx_count = 0;
|
||
|
||
/* SEQ должен инкрементироваться сам. */
|
||
for (int i = 0; i < 3; i++) {
|
||
pcan_frame_t f;
|
||
memset(&f, 0, sizeof f);
|
||
f.flags = PCAN_FLAG_IDE;
|
||
f.id = (uint32_t)(0x200 + i);
|
||
f.dlc = 1U;
|
||
f.data[0] = (uint8_t)i;
|
||
CHECK(pcan_link_send(&link, &f));
|
||
CHECK(f.seq == (uint8_t)i);
|
||
}
|
||
CHECK(link.tx.tx_frames == 3U);
|
||
|
||
/* Тот же поток скармливаем обратно, по кускам произвольной длины. */
|
||
size_t off = 0U;
|
||
while (off < s_line_len) {
|
||
size_t chunk = ((s_line_len - off) > 5U) ? 5U : (s_line_len - off);
|
||
pcan_link_feed(&link, &s_line[off], chunk);
|
||
off += chunk;
|
||
}
|
||
CHECK(s_rx_count == 3);
|
||
CHECK(s_rx_last.id == 0x202UL);
|
||
CHECK(s_rx_last.seq == 2U);
|
||
CHECK(pcan_link_rx_stats(&link)->frames == 3U);
|
||
|
||
/* Переполнение очереди: кадр не должен уйти обрывком. */
|
||
s_line_space = 4U;
|
||
s_line_len = 0U;
|
||
pcan_frame_t big;
|
||
memset(&big, 0, sizeof big);
|
||
big.flags = PCAN_FLAG_IDE;
|
||
big.dlc = 8U;
|
||
uint8_t seq_before = link.tx_seq;
|
||
CHECK(!pcan_link_send(&link, &big));
|
||
CHECK(s_line_len == 0U);
|
||
CHECK(link.tx.tx_dropped == 1U);
|
||
/* SEQ не двигается на неотправленном кадре. */
|
||
CHECK(link.tx_seq == seq_before);
|
||
CHECK(!pcan_link_can_send(&link));
|
||
s_line_space = sizeof s_line;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
test_crc();
|
||
test_encode();
|
||
test_encode_max();
|
||
test_roundtrip();
|
||
test_resync();
|
||
test_bad_crc();
|
||
test_id();
|
||
test_ring();
|
||
test_gas();
|
||
test_gas_frames();
|
||
test_link();
|
||
|
||
if (g_fail == 0) {
|
||
printf("all tests passed\n");
|
||
return 0;
|
||
}
|
||
printf("%d check(s) failed\n", g_fail);
|
||
return 1;
|
||
}
|