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<CCRpreload register ( OCxPE,ARPE)предзагрузка: новое значение — со следующего периода update event, update interrupt событие обновления, прерывание по нему main output enable ( MOE)главный выключатель выходов TIM1 five-volt tolerant (FT) вывод с допуском 5 В open-drain открытый сток bit-banging ногодрыг: сигнал выводом общего назначения из программы
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 Ногодрыгом — и почему это хуже
Тот же сигнал можно сделать без таймера: вывод в единицу, задержка на ширину импульса, вывод в ноль, задержка до конца периода. Так сделан пример «Сервопривод ногодрыгом» в стенде лаборатории. Работает — но:
- ядро всё время занято задержками: 20 мс из 20 мс на один привод, а четыре привода с разными импульсами уже требуют сортировки фронтов;
- задержка по счёту тактов не знает о прерываниях. Прерывание, пришедшее во время импульса, удлиняет его на свою длину.
Модель: прерывание каждую 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 А в пике.
Отсюда правила, без чисел — числа даёт документ на привод и источник:
- Отдельный источник 5 В для приводов, а не вывод 3,3 В и не стабилизатор платы с контроллером. Рывок тока проседает по питанию, и если на той же линии сидит контроллер, просадка может его сбросить — привод дёрнулся, контроллер перезапустился, привод снова дёрнулся.
- Земля общая. Импульс — это напряжение сигнала относительно земли привода; без общего провода у привода нет опоры, и сигнал для него неопределённый. Землю приводов ведут к источнику толстым проводом, а к контроллеру — отдельным, чтобы ток двигателей не шёл через землю платы.
- Конденсатор у разъёма приводов сглаживает короткие рывки тока; ёмкость берут по документу на привод и источник.
- Не стартовать все сразу. Плавный ход из 13а.4 и пуск по очереди растягивают нагрузку на источник; в модели стенда пиковый ток уменьшился от пуска по очереди, а разгона, от которого зависит пусковой ток настоящего двигателя, в модели нет.
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 Итоги
- Сервопривод ждёт импульс раз в 20 мс (50 Гц); угол задаёт ширина: 1000 мкс — 0°, 1500 мкс — 90°, 2000 мкс — 180°; у «широких» — 500…2500 мкс, по документу на привод.
- TIM2 на F103: 72 МГц,
PSC= 71 — отсчёт 1 мкс,ARR= 19 999 — 20 мс;CCR= ширина импульса в мкс; шаг 1 мкс — 0,18°. - Четыре привода — четыре канала одного таймера; прерывание обновления
раз в 20 мс пишет все четыре
CCR, аOCxPEне даёт импульсу порваться. - Плавный ход — шаг
CCRраз в период: 10 мкс за период — 90° в секунду, 180° за 2 с, 100 периодов; скачок в модели — 298 мс. - Ногодрыг занимает ядро целиком, и прерывание 10 мкс растягивает импульс до 1510–1520 мкс — привод дрожит.
- Питание приводов — отдельный источник 5 В, земля общая, конденсатор у разъёма; 3,3 В на входе — по документу на привод.
Полные исходники
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б «Зуммер: активный и пассивный, ноты на таймере, мелодия без блокировки».