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 без блокировки: код не ждёт в цикле
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, ¬e) != 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 Итоги
- Активный зуммер — генератор внутри, нужен уровень; пассивный — частоту даёт таймер, меандр 50 %.
- Зуммер — через транзистор; электромагнитному — обратный диод; PA8 —
TIM1_CH1, полубайт 0xB, у TIM1 битMOE. - Нота: 440 × 2^((n − 69) / 12) Гц; при 72 МГц наименьший делитель
держит 108 нот C0…B8 с ошибкой не больше 0,0882 цента;
PSC= 0 хватает с C#6. - Счёт 1 МГц даёт до 6,17 цента ошибки на B8, округление до целых герц — до 2,84 цента на C4.
- Мелодия — таблица (нота, мс), плеер раз в 1 мс из SysTick, зазор 20 мс; мотив из 12 шагов — 3500 мс, 23 записи в таймер, сдвиг начала ноты до 1,902 мс.
- Громкость скважностью — 0,16 от полной при 5 % и лишние гармоники;
предзагрузка
ARPE/OCxPEубирает рваные периоды.
Полные исходники
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, ¬e) != 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, когда таймеров на все приводы и светодиоды не хватает.