197. Мини-проект: частотомер на таймере
Второй прибор — частотомер: на вход PA0 подаётся прямоугольный сигнал, раз в полсекунды в консоль выводится частота. Задача простая на вид, но в ней две классические ловушки: какой способ измерения выбрать для какой частоты и как не ошибиться на 65 536 тактов, когда 16-битный таймер переполняется в тот же миг, когда пришёл фронт.
Как и термостат (раздел 196), прибор разделён на логику (freq.c) и
платформу (plat_f103.c); логика прогнана на компьютере для частот
от 1 Гц до 10 МГц, прошивка собрана для BluePill. Файлы — _verify/proj197/,
сверяет _verify/proj197_check.py.
Как это называется в документации
Оригинал Перевод input capture захват: таймер запоминает свой счёт по фронту входа gated counting счёт импульсов за время «ворот» reciprocal counting измерение через период с усреднением update event (overflow), UIF переполнение таймера и его флаг quantization error, ±1 count погрешность квантования, «±1 отсчёт» external clock mode, ETR внешнее тактирование таймера от входа
197.1 Три способа измерить частоту
| Способ | Как | Погрешность (±1 отсчёт) | Хорош для |
|---|---|---|---|
| по одному периоду | захват двух соседних фронтов; f = fтакт / (t₁ − t₀) | ≈ f / fтакт | низких частот |
| счётом | импульсы за 1 с | ≈ 1 / (f · 1 с) | высоких частот |
| усреднение периодов | первый и последний фронт внутри секунды, n периодов между ними | ≈ 1 / (fтакт · 1 с) | всех — если успевать считать периоды |
У первых двух погрешности идут навстречу: одна растёт с частотой, другая падает. Они равны, когда f / fтакт = 1 / (f · T), то есть при
f = √(fтакт / T) = √(72 МГц / 1 с) ≈ 8485 Гц.
Ниже — выгоднее мерить период, выше — считать импульсы. Функция
freq_prefer_period выбирает способ по той же формуле.
197.2 Прогон: погрешность от 1 Гц до 10 МГц
Модель на компьютере для каждой частоты перебирает 50 разных фаз входа относительно такта таймера и берёт худшую ошибку:
| Частота | По одному периоду | Счётом за 1 с | Усреднение за 1 с | Выбран |
|---|---|---|---|---|
| 10 Гц | 7,0·10⁻⁸ | 9,9·10⁻² | 8,4·10⁻⁹ | по периоду |
| 1 кГц | 1,2·10⁻⁵ | 8,6·10⁻⁴ | 1,1·10⁻⁸ | по периоду |
| 8485 Гц | 6,8·10⁻⁵ | 1,0·10⁻⁴ | 1,3·10⁻⁸ | по периоду |
| 100 кГц | 1,3·10⁻³ | 7,0·10⁻⁶ | 1,2·10⁻⁸ | счётом |
| 1 МГц | 1,4·10⁻² | 7,0·10⁻⁷ | 8,3·10⁻⁹ | счётом |
- Счёт на 10 Гц ошибается на 10 %: за секунду 10 импульсов, ±1 — это десятая.
- Один период на 1 МГц — это 72 такта, ±1 такт — 1,4 %.
- Около 8,5 кГц оба способа дают около 10⁻⁴ — точка пересечения на месте. Строка 8485 Гц лежит чуть ниже границы √(72·10⁶) ≈ 8485,3 Гц, поэтому выбран период; на самой границе оценки ошибок равны, и выбор там не важен.
- Усреднение даёт около 10⁻⁸ на всех частотах: ±1 такт на всю секунду.
Почему тогда не мерить всегда усреднением? Нужно знать число периодов
между первым и последним захватом. На низких частотах его считает программа
в прерывании захвата; на мегагерцах прерывание на каждый фронт уже не успевает —
периоды считает второй таймер. Вход PA0 принадлежит только TIM2 (и TIM2_CH1,
и TIM2_ETR), поэтому тот же сигнал заводят ещё и на вход другого таймера,
например на TIM3_CH1 (PA6) во внешнем тактировании.
197.3 Ловушка: захват и переполнение в один миг
Таймер 16-битный, при 72 МГц переполняется 1099 раз в секунду. Так у F103: у F4, F7, G4, L4 и H7 таймеры TIM2 и TIM5 32-битные, при 72 МГц переполняются раз в 59,7 с, и для периодов короче этого расширение не нужно. Время захвата расширяют до 32 бит: старшие биты — счётчик переполнений, который прибавляет прерывание переполнения. Опасный момент — когда фронт пришёл сразу после переполнения, а прерывание переполнения ещё не обработано: захват уже маленький («новый круг»), а счётчик переполнений ещё старый.
Проверка в модели: настоящий период — 1000 тактов, первый захват на 65 000 при 7 переполнениях, второй — на 464, уже после переполнения:
| Код | Период | Частота |
|---|---|---|
наивный: ovf << 16 \| capture |
4 294 902 760 тактов | 0,017 Гц |
| исправленный | 1000 тактов | 72 000,0 Гц |
Исправление — смотреть на флаг переполнения в момент захвата:
uint32_t hi = ovf;
if (uif_pending && capture < 0x8000u)
hi++;
return hi << 16 | capture;
Если флаг висит, а захват маленький (меньше половины круга), значит фронт пришёл уже после переполнения — к старшей части добавляется единица. Если захват большой (65 500) — фронт был до переполнения, прибавлять нельзя: модель проверила и этот случай.
197.4 Прошивка для BluePill
plat_f103.c:
- такт — кварц 8 МГц × 9 = 72 МГц; APB1 = 36 МГц, но таймеры на APB1 при делителе 2 получают удвоенную частоту — 72 МГц;
- вход PA0 — это TIM2_CH1 (и ETR); захват по фронту, фильтр 8 тактов: он пропускает уровень, который держится не меньше 8 / 72 МГц = 111 нс, — для меандра это до 4,5 МГц;
- в прерывании TIM2 сначала обрабатывается захват (с проверкой флага переполнения), потом само переполнение;
- раз в 500 мс: если захватов два и больше — среднее по ним, если один — по
последнему периоду; результат форматируется
freq_formatи уходит в USART1 (делитель 625: 72 МГц / 625 = ровно 115 200 бод).
⚠️ PA0 у F103 не выдерживает 5 В: у него есть вход АЦП, пометки FT нет (приложение E). Сигнал 5-вольтовой логики или генератора подают через делитель (раздел 62.4) или через формирователь (197.5).
Сборка arm-none-eabi-gcc -Os для Cortex-M3 — 3744 байта Flash, 28 байт ОЗУ.
Заметная часть — программная арифметика double (у Cortex-M3 нет FPU): сложение
и умножение двойной точности — около 600 байт каждое.
Предел самой прошивки — прерывание на каждый фронт. Путь захвата в
TIM2_IRQHandler — 35 команд, а со входом в прерывание (12 тактов плюс
ожидание Flash, раздел 7), выходом из него и обращениями к таймеру на APB1
это грубо сотня тактов на фронт — оценка по дизассемблеру, не замер.
Значит, прошивка успевает примерно до 72 МГц / 100 ≈ 700 кГц, а с запасом
для остальной программы — вдвое ниже; выше она «зависает» (197.6). Дёшево
расширить диапазон помогает предделитель захвата — поле IC1PSC в
CCMR1: захват каждого 2-го, 4-го или 8-го фронта, прерываний во столько
же раз меньше, а найденный период делят на 2, 4 или 8. Ещё выше —
аппаратный счёт (197.5).
Формат вывода — пять значащих цифр и приставка: 1234,5678 Гц → «1.2346 кГц»; 999 999 Гц после округления — «1.0000 МГц», а не «1000.00 кГц».
197.5 Что улучшить
- Счёт на высоких частотах: TIM2 переводят во внешнее тактирование от ETR
(это тот же вывод PA0, режим 2, бит
ECE), и он сам считает импульсы, а ворота 1 с отмеряет TIM3 через внутреннюю линию запуска: для TIM2 этоITR2, ведомый режим «ворота». Выше примерно 8,5 кГц это точнее. Без делителя ETR принимает до четверти такта таймера — 18 МГц. - Целочисленная арифметика вместо
double: частота в миллигерцах в 64-битном целом — минус килобайт кода. - Делитель на входе (74HC4040 и подобные) — для частот выше десятков мегагерц.
- Формирователь входа (компаратор с гистерезисом, раздел 44) — для синуса и зашумлённых сигналов.
197.6 Грабли
Частота «прыгает» на порядки раз в минуту. Гонка захвата и переполнения: наивное расширение ошибается ровно на 65 536 тактов.
На 10 Гц показывает то 9, то 10, то 11. Счёт за секунду: ±1 импульс — это 10 %. Для низких частот — период.
На 1 МГц погрешность — проценты. Один период — 72 такта. Выше 8,5 кГц — счёт или усреднение.
Частота вдвое больше реальной. Захват по обоим фронтам вместо одного.
Таймер на APB1 тикает не с той частотой. При делителе APB1 не 1 таймер получает удвоенную частоту шины — 72 МГц, а не 36.
На высоких частотах прибор «зависает». Прерывание на каждый фронт: на мегагерце процессору больше некогда. Там — аппаратный счёт.
197.7 Итоги
- По периоду — ошибка ≈ f / fтакт, счётом — ≈ 1 / (f · T); равны при √(fтакт / T) ≈ 8485 Гц.
- Модель: 10 Гц счётом — 10 % ошибки, 1 МГц по одному периоду — 1,4 %; усреднение за секунду — около 10⁻⁸ везде.
- Захват и переполнение: смотреть флаг переполнения в момент захвата, иначе ошибка ровно на 65 536 тактов.
- Прошивка BluePill — 3744 байта; таймер TIM2 на 72 МГц при APB1 = 36 МГц.
Полные исходники
freq.c — логика частотомера без железа.
/* Мини-проект «Частотомер»: логика без железа.
*
* Два способа. По периоду: таймер считает такты f_clk, захват по фронту входа; частота = f_clk × N / (tN − t0).
* Счётом: вход тактирует счётчик, за ворота T считаем импульсы; частота = импульсы / T.
* Погрешность ±1 такта у первого — f/f_clk от периода; ±1 импульса у второго — 1/(f·T). Выбирается тот,
* у которого она меньше; равны они при f = √(f_clk / T).
*
* Таймер 16-битный: к захвату дописываются старшие биты из счётчика переполнений. Если переполнение
* и захват случились почти одновременно, наивный код ошибается ровно на 65 536 тактов — см. extend(). */
#include <stdint.h>
#include "freq.h"
/* Расширение 16-битного захвата до 32 бит.
* ovf — счётчик переполнений, прочитанный в обработчике захвата; uif — флаг переполнения, ещё не
* обработанный в этот момент (переполнение пришло, но его прерывание ждёт). Если захват маленький
* (< половины периода), а переполнение висит, — захват случился ПОСЛЕ переполнения: +1 к старшей части. */
uint32_t freq_extend(uint16_t capture, uint16_t ovf, int uif_pending)
{
uint32_t hi = ovf;
if (uif_pending && capture < 0x8000u)
hi++;
return hi << 16 | capture;
}
uint32_t freq_extend_naive(uint16_t capture, uint16_t ovf, int uif_pending)
{
(void)uif_pending;
return (uint32_t)ovf << 16 | capture;
}
/* по периоду: t0 и tn — расширенные захваты, n периодов между ними */
double freq_by_period(uint32_t t0, uint32_t tn, uint32_t n, double f_clk)
{
uint32_t dt = tn - t0; /* разность беззнаковых — верна и через переполнение 32 бит */
return dt ? f_clk * n / dt : 0.0;
}
double freq_by_count(uint32_t pulses, double gate_s)
{
return pulses / gate_s;
}
/* какой способ точнее: сравнить относительные погрешности ±1 такт и ±1 импульс */
int freq_prefer_period(double f_est, double f_clk, double gate_s)
{
return f_est / f_clk < 1.0 / (f_est * gate_s);
}
/* «12.345 кГц»: 5 значащих цифр, приставка по величине */
void freq_format(double f, char *out, int n)
{
static const char *const unit[] = {"Гц", "кГц", "МГц"};
int u = 0;
while (f >= 1000.0 && u < 2) {
f /= 1000.0;
u++;
}
/* после округления до пяти цифр 999,999 превращается в 1000,00 — тогда на ступень выше */
if (f >= 999.995 && u < 2) {
f /= 1000.0;
u++;
}
int dec = f >= 99.9995 ? 2 : f >= 9.99995 ? 3 : 4;
long scaled = (long)(f * (dec == 2 ? 100 : dec == 3 ? 1000 : 10000) + 0.5);
long ip = scaled / (dec == 2 ? 100 : dec == 3 ? 1000 : 10000);
long fp = scaled % (dec == 2 ? 100 : dec == 3 ? 1000 : 10000);
/* ручная печать без printf: целая часть, точка, дробная с ведущими нулями */
char b[24];
int i = 0;
char t[12];
int k = 0;
do {
t[k++] = (char)('0' + ip % 10);
ip /= 10;
} while (ip);
while (k)
b[i++] = t[--k];
b[i++] = '.';
for (long d = (dec == 2 ? 10 : dec == 3 ? 100 : 1000); d; d /= 10)
b[i++] = (char)('0' + (fp / d) % 10);
b[i++] = ' ';
for (const char *s = unit[u]; *s && i < 23; s++)
b[i++] = *s;
b[i] = 0;
for (k = 0; k < n - 1 && b[k]; k++)
out[k] = b[k];
out[k] = 0;
}
Дальше: раздел 198 «Мини-проект: часы на RTC с будильником и коррекцией хода».