Embedded HandbookApps
All sections › III. Peripherals
STM32F103 · level intermediate · 19 min read
Open in the interactive handbook

13б. Buzzer: active and passive, notes on a timer, a non-blocking melody

In section 13а the timer gave a servo the pulse width at a constant frequency. A buzzer needs the opposite: the meaning is in the frequency, and the duty cycle stays near one half. Here is how an active buzzer differs from a passive one, how to switch it through a transistor, how to get PSC and ARR of TIM1 at 72 MHz from a note with an error of hundredths of a cent, how to play a melody from the SysTick interrupt without stopping main, and why the duty cycle is a poor volume knob.

Sources: RM0008 (DocID13902 Rev 17): PWM, the OCxPE and ARPE preload, the UG bit, p. 386; the MOE bit, p. 358; pin modes in CRH, p. 171. DocID13587 Rev 17 (STM32F103x8/xB): PA8 is TIM1_CH1 with 5 V tolerance, p. 31; FT means 5 V tolerant, p. 33; the maximum pin current is 25 mA, p. 37. The project has no documents on buzzers: note frequencies are equal temperament from A4 = 440 Hz and are computed in the run. All numbers below come from a run of the code _verify/buzzer13c/buzzer13c.c on a computer (gcc 12.2.0) with our own model of TIM1 in Python 3.11; the firmware is built by armclang 6.24 for the Cortex-M3.

What it is called in the documentation

Original Meaning
active buzzer active buzzer: the generator is inside, it needs only a level
passive buzzer, piezo transducer passive buzzer, piezo transducer: the controller supplies the frequency
magnetic buzzer electromagnetic buzzer: a coil, it needs a diode
equal temperament equal temperament: a semitone is a factor of 2^(1/12)
cent cent — a hundredth of a semitone
prescaler (PSC) timer prescaler
auto-reload register (ARR) auto-reload register: the counting period
duty cycle duty cycle
update event update event: the end of a period, the buffers move into the working registers
preload (ARPE, OCxPE) preload: a new value takes effect from the next period
flyback diode flyback diode: suppresses the coil spike
non-blocking non-blocking: the code does not wait in a loop

Top — two kinds of buzzers: active, with the generator inside — it needs only a level, "1" sounds its own tone, PB0 is a plain output, 2.4 kHz in the stand model; passive — the sound is the frequency the timer supplies, PA8 = TIM1_CH1, a square at the note frequency, "1" without PWM is a single click. Middle — the connection: the pin only controls, the buzzer current goes through a transistor, an F103 pin gives at most 25 mA, a magnetic buzzer needs the suppression diode VD1 (section 51.2), a piezo has no coil; right — TIM1 for the note A4: PSC 2, ARR 54 544, CCR1 27 272, 72 MHz / 3 / 54 545 = 440.004 Hz, CCR1 is half the period, a rest is CCR1 = 0. Bottom — a melody without waiting: SysTick every 1 ms calls bz_tick, the steps E4 A4 B4 D5 C5 A4, a 200 ms step is the sound minus a 20 ms gap of silence, tone_set writes PSC, ARR and CCR1 into the buffers, a new note sounds from the nearest TIM1 update — a delay of at most 1.9 ms


13б.1 Active and passive: how to tell them apart

An active buzzer carries a generator inside: apply a level and it beeps at its own frequency, remove it and it goes quiet. A plain output is enough for the controller, like for an LED, but you cannot play a melody on it: there is only one frequency.

A passive buzzer is just a piezo disc or a coil: at a constant level it is silent, the sound comes only from an alternating signal of the right frequency. It has to be fed a PWM square from a timer — but then any note is possible.

To tell them apart on the bench: apply a constant voltage to the buzzer through a resistor. An active one beeps, a passive one only clicks at the moment of connection. Another sign — an active one often has a board with parts underneath and a "+" mark, a passive one is a bare disc.

The firmware of this section has both: a passive one on PA8 (TIM1_CH1), an active one on PB0 — a short beep at start-up, without waiting in code.


13б.2 Connection: a transistor and a diode

Better not to take the buzzer current from the pin: the buzzer goes through an NPN transistor, the pin drives the base through a resistor, and the buzzer sits between the supply plus and the collector. Then the pin gives only the base current, and the buzzer can run from 5 V, above the 3.3 V logic. An electromagnetic buzzer (a coil) needs a flyback diode in parallel: at switch-off the coil throws out a voltage spike, and without the diode it hits the transistor. A piezo transducer needs no diode.

PA8 is a 5 V tolerant pin (FT, DocID13587, p. 31 and 33), but tolerance only says the pin survives 5 V at its input. The pin current is limited to 25 mA (p. 37) — and that is the absolute maximum, not a working value. Resistor values and currents were not computed here — they depend on the buzzer and the transistor, take them from the documents on your parts.

The timer pin is an alternate-function push-pull output, nibble 0xB in CRH (RM0008, p. 171); TIM1 also needs the MOE bit (p. 358), otherwise the output stays silent while the counter runs (section 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 Notes: from A4 = 440 Hz to PSC and ARR

The frequency of the note with MIDI number n in equal temperament is 440 × 2^((n − 69) / 12) Hz: A4 is 69, C4 is 60, a semitone is a factor of 2^(1/12). The code holds a table of one octave (the eighth) in hundredths of a hertz, and below it halves the frequency for every octave, without floating point:

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 gives the note period in timer ticks: 72 MHz / f. For A4 that is 163 636 ticks — it does not fit into a 16-bit ARR, so bz_timing takes the smallest divider that makes it fit and rounds the period:

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;
}

The run for the notes of the tune and the first-octave C (TIM1 counts from 72 MHz):

Note Exact, Hz PSC ARR CCR Result, Hz Error, cent
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

The whole table from C0 (16.35 Hz) to B8 (7902.13 Hz) is 108 notes: the worst error is 0.0882 cent at B8, and CCR is exactly half the period everywhere. The divider grows towards low notes: C0 has PSC = 67, and from C#6 (1108.73 Hz) PSC = 0 is enough — at 72 MHz a 16-bit count without a divider gives no lower than 1098.6 Hz. A rest and numbers outside the table (0, 11, 120) give 0 ticks — silence, without division.

Try it in the app: Lab → bench "Buzzer".


13б.4 Accuracy: own divider versus a 1 MHz count

The timer is often set to 1 MHz (PSC = 71) and ARR = 1 000 000 / f is written. The step of such a count is 1 µs, and on high notes the period becomes short and the rounding noticeable:

Note ARR Result, Hz Error, cent
A4 2272 439.947 −0.21
C7 477 2092.050 −0.79
C8 238 4184.100 −0.79
B8 126 7874.016 −6.17

From C4 to B8 the worst error is 6.17 cents at B8. The smallest divider of 13б.3 keeps all 108 notes within 0.0882 cent. The example "The C major scale" in the lab rounds the frequency to whole hertz and writes ARR at a 1 MHz count: the worst note C4 is 262 Hz instead of 261.63, an error of +2.84 cents. The "1 kHz" tone of the example "A 1 kHz tone on TIM1" is not a note: the nearest is B5, and it is 21.3 cents above it.


13б.5 A melody without blocking

A melody is an array of steps (note, duration in ms), a rest is note 0. The player bz_tick is called once per 1 ms from the SysTick interrupt; it counts the step time itself and says when to change the note. At the end of each note there is a gap of silence, otherwise two equal notes in a row merge into one:

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;
}

In the firmware the interrupt drives the player, and main is free — it blinks an LED by the millisecond counter without stopping:

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;
}

New PSC, ARR and CCR are written into the buffers (ARPE and OCxPE, RM0008, p. 386) and take effect together at the nearest update — so the sound of a note starts not at once but, in the model, up to 1.902 ms after the write. The tune of 12 steps (the same as in the bench example "A melody from a note table") with a 20 ms gap plays in 3500 ms: 23 writes to the timer, 11 notes, the durations differ from the given ones (minus the gap) by no more than 2.29 ms. Without the gap there are 12 writes and 10 notes: two A4 of 150 ms in a row merged into one A4 of 300 ms.


13б.6 Volume and duty cycle

It is tempting to set the volume by the duty cycle: CCR below half — quieter. But a 50 % square is the strongest at the fundamental, and a smaller duty not only weakens it but also adds the second harmonic — the sound becomes thinner and shriller. The run from the Fourier series of the square:

Duty CCR (A4) Fundamental, rel. to 50 % Second harmonic
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

The fundamental of a square is proportional to sin(π·D): at 5 % there is 0.16 of the 50 % level left. The bench example "Error: a 5 % duty — quiet" — CCR = 50 at ARR = 999 — gives the same 0.16. Change an even volume by the buzzer supply or by the choice of the buzzer itself, and leave the duty at 50 %.


13б.7 Pitfalls

A rest in the frequency table — and the controller falls over. A tone in hertz without a check: 1000000 / hz at hz = 0. On a computer such a process is killed by a division exception (exit code 136). bz_ticks returns 0 for a rest before any division, and tone_set sets CCR1 = 0 — silence.

A note out of range is silent or plays the wrong pitch. A number below 12 or above 119 is not in the table and gives silence; without the check the octave shift would go out of bounds.

A forgotten prescaler. PSC = 0 instead of 71 at ARR = 2271: the timer counts at 72 MHz, and "440 Hz" turns into 31 690 Hz — the ear does not hear it (the bench example "Error: PSC = 0 — above 20 kHz").

Clicks and torn notes. Without preload a new ARR takes effect at once: if the counter has already gone further, it counts up to 65 535 and only then wraps — an extra 2.731 ms at PSC = 2 and 3.641 ms at PSC = 3. In the run of 10 loops of the tune without a gap: with preload not a single bent period, without it — 7 stretched and 24 shortened, the worst by 2.731 ms.

Two equal notes sound like one. No gap between notes — see 13б.5: A4 and A4 merged.

The melody stops the program. Notes through delays in main: while it plays, buttons and sensors are not polled. The player is in the SysTick interrupt, main is free.

An active buzzer on PWM. An active one has its own generator (in the bench model — 2.4 kHz): PWM at its input switches it on and off, and on the bench there is crackle instead of a clean tone. It needs a plain output, level "1".


13б.8 Summary

Full sources

buzzer13c.h — notes, timer timing, melody steps and the player.

/* 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 — note frequencies, PSC/ARR, the melody player.

/* 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 firmware: TIM1 on PA8, an active buzzer on PB0, the player in 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;
        }
    }
}

Next: section 13в "16-channel PWM PCA9685 over I2C: servos and LEDs without the microcontroller's timers" — sixteen PWM channels over two I2C wires, when there are not enough timers for all the servos and LEDs.

Open in the interactive handbook