Перейти к содержимому

Язык C для встраиваемых систем и архитектура BeeFC … ↩

Язык C для встраиваемых систем и архитектура BeeFC mod. Четверг, 15 октября 2026 22:55 . См. 5

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_t8 битбайты протоколов, номера выводов
int16_t16 битпоказания датчиков
uint32_t32 битавремя в микросекундах, значения регистров
float32 битафильтры и регуляторы
Числа с плавающей точкой: Сопроцессор ядра Cortex-M4F работает только с числами одинарной точности. Операции с типом 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все слои—
corehal/include, другие модули corestm32f4xx.h, core_cm4.h, drivers
drivershal/include, corestm32f4xx.h, core_cm4.h
hal/stm32f4CMSIS, hal/includecore, drivers
hal/fakehal/includeCMSIS

Команда make arch-check ищет в каталогах core и drivers включение заголовочных файлов микроконтроллера. Если такое включение найдено, проверка не пройдена.

Слои BeeFC и направление зависимостей: ядро и драйверы работают с аппаратной частью только через интерфейсы каталога hal/include.
Слои BeeFC и направление зависимостей: ядро и драйверы работают с аппаратной частью только через интерфейсы каталога 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);
Plain text

Вывод в 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;
}
Plain text

Запись уровня выполняется через регистр 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);
Plain text
↑ К оглавлению

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);
Plain text

Файл 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);
}
Plain text

Тест создаёт модуль, вызывает его функции и проверяет состояние поддельного вывода. Каждый тест построен по одной схеме: подготовка, действие, проверка.

Файл 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));
}
Plain text
make test
tests/unit/test_level1.c:25:test_led_active_low_inverts_level:PASS
...
-----------------------
16 Tests 0 Failures 0 Ignored
OK
Plain text
Тест не заменяет проверку на плате: Модульный тест проверяет логику модуля, но не правильность номера вывода и не настройку тактирования порта. Каждый новый модуль после прохождения тестов проверяется на плате.

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. Как это сделано в больших прошивках

ПрошивкаВерсияФайл в исходном коде
Betaflight2026.6.2src/main/drivers/io.c, src/platform/STM32/io_stm32.c, src/platform/AT32
INAV9.1.0src/main/drivers/io.c
ArduPilotCopter-4.7.2libraries/AP_HAL/GPIO.h, libraries/AP_HAL_ChibiOS/GPIO.cpp
PX4v1.17.0platforms/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. Проверьте себя

  1. Почему в BeeFC используются типы из stdint.h, а не int и long?
  2. Делает ли квалификатор volatile операцию counter++ атомарной?
  3. Почему запись уровня вывода выполняется через BSRR, а не через ODR?
  4. Какой заголовочный файл нельзя включать в модули каталога core и почему?
  5. Почему константа 0.5 без суффикса замедляет вычисления на Cortex-M4F?
↑ К оглавлению

9. Чек-лист самопроверки

Результат: Все пункты выполнены. Можно переходить к уроку S5.
↑ К оглавлению

10. Источники

  1. Библиотека модульного тестирования Unity.
  2. Справочное руководство RM0090, раздел 8.4.7 о регистре GPIOx_BSRR.
  3. Руководство программиста PM0214, раздел о сопроцессоре плавающей точки.
  4. Руководство разработчика ArduPilot: уровень аппаратных абстракций.
  5. Пакет Proteus компании Labcenter Electronics.
↑ К оглавлению

11. Что дальше

  • Следующий урок: S5. Порты ввода-вывода, прерывания и контроллер прерываний NVIC.
  • Полный перечень уроков приведён в статье «Курс BeeFC: содержание уровня „Старт“».

Проверено: 13.10.2026

↑ К оглавлению
  • мод. : Четверг, 15 октября 2026