129а. Unit tests for firmware on the PC: faking the hardware, a small runner of your own and coverage
Section 129 looked for bugs without running the code: the compiler, cppcheck, path analysis. Section 184 ran pure logic on the computer — a frame parser that contains not a single register. The next step is here: code that touches the hardware. A driver writes to USART registers and calls HAL — and it too can be run on the PC if you substitute three things: the peripheral addresses, the CMSIS core and some of the HAL functions. The driver sources do not change by a single letter: armclang builds the very same files for the board.
The test subject is the RS-485 driver from section 16а: interrupt-driven transmission, DE
is released on the TC flag. There it was checked by a timed USART model; here it gets
unit tests, each about one rule of the driver. Next to it is the same exchange
on HAL, to show function substitution at link time.
Sources: the stm32f10x.h header from the Keil STM32F1xx_DFP 2.4.1 pack (the same one
as in 16а), stm32f103xb.h and HAL from the STM32CubeF1 1.8.7 archive, the reference
manual RM0008 (offsets and reset values of the USART and GPIO registers). Everything was built
and run in the stm-lab:1 image (gcc 12), directory _verify/utest129b;
the drivers were built with armclang for Cortex-M3. The well-known ready-made frameworks —
Unity, the CMock fake generator and the Ceedling build tool — were not run here:
everything needed fits into a runner of your own of 52 lines.
Terms used in the documentation
Term Meaning unit test a test of one module in isolation test runner a program that runs the tests and counts the failures fake, stub, mock a working substitute, a canned-answer substitute, a substitute that checks the calls test double any substitute for a dependency in a test seam a place where a dependency can be substituted weak symbol ( __weak)a symbol that gives way to an ordinary definition --wrap=symbola linker option: calls go to __wrap_symbolcode coverage: line / branch coverage by lines, by branches characterization test a test that pins down the behavior of someone else's code
129а.1 Where the seam between the driver and the hardware is
The driver has three dependencies on the board, and each has its own seam:
| Dependency | What it looks like in the code | What replaces it on the PC |
|---|---|---|
| peripheral registers | USART2->CR1, GPIOA->BSRR |
a structure in RAM instead of an address |
| the core | NVIC_EnableIRQ, SysTick from core_cm3.h |
your own core_cm3.h earlier in the search path |
| HAL functions | HAL_GPIO_WritePin, HAL_UART_Transmit |
your own function at link time |
The first two seams are made by the preprocessor, the third by the linker. The driver code
does not know where it is being built: there is not a single #ifdef TEST in it. If
a #ifdef appears in the driver for the sake of tests, the tests no longer check the code
that will go into the board.
129а.2 A runner in 52 lines
The runner from 184.3 counted checks and failures in one process. For a driver
that is not enough. A driver has static state — the busy flag
tx_busy, a pointer to the data — and it cannot be reset between tests from outside:
the variables are static. A test that leaves the flag raised hangs
the next one. So every test runs in its own process:
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;
}
A process per test gives three things at once. Each test has its own static state,
starting from zero. A hung test is killed by the alarm after one second — the runner prints
HANG and moves on. Dereferencing a bad pointer crashes only the child —
the runner prints CRASH. On failure, the CHECK and CHECK_EQ checks print
the file, line, expression and both values and end the process. The command-line
argument is a filter: ./t de_ ran three tests out of six. The exit
code is non-zero on any failure — that is enough for the pipeline from
section 110.
fork exists on Linux and in the container where the tests run. That is all the runner needs:
it does not run on the board; on the board, the drivers run.
129а.3 Registers: a structure instead of an address
In the device header a peripheral is a type and an address:
#define USART2 ((USART_TypeDef *) USART2_BASE)
The USART_TypeDef type describes the register layout, the address says where it lies.
For a test you need the same type, but in PC memory. Take the device header —
the real one, from the pack — and include your own after it:
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)
The layout matches by itself: it is one and the same type. To be safe, in the same file
_Static_assert checks the offsets against the RM0008 register map: USART_DR is
0x04, USART_CR1 is 0x0C, GPIOx_BSRR is 0x10. If the header changes,
the test will not build.
The device header pulls in core_cm3.h, and that contains inline assembly for
Cortex-M — gcc for the PC will not build it. The pack header includes the core in
quotes, so it is enough to put your own core_cm3.h into the host/ directory
and place it in the search path before CMSIS. It contains only what is needed:
the __IO, __I, __O qualifiers and their counterparts __IOM/__IM/__OM,
the SysTick_Type structure and NVIC_EnableIRQ, which simply remembers
the interrupt number. Building the driver on the PC:
gcc -std=c11 -Wall -Wextra -DSTM32F10X_MD -I host -I <пак>/Device/Include \
-include stm32f10x.h -include fake_periph.h -c rs485_f103.c
Not a single warning — neither for the driver nor for the tests.
What the structure cannot do. It remembers only the last value
written: the order of writes is not visible, and a test "DE is raised before
the interrupt is enabled" cannot be written this way. It knows nothing about the side effects of the
hardware: in the USART the pair "read SR — write DR" clears TC, but in
the structure nothing is cleared. So the test sets the flags by hand
before every call of the handler. Where timing and side effects matter,
you need a peripheral model, as in 16а.4. A unit test checks something else:
what the driver does in response to a given state of the registers.
129а.4 Tests of the RS-485 driver
The file rs485_f103.c is taken from 16а without changes. Six tests — one per
rule of the driver:
| Test | Rule | Checks |
|---|---|---|
init_9600_8E1_listening |
BRR = 3750, 8E1, DE = 0, USART2 interrupt enabled |
4 |
send_raises_de_and_mutes_rx |
when sending, DE = 1, reception off, no TCIE yet |
4 |
bytes_in_order_de_held |
bytes go out in order, DE is still raised after the last one | 6 |
de_drops_on_tc |
on TC, DE = 0, reception on, the driver is free |
7 |
rx_byte_to_parser |
a received byte goes to the parser | 2 |
zero_length_ignored |
an empty send touches nothing | 2 |
Two of them in full:
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);
}
The flags in send_all are TXE and TC together, as in USART_SR after reset
(0x00C0 per RM0008). This is not by chance: TC is raised from reset onward, and
the driver must not mistake it for the end of its own transmission. Six tests —
25 checks, all passed.
129а.5 HAL: substitution at link time
The second driver is the same exchange on 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 in STM32CubeF1 is blocking and waits for UART_FLAG_TC itself at the end,
so DE is released right after it. The timeout is 2 ms per byte plus 10 ms:
eight bytes of 8E1 at 9600 baud take 9.17 ms (16а), the timeout is 26 ms.
The test build includes the real stm32f1xx_hal.c and
stm32f1xx_hal_gpio.c from STM32CubeF1 — gcc builds them on the PC with the same
substituted core without a single warning. Three HAL functions
are substituted in three different ways:
| Function | Method | When it fits |
|---|---|---|
HAL_UART_Transmit |
your own function, stm32f1xx_hal_uart.c is left out of the build |
you do not need the whole HAL file |
HAL_GetTick |
an ordinary definition in the test wins over __weak from stm32f1xx_hal.c |
the function is weak in HAL |
HAL_GPIO_WritePin |
-Wl,--wrap=HAL_GPIO_WritePin: log, then the real one |
you need both a log and the real code |
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 */
}
With --wrap, calls to HAL_GPIO_WritePin from the driver go to
__wrap_HAL_GPIO_WritePin, and __real_… is the original HAL function. The test
sees the order of calls — something the register structure could not see:
DE = 1, HAL_UART_Transmit(n = 8, timeout 26), DE = 0. And it sees the result of
the real HAL code: fake_gpioa.BSRR holds GPIO_PIN_1 << 16.
Five HAL tests — 269 checks (255 of them are the timeout for lengths from 1 to 255
bytes). The fifth test checks not our code but HAL: HAL_Delay(30) with the test
clock, which adds 1 ms on every call to HAL_GetTick, reads 31
ticks. The function adds uwTickFreq to the delay to guarantee the
minimum — so it waits one tick longer than requested. Such a test
pins down the behavior of someone else's code: when HAL is updated, the test will tell you if
the behavior has changed.
--wrap does not see calls inside their own file. The same fake clock,
hooked up through --wrap=HAL_GetTick rather than an ordinary definition,
hangs the HAL_Delay test: the runner prints HANG. HAL_Delay and HAL_GetTick
live in the same stm32f1xx_hal.c, the reference between them is resolved already at
compile time, and the linker has nothing to substitute — HAL_Delay spins on
the real HAL_GetTick, which does not advance on the PC. A weak symbol has no such
trap: nm shows the letter W for HAL_GetTick and HAL_Delay in
stm32f1xx_hal.o, and an ordinary definition from the test replaces
the function for all calls.
129а.6 Planted bugs, coverage and the armclang build
Green tests prove nothing yet — you have to make sure they turn red.
The mutants.py script makes one change in a copy of the driver and runs the tests
(the approach of 184.5, but the changes here are hand-made — these are the bugs people
really make with RS-485):
| Bug | Which test failed |
|---|---|
DE released on TXE of the last byte |
bytes_in_order_de_held |
TCIE enabled right away on send |
send_raises_de_and_mutes_rx |
| reception not re-enabled after transmission | de_drops_on_tc |
| busy flag not cleared | de_drops_on_tc — HANG |
| on HAL: on error, return before releasing DE | de_released_on_timeout |
| on HAL: timeout with no margin, 1 ms per byte | de_around_transmit, timeout_covers_frame |
Six out of six. The message tells you right away what is wrong:
/v/test_rs485.c:64: fake_gpioa.BSRR == 1u << 1: 131072 != 2
131072 is 1 << 17: DE was released while the last byte was still on the line.
Without a process per test, the fourth bug would have hung the whole run; with it —
one HANG line.
Coverage — gcc --coverage and gcov -b in the same image:
| Run | Lines out of 34 | Branches taken out of 16 |
|---|---|---|
| all 6 tests | 100.00 % | 81.25 % |
| without the reception test | 97.06 % | 68.75 % |
Without the reception test, gcov marks exactly one line with ##### — the call to
rs485_rx_byte. The full set goes through all lines, but three branches out of 16 it never
takes: the body of the while (tx_busy) wait (in a single-threaded test the
driver is never busy when sending) and two cases in the handler — "TXEIE
enabled but TXE not set" and "TCIE enabled but TC not set". This is exactly what
branch coverage is good for: it shows which combinations of flags were never tried.
rs485_hal.c — 9 lines and 2 branches, everything covered.
llvm-cov exists only in the stm-rust:1 image — from the Rust toolchain,
LLVM 22.1.8. It does not read gcc 14 coverage files from the same image ("Invalid .gcno File!").
clang 14 from stm-sys:1 builds and passes the tests, but coverage cannot be
collected with it: the libclang_rt.profile library is missing. So the coverage here is
gcov.
The build for the board — the same files, armclang, Cortex-M3, -Wall -Wextra:
rs485_f103.c with stm32f10x.h from the Keil pack and the real core_cm3.h from
CMSIS; rs485_hal.c and stm32f1xx_hal.c with the STM32CubeF1 headers and the same
stm32f1xx_hal_conf.h as the tests. Not a single warning. In the
HAL object built by armclang, HAL_GetTick and HAL_Delay are weak too
(Wk in fromelf -s): substitution through an ordinary definition works in the
firmware as well. That is exactly why HAL makes them weak — the comment in
stm32f1xx_hal.c says the timing functions are declared __weak
so that they can be replaced with your own.
129а.7 Pitfalls
A test hangs the next one. The driver's static state (tx_busy)
survives a test if all tests run in one process. A process per test
or a reset function in the driver itself — there is no third option.
#ifdef TEST in the driver. The tests check a build that will never get
into the board. The seams belong in headers and in linking, not in the driver code.
Your own register header instead of the real one. A structure rewritten by hand
will one day drift away from the pack. The real header and
_Static_assert on the offsets from RM0008 will not let that happen.
The register structure is taken for a model of the hardware. It does not clear TC
on a write to DR and does not remember the order of writes. Timing and side effects need
a model, as in 16а.4; the order of calls needs the fake's log.
--wrap on a function that is called from the same file. Calls from inside
stm32f1xx_hal.c bypass the wrapper: HAL_Delay hangs on the real
HAL_GetTick. For weak HAL functions, use an ordinary definition.
The expected value in the test is computed with the same formulas as in the code. The timeout test compares against a frame time computed separately — that is why the "1 ms per byte" timeout did not pass.
_exit in the child process. gcov counters are written on an ordinary
exit; with _exit the coverage will silently be zero.
129а.8 Summary
- Registers are substituted without editing the driver: the real pack header,
after it
#undefand#define USART2 (&fake_usart2); the core — your owncore_cm3.hbefore CMSIS in the search path. - The runner is 52 lines: a process per test, a 1 s alarm, a filter by name, an exit code. 11 tests: 6 on registers (25 checks), 5 on HAL (269).
- HAL is substituted at link time in three ways: do not build the file,
override
__weak, wrap with--wrap.--wrapdoes not see calls inside its own file — that is how theHAL_Delaytest hangs. - The real
HAL_Delay(30)on the test clock waits 31 ticks. - 6 planted bugs out of 6 are caught, one of them through
HANG. - Coverage of the register-level driver: lines 100 %, branches taken 81.25 % — gcov shows the three branches not taken by line.
- armclang builds the same sources for STM32F103 without warnings.
Full sources
tiny_test.h — the unit test runner: each test in its own process.
/* 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 — the peripherals as ordinary structures in the computer's RAM.
/* 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 — tests of the RS-485 driver from section 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 — the same RS-485 on 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 — tests of rs485_hal.c with HAL functions substituted at link time.
/* 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);
}
Next: section 129б "Static analysis of firmware: the clang analyzer, gcc -fanalyzer and cppcheck in the pipeline" — how to run the analyzers over the real Cortex-M build and make them a gate in the pipeline.