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

125. Объявления и указатели на функции: таблицы обработчиков и обратные вызовы

Объявления в Си читаются не слева направо, и void (*const handlers[3])(void) пугает даже тех, кто пишет на Си годами. Но именно такие конструкции держат прошивку: таблица векторов прерываний — массив указателей на функции, драйверы отдают данные обратными вызовами, конечные автоматы переключают состояния указателем на функцию. Раздел учит читать объявления по одному правилу, проверяя себя самим компилятором, и строить таблицы обработчиков так, чтобы они жили во флеш-памяти.

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

Оригинал Перевод
declarator описатель: имя вместе с *, [] и ()
pointer to function указатель на функцию
function pointer table, dispatch table таблица обработчиков
callback обратный вызов
context pointer (void *user) указатель на контекст
typedef псевдоним типа
generic selection (_Generic) выбор по типу выражения
state machine конечный автомат
tail call вызов в конце функции, ставший переходом
indirect branch непрямой переход по адресу из регистра

125.1 Правило чтения: от имени наружу

Как читать объявление таблицы обработчиков: от имени — сначала вправо, потом влево, скобки меняют порядок

Объявление читают от имени, сначала вправо ([] — массив, () — функция), потом влево (* — указатель), а скобки меняют порядок, как в арифметике. void (*const handlers[3])(void): handlers — справа [3], массив из трёх; слева *const — константных указателей; за скобкой справа (void) — на функции без аргументов; слева void — возвращающие void.

Проверять себя удобно не глазами, а компилятором: _Generic выбирает ветку по типу выражения, и программа сама называет тип.

int (*)[4]: "указатель на массив из 4 int",                  \
void (**)(void): "указатель на указатель на функцию",        \

125.2 Скобки меняют всё

Опыт — три объявления, их типы по _Generic и размеры на двух машинах:

Объявление Что это Cortex-M, байт x86-64, байт
int *ptrs[4] массив из 4 указателей на int 16 32
int (*pa)[4] указатель на массив из 4 int 4 8
void (*table[3])(void) массив из 3 указателей на функцию 12 24

int *ptrs[4] и int (*pa)[4] отличаются одной парой скобок, а по смыслу — всем: первое занимает четыре указателя, второе — один. Названия типов у gcc, clang и ARM совпали полностью, размеры на Cortex-M вдвое меньше — указатель там 4 байта (раздел 122).

Сам массив _Generic не назовёт: выражение внутри проходит обычные преобразования, и массив превращается в указатель на первый элемент — _Generic(ptrs, …) видит int **, а _Generic(table, …) — void (**)(void). Поэтому опыт спрашивает тип элемента (ptrs[0]) и адреса (&table[0]), а тип самого массива проверяют по его адресу: _Generic(&ptrs, int *(*)[4]: …).

Массив из четырёх указателей на int и один указатель на целый массив из четырёх int


125.3 typedef делает объявления читаемыми

Длинное объявление разбивают псевдонимом типа:

typedef void (*handler_t)(void);

После этого handler_t table[3] читается сразу. Правило для прошивки: тип обработчика или обратного вызова — всегда через typedef, с именем, которое говорит о назначении, а не об устройстве.


125.4 Таблица команд

Классический разбор команд из UART или CAN: имя и обработчик в одной таблице, отсортированной по имени, поиск — двоичный.

static const struct cmd commands[] = {         /* по алфавиту — для двоичного поиска */
    { "led", cmd_led },
    { "reset", cmd_reset },
    { "speed", cmd_speed },
};
const struct cmd *c = bsearch(name, commands, sizeof commands / sizeof commands[0],
                              sizeof commands[0], by_name);
return c ? c->fn(arg) : -1;

В опыте reset 7 дал 207, speed 1 — 301, неизвестная команда — -1. Добавить команду — одна строка в таблице, а не новая ветка в switch на двести строк. Главное условие двоичного поиска — порядок строк; его удобно проверять тестом на компьютере.


125.5 Таблица во флеш-памяти

Для платы важно, где лежит таблица. const у самого указателя — void (*const handlers[3])(void) — делает таблицу неизменяемой, и armclang кладёт её прямо за кодом функции, во флеш-память:

if (event < sizeof handlers / sizeof handlers[0])
    handlers[event]();               /* непрямой вызов: BLX по адресу из таблицы */

В объектном файле функция вызова — это проверка границы CMP #2, чтение адреса из таблицы LDR r0, [r1, r0, LSL #2] и переход BX r0: вызов стал переходом, потому что он последний в функции. Три адреса обработчиков лежат сразу за этими командами.

Та же таблица без const оказывается в .data — в ОЗУ. Там её может испортить любая ошибка записи за границу массива, и следующий вызов уйдёт по мусорному адресу — прямой путь в HardFault. Проверка границы перед вызовом обязательна в обоих случаях: индекс чаще всего приходит извне.


125.6 Обратные вызовы с контекстом

Драйвер не знает, что делать с принятыми данными, — он вызывает функцию, которую ему дали. Чтобы одна и та же функция обслуживала несколько экземпляров, ей передают контекст через void *:

typedef void (*visit_fn)(int value, void *ctx);
static void sum_up(int value, void *ctx)
{
    *(int *)ctx += value;
}

Обход for_each не знает, что за ctx, — в опыте он сложил 3 + 1 + 4 + 1 + 5 и получил 14, а та же функция обхода с другим обратным вызовом ищет максимум. Так устроены обратные вызовы HAL (HAL_UART_RegisterCallback), очереди FreeRTOS и почти любые библиотеки на Си.


125.7 Конечный автомат на указателях

Состояние автомата — это функция, которая по входу возвращает следующее состояние:

typedef const struct state *(*step_fn)(uint8_t input);
current = current->step(in);

Автомат из опыта ищет в потоке байтов пару «AB». Добавить состояние — одна функция и одна структура; switch по номеру состояния с каждым новым случаем растёт и путается. Состояния объявлены const — они тоже лежат во флеш-памяти.

Второй способ — таблица «состояние × событие». Когда состояний и событий немного, а значение имеет каждое сочетание, автомат удобнее записать одной таблицей: строка — текущее состояние, столбец — событие, в ячейке — следующее состояние и действие.

struct cell {
    uint8_t next;
    void (*act)(void);
};

static const struct cell fsm[ST_N][EV_N] = {
    /*              пуск                  стоп                  перегрев                сброс */
    [ST_IDLE]  = { {ST_RUN, motor_start}, {ST_IDLE, none},      {ST_FAULT, none},       {ST_IDLE, none} },
    [ST_RUN]   = { {ST_RUN, none},        {ST_IDLE, motor_stop}, {ST_FAULT, motor_stop}, {ST_RUN, none} },
    [ST_FAULT] = { {ST_FAULT, none},      {ST_FAULT, none},     {ST_FAULT, none},       {ST_IDLE, none} },
};

static void feed(enum event ev)
{
    if (ev >= EV_N)                     /* событие пришло извне — проверить */
        return;
    const struct cell *c = &fsm[st][ev];
    if (c->act)                         /* пропущенная ячейка — NULL */
        c->act();
    st = (enum state)c->next;
}

Каждая пара записана явно. В switch забытый case молча уходит в default, а здесь незаполненная ячейка видна при чтении. При работе она нулевая: указатель на функцию — NULL, следующее состояние — 0. Поэтому во всех ячейках стоит хотя бы none, а feed проверяет и указатель, и номер события, пришедший извне (125.5). В опыте двигатель проходит цепочку «пуск → перегрев → пуск → сброс → пуск → стоп»: перегрев в работе останавливает двигатель и переводит в аварию, пуск в аварии ничего не делает, выйти из неё можно только сбросом. Таблица из 12 ячеек занимает 96 байт на Cortex-M — ячейка 8 байт: номер состояния и указатель на функцию с выравниванием — и 192 байта на 64-разрядном компьютере. С const она лежит во флеш-памяти.


125.8 Таблица векторов — такой же массив

Стартовый файл Cortex-M — массив: первое слово — начальный указатель стека, дальше — адреса обработчиков (раздел 7). Написанный на Си, он выглядит так же, как таблица из 125.5, только компоновщик кладёт её в начало флеш-памяти (0x08000000), а по адресу 0 её показывает отображение памяти при загрузке; приложение за загрузчиком указывает её адрес в VTOR (11.7). Поэтому опечатка в имени обработчика прерывания не ошибка компиляции: в таблицу попадает обработчик по умолчанию, и прерывание «уходит в пустоту».


125.9 Опасности


125.10 Грабли

Не могу прочитать объявление. От имени: вправо [] и (), влево *, скобки меняют порядок. Разбить typedef. Проверить _Generic.

HardFault при вызове обработчика. Нулевой указатель или испорченная таблица в ОЗУ. Проверка перед вызовом, таблица const.

Команда не находится, хотя есть в таблице. Таблица для двоичного поиска не отсортирована. Тест порядка на компьютере.

Обработчик прерывания не вызывается. Опечатка в имени — в векторах остался обработчик по умолчанию.

sizeof массива в функции — 4. Массив в параметре стал указателем (раздел 68). Передавать размер отдельно.


125.11 Итоги

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

decl125.c — сложные объявления, таблица команд и обратный вызов с контекстом — опыт на трёх машинах.

/* Раздел 125: объявления и указатели на функции — опыт на трёх машинах.
 * Типы сложных объявлений называет сама программа через _Generic: так нельзя
 * ошибиться в чтении — компилятор решает, что это за тип. Дальше — таблица
 * команд с поиском по имени и обратный вызов с указателем на контекст. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define KIND(x) _Generic((x),                                    \
    int *: "указатель на int",                                   \
    int **: "указатель на указатель на int",                     \
    int (*)[4]: "указатель на массив из 4 int",                  \
    void (*)(void): "указатель на функцию",                      \
    void (**)(void): "указатель на указатель на функцию",        \
    default: "другое")

static void on_start(void) { }

/* ── таблица команд: имя и обработчик, отсортирована по имени ─────────── */
typedef int (*cmd_fn)(int arg);

static int cmd_led(int arg) { return 100 + arg; }
static int cmd_reset(int arg) { return 200 + arg; }
static int cmd_speed(int arg) { return 300 + arg; }

struct cmd {
    const char *name;
    cmd_fn fn;
};

static const struct cmd commands[] = {         /* по алфавиту — для двоичного поиска */
    { "led", cmd_led },
    { "reset", cmd_reset },
    { "speed", cmd_speed },
};

static int by_name(const void *key, const void *elem)
{
    return strcmp((const char *)key, ((const struct cmd *)elem)->name);
}

static int run_command(const char *name, int arg)
{
    const struct cmd *c = bsearch(name, commands, sizeof commands / sizeof commands[0],
                                  sizeof commands[0], by_name);
    return c ? c->fn(arg) : -1;
}

/* ── обратный вызов с контекстом: функция не знает, что лежит за void * ── */
typedef void (*visit_fn)(int value, void *ctx);

static void for_each(const int *a, int n, visit_fn visit, void *ctx)
{
    for (int i = 0; i < n; i++)
        visit(a[i], ctx);
}

static void sum_up(int value, void *ctx)
{
    *(int *)ctx += value;
}

int main(void)
{
    int x = 0, *p = &x, arr[4] = { 0 };
    int *ptrs[4] = { 0 };
    void (*h)(void) = on_start;
    void (*table[3])(void) = { on_start, on_start, on_start };

    printf("kind_p=%s\n", KIND(p));
    printf("kind_pp=%s\n", KIND(&p));
    printf("kind_arr_addr=%s\n", KIND(&arr));
    printf("kind_ptrs_elem=%s\n", KIND(ptrs[0]));
    printf("kind_h=%s\n", KIND(h));
    printf("kind_table_elem_addr=%s\n", KIND(&table[0]));
    printf("size_ptrs=%zu\n", sizeof ptrs);         /* массив из 4 указателей */
    printf("size_arr_ptr=%zu\n", sizeof(&arr));     /* один указатель на массив */
    printf("size_table=%zu\n", sizeof table);       /* 3 указателя на функцию */

    printf("cmd_reset=%d\n", run_command("reset", 7));
    printf("cmd_speed=%d\n", run_command("speed", 1));
    printf("cmd_unknown=%d\n", run_command("stop", 0));

    int data[5] = { 3, 1, 4, 1, 5 }, total = 0;
    for_each(data, 5, sum_up, &total);
    printf("callback_sum=%d\n", total);
    printf("done=1\n");
    return 0;
}

decl125_arm.c — указатели на функции на Cortex-M: таблица во флеш-памяти и автомат на указателях.

/* Раздел 125: указатели на функции на Cortex-M — armclang для F103 и F407.
 * Таблица обработчиков с const лежит во флеш-памяти, вызов через неё — BLX;
 * конечный автомат, где состояние — это указатель на функцию. */
#include <stdint.h>

typedef void (*handler_t)(void);

volatile uint32_t g_log;

static void on_idle(void) { g_log = 1; }
static void on_run(void) { g_log = 2; }
static void on_error(void) { g_log = 3; }

/* Таблица только для чтения: массив константных указателей на функции. */
static void (*const handlers[3])(void) = { on_idle, on_run, on_error };

/* Та же таблица без const — окажется в ОЗУ и её можно испортить. */
handler_t handlers_ram[3] = { on_idle, on_run, on_error };

void dispatch(unsigned event)
{
    if (event < sizeof handlers / sizeof handlers[0])
        handlers[event]();               /* непрямой вызов: BLX по адресу из таблицы */
}

/* ── конечный автомат: состояние — функция, возвращающая следующее ─────── */
struct state;
typedef const struct state *(*step_fn)(uint8_t input);
struct state {
    step_fn step;
};

static const struct state st_wait, st_got_a;

static const struct state *wait_step(uint8_t in)
{
    return in == 'A' ? &st_got_a : &st_wait;
}

static const struct state *got_a_step(uint8_t in)
{
    if (in == 'B')
        g_log = 0xAB;                    /* найдена пара «AB» */
    return &st_wait;
}

static const struct state st_wait = { wait_step };
static const struct state st_got_a = { got_a_step };

static const struct state *current = &st_wait;

void feed(uint8_t in)
{
    current = current->step(in);
}

fsm125.c — автомат таблицей «состояние × событие».

/* Подраздел 125.7: автомат таблицей «состояние × событие».
 * Двигатель: ожидание, работа, авария; события — пуск, стоп, перегрев, сброс.
 * В ячейке — следующее состояние и действие. Каждая пара «состояние, событие»
 * записана явно: забыть случай нельзя, а таблица const лежит во флеш-памяти. */
#include <stdint.h>
#include <stdio.h>

enum state { ST_IDLE, ST_RUN, ST_FAULT, ST_N };
enum event { EV_START, EV_STOP, EV_HOT, EV_RESET, EV_N };

static int motor_on;
static void none(void) { }
static void motor_start(void) { motor_on = 1; }
static void motor_stop(void) { motor_on = 0; }

struct cell {
    uint8_t next;
    void (*act)(void);
};

static const struct cell fsm[ST_N][EV_N] = {
    /*              пуск                  стоп                  перегрев                сброс */
    [ST_IDLE]  = { {ST_RUN, motor_start}, {ST_IDLE, none},      {ST_FAULT, none},       {ST_IDLE, none} },
    [ST_RUN]   = { {ST_RUN, none},        {ST_IDLE, motor_stop}, {ST_FAULT, motor_stop}, {ST_RUN, none} },
    [ST_FAULT] = { {ST_FAULT, none},      {ST_FAULT, none},     {ST_FAULT, none},       {ST_IDLE, none} },
};

static enum state st = ST_IDLE;

static void feed(enum event ev)
{
    if (ev >= EV_N)                     /* событие пришло извне — проверить */
        return;
    const struct cell *c = &fsm[st][ev];
    if (c->act)                         /* пропущенная ячейка — NULL */
        c->act();
    st = (enum state)c->next;
}

int main(void)
{
    static const char *const sname[ST_N] = {"idle", "run", "fault"};
    static const char *const ename[EV_N] = {"start", "stop", "hot", "reset"};
    static const enum event trace[] = {EV_START, EV_HOT, EV_START, EV_RESET, EV_START, EV_STOP};

    for (unsigned i = 0; i < sizeof trace / sizeof trace[0]; i++) {
        feed(trace[i]);
        printf("step_%u=%s>%s motor=%d\n", i, ename[trace[i]], sname[st], motor_on);
    }
    printf("cell=%u table=%u\n", (unsigned)sizeof(struct cell), (unsigned)sizeof fsm);
    printf("done=1\n");
    return 0;
}

Дальше: раздел 126 «Данные в памяти» — выравнивание, упаковка, битовые поля и порядок байт.

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