Язык C для встраиваемых систем и архитектура BeeFC … ↩
mod. Суббота, 17 октября 2026 22:14
. См. 89
BeeFC · Урок S4 из 33 · Уровень «Старт»
Какие возможности языка C особенно важны при программировании микроконтроллеров, как разделить прошивку на слои и почему благодаря этому почти весь код BeeFC проверяется модульными тестами на компьютере без платы.
Начиная с этого урока ядро BeeFC не обращается к регистрам напрямую. Правило проверяется автоматически при каждой отправке изменений.
Содержание
1. Цели урока
Продолжительность урока — четыре часа: один час теории и три часа практики. После урока слушатель:
- применяет типы фиксированного размера, квалификатор
volatileи битовые операции по назначению; - объясняет правила зависимостей между слоями BeeFC;
- пишет модуль ядра, который работает через интерфейс уровня аппаратных абстракций;
- пишет и запускает модульные тесты с поддельной реализацией портов ввода-вывода.
2. Термины и сокращения
| Термин | Определение |
|---|---|
| уровень аппаратных абстракций (HAL) | набор функций, скрывающих от остального кода особенности конкретного микроконтроллера |
| интерфейс | заголовочный файл с объявлениями функций без их реализации |
| поддельная реализация | реализация интерфейса для компьютера, которая запоминает вызовы вместо работы с регистрами |
| модульный тест | функция, проверяющая поведение одного модуля при заданных входных данных |
| Unity | библиотека модульного тестирования на языке C |
квалификатор volatile |
указание компилятору, что значение переменной может измениться без участия текущего кода |
| атомарная операция | операция, которую не может прервать обработчик прерывания |
3. Язык C во встраиваемых системах
Прошивка BeeFC написана на языке C стандарта C99. Курс не повторяет основы языка, а разбирает возможности, без которых программирование микроконтроллера невозможно. Слушателям, не прошедшим входной тест по языку C, предлагается подготовительный модуль «C для BeeFC» с автоматической проверкой.
Типы фиксированного размера
Размер типа int стандарт языка не фиксирует: на микроконтроллере он равен 32 битам, на некоторых других процессорах — 16. Регистры периферии имеют точно заданную разрядность, поэтому в BeeFC используются типы из заголовочного файла stdint.h.
| Тип | Разрядность | Применение в BeeFC |
|---|---|---|
uint8_t |
8 бит | байты протоколов, номера выводов |
int16_t |
16 бит | показания датчиков |
uint32_t |
32 бита | время в микросекундах, значения регистров |
float |
32 бита | фильтры и регуляторы |
double выполняются программно и в десятки раз медленнее. Поэтому константы в BeeFC записываются с суффиксом f: 0.5f, а не 0.5. Без суффикса выражение x * 0.5 вычисляется в двойной точности.Квалификатор volatile
Компилятор вправе прочитать переменную один раз и дальше использовать сохранённое значение. Для обычной переменной это правильная оптимизация. Для регистра периферии или переменной, которую меняет обработчик прерывания, это ошибка: цикл ожидания никогда не закончится. Квалификатор volatile запрещает компилятору такую оптимизацию, и каждое обращение выполняется заново.
Квалификатор volatile не делает операцию атомарной. Выражение counter++ для переменной с квалификатором volatile по-прежнему состоит из трёх инструкций: чтения, сложения и записи. Если между ними сработает прерывание, изменяющее ту же переменную, одно изменение потеряется.
Битовые операции
| Действие | Выражение | Пример |
|---|---|---|
| установить бит | reg |= (1u << n) |
включить тактирование порта |
| сбросить бит | reg &= ~(1u << n) |
выключить прерывание |
| переключить бит | reg ^= (1u << n) |
переключить светодиод |
| проверить бит | (reg >> n) & 1u |
прочитать флаг готовности |
| записать поле | reg = (reg & ~mask) | (value << pos) |
выбрать режим вывода в MODER |
Суффикс u у единицы обязателен: сдвиг знакового числа 1 << 31 даёт неопределённое поведение по стандарту языка.
Правила BeeFC
- Динамическое выделение памяти функцией
mallocне используется: вся память распределяется при компиляции, и её объём виден в выводе утилитыsize. - Рекурсия не используется: глубина стека должна быть известна заранее.
- Глобальные переменные модуля объявляются с квалификатором
staticи недоступны другим модулям. - Таблицы констант объявляются с квалификатором
constи размещаются во флеш-памяти, а не в ОЗУ.
4. Архитектура BeeFC
Правила зависимостей
Каждый слой BeeFC может обращаться только к слоям ниже себя. Ядро и драйверы видят только интерфейсы уровня аппаратных абстракций из каталога hal/include и ничего не знают о регистрах STM32.
| Слой | Может включать | Не может включать |
|---|---|---|
app |
все слои | — |
core |
hal/include, другие модули core |
stm32f4xx.h, core_cm4.h, drivers |
drivers |
hal/include, core |
stm32f4xx.h, core_cm4.h |
hal/stm32f4 |
CMSIS, hal/include |
core, drivers |
hal/fake |
hal/include |
CMSIS |
Команда make arch-check ищет в каталогах core и drivers включение заголовочных файлов микроконтроллера. Если такое включение найдено, проверка не пройдена.

hal/include.Интерфейс портов ввода-вывода
Файл hal/include/hal_gpio.h
typedef uint8_t hal_pin_t; /* номер вывода в нумерации платы */
typedef enum {
HAL_GPIO_INPUT,
HAL_GPIO_INPUT_PULLUP,
HAL_GPIO_OUTPUT
} hal_gpio_mode_t;
void hal_gpio_init(hal_pin_t pin, hal_gpio_mode_t mode);
void hal_gpio_write(hal_pin_t pin, bool level);
bool hal_gpio_read(hal_pin_t pin);
void hal_delay_us(uint32_t us);
Вывод в BeeFC обозначается одним числом: номер порта, умноженный на 16, плюс номер бита. Вывод PB5 имеет номер \( 1 \cdot 16 + 5 = 21 \).
Реализация для STM32
Файл hal/stm32f4/gpio_stm32.c
static GPIO_TypeDef *const ports[] = { GPIOA, GPIOB, GPIOC, GPIOD };
#define PORT_OF(pin) (ports[(pin) / 16u])
#define BIT_OF(pin) ((uint32_t)(pin) % 16u)
void hal_gpio_init(hal_pin_t pin, hal_gpio_mode_t mode)
{
GPIO_TypeDef *gpio = PORT_OF(pin);
uint32_t b = BIT_OF(pin);
RCC->AHB1ENR |= 1u << (pin / 16u); /* тактирование порта */
(void)RCC->AHB1ENR;
gpio->MODER &= ~(3u << (b * 2u));
gpio->PUPDR &= ~(3u << (b * 2u));
if (mode == HAL_GPIO_OUTPUT) gpio->MODER |= 1u << (b * 2u);
if (mode == HAL_GPIO_INPUT_PULLUP) gpio->PUPDR |= 1u << (b * 2u);
}
void hal_gpio_write(hal_pin_t pin, bool level)
{
/* BSRR: младшие 16 бит устанавливают вывод, старшие — сбрасывают.
* Запись атомарна, прерывание не может испортить соседние выводы. */
PORT_OF(pin)->BSRR = level ? (1u << BIT_OF(pin)) : (1u << (BIT_OF(pin) + 16u));
}
bool hal_gpio_read(hal_pin_t pin)
{
return (PORT_OF(pin)->IDR >> BIT_OF(pin)) & 1u;
}
Запись уровня выполняется через регистр BSRR, а не через ODR. Запись в ODR требует чтения, изменения и записи регистра. Если между чтением и записью прерывание изменит другой вывод того же порта, его изменение будет потеряно. Запись в BSRR выполняется одной инструкцией и затрагивает только выбранный вывод.
Поддельная реализация для тестов
Поддельная реализация хранит состояние выводов в массиве и считает число записей. Функции-перехватчики позволяют тесту подставить своё поведение: например, имитировать микросхему, которая отвечает на импульсы тактового сигнала. Такой перехватчик понадобится в уроке S10 для драйвера тензодатчика.
Файл hal/fake/gpio_fake.h
typedef bool (*fake_read_hook_t)(hal_pin_t pin);
typedef void (*fake_write_hook_t)(hal_pin_t pin, bool level);
void fake_gpio_reset(void);
void fake_gpio_set_input(hal_pin_t pin, bool level);
bool fake_gpio_get_output(hal_pin_t pin);
uint32_t fake_gpio_write_count(hal_pin_t pin);
void fake_gpio_set_hooks(fake_read_hook_t r, fake_write_hook_t w);
↑ К оглавлению5. Модульные тесты
Модуль светодиода — самый простой модуль ядра. На многих платах светодиод включается низким уровнем, и модуль скрывает эту особенность от остального кода.
Файл core/led.h
typedef struct {
hal_pin_t pin;
bool active_low; /* на многих платах светодиод включается нулём */
bool on;
} led_t;
void led_init(led_t *led, hal_pin_t pin, bool active_low);
void led_set(led_t *led, bool on);
void led_toggle(led_t *led);
Файл core/led.c
void led_init(led_t *led, hal_pin_t pin, bool active_low)
{
led->pin = pin;
led->active_low = active_low;
hal_gpio_init(pin, HAL_GPIO_OUTPUT);
led_set(led, false);
}
void led_set(led_t *led, bool on)
{
led->on = on;
hal_gpio_write(led->pin, on != led->active_low);
}
void led_toggle(led_t *led)
{
led_set(led, !led->on);
}
Тест создаёт модуль, вызывает его функции и проверяет состояние поддельного вывода. Каждый тест построен по одной схеме: подготовка, действие, проверка.
Файл tests/unit/test_level1.c
void test_led_active_low_inverts_level(void)
{
led_t led;
led_init(&led, 5, true); /* включается нулём */
TEST_ASSERT_EQUAL(HAL_GPIO_OUTPUT, fake_gpio_mode(5));
TEST_ASSERT_TRUE(fake_gpio_get_output(5)); /* выключен: на выводе 1 */
led_set(&led, true);
TEST_ASSERT_FALSE(fake_gpio_get_output(5)); /* включён: на выводе 0 */
led_toggle(&led);
TEST_ASSERT_TRUE(fake_gpio_get_output(5));
}
make test
tests/unit/test_level1.c:25:test_led_active_low_inverts_level:PASS
...
-----------------------
16 Tests 0 Failures 0 Ignored
OK
Proteus и Renode
Пакет Proteus моделирует микроконтроллер STM32F401, а не STM32F405. Порты ввода-вывода у них устроены одинаково, поэтому реализацию hal/stm32f4/gpio_stm32.c можно проверить в Proteus с моделью светодиода и кнопки. Для слушателей без Proteus те же проверки выполняет Renode на сервере автоматической проверки.
6. Практика
Задание S4.1. Модуль светодиода
Продолжительность — 40 минут, выполняется на компьютере.
Реализовать функции модуля core/led.c так, чтобы тест test_led_active_low_inverts_level был пройден. Модуль не должен включать заголовочные файлы микроконтроллера.
Критерий автоматической проверки: тест пройден, проверка make arch-check пройдена.
Задание S4.2. Модуль звукового сигнализатора
Продолжительность — 80 минут, выполняется на компьютере.
Создать модуль core/beeper с функциями beeper_init, beeper_play и beeper_update. Функция beeper_play принимает последовательность длительностей звучания и пауз в миллисекундах, функция beeper_update вызывается периодически и переключает вывод. Написать не менее трёх собственных тестов.
Критерий автоматической проверки: собственные тесты пройдены; скрытые тесты сервера проверяют последовательность из трёх сигналов, повторный вызов beeper_play во время звучания и инверсный вывод.
Задание S4.3. Светодиод через уровень аппаратных абстракций
Продолжительность — 60 минут.
Перевести программу мигания на модуль led и реализацию hal/stm32f4/gpio_stm32.c. В функции main не должно остаться обращений к регистрам портов.
Критерий автоматической проверки: прошивка собирается, в модели Renode вывод светодиода переключается.
7. Как это сделано в больших прошивках
| Прошивка | Версия | Файл в исходном коде |
|---|---|---|
| Betaflight | 2026.6.2 |
src/main/drivers/io.c, src/platform/STM32/io_stm32.c, src/platform/AT32 |
| INAV | 9.1.0 |
src/main/drivers/io.c |
| ArduPilot | Copter-4.7.2 |
libraries/AP_HAL/GPIO.h, libraries/AP_HAL_ChibiOS/GPIO.cpp |
| PX4 | v1.17.0 |
platforms/common/include/px4_platform_common/Serial.hpp |
- В Betaflight вывод представлен типом
IO_t, а функцииIOWrite,IOReadиIOConfigGPIOобъявлены в общем файлеio.c. Их реализация для STM32 находится в каталогеsrc/platform/STM32, а для микроконтроллеров Artery — в каталогеsrc/platform/AT32. Так одна прошивка поддерживает микроконтроллеры разных производителей. - В INAV устройство модуля
io.cтакое же, как в Betaflight: обе прошивки унаследовали его от Cleanflight. - В ArduPilot уровень аппаратных абстракций построен на классах языка C++. Файл
AP_HAL/GPIO.hобъявляет абстрактный класс с виртуальными методами, аAP_HAL_ChibiOS/GPIO.cppреализует его для ChibiOS. Для программной имитации существует отдельная реализация, поэтому ArduPilot целиком запускается на компьютере. - В PX4 платформенно-независимые интерфейсы собраны в каталоге
px4_platform_common; файлSerial.hpp— пример интерфейса последовательного порта, у которого есть реализации для NuttX и для Linux.
8. Проверьте себя
- Почему в BeeFC используются типы из
stdint.h, а неintиlong? - Делает ли квалификатор
volatileоперациюcounter++атомарной? - Почему запись уровня вывода выполняется через
BSRR, а не черезODR? - Какой заголовочный файл нельзя включать в модули каталога
coreи почему? - Почему константа
0.5без суффикса замедляет вычисления на Cortex-M4F?
9. Чек-лист самопроверки
10. Источники
- Библиотека модульного тестирования Unity.
- Справочное руководство RM0090, раздел 8.4.7 о регистре
GPIOx_BSRR. - Руководство программиста PM0214, раздел о сопроцессоре плавающей точки.
- Руководство разработчика ArduPilot: уровень аппаратных абстракций.
- Пакет Proteus компании Labcenter Electronics.
11. Что дальше
- Следующий урок: S5. Порты ввода-вывода, прерывания и контроллер прерываний NVIC.
- Полный перечень уроков приведён в статье «Курс BeeFC: содержание уровня „Старт“».
Проверено: 13.10.2026
↑ К оглавлению- мод. : Суббота, 17 октября 2026