Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103 · уровень средний · 19 мин чтения
Открыть в интерактивном справочнике

13б. Зуммер: активный и пассивный, ноты на таймере, мелодия без блокировки

В разделе 13а таймер задавал сервоприводу ширину импульса при постоянной частоте. Зуммеру нужно обратное: смысл несёт частота, а заполнение держится около половины. Здесь — чем активный зуммер отличается от пассивного, как включить его через транзистор, как из ноты получить PSC и ARR таймера TIM1 на 72 МГц с ошибкой в сотые доли цента, как играть мелодию из прерывания SysTick, не останавливая main, и почему скважность — плохая ручка громкости.

Опоры: RM0008 (DocID13902 Rev 17): ШИМ, предзагрузка OCxPE и ARPE, бит UG, с. 386; бит MOE, с. 358; режимы выводов в CRH, с. 171. DocID13587 Rev 17 (STM32F103x8/xB): PA8 — TIM1_CH1 с допуском 5 В, с. 31; FT — допуск 5 В, с. 33; предельный ток вывода 25 мА, с. 37. Документов на зуммеры в проекте нет: частоты нот — равномерная темперация от A4 = 440 Гц, считаются в прогоне. Все числа ниже — прогон кода _verify/buzzer13c/buzzer13c.c на компьютере (gcc 12.2.0) со своей моделью TIM1 на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3.

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

Оригинал Перевод
active buzzer активный зуммер: генератор внутри, нужен только уровень
passive buzzer, piezo transducer пассивный зуммер, пьезоизлучатель: частоту даёт контроллер
magnetic buzzer электромагнитный зуммер: катушка, нужен диод
equal temperament равномерная темперация: полутон — множитель 2^(1/12)
cent цент — сотая доля полутона
prescaler (PSC) предделитель таймера
auto-reload register (ARR) регистр перезагрузки: период счёта
duty cycle скважность (заполнение)
update event событие обновления: конец периода, буферы переходят в рабочие регистры
preload (ARPE, OCxPE) предзагрузка: новое значение — со следующего периода
flyback diode обратный диод: гасит выброс катушки
non-blocking без блокировки: код не ждёт в цикле

Вверху — два вида зуммеров: активный, генератор внутри — нужен только уровень, «1» — звучит свой тон, PB0 — обычный выход, в модели стенда тон 2,4 кГц; пассивный — звук даёт частота, которую подаёт таймер, PA8 = TIM1_CH1, меандр с частотой ноты, «1» без ШИМ — один щелчок. Посередине — подключение: вывод только управляет, ток пищалки идёт через транзистор, вывод F103 — не больше 25 мА, у магнитного зуммера гасящий диод VD1 (раздел 51.2), у пьезо катушки нет; справа — TIM1 для ноты A4: PSC 2, ARR 54 544, CCR1 27 272, 72 МГц / 3 / 54 545 = 440,004 Гц, CCR1 — половина периода, пауза — CCR1 = 0. Внизу — мелодия без ожидания: SysTick раз в 1 мс зовёт bz_tick, шаги E4 A4 B4 D5 C5 A4, шаг 200 мс — звук минус зазор 20 мс тишины, tone_set пишет PSC, ARR и CCR1 в буферы, новая нота звучит с ближайшего обновления TIM1 — задержка не больше 1,9 мс


13б.1 Активный и пассивный: как различить

Активный зуммер несёт генератор внутри: подали уровень — он пищит на своей частоте, сняли — замолчал. Контроллеру достаточно обычного выхода, как светодиоду, но мелодии из него не сыграть: частота одна.

Пассивный зуммер — просто пьезопластина или катушка: на постоянном уровне он молчит, звук даёт только переменный сигнал нужной частоты. Его и надо кормить ШИМ-меандром от таймера — зато любая нота.

Различить на столе: подать на зуммер постоянное напряжение через резистор. Активный запищит, пассивный лишь щёлкнет в момент подключения. Ещё признак — у активного часто плата с компонентами снизу и метка «+», у пассивного — голая пластина.

В прошивке этого раздела оба: пассивный на PA8 (TIM1_CH1), активный на PB0 — короткий писк при старте, без ожидания в коде.


13б.2 Подключение: транзистор и диод

Ток зуммера лучше не брать с вывода: зуммер — через NPN-транзистор, вывод — на базу через резистор, зуммер — между плюсом питания и коллектором. Тогда вывод отдаёт только ток базы, а напряжение на зуммере можно взять 5 В, выше 3,3 В логики. У электромагнитного зуммера (катушка) параллельно ему нужен обратный диод: в момент выключения катушка выбрасывает напряжение, и без диода оно бьёт в транзистор. Пьезоизлучателю диод не нужен.

PA8 — вывод с допуском 5 В (FT, DocID13587, с. 31 и 33), но допуск говорит лишь о том, что вывод выдержит 5 В на входе. Ток вывода ограничен 25 мА (с. 37) — и это предельное значение, а не рабочее. Номиналы резистора и ток здесь не считались — это зависит от зуммера и транзистора, берите их из документов на свои детали.

Вывод под таймер — альтернативная функция push-pull, полубайт 0xB в CRH (RM0008, с. 171); у TIM1 ещё нужен бит MOE (с. 358), иначе выход молчит при работающем счётчике (раздел 13.5):

static void tim1_init(void)
{
    GPIOA->CRH = (GPIOA->CRH & ~0xFu) | 0xBu;           /* PA8 — TIM1_CH1, push-pull, 50 МГц */
    TIM1->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;   /* ШИМ 1, буфер CCR1 */
    TIM1->CCER = TIM_CCER_CC1E;
    TIM1->BDTR = TIM_BDTR_MOE;                          /* у TIM1 без MOE выход молчит (13.5) */
    TIM1->CCR1 = 0;
    TIM1->CR1 = TIM_CR1_ARPE;                           /* буфер ARR */
    TIM1->EGR = TIM_EGR_UG;
    TIM1->CR1 |= TIM_CR1_CEN;
}

13б.3 Ноты: от A4 = 440 Гц к PSC и ARR

Частота ноты с номером MIDI n в равномерной темперации — 440 × 2^((n − 69) / 12) Гц: A4 — это 69, C4 — 60, полутон — множитель 2^(1/12). В коде таблица одной октавы (восьмой) в сотых долях герца, а ниже — деление пополам на каждую октаву, без плавающей точки:

uint32_t bz_ticks(uint8_t note, uint32_t tim_hz)
{
    if (note < BZ_LOW || note > BZ_HIGH)
        return 0;                                       /* пауза и всё вне таблицы — тишина, без деления */
    uint32_t f = f8_chz[note % 12u];
    uint32_t sh = 9u - note / 12u;                      /* сколько октав вниз от восьмой */
    uint64_t num = ((uint64_t)tim_hz * 100u) << sh;
    return (uint32_t)((num + f / 2u) / f);              /* f_tim / f_ноты, округлено */
}

bz_ticks даёт период ноты в тактах таймера: 72 МГц / f. Для A4 это 163 636 тактов — в 16-битный ARR не влезает, поэтому bz_timing берёт наименьший делитель, при котором влезает, и округляет период:

int bz_timing(uint32_t ticks, bz_tim_t *t)
{
    if (ticks < 2u || (ticks - 1u) >> 16 > 0xFFFFu) {
        t->ccr = 0;                                     /* CCR = 0 — выход в нуле, звука нет */
        return 0;
    }
    uint32_t d = ((ticks - 1u) >> 16) + 1u;             /* наименьший делитель, при котором ARR влезает в 16 бит */
    uint32_t per = (ticks + d / 2u) / d;                /* период в отсчётах счётчика, округлён */
    t->psc = (uint16_t)(d - 1u);
    t->arr = (uint16_t)(per - 1u);
    t->ccr = (uint16_t)(per / 2u);                      /* меандр: половина периода */
    return 1;
}

Прогон для нот мотива и до первой октавы (TIM1 считает от 72 МГц):

Нота Точно, Гц PSC ARR CCR Получилось, Гц Ошибка, цент
C4 261,63 4 55 039 27 520 261,628 +0,0155
D4 293,66 3 61 293 30 647 293,667 +0,0107
E4 329,63 3 54 606 27 303 329,628 +0,0027
G4 392,00 2 61 224 30 612 391,997 +0,0057
A4 440,00 2 54 544 27 272 440,004 +0,0144
B4 493,88 2 48 593 24 297 493,888 +0,0169
C5 523,25 2 45 866 22 933 523,252 +0,0029
D5 587,33 1 61 294 30 647 587,324 −0,0175

Вся таблица от C0 (16,35 Гц) до B8 (7902,13 Гц) — 108 нот: худшая ошибка 0,0882 цента у B8, CCR везде ровно половина периода. Делитель растёт к низким нотам: у C0 PSC = 67, а начиная с C#6 (1108,73 Гц) хватает PSC = 0 — при 72 МГц 16-битный счёт без делителя даёт не ниже 1098,6 Гц. Пауза и номера вне таблицы (0, 11, 120) дают 0 тактов — тишину, без деления.

Попробуйте в приложении: Лаборатория → стенд «Зуммер».


13б.4 Точность: свой делитель против счёта 1 МГц

Часто таймер ставят на 1 МГц (PSC = 71) и пишут ARR = 1 000 000 / f. Шаг такого счёта — 1 мкс, и на высоких нотах период становится коротким, а округление — заметным:

Нота ARR Получилось, Гц Ошибка, цент
A4 2272 439,947 −0,21
C7 477 2092,050 −0,79
C8 238 4184,100 −0,79
B8 126 7874,016 −6,17

От C4 до B8 худшая ошибка — 6,17 цента у B8. Наименьший делитель из 13б.3 держит все 108 нот в пределах 0,0882 цента. Пример «Гамма до мажор» в лаборатории округляет частоту до целых герц и пишет ARR при счёте 1 МГц: худшая нота C4 — 262 Гц вместо 261,63, ошибка +2,84 цента. Тон «1 кГц» из примера «Тон 1 кГц на TIM1» — это не нота: ближайшая B5, и он выше неё на 21,3 цента.


13б.5 Мелодия без блокировки

Мелодия — массив шагов (нота, длительность в мс), пауза — нота 0. Плеер bz_tick вызывают раз в 1 мс из прерывания SysTick; он сам считает время шага и говорит, когда сменить ноту. В конце каждой ноты — зазор тишины, иначе две одинаковые ноты подряд сольются в одну:

int bz_tick(bz_player_t *p, uint8_t *note)
{
    if (!p->on)
        return BZ_NONE;
    if (p->i >= p->n) {
        if (!p->loop) {
            p->on = 0;
            *note = BZ_REST;
            return BZ_END;                              /* мелодия кончилась: тишина */
        }
        p->i = 0;
    }
    const bz_step_t *s = &p->song[p->i];
    uint8_t want = s->note;
    if (want != BZ_REST && p->t + p->gap >= s->ms)
        want = BZ_REST;                                 /* хвост ноты — зазор, чтобы две одинаковые не слились */
    if (++p->t >= s->ms) {
        p->t = 0;                                       /* шаг кончился: со следующей мс — новый */
        p->i++;
    }
    if (want == p->cur)
        return BZ_NONE;                                 /* таймер уже играет то, что нужно */
    p->cur = want;
    *note = want;
    return BZ_NOTE;
}

В прошивке плеер ведёт прерывание, а main свободен — мигает светодиодом по счётчику миллисекунд, не останавливаясь:

void SysTick_Handler(void)
{
    uint8_t note;
    ms++;
    if (bz_tick(&pl, &note) != BZ_NONE)
        tone_set(note);
    if (beep_left && --beep_left == 0u)
        GPIOB->BSRR = GPIO_BSRR_BR0;                    /* активный: время вышло — «0» */
}
static void tone_set(uint8_t note)
{
    bz_tim_t t;
    if (!bz_timing(bz_ticks(note, TIM_HZ), &t)) {
        TIM1->CCR1 = 0;                                 /* пауза: выход в нуле, ARR не трогаем */
        return;
    }
    TIM1->PSC = t.psc;                                  /* все три — в буферы: вступят вместе */
    TIM1->ARR = t.arr;                                  /* на ближайшем обновлении, без обрезанного периода */
    TIM1->CCR1 = t.ccr;
}

Новые PSC, ARR и CCR пишутся в буферы (ARPE и OCxPE, RM0008, с. 386) и вступают вместе на ближайшем обновлении — поэтому звук ноты начинается не сразу, а в модели позже записи до 1,902 мс. Мотив из 12 шагов (тот же, что в примере стенда «Мелодия по таблице нот») с зазором 20 мс играется за 3500 мс: 23 записи в таймер, 11 нот, длительности отличаются от заданных (минус зазор) не больше чем на 2,29 мс. Без зазора записей 12, а нот 10: две A4 по 150 мс подряд слились в одну A4 длиной 300 мс.


13б.6 Громкость и скважность

Заманчиво делать громкость скважностью: CCR меньше половины — тише. Но меандр 50 % — самый сильный по основной частоте, а меньшее заполнение не только ослабляет её, но и добавляет вторую гармонику — звук становится тоньше и визгливее. Прогон по разложению меандра в ряд:

Заполнение CCR (A4) Основная, отн. 50 % Вторая гармоника
50 % 27 272 1,00 0,0000
25 % 13 636 0,71 0,3183
10 % 5454 0,31 0,1871
5 % 2727 0,16 0,0984

Основная гармоника меандра пропорциональна sin(π·D): при 5 % остаётся 0,16 от 50 %. Пример стенда «Ошибка: скважность 5 % — тихо» — CCR = 50 при ARR = 999 — даёт те же 0,16. Ровную громкость лучше менять питанием зуммера или выбором самого зуммера, а скважность оставить 50 %.


13б.7 Грабли

Пауза в таблице частот — и контроллер падает. Тон по герцам без проверки: 1000000 / hz при hz = 0. На компьютере такой процесс убит исключением деления (код возврата 136). bz_ticks возвращает 0 для паузы до всякого деления, а tone_set ставит CCR1 = 0 — тишину.

Нота вне диапазона молчит или пищит не той. Номер меньше 12 или больше 119 в таблицу не попадает и даёт тишину; без проверки сдвиг на октаву ушёл бы за пределы.

Забытый делитель. PSC = 0 вместо 71 при ARR = 2271: таймер считает 72 МГц, и «440 Гц» превращаются в 31 690 Гц — ухо не слышит (пример стенда «Ошибка: PSC = 0 — выше 20 кГц»).

Щелчки и рваные ноты. Без предзагрузки новый ARR вступает сразу: если счётчик уже ушёл дальше, он досчитывает до 65 535 и только потом обнуляется — лишние 2,731 мс при PSC = 2 и 3,641 мс при PSC = 3. В прогоне 10 кругов мотива без зазора: с предзагрузкой ни одного кривого периода, без неё — 7 растянутых и 24 укороченных, худший на 2,731 мс.

Две одинаковые ноты звучат как одна. Нет зазора между нотами — см. 13б.5: A4 и A4 слились.

Мелодия останавливает программу. Ноты через задержки в main: пока играет, кнопки и датчики не опрашиваются. Плеер — в прерывании SysTick, main свободен.

Активный зуммер на ШИМ. У активного свой генератор (в модели стенда — 2,4 кГц): ШИМ на входе его то включает, то выключает, и в стенде вместо чистого тона треск. Ему — обычный выход, уровень «1».


13б.8 Итоги

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

buzzer13c.h — ноты, тайминг таймера, шаги мелодии и плеер.

/* buzzer13c.h — раздел 13б: ноты и мелодия для пассивного зуммера без регистров.
 * Ноты — номера MIDI (69 — A4 = 440 Гц, 60 — C4), 0 — пауза. Частоты — равномерная темперация.
 * bz_ticks и bz_timing переводят ноту в PSC/ARR/CCR таймера, плеер bz_tick вызывают раз в 1 мс
 * (в прошивке fw13c.c — из SysTick, в стенде host13c.c — в цикле), он говорит, когда сменить ноту. */
#ifndef BUZZER13C_H
#define BUZZER13C_H
#include <stdint.h>

#define BZ_REST     0u                  /* пауза: таймер молчит, деления нет */
#define BZ_LOW     12u                  /* C0, 16,35 Гц — ниже таблицы нет */
#define BZ_HIGH   119u                  /* B8, 7902 Гц — последняя нота таблицы */

/* ноты мелодии из стенда лаборатории */
#define D4 62u
#define E4 64u
#define G4 67u
#define A4 69u
#define B4 71u
#define C5 72u
#define D5 74u

typedef struct {
    uint16_t psc, arr, ccr;             /* значения для TIM1: частота = f_tim / ((PSC + 1)(ARR + 1)) */
} bz_tim_t;

uint32_t bz_ticks(uint8_t note, uint32_t tim_hz);       /* период ноты в тактах таймера; 0 — пауза или вне таблицы */
int bz_timing(uint32_t ticks, bz_tim_t *t);             /* PSC, ARR и CCR = половина периода; 0 — тишина */
uint16_t bz_ccr(uint16_t arr, uint8_t pct);             /* CCR для заполнения pct % */

/* ── мелодия: шаги (нота, длительность) ─────────────────────────────── */
typedef struct {
    uint8_t note;                       /* MIDI или BZ_REST */
    uint16_t ms;                        /* длительность шага с паузой-зазором */
} bz_step_t;

enum { BZ_NONE, BZ_NOTE, BZ_END };      /* что вернул bz_tick */

typedef struct {
    const bz_step_t *song;
    uint16_t n, i;                      /* шагов, текущий шаг */
    uint16_t t;                         /* мс от начала шага */
    uint8_t gap;                        /* тишина в конце каждой ноты, мс */
    uint8_t loop, on;
    uint8_t cur;                        /* что сейчас стоит на таймере; 0xFF — ещё ничего */
} bz_player_t;

void bz_play(bz_player_t *p, const bz_step_t *song, uint16_t n, uint8_t gap_ms, uint8_t loop);
int bz_tick(bz_player_t *p, uint8_t *note);             /* раз в 1 мс; BZ_NOTE — поставить *note */

#define BZ_DEMO_N 12u
extern const bz_step_t bz_demo[BZ_DEMO_N];              /* мотив из стенда лаборатории */

#endif

buzzer13c.c — частоты нот, PSC/ARR, плеер мелодии.

/* buzzer13c.c — раздел 13б: ноты в PSC/ARR и плеер мелодии без регистров. */
#include "buzzer13c.h"

/* Октава 8 (MIDI 108…119) в сотых долях герца: 440 × 2^((n − 69) / 12) × 100, округлено.
 * Нижние октавы — делением пополам: на октаву ниже частота вдвое меньше. */
static const uint32_t f8_chz[12] = {
    418601u, 443492u, 469864u, 497803u, 527404u, 558765u,   /* C8 C#8 D8 D#8 E8 F8 */
    591991u, 627193u, 664488u, 704000u, 745862u, 790213u,   /* F#8 G8 G#8 A8 A#8 B8 */
};

uint32_t bz_ticks(uint8_t note, uint32_t tim_hz)
{
    if (note < BZ_LOW || note > BZ_HIGH)
        return 0;                                       /* пауза и всё вне таблицы — тишина, без деления */
    uint32_t f = f8_chz[note % 12u];
    uint32_t sh = 9u - note / 12u;                      /* сколько октав вниз от восьмой */
    uint64_t num = ((uint64_t)tim_hz * 100u) << sh;
    return (uint32_t)((num + f / 2u) / f);              /* f_tim / f_ноты, округлено */
}

int bz_timing(uint32_t ticks, bz_tim_t *t)
{
    if (ticks < 2u || (ticks - 1u) >> 16 > 0xFFFFu) {
        t->ccr = 0;                                     /* CCR = 0 — выход в нуле, звука нет */
        return 0;
    }
    uint32_t d = ((ticks - 1u) >> 16) + 1u;             /* наименьший делитель, при котором ARR влезает в 16 бит */
    uint32_t per = (ticks + d / 2u) / d;                /* период в отсчётах счётчика, округлён */
    t->psc = (uint16_t)(d - 1u);
    t->arr = (uint16_t)(per - 1u);
    t->ccr = (uint16_t)(per / 2u);                      /* меандр: половина периода */
    return 1;
}

uint16_t bz_ccr(uint16_t arr, uint8_t pct)
{
    return (uint16_t)(((uint32_t)arr + 1u) * pct / 100u);
}

void bz_play(bz_player_t *p, const bz_step_t *song, uint16_t n, uint8_t gap_ms, uint8_t loop)
{
    *p = (bz_player_t){0};
    p->song = song;
    p->n = n;
    p->gap = gap_ms;
    p->loop = loop;
    p->on = n > 0u;
    p->cur = 0xFFu;
}

int bz_tick(bz_player_t *p, uint8_t *note)
{
    if (!p->on)
        return BZ_NONE;
    if (p->i >= p->n) {
        if (!p->loop) {
            p->on = 0;
            *note = BZ_REST;
            return BZ_END;                              /* мелодия кончилась: тишина */
        }
        p->i = 0;
    }
    const bz_step_t *s = &p->song[p->i];
    uint8_t want = s->note;
    if (want != BZ_REST && p->t + p->gap >= s->ms)
        want = BZ_REST;                                 /* хвост ноты — зазор, чтобы две одинаковые не слились */
    if (++p->t >= s->ms) {
        p->t = 0;                                       /* шаг кончился: со следующей мс — новый */
        p->i++;
    }
    if (want == p->cur)
        return BZ_NONE;                                 /* таймер уже играет то, что нужно */
    p->cur = want;
    *note = want;
    return BZ_NOTE;
}

const bz_step_t bz_demo[BZ_DEMO_N] = {
    {E4, 200}, {A4, 200}, {B4, 200}, {D5, 400}, {C5, 200}, {A4, 200},
    {G4, 200}, {B4, 400}, {A4, 150}, {A4, 150}, {D4, 600}, {BZ_REST, 600},
};

fw13c.c — прошивка STM32F103: TIM1 на PA8, активный зуммер на PB0, плеер в SysTick.

/* fw13c.c — раздел 13б: зуммеры на STM32F103 (BluePill), 72 МГц.
 * Пассивный зуммер — через транзистор на PA8 (TIM1_CH1): ШИМ-меандр с частотой ноты.
 * Активный зуммер — через транзистор на PB0: свой генератор, включается уровнем «1».
 * SysTick раз в 1 мс ведёт плеер мелодии (buzzer13c.c) и отсчитывает писк активного;
 * основной цикл свободен — мигает светодиодом PC13 по счётчику миллисекунд.
 * Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется); TIM1 на APB2 считает от 72 МГц. */
#include "stm32f10x.h"
#include "buzzer13c.h"

#define TIM_HZ 72000000u

_Static_assert(TIM_HZ / 1000u - 1u == 71999u, "SysTick: LOAD = 71999 — прерывание раз в 1 мс");

static bz_player_t pl;
static volatile uint32_t ms;
static volatile uint32_t beep_left;

static void tone_set(uint8_t note)
{
    bz_tim_t t;
    if (!bz_timing(bz_ticks(note, TIM_HZ), &t)) {
        TIM1->CCR1 = 0;                                 /* пауза: выход в нуле, ARR не трогаем */
        return;
    }
    TIM1->PSC = t.psc;                                  /* все три — в буферы: вступят вместе */
    TIM1->ARR = t.arr;                                  /* на ближайшем обновлении, без обрезанного периода */
    TIM1->CCR1 = t.ccr;
}

void SysTick_Handler(void)
{
    uint8_t note;
    ms++;
    if (bz_tick(&pl, &note) != BZ_NONE)
        tone_set(note);
    if (beep_left && --beep_left == 0u)
        GPIOB->BSRR = GPIO_BSRR_BR0;                    /* активный: время вышло — «0» */
}

static void beep(uint32_t len_ms)                       /* активный зуммер: писк без ожидания */
{
    __disable_irq();
    beep_left = len_ms;
    GPIOB->BSRR = GPIO_BSRR_BS0;
    __enable_irq();
}

static void tim1_init(void)
{
    GPIOA->CRH = (GPIOA->CRH & ~0xFu) | 0xBu;           /* PA8 — TIM1_CH1, push-pull, 50 МГц */
    TIM1->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;   /* ШИМ 1, буфер CCR1 */
    TIM1->CCER = TIM_CCER_CC1E;
    TIM1->BDTR = TIM_BDTR_MOE;                          /* у TIM1 без MOE выход молчит (13.5) */
    TIM1->CCR1 = 0;
    TIM1->CR1 = TIM_CR1_ARPE;                           /* буфер ARR */
    TIM1->EGR = TIM_EGR_UG;
    TIM1->CR1 |= TIM_CR1_CEN;
}

int main(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_TIM1EN;
    GPIOB->CRL = (GPIOB->CRL & ~0xFu) | 0x2u;           /* PB0 — выход push-pull 2 МГц, к базе транзистора */
    GPIOC->CRH = (GPIOC->CRH & ~(0xFu << 20)) | 0x2u << 20;    /* PC13 — светодиод платы */
    tim1_init();
    bz_play(&pl, bz_demo, BZ_DEMO_N, 20u, 1u);          /* зазор 20 мс, по кругу */
    SysTick_Config(TIM_HZ / 1000u);
    beep(100u);
    uint32_t next = 0;
    for (;;) {
        if ((int32_t)(ms - next) >= 0) {                /* не ждём: просто смотрим на часы */
            next += 500u;
            GPIOC->ODR ^= 1u << 13;
        }
    }
}

Дальше: раздел 13в «16-канальный ШИМ PCA9685 по I2C: сервоприводы и светодиоды без таймеров микроконтроллера» — шестнадцать каналов ШИМ по двум проводам I2C, когда таймеров на все приводы и светодиоды не хватает.

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