Embedded HandbookПриложения
Все разделы › XV. Язык C глубже
STM32F103, STM32F407 · уровень средний · 14 мин чтения
Открыть в интерактивном справочнике

128. Стандарты C99–C23: что изменилось для прошивки

Язык C меняется медленно, но меняется: за четверть века вышли C99, C11, C17 и C23. Многие прошивки до сих пор написаны так, будто на дворе 1990 год: #define вместо констант, ручное обнуление структур, unsigned char вместо uint8_t. Раздел показывает, что из каждой версии пригодится на Cortex-M, что опасно и что ломает старый код. Всё проверено: C99 и C11 — на gcc, clang и gcc для ARM с ключом -pedantic, C23 — на armclang 6.24 для F103 и F407.

Как это называется в документации

Оригинал Перевод
ISO/IEC 9899 стандарт языка C
__STDC_VERSION__ номер версии стандарта, который выбран при сборке
designated initializer именованный инициализатор: .поле =, [индекс] =
compound literal составной литерал: безымянный объект (тип){ … }
flexible array member гибкий массив в конце структуры
variable length array (VLA) массив переменной длины
anonymous structure / union анонимная структура / объединение
attribute [[…]] атрибут стандарта C23
prototype прототип: объявление функции с типами параметров
GNU extensions (gnu11) расширения GNU к стандарту

128.1 Какой стандарт у вас сейчас

Пять версий C на оси времени: значение __STDC_VERSION__ и главное для прошивки

Версию выбирает ключ -std, а программа узнаёт её по макросу __STDC_VERSION__. Опыт на трёх компиляторах:

Ключ armclang 6.24 gcc 12, clang 14
-std=c99 199901 199901
-std=c11 201112 201112
-std=c17 201710 201710
-std=c23 / c2x 202311 202000 — черновик
без ключа 201710 201710

armclang по умолчанию собирает C17 с расширениями GNU: макрос __STRICT_ANSI__ появляется только при явном -std=c17. В Keil стандарт выбирают в поле «Language C» на вкладке C/C++. gnu11 и c11 отличаются не версией, а расширениями: с c11 и -pedantic компилятор предупредит о любой конструкции вне стандарта. gcc 12 и clang 14 знают C23 только в черновике — 202000, — и половины нового там нет.

Новые версии это меняют. gcc 14 без ключа — ещё 201710, с -std=c23 — всё ещё 202000, но constexpr, nullptr, typeof и перечисление с типом уже собираются: они появились в gcc 13. #embed — только с gcc 15 и clang 19. А gcc 15 без ключа собирает уже по gnu23 — и старый код из 128.9 (int f(); с аргументами, свой bool) перестаёт собираться от одного обновления компилятора. Поэтому стандарт в проекте задают явно: -std=gnu17 или -std=c11.

Какие расширения GNU встречаются в прошивках. Чаще всего — макрос MAX на выражении-блоке ({ … }), который вычисляет аргументы один раз:

#define MAX_NAIVE(a, b) ((a) > (b) ? (a) : (b))
#define MAX_ONCE(a, b) ({ __typeof__(a) _a = (a); __typeof__(b) _b = (b); _a > _b ? _a : _b; })

В опыте MAX_NAIVE(i++, j) при i = 5 вернул 6, а i стал 7 — i++ вычислился дважды; MAX_ONCE вернул 5, i стал 6. Дальше — упаковка __attribute__((packed)) (структура из байта и 32-битного слова занимает 5 байт вместо 8), диапазоны в case 0 ... 49:, массив нулевой длины data[0] в конце структуры, двоичные константы 0b1010 и встроенные функции вроде __builtin_popcount.

С -std=c11 -pedantic gcc и clang предупредили о четырёх из них: массив нулевой длины, диапазон в case, выражение-блок и двоичная константа. __attribute__, __typeof__ и __builtin_popcount прошли молча: имена с двумя подчёркиваниями зарезервированы для компилятора, и -pedantic их не проверяет. Поэтому «собирается с -pedantic» ещё не значит «переносится на другой компилятор» — packed и встроенные функции ищут отдельно.


128.2 C99: то, без чего прошивка уже не пишется

C99 дал то, что сегодня кажется частью языка:

Одна тонкость bool: приведение к нему — сравнение с нулём. (bool)0.5 — 1, а (int)0.5 — 0: флаг, полученный делением, при переходе с int на bool может поменять значение.


128.3 Именованные инициализаторы и составные литералы

Настройка периферии — структура, в которой важны два поля из пяти:

struct uart_cfg {
    uint32_t baud;
    uint8_t data_bits;
    uint8_t parity;
    uint8_t stop_bits;
    bool flow;
};
static const struct uart_cfg console = { .baud = 115200, .data_bits = 8 };

На всех трёх машинах поля вышли 115200, 8, 0, 0, 0: всё, что не названо, получает ноль. Порядок полей в структуре можно менять — инициализация не сломается.

У массивов то же с индексами:

static const char *const state_name[] = { [2] = "run", [0] = "idle", [5] = "fault" };

Массив получил 6 элементов — по наибольшему индексу, — а пропущенные стали NULL. Это удобно для таблиц «код → текст», где коды идут с пропусками.

Составной литерал — безымянный объект прямо в выражении:

printf("compound=%d\n", sum((const int[]){ 1, 2, 3, 4 }, 4));

Функция получила массив, которого нет в объявлениях, и вернула 10. Время жизни такого объекта — до конца блока, как у обычной локальной переменной.


128.4 Гибкий массив в конце структуры

Пакет переменной длины: заголовок и данные сразу за ним.

struct packet {
    uint8_t type;
    uint8_t len;
    uint8_t data[];
};

sizeof(struct packet) — 2: массив без размера места не занимает. Память выделяют как sizeof *p + длина, и p->data[3] читается как обычный элемент. Раньше писали data[1] и вычитали единицу — это неопределённое поведение при выходе за объявленный размер (раздел 123). Если перед гибким массивом стоит поле короче его элемента, появится заполнение: у uint16_t и массива uint32_t размер уже 4 (раздел 126).

Гибкий массив: заголовок из двух байтов и четыре байта данных сразу за ним


128.5 Массивы переменной длины

VLA — массив в стеке с размером из переменной. Выглядит невинно:

uint8_t buf[n];
memcpy(buf, src, n);
process(buf, n);

armclang с ключом -Wvla предупредил: «variable length array used». В объектном файле видно, чем это плохо: стек сдвигается командой SUB r5, sp, r0 — на величину из регистра. У той же функции с массивом [64] — SUB sp, sp, #0x40: размер известен при сборке, и запас стека можно посчитать заранее (разделы 11.4 и 35.4). Большое n от собеседника по UART молча проходит сквозь стек задачи в чужую память. C11 сделал VLA необязательными, в правилах для прошивки их запрещают. Замена — массив наибольшего размера с проверкой длины.


128.6 C11: проверки, выравнивание, атомарность

union reg {
    uint32_t word;
    struct {
        uint16_t lo;
        uint16_t hi;
    };
};

После .word = 0x12345678 поле r.hi — 0x1234, r.lo — 0x5678: к половинам обращаются без лишнего имени вложенной структуры. Так раскладывают машины с младшим байтом вперёд — Cortex-M и x86; на big-endian половины поменяются местами. Для своих регистров приём годится, для пакетов и файлов — нет (раздел 126.4).

Что из C11 на плате не пригодится: потоки <threads.h>. В библиотеке armclang такого заголовка нет — задачи создаёт ОСРВ (раздел 24).


128.7 C17: только исправления

C17 не добавил ничего нового — только уточнил формулировки C11. Для программиста это та же версия языка с другим номером: 201710. Именно её armclang выбирает по умолчанию.


128.8 C23: что пригодится в прошивке

C23 — самое большое обновление со времён C99. Файл для платы собран armclang с -std=c23:

constexpr uint32_t PCLK_HZ = 72'000'000;
constexpr uint32_t BAUD = 115'200;
static_assert((PCLK_HZ + BAUD / 2) / BAUD == 625);
enum mode : uint8_t { MODE_IDLE, MODE_RUN, MODE_FAULT = 0b1000'0000 };
static_assert(sizeof(enum mode) == 1);
static const uint8_t logo[] = {
#embed "logo128.bin"
};
static_assert(sizeof logo == 4);

Четыре байта файла легли во флеш-память сразу за кодом — STM2. Без #embed для этого нужна утилита, которая превращает файл в текст на Си.

Тот же файл с -std=c11 не собрался: «unknown type name 'constexpr'», всего 7 ошибок. Новые возможности работают только с новым ключом, и переходить на них можно по одной.


128.9 Что C23 ломает в старом коде

Объявление без параметров до C23 значило «параметры не указаны»:

int legacy();
return legacy(1, 2);

С -std=c17 armclang собрал это с предупреждением: «passing arguments to 'legacy' without a prototype is deprecated in all versions of C and is not supported in C23». С -std=c23 — ошибка: «too many arguments to function call, expected 0, have 2». В C23 пустые скобки значат то же, что (void). Старые определения функций в стиле K&R — с типами параметров после скобок — C23 не принимает вовсе.

Кроме этого, bool, true, false, nullptr, constexpr, typeof и static_assert стали ключевыми словами: старый код, где так названы свои макросы или переменные, придётся переименовать.


128.10 Грабли

Библиотека собирается у автора и не собирается у вас. Разные -std или расширения GNU. Задавать стандарт явно, проверять -pedantic.

Флаг поменял значение после перехода на bool. (bool)0.5 — 1, (int)0.5 — 0. Сравнивать явно: x != 0.

Задача портит чужую память при длинном пакете. Массив переменной длины на стеке. -Wvla и массив наибольшего размера с проверкой.

После перехода на C23 — «too many arguments». Объявление f() без void. Написать прототип.

После обновления gcc старый проект перестал собираться. gcc 15 по умолчанию собирает по gnu23. Задать -std явно.

constexpr не собирается. Ключ ниже C23 или компилятор без C23: gcc 12 и clang 14 его не знают.

Структура настройки с мусором в полях. Поля присвоены по одному, остальные не тронуты. Именованный инициализатор обнуляет всё неназванное.


128.11 Итоги

Полные исходники

gnu128.c — расширения GNU, которые встречаются в прошивках, — для сборки с gnu11 и с c11 -pedantic.

/* Подраздел 128.1: расширения GNU, которые встречаются в прошивках, и что с ними делает
 * -pedantic. Собирается дважды: -std=gnu11 (тишина) и -std=c11 -pedantic (предупреждения). */
#include <stdint.h>
#include <stdio.h>

#define MAX_NAIVE(a, b) ((a) > (b) ? (a) : (b))
#define MAX_ONCE(a, b) ({ __typeof__(a) _a = (a); __typeof__(b) _b = (b); _a > _b ? _a : _b; })

struct __attribute__((packed)) frame {
    uint8_t type;
    uint32_t value;
};

struct msg {
    uint8_t len;
    uint8_t data[0];
};

static int grade(int x)
{
    switch (x) {
    case 0 ... 49:
        return 2;
    case 50 ... 100:
        return 5;
    default:
        return 0;
    }
}

int main(void)
{
    int i = 5, j = 3;
    int naive = MAX_NAIVE(i++, j);                /* i++ вычисляется дважды */
    printf("naive=%d i=%d\n", naive, i);
    i = 5;
    int once = MAX_ONCE(i++, j);                  /* один раз */
    printf("once=%d i=%d\n", once, i);
    printf("packed=%u\n", (unsigned)sizeof(struct frame));
    printf("grade=%d,%d\n", grade(42), grade(77));
    printf("bin=%d\n", 0b1010);
    printf("msg=%u\n", (unsigned)sizeof(struct msg));
    printf("popcount=%d\n", __builtin_popcount(0xF0F0u));
    printf("done=1\n");
    return 0;
}

std128.c — что дали C99 и C11: именованные инициализаторы, составные литералы, гибкий массив, анонимные структуры, _Alignof, bool.

/* Раздел 128: что дали C99 и C11 — опыт на трёх машинах.
 * Именованные инициализаторы, составные литералы, гибкий массив в конце
 * структуры, анонимные структуры в объединении, _Alignof, bool. Собирается
 * с -std=c11: всё показанное — переносимый язык, без расширений. */
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* ── C99: именованные инициализаторы — неназванные поля станут нулями ──── */
struct uart_cfg {
    uint32_t baud;
    uint8_t data_bits;
    uint8_t parity;
    uint8_t stop_bits;
    bool flow;
};
static const struct uart_cfg console = { .baud = 115200, .data_bits = 8 };

/* ── C99: индексы в инициализаторе массива — пропуски тоже нули ────────── */
static const char *const state_name[] = { [2] = "run", [0] = "idle", [5] = "fault" };

/* ── C99: гибкий массив в конце структуры ──────────────────────────────── */
struct packet {
    uint8_t type;
    uint8_t len;
    uint8_t data[];
};

/* ── C11: анонимная структура внутри объединения ───────────────────────── */
union reg {
    uint32_t word;
    struct {
        uint16_t lo;
        uint16_t hi;
    };
};

static int sum(const int *v, size_t n)
{
    int s = 0;
    for (size_t i = 0; i < n; i++)
        s += v[i];
    return s;
}

int main(void)
{
    printf("cfg=%u,%u,%u,%u,%d\n", (unsigned)console.baud, console.data_bits, console.parity, console.stop_bits,
           console.flow);
    printf("states=%zu,%s,%s\n", sizeof state_name / sizeof state_name[0], state_name[5],
           state_name[1] ? state_name[1] : "NULL");
    struct packet *p = malloc(sizeof *p + 4);
    if (!p)
        return 1;
    p->len = 4;
    memcpy(p->data, "\x0A\x0B\x0C\x0D", 4);
    printf("fam=%zu,%u\n", sizeof(struct packet), p->data[3]);
    free(p);
    printf("compound=%d\n", sum((const int[]){ 1, 2, 3, 4 }, 4));
    union reg r = { .word = 0x12345678u };
    printf("anon=%X,%X\n", r.hi, r.lo);
    printf("alignof=%zu,%zu\n", _Alignof(uint16_t), _Alignof(uint32_t));
    printf("bool=%d,%d\n", (bool)0.5, (int)0.5);
    printf("done=1\n");
    return 0;
}

vla128_arm.c — массив переменной длины на стеке.

/* Раздел 128: массив переменной длины на стеке. В C99 он обязателен,
 * в C11 стал необязательным; размер стека функции становится неизвестен
 * при сборке. -Wvla находит такие места. */
#include <stdint.h>
#include <string.h>

void process(uint8_t *buf, unsigned n);

void handle(const uint8_t *src, unsigned n)
{
    uint8_t buf[n];
    memcpy(buf, src, n);
    process(buf, n);
}

void handle_fixed(const uint8_t *src, unsigned n)
{
    uint8_t buf[64];
    if (n > sizeof buf)
        n = sizeof buf;
    memcpy(buf, src, n);
    process(buf, n);
}

c23_128_arm.c — возможности C23 на плате, собирается armclang с -std=c23.

/* Раздел 128: C23 на плате — armclang -std=c23 для F103 и F407.
 * constexpr и разделители разрядов, перечисление с заданным типом, двоичные
 * литералы, bool и nullptr как ключевые слова, typeof и auto, атрибуты
 * [[nodiscard]] и [[fallthrough]], static_assert без сообщения, #embed.
 * Тот же файл с -std=c11 обязан не собраться. */
#include <stdint.h>

constexpr uint32_t PCLK_HZ = 72'000'000;
constexpr uint32_t BAUD = 115'200;
static_assert((PCLK_HZ + BAUD / 2) / BAUD == 625);

enum mode : uint8_t { MODE_IDLE, MODE_RUN, MODE_FAULT = 0b1000'0000 };
static_assert(sizeof(enum mode) == 1);

static const uint8_t logo[] = {
#embed "logo128.bin"
};
static_assert(sizeof logo == 4);

bool ready = false;

[[nodiscard]] int start_dma(void);

uint32_t *first_or_null(uint32_t *p)
{
    return p ? p : nullptr;
}

uint32_t uart_div(void)
{
    auto d = (PCLK_HZ + BAUD / 2) / BAUD;
    typeof(d) copy = d;
    return copy;
}

int classify(int x)
{
    switch (x) {
    case 0:
        x += 10;
        [[fallthrough]];
    case 1:
        return x + 1;
    default:
        return -1;
    }
}

uint8_t logo_byte(unsigned i)
{
    return i < sizeof logo ? logo[i] : 0u;
}

void kick(void)
{
    start_dma();
}

Дальше: раздел 129 «MISRA C и защитное программирование».

Открыть в интерактивном справочнике