Добавить в избранное

Таймеры AVR на C: Timer0/1/2, режим CTC, ШИМ и управление сервоприводом

Чтение ≈ 4 мин

Разбираем таймеры/счётчики ATmega328P на чистом C: предделители и расчёт частоты, системный тик на 1 мс в CTC-режиме, Fast PWM и Phase Correct PWM для яркости светодиодов и мощности, точное управление сервоприводом на 16-битном Timer1 и генерация звука.

Какие таймеры есть в ATmega328P

В ATmega328P три таймера/счётчика:

  • Timer0 — 8-битный, два канала ШИМ (OC0A = PD6, OC0B = PD5). В Arduino занят под millis().
  • Timer1 — 16-битный, каналы OC1A (PB1) и OC1B (PB2), есть вход захвата ICP1. Самый гибкий.
  • Timer2 — 8-битный, OC2A (PB3) и OC2B (PD3), умеет работать от часового кварца 32768 Гц в асинхронном режиме — основа часов реального времени.

Таймер — это просто счётчик TCNTn, который инкрементируется на каждый такт (или каждые N тактов через предделитель). При совпадении с регистром сравнения OCRn или при переполнении возникает событие: прерывание или автоматическое переключение вывода OCnx.

Предделители: первый шаг

Частота счёта = F_CPU / N, где N — предделитель: 1, 8, 64, 256 или 1024. Задаётся битами CS в регистре TCCRnB. Пока CS=0 — таймер стоит.

Для 8-битного таймера при 16 МГц максимальный период: 256 тиков × 1024 / 16 МГц = 16,38 мс. Для 16-битного: 65536 × 1024 / 16 МГц ≈ 4,19 с. Нужны длиннее интервалы — считайте переполнения в прерывании.

Режим CTC: точный системный тик на 1 мс

CTC (Clear Timer on Compare) — счётчик обнуляется при совпадении с OCRnA, генерируя прерывание с точной частотой. Для 1 мс на Timer0: 16000000 / 64 / 1000 - 1 = 249.

#define F_CPU 16000000UL
#include <avr/io.h>
#include <avr/interrupt.h>

volatile uint32_t g_ms = 0;

ISR(TIMER0_COMPA_vect)
{
    g_ms++;
}

static void timer0_init(void)
{
    TCCR0A = (1 << WGM01);          // режим CTC
    TCCR0B = (1 << CS01) | (1 << CS00); // предделитель 64
    OCR0A  = 249;                   // 16000000/64/1000 - 1
    TIMSK0 = (1 << OCIE0A);         // прерывание по совпадению A
}

uint32_t millis(void)
{
    uint32_t ms;
    cli();
    ms = g_ms;
    sei();
    return ms;
}

int main(void)
{
    DDRB |= (1 << PB5);
    timer0_init();
    sei();

    uint32_t last = 0;
    while (1) {
        if (millis() - last >= 500) {
            last = millis();
            PINB = (1 << PB5);      // toggle LED
        }
        // процессор свободен для других задач
    }
}

Получили свой millis() — базу для неблокирующей многозадачности. Переменная g_ms обязательно volatile, а чтение 32-битного значения оборачиваем в cli()/sei() — иначе прерывание может обновить его «наполовину».

Формулы для любой частоты

Частота прерывания CTC: F_int = F_CPU / (N × (OCR + 1)). Отсюда:

// искомый OCR под нужную частоту:
OCR = F_CPU / (N * F_int) - 1;

Примеры для 16 МГц:

  • 1 кГц, N=64 → OCR = 249
  • 10 кГц, N=8 → OCR = 199
  • 100 Гц, N=1024 → OCR = 155 (8-бит) или 15624 при N=1 для Timer1

Всегда проверяйте, что OCR влезает в разрядность таймера, и подбирайте минимальный предделитель — так точность выше.

Fast PWM: яркость светодиода

ШИМ аппаратно дёргает ножку OCnx: чем больше OCRn, тем больше скважность. Никакого кода в цикле — таймер работает сам.

// Fast PWM 8-bit на Timer2, выход OC2A = PB3 (Arduino D11)
static void pwm_init(void)
{
    DDRB |= (1 << PB3);
    TCCR2A = (1 << COM2A1)              // неинверсный ШИМ на OC2A
           | (1 << WGM21) | (1 << WGM20); // Fast PWM, TOP=255
    TCCR2B = (1 << CS22);               // предделитель 64 → ~976 Гц
}

static inline void pwm_set(uint8_t duty) { OCR2A = duty; }

Плавное «дыхание» светодиода:

int main(void)
{
    pwm_init();
    uint8_t d = 0;
    int8_t dir = 1;
    while (1) {
        pwm_set(d);
        d += dir;
        if (d == 0 || d == 255) dir = -dir;
        _delay_ms(4);
    }
}

Частота Fast PWM 8-бит: F_pwm = F_CPU / (N × 256). При N=64 и 16 МГц — 976 Гц: для светодиодов достаточно, мерцания не видно.

Phase Correct PWM и 16-битный Timer1

Phase Correct считает вверх-вниз — частота вдвое ниже, зато симметричные фронты: лучше для моторов и звука. Timer1 позволяет задать TOP через ICR1 и получить любую частоту с разрешением до 16 бит.

Сервопривод: 50 Гц и импульс 1–2 мс

Серву нужен период 20 мс (50 Гц) и импульс 0,5–2,5 мс для крайних положений. Идеально ложится на Timer1: режим Fast PWM с TOP в ICR1.

// ICR1 задаёт период 20 мс: 16000000/8/50 = 40000 тиков
// OCR1A задаёт импульс: 1 мс = 2000 тиков, 2 мс = 4000 тиков
static void servo_init(void)
{
    DDRB |= (1 << PB1);                 // OC1A = PB1 (Arduino D9)
    TCCR1A = (1 << COM1A1) | (1 << WGM11);
    TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS11); // Fast PWM, TOP=ICR1, N=8
    ICR1   = 39999;                     // 20 мс
    OCR1A  = 3000;                      // 1.5 мс — центр
}

// угол 0..180 -> импульс 2000..4000 тиков (1.0..2.0 мс)
static void servo_angle(uint8_t deg)
{
    OCR1A = 2000 + (uint16_t)deg * 2000 / 180;
}

Точность — 0,5 мкс на тик, джиттер нулевой: серва стоит мёртво, чего не добиться программным ШИМом. Второй канал OCR1B (PB2) управляет ещё одной сервой независимо.

Генерация звука: toggle-режим

Если ШИМ не нужен, а нужна частота — режим CTC с автоматическим toggle ножки OCnx (COM0A0=1). Пьезопищалка на OC0A (PD6):

// частота = F_CPU / (2 * N * (OCR0A + 1))
static void beep(uint8_t ocr)
{
    DDRD |= (1 << PD6);
    TCCR0A = (1 << COM0A0) | (1 << WGM01);   // toggle OC0A, CTC
    TCCR0B = (1 << CS01) | (1 << CS00);      // N=64
    OCR0A = ocr;                              // 124 -> ~1 кГц
}

static void silence(void) { TCCR0B = 0; PORTD &= ~(1 << PD6); }

Нота Ля (440 Гц): OCR0A = 16000000/(2×64×440) - 1 ≈ 283.

Input Capture: измерение периода сигнала

У Timer1 есть аппаратный вход захвата ICP1 (PB0). По фронту/спаду на нём текущее значение TCNT1 мгновенно фиксируется в ICR1 и вызывает прерывание — точность измерения периода до такта, не зависящая от длительности ISR.

volatile uint16_t g_period_ticks = 0;

ISR(TIMER1_CAPT_vect)
{
    static uint16_t prev = 0;
    uint16_t now = ICR1;
    g_period_ticks = now - prev;    // разность корректна и при переполнении
    prev = now;
}

static void icp_init(void)
{
    DDRB &= ~(1 << PB0);            // ICP1 — вход
    TCCR1B = (1 << ICNC1)           // noise canceler (4 выборки)
           | (1 << ICES1)           // захват по фронту
           | (1 << CS11);           // предделитель 8 -> 0.5 мкс/тик
    TIMSK1 = (1 << ICIE1);
}

Частота сигнала: F = F_CPU / (N * g_period_ticks). При 0,5 мкс на тик и периоде 2000 тиков получаем ровно 1 кГц с точностью кварца — так делают частотомеры и измерение оборотов по датчику Холла.

Типовые ошибки

  • Забыли DDRx |= на выход OCnx — ШИМ «есть», а на ножке тишина.
  • Неправильный режим WGM — у каждого таймера своя раскладка битов WGMx0..3 между TCCRxA и TCCRxB, сверяйтесь с таблицей даташита.
  • OCR больше TOP — в 8-битном Fast PWM с TOP=ICRn запись OCR выше TOP даст 100% скважность.
  • Делить регистры между Arduino и своим кодом — analogWrite() переинициализирует таймер, ваши настройки слетят.
  • Писать 16-битные регистры как два байта вручную — OCR1A = x в avr-libc атомарна только в main; в прерываниях сохраняйте порядок: старший байт, потом младший.

Итог

Таймеры — сердце AVR: системный тик, ШИМ, сервы, звук, точные интервалы — всё это работает аппаратно, пока процессор занят логикой. Следующий шаг — прерывания: именно через них таймеры и периферия сообщают программе о событиях.

Поделиться
Понравился материал?

Комментарии

Свернуть форму

Необходимо Войти или Зарегистрироваться, чтобы оставлять комментарии