57ж. Микрошаг без драйвера: стрелочный шаговый двигатель прямо от таймера
В 57е разобран большой шаговый двигатель: обмотка 1,5 Ом, ток 1,5 А, шина 24 В. Подключи такую обмотку к шине напрямую — ток во много раз превысит допустимый, и она сгорит. Поэтому её питают через драйвер, который сам держит ток: включает обмотку, ждёт, пока ток дорастёт до нужного, выключает и снова включает — десятки тысяч раз в секунду. Такой драйвер называют чоппером (от англ. chop — рубить): так устроены A4988 и TMC2209.
Здесь другой край: стрелка спидометра или тахометра. Двигатель в ней крошечный, обмотка — сотни ом, ток — десятки миллиампер, и его ограничивает само сопротивление обмотки. Драйвер не нужен: на обмотку подают ШИМ прямо с выводов микроконтроллера, через простой буфер.
ST описывает это в AN3208 для STM8: двигатель Switec X25.689, по одному каналу ШИМ и одному выводу на обмотку, таблица скважностей на 24 микрошага. Здесь — откуда берётся эта таблица, что говорят паспорта двигателей той же геометрии (ток, индуктивность, частота пуска, упор), почему для отрицательного тока скважность переворачивается, где AN противоречит сама себе, как перенести всё на STM32F103 и не получить лишний импульс при смене знака, как вести стрелку с разгоном и находить ноль без датчика, как поставить четыре стрелки на один камень, что даёт STM8S003 с питанием 5 В и когда проще взять драйвер VID6608.
Опоры: ST AN3208 Rev 1 (Doc ID 17411, июль 2010) — разделы 1–4 (типы
двигателей, геометрия X25.689, таблица 1), раздел 5.2.2 (таблица 2,
уравнение 1, частоты и предел скорости). Паспорта двигателей: VID29-XX/XXP
Rev 1 (Wellgain, 2006) и X40 v1.1 (Juken Swiss Technology, март 2011),
каталог Juken vers. 4.1; драйвер VID66-08 (Wellgain, 2006). RM0008 Rev 17
(F1: режимы ШИМ, предзагрузка CCR, бит UDIS); DocID13587 Rev 17 (выводы и
предельные токи STM32F103x8/xB); DocID018576 Rev 8 (STM8S003F3/K3: выводы,
карта регистров, векторы, токи) и RM0016 Rev 14 (DocID14587: тактирование,
запись 16-битных регистров TIM1). Числа — прогон своего стенда
_verify/gauge57zh/sim57zh.c на компьютере и тем же исходником в QEMU на
Cortex-M3, независимо — ref57zh.py (в нём же модель обмотки как RL-цепи).
Прошивки fw57zh.c для F103 и fw57zh_stm8.c для STM8S003 собраны и
измерены, на плате не запускались.
Как это называется в документации
Оригинал Перевод gauge stepper, instrument cluster motor стрелочный шаговый двигатель приборной панели full step, partial step, microstep полный шаг, частичный шаг, микрошаг gear reduction ratio, pointer shaft передаточное число редуктора, ось стрелки variable reluctance (VR), permanent magnet (PM), hybrid реактивный, с постоянным магнитом, гибридный unipolar, bipolar winding однополярная (со средней точкой), двухполярная обмотка duty cycle скважность (доля периода во включённом состоянии) start-stop frequency частота, с которой двигатель трогается и встаёт без разгона internal stop, zero detection внутренний упор, отверстия для поиска нуля preload register, shadow register буферный и действующий регистр update event (UEV), UDISсобытие обновления, запрет обновления buffer / line driver (74HC244) буфер-формирователь high sink port вывод повышенной нагрузки mechanical stop, homing, return to zero механический упор, поиск нуля, возврат в ноль needle sweep проход стрелки по шкале при включении
57ж.1 Маленький шаговый: ток задаёт сама обмотка
Шаговые двигатели бывают трёх устройств (AN3208, раздел 2). Реактивный (VR) — зубчатый ротор из мягкого железа без магнита, момент небольшой. С постоянным магнитом (PM, «консервная банка») — дешёвый, шаг обычно 7,5…15°. Гибридный — зубчатый ротор с магнитом, шаг 3,6…0,9°; это двигатели станков и принтеров, у них 1,8° на шаг (57а.3). Обмотки — по разделу 1 AN: однополярные (со средней точкой, ток в каждой половине течёт в одну сторону) и двухполярные (без средней точки, ток меняет направление, нужен мост). Стрелочный X25.689 — двухполярный: четыре провода, две обмотки.
Почему его можно вести прямо от выводов. В 57е обмотке 1,5 Ом подают 24 В — без ограничения ток вырос бы до 16 А, поэтому драйвер режет его чоппером. У стрелочного двигателя AN называет ток порядка 20 мА. Паспорта X25.689 в проекте нет, но есть паспорта двигателей той же геометрии (57ж.2): у VID29 обмотка 260…300 Ом (типично 280) при 25 °C, питание 5…10 В. От 5 В это 17,9 мА. Ток ограничен самой обмоткой: можно подать полное напряжение, и ничего не сгорит. Значит, регулировать ток не нужно — достаточно регулировать среднее напряжение ШИМ-ом: обмотка усредняет его, и ток пропорционален скважности (57.5 — почему ток не следует за напряжением мгновенно, 57ж.3 — насколько). 57а.3 предупреждал, что простой ШИМ по напряжению теряет момент на скорости; для стрелки, которой не нужно ни момента, ни скорости станка, это не помеха.
57ж.2 Геометрия стрелочного двигателя: 12 микрошагов на градус
Внутри X25.689 маленький ротор с магнитом и редуктор 1/180 (AN3208, раздел 4). Полный шаг ротора — 180°, и он превращается в 1° стрелки. Полный шаг делится на 3 частичных (1/3° — «разрешение» из списка свойств в AN), каждый частичный — на 4 микрошага. Итого:
- 12 микрошагов на градус стрелки, один микрошаг — 1/12°;
- полный оборот ротора (360°, электрический оборот) — 24 микрошага, то есть 2° стрелки;
- ход стрелки 0…320° (как в AN) — 3840 микрошагов, или 160
электрических оборотов; AN считает этот ход переменной
Rotation_Noдо 160.
Та же геометрия у VID29 (паспорт, с. 2–3: редуктор 180:1, шаг 1°, 1/3° и 1/12°; ротор — 180°, 60° и 15°) и у X40 (с. 2: электрический оборот 2°, 1:180, сдвиг фаз 60°). Но у обоих внутренний упор — на 315°, поэтому наша шкала — 0…300°: 3600 микрошагов, 150 электрических оборотов (57ж.6).
Особенность двигателя: токи двух обмоток сдвинуты не на 90°, как в 57а.3, а
на 60°. Таблица строится так: на микрошаге k (0…23) фаза обмотки 0 —
60° + 15°·k, обмотки 1 — на 60° больше; ток — синус фазы. Модуль идёт в
скважность ШИМ, знак — в направление тока. Таблица у нас не набрана руками:
четверть синуса шагами по 15° — семь чисел в Q15
(32768·sin(15°·k): 0, 8481, 16384, 23170, 28378, 31651, 32768), остальное
считает код. Знак в нуле — дело соглашения: фаза в (0°, 180°] — «+», в
(180°, 360°] — «−», то есть в нуле обмотка остаётся на знаке полуволны,
которую закончила. Так же сделано в AN: из 25 строк её таблицы 2 знак
второго конца совпал со знаком по этому правилу во всех 25. Нумерация у AN
своя: шаги с 1, строка 25 повторяет строку 1, обмотки — 1 и 2. Наш шаг k —
её шаг k + 1, наши обмотки 0 и 1 — её 1 и 2.
/* Ток обмотки coil (0 или 1) на микрошаге idx (0…23): модуль в Q15 и знак.
* Фаза обмотки 0 — 60° + 15°·idx, обмотки 1 — на 60° больше. Знак «+» — фаза в (0°, 180°],
* «−» — в (180°, 360°]: в нуле обмотка остаётся на знаке той полуволны, которую закончила. */
int8_t gauge_level(uint8_t idx, uint8_t coil, uint16_t *mag)
{
uint8_t j = (uint8_t)((idx + 4u + 4u * coil) % GAUGE_USTEPS); /* фаза в шагах по 15° */
uint8_t q = j % 12u;
*mag = gauge_sin_q15[q <= 6u ? q : 12u - q];
return (j >= 1u && j <= 12u) ? 1 : -1;
}
Первые 12 микрошагов (вторая половина — то же с другим знаком), CCR — для F103 с периодом 1200 тактов (57ж.5) и полной амплитудой, как в AN:
| Шаг | Фаза 0 | Фаза 1 | Ток 0 (Q15) | Ток 1 (Q15) | Второй конец 0 / 1 | CCR 0 / 1 |
|---|---|---|---|---|---|---|
| 0 | 60° | 120° | 28378 | 28378 | 0 / 0 | 1039 / 1039 |
| 1 | 75° | 135° | 31651 | 23170 | 0 / 0 | 1159 / 849 |
| 2 | 90° | 150° | 32768 | 16384 | 0 / 0 | 1200 / 600 |
| 3 | 105° | 165° | 31651 | 8481 | 0 / 0 | 1159 / 311 |
| 4 | 120° | 180° | 28378 | 0 | 0 / 0 | 1039 / 0 |
| 5 | 135° | 195° | 23170 | −8481 | 0 / 1 | 849 / 889 |
| 6 | 150° | 210° | 16384 | −16384 | 0 / 1 | 600 / 600 |
| 7 | 165° | 225° | 8481 | −23170 | 0 / 1 | 311 / 351 |
| 8 | 180° | 240° | 0 | −28378 | 0 / 1 | 0 / 161 |
| 9 | 195° | 255° | −8481 | −31651 | 1 / 1 | 889 / 41 |
| 10 | 210° | 270° | −16384 | −32768 | 1 / 1 | 600 / 0 |
| 11 | 225° | 285° | −23170 | −31651 | 1 / 1 | 351 / 41 |
57ж.3 Паспорт двигателя: ток, индуктивность, ступени
Паспорта X25.689 в проекте нет. Есть паспорт VID29 (Wellgain) — двигателя той же геометрии, паспорт драйвера VID66-08 под него и паспорт X40 (Juken), двух стрелок на одной оси (57ж.9). По ним можно проверить то, что AN оставила общими словами.
Ток и холод. Обмотка VID29 — 260…300 Ом при 25 °C (паспорт, с. 3), а по паспорту драйвера VID66-08 (с. 5) — около 190 Ом при −40 °C и 340 Ом при 105 °C; у X40 — 230…290 Ом (с. 2). Рекомендуемый пик тока VID29 — от 12,9 до 16,07 мА (с. 7). При полной скважности от 5 В:
| Обмотка | От 5 В | От 5 В, амплитуда 3/4 | От 3,3 В |
|---|---|---|---|
| VID29, 260 Ом (25 °C, наименьшее) | 19,2 мА | 14,4 мА | 12,7 мА |
| VID29, 280 Ом (25 °C, типично) | 17,9 мА | 13,4 мА | 11,8 мА |
| VID29, 300 Ом (25 °C, наибольшее) | 16,7 мА | 12,5 мА | 11,0 мА |
| VID29, 190 Ом (−40 °C) | 26,3 мА | 19,7 мА | 17,4 мА |
| VID29, 340 Ом (105 °C) | 14,7 мА | 11,0 мА | 9,7 мА |
| X40, 230 Ом (наименьшее) | 21,7 мА | 16,3 мА | 14,3 мА |
От 5 В на холоде 26,3 мА — выше предела вывода в 20 мА и выше
рекомендованного пика. Поэтому прошивки ограничивают амплитуду: пик синуса —
3/4 периода (AMP = 900 из 1200 у F103, 100 из 134 у STM8), и gauge_coil
получил параметр amp. Для 20 мА на холоде хватило бы 0,76; с 0,75 на
−40 °C — 19,7 мА, при 25 °C — 12,5…14,4 мА, у нижней границы рекомендованного,
при 105 °C — 11,0 мА. Таблицы этого раздела, где сравнение идёт с AN, —
с полной амплитудой.
/* Обмотка между выводом ШИМ и вторым выводом: напряжение на ней — ШИМ минус второй вывод.
* При io = 0 доля «+» за период — CCR/period; при io = 1 доля «−» — (period − CCR)/period:
* скважность для отрицательного тока переворачивается. period = ARR + 1.
* amp — сколько тактов из period даёт пик синуса: amp < period ограничивает ток обмотки. */
void gauge_coil(uint8_t idx, uint8_t coil, uint16_t period, uint16_t amp, coil_t *o)
{
uint16_t mag;
int8_t sign = gauge_level(idx, coil, &mag);
uint16_t d = (uint16_t)(((uint32_t)mag * amp + 16384u) >> 15);
o->io = sign < 0;
o->ccr = o->io ? (uint16_t)(period - d) : d;
}
Индуктивность. Паспорт VID66-08 (с. 5) даёт индуктивность фазы
0,4 Гн. С 280 Ом постоянная времени τ = L/R = 1,429 мс: за период
ШИМ 60 кГц (16,667 мкс) ток почти не меняется — размах пилы при скважности
1/2 всего 52,1 мкА, 0,58 % тока. Так что скважность и правда задаёт ток.
Но установление — 3τ = 4,29 мс, а микрошаг на скорости короче. Модель
обмотки как RL-цепи под тем же ШИМ и той же таблицей (амплитуда 3/4, шаги —
накопителем, как в gauge_tick) на разной скорости стрелки:
| Скорость стрелки | Микрошаг | Микрошаг / τ | Первая гармоника тока | Отставание | RL первого порядка |
|---|---|---|---|---|---|
| 125°/с | 666,7 мкс | 0,467 | 0,871 | 29,1° | 0,872 / 29,3° |
| 200°/с | 416,7 мкс | 0,292 | 0,744 | 41,6° | 0,744 / 41,9° |
| 400°/с | 208,3 мкс | 0,146 | 0,486 | 60,3° | 0,487 / 60,9° |
| 600°/с | 138,9 мкс | 0,097 | 0,348 | 68,7° | 0,348 / 69,6° |
Первая гармоника тока падает и отстаёт ровно как у RL-фильтра первого
порядка 1/√(1 + (ωτ)²): на 600°/с микрошаг 138,9 мкс — 0,097 τ, ток
успевает дойти только до 0,348 заданного и отстаёт на 68,7°. Отношение
гармоник на 400 и 200°/с — 0,654; у паспортного динамического момента VID29
(с. 3: 0,85 и 1,2 мН·м типично) — 0,708: тенденция та же. Противо-ЭДС
ротора в модели нет.
Частота пуска и упор. Частота пуска-останова VID29 — 125 Гц полных шагов (с. 3), а полный шаг — 1° стрелки, то есть 125°/с, 1500 микрошагов/с; у X40 — 200°/с (с. 2). Быстрее — только с разгоном (до 600°/с у обоих). Наш разгон (57ж.6) трогается с 30 микрошагов/с (2,5°/с) и встаёт со скорости не выше 300 (25°/с) — ниже обеих. Ход с внутренним упором — 315° (VID29, с. 3; X40, с. 2). Паспорт VID29 (с. 8): шкала обычно меньше 300°, ноль задают упором при каждом включении, а разгон начинают с частоты пуска.
Ступени VID29 вместо синуса. Пример микрошага в паспорте VID29 (с. 7) —
не синус, а ступени равной высоты: шесть на четверть волны, 0; 3,33; 6,67;
10; 13,33; 16,67; 20 (пик в примере — 16,07 мА), правая обмотка сдвинута на
4 микрошага, на 60°. Если считать, что ротор встаёт по фазе синусной модели
AN (i0 = A·sin φ, i1 = A·sin(φ + 60°)), то:
| Таблица | Ошибка фазы, эл. град | То же на стрелке | Модуль A, от … до |
|---|---|---|---|
| ступени VID29 | 10,89 | 0,0605° | 0,6667…1,0184 |
| синус Q15 (наш) | 0,00 | 0,0000° | 1,0000…1,0000 |
Ступени сдвигают положение до 10,89 электрических градусов — 0,0605° стрелки при микрошаге 0,0833°, — а модуль «пляшет» на 34,5 %. Синус из семи чисел Q15 этого не делает.
Частичный шаг на трёх выводах, без ШИМ. Паспорт VID29 (с. 6–7) описывает самый простой режим: контакты 2 и 3 соединены, двигатель на трёх выводах логики, шесть состояний на электрический оборот, шаг 1/3°. Свою последовательность мы вывели из той же модели: напряжение левой обмотки — контакт 1 минус контакты 2+3, правой — контакт 4 минус они же:
/* Частичный шаг без ШИМ (паспорт VID29, с. 6–7): контакты 2 и 3 соединены, двигатель на трёх выводах.
* Бит 2 — контакт 1, бит 1 — контакты 2 и 3, бит 0 — контакт 4. Шесть состояний на электрический
* оборот, за шаг меняется один вывод; шаг — 60° ротора, 1/3° стрелки. */
const uint8_t gauge_partial[6] = { 0x1, 0x5, 0x4, 0x6, 0x2, 0x3 };
| Шаг | Контакт 1, 2+3, 4 | Левая / правая обмотка | Фаза | Модуль | Ток общего вывода |
|---|---|---|---|---|---|
| 0 | 0 0 1 | 0 / +1 | 0° | 1,1547 | 17,9 мА |
| 1 | 1 0 1 | +1 / +1 | 60° | 1,1547 | 35,7 мА |
| 2 | 1 0 0 | +1 / 0 | 120° | 1,1547 | 17,9 мА |
| 3 | 1 1 0 | 0 / −1 | 180° | 1,1547 | 17,9 мА |
| 4 | 0 1 0 | −1 / −1 | 240° | 1,1547 | 35,7 мА |
| 5 | 0 1 1 | −1 / 0 | 300° | 1,1547 | 17,9 мА |
Фаза идёт ровно через 60°, модуль постоянный — 1,1547, за шаг меняется один вывод. Но в двух состояниях из шести общий вывод 2+3 несёт ток обеих обмоток: при 280 Ом — 35,7 мА, вдвое больше, чем любой другой, и выше предела вывода 20 мА. Общий конец — на двух выводах параллельно или через буфер.
Стрелка и направление. Стрелка VID29 — до 80 мм и 10 г, дисбаланс до 0,025 мН·м (с. 6): 2,9 % типичного момента на 400 Гц и 0,7 % удерживающего 3,5 мН·м. Частота пуска дана для стрелки 0,2·10⁻⁶ кг·м² (с. 3). Ускорение оси до 1000 рад/с² (с. 4) — у нас 2500°/с², это 43,6 рад/с². Люфт — 0,5…1,0°. VID29-XX и VID29-XXP (с. 6, 8) отличаются зеркальной разводкой: одна и та же последовательность крутит их в разные стороны.
57ж.4 Обмотка = ШИМ + вывод: скважность переворачивается
Моста нет. Один конец обмотки — на канале ШИМ, другой — на обычном выводе (AN3208, раздел 4: каждая обмотка — одна линия ШИМ и одна линия ввода-вывода). Напряжение на обмотке — ШИМ минус вывод:
| Второй конец | ШИМ = 1 | ШИМ = 0 | Среднее за период |
|---|---|---|---|
| 0 | +V | 0 | +V·d |
| 1 | 0 | −V | −V·(1 − d) |
d — доля периода, когда выход ШИМ в единице, d = CCR/(ARR + 1). При
втором конце в единице отрицательное напряжение получается, пока ШИМ в
нуле. Поэтому для тока «−m» пишут CCR = (1 − m)·(ARR + 1), а не
m·(ARR + 1). Отсюда в таблице 2 AN «странное» значение: шаг 6 (наш 5), обмотка 2,
скважность 26 %, а в регистре 0x63 = 99 из 134 — это (1 − 0,26)·134 =
99,16. Для нуля при втором конце в единице нужен CCR = ARR + 1 (ШИМ всё
время в единице), а не 0 — у AN это 0x86 = 134. У F103 так тоже можно: по
RM0008 при CCR > ARR в режиме ШИМ 1 выход постоянно в единице. С amp
меньше периода (57ж.3) это не меняется: модуль считается от amp, а
переворот — от периода.
Сверка таблицы 2 AN3208. Таймер STM8 в AN — 8 МГц и ARR = 0x85,
период 134 такта. Каждый из 50 CCR таблицы 2 пересчитан по уравнению 1
тремя способами (ref57zh.py):
| Как считать | Совпало с AN |
|---|---|
| скважность из таблицы 1 (два знака), дробь отбросить | 50 из 50 |
| скважность из таблицы 1, округлить | 32 из 50 |
точный синус, округлить (наш gauge_coil) |
34 из 50 |
Значит, AN брала синус, округлённый до двух знаков, и отбрасывала дробь.
Пример: шаг 8 (наш 7), обмотка 2, 71 % при втором конце в единице:
(1 − 0,71)·134 = 38,86, в AN — 0x26 (38), округление дало бы 0x27, точный
синус 225° — тоже 0x27. Отбрасывание дроби никогда не увеличивает CCR, а для
отрицательного тока меньший CCR — больший модуль. Половины волны
получаются неравными: при 87 % «+» даёт 116/134 = 0,8657, «−» — 1 − 17/134 =
0,8731, разница до 0,75 процентного пункта. У gauge_coil CCR для «−» —
period − d, и половины равны точно.
Где AN противоречит сама себе. Данные сверены с PDF (gauge57zh_check.py):
- Пояснение к таблице 1 для шага 2 называет модуль обмотки 1 равным 0,71, а обмотки 2 — 0,97; в самой таблице 1 и в таблице 2 наоборот: 0,97 и 0,71. Верна таблица: фазы шага 2 — 75° и 135°, синусы 0,97 и 0,71.
- Раздел 5.2.2: наибольшая скорость — 7200 микрошагов/с, отсюда интервал не меньше 139 мкс (расчёт: 138,889). Тремя предложениями ниже — «нижний предел 300 мкс». Откуда 300, не сказано; по расчёту предел — 139.
- Там же период ШИМ назван 16,6 мкс, а шаг — 830 мкс (50 периодов). При
8 МГц и
ARR= 0x85 частота 59,701 кГц, период 16,750 мкс, 50 периодов — 837,5 мкс. «60 кГц» в AN — округление. - Свойства двигателя в разделе 4: «скорость больше 600°/с»; раздел 5.2.2: «наибольшая — 600 полных шагов в секунду», а полный шаг — 1° стрелки. Предел у AN то выше 600°/с, то ровно 600. Паспорт VID29 (с. 3) даёт ровно 600 Гц полных шагов.
- Ход 0…320° в AN: у VID29 и X40 упор на 315° (57ж.6).
Правило раздела то же, что с даташитами: где документ спорит сам с собой, верить расчёту. Таблицу не переписывают из AN, а считают из синуса и периода своего таймера.
57ж.5 На STM32F103: TIM2 на 60 кГц и ловушка предзагрузки
На BluePill таймеры тактируются от 72 МГц (у TIM2…TIM4 шина APB1 — 36 МГц,
но при делителе APB1, не равном 1, таймер получает удвоенную частоту).
60 кГц — это ARR = 1199, период 1200 тактов: частота 60000 Гц ровно,
период 16,667 мкс. Микрошаг через 50 периодов, как в AN, —
833,333 мкс, вся шкала 0…300° — 3000 мс. CCR = 1200 даёт 100 %.
Вариант «как в AN»: обмотки на TIM2 CH1 (PA0) и CH2 (PA1), вторые концы — выводы PB12 и PB13. Прерывание по обновлению TIM2 раз в период ШИМ отсчитывает микрошаги и пишет новые CCR.
Ловушка. Режим ШИМ требует предзагрузки CCR (OCxPE; RM0008: «нужно
включить буферный регистр»): записанный CCR вступает в силу только на
следующем событии обновления, а вывод в GPIOB->BSRR меняется сразу. Если
в одном прерывании записать и CCR, и вывод, то до конца текущего периода на
обмотке новый второй конец и старый CCR. Хуже всего — смена знака:
старый ноль записан как CCR = 0 при втором конце 0 (ШИМ всё время в нуле),
второй конец уходит в 1 — и обмотка получает −V почти весь период вместо
−26 %. Модель таймера в стенде (CCR в силу на обновлении, вывод сразу,
запись через 36 тактов после обновления — 0,5 мкс, допущение на вход в
прерывание, 7а) на одном электрическом обороте:
| Как менять знак | Смен знака | Периодов с ошибкой | Худшая ошибка за период |
|---|---|---|---|
| вывод сразу, в том же прерывании | 4 | 4 | 1164 такта из 1200 (97,0 %) |
| вывод в следующем прерывании | 4 | 4 | 36 тактов (3,0 %) |
| второй конец на канале того же таймера | 4 | 0 | 0 |
Ошибка — лишний импульс на каждой смене знака, четыре раза на каждые 24 микрошага, то есть на каждые 2° стрелки. Индуктивность (57ж.3) его сгладит, но не уберёт: это толчок в обмотке, который виден на осциллографе. Лечится двумя способами.
Вывод — в следующем прерывании. Следующее прерывание по обновлению и есть тот момент, когда новые CCR вступили. Остаётся только задержка входа: те самые 36 тактов в модели. Микрошаги идут не чаще чем раз в 8 периодов даже на 7200 микрошагах/с (57ж.6) — отложенная запись успевает всегда.
/* Микрошаг в TIM2 и выводы. CCR вступят на следующем обновлении, а запись в BSRR — сразу,
* поэтому выводы откладываем до следующего прерывания: оно и есть это обновление. */
static void gauge_out(uint32_t i, uint8_t now)
{
coil_t a, b;
uint8_t idx = gauge_index(&mv[i]);
gauge_coil(idx, 0, PERIOD, AMP, &a);
gauge_coil(idx, 1, PERIOD, AMP, &b);
TIM2->CCR1 = a.ccr;
TIM2->CCR2 = b.ccr;
io_next = (a.io ? GPIO_BSRR_BS12 : GPIO_BSRR_BR12) | (b.io ? GPIO_BSRR_BS13 : GPIO_BSRR_BR13);
if (now) {
GPIOB->BSRR = io_next;
io_next = 0;
}
}
void TIM2_IRQHandler(void)
{
TIM2->SR = (uint16_t)~TIM_SR_UIF;
if (io_next) { /* CCR из прошлого прерывания только что вступили — теперь выводы */
GPIOB->BSRR = io_next;
io_next = 0;
}
if (gauge_tick(&mv[0]))
gauge_out(0, 0);
idle = mv[0].pos == mv[0].target && mv[0].v == 0;
}
Второй конец — тоже канал таймера. Канал в режиме ШИМ с CCR = 0 — это постоянный ноль, с CCR = 1200 — постоянная единица, и он тоже предзагружен. Тогда скважность и направление вступают на одном обновлении, ошибки нет совсем (третья строка таблицы). Цена — четыре канала на стрелку вместо двух; у TIM2 их как раз четыре: PA0…PA3. Так сделана основная прошивка (57ж.7).
57ж.6 Стрелка: скорость, разгон и ноль без датчика
Предел скорости. AN3208 даёт 600 полных шагов в секунду, то есть 7200 микрошагов/с, интервал 138,889 мкс. Но здесь часы шагов — сам период ШИМ, 60 кГц: интервал бывает только целым числом периодов. Ближайший не короче предела — 9 периодов, 150 мкс, 6666,667 микрошага/с. Если задать ровно 7200 микрошагов/с через накопитель (ниже), интервалы пойдут по 8 и 9 периодов, и 8 периодов — 133,333 мкс, короче предела: в прогоне на ходу 0…300° таких интервалов 1331. Прошивка берёт с запасом 6000 микрошагов/с (500°/с) — ровно 10 периодов.
Разгон. В 57а.4 скорость задаётся интервалом таймера шагов: ARR
пересчитывают на каждом шаге. Здесь так нельзя — ARR занят под 60 кГц
ШИМ. Поэтому шаг делает накопитель фазы: каждый период ШИМ к нему
прибавляется скорость (микрошагов/с), и когда набралось 60000 — шаг.
Скорость пересчитывается раз в миллисекунду: не выше vmax и не выше той,
с которой ещё можно встать, v² ≤ 2a·путь, меняется не больше чем на
dv за миллисекунду. Если цель оказалась позади — та же формула тормозит
и разворачивает.
/* Раз в миллисекунду. Скорость к цели — не больше vmax и не больше той, с которой ещё успеем
* встать: v² <= 2a·путь. Меняется не больше чем на dv за миллисекунду — это и есть разгон,
* торможение и разворот, если цель оказалась позади. Путь — до 71582 микрошагов (2000·dv·путь в 32 битах при dv = 30). */
static void gauge_speed(gauge_move_t *m)
{
int32_t dist = m->target - m->pos;
uint32_t d = (uint32_t)(dist < 0 ? -dist : dist);
uint32_t ahead = ((uint32_t)(m->v < 0 ? -m->v : m->v) + 999u) / 1000u; /* пройдём за эту миллисекунду */
uint32_t lim = isqrt32(2000ul * m->dv * (d > ahead ? d - ahead : 0u)); /* 2000ul: 16-битный int переполнился бы уже при dv = 33 */
if (lim > m->vmax)
lim = m->vmax;
int32_t want = dist < 0 ? -(int32_t)lim : (int32_t)lim;
if (want > m->v + m->dv)
want = m->v + m->dv;
else if (want < m->v - m->dv)
want = m->v - m->dv;
m->v = want;
}
Ускорение в прошивке — 30000 микрошагов/с², то есть 2500°/с². Прогон
(dv = 30, vmax = 6000):
| Движение | Время в модели | Идеальная трапеция | Что видно |
|---|---|---|---|
| 0 → 300° | 796,567 мс | 800,0 мс | разгон 597 шагов (идеально 600 = 50° за 200 мс), без перелёта, встаёт на 114 микрошагах/с |
| 300° → 0 | 797,000 мс | 800,0 мс | то же в обратную сторону |
| к 300°, через 300 мс — к 50° | 897,000 мс | — | доезжает до 150° (1800 микрошагов), тормозит и возвращается |
У цели стрелка останавливается, если скорость не выше 300 микрошагов/с
(GAUGE_VSTOP), а не тормозит до нуля. Отсюда и выигрыш у трапеции:
800,0 − 796,567 = 3,433 мс — около того, сколько тормозить со 114
микрошагов/с до нуля, 114/30000 с = 3,8 мс. И трогается, и встаёт стрелка
медленнее частоты пуска-останова (57ж.3).
Ноль без датчика. Датчика положения у стрелки нет; счёт pos после
включения ничего не значит. Решение — механический упор у нуля: стрелку
ведут назад на весь механический ход 315° плюс запас, здесь 325° (3900
микрошагов, 10° запаса), и где бы она ни стояла, она дойдёт до упора и
простоит в нём остаток пути — ротор проскальзывает, стрелка нет. После этого
счёт обнуляют. Скорость поиска — 1500 микрошагов/с, 125°/с: частота
пуска-останова VID29, поиск занимает 2644,650 мс (трапеция — 2650,0).
В модели стрелка из 0°, 100° и 315° везде кончила в нуле, «потерянных» у
упора шагов — 3900, 2700 и 120.
/* Поиск нуля без датчика: считаем, что стрелка на 325°, и ведём её в 0. Где бы она ни была,
* она дойдёт до упора и простоит в нём остаток пути: ротор проскальзывает, стрелка — нет. */
void gauge_home(gauge_move_t *m)
{
m->pos = GAUGE_HOME;
m->target = 0;
m->v = 0;
m->acc = 0;
m->ms = 0;
}
Это работает только у двигателя с внутренним упором. По каталогу Juken (vers. 4.1, с. 1) X27.579 и X27.679 упора не имеют, X27.589 и X27.689 — имеют; у X10.504 и соседних (с. 2) есть отверстия для поиска нуля — это другой путь, с датчиком. X25.689 в каталоге нет. И шкала должна кончаться до упора: ход AN до 320° у двигателя с упором на 315° теряет в модели 60 микрошагов, и потом стрелка, отправленная на 150°, показывает 145° (−60 микрошагов) — до следующего поиска нуля.
Приборные панели делают поиск нуля при каждом включении (паспорт VID29,
с. 8): стрелки уходят к упору, потом проходят шкалу до конца и
возвращаются — водитель видит «проверку приборов», а прошивка тем временем
нашла ноль. В fw57zh.c то же: поиск нуля, проход до 300° и обратно.
57ж.7 Четыре стрелки на одном камне
В AN3208 STM8 ведёт четыре стрелки: восемь каналов ШИМ — четыре у TIM1, два у TIM2 (третий свободен) и два у TIM3 — плюс восемь выводов. На F103 сделано иначе: у каждой стрелки свой таймер и все четыре его канала (57ж.5, второй конец — канал), без обычных выводов:
| Стрелка | Таймер | CH1 | CH2 | CH3 | CH4 |
|---|---|---|---|---|---|
| 0 | TIM1 | PA8 | PA9 | PA10 | PA11 |
| 1 | TIM2 | PA0 | PA1 | PA2 | PA3 |
| 2 | TIM3 | PA6 | PA7 | PB0 | PB1 |
| 3 | TIM4 | PB6 | PB7 | PB8 | PB9 |
CH1 и CH3 — обмотка 0, CH2 и CH4 — обмотка 1. Выводы — по таблице 5
DocID13587 без переназначения. PA11 — это ещё и USB D−: с USB четвёртая
стрелка переезжает. У TIM1 выходы открывает бит MOE.
Прерывание одно — по обновлению TIM2, раз в период ШИМ, а пишет оно во
все четыре таймера. Счётчики запущены почти одновременно, но не связаны:
если обновление TIM3 придётся между записью его CCR1 и CCR4, одна обмотка
получит новое значение, другая — старое. Поэтому на время записи ставится
UDIS (RM0008: «событие обновления не формируется, действующие регистры
сохраняют значение»), и обновление, попавшее в запись, просто перенесёт всё
периодом позже.
/* Микрошаг в таймер стрелки i: все четыре CCR вступят на одном обновлении. Пока пишем, стоит UDIS:
* обновление посреди записи не перенесёт половину значений, а подождёт до следующего периода. */
static void gauge_out(uint32_t i, uint8_t now)
{
TIM_TypeDef *t = tim[i];
coil_t a, b;
uint8_t idx = gauge_index(&mv[i]);
(void)now;
gauge_coil(idx, 0, PERIOD, AMP, &a);
gauge_coil(idx, 1, PERIOD, AMP, &b);
t->CR1 |= TIM_CR1_UDIS;
t->CCR1 = a.ccr;
t->CCR3 = a.io ? PERIOD : 0u;
t->CCR2 = b.ccr;
t->CCR4 = b.io ? PERIOD : 0u;
t->CR1 &= (uint16_t)~TIM_CR1_UDIS;
}
void TIM2_IRQHandler(void)
{
uint8_t s = 0;
TIM2->SR = (uint16_t)~TIM_SR_UIF;
for (uint32_t i = 0; i < N; i++) {
if (gauge_tick(&mv[i]))
gauge_out(i, 0);
if (mv[i].pos == mv[i].target && mv[i].v == 0)
s |= (uint8_t)(1u << i);
}
idle = s;
}
На 72 МГц и 60 кГц на период приходится 1200 тактов; сколько из них
занимает прерывание с четырьмя стрелками — не мерилось. Сборка
arm-none-eabi-gcc 12.2.1 с -Os -Wall -Wextra -Werror и заголовком
CMSIS stm32f10x.h: fw57zh.o — 724 байта кода для четырёх стрелок и
528 для одной с выводами, gauge57zh.o — 408; образ целиком со
стартовым кодом — 1376 и 1180 байт. Из них gauge_tick — 228,
обработчик TIM2 — 76 и 88.
Ток. У F103 выводы на 3,3 В. По паспортным сопротивлениям (57ж.3)
обмотке достанется 11,8 мА при 280 Ом и 11,0 мА при 300 — ниже
рекомендованных 12,9 мА, и это ещё без просадки на выводах. А по
DocID13587 вывод держит ±8 мА с нормальными уровнями и ±20 мА с
«ослабленными»: при 20 мА единица может просесть до VDD − 1,3 В, ноль
подняться до 1,3 В. Предел на весь корпус — 150 мА, и он не мешает: четыре
стрелки на холоде (17,4 мА на обмотку) — 120,3 мА. Мешает напряжение.
Поэтому, как в AN, между выводами и обмотками буфер 74HC244 на 5 В: восемь
выходов — две стрелки на корпус. Через вывод питания корпуса пойдёт ток
обеих стрелок: при амплитуде 3/4 на холоде — до 34,2 мА на стрелку, 68,4 мА
на две. У 74HC244 обычно ±35 мА на выход и ±70 мА на вывод питания и земли —
это типичные цифры, не из даташита (в проекте его нет): прежде чем ставить
две стрелки на корпус, сверьте свой.
57ж.8 На STM8S003 от 5 В: без преобразователя уровня и регулятора
Почему 5 В. Стрелочные двигатели этого класса рассчитаны на 5 В и выше (VID29 — 5…10 В, X40 — 5…9 В). F103 на выводах даёт 3,3 В: либо буфер на 5 В и рядом регулятор 3,3 В для самого F103, либо двигатель на пониженном токе (57ж.7). STM8S003F3 питается от тех же 5 В, что и двигатель, — ни регулятора, ни преобразователя уровня. AN3208 в свойствах X25.689 так и пишет: двигатель можно вести прямо от микроконтроллера; паспорт VID29 (с. 1) — тоже.
Токи выводов. По даташиту STM8S003F3/K3 (DocID018576 Rev 8):
- предел вывода — 20 мА и на втекание, и на вытекание; ток в выводы питания VDD — не больше 100 мА, из выводов земли VSS — не больше 80 мА (с. 48);
- у выводов повышенной нагрузки (high sink, с. 69) при 5 В и 20 мА на четырёх выводах единица не ниже 3,3 В, ноль не выше 1,5 В — на обмотке гарантированно 1,8 В; при 10 мА на восьми — 4,0 и 0,8 В, то есть 3,2 В. У обычных выводов при 10 мА на восьми — 2,8 и 2 В (с. 68): 0,8 В;
- PC3 (
TIM1_CH3), PC4 (TIM1_CH4), PD2 и PD3 — выводы повышенной нагрузки (с. 27–28 для F3, с. 23–24 для K3), и такие выводы надо разносить по корпусу равномерно.
Сколько тянет одна стрелка: токи обмоток сдвинуты на 60°, и сумма их модулей наибольшая там, где обе по 0,866, — 1,7321 полного тока. С амплитудой 100 из 134 (0,7463) на холоде обмотка берёт 19,6 мА, стрелка — 34,0 мА: две — 68,0 мА, в 80 мА на VSS укладываются; три — 102,0 мА — уже нет (при 25 °C три уложились бы — 69,2 мА, но на холоде нет). Без амплитуды 3/4 обмотка на холоде взяла бы 26,3 мА — выше предела вывода.
Таймеры. «3 timers» в заголовке даташита (с. 1) — это TIM1 с четырьмя каналами, TIM2 с тремя и базовый TIM4; TIM3, который у STM8 в AN даёт ещё два канала, в даташите S003 не упоминается вовсе, в таблице векторов (с. 41) — только TIM1, TIM2 и TIM4. Стрелке нужны 2 канала ШИМ и 2 вывода: по каналам выходит 3 стрелки — две на TIM1, одна на TIM2, и 1 канал TIM2 остаётся. Четвёртой стрелке нужны ещё два канала — аппаратного ШИМ на неё нет, только программный. По току прямо от выводов — 2 стрелки; три — через буфер, но уже на тех же 5 В, без преобразования уровня.
Частота. Тактирование — встроенный RC-генератор HSI на 16 МГц (даташит,
с. 1; RM0016, с. 81). После сброса CLK_CKDIVR = 0x18 — делитель HSI 8 и
2 МГц (RM0016, с. 94); прошивка пишет 0. Счётчик TIM1 считает на
fCK_PSC/(PSCR + 1) (RM0016, с. 142). Варианты около 60 кГц:
| PSCR | Такт счётчика | ARR | Период | Частота | Отклонение от 60 кГц |
|---|---|---|---|---|---|
| 0 | 16 МГц | 265 | 266 | 60,150 кГц | +0,251 % |
| 0 | 16 МГц | 266 | 267 | 59,925 кГц | −0,125 % |
| 1 | 8 МГц | 132 | 133 | 60,150 кГц | +0,251 % |
| 1 | 8 МГц | 133 (0x85) | 134 | 59,701 кГц | −0,498 % |
Ближе всех к 60 кГц — PSCR = 0 и ARR = 266, и CCR у него вдвое точнее.
У AN — PSCR = 1 и 0x85, самый далёкий из четырёх: даже при 8 МГц ближе
ARR = 132. Прошивка всё же берёт настройку AN — тогда её CCR при полной
амплитуде сравниваются с таблицей 2 напрямую: это строки ccr134 стенда,
34 из 50 совпали, остальные 16 — отброшенная дробь AN (57ж.4). Движение
считает тик за 60 кГц, а он 59701,5 Гц: все скорости ниже на 0,5 % (6000 →
5970,1 микрошага/с, 10 периодов — 167,5 мкс).
Выравнивание: две ловушки. Первая — та же, что у F103: CCR предзагружен
(OCxPE в TIM1_CCMRx) и вступает на обновлении, а PD_ODR меняется сразу;
выводы пишем в следующем прерывании. Вторая — своя у STM8: CCR 16-битный, а
запись — по байту. RM0016 (§17.5.1, с. 167; для ARR — §17.3.2, с. 142, то же
для PSCR): старший байт пишут первым; запись старшего запирает перенос в
действующий регистр до записи младшего; инструкцию LDW не применять — она
пишет младший байт первым. Если младший записать первым, его запись снимет
запор, а следующая за ней запись старшего запрёт снова — и ближайшее
обновление перенесёт уже нечего. В модели по этому описанию при обратном
порядке из 48 значений CCR за оборот (две обмотки) не доходят до таймера 44
— и при периоде 134, и при 267; при прямом — 0. Прошивка пишет макросом
CCR_WRITE, и в листинге SDCC порядок виден: 0x5269 (TIM1_CCR3H) раньше
0x526A (TIM1_CCR3L), 0x526B раньше 0x526C, PD_ODR (0x500F) — после них;
PSCR и ARR в main — тоже старшим байтом вперёд, LDW в регистры TIM1 нет
ни одной.
/* 16-битный CCR у TIM1 STM8 пишут двумя байтами, старший первым: старший ждёт в буфере, запись
* младшего переносит оба. Обратный порядок даёт CCR со старым старшим байтом. */
#define CCR_WRITE(H, L, v) do { H = (uint8_t)((v) >> 8); L = (uint8_t)(v); } while (0)
static void gauge_out(uint8_t now)
{
coil_t a, b;
uint8_t idx = gauge_index(&mv);
gauge_coil(idx, 0, PERIOD, AMP, &a);
gauge_coil(idx, 1, PERIOD, AMP, &b);
CCR_WRITE(TIM1_CCR3H, TIM1_CCR3L, a.ccr); /* вступят на следующем обновлении */
CCR_WRITE(TIM1_CCR4H, TIM1_CCR4L, b.ccr);
io_next = (uint8_t)((a.io ? 0x04u : 0u) | (b.io ? 0x08u : 0u));
io_pending = 1;
if (now) {
PD_ODR = (uint8_t)((PD_ODR & (uint8_t)~IO_MASK) | io_next);
io_pending = 0;
}
}
void tim1_update_isr(void) __interrupt(TIM1_IRQ_UPD)
{
TIM1_SR1 = (uint8_t)~SR1_UIF;
if (io_pending) { /* CCR из прошлого прерывания только что вступили */
PD_ODR = (uint8_t)((PD_ODR & (uint8_t)~IO_MASK) | io_next);
io_pending = 0;
}
if (gauge_tick(&mv))
gauge_out(0);
idle = mv.pos == mv.target && mv.v == 0;
}
Общая логика под SDCC. gauge57zh.c собрался SDCC без правок по
смыслу, но с двумя исправлениями переносимости: у SDCC для STM8 int
16-битный. 1u << 30 в isqrt32 там не 2³⁰, а 2000u * dv переполнялся бы
уже при dv = 33. Теперь (uint32_t)1 << 30 и 2000ul.
Адреса регистров regs57zh_stm8.h — все 23 — сверены с картой регистров
даташита S003 (с. 31–37), вектор TIM1 — IRQ 11 (с. 41). Карту нельзя брать
у соседа по линейке: у STM8S105 (DocID14771) TIM2_IER по адресу 0x5301, у
S003 — 0x5303.
Сборка SDCC 4.2.0 с --std-c99 --opt-code-size: код — 2083 байта, во
флеш-памяти всего 2191 (с таблицей векторов, инициализацией и
константами), ОЗУ — 24 байта. Из кода 452 байта — 32-битная арифметика из
библиотеки SDCC (умножение, деление, остаток), gauge57zh.c — 1242 байта
против 408 у Cortex-M3, fw57zh_stm8.c — 389. Симулятора ucsim в образе
stm-sdcc:1 нет — прошивка не запускалась; CCR, которые она пишет, — это
gauge_coil с периодом 134, но исполнение кода SDCC не проверено.
57ж.9 Драйвер VID6608 и две стрелки на оси X40
Третий путь — драйвер. Кроме «прямо от выводов» и «через буфер», есть
микросхема-драйвер: VID66-08 (на схеме в паспорте — VID6608), КМОП в корпусе
SOP-16, ведёт две стрелки со сдвигом фаз 60° (с. 2–3). На стрелку — два
входа: f(scx), где каждый передний фронт — микрошаг 1/12° (с. 6), и
направление CW/CCW; вход RESET общий (с. 4). Таблицу, ШИМ и ток
микросхема делает сама. Что важно по паспорту:
- питание 4,5…5,5 В; ток типично 76 мА (5 В, 200°/с, 280 Ом), наибольший — 200 мА (5,5 В, −40 °C, 190 Ом), покоя — до 300 мкА (с. 4);
- входы: ноль до 1,35 В, единица от 3,15 В (с. 4);
- фронт не короче 450 нс, вход до 1,1 МГц, но двигателю — не больше 7,2 кГц (с. 5): это 600°/с, 7200 × 1/12°;
- выход — до ±35 мА (с. 5); на холоде при 5,5 В обмотка 190 Ом возьмёт 28,9 мА — в пределе;
- импульсы короче 20 нс отфильтрованы; по 100 нФ у выводов питания 8 и 16 и
22 мкФ рядом;
RESETдержать в нуле около 1 мс после подачи питания и подтянуть к земле резистором (с. 6).
Единица от 3,15 В — это про логику на 5 В. У F103 даташит гарантирует
единицу только как VDD − 0,4 В — 2,9 В при 3,3 В, ниже порога: нужен
преобразователь уровня. У STM8S003 выводы повышенной нагрузки дают не ниже
4,0 В даже при 10 мА (57ж.8). Два вывода на стрелку вместо четырёх и никакой
таблицы в прошивке — плата за это: ещё одна микросхема и шаги, которые
прошивка должна выдавать сама (тот же gauge_tick, только вместо CCR —
фронт на f(scx)).
Две стрелки на одной оси — X40. У X40 (Juken, v1.1) две независимые
соосные оси, каждая до 600°/с (с. 1) — две шкалы на одном месте (часы —
самый наглядный пример). По таблице на с. 2: момент 1,0…1,45 мН·м на 200°/с
и 5 В; пуск-останов — 200°/с, 600°/с — с разгоном; упор — 315°; удержание
3,5…4,0 мН·м; питание 5,0…9,0 В; обмотка 230…290 Ом; люфт каждой оси
0,5…1°; электрический оборот — 2° (1:180, сдвиг фаз 60°) — та же таблица и та
же прошивка. Обмоток четыре, ток из выводов — 2 × 1,7321 × I: с амплитудой
3/4 при 260 Ом — 50,0 мА, при 230 Ом — 56,5 мА. Для STM8S003 это ровно
случай «двух стрелок» (57ж.8): в 80 мА укладывается. Сопротивления X40 на
холоде паспорт не даёт.
57ж.10 Грабли
Отрицательная половина — не тот ток. Для второго конца в единице
CCR = m·(ARR + 1) вместо (1 − m)·(ARR + 1): где нужен модуль 0,26, на
обмотке 0,74. Стрелка дёргается на каждой отрицательной полуволне.
Ноль при втором конце в единице записан как CCR = 0. Это не ноль, а полное −V весь период. Ноль там — CCR = ARR + 1 (выход ШИМ постоянно в единице), у AN — 0x86, у F103 — 1200.
Полная скважность на холоде. Обмотка 190 Ом при −40 °C от 5 В — 26,3 мА, выше предела вывода 20 мА и рекомендованного пика 16,07 мА. Амплитуда 3/4 периода — 19,7 мА.
Лишний импульс на смене знака. CCR с предзагрузкой, вывод — сразу: 1164 такта из 1200 (97 %) на обмотке не то, что нужно, четыре раза за электрический оборот. Вывод — в следующем прерывании (остаётся 3 %) или второй конец на канале таймера (0).
Половина CCR записана в одном периоде, половина — в другом. Одно
прерывание пишет в несколько таймеров, а обновление чужого таймера пришлось
посреди записи. UDIS на время записи.
Скорость 7200 «по паспорту» — а интервалы короче предела. Часы шагов — период ШИМ: на 7200 микрошагах/с интервалы 8 и 9 периодов, 8 — это 133,333 мкс при пределе 138,889. Брать скорость, при которой интервал — целое число периодов (60000/n), с запасом: 6000 — ровно 10 периодов.
На скорости ток не успевает. τ = 1,429 мс, а микрошаг на 600°/с — 138,9 мкс: первая гармоника тока — 0,348 заданной, отставание 68,7°. Отсюда и меньший момент на скорости.
Шкала до 320° при упоре на 315°. 60 микрошагов уходят в упор, и стрелка врёт на 5° до следующего поиска нуля. Шкала — меньше упора.
Поиск нуля у двигателя без упора. X27.579 и X27.679 упора не имеют: стрелка проедет ноль. Нужен двигатель с упором или с отверстиями для поиска нуля (X10.504 и соседние).
Ноль ищут на полной скорости. Чем быстрее стрелка приходит к упору, тем сильнее отскакивает; в модели этого не видно (стрелка идеальная), поэтому поиск — на 125°/с, частоте пуска-останова VID29, и запас 10° сверх механического хода.
Таблицу переписали из AN. Отбрасывание дроби делает «−» сильнее «+» до 0,75 процентного пункта, а для другого таймера числа AN не годятся вовсе. Считать из синуса и своего периода.
Ступени VID29 вместо синуса. Модуль поля скачет на 34,5 %, фаза — до 0,0605° стрелки. Синус из таблицы Q15 ровнее.
Общий вывод частичного шага. Контакты 2+3 в двух состояниях из шести несут ток обеих обмоток — 35,7 мА при 280 Ом. Два вывода параллельно или буфер.
XX и XXP вращаются в разные стороны. Одна и та же последовательность у VID29-XX и VID29-XXP даёт противоположное направление: направление — в настройку, а не в код.
F103 прямо на обмотки. От 3,3 В — 11,8 мА при 280 Ом, ниже рекомендованных 12,9 мА, и это ещё без просадки на выводах. Нужен буфер на 5 В — или STM8S003.
Три стрелки прямо от STM8S003. На холоде 102,0 мА из VSS при пределе 80 мА. Прямо от выводов — две, больше — через буфер. И две стрелки — это восемь нагруженных выводов, а при 20 мА уровни гарантированы только для четырёх: при восьми гарантия есть лишь на 10 мА.
CCR у STM8 записан младшим байтом вперёд или LDW. Запись старшего байта запирает перенос, и CCR не доходит до таймера: 44 из 48 значений за оборот. Старший байт первым, по байту; компилятор не должен склеить запись в LDW — проверять по листингу.
Карта регистров от соседа. TIM2_IER у STM8S105 — 0x5301, у S003 —
0x5303. Заголовок — из даташита своего камня.
1u << 30 под SDCC. int для STM8 16-битный: сдвиг и 2000u * dv
считаются в 16 битах. Константы — (uint32_t)1, 2000ul.
Логика 3,3 В на входах VID6608. Порог единицы — 3,15 В, а F103 гарантирует 2,9 В. Преобразователь уровня или камень на 5 В.
57ж.11 Итоги
- Стрелочный шаговый (X25.689, VID29, X40) — обмотка 260…300 Ом, от 5 В 17,9 мА типично; чоппер не нужен, хватает ШИМ по напряжению от таймера.
- Редуктор 1/180: 12 микрошагов на градус, 24 на электрический оборот (2°). Упор на 315°, поэтому шкала — до 300°: 3600 микрошагов. Токи обмоток сдвинуты на 60°, таблица считается кодом из семи чисел Q15.
- Паспорт: рекомендуемый пик 12,9…16,07 мА, на холоде обмотка 190 Ом — амплитуда 3/4 периода. Индуктивность 0,4 Гн, τ = 1,429 мс: ШИМ 60 кГц сглажен до 0,58 %, но на 600°/с ток доходит только до 0,348 заданного.
- Обмотка между ШИМ и выводом: для отрицательного тока
CCR = (1 − m)·(ARR + 1). Все 50 CCR таблицы 2 AN3208 — уравнение 1 с отброшенной дробью; точный синус расходится с ними в 16 местах. - AN противоречит сама себе: шаг 2 в тексте и в таблице, 139 и 300 мкс, 16,6 мкс вместо 16,750, 830 мкс вместо 837,5, «больше 600°/с» и «не больше 600 шагов/с».
- На F103:
ARR= 1199, ровно 60 кГц. CCR с предзагрузкой и вывод сразу — лишний импульс 97 % периода на каждой смене знака; вывод в следующем прерывании — 3 %, второй конец на канале таймера — ноль. - Движение: накопитель фазы на 60 кГц, скорость раз в 1 мс по
v² ≤ 2a·путь, трогается и встаёт медленнее частоты пуска-останова; 0 → 300° за 796,567 мс без перелёта; ноль — назад на 325° к упору на 125°/с. - Четыре стрелки: TIM1…TIM4 по четыре канала,
UDISна время записи; 724 байта кода плюс 408 общей логики. F103 на 3,3 В двигатель недокармливает. - STM8S003 от 5 В: обмотки прямо от выводов, без регулятора и преобразователя уровня; по току — две стрелки (68,0 мА из 80 на холоде); по каналам TIM1 и TIM2 — три, TIM3 у S003 нет. CCR — старшим байтом первым, без LDW. SDCC: 2083 байта кода, 2191 во флеш-памяти.
- Третий путь — драйвер VID6608: два входа на стрелку, две стрелки на микросхему, не больше 7,2 кГц шагов, логика на 5 В. X40 — две стрелки на одной оси, четыре обмотки, 50,0 мА из выводов.
Полные исходники
gauge57zh.h — общая логика: определения, таблица микрошага, движение стрелки.
/* Раздел 57ж: стрелочный шаговый двигатель без драйвера — таблица микрошага и движение стрелки.
* Общая для прошивки (fw57zh.c) и стенда на компьютере и в QEMU (sim57zh.c): регистров здесь нет,
* только числа, которые прошивка пишет в CCRx таймера и в выводы. */
#ifndef GAUGE57ZH_H
#define GAUGE57ZH_H
#include <stdint.h>
#define GAUGE_USTEPS 24u /* микрошагов на электрический оборот (360° ротора = 2° стрелки) */
#define GAUGE_PER_DEG 12u /* микрошагов на градус стрелки */
#define GAUGE_TRAVEL (300 * 12) /* шкала 0…300°: внутренний упор у VID29 и X40 — на 315° */
#define GAUGE_HOME (325 * 12) /* к упору: весь механический ход 315° и ещё 10° */
#define GAUGE_TICK_HZ 60000u /* тик движения — период ШИМ, 60 кГц */
#define GAUGE_VSTOP 300 /* на цели останавливаемся, если скорость не выше, микрошагов/с */
/* Что записать в таймер для одной обмотки: CCR канала ШИМ и уровень второго конца обмотки. */
typedef struct {
uint16_t ccr;
uint8_t io; /* 0 — второй конец на земле, 1 — на питании */
} coil_t;
/* Движение стрелки. Скорость меняется раз в миллисекунду, шаг — по накопителю фазы. */
typedef struct {
int32_t pos; /* где стрелка по счёту прошивки, микрошаги */
int32_t target; /* куда её ведём */
int32_t v; /* скорость со знаком, микрошагов/с */
uint32_t acc; /* накопитель: шаг, когда наберётся GAUGE_TICK_HZ */
uint16_t vmax; /* предел скорости, микрошагов/с */
uint16_t dv; /* прирост скорости за 1 мс (ускорение / 1000) */
uint8_t ms; /* тиков до следующего пересчёта скорости */
} gauge_move_t;
extern const uint16_t gauge_sin_q15[7];
extern const uint8_t gauge_partial[6];
int8_t gauge_level(uint8_t idx, uint8_t coil, uint16_t *mag);
void gauge_coil(uint8_t idx, uint8_t coil, uint16_t period, uint16_t amp, coil_t *o);
uint8_t gauge_index(const gauge_move_t *m);
int8_t gauge_tick(gauge_move_t *m);
void gauge_home(gauge_move_t *m);
#endif
gauge57zh.c — таблица микрошага, частичный шаг и движение стрелки, общая для прошивок и стенда.
/* Раздел 57ж: таблица микрошага стрелочного двигателя и движение стрелки. */
#include "gauge57zh.h"
/* Четверть синуса шагами по 15°: 32768·sin(15°·k), k = 0…6. Электрический оборот — 24 таких шага. */
const uint16_t gauge_sin_q15[7] = { 0, 8481, 16384, 23170, 28378, 31651, 32768 };
/* Ток обмотки coil (0 или 1) на микрошаге idx (0…23): модуль в Q15 и знак.
* Фаза обмотки 0 — 60° + 15°·idx, обмотки 1 — на 60° больше. Знак «+» — фаза в (0°, 180°],
* «−» — в (180°, 360°]: в нуле обмотка остаётся на знаке той полуволны, которую закончила. */
int8_t gauge_level(uint8_t idx, uint8_t coil, uint16_t *mag)
{
uint8_t j = (uint8_t)((idx + 4u + 4u * coil) % GAUGE_USTEPS); /* фаза в шагах по 15° */
uint8_t q = j % 12u;
*mag = gauge_sin_q15[q <= 6u ? q : 12u - q];
return (j >= 1u && j <= 12u) ? 1 : -1;
}
/* Обмотка между выводом ШИМ и вторым выводом: напряжение на ней — ШИМ минус второй вывод.
* При io = 0 доля «+» за период — CCR/period; при io = 1 доля «−» — (period − CCR)/period:
* скважность для отрицательного тока переворачивается. period = ARR + 1.
* amp — сколько тактов из period даёт пик синуса: amp < period ограничивает ток обмотки. */
void gauge_coil(uint8_t idx, uint8_t coil, uint16_t period, uint16_t amp, coil_t *o)
{
uint16_t mag;
int8_t sign = gauge_level(idx, coil, &mag);
uint16_t d = (uint16_t)(((uint32_t)mag * amp + 16384u) >> 15);
o->io = sign < 0;
o->ccr = o->io ? (uint16_t)(period - d) : d;
}
/* Частичный шаг без ШИМ (паспорт VID29, с. 6–7): контакты 2 и 3 соединены, двигатель на трёх выводах.
* Бит 2 — контакт 1, бит 1 — контакты 2 и 3, бит 0 — контакт 4. Шесть состояний на электрический
* оборот, за шаг меняется один вывод; шаг — 60° ротора, 1/3° стрелки. */
const uint8_t gauge_partial[6] = { 0x1, 0x5, 0x4, 0x6, 0x2, 0x3 };
/* Микрошаг таблицы (0…23) по положению стрелки. */
uint8_t gauge_index(const gauge_move_t *m)
{
int32_t r = m->pos % (int32_t)GAUGE_USTEPS;
return (uint8_t)(r < 0 ? r + (int32_t)GAUGE_USTEPS : r);
}
/* Целый квадратный корень: наибольшее r, у которого r·r <= x. */
static uint32_t isqrt32(uint32_t x)
{
uint32_t r = 0, b = (uint32_t)1 << 30; /* не 1u << 30: у SDCC для STM8 int 16-битный */
while (b > x)
b >>= 2;
while (b) {
if (x >= r + b) {
x -= r + b;
r = (r >> 1) + b;
} else {
r >>= 1;
}
b >>= 2;
}
return r;
}
/* Раз в миллисекунду. Скорость к цели — не больше vmax и не больше той, с которой ещё успеем
* встать: v² <= 2a·путь. Меняется не больше чем на dv за миллисекунду — это и есть разгон,
* торможение и разворот, если цель оказалась позади. Путь — до 71582 микрошагов (2000·dv·путь в 32 битах при dv = 30). */
static void gauge_speed(gauge_move_t *m)
{
int32_t dist = m->target - m->pos;
uint32_t d = (uint32_t)(dist < 0 ? -dist : dist);
uint32_t ahead = ((uint32_t)(m->v < 0 ? -m->v : m->v) + 999u) / 1000u; /* пройдём за эту миллисекунду */
uint32_t lim = isqrt32(2000ul * m->dv * (d > ahead ? d - ahead : 0u)); /* 2000ul: 16-битный int переполнился бы уже при dv = 33 */
if (lim > m->vmax)
lim = m->vmax;
int32_t want = dist < 0 ? -(int32_t)lim : (int32_t)lim;
if (want > m->v + m->dv)
want = m->v + m->dv;
else if (want < m->v - m->dv)
want = m->v - m->dv;
m->v = want;
}
/* Раз в период ШИМ. Возвращает +1 или −1, если в этом тике микрошаг, иначе 0. */
int8_t gauge_tick(gauge_move_t *m)
{
if (m->ms == 0u) {
m->ms = (uint8_t)(GAUGE_TICK_HZ / 1000u);
gauge_speed(m);
}
m->ms--;
if (m->v == 0)
return 0;
m->acc += (uint32_t)(m->v < 0 ? -m->v : m->v);
if (m->acc < GAUGE_TICK_HZ)
return 0;
m->acc -= GAUGE_TICK_HZ;
int8_t d = m->v > 0 ? 1 : -1;
m->pos += d;
if (m->pos == m->target && m->v <= GAUGE_VSTOP && m->v >= -GAUGE_VSTOP) {
m->v = 0;
m->acc = 0;
}
return d;
}
/* Поиск нуля без датчика: считаем, что стрелка на 325°, и ведём её в 0. Где бы она ни была,
* она дойдёт до упора и простоит в нём остаток пути: ротор проскальзывает, стрелка — нет. */
void gauge_home(gauge_move_t *m)
{
m->pos = GAUGE_HOME;
m->target = 0;
m->v = 0;
m->acc = 0;
m->ms = 0;
}
fw57zh.c — прошивка для STM32F103: четыре стрелки на TIM1…TIM4 или одна с выводами (-DGAUGE_GPIO).
/* fw57zh.c — раздел 57ж: стрелочные шаговые двигатели без драйвера на STM32F103 (BluePill, 72 МГц).
* По умолчанию — четыре стрелки на TIM1…TIM4, у каждой свой таймер и все четыре его канала:
* CH1 — ШИМ обмотки 0, CH3 — её второй конец; CH2 — ШИМ обмотки 1, CH4 — её второй конец.
* Второй конец — тоже ШИМ, но с CCR = 0 или CCR = PERIOD (0 % или 100 %), и в силу он вступает
* на том же обновлении таймера, что и CCR канала ШИМ.
* С -DGAUGE_GPIO — одна стрелка, как в AN3208: TIM2 CH1 (PA0) и CH2 (PA1) — ШИМ, вторые концы —
* обычные выводы PB12 и PB13; их пишем на одно прерывание позже CCR.
* Выводы идут на обмотки через буфер (57ж.6). */
#include "stm32f10x.h"
#include "gauge57zh.h"
#define PERIOD 1200u /* 72 МГц / 60 кГц, ARR = 1199 */
#define AMP (PERIOD * 3u / 4u) /* пик синуса — 3/4 периода: ток обмотки не выше паспортного (57ж.3) */
#define V_HOME 1500u /* поиск нуля: 125°/с — частота пуска-останова VID29 */
#define V_RUN 6000u /* ход: 500°/с — ниже предела 7200 микрошагов/с из AN3208 */
#define DV 30u /* ускорение 30000 микрошагов/с² = 2500°/с² */
#if defined(GAUGE_GPIO)
#define N 1u
static TIM_TypeDef *const tim[N] = { TIM2 };
static uint32_t io_next; /* что записать в GPIOB->BSRR в следующем прерывании */
#else
#define N 4u
static TIM_TypeDef *const tim[N] = { TIM1, TIM2, TIM3, TIM4 };
#endif
static gauge_move_t mv[N];
static volatile uint8_t idle; /* бит i — стрелка i стоит на цели */
static void clock_72mhz(void)
{
RCC->CR |= RCC_CR_HSEON;
while (!(RCC->CR & RCC_CR_HSERDY))
;
FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2;
/* кварц 8 МГц × 9 = 72 МГц; APB1 = 36 МГц, таймеры на нём при делителе 2 — снова 72 */
RCC->CFGR = RCC_CFGR_PLLMULL9 | RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PPRE1_DIV2;
RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY))
;
RCC->CFGR |= RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL)
;
}
/* Режим вывода в CRL/CRH: 0xA — альтернативная функция, двухтактный, 2 МГц; 0x2 — обычный выход. */
static void pin_mode(GPIO_TypeDef *g, uint32_t pin, uint32_t mode)
{
volatile uint32_t *cr = pin < 8u ? &g->CRL : &g->CRH;
uint32_t sh = (pin & 7u) * 4u;
*cr = (*cr & ~(0xFu << sh)) | (mode << sh);
}
/* ШИМ 1 на всех четырёх каналах с предзагрузкой CCR: новое значение вступит на обновлении. */
static void tim_pwm(TIM_TypeDef *t, uint16_t ccer)
{
t->PSC = 0;
t->ARR = PERIOD - 1u;
t->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE
| TIM_CCMR1_OC2M_2 | TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2PE;
t->CCMR2 = TIM_CCMR2_OC3M_2 | TIM_CCMR2_OC3M_1 | TIM_CCMR2_OC3PE
| TIM_CCMR2_OC4M_2 | TIM_CCMR2_OC4M_1 | TIM_CCMR2_OC4PE;
t->CCER = ccer;
}
#if defined(GAUGE_GPIO)
/* Микрошаг в TIM2 и выводы. CCR вступят на следующем обновлении, а запись в BSRR — сразу,
* поэтому выводы откладываем до следующего прерывания: оно и есть это обновление. */
static void gauge_out(uint32_t i, uint8_t now)
{
coil_t a, b;
uint8_t idx = gauge_index(&mv[i]);
gauge_coil(idx, 0, PERIOD, AMP, &a);
gauge_coil(idx, 1, PERIOD, AMP, &b);
TIM2->CCR1 = a.ccr;
TIM2->CCR2 = b.ccr;
io_next = (a.io ? GPIO_BSRR_BS12 : GPIO_BSRR_BR12) | (b.io ? GPIO_BSRR_BS13 : GPIO_BSRR_BR13);
if (now) {
GPIOB->BSRR = io_next;
io_next = 0;
}
}
void TIM2_IRQHandler(void)
{
TIM2->SR = (uint16_t)~TIM_SR_UIF;
if (io_next) { /* CCR из прошлого прерывания только что вступили — теперь выводы */
GPIOB->BSRR = io_next;
io_next = 0;
}
if (gauge_tick(&mv[0]))
gauge_out(0, 0);
idle = mv[0].pos == mv[0].target && mv[0].v == 0;
}
static void pins_init(void)
{
pin_mode(GPIOA, 0, 0xAu);
pin_mode(GPIOA, 1, 0xAu);
pin_mode(GPIOB, 12, 0x2u);
pin_mode(GPIOB, 13, 0x2u);
}
#else
/* Микрошаг в таймер стрелки i: все четыре CCR вступят на одном обновлении. Пока пишем, стоит UDIS:
* обновление посреди записи не перенесёт половину значений, а подождёт до следующего периода. */
static void gauge_out(uint32_t i, uint8_t now)
{
TIM_TypeDef *t = tim[i];
coil_t a, b;
uint8_t idx = gauge_index(&mv[i]);
(void)now;
gauge_coil(idx, 0, PERIOD, AMP, &a);
gauge_coil(idx, 1, PERIOD, AMP, &b);
t->CR1 |= TIM_CR1_UDIS;
t->CCR1 = a.ccr;
t->CCR3 = a.io ? PERIOD : 0u;
t->CCR2 = b.ccr;
t->CCR4 = b.io ? PERIOD : 0u;
t->CR1 &= (uint16_t)~TIM_CR1_UDIS;
}
void TIM2_IRQHandler(void)
{
uint8_t s = 0;
TIM2->SR = (uint16_t)~TIM_SR_UIF;
for (uint32_t i = 0; i < N; i++) {
if (gauge_tick(&mv[i]))
gauge_out(i, 0);
if (mv[i].pos == mv[i].target && mv[i].v == 0)
s |= (uint8_t)(1u << i);
}
idle = s;
}
static void pins_init(void)
{
static const uint8_t pa[] = { 8, 9, 10, 11, 0, 1, 2, 3, 6, 7 }; /* TIM1 CH1…4, TIM2 CH1…4, TIM3 CH1, CH2 */
static const uint8_t pb[] = { 0, 1, 6, 7, 8, 9 }; /* TIM3 CH3, CH4, TIM4 CH1…4 */
for (uint32_t k = 0; k < sizeof pa; k++)
pin_mode(GPIOA, pa[k], 0xAu);
for (uint32_t k = 0; k < sizeof pb; k++)
pin_mode(GPIOB, pb[k], 0xAu);
}
#endif
static void wait_idle(void)
{
idle = 0; /* пересчитает следующее прерывание — уже с новыми целями */
while (idle != (1u << N) - 1u)
;
}
int main(void)
{
clock_72mhz();
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_TIM1EN;
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN | RCC_APB1ENR_TIM3EN | RCC_APB1ENR_TIM4EN;
pins_init();
for (uint32_t i = 0; i < N; i++) {
mv[i].vmax = V_HOME;
mv[i].dv = DV;
gauge_home(&mv[i]); /* стрелка неизвестно где: считаем, что на 325°, ведём в 0 */
#if defined(GAUGE_GPIO)
tim_pwm(tim[i], TIM_CCER_CC1E | TIM_CCER_CC2E);
#else
tim_pwm(tim[i], TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC3E | TIM_CCER_CC4E);
#endif
gauge_out(i, 1);
tim[i]->EGR = TIM_EGR_UG; /* перенести предзагруженное до пуска */
}
TIM1->BDTR = TIM_BDTR_MOE; /* у TIM1 выходы ещё и под общим разрешением */
TIM2->DIER = TIM_DIER_UIE;
NVIC_EnableIRQ(TIM2_IRQn);
for (uint32_t i = 0; i < N; i++)
tim[i]->CR1 |= TIM_CR1_CEN;
wait_idle(); /* все стрелки у упора, счёт с нуля */
for (uint32_t i = 0; i < N; i++) { /* проход до конца шкалы и обратно, как при включении приборной панели */
mv[i].vmax = V_RUN;
mv[i].target = GAUGE_TRAVEL;
}
wait_idle();
for (uint32_t i = 0; i < N; i++)
mv[i].target = 0;
wait_idle();
for (;;)
__WFI(); /* дальше цели пишет приложение: скорость, обороты, температура */
}
start57zh.c — стартовый код Cortex-M3 с таблицей векторов до TIM2.
/* start57zh.c — раздел 57ж: стартовый код Cortex-M3 для STM32F103 (и для стенда в QEMU netduino2).
* Таблица векторов до TIM2 (прерывание 28, адрес 0xB0 по RM0008), копия .data, обнуление .bss, main. */
#include <stdint.h>
extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;
int main(void);
void Default_Handler(void)
{
for (;;)
;
}
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void TIM2_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void Reset_Handler(void)
{
uint32_t *s = &_sidata, *d = &_sdata;
while (d < &_edata)
*d++ = *s++;
for (d = &_sbss; d < &_ebss;)
*d++ = 0;
main();
for (;;)
;
}
__attribute__((section(".isr_vector"), used)) void (*const vectors[16 + 29])(void) = {
[0] = (void (*)(void))&_estack,
[1] = Reset_Handler,
[2] = Default_Handler, /* NMI */
[3] = HardFault_Handler,
[4 ... 15] = Default_Handler,
[16 ... 43] = Default_Handler,
[16 + 28] = TIM2_IRQHandler,
};
f103_57zh.ld — сценарий компоновки для STM32F103C8.
/* f103_57zh.ld — раздел 57ж: STM32F103C8 (BluePill): 64 КБ флеш-памяти, 20 КБ ОЗУ.
* Тем же сценарием собран стенд для QEMU netduino2 (STM32F205: памяти там больше, хватает и этой). */
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH
.text : { *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); } > FLASH
.ARM.exidx : { *(.ARM.exidx*) } > FLASH
.data : { _sdata = .; *(.data .data.*) . = ALIGN(4); _edata = .; } > RAM AT > FLASH
_sidata = LOADADDR(.data);
.bss (NOLOAD) : { _sbss = .; *(.bss .bss.*) *(COMMON) . = ALIGN(8); _ebss = .; } > RAM
end = .;
}
regs57zh_stm8.h — регистры STM8S003 для прошивки на STM8.
/* regs57zh_stm8.h — раздел 57ж: регистры STM8S003 для fw57zh_stm8.c.
* Адреса — карта регистров даташита STM8S105 (DocID14771 Rev 9, таблицы карты регистров и
* таблица 11 векторов): у TIM1, портов C/D и CLK_CKDIVR в линейке STM8S они одни и те же.
* Даташита STM8S003 в проекте нет — адреса с ним не сверены. Биты — по RM0016 (его в проекте тоже нет). */
#ifndef REGS57ZH_STM8_H
#define REGS57ZH_STM8_H
#include <stdint.h>
#define R8(a) (*(volatile uint8_t *)(a))
#define CLK_CKDIVR R8(0x50C6) /* после сброса 0x18: HSI / 8 = 2 МГц; 0x00 — 16 МГц */
#define PC_ODR R8(0x500A)
#define PC_DDR R8(0x500C)
#define PC_CR1 R8(0x500D)
#define PD_ODR R8(0x500F)
#define PD_DDR R8(0x5011)
#define PD_CR1 R8(0x5012)
#define TIM1_CR1 R8(0x5250)
#define TIM1_IER R8(0x5254)
#define TIM1_SR1 R8(0x5255)
#define TIM1_EGR R8(0x5257)
#define TIM1_CCMR3 R8(0x525A)
#define TIM1_CCMR4 R8(0x525B)
#define TIM1_CCER2 R8(0x525D)
#define TIM1_PSCRH R8(0x5260)
#define TIM1_PSCRL R8(0x5261)
#define TIM1_ARRH R8(0x5262)
#define TIM1_ARRL R8(0x5263)
#define TIM1_CCR3H R8(0x5269)
#define TIM1_CCR3L R8(0x526A)
#define TIM1_CCR4H R8(0x526B)
#define TIM1_CCR4L R8(0x526C)
#define TIM1_BKR R8(0x526D)
#define TIM1_IRQ_UPD 11 /* TIM1 update/overflow/underflow/trigger/break, вектор 0x8034 */
#define CR1_CEN 0x01u
#define IER_UIE 0x01u
#define SR1_UIF 0x01u
#define EGR_UG 0x01u
#define CCMR_PWM1 0x60u /* OCxM = 110 в битах 6:4 */
#define CCMR_OCPE 0x08u /* OCxPE: предзагрузка CCRx */
#define CCER2_CC3E 0x01u
#define CCER2_CC4E 0x10u
#define BKR_MOE 0x80u
#endif
fw57zh_stm8.c — прошивка для STM8S003F3: одна стрелка на TIM1 от 5 В, компилятор SDCC.
/* fw57zh_stm8.c — раздел 57ж: стрелка без драйвера и без буфера на STM8S003F3 от 5 В, компилятор SDCC.
* Таймер как в AN3208: HSI 16 МГц, TIM1 с делителем 2 (PSCR = 1) — 8 МГц, ARR = 0x85 — период 134 такта.
* TIM1 CH3 (PC3) и CH4 (PC4) — ШИМ обмоток, PD2 и PD3 — их вторые концы; выводы пишем на одно
* прерывание позже CCR, как в варианте с выводами для F103 (57ж.4). Логика — тот же gauge57zh.c. */
#include <stdint.h>
#include "regs57zh_stm8.h"
#include "gauge57zh.h"
#define PERIOD 134u /* ARR + 1 = 0x85 + 1 */
#define AMP (PERIOD * 3u / 4u) /* 100 из 134: ток обмотки не выше паспортного и на холоде (57ж.3) */
#define IO_MASK 0x0Cu /* PD2 — второй конец обмотки 0, PD3 — обмотки 1 */
static gauge_move_t mv;
static uint8_t io_next; /* уровни PD2/PD3 для следующего прерывания */
static uint8_t io_pending;
static volatile uint8_t idle;
/* 16-битный CCR у TIM1 STM8 пишут двумя байтами, старший первым: старший ждёт в буфере, запись
* младшего переносит оба. Обратный порядок даёт CCR со старым старшим байтом. */
#define CCR_WRITE(H, L, v) do { H = (uint8_t)((v) >> 8); L = (uint8_t)(v); } while (0)
static void gauge_out(uint8_t now)
{
coil_t a, b;
uint8_t idx = gauge_index(&mv);
gauge_coil(idx, 0, PERIOD, AMP, &a);
gauge_coil(idx, 1, PERIOD, AMP, &b);
CCR_WRITE(TIM1_CCR3H, TIM1_CCR3L, a.ccr); /* вступят на следующем обновлении */
CCR_WRITE(TIM1_CCR4H, TIM1_CCR4L, b.ccr);
io_next = (uint8_t)((a.io ? 0x04u : 0u) | (b.io ? 0x08u : 0u));
io_pending = 1;
if (now) {
PD_ODR = (uint8_t)((PD_ODR & (uint8_t)~IO_MASK) | io_next);
io_pending = 0;
}
}
void tim1_update_isr(void) __interrupt(TIM1_IRQ_UPD)
{
TIM1_SR1 = (uint8_t)~SR1_UIF;
if (io_pending) { /* CCR из прошлого прерывания только что вступили */
PD_ODR = (uint8_t)((PD_ODR & (uint8_t)~IO_MASK) | io_next);
io_pending = 0;
}
if (gauge_tick(&mv))
gauge_out(0);
idle = mv.pos == mv.target && mv.v == 0;
}
static void wait_idle(void)
{
idle = 0;
while (!idle)
;
}
void main(void)
{
CLK_CKDIVR = 0x00; /* HSI 16 МГц без делителя */
PC_DDR |= 0x18u; /* PC3, PC4 — выходы каналов TIM1 */
PC_CR1 |= 0x18u;
PD_DDR |= IO_MASK; /* PD2, PD3 — двухтактные выходы */
PD_CR1 |= IO_MASK;
TIM1_PSCRH = 0;
TIM1_PSCRL = 1; /* 16 МГц / (1 + 1) = 8 МГц */
TIM1_ARRH = 0;
TIM1_ARRL = (uint8_t)(PERIOD - 1u); /* 0x85 */
TIM1_CCMR3 = CCMR_PWM1 | CCMR_OCPE;
TIM1_CCMR4 = CCMR_PWM1 | CCMR_OCPE;
TIM1_CCER2 = CCER2_CC3E | CCER2_CC4E;
mv.vmax = 1500u; /* поиск нуля: 125°/с */
mv.dv = 30u;
gauge_home(&mv);
gauge_out(1);
TIM1_EGR = EGR_UG; /* перенести предзагруженное до пуска */
TIM1_BKR = BKR_MOE;
TIM1_IER = IER_UIE;
TIM1_CR1 = CR1_CEN;
__asm__("rim");
wait_idle(); /* у упора, счёт с нуля */
mv.vmax = 6000u;
mv.target = GAUGE_TRAVEL; /* проход по шкале и обратно */
wait_idle();
mv.target = 0;
wait_idle();
for (;;)
__asm__("wfi");
}
Дальше: раздел 57з «Драйвер коллекторного мотора: L298N и TB6612FNG — H-мост, ШИМ на таймере, торможение, энкодер и ПИД скорости» — от стрелки на десятки миллиампер к мотору на амперы: готовый мост, энкодер на таймере и регулятор скорости.