В этой статье мы разберёмся, как «призвать» (вызвать) функцию строго каждые 3750 мс на микроконтроллере AVR ATmega128/328, которые часто используют в проектах Arduino. Ниже приведены готовые решения, которые легко перенести и на «вольт» (вольтметр) с записью данных на SD-карту, и на любую другую задачу, где нужен точный таймер.
Почему не подойдёт простой delay
- delay «замораживает» весь контроллер — в это время не работают ни Serial, ни SD.
- При 3750 мс погрешность будет накапливаться, особенно если внутри loop есть ещё какие-то операции.
Аппаратный таймер Timer2 — 100 % точность без прерывания loop
Схема подключения
Для примера будем опрашивать кнопку (вместо геркона) на пине D2 (INT0), светодиод — на D13, а данные писать на SD через стандартный модуль SPI.
Калькуляция регистров
Частота кварца 16 МГц. Нужно получить прерывание каждые 3750 мс.
- Делитель 1024 → 16 000 000 / 1024 = 15 625 Гц;
- Кол-во тиков за 3,75 с: 15 625 × 3,75 = 58 593,75.
- Считаем от 0 до 58 593 (ближайшее целое) и сбрасываем.
Готовый скетч
#include <SD.h>
#include <SPI.h>
const uint8_t PIN_LED = 13;
const uint8_t PIN_BTN = 2;
const uint8_t CS_SD = 4;
volatile bool flag_3750ms = false; // «вызыватель» функции
// Таблица указателей на функции (если нужно выбирать разные режимы)
typedef void (*FuncPtr)(void);
FuncPtr callAfterRitual = readVoltageAndLog; // «приворот» на вольт
//-------------------------- Настройка Timer2 --------------------------
ISR(TIMER2_COMP_vect) {
static uint16_t ticks = 0;
if (++ticks >= 586) { // 586 × 64 мкс ≈ 37,5 мс → ×100 ≈ 3,75 с
ticks = 0;
flag_3750ms = true;
}
}
void start3750msTimer {
// Timer2: CTC, prescaler=1024
TCCR2A = _BV(WGM21);
TCCR2B = _BV(CS22) | _BV(CS21) | _BV(CS20);
OCR2A = 249; // 249+1 = 250 тактов => 16 000 000 / 1024 / 250 = 62,5 Гц
TIMSK |= _BV(OCIE2); // Разрешить прерывание совпадения
}
//--------------------------------------------------------------------
void setup {
pinMode(PIN_LED, OUTPUT);
pinMode(PIN_BTN, INPUT_PULLUP);
Serial.begin(115200);
SD.begin(CS_SD);
start3750msTimer;
}
void loop {
// 1. Работаем с Serial/SD без задержек
if (Serial.available) handleSerial;
// 2. «Вызов после приворота на вольт» каждые 3750 мс
if (flag_3750ms) {
flag_3750ms = false;
callAfterRitual; // «магия» срабатывает точно по расписанию
}
}
//----------------------- Замер напряжения и запись --------------------
void readVoltageAndLog {
const float VREF = 5.0;
uint16_t raw = analogRead(A0);
float voltage = raw * VREF / 1024.0;
Serial.print(F("Voltage = "));
Serial.println(voltage, 2);
File f = SD.open("volt.csv", FILE_WRITE);
if (f) {
f.print(millis);
f.print(';');
f.println(voltage, 2);
f.close;
}
digitalWrite(PIN_LED, !digitalRead(PIN_LED)); // мигнем светодиодом
}
//--------------------------------------------------------------------
Альтернатива: millis-кадрирование (без прерываний)
Если вы предпочитаете «ардуино-стиль» без прямого доступа к регистрам, можно использовать стандартный millis:
uint32_t prev = 0;
const uint32_t PERIOD = 3750;
void loop {
uint32_t now = millis;
if (now ⸺ prev >= PERIOD) {
prev = now;
readVoltageAndLog;
}
// остальной код
}
Но на высоких скоростях и при большом количестве задач может появиться дрейф ±1-2 мс.
Советы по надежности
- Всегда объявляйте функции-обработчики типом void f и помещайте до setup или в отдельный модуль.
- Для SD-карты используйте SdFat вместо старой SD-библиотеки — быстрее и меньше прерывают таймеры.
- Если точность критична, не используйте delay внутри обработчиков прерываний.
- Для измерения напряжения с точностью до 0,01 В возьмите опорное напряжение 2,56 В и analogReference(INTERNAL).
С помощью аппаратного тайнера Timer2 мы «навели приворот» на микроконтроллер: теперь функция readVoltageAndLog вызывается каждые ровные 3750 мс, неважно, работает ли в это время Serial или идёт запись на SD. Такой приём пригодится и для «вольтметра», и для любых других периодических задач.
Приятной магии ваших 3,75 секунд!