129а. Модульные тесты прошивки на ПК: подмена железа, свой маленький раннер и покрытие
Раздел 129 искал ошибки, не запуская код: компилятор, cppcheck, анализ путей. Раздел 184 запускал на компьютере чистую логику — разборщик кадров, в котором нет ни одного регистра. Здесь следующий шаг: код, который трогает железо. Драйвер пишет в регистры USART, зовёт HAL — и его тоже можно гонять на ПК, если подменить три вещи: адреса периферии, ядро CMSIS и часть функций HAL. Исходники драйвера при этом не меняются ни на букву: те же файлы собирает armclang для платы.
Подопытный — драйвер RS-485 из раздела 16а: передача по прерываниям, DE
снимается по флагу TC. Там его проверяла модель USART по времени; здесь —
модульные тесты, каждый про одно правило драйвера. Рядом — тот же обмен
на HAL, чтобы показать подмену функций при компоновке.
Опоры: заголовок stm32f10x.h из пака Keil STM32F1xx_DFP 2.4.1 (тот же,
что в 16а), stm32f103xb.h и HAL из архива STM32CubeF1 1.8.7, справочное
руководство RM0008 (смещения и сброс регистров USART и GPIO). Всё собрано
и прогнано в образе stm-lab:1 (gcc 12), каталог _verify/utest129b;
драйверы собраны armclang для Cortex-M3. Известные готовые каркасы —
Unity, генератор подделок CMock и сборщик Ceedling — здесь не запускались:
всё нужное умещается в свой раннер на 52 строки.
Как это называется в документации
Оригинал Перевод unit test модульный тест test runner раннер: программа, которая запускает тесты и считает провалы fake, stub, mock подделка, заглушка, подделка с проверкой вызовов test double подмена зависимости в тесте seam шов: место, где зависимость можно подменить weak symbol ( __weak)слабый символ: уступает обычному определению --wrap=symbolключ компоновщика: вызовы уходят в __wrap_symbolcode coverage: line / branch покрытие: по строкам, по ветвям characterization test тест, который фиксирует поведение чужого кода
129а.1 Где шов между драйвером и железом
У драйвера три зависимости от платы, и у каждой свой шов:
| Зависимость | Как выглядит в коде | Чем подменяется на ПК |
|---|---|---|
| регистры периферии | USART2->CR1, GPIOA->BSRR |
структура в ОЗУ вместо адреса |
| ядро | NVIC_EnableIRQ, SysTick из core_cm3.h |
свой core_cm3.h раньше в пути поиска |
| функции HAL | HAL_GPIO_WritePin, HAL_UART_Transmit |
своя функция при компоновке |
Первые два шва делает препроцессор, третий — компоновщик. Код драйвера
не знает, где его собирают: ни одного #ifdef TEST в нём нет. Если в
драйвере появляется #ifdef ради тестов, тесты проверяют уже не тот код,
который уйдёт в плату.
129а.2 Раннер на 52 строки
Раннер из 184.3 считал проверки и провалы в одном процессе. Для драйвера
этого мало. У драйвера есть статическое состояние — флаг занятости
tx_busy, указатель на данные, — и сбросить его между тестами снаружи
нельзя: переменные static. Тест, оставивший флаг поднятым, вешает
следующий. Поэтому каждый тест идёт в своём процессе:
static int tt_run(const struct tt_case *c, int n, int argc, char **argv)
{
int run = 0, bad = 0;
for (int i = 0; i < n; i++) {
if (argc > 1 && strstr(c[i].name, argv[1]) == NULL)
continue; /* фильтр по имени: ./tests uart */
run++;
fflush(stdout);
pid_t pid = fork();
if (pid == 0) { /* ребёнок: один тест */
alarm(1); /* повис — через секунду SIGALRM */
setup();
c[i].fn();
printf("ok %s, проверок: %d\n", c[i].name, tt_checks);
exit(0); /* exit, а не _exit: gcov допишет счётчики */
}
int st = 0;
waitpid(pid, &st, 0);
if (WIFEXITED(st) && WEXITSTATUS(st) == 0)
continue;
bad++;
printf("%s %s\n", WIFSIGNALED(st) && WTERMSIG(st) == SIGALRM ? "HANG"
: WIFSIGNALED(st) ? "CRASH" : "FAIL", c[i].name);
}
printf("TOTAL tests=%d failed=%d\n", run, bad);
return bad != 0;
}
Процесс на тест даёт сразу три вещи. Статическое состояние у каждого теста
своё, с нуля. Зависший тест снимает будильник через секунду — раннер пишет
HANG и идёт дальше. Обращение по битому указателю роняет только ребёнка —
раннер пишет CRASH. Проверки CHECK и CHECK_EQ при провале печатают
файл, строку, выражение и оба значения и завершают процесс. Аргумент
командной строки — фильтр: ./t de_ запустил три теста из шести. Код
возврата не ноль при любом провале — этого достаточно конвейеру из
раздела 110.
fork есть в Linux и в контейнере, где идут тесты. Раннеру это и нужно:
на плате он не работает, на плате работают драйверы.
129а.3 Регистры: структура вместо адреса
В заголовке устройства периферия — это тип и адрес:
#define USART2 ((USART_TypeDef *) USART2_BASE)
Тип USART_TypeDef описывает раскладку регистров, адрес — где она лежит.
Для теста нужен тот же тип, но в памяти ПК. Заголовок устройства берём
настоящий, из пака, и после него подключаем свой:
extern USART_TypeDef fake_usart2;
extern GPIO_TypeDef fake_gpioa;
extern RCC_TypeDef fake_rcc;
#undef USART2
#undef GPIOA
#undef RCC
#define USART2 (&fake_usart2) /* было ((USART_TypeDef *) USART2_BASE) */
#define GPIOA (&fake_gpioa)
#define RCC (&fake_rcc)
Раскладка совпадает сама: тип один и тот же. Для верности в том же файле
_Static_assert сверяет смещения с картой регистров RM0008: USART_DR —
0x04, USART_CR1 — 0x0C, GPIOx_BSRR — 0x10. Поменяется заголовок —
тест не соберётся.
Заголовок устройства тянет core_cm3.h, а в нём ассемблерные вставки для
Cortex-M — gcc для ПК их не соберёт. Заголовок пака подключает ядро в
кавычках, поэтому достаточно положить свой core_cm3.h в каталог host/
и поставить его в пути поиска раньше CMSIS. В нём — только то, что нужно:
квалификаторы __IO, __I, __O и их пары __IOM/__IM/__OM,
структура SysTick_Type и NVIC_EnableIRQ, которая просто запоминает
номер прерывания. Сборка драйвера на ПК:
gcc -std=c11 -Wall -Wextra -DSTM32F10X_MD -I host -I <пак>/Device/Include \
-include stm32f10x.h -include fake_periph.h -c rs485_f103.c
Ни одного предупреждения — ни у драйвера, ни у тестов.
Чего структура не умеет. Она помнит только последнее записанное
значение: порядок записей не виден, и тест «DE поднят раньше, чем
разрешено прерывание» так не написать. Она не знает побочных действий
железа: в USART пара «чтение SR — запись DR» сбрасывает TC, а у
структуры ничего не сбрасывается. Поэтому флаги тест выставляет руками,
перед каждым вызовом обработчика. Где важны время и побочные действия —
нужна модель периферии, как в 16а.4. Модульный тест проверяет другое:
что драйвер делает в ответ на данное состояние регистров.
129а.4 Тесты драйвера RS-485
Файл rs485_f103.c взят из 16а без правок. Шесть тестов — по одному на
правило драйвера:
| Тест | Правило | Проверок |
|---|---|---|
init_9600_8E1_listening |
BRR = 3750, 8E1, DE = 0, прерывание USART2 разрешено |
4 |
send_raises_de_and_mutes_rx |
при отправке DE = 1, приём выключен, TCIE ещё нет |
4 |
bytes_in_order_de_held |
байты уходят по порядку, после последнего DE ещё поднят | 6 |
de_drops_on_tc |
по TC DE = 0, приём включён, драйвер свободен |
7 |
rx_byte_to_parser |
принятый байт уходит в разборщик | 2 |
zero_length_ignored |
пустая отправка ничего не трогает | 2 |
Два из них целиком:
static void send_all(void) /* TXE и TC подняты, как после сброса */
{
rs485_init();
rs485_send(req, sizeof req);
for (unsigned i = 0; i < sizeof req; i++) {
fake_usart2.SR = USART_SR_TXE | USART_SR_TC;
USART2_IRQHandler();
CHECK_EQ(fake_usart2.DR, req[i]);
}
}
TEST(bytes_in_order_de_held)
{
send_all();
CHECK_EQ(fake_gpioa.BSRR, 1u << 1); /* последний байт ещё в линии */
CHECK_EQ(fake_usart2.CR1 & USART_CR1_TXEIE, 0);
CHECK(fake_usart2.CR1 & USART_CR1_TCIE);
}
TEST(de_drops_on_tc)
{
send_all();
fake_usart2.SR = USART_SR_TXE | USART_SR_TC;
USART2_IRQHandler();
CHECK_EQ(fake_gpioa.BSRR, 1u << 17); /* DE = 0 */
CHECK(fake_usart2.CR1 & USART_CR1_RE);
CHECK_EQ(fake_usart2.CR1 & USART_CR1_TCIE, 0);
rs485_send(req, 1); /* не занят: не повиснет */
CHECK_EQ(fake_gpioa.BSRR, 1u << 1);
}
Флаги в send_all — TXE и TC вместе, как в USART_SR после сброса
(0x00C0 по RM0008). Это не случайность: TC поднят с самого сброса, и
драйвер обязан не принять его за конец своей передачи. Шесть тестов —
25 проверок, все прошли.
129а.5 HAL: подмена при компоновке
Второй драйвер — тот же обмен на HAL:
HAL_StatusTypeDef rs485_hal_send(UART_HandleTypeDef *hu, const uint8_t *p, uint16_t n)
{
HAL_StatusTypeDef st;
if (n == 0u)
return HAL_OK;
HAL_GPIO_WritePin(DE_PORT, DE_PIN, GPIO_PIN_SET);
st = HAL_UART_Transmit(hu, p, n, rs485_timeout_ms(n));
HAL_GPIO_WritePin(DE_PORT, DE_PIN, GPIO_PIN_RESET); /* иначе линия занята навсегда */
return st;
}
HAL_UART_Transmit в STM32CubeF1 блокирующий и в конце сам ждёт UART_FLAG_TC,
поэтому DE снимается сразу после него. Таймаут — 2 мс на байт плюс 10 мс:
восемь байт 8E1 на 9600 бод идут 9,17 мс (16а), таймаут — 26 мс.
В тестовую сборку входят настоящие stm32f1xx_hal.c и
stm32f1xx_hal_gpio.c из STM32CubeF1 — они собираются gcc на ПК с тем же
подменённым ядром без единого предупреждения. Три функции HAL
подменены тремя разными способами:
| Функция | Способ | Когда годится |
|---|---|---|
HAL_UART_Transmit |
своя функция, stm32f1xx_hal_uart.c в сборку не входит |
весь файл HAL не нужен |
HAL_GetTick |
обычное определение в тесте побеждает __weak из stm32f1xx_hal.c |
функция в HAL слабая |
HAL_GPIO_WritePin |
-Wl,--wrap=HAL_GPIO_WritePin: журнал, потом настоящая |
нужен и журнал, и настоящий код |
void __real_HAL_GPIO_WritePin(GPIO_TypeDef *port, uint16_t pin, GPIO_PinState s);
void __wrap_HAL_GPIO_WritePin(GPIO_TypeDef *port, uint16_t pin, GPIO_PinState s)
{
if (port == GPIOA && pin == GPIO_PIN_1 && nlog < 8)
log_[nlog++].what = s == GPIO_PIN_SET ? DE_SET : DE_RESET;
__real_HAL_GPIO_WritePin(port, pin, s); /* пишет в fake_gpioa.BSRR */
}
С --wrap вызовы HAL_GPIO_WritePin из драйвера уходят в
__wrap_HAL_GPIO_WritePin, а __real_… — это исходная функция HAL. Тест
видит порядок вызовов — то, чего не видела структура регистров:
DE = 1, HAL_UART_Transmit(n = 8, таймаут 26), DE = 0. И видит результат
настоящего кода HAL: в fake_gpioa.BSRR лежит GPIO_PIN_1 << 16.
Пять тестов HAL — 269 проверок (255 из них — таймаут для длин от 1 до 255
байт). Пятый тест проверяет не наш код, а HAL: HAL_Delay(30) с часами
теста, которые прибавляют 1 мс на каждый вызов HAL_GetTick, читает 31
тик. Функция добавляет к задержке uwTickFreq, чтобы гарантировать
минимум, — ждать приходится на тик больше заказанного. Такой тест
фиксирует поведение чужого кода: обновится HAL — тест скажет, если
поведение поменялось.
--wrap не видит вызовов внутри своего файла. Та же подделка часов,
подключённая через --wrap=HAL_GetTick, а не обычным определением,
вешает тест HAL_Delay: раннер пишет HANG. HAL_Delay и HAL_GetTick
лежат в одном stm32f1xx_hal.c, ссылка между ними разрешена ещё при
компиляции, и компоновщику подменять нечего — HAL_Delay крутится на
настоящем HAL_GetTick, который на ПК не растёт. Слабый символ такой
ловушки не имеет: nm показывает у HAL_GetTick и HAL_Delay в
stm32f1xx_hal.o букву W, и обычное определение из теста заменяет
функцию для всех вызовов.
129а.6 Подложенные ошибки, покрытие и сборка под armclang
Зелёные тесты ещё ничего не доказывают — надо убедиться, что они краснеют.
Скрипт mutants.py вносит в копию драйвера одну правку и гоняет тесты
(подход 184.5, но правки здесь ручные — это ошибки, которые на RS-485
делают на самом деле):
| Ошибка | Какой тест упал |
|---|---|
DE снят по TXE последнего байта |
bytes_in_order_de_held |
TCIE разрешён сразу при отправке |
send_raises_de_and_mutes_rx |
| приём не включён после передачи | de_drops_on_tc |
| флаг занятости не сброшен | de_drops_on_tc — HANG |
| на HAL: при ошибке выход до снятия DE | de_released_on_timeout |
| на HAL: таймаут без запаса, 1 мс на байт | de_around_transmit, timeout_covers_frame |
Шесть из шести. Сообщение сразу говорит, что не так:
/v/test_rs485.c:64: fake_gpioa.BSRR == 1u << 1: 131072 != 2
131072 — это 1 << 17: DE сняли, пока последний байт ещё в линии.
Четвёртая ошибка без процесса на тест повесила бы весь прогон; с ним —
одна строка HANG.
Покрытие — gcc --coverage и gcov -b в том же образе:
| Прогон | Строк из 34 | Ветвей взято из 16 |
|---|---|---|
| все 6 тестов | 100,00 % | 81,25 % |
| без теста приёма | 97,06 % | 68,75 % |
Без теста приёма gcov помечает ##### ровно одну строку — вызов
rs485_rx_byte. Полный набор проходит все строки, но три ветви из 16 не
берёт ни разу: тело ожидания while (tx_busy) (в однопоточном тесте
драйвер никогда не занят при отправке) и два случая в обработчике — «TXEIE
разрешён, а TXE нет» и «TCIE разрешён, а TC нет». Это и есть польза
покрытия по ветвям: оно показывает, какие сочетания флагов не пробовали.
rs485_hal.c — 9 строк и 2 ветви, покрыто всё.
llvm-cov есть только в образе stm-rust:1 — из набора Rust,
LLVM 22.1.8. Файлы покрытия gcc 14 из того же образа он не читает («Invalid .gcno File!»).
clang 14 из stm-sys:1 тесты собирает и проходит, но покрытие в нём не
собрать: нет библиотеки libclang_rt.profile. Поэтому покрытие здесь —
gcov.
Сборка для платы — те же файлы, armclang, Cortex-M3, -Wall -Wextra:
rs485_f103.c с stm32f10x.h из пака Keil и настоящим core_cm3.h из
CMSIS; rs485_hal.c и stm32f1xx_hal.c — с заголовками STM32CubeF1 и тем
же stm32f1xx_hal_conf.h, что у тестов. Ни одного предупреждения. В
объекте HAL, собранном armclang, HAL_GetTick и HAL_Delay — тоже слабые
(Wk в fromelf -s): подмена через обычное определение работает и в
прошивке. HAL для того их и делает слабыми — в комментарии к
stm32f1xx_hal.c сказано, что функции отсчёта времени объявлены __weak,
чтобы их можно было заменить своими.
129а.7 Грабли
Тест вешает следующий. Статическое состояние драйвера (tx_busy)
переживает тест, если все тесты идут в одном процессе. Процесс на тест
или функция сброса в самом драйвере — третьего не дано.
В драйвере #ifdef TEST. Тесты проверяют сборку, которая в плату не
попадёт. Швы — в заголовках и в компоновке, не в коде драйвера.
Свой заголовок регистров вместо настоящего. Переписанная руками
структура однажды разойдётся с паком. Настоящий заголовок и
_Static_assert на смещения по RM0008 этого не допустят.
Структура регистров принята за модель железа. Она не сбрасывает TC
по записи в DR и не помнит порядок записей. Время и побочные действия —
модель, как в 16а.4; порядок вызовов — журнал подделки.
--wrap на функцию, которую зовут из того же файла. Вызовы изнутри
stm32f1xx_hal.c мимо обёртки: HAL_Delay висит на настоящем
HAL_GetTick. Для слабых функций HAL — обычное определение.
Ожидание в тесте считают по тем же формулам, что и в коде. Тест таймаута сравнивает с временем кадра, посчитанным отдельно, — поэтому таймаут «1 мс на байт» не прошёл.
_exit в дочернем процессе. Счётчики gcov пишутся при обычном
exit; с _exit покрытие молча окажется нулевым.
129а.8 Итоги
- Регистры подменяются без правки драйвера: настоящий заголовок пака,
после него
#undefи#define USART2 (&fake_usart2); ядро — свойcore_cm3.hраньше CMSIS в пути поиска. - Раннер — 52 строки: процесс на тест, будильник на 1 с, фильтр по имени, код возврата. 11 тестов: 6 на регистрах (25 проверок), 5 на HAL (269).
- HAL подменяется при компоновке тремя способами: не собирать файл,
перекрыть
__weak, обернуть через--wrap.--wrapне видит вызовов внутри своего файла — тестHAL_Delayтак повисает. - Настоящий
HAL_Delay(30)на часах теста ждёт 31 тик. - 6 подложенных ошибок из 6 пойманы, одна из них — через
HANG. - Покрытие драйвера на регистрах: строк 100 %, ветвей взято 81,25 % — три не пройденные ветви gcov показывает по строкам.
- Те же исходники собирает armclang для STM32F103 без предупреждений.
Полные исходники
tiny_test.h — раннер модульных тестов: каждый тест в своём процессе.
/* 129а: маленький раннер модульных тестов. Каждый тест — в своём процессе (fork):
статические переменные модуля не переходят из теста в тест, зависание ловит
будильник, падение — сигнал. Провал проверки печатает файл и строку. */
#ifndef TINY_TEST_H
#define TINY_TEST_H
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
struct tt_case { const char *name; void (*fn)(void); };
#define TEST(n) static void n(void)
#define CASE(n) { #n, n }
static int tt_checks;
#define TT_FAIL(...) do { printf(" %s:%d: ", __FILE__, __LINE__); printf(__VA_ARGS__); exit(1); } while (0)
#define CHECK(c) do { tt_checks++; if (!(c)) TT_FAIL("%s\n", #c); } while (0)
#define CHECK_EQ(a, b) do { long long a_ = (long long)(a), b_ = (long long)(b); tt_checks++; \
if (a_ != b_) TT_FAIL("%s == %s: %lld != %lld\n", #a, #b, a_, b_); } while (0)
void setup(void); /* у каждого набора своя: сбросить подделки */
static int tt_run(const struct tt_case *c, int n, int argc, char **argv)
{
int run = 0, bad = 0;
for (int i = 0; i < n; i++) {
if (argc > 1 && strstr(c[i].name, argv[1]) == NULL)
continue; /* фильтр по имени: ./tests uart */
run++;
fflush(stdout);
pid_t pid = fork();
if (pid == 0) { /* ребёнок: один тест */
alarm(1); /* повис — через секунду SIGALRM */
setup();
c[i].fn();
printf("ok %s, проверок: %d\n", c[i].name, tt_checks);
exit(0); /* exit, а не _exit: gcov допишет счётчики */
}
int st = 0;
waitpid(pid, &st, 0);
if (WIFEXITED(st) && WEXITSTATUS(st) == 0)
continue;
bad++;
printf("%s %s\n", WIFSIGNALED(st) && WTERMSIG(st) == SIGALRM ? "HANG"
: WIFSIGNALED(st) ? "CRASH" : "FAIL", c[i].name);
}
printf("TOTAL tests=%d failed=%d\n", run, bad);
return bad != 0;
}
#endif
fake_periph.h — периферия как обычные структуры в ОЗУ компьютера.
/* 129а: периферия — обычные структуры в ОЗУ ПК. Тип тот же, что в заголовке
пака, поэтому и раскладка та же; меняется только адрес. Подключается
ПОСЛЕ заголовка устройства: -include stm32f10x.h -include fake_periph.h */
#ifndef FAKE_PERIPH_H
#define FAKE_PERIPH_H
#include <stddef.h>
extern USART_TypeDef fake_usart2;
extern GPIO_TypeDef fake_gpioa;
extern RCC_TypeDef fake_rcc;
#undef USART2
#undef GPIOA
#undef RCC
#define USART2 (&fake_usart2) /* было ((USART_TypeDef *) USART2_BASE) */
#define GPIOA (&fake_gpioa)
#define RCC (&fake_rcc)
_Static_assert(offsetof(USART_TypeDef, DR) == 0x04 && offsetof(USART_TypeDef, CR1) == 0x0C,
"смещения USART_DR и USART_CR1 — как в карте регистров RM0008");
_Static_assert(offsetof(GPIO_TypeDef, BSRR) == 0x10, "GPIOx_BSRR — смещение 0x10");
#endif
test_rs485.c — тесты драйвера RS-485 из раздела 16а.
/* 129а: модульные тесты драйвера RS-485 из раздела 16а (rs485_f103.c без правок).
Сборка на ПК: -DSTM32F10X_MD -include stm32f10x.h -include fake_periph.h,
каталог host/ с подменой core_cm3.h — раньше каталога CMSIS. */
#include "tiny_test.h"
USART_TypeDef fake_usart2;
GPIO_TypeDef fake_gpioa;
RCC_TypeDef fake_rcc;
int fake_nvic_irq;
void rs485_init(void);
void rs485_send(const uint8_t *p, uint32_t n);
void USART2_IRQHandler(void);
static uint8_t rx_log[8];
static int rx_count;
void rs485_rx_byte(uint8_t b) { rx_log[rx_count++ & 7] = b; } /* подделка разборщика 16.8 */
void setup(void)
{
memset(&fake_usart2, 0, sizeof fake_usart2);
memset(&fake_gpioa, 0, sizeof fake_gpioa);
memset(&fake_rcc, 0, sizeof fake_rcc);
fake_nvic_irq = -1;
rx_count = 0;
}
static const uint8_t req[3] = { 0x01, 0x03, 0x7F };
TEST(init_9600_8E1_listening)
{
rs485_init();
CHECK_EQ(fake_usart2.BRR, 3750); /* 36 МГц / 9600 */
CHECK_EQ(fake_usart2.CR1, USART_CR1_UE | USART_CR1_M | USART_CR1_PCE | USART_CR1_TE
| USART_CR1_RE | USART_CR1_RXNEIE);
CHECK_EQ(fake_gpioa.BSRR, (1u << 17) | (1u << 3)); /* DE = 0, подтяжка RX */
CHECK_EQ(fake_nvic_irq, USART2_IRQn);
}
TEST(send_raises_de_and_mutes_rx)
{
rs485_init();
rs485_send(req, sizeof req);
CHECK_EQ(fake_gpioa.BSRR, 1u << 1); /* DE = 1 */
CHECK_EQ(fake_usart2.CR1 & USART_CR1_RE, 0);
CHECK(fake_usart2.CR1 & USART_CR1_TXEIE);
CHECK_EQ(fake_usart2.CR1 & USART_CR1_TCIE, 0); /* TC = 1 с самого сброса */
}
static void send_all(void) /* TXE и TC подняты, как после сброса */
{
rs485_init();
rs485_send(req, sizeof req);
for (unsigned i = 0; i < sizeof req; i++) {
fake_usart2.SR = USART_SR_TXE | USART_SR_TC;
USART2_IRQHandler();
CHECK_EQ(fake_usart2.DR, req[i]);
}
}
TEST(bytes_in_order_de_held)
{
send_all();
CHECK_EQ(fake_gpioa.BSRR, 1u << 1); /* последний байт ещё в линии */
CHECK_EQ(fake_usart2.CR1 & USART_CR1_TXEIE, 0);
CHECK(fake_usart2.CR1 & USART_CR1_TCIE);
}
TEST(de_drops_on_tc)
{
send_all();
fake_usart2.SR = USART_SR_TXE | USART_SR_TC;
USART2_IRQHandler();
CHECK_EQ(fake_gpioa.BSRR, 1u << 17); /* DE = 0 */
CHECK(fake_usart2.CR1 & USART_CR1_RE);
CHECK_EQ(fake_usart2.CR1 & USART_CR1_TCIE, 0);
rs485_send(req, 1); /* не занят: не повиснет */
CHECK_EQ(fake_gpioa.BSRR, 1u << 1);
}
TEST(rx_byte_to_parser)
{
rs485_init();
fake_usart2.SR = USART_SR_RXNE;
fake_usart2.DR = 0x5A;
USART2_IRQHandler();
CHECK_EQ(rx_count, 1);
CHECK_EQ(rx_log[0], 0x5A);
}
TEST(zero_length_ignored)
{
rs485_init();
uint32_t cr1 = fake_usart2.CR1, bsrr = fake_gpioa.BSRR;
rs485_send(req, 0);
CHECK_EQ(fake_usart2.CR1, cr1);
CHECK_EQ(fake_gpioa.BSRR, bsrr);
}
static const struct tt_case cases[] = {
CASE(init_9600_8E1_listening), CASE(send_raises_de_and_mutes_rx), CASE(bytes_in_order_de_held),
CASE(de_drops_on_tc), CASE(rx_byte_to_parser), CASE(zero_length_ignored),
};
int main(int argc, char **argv)
{
return tt_run(cases, (int)(sizeof cases / sizeof cases[0]), argc, argv);
}
rs485_hal.c — тот же RS-485 на HAL.
/* 129а: тот же RS-485 на HAL. HAL_UART_Transmit блокирующий и сам ждёт TC
в конце кадра, поэтому DE снимается сразу после него — и при ошибке тоже. */
#include "stm32f1xx_hal.h"
#define DE_PORT GPIOA
#define DE_PIN GPIO_PIN_1
uint32_t rs485_timeout_ms(uint16_t n)
{
return 2u * n + 10u; /* 8E1 на 9600: 1,146 мс на байт — запас вдвое и 10 мс */
}
HAL_StatusTypeDef rs485_hal_send(UART_HandleTypeDef *hu, const uint8_t *p, uint16_t n)
{
HAL_StatusTypeDef st;
if (n == 0u)
return HAL_OK;
HAL_GPIO_WritePin(DE_PORT, DE_PIN, GPIO_PIN_SET);
st = HAL_UART_Transmit(hu, p, n, rs485_timeout_ms(n));
HAL_GPIO_WritePin(DE_PORT, DE_PIN, GPIO_PIN_RESET); /* иначе линия занята навсегда */
return st;
}
test_rs485_hal.c — тесты rs485_hal.c с подменой функций HAL при компоновке.
/* 129а: тесты rs485_hal.c. В сборке — настоящие stm32f1xx_hal.c и stm32f1xx_hal_gpio.c
из STM32CubeF1, и три способа подмены при компоновке:
HAL_UART_Transmit — своя функция, файл stm32f1xx_hal_uart.c просто не собираем;
HAL_GetTick — сильное определение побеждает __weak из stm32f1xx_hal.c;
HAL_GPIO_WritePin — -Wl,--wrap: журнал вызовов, затем настоящий код HAL. */
#include "tiny_test.h"
USART_TypeDef fake_usart2;
GPIO_TypeDef fake_gpioa;
RCC_TypeDef fake_rcc;
int fake_nvic_irq;
SysTick_Type fake_systick;
uint32_t rs485_timeout_ms(uint16_t n);
HAL_StatusTypeDef rs485_hal_send(UART_HandleTypeDef *hu, const uint8_t *p, uint16_t n);
enum { DE_SET = 1, DE_RESET, TX };
static struct { int what; uint16_t n; uint32_t timeout; } log_[8];
static int nlog;
static HAL_StatusTypeDef tx_result;
static uint32_t now;
void __real_HAL_GPIO_WritePin(GPIO_TypeDef *port, uint16_t pin, GPIO_PinState s);
void __wrap_HAL_GPIO_WritePin(GPIO_TypeDef *port, uint16_t pin, GPIO_PinState s)
{
if (port == GPIOA && pin == GPIO_PIN_1 && nlog < 8)
log_[nlog++].what = s == GPIO_PIN_SET ? DE_SET : DE_RESET;
__real_HAL_GPIO_WritePin(port, pin, s); /* пишет в fake_gpioa.BSRR */
}
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *hu, const uint8_t *p, uint16_t n, uint32_t timeout)
{
(void)hu;
(void)p;
if (nlog < 8) {
log_[nlog].what = TX;
log_[nlog].n = n;
log_[nlog++].timeout = timeout;
}
return tx_result;
}
#ifndef WRAP_TICK
uint32_t HAL_GetTick(void) { return now++; } /* часы теста: +1 мс на каждый вызов */
#else
uint32_t __wrap_HAL_GetTick(void) { return now++; } /* та же подделка через --wrap */
#endif
void setup(void)
{
memset(&fake_gpioa, 0, sizeof fake_gpioa);
memset(log_, 0, sizeof log_);
nlog = 0;
tx_result = HAL_OK;
now = 100;
}
static UART_HandleTypeDef hu;
static const uint8_t req[8] = { 0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0xC4, 0x0B };
TEST(de_around_transmit)
{
CHECK_EQ(rs485_hal_send(&hu, req, 8), HAL_OK);
CHECK_EQ(nlog, 3);
CHECK_EQ(log_[0].what, DE_SET);
CHECK_EQ(log_[1].what, TX);
CHECK_EQ(log_[1].n, 8);
CHECK_EQ(log_[1].timeout, 26);
CHECK_EQ(log_[2].what, DE_RESET);
CHECK_EQ(fake_gpioa.BSRR, (uint32_t)GPIO_PIN_1 << 16); /* это записал настоящий HAL */
}
TEST(de_released_on_timeout)
{
tx_result = HAL_TIMEOUT;
CHECK_EQ(rs485_hal_send(&hu, req, 8), HAL_TIMEOUT);
CHECK_EQ(nlog, 3);
CHECK_EQ(log_[2].what, DE_RESET);
}
TEST(timeout_covers_frame)
{
for (uint32_t n = 1; n <= 255; n++) /* кадр 8E1 — 11 бит на 9600 бод */
CHECK(rs485_timeout_ms((uint16_t)n) * 9600u > n * 11u * 1000u);
}
TEST(zero_length_no_calls)
{
CHECK_EQ(rs485_hal_send(&hu, req, 0), HAL_OK);
CHECK_EQ(nlog, 0);
}
TEST(hal_delay_30_waits_31_ticks)
{
HAL_Delay(30); /* настоящий код из stm32f1xx_hal.c */
CHECK_EQ(now - 1 - 100, 31); /* последний прочитанный тик минус первый */
}
static const struct tt_case cases[] = {
CASE(de_around_transmit), CASE(de_released_on_timeout), CASE(timeout_covers_frame),
CASE(zero_length_no_calls), CASE(hal_delay_30_waits_31_ticks),
};
int main(int argc, char **argv)
{
return tt_run(cases, (int)(sizeof cases / sizeof cases[0]), argc, argv);
}
Дальше: раздел 129б «Статический анализ прошивки: clang-анализатор, gcc -fanalyzer и cppcheck в конвейере» — как прогнать анализаторы по настоящей сборке под Cortex-M и поставить их воротами в конвейер.