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

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 МГц

Погрешность трёх способов измерения частоты в зависимости от частоты, такт 72 МГц, время 1 с

Модель на компьютере для каждой частоты перебирает 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⁻⁹ счётом

Почему тогда не мерить всегда усреднением? Нужно знать число периодов между первым и последним захватом. На низких частотах его считает программа в прерывании захвата; на мегагерцах прерывание на каждый фронт уже не успевает — периоды считает второй таймер. Вход PA0 принадлежит только TIM2 (и TIM2_CH1, и TIM2_ETR), поэтому тот же сигнал заводят ещё и на вход другого таймера, например на TIM3_CH1 (PA6) во внешнем тактировании.


197.3 Ловушка: захват и переполнение в один миг

Счётчик таймера проходит переполнение, фронт приходит на отсчёте 464, прерывание переполнения ещё ждёт

Таймер 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:

⚠️ 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 Что улучшить


197.6 Грабли

Частота «прыгает» на порядки раз в минуту. Гонка захвата и переполнения: наивное расширение ошибается ровно на 65 536 тактов.

На 10 Гц показывает то 9, то 10, то 11. Счёт за секунду: ±1 импульс — это 10 %. Для низких частот — период.

На 1 МГц погрешность — проценты. Один период — 72 такта. Выше 8,5 кГц — счёт или усреднение.

Частота вдвое больше реальной. Захват по обоим фронтам вместо одного.

Таймер на APB1 тикает не с той частотой. При делителе APB1 не 1 таймер получает удвоенную частоту шины — 72 МГц, а не 36.

На высоких частотах прибор «зависает». Прерывание на каждый фронт: на мегагерце процессору больше некогда. Там — аппаратный счёт.


197.7 Итоги

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

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 с будильником и коррекцией хода».

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