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

13а. Сервопривод: ШИМ 50 Гц, импульс 1–2 мс, угол, плавное движение и питание

В разделе 13 таймер выдавал ШИМ для светодиода: частота повыше, чтобы глаз не видел мерцания, а смысл несёт заполнение. Сервопривод читает тот же ШИМ иначе: частота низкая и постоянная, а угол задаёт ширина импульса в микросекундах. Здесь — что привод ждёт на входе, как получить это на TIM2 STM32F103 так, чтобы CCR был прямо шириной импульса, четыре привода на одном таймере, плавный ход без задержек в main, почему ногодрыг хуже и как не уронить контроллер питанием приводов.

Опоры: RM0008 (DocID13902 Rev 17): частота таймеров на APB1 удваивается, с. 93; ШИМ, предзагрузка OCxPE и ARPE, бит UG, с. 386; выход активен, пока CNT < CCR, с. 387; бит MOE, с. 358; бит URS, с. 404; режимы выводов в CRL и CRH, с. 170 и 171. DocID13587 Rev 17 (STM32F103x8/xB): PA0…PA3 — TIM2_CH1…TIM2_CH4 без допуска 5 В, с. 29; PA8 — TIM1_CH1 с допуском 5 В, с. 31; FT — допуск 5 В, с. 33. Документов на сервоприводы в проекте нет: импульс 1–2 мс раз в 20 мс — общеизвестный, тот же, что в стенде лаборатории. Все числа ниже — прогон кода _verify/servo13b/servo13b.c на компьютере (gcc 12.2.0) со своей моделью TIM2 и привода на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3.

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

Оригинал Перевод
servo, RC servo сервопривод (рулевая машинка)
pulse width ширина импульса
neutral position среднее положение, 90°
dead band мёртвая зона: изменение импульса, на которое привод не отвечает
stall current ток заторможенного двигателя: вал в упоре
PWM mode 1 режим ШИМ 1: выход активен, пока CNT < CCR
preload register (OCxPE, ARPE) предзагрузка: новое значение — со следующего периода
update event, update interrupt событие обновления, прерывание по нему
main output enable (MOE) главный выключатель выходов TIM1
five-volt tolerant (FT) вывод с допуском 5 В
open-drain открытый сток
bit-banging ногодрыг: сигнал выводом общего назначения из программы

Вверху — сигнал на входе сервопривода: импульс в начале каждого периода 20 мс, ширина задаёт угол — 1000 мкс это 0°, 1500 мкс это 90°, 2000 мкс это 180°; TIM2 считает 72 МГц / (PSC 71 + 1) = 1 МГц, ARR 19 999 даёт период 20 мс, и CCR равен ширине импульса в микросекундах; у «широких» приводов ход 500…2500 мкс — по документу на свой привод. Посередине — подключение: STM32F103, выводы PA0…PA3 — каналы CH1…CH4 таймера TIM2, сигнал 3,3 В к четырём приводам; плюс 5 В приводам даёт отдельный источник, конденсатор у разъёма, земля общая у контроллера, приводов и источника. Внизу — CCR по периодам: при скачке CCR сразу 2000 и привод в модели доходит до 180° за 298 мс; при плавном ходе servo_move шаг 10 мкс за период, 90° в секунду, ход за 2 с, CCR меняет прерывание обновления


13а.1 Как сервопривод понимает импульс

Внутри обычного сервопривода — двигатель, редуктор, потенциометр на выходном валу и схема управления. Схема сравнивает ширину пришедшего импульса с положением потенциометра и крутит двигатель, пока они не сойдутся. Привод ждёт импульс примерно раз в 20 мс — это 50 Гц, — и ширина импульса задаёт угол: 1000 мкс — 0°, 1500 мкс — середина, 90°, 2000 мкс — 180°. Частота сама по себе угол не несёт: важна только ширина, период лишь должен быть «примерно 20 мс».

У многих приводов ход шире — 500…2500 мкс на те же 180°, а бывают и приводы на 270° или на непрерывное вращение. Какой ход у вашего привода, пишет его документ; в проекте документов на приводы нет, поэтому ниже везде два варианта: стандартный 1000…2000 мкс и «широкий» 500…2500 мкс, как в стенде лаборатории. Сами границы для конкретного привода не проверены.

Модель привода в проверке — та же, что в стенде: угол (импульс − 1000) / 1000 × 180, скорость 600° в секунду (0,1 с на 60°), мёртвая зона 1°. Модель встаёт в пределах мёртвой зоны: 1250 мкс — 45,6°, 1750 мкс — 134,4°. Если импульс уводит угол за край хода больше чем на 3°, вал упирается, и модель берёт ток заторможенного двигателя — 0,65 А против 0,01 А в покое. Это допущения модели стенда, а не данные привода.


13а.2 TIM2 на 50 Гц: CCR в микросекундах

Удобнее всего, чтобы один отсчёт таймера был одной микросекундой: тогда CCR — прямо ширина импульса. TIM2 висит на APB1 (36 МГц), и раз делитель APB1 не 1, частота таймера удвоена — 72 МГц (с. 93). PSC = 71 даёт 72 МГц / (71 + 1) = 1 МГц, ARR = 19 999 — 20 000 отсчётов, период 20 мс, 50 Гц. В режиме ШИМ 1 выход активен, пока CNT < CCR (с. 387): CCR = 1500 — импульс 1500 мкс.

static void servo_tim2_init(void)
{
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    GPIOA->CRL = (GPIOA->CRL & ~0xFFFFu) | 0xBBBBu;             /* PA0…PA3 — альтернативная функция, push-pull */
    TIM2->PSC = PSC_VAL;
    TIM2->ARR = ARR_VAL;
    TIM2->CCR1 = servo_tick(&sv[0]);
    TIM2->CCR2 = servo_tick(&sv[1]);
    TIM2->CCR3 = servo_tick(&sv[2]);
    TIM2->CCR4 = servo_tick(&sv[3]);
    TIM2->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE     /* ШИМ 1 с предзагрузкой */
                | TIM_CCMR1_OC2M_2 | TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2PE;
    TIM2->CCMR2 = TIM_CCMR2_OC3M_2 | TIM_CCMR2_OC3M_1 | TIM_CCMR2_OC3PE
                | TIM_CCMR2_OC4M_2 | TIM_CCMR2_OC4M_1 | TIM_CCMR2_OC4PE;
    TIM2->CCER = TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC3E | TIM_CCER_CC4E;
    TIM2->CR1 = TIM_CR1_ARPE | TIM_CR1_URS;
    TIM2->EGR = TIM_EGR_UG;                                     /* загрузить PSC, ARR и CCR до старта */
    TIM2->SR = 0;
    TIM2->DIER = TIM_DIER_UIE;
    NVIC_EnableIRQ(TIM2_IRQn);
    TIM2->CR1 |= TIM_CR1_CEN;
}

Предзагрузка OCxPE и ARPE (с. 386) нужна здесь не для красоты: новое CCR ждёт в теневом регистре до конца периода, и импульс никогда не рвётся пополам (раздел 13.4). Поэтому же перед стартом стоит UG — он переносит PSC, ARR и CCR в рабочие регистры. URS = 1 (с. 404) не даёт этому UG поднять флаг обновления и вызвать лишнее прерывание. Полубайт 0xB в CRL — альтернативная функция push-pull на 50 МГц (с. 170). Числа в прошивке проверяет сам компилятор: _Static_assert на PSC = 71, ARR = 19 999 и на то, что ARR влезает в 16 бит. С отсчётом 1 мкс самый длинный период — 65,536 мс, так что 20 мс помещаются с запасом.

Угол в CCR. Формула — линейная: импульс = min + (max − min) × угол / 180°. Угол в коде — в десятых долях градуса, с округлением и с ограничением 0…180°:

uint16_t servo_us(const servo_t *s, int32_t deg10)
{
    uint32_t span = (uint32_t)(s->max_us - s->min_us);
    return (uint16_t)(s->min_us + (span * (uint32_t)clamp_deg(deg10) + 900u) / 1800u);  /* с округлением */
}
Угол 0° 45° 90° 135° 180°
CCR, ход 1000…2000 мкс 1000 1250 1500 1750 2000
CCR, ход 500…2500 мкс 500 1000 1500 2000 2500

Угол 33,3° даёт 1185 и 870; −10° и 190° обрезаются до 1000 и 2000 (500 и 2500 у широкого) — за край хода код не выходит. Шаг CCR в 1 мкс — это 0,18° у стандартного привода и 0,09° у широкого; мёртвая зона модели 1° — больше пяти таких шагов, так что разрешения 1 мкс хватает с запасом.

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

В стенде привод один — на TIM1_CH1 (PA8), с теми же PSC = 71 и ARR = 19 999; у TIM1 дополнительно нужен бит MOE (13а.7). Там же плавный проход тем же шагом 10 мкс за период, ногодрыг и две ошибки из 13а.7: ШИМ 1 кГц и ход 0,5…2,5 мс на стандартном приводе.


13а.3 Четыре привода на одном таймере

У TIM2 четыре канала сравнения, а период у них общий — тот самый 20 мс, который нужен всем приводам. Значит, один таймер ведёт четыре привода: PA0…PA3 — TIM2_CH1…TIM2_CH4 (с. 29), у каждого свой CCR. Все импульсы начинаются одновременно, в начале периода, и кончаются каждый в своё время.

Прерывание обновления приходит раз в период и обновляет все четыре CCR сразу. Записанное значение из-за OCxPE действует со следующего периода:

void TIM2_IRQHandler(void)
{
    TIM2->SR = ~TIM_SR_UIF;
    TIM2->CCR1 = servo_tick(&sv[0]);                            /* OC1PE: новое значение — со следующего периода */
    TIM2->CCR2 = servo_tick(&sv[1]);
    TIM2->CCR3 = servo_tick(&sv[2]);
    TIM2->CCR4 = servo_tick(&sv[3]);
    periods++;
}

В прошивке четыре привода ходят между двумя позами: из (0°, 90°, 180°, 30°) в (180°, 45°, 0°, 120°) и обратно, каждый ход — servo_move(…, 2000). Прогон: CCR на старте 1000, 1500, 2000 и 1167, шаг за период — 10; 2,5; 10 и 5 мкс, и все четыре делают последний шаг в одном и том же, 100-м периоде — через 2 с — и встают на 2000, 1250, 1000 и 1667. Привод с ходом на 45° идёт вчетверо медленнее привода с ходом на 180°, зато приходят они вместе.

Больше четырёх приводов — второй таймер: у F103C8 и F103CB есть ещё TIM3 и TIM4 по четыре канала (раздел 13.1), а TIM1 — на APB2, тоже на 72 МГц.


13а.4 Плавное движение без задержек

Если сразу записать в CCR новый угол, привод рванёт к нему с полной скоростью. В модели скачок с 0° на 180° занял 298 мс. Для руки манипулятора или заслонки это рывок, и двигатель всё это время тянет ток поворота. Плавный ход — это CCR, который меняется маленькими шагами раз в период.

Шаги делает прерывание обновления: оно уже приходит раз в 20 мс, и main не нужно ждать ни одной задержки. Код хранит положение в 1/16 мкс, чтобы дробный шаг не терялся, а servo_move считает шаг так, чтобы дойти за заданное время, но не быстрее предела скорости:

void servo_move(servo_t *s, int32_t deg10, uint32_t time_ms)
{
    int32_t d;
    uint32_t n = time_ms / SERVO_PERIOD_MS;                     /* сколько периодов на ход */
    s->target = (int32_t)servo_us(s, deg10) * SERVO_Q;
    d = s->target - s->pos;
    if (d < 0)
        d = -d;
    if (n == 0u)
        n = 1u;
    s->step = (int32_t)(((uint32_t)d + n - 1u) / n);            /* с округлением вверх: успеть за n периодов */
    if (s->step > s->vmax)
        s->step = s->vmax;                                      /* быстрее предела — нельзя */
    if (s->step < 1)
        s->step = 1;
}

uint16_t servo_tick(servo_t *s)
{
    int32_t d = s->target - s->pos;
    if (d > s->step)
        d = s->step;
    else if (d < -s->step)
        d = -s->step;
    s->pos += d;
    return (uint16_t)((s->pos + SERVO_Q / 2) / SERVO_Q);       /* 1/16 мкс → мкс с округлением */
}

Прогон servo_move(&sv, 1800, 2000) из 0°: CCR идёт 1010, 1020, 1030, 1040… — 10 мкс за период, это 1,8° за 20 мс, 90° в секунду. Последний шаг — в 100-м периоде, через 2000 мс. Модель привода идёт следом: 42,6° через 0,5 с, 87,6° через 1 с, 132,6° через 1,5 с, 177,6° через 2 с, а в 1° от цели оказывается через 2003 мс. Отстаёт она не больше чем на 1,8° — на один шаг: её скорость, 600° в секунду, в 6,7 раза больше заданной.

Предел скорости. В прошивке он 90° в секунду, это 10 мкс за период. Тот же ход за 200 мс (servo_move(&sv, 1800, 200)) или просто servo_set всё равно занимает 100 периодов, 2000 мс: шаг упирается в предел. Так одна строчка защищает механику от команды «мгновенно».

Цель и шаг меняются в main, а читает их прерывание, поэтому в прошивке servo_move для четырёх приводов вызывается при запрещённых прерываниях — иначе прерывание могло бы взять новую цель со старым шагом. Тот же приём годится и для SysTick: шаг можно делать в его прерывании, если таймер привода занят чем-то другим, — важно лишь, чтобы шаг был раз в период ШИМ, а не чаще.


13а.5 Ногодрыгом — и почему это хуже

Тот же сигнал можно сделать без таймера: вывод в единицу, задержка на ширину импульса, вывод в ноль, задержка до конца периода. Так сделан пример «Сервопривод ногодрыгом» в стенде лаборатории. Работает — но:

Модель: прерывание каждую 1 мс длиной 10 мкс (длина — допущение модели, а не замер), ногодрыг держит 1500 мкс, прогон 5 с. Без прерываний — 250 импульсов, все по 1500 мкс, привод не шелохнулся. С прерываниями — 248 импульсов: 118 по 1510 мкс и 130 по 1520 мкс, в зависимости от того, сколько прерываний попало в импульс, а период вырос до 20,2…20,21 мс. 10 мкс — это 1,8°, больше мёртвой зоны: модель привода сдвигалась 101 раз за 5 с. Это и есть «дрожание». С таймером ширина импульса не зависит от программы вовсе: прерывание обновления может опоздать, но CCR уже лежит в теневом регистре.


13а.6 Питание: отдельные 5 В и общая земля

Двигатель привода берёт ток рывками: на старте поворота, под нагрузкой и особенно в упоре. Сколько именно, пишет документ на привод, — в проекте его нет, поэтому ниже токи только из модели стенда (0,25 А на поворот, 0,65 А в упоре — допущения). Даже по модели видно, как складываются четыре привода: скачок всех четырёх разом — 1 А, те же скачки по очереди, через 300 мс, — 0,28 А в пике.

Отсюда правила, без чисел — числа даёт документ на привод и источник:

3,3 В на сигнальном входе. Контроллер выдаёт импульс 3,3 В, а привод питается от 5 В. Примет ли его вход 3,3 В как единицу, пишет документ на привод; в проекте его нет, и это не проверялось. Если не примет — согласователь уровня или открытый сток с подтяжкой к 5 В. Второе можно только на выводе с допуском 5 В: PA0…PA3 его не имеют (с. 29), PA8 — имеет (с. 31). Для PA8 в прошивке есть вариант с открытым стоком — полубайт 0xF в CRH (с. 171):

void servo_tim1_init(uint16_t pulse_us, int open_drain)
{
    RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
    GPIOA->CRH = (GPIOA->CRH & ~0xFu) | (open_drain ? 0xFu : 0xBu);   /* PA8: AF открытый сток или push-pull */
    TIM1->PSC = PSC_VAL;                                        /* TIM1 на APB2 = 72 МГц */
    TIM1->ARR = ARR_VAL;
    TIM1->CCR1 = pulse_us;
    TIM1->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;
    TIM1->CCER = TIM_CCER_CC1E;
    TIM1->BDTR = TIM_BDTR_MOE;                                  /* без MOE выход TIM1 молчит */
    TIM1->CR1 = TIM_CR1_ARPE;
    TIM1->EGR = TIM_EGR_UG;
    TIM1->CR1 |= TIM_CR1_CEN;
}

Номинал подтяжки и фронт импульса с ней здесь не считались.


13а.7 Грабли

Привод дрожит и гудит, ШИМ 1 кГц. Таймер настроили, как для светодиода: ARR = 999, период 1 мс. Модель стенда держит угол при периоде от 5 до 40 мс, а при 1 мс и 41 мс — дрожит. Нужен ARR = 19 999 при счёте 1 МГц.

Забытый делитель. PSC = 0: счёт 72 МГц, CCR = 1500 — импульс 20,8 мкс, период 0,2778 мс. Модель дрожит, угол не держится.

Вал в упоре, привод греется. Ход 500…2500 мкс от «широкого» привода на стандартном: 500 и 2500 мкс уводят вал за край, и модель тянет 0,65 А заторможенного двигателя. Брать ход из документа на свой привод и ограничивать угол в коде, как servo_us.

Импульс дёргается, хотя код «правильный». Ногодрыг: прерывание длиной 10 мкс растягивает импульс на 1,8° — в модели 101 сдвиг за 5 с. Только таймер.

Таймер считает, а на выводе тишина. TIM1: нет MOE в BDTR (с. 358) — у TIM2 такого бита нет, и код, перенесённый с TIM2 на TIM1, молчит (раздел 13.5). Или вывод не переведён в альтернативную функцию: полубайт 0xB, а не 0x2.

Импульс рвётся при смене угла. Нет OCxPE: новое CCR, записанное посреди импульса, действует сразу, и один импульс выходит не той ширины.

Контроллер перезагружается при движении приводов. Приводы на одном питании с контроллером. Отдельный источник, общая земля, конденсатор у разъёма.

Привод не реагирует на 3,3 В. Вход привода ждёт уровня от 5 В. Согласователь или открытый сток с подтяжкой к 5 В — только на выводе FT, как PA8.


13а.8 Итоги

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

servo13b.h — константы периода, состояние привода, функции.

/* servo13b.h — раздел 13а: сервопривод без регистров — угол в импульс и плавное движение.
 * Счёт таймера — 1 МГц, поэтому CCR равен ширине импульса в микросекундах.
 * В прошивке fw13b.c servo_tick() зовёт прерывание обновления TIM2 раз в период ШИМ (20 мс),
 * в стенде host13b.c — модель model13b.py; код один и тот же. */
#ifndef SERVO13B_H
#define SERVO13B_H
#include <stdint.h>

#define SERVO_PERIOD_US 20000u          /* 50 Гц: период ШИМ 20 мс */
#define SERVO_PERIOD_MS 20u
#define SERVO_Q         16              /* положение хранится в 1/16 мкс: дробный шаг не теряется */

typedef struct {
    uint16_t min_us, max_us;            /* импульс для 0° и для 180° */
    int32_t pos;                        /* текущий импульс, 1/16 мкс */
    int32_t target;                     /* куда идём, 1/16 мкс */
    int32_t step;                       /* шаг за период, 1/16 мкс */
    int32_t vmax;                       /* предел скорости: шаг за период, 1/16 мкс */
} servo_t;

/* min_us/max_us — ход привода по его документу; vmax — предел, градусов в секунду; deg10 — угол в 0,1°. */
void servo_init(servo_t *s, uint16_t min_us, uint16_t max_us, uint16_t vmax_dps, int32_t deg10);
uint16_t servo_us(const servo_t *s, int32_t deg10);              /* угол → импульс, мкс (0…180°, с ограничением) */
void servo_set(servo_t *s, int32_t deg10);                       /* идти к углу с предельной скоростью */
void servo_move(servo_t *s, int32_t deg10, uint32_t time_ms);    /* дойти за time_ms, но не быстрее vmax */
uint16_t servo_tick(servo_t *s);                                 /* раз в период: следующий CCR, мкс */
int servo_busy(const servo_t *s);

#endif

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

/* servo13b.c — раздел 13а: угол в импульс и плавное движение сервопривода (без регистров). */
#include "servo13b.h"

static int32_t clamp_deg(int32_t deg10)
{
    return deg10 < 0 ? 0 : (deg10 > 1800 ? 1800 : deg10);    /* за край хода не выходим: там упор */
}

uint16_t servo_us(const servo_t *s, int32_t deg10)
{
    uint32_t span = (uint32_t)(s->max_us - s->min_us);
    return (uint16_t)(s->min_us + (span * (uint32_t)clamp_deg(deg10) + 900u) / 1800u);  /* с округлением */
}

void servo_init(servo_t *s, uint16_t min_us, uint16_t max_us, uint16_t vmax_dps, int32_t deg10)
{
    s->min_us = min_us;
    s->max_us = max_us;
    /* градусы в секунду → 1/16 мкс за период: span × 16 × v × 20 мс / (180° × 1000 мс) */
    s->vmax = (int32_t)(((uint32_t)(max_us - min_us) * SERVO_Q * vmax_dps * SERVO_PERIOD_MS + 90000u) / 180000u);
    if (s->vmax < 1)
        s->vmax = 1;
    s->pos = s->target = (int32_t)servo_us(s, deg10) * SERVO_Q;
    s->step = s->vmax;
}

void servo_set(servo_t *s, int32_t deg10)
{
    s->target = (int32_t)servo_us(s, deg10) * SERVO_Q;
    s->step = s->vmax;
}

void servo_move(servo_t *s, int32_t deg10, uint32_t time_ms)
{
    int32_t d;
    uint32_t n = time_ms / SERVO_PERIOD_MS;                     /* сколько периодов на ход */
    s->target = (int32_t)servo_us(s, deg10) * SERVO_Q;
    d = s->target - s->pos;
    if (d < 0)
        d = -d;
    if (n == 0u)
        n = 1u;
    s->step = (int32_t)(((uint32_t)d + n - 1u) / n);            /* с округлением вверх: успеть за n периодов */
    if (s->step > s->vmax)
        s->step = s->vmax;                                      /* быстрее предела — нельзя */
    if (s->step < 1)
        s->step = 1;
}

uint16_t servo_tick(servo_t *s)
{
    int32_t d = s->target - s->pos;
    if (d > s->step)
        d = s->step;
    else if (d < -s->step)
        d = -s->step;
    s->pos += d;
    return (uint16_t)((s->pos + SERVO_Q / 2) / SERVO_Q);       /* 1/16 мкс → мкс с округлением */
}

int servo_busy(const servo_t *s)
{
    return s->pos != s->target;
}

fw13b.c — прошивка STM32F103: четыре привода на TIM2 (PA0…PA3), один на TIM1 (PA8).

/* fw13b.c — раздел 13а: четыре сервопривода на TIM2 STM32F103 (BluePill), 72 МГц.
 * TIM2_CH1…CH4 на PA0…PA3, счёт 1 МГц, период 20 мс (50 Гц), CCR — ширина импульса в мкс.
 * Прерывание обновления раз в 20 мс двигает все четыре привода на шаг (servo_tick) — без задержек в main.
 * Один привод на TIM1_CH1 (PA8, вывод с допуском 5 В) — servo_tim1_init: у TIM1 нужен бит MOE.
 * Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется); APB1 = 36 МГц, таймеры на нём — 72 МГц. */
#include "stm32f10x.h"
#include "servo13b.h"

#define TIM_HZ   72000000u
#define TICK_HZ  1000000u                                       /* счёт 1 МГц: 1 мкс на отсчёт */
#define PSC_VAL  (TIM_HZ / TICK_HZ - 1u)
#define ARR_VAL  (SERVO_PERIOD_US - 1u)

_Static_assert(PSC_VAL == 71u, "72 МГц / (71 + 1) = 1 МГц");
_Static_assert(ARR_VAL == 19999u, "20 000 отсчётов по 1 мкс = 20 мс");
_Static_assert(ARR_VAL <= 0xFFFFu, "ARR у TIM2 на F103 — 16 бит");

static servo_t sv[4];
static volatile uint32_t periods;                               /* счётчик периодов: часы программы */

void TIM2_IRQHandler(void)
{
    TIM2->SR = ~TIM_SR_UIF;
    TIM2->CCR1 = servo_tick(&sv[0]);                            /* OC1PE: новое значение — со следующего периода */
    TIM2->CCR2 = servo_tick(&sv[1]);
    TIM2->CCR3 = servo_tick(&sv[2]);
    TIM2->CCR4 = servo_tick(&sv[3]);
    periods++;
}

static void servo_tim2_init(void)
{
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    GPIOA->CRL = (GPIOA->CRL & ~0xFFFFu) | 0xBBBBu;             /* PA0…PA3 — альтернативная функция, push-pull */
    TIM2->PSC = PSC_VAL;
    TIM2->ARR = ARR_VAL;
    TIM2->CCR1 = servo_tick(&sv[0]);
    TIM2->CCR2 = servo_tick(&sv[1]);
    TIM2->CCR3 = servo_tick(&sv[2]);
    TIM2->CCR4 = servo_tick(&sv[3]);
    TIM2->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE     /* ШИМ 1 с предзагрузкой */
                | TIM_CCMR1_OC2M_2 | TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2PE;
    TIM2->CCMR2 = TIM_CCMR2_OC3M_2 | TIM_CCMR2_OC3M_1 | TIM_CCMR2_OC3PE
                | TIM_CCMR2_OC4M_2 | TIM_CCMR2_OC4M_1 | TIM_CCMR2_OC4PE;
    TIM2->CCER = TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC3E | TIM_CCER_CC4E;
    TIM2->CR1 = TIM_CR1_ARPE | TIM_CR1_URS;
    TIM2->EGR = TIM_EGR_UG;                                     /* загрузить PSC, ARR и CCR до старта */
    TIM2->SR = 0;
    TIM2->DIER = TIM_DIER_UIE;
    NVIC_EnableIRQ(TIM2_IRQn);
    TIM2->CR1 |= TIM_CR1_CEN;
}

/* Один привод на PA8: вывод допускает 5 В, поэтому можно открытый сток с подтяжкой к питанию привода. */
void servo_tim1_init(uint16_t pulse_us, int open_drain)
{
    RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
    GPIOA->CRH = (GPIOA->CRH & ~0xFu) | (open_drain ? 0xFu : 0xBu);   /* PA8: AF открытый сток или push-pull */
    TIM1->PSC = PSC_VAL;                                        /* TIM1 на APB2 = 72 МГц */
    TIM1->ARR = ARR_VAL;
    TIM1->CCR1 = pulse_us;
    TIM1->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;
    TIM1->CCER = TIM_CCER_CC1E;
    TIM1->BDTR = TIM_BDTR_MOE;                                  /* без MOE выход TIM1 молчит */
    TIM1->CR1 = TIM_CR1_ARPE;
    TIM1->EGR = TIM_EGR_UG;
    TIM1->CR1 |= TIM_CR1_CEN;
}

static uint32_t now_periods(void)
{
    return periods;
}

int main(void)
{
    static const int16_t pose[2][4] = {{0, 900, 1800, 300}, {1800, 450, 0, 1200}};   /* углы в 0,1° */
    uint32_t t0;
    int k = 0;
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    for (int i = 0; i < 4; i++)
        servo_init(&sv[i], 1000u, 2000u, 90u, pose[0][i]);     /* ход 1…2 мс, не быстрее 90°/с */
    servo_tim2_init();
    t0 = now_periods();
    for (;;) {
        __WFI();                                                /* спим до прерывания: ядро свободно */
        if (now_periods() - t0 >= 150u) {                       /* каждые 3 с — новая поза */
            t0 = now_periods();
            k ^= 1;
            __disable_irq();                                    /* цель и шаг меняются вместе */
            for (int i = 0; i < 4; i++)
                servo_move(&sv[i], pose[k][i], 2000u);          /* все четыре приходят разом, за 2 с */
            __enable_irq();
        }
    }
}

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

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