Таймеры и задержки: сборник рецептов
Увидел в статье DI HALT "Простой программный таймер для конечных автоматов" некий кусок кода и решил накатать статью…
Сначала писал длинно, с приколами и разжевыванием, но мне не понравилось и в итоге убрал в черновики. Решил переоформить как краткий сборник рецептов.
Прибавили к текущему счёту нужную задержку и включили прерывание. Важно сначала обновить OCR, а затем включать прерывание, иначе можно включить, а оно тут же сработает — ведь что сейчас в TCNT и OCR мы не знаем. Фактически интервал будет отсчитан от момента чтения TCNT.
Что делать НЕ надо:
Запустили таймер, включили прерывание, в начале или конце прерывания обновляем OCR.
Что делать НЕ надо:
При тактовой частоте 16 МГц нужно вызывать прерывание с частотой 4800 Гц. Таймер 0 работает с прескалером 64. Получается, что нужен период в 52 и 1/12 тика.
Дробную часть периода каждый раз прибавляем к аккумулятору ошибки. Накопили целый тик — отсчитали период на тик больше и вычли тик из аккумулятора ошибки.
Данные меньше разрядности int (8 бит)
Данные больше или равны разрядности int (16, 32, 64 бита)
Достаточно просто вычесть. Главное, чтобы время прошедшее между t1 и t2 не превышало разрядность данных (от 0 до 255 тиков для uint8_t, от 0 до 65535 тиков для uint16_t).
Мигалка:
Что делать НЕ надо:
По сути, основа любимых в народе программных таймеров.
Операция "(int8_t)(TCNT0 — t1)" вернёт количество тиков, прошедших с момента времени t1 до текущего. Похоже на предыдущее, но теперь результат был интерпретирован как int8_t. Если t1 ещё в будущем, то результат отрицательный, если уже в прошлом — положительный. Главное, чтобы этот результат вписывался в диапазон int8_t — от -128 до 127. Следовательно, максимальная задержка — 128 тиков для 8 бит, 32768 тиков для 16 бит… Остальная часть диапазона — запас, в течении которго нужно успеть обработать срабатывание.
Что делать НЕ надо:
Похоже на предыдущее, но теперь разность TCNT0 — t1 интерпретируется как число в диапазоне от -240 до 15. Следовательно, теперь максимальная задержка — 240 тиков и 16 тиков остаётся чтобы успеть обработать срабатывание. Добавление 240 смещает диапазон -240..15 в 0..255 для корректного сравнения. Для данных большей разрядности — заменить 240 на нужный диапазон и убрать приведение к uint8_t.
0xFF00 (65280) тиков из 65536 — максимальная задержка и оставшиеся 256 тиков — запас.
Что делать НЕ надо:
Теперь вместо «OCR0 += 100» можно писать «OCR0 += T0_US(400)». Даже при пересчёте 1:1 запись с единицей измерения («T_US(25)») выглядит лучше чем просто число («25»). Особенно в конфиге, где не ясно из контекста 25 чего имеется в виду. Половинка добавляется для более правильного округления.
Что делать НЕ надо:
Функция t0_update() обновляет значение полного счётчика тиков. Обычно задержки проверяются один раз за проход главного цикла, потому и обновлять значение системного тика удобно один раз из главного цикла. Младшая часть значения получается из аппаратного счётчика, старшая — из посчитанного количества переполнений.
Нужно предусмотреть обработку ситуации когда мы прочитали старший байт (счётчик переполнений), после чего младший (регистр таймера) обнулился и его прочитали уже после обнуления. Тогда, например, если тик должен был смениться с 0x33FF на 0x3400, можно получить некорректное значение 0x3300 — тик резко скакнёт назад. Или может получиться 0x34FF, если читать сначала младший байт. Поэтому читаем сначала старший байт, затем младший и проверяем не изменился ли старший. Если переполнение произойдёт после чтения ovf_ctr в ctr_high мы это обнаружим по изменившемуся ovf_ctr и нужно будет повторить чтение. Желательно ещё проверять флажок переполнения TOV0 на случай если мы прочитали ovf_ctr, TCNT0 переполнился, но прерывание ещё не вызвалось и не успело обновить ovf_ctr, чтобы это можно было обнаружить. Поскольку AVR при возврате из прерывания всегда выполняет ещё одну командочку прежде чем обрабатывать следуюшее, в принципе такое возможно…
Если счётчик переполнений — больше одного байта, для проверки его изменения достаточно проверить только младший байт — он гарантировано меняется при инкрементировании:
Мигалка:
Что делать НЕ надо:
Это мой любимый подход в организации системного тика. При тактовой частоте 16 МГц, прескалере таймера 256 получается разрешение тика в 16 мкс и полный цикл в 1 секунду. При прескалере 1024 — 64 мкс и 4 секунды. При этом аппаратный таймер может выполнять ещё кучу полезной работы.
Переименовать фактический счётчик тиков во что-нибудь вроде "__t0_ctr_internal" и написать макрос, обращающийся к нему, например, как #define t0_ctr() (__t0_ctr_internal + 0). Компилятор не позволит присвоить выражению.
Что делать НЕ надо:
Иногда от лишних прерываний стоит избавляться дабы уменьшить джиттер действительно важных прерываний и уменьшить расход тактов впустую на вход в прерывание и выход. Можно проверять переполнение при обновлении счётчика тиков. Если, конечно, есть уверенность в том, что таймер не успеет переполниться более одного раза за то время, пока тупит главный цикл. Максимальное время оборота главного цикла обычно ограниченно, в т.ч. и сторожевым псом, так что подход вполне применим. Особенно с длинным 16-битным таймером.
Нельзя проверить TOV0 и, если он сброшен, считать что переполнения не было. Переполнение может произойти сразу же после проверки. Зато если флажок сброшен когда уже прочитали — значит всё в порядке. Если же флажок установлен — переполнение было и нужно увеличить старший байтик, а также перечитать TCNT0 — ведь неизвестно было ли переполнение в момент первого чтения. Ну и сбросить флажок не забыть.
Что делать НЕ надо:
Имея счётчик тиков высокого разрешения легко можно сделать вторичные счётчики более низкого
Полный цикл t_ms — чуть больше минуты, t_sec — чуть больше 18 часов, t_sec32 — чуть больше 136 лет.
Метки времени могут быть и меньшей разрядности чем счётчик если их хватает для отмеряемых интервалов. В данном случае, t_ms — 16 бит, t_led_off — 8 бит. Главное — не забывать приводить разность к нужному размеру если он меньше чем разрядность уменьшаемого, вычитаемого или int.
При проверке пора ли гасить диодик сперва проверяется включен ли диодик. Хотя делать это и не обязательно, это экономит чутка тактов — проверка бита в порту выполняется быстрее плюс LED_OFF() обходится.
Будет посчитано суммарное время работы мотора в секундах. Можно использовать для last_update байтик, пока интервал между вызовами функции в него влезает (255 секунд, ещё бы ему не влезть).
Сделаем счётчик моточасов энергонезависимым:
p.s. Давно хотел написать ещё про кольцевые буферы — там тоже рожают тех ещё уродцев. И про работу с TWI по-человечески… А интересно ли это кому-нибудь? :-[
Функция опрашивает 4 кнопочки A, B, C, D. 4 младших бита релуьтата указывают на событие нажатия соответствующей кнопочки. Если функция вернула единичку — можно выполнить соответствующее действие. В старшем байте 4 бита указывают на текущее состояние кнопочек, которое полезно, например, если какие-то кнопочки используются в качестве модификаторов («нажми A, удерживая B»). Кнопочки A и C имеют функцию автоматичского повторения — если кнопочку удерживать более 0.5с, функция начинает возвращать событие «нажата кнопочка A» с периодом в 75 мс. Это удобно для кнопочек типа +/-, вверх/вниз и т.п.
При опросе кнопочек приходится принимать меры для защиты от дребезга контактов, чтобы не поймать несколько фронтов на одно нажатие или отпускание кнопочки.

Простейший способ защититься от дребезга — ограничить интервал опроса кнопочки, чтобы за время нестабильности не опросить кнопочку более 1 раза. В таком случае, неважно, что вернёт нам этот опрос — программа так или иначе увидит один фронт.

Время дребезга контактов для подавляющего большинства кнопочек составляет не более 5-9 мс. Интервал опроса можно выбрать в районе 20-60 мс. Не стоит лепить интервал в сотни миллисекунд — это не даст никакой пользы, лишь будет раздражать юзера медленным откликом устройства.
При опросе мы сперва читаем состояние кнопочек в переменную new_key_state
С состоянием кнопочек в виде флажков, упакованных в переменную очень удобно работать — например, выделить нажатия:
В переменной key_push будут выставлены единички для тех кнопочек, которые нажаты при данном опросе и не были нажаты до него.
Затем можно записать текущее состояние кнопочек
Чтобы сделать повторение при удержании кнопочек заводится переменная «время следующего повторения» для каждой из кнопочек. При нажатии кнопочки записывается время первого повторения через 500 мс.
Затем, если кнопочка всё ещё нажата, проверятеся истекло ли это время. Если да — то повторяется нажатие кнопочки установкой флажка в переменной key_push и записывается время следующего повторения через 75 мс.
Обратите внимание на обновлении метки времени как «t_rept_a += T_KEY_REPT_PERIOD». При такой конструкции будет сегенерировано ровно по одному событию на 75 мс. Даже если главный цикл затупит, скажем, на 750 мс — при следующих проходах будет сгенерировано 10 «нажатий» подряд. Можно переписать как «t_rept_a = t_ms + T_KEY_REPT_PERIOD» — тогда между «нажатиями» будет не менее 75 мс. Что лучше — зависит от задачи…
Функцию read_keys можно вызываеть из главного цикла, например, как-то так
Что делать НЕ надо:
bool MainTimerIsExpired(const uint32_t Timer) // ???
{ // ???
if ((TimeMs - Timer) < (1UL << 31)) return (Timer <= TimeMs); // ???
return false; // ???
} // ???
Сначала писал длинно, с приколами и разжевыванием, но мне не понравилось и в итоге убрал в черновики. Решил переоформить как краткий сборник рецептов.
Задача 1: выполнить прерывание с заданной задержкой в тиках
ISR(TIMER0_COMP_vect)
{
// ... code ...
TIMSK &= ~(1<<OCIE0); /* отключить прерывание */
}
void set_timer(uint8_t delay)
{
OCR0 = TCNT0 + delay; /* выставить нужный интервал */
TIFR = 1<<OCF0; /* сбросить флажок прерывания */
TIMSK |= 1<<OCIE0; /* включить прерывание */
}
Прибавили к текущему счёту нужную задержку и включили прерывание. Важно сначала обновить OCR, а затем включать прерывание, иначе можно включить, а оно тут же сработает — ведь что сейчас в TCNT и OCR мы не знаем. Фактически интервал будет отсчитан от момента чтения TCNT.
Что делать НЕ надо:
- Останавливать таймер и обнулять TCNT0 — это лишь увеличит код на 2 строчки и не даст использовать таймер параллельно для других целей.
- 16-битный таймер останавливать тоже не нужно, теневые регистры в таймере как раз для того, чтобы можно было писать и читать на ходу.
- Заботиться о «переполнении» — оно на самом деле вообще не переполнение, а красивый механизм, позволяющий не хранить лишние данные и не считать лишние разряды. Главное — чтобы интервал не превышал разрядность таймера (255 тиков для 8-битного таймера, 65535 тиков для 16-битного).
Задача 2: выполнять прерывание с заданным периодом
ISR(TIMER0_COMP_vect)
{
//code
OCR0 += 100; /* 100 тиков от последнего срабатывания */
}
Запустили таймер, включили прерывание, в начале или конце прерывания обновляем OCR.
Что делать НЕ надо:
- Вместо «OCR0 += 100» писать наподобие «OCR0 = TCNT0 + 100». Так вместо периода ровно в 100 тиков получим 100 тиков + задержка входа в прерывание + пока выполнение дойдёт до обновления OCR.
- Использовать CTC режим. Он похоронит второй канал и другие ресурсы таймера.
- Обнулять TCNT — по тем же причинам.
- Заботиться о «переполнении» (см. задачу 1)
Задача 3: выполнять прерывание с заданным дробным периодом
При тактовой частоте 16 МГц нужно вызывать прерывание с частотой 4800 Гц. Таймер 0 работает с прескалером 64. Получается, что нужен период в 52 и 1/12 тика.
/* делитель частоты в виде правильной дроби
* 16 МГц / 64 / 4800 = 52 и 1/12 */
#define DIV_INT 52 /* целая часть */
#define DIV_FRAC_P 1 /* числитель дробной части */
#define DIV_FRAC_Q 12 /* знаменатель дробной части */
ISR(TIMER0_COMP_vect)
{
static uint8_t err_acc; /* аккумулятор ошибки */
// code
/* Обновляем регистр сравнения */
err_acc += DIV_FRAC_P; /* прибавляем дробную часть тика к аккумулятору ошибки */
if(err_acc >= DIV_FRAC_Q) { /* проверяем накопление целого тика в аккумуляторе ошибки */
OCR0 += DIV_INT + 1; /* если накопили - отсчитываем период на один тик больше */
err_acc -= DIV_FRAC_Q; /* и уменьшаем аккумулятор на один тик */
} else {
OCR0 += DIV_INT; /* а иначе - отсчиываем обычный период */
}
}
Дробную часть периода каждый раз прибавляем к аккумулятору ошибки. Накопили целый тик — отсчитали период на тик больше и вычли тик из аккумулятора ошибки.
Задача 4: узнать сколько прошло времени между метками t1 и t2
Данные меньше разрядности int (8 бит)
uint8_t t1, t2, elapsed;
t1 = TCNT0; /* записали первую метку */
t2 = TCNT0; /* записали вторую метку */
elapsed = (uint8_t)(t2 - t1);
Данные больше или равны разрядности int (16, 32, 64 бита)
uint16_t t1, t2, elapsed;
t1 = TCNT1; /* записали первую метку */
t2 = TCNT1; /* записали вторую метку */
elapsed = t2 - t1;
Достаточно просто вычесть. Главное, чтобы время прошедшее между t1 и t2 не превышало разрядность данных (от 0 до 255 тиков для uint8_t, от 0 до 65535 тиков для uint16_t).
Мигалка:
if(TCNT1 - last_blink >= 2230) /* ~3.5 Hz @ 16 MHz/1024 */
{
last_blink = TCNT1;
LED_TOGGLE();
}
Что делать НЕ надо:
- Заботиться о «переполнении»: имеющиеся 8 или 16 бит есть по сути младшие разряды полных меток времени. Для того, чтобы вычислить младшие разряды разности достаточно знать младшие разряды уменьшаемого и вычитаемого. Посчитайте в столбик и увидите. Как я уже сказал, «переполнение» (а вернее, обрезание старших разрядов) — это всего лишь механизм, позволяющий не хранить лишние данные.
Задача 5: узнать сколько тиков осталось до метки или прошло после метки
По сути, основа любимых в народе программных таймеров.
uint8_t t1;
t1 = TCNT0 + delay; /* установили задержку */
if((int8_t)(TCNT0 - t1) >= 0) /* проверяем прошла ли задержка */
Операция "(int8_t)(TCNT0 — t1)" вернёт количество тиков, прошедших с момента времени t1 до текущего. Похоже на предыдущее, но теперь результат был интерпретирован как int8_t. Если t1 ещё в будущем, то результат отрицательный, если уже в прошлом — положительный. Главное, чтобы этот результат вписывался в диапазон int8_t — от -128 до 127. Следовательно, максимальная задержка — 128 тиков для 8 бит, 32768 тиков для 16 бит… Остальная часть диапазона — запас, в течении которго нужно успеть обработать срабатывание.
next_blink = TCNT1 + 32768; /* макс. задержка */
//...
if((int16_t)(TCNT1 - next_blink) >= 0)
{
next_blink += 2230; /* исключает ошибку частоты */
LED_TOGGLE();
}
Что делать НЕ надо:
- Заботиться о «переполнении» — см. предыдущие задачи.
- Лепить уродцев типа того, что я привёл в начале поста.
Задача 6: увеличить диапазон задержек программного таймера
uint8_t t1;
t1 = TCNT0 + delay; /* установили задержку */
if((uint8_t)(TCNT0 - t1 + 240) >= 240) /* проверяем кончилась ли задержка */
Похоже на предыдущее, но теперь разность TCNT0 — t1 интерпретируется как число в диапазоне от -240 до 15. Следовательно, теперь максимальная задержка — 240 тиков и 16 тиков остаётся чтобы успеть обработать срабатывание. Добавление 240 смещает диапазон -240..15 в 0..255 для корректного сравнения. Для данных большей разрядности — заменить 240 на нужный диапазон и убрать приведение к uint8_t.
uint16_t t1;
t1 = TCNT1 + delay; /* установили задержку */
if(TCNT1 - t1 + 0xFF00 >= 0xFF00) /* проверяем кончилась ли задержка */
0xFF00 (65280) тиков из 65536 — максимальная задержка и оставшиеся 256 тиков — запас.
Что делать НЕ надо:
- Заботиться о «переполнении» (если вы всё ещё не поняли, что «переполнение», а вернее — циклический код — это круто!)
Задача 7: избавиться от ручного пересчёта стандартных единиц времени в тики таймера
#define T0_PRESC (1<<CS02) /* F_CPU / 256 */
#define T0_FREQ (F_CPU / 256.0)
#define T0_US(n) (uint16_t)(1e-6 * (n) * T0_FREQ + 0.5)
#define T0_MS(n) (uint16_t)(1e-3 * (n) * T0_FREQ + 0.5)
#define T0_SEC(n) (uint16_t)( 1.0 * (n) * T0_FREQ + 0.5)
#define T0_START() TCCR0 = T0_PRESC
Теперь вместо «OCR0 += 100» можно писать «OCR0 += T0_US(400)». Даже при пересчёте 1:1 запись с единицей измерения («T_US(25)») выглядит лучше чем просто число («25»). Особенно в конфиге, где не ясно из контекста 25 чего имеется в виду. Половинка добавляется для более правильного округления.
Что делать НЕ надо:
- Использовать эти макросы не для констант. Пересчёт в рантайме через флоаты будет убийственно неоптимален, особенно если компилить в avr-gcc без -lm.
Задача 8: расширить разрядность таймера для организации системного тика
//tick.h
#pragma once
#include <stdint.h>
#define T0_PRESC (1<<CS02)
#define T0_FREQ (F_CPU / 256.0)
#define T0_US(n) (uint16_t)(1e-6 * (n) * T0_FREQ + 0.5)
#define T0_MS(n) (uint16_t)(1e-3 * (n) * T0_FREQ + 0.5)
#define T0_SEC(n) (uint16_t)( 1.0 * (n) * T0_FREQ + 0.5)
extern uint16_t t0_ctr;
void t0_update();
void t0_init();
//tick.c
#include <avr/io.h>
#include <avr/interrupt.h>
#include "tick.h"
/* расширенный счётчик тиков */
uint16_t t0_ctr;
/* считаем переполнения */
static volatile uint8_t ovf_ctr;
ISR(TIMER0_OVF_vect)
{
ovf_ctr++;
}
/* обновляем счётчик тиков */
void t0_update()
{
uint8_t ctr_high, ctr_low;
do {
ctr_high = ovf_ctr;
ctr_low = TCNT0;
} while((TIFR & (1<<TOV0)) || (ovf_ctr != ctr_high));
t0_ctr = ctr_low | (ctr_high << 8);
}
/* запуск таймера */
void t0_init()
{
TCCR0B = T0_PRESC; /* запустить счёт */
TIMSK |= 1<<TOIE0; /* разрешить прерывание по переполнению */
}
Функция t0_update() обновляет значение полного счётчика тиков. Обычно задержки проверяются один раз за проход главного цикла, потому и обновлять значение системного тика удобно один раз из главного цикла. Младшая часть значения получается из аппаратного счётчика, старшая — из посчитанного количества переполнений.
Нужно предусмотреть обработку ситуации когда мы прочитали старший байт (счётчик переполнений), после чего младший (регистр таймера) обнулился и его прочитали уже после обнуления. Тогда, например, если тик должен был смениться с 0x33FF на 0x3400, можно получить некорректное значение 0x3300 — тик резко скакнёт назад. Или может получиться 0x34FF, если читать сначала младший байт. Поэтому читаем сначала старший байт, затем младший и проверяем не изменился ли старший. Если переполнение произойдёт после чтения ovf_ctr в ctr_high мы это обнаружим по изменившемуся ovf_ctr и нужно будет повторить чтение. Желательно ещё проверять флажок переполнения TOV0 на случай если мы прочитали ovf_ctr, TCNT0 переполнился, но прерывание ещё не вызвалось и не успело обновить ovf_ctr, чтобы это можно было обнаружить. Поскольку AVR при возврате из прерывания всегда выполняет ещё одну командочку прежде чем обрабатывать следуюшее, в принципе такое возможно…
Если счётчик переполнений — больше одного байта, для проверки его изменения достаточно проверить только младший байт — он гарантировано меняется при инкрементировании:
while((TIFR & (1<<TOV1)) || ((uint8_t)ovf_ctr != (uint8_t)ctr_high))
Мигалка:
//main.c
#include
#include "tick.h"
#define BLINK_FREQ 2.5 /* Hz */
int main()
{
uint16_t next_blink;
/* ... */
t0_init();
next_blink = t0_ctr; /* сработает при первом же проходе */
for(;;)
{
/* ... */
t0_update();
/* ... */
if((int16_t)(t0_ctr - next_blink) >= 0)
{
next_blink += T0_SEC(0.5 / BLINK_FREQ);
LED_TOGGLE();
}
}
return 0;
}
Что делать НЕ надо:
- Собирать счётчик тиков из половинок каждый раз, когда нужно к нему обратиться. Достаточно это делать один раз за проход главного цикла.
- Лепить функцию, которая тупо возвращает текущее значение системного тика. Доступ через глобальную переменную куда как эффективней.
- Совать в атомики доступ к переменным, которые могут измениться в прерывании. Лучше применять неблокирующие подходы — например, прочитать дважды и сравнить.
Это мой любимый подход в организации системного тика. При тактовой частоте 16 МГц, прескалере таймера 256 получается разрешение тика в 16 мкс и полный цикл в 1 секунду. При прескалере 1024 — 64 мкс и 4 секунды. При этом аппаратный таймер может выполнять ещё кучу полезной работы.
Задача 9: защитить счётчик тиков от случайного изменения
Переименовать фактический счётчик тиков во что-нибудь вроде "__t0_ctr_internal" и написать макрос, обращающийся к нему, например, как #define t0_ctr() (__t0_ctr_internal + 0). Компилятор не позволит присвоить выражению.
Что делать НЕ надо:
- Страдать этой хренотенью. Мало ли куда и что можно по ошибке записать. Давайте тогда и TCNT в функцию get_tcnt() оборачивать, равно как и все остальные регистры и данные. А, можно и в обход функции к регистру обратиться? Блин. Ну давайте ещё что-нибудь налепим… Все эти ограничительства попахивают роскомнадзорщиной — банить что первое под руку подвернётся.
Задача 10: обойтись без прерывания при подсчёте переполнений
//tick.h
#pragma once
#include <stdint.h>
#define T0_PRESC (1<<CS02)
#define T0_FREQ (F_CPU / 256.0)
#define T0_US(n) (uint16_t)(1e-6 * (n) * T0_FREQ + 0.5)
#define T0_MS(n) (uint16_t)(1e-3 * (n) * T0_FREQ + 0.5)
#define T0_SEC(n) (uint16_t)( 1.0 * (n) * T0_FREQ + 0.5)
extern uint16_t t0_ctr;
void t0_update();
#define t0_init() TCCR0B = T0_PRESC
//tick.c
#include <avr/io.h>
#include "tick.h"
uint16_t t0_ctr;
void t0_update()
{
uint8_t ctr_high, ctr_low;
ctr_high = t0_ctr >> 8;
ctr_low = TCNT0;
if(TIFR & (1<<TOV0))
{
TIFR = 1<<TOV0;
ctr_high++;
ctr_low = TCNT0;
}
t0_ctr = ctr_low | (ctr_high << 8);
}
Иногда от лишних прерываний стоит избавляться дабы уменьшить джиттер действительно важных прерываний и уменьшить расход тактов впустую на вход в прерывание и выход. Можно проверять переполнение при обновлении счётчика тиков. Если, конечно, есть уверенность в том, что таймер не успеет переполниться более одного раза за то время, пока тупит главный цикл. Максимальное время оборота главного цикла обычно ограниченно, в т.ч. и сторожевым псом, так что подход вполне применим. Особенно с длинным 16-битным таймером.
Нельзя проверить TOV0 и, если он сброшен, считать что переполнения не было. Переполнение может произойти сразу же после проверки. Зато если флажок сброшен когда уже прочитали — значит всё в порядке. Если же флажок установлен — переполнение было и нужно увеличить старший байтик, а также перечитать TCNT0 — ведь неизвестно было ли переполнение в момент первого чтения. Ну и сбросить флажок не забыть.
Что делать НЕ надо:
- Использовать этот подход если нет уверенности в том, что время оборота главного цикла всегда меньше полного цикла таймера и недопустимо растягивание задержек при затупах главного цикла
Задача 11: организовать вторичные счётчики времени в миллисекундах и секундах
Имея счётчик тиков высокого разрешения легко можно сделать вторичные счётчики более низкого
//tick.h
//...
extern uint16_t t_ms;
extern uint32_t t_sec32;
#define t_sec (uint16_t)t_sec32
//...
//tick.c
uint16_t t_ms;
uint32_t t_sec32;
void t0_update()
{
static uint16_t last_ms, last_sec;
/* ... обновляем t0_ctr ... */
while(t0_ctr - last_ms >= T0_MS(1))
{
t_ms++;
last_ms += T0_MS(1);
if(t_ms - last_sec >= 1000)
{
t_sec32++;
last_sec += 1000;
}
}
}
Полный цикл t_ms — чуть больше минуты, t_sec — чуть больше 18 часов, t_sec32 — чуть больше 136 лет.
Задача 12: сэкономить память на хранении меток времени
void handle_some_protocol()
{
static uint8_t t_led_off;
uint8_t buf[MAX_PACKET];
uint16_t req_len, resp_len;
if((req_len = get_request(buf)) != 0) /* прочитать новый запрос */
{
if((resp_len = handle_request(buf, req_len)) != 0) /* обработать запрос */
{
send_response(buf, resp_len); /* отправить ответ */
/* мигнуть светодиодиком на 50 мс - запрос обработан */
LED_ON();
t_led_off = t_ms + 50;
}
}
/* а не пора ли гасить диодик? */
if(LED_IS_ON() && ((int8_t)(t_ms - t_led_off) >= 0))
LED_OFF();
}
Метки времени могут быть и меньшей разрядности чем счётчик если их хватает для отмеряемых интервалов. В данном случае, t_ms — 16 бит, t_led_off — 8 бит. Главное — не забывать приводить разность к нужному размеру если он меньше чем разрядность уменьшаемого, вычитаемого или int.
При проверке пора ли гасить диодик сперва проверяется включен ли диодик. Хотя делать это и не обязательно, это экономит чутка тактов — проверка бита в порту выполняется быстрее плюс LED_OFF() обходится.
Задача 13: считать наработку оборудования
uint32_t motor_run_time;
void update_run_time()
{
static uint8_t last_update;
/* считаем время наработки */
if(IS_MOTOR_ON())
motor_run_time += (uint8_t)((uint8_t)t_sec32 - last_update);
last_update = t_sec32;
}
Будет посчитано суммарное время работы мотора в секундах. Можно использовать для last_update байтик, пока интервал между вызовами функции в него влезает (255 секунд, ещё бы ему не влезть).
Сделаем счётчик моточасов энергонезависимым:
uint32_t motor_run_time;
static uint32_t EEMEM ee_persist_run_time;
static uint32_t persist_run_time;
void update_run_time()
{
static uint8_t last_update;
/* считаем время наработки */
if(IS_MOTOR_ON())
motor_run_time += (uint8_t)((uint8_t)t_sec32 - last_update);
/* сбрасываем в EEPROM каждый час и при остановке */
if(motor_run_time != persist_run_time)
{
if(!IS_MOTOR_ON() || (motor_run_time - persist_run_time >= 3600))
{
eeprom_write_dword(&ee_persist_run_time, motor_run_time);
persist_run_time = motor_run_time;
}
}
last_update = t_sec32;
}
void init_run_time()
{
persist_run_time = eeprom_read_dword(&ee_persist_run_time);
if(persist_run_time == 0xffffffff) /* обнулить если не инициализировано */
persist_run_time = 0;
motor_run_time = persist_run_time;
}
p.s. Давно хотел написать ещё про кольцевые буферы — там тоже рожают тех ещё уродцев. И про работу с TWI по-человечески… А интересно ли это кому-нибудь? :-[
Upd:
Пример 1. Задержки и опрос кнопочек
Функция опрашивает 4 кнопочки A, B, C, D. 4 младших бита релуьтата указывают на событие нажатия соответствующей кнопочки. Если функция вернула единичку — можно выполнить соответствующее действие. В старшем байте 4 бита указывают на текущее состояние кнопочек, которое полезно, например, если какие-то кнопочки используются в качестве модификаторов («нажми A, удерживая B»). Кнопочки A и C имеют функцию автоматичского повторения — если кнопочку удерживать более 0.5с, функция начинает возвращать событие «нажата кнопочка A» с периодом в 75 мс. Это удобно для кнопочек типа +/-, вверх/вниз и т.п.
#define KEY_A (1<<0)
#define KEY_B (1<<1)
#define KEY_C (1<<2)
#define KEY_D (1<<3)
#define T_KEY_REPT_START 500 /* задержка начала повторений */
#define T_KEY_REPT_PERIOD 75 /* период повторений */
static uint16_t read_keys()
{
static uint8_t cur_key_state;
static uint16_t t_last_poll;
static uint16_t t_rept_a, t_rept_c;
uint8_t new_key_state, key_push = 0;
/* Опрашиваем кнопочки каждые 25 мс */
if(t_ms - t_last_poll >= 25)
{
t_last_poll = t_ms;
/* Читаем текущее состояние кнопочек */
new_key_state = 0;
if(KEY_A_IN_STATE()) new_key_state |= KEY_A;
if(KEY_B_IN_STATE()) new_key_state |= KEY_B;
if(KEY_C_IN_STATE()) new_key_state |= KEY_C;
if(KEY_D_IN_STATE()) new_key_state |= KEY_D;
/* Выделяем фронты нажатия */
key_push = new_key_state & ~cur_key_state;
cur_key_state = new_key_state;
}
/* Повторение кнопочки A */
if(key_push & KEY_A)
t_rept_a = t_ms + T_KEY_REPT_START;
if((cur_key_state & KEY_A) && ((int16_t)(t_ms - t_rept_a) >= 0)) {
t_rept_a += T_KEY_REPT_PERIOD;
key_push |= KEY_A;
}
/* Повторение кнопочки C */
if(key_push & KEY_C)
t_rept_c = t_ms + T_KEY_REPT_START;
if((cur_key_state & KEY_C) && ((int16_t)(t_ms - t_rept_c) >= 0)) {
t_rept_c += T_KEY_REPT_PERIOD;
key_push |= KEY_C;
}
return key_push | (cur_key_state << 8);
}
При опросе кнопочек приходится принимать меры для защиты от дребезга контактов, чтобы не поймать несколько фронтов на одно нажатие или отпускание кнопочки.

Простейший способ защититься от дребезга — ограничить интервал опроса кнопочки, чтобы за время нестабильности не опросить кнопочку более 1 раза. В таком случае, неважно, что вернёт нам этот опрос — программа так или иначе увидит один фронт.

Время дребезга контактов для подавляющего большинства кнопочек составляет не более 5-9 мс. Интервал опроса можно выбрать в районе 20-60 мс. Не стоит лепить интервал в сотни миллисекунд — это не даст никакой пользы, лишь будет раздражать юзера медленным откликом устройства.
При опросе мы сперва читаем состояние кнопочек в переменную new_key_state
new_key_state = 0;
if(KEY_A_IN_STATE()) new_key_state |= KEY_A;
if(KEY_B_IN_STATE()) new_key_state |= KEY_B;
if(KEY_C_IN_STATE()) new_key_state |= KEY_C;
if(KEY_D_IN_STATE()) new_key_state |= KEY_D;
С состоянием кнопочек в виде флажков, упакованных в переменную очень удобно работать — например, выделить нажатия:
key_push = new_key_state & ~cur_key_state;
В переменной key_push будут выставлены единички для тех кнопочек, которые нажаты при данном опросе и не были нажаты до него.
Затем можно записать текущее состояние кнопочек
cur_key_state = new_key_state;
Чтобы сделать повторение при удержании кнопочек заводится переменная «время следующего повторения» для каждой из кнопочек. При нажатии кнопочки записывается время первого повторения через 500 мс.
if(key_push & KEY_A)
t_rept_a = t_ms + T_KEY_REPT_START;
Затем, если кнопочка всё ещё нажата, проверятеся истекло ли это время. Если да — то повторяется нажатие кнопочки установкой флажка в переменной key_push и записывается время следующего повторения через 75 мс.
if((cur_key_state & KEY_A) && ((int16_t)(t_ms - t_rept_a) >= 0)) {
key_push |= KEY_A;
t_rept_a += T_KEY_REPT_PERIOD;
}
Обратите внимание на обновлении метки времени как «t_rept_a += T_KEY_REPT_PERIOD». При такой конструкции будет сегенерировано ровно по одному событию на 75 мс. Даже если главный цикл затупит, скажем, на 750 мс — при следующих проходах будет сгенерировано 10 «нажатий» подряд. Можно переписать как «t_rept_a = t_ms + T_KEY_REPT_PERIOD» — тогда между «нажатиями» будет не менее 75 мс. Что лучше — зависит от задачи…
Функцию read_keys можно вызываеть из главного цикла, например, как-то так
int main()
{
uint16_t key_state;
/* ... */
for(;;)
{
/* ... */
key_state = read_keys();
if(key_state & KEY_A) value++;
if(key_state & KEY_C) value--;
if(key_state & KEY_B) LED_ON();
if(key_state & KEY_D) LED_OFF();
/* ... */
}
}
Что делать НЕ надо:
- Городить развесистые автоматы, когда можно обойтись одним флажком...
- +11
- 31 июля 2018, 10:30
- Lifelover
Чет регистры какие-то странные. У ARM таких нету.
Upd.: пролистал, это для каких-то допотопных микроконтроллеров, типа AVR
Upd.: пролистал, это для каких-то допотопных микроконтроллеров, типа AVR
Интересная у вас логика, сэр, если ждёте про свои сраные армы в блоге «AVR»… Наверное, надо в следующий раз в арм писать.
Или имеется в виду, что всё должно быть про арм по умолчанию? Не дождётесь.
Или имеется в виду, что всё должно быть про арм по умолчанию? Не дождётесь.
В прицнипе, это вообще надо было в «алгоритмы» публиковать, из платформозависимого здесь только регистры таймера в примерах, сами идеи и рецепты отлично работают на любом МК, где есть таймер.
VGA +++. Процессорозависимой части здесь нет, наверное эти алгоритмы могут быть не так эффективны на других архитектурах, но познавательной значимости это их не лишает. С точки зрения ассемблерщика один и тот же алгоритм реализации флагового автомата на разных архитектурах может значительно отличаться в деталях реализации.
Но с точки зрения языка С все они могут быть описаны в одних и тех же командах. Полезная статья.
Но с точки зрения языка С все они могут быть описаны в одних и тех же командах. Полезная статья.
Задача 7: избавиться от ручного пересчёта стандартных единиц времени в тики таймера
Что делать НЕ надо: Использовать эти макросы не для констант. Пересчёт в рантайме через флоаты будет убийственно неоптимален, особенно если компилить в avr-gcc без -lm.
+ совмещено с Задачей 8: расширить разрядность таймера для организации системного тика
До внедрения такого (как в задачах 7,8) варианта обработки програмного таймера, мой исходник (с кучей моих функций, не имеющих отношение к таймерной службе) занимал ~1,5кБ. Тоесть у меня был системный тик ровно 1 мс, я в таймерах всегда прописывал задержки в мс. После того как я это переделал, исходник стал занимать 4,6кБ. Свои функции я не трогал, использовал макросы ТОЛЬКО для констант. Такое увеличение это норма при таком подходе? Старый вариант: www.dropbox.com/sh/mv806gbz9ufbtc6/AADouVCpwlRWMT7PXrOTEBooa?dl=0 Вариант после переделки, как в задачах 7,8: www.dropbox.com/sh/s75nzd5z252od6l/AAD2leiC_talRRgo0kTkrsvRa?dl=0
Что делать НЕ надо: Использовать эти макросы не для констант. Пересчёт в рантайме через флоаты будет убийственно неоптимален, особенно если компилить в avr-gcc без -lm.
+ совмещено с Задачей 8: расширить разрядность таймера для организации системного тика
До внедрения такого (как в задачах 7,8) варианта обработки програмного таймера, мой исходник (с кучей моих функций, не имеющих отношение к таймерной службе) занимал ~1,5кБ. Тоесть у меня был системный тик ровно 1 мс, я в таймерах всегда прописывал задержки в мс. После того как я это переделал, исходник стал занимать 4,6кБ. Свои функции я не трогал, использовал макросы ТОЛЬКО для констант. Такое увеличение это норма при таком подходе? Старый вариант: www.dropbox.com/sh/mv806gbz9ufbtc6/AADouVCpwlRWMT7PXrOTEBooa?dl=0 Вариант после переделки, как в задачах 7,8: www.dropbox.com/sh/s75nzd5z252od6l/AAD2leiC_talRRgo0kTkrsvRa?dl=0
Прошу прощения, в предыдущем посте я соврал. Закомментировал все 15 установок задержек в своей программе, поочередно раскоменчивал, компилил и смотрел результат. И в 1 месте я использовал макрос для передачи переменной. Одна эта строчка и давала +3 кб кода))))))))) Эмпирически подтвердил Ваше «Что делать НЕ надо». Там надо написать «НЕ НАДО !!!!!!!!!!!!!!!!111111111111» )))
А так, при использовании эти макросов для констант, я имел для каждого вызова от: «ни байта кода не добавилось» до "+32 байта" при установке задержки длительностью в 750 мс. + кое-где сжирает по 1-2 байта ОЗУ на вызов. В общем заметил, что чем меньше задержка, тем меньше кода добавляет макрос.
А так, при использовании эти макросов для констант, я имел для каждого вызова от: «ни байта кода не добавилось» до "+32 байта" при установке задержки длительностью в 750 мс. + кое-где сжирает по 1-2 байта ОЗУ на вызов. В общем заметил, что чем меньше задержка, тем меньше кода добавляет макрос.
Задача 6: увеличить диапазон задержек программного таймераНе знаю почему, но для «сраных армов» в Keil, пришлось делать приведение к uint16_t, иначе периодически возникал длительный затуп в работе таймера (светодиодик не мигал).
… Для данных большей разрядности — заменить 240 на нужный диапазон и убрать приведение к uint8_t...
Взрослый и упитанный дядя стэмщик32(со сраных армов быдлочипщик) облажался? Там же все тебе товарищ студент уже написал для 16 бит, ты что там наделал? Убрал приведение к uint8_t, а переменную определил как int или что? Что ты там наТВОРИЛ?
- well-man2000
- 15 августа 2018, 23:40
- ↑
- ↓
Откуда столько злобы в таком маленьком тельце ?)) С чего Вы взяли, что я «дядя стэмщик32»? Я вообще пики 12-16 да 8051 люблю по вечерам после работы ковырять. Я облажался? Еще как!!! Я Си только взялся изучать, и я еще столько раз облажаюсь, что просто жуть.
Нормально про «разрядность данных меньше разрядности int» я понял только после прочтения Задачи 12, в которой четко написано «Главное — не забывать приводить разность к нужному размеру если он меньше чем разрядность уменьшаемого, вычитаемого или int». А читать я ее начал гораздо позже, только после того, как попылася сделать задачи 5,6 на «сраном АРМЕ». А делать я это начал только после того, как сделал их на АВР.
Нормально про «разрядность данных меньше разрядности int» я понял только после прочтения Задачи 12, в которой четко написано «Главное — не забывать приводить разность к нужному размеру если он меньше чем разрядность уменьшаемого, вычитаемого или int». А читать я ее начал гораздо позже, только после того, как попылася сделать задачи 5,6 на «сраном АРМЕ». А делать я это начал только после того, как сделал их на АВР.
Нормально про «разрядность данных меньше разрядности int»Нет такой разрядности int (по крайней мере при говнокодировании для MCU), так что про int забудь.
- well-man2000
- 16 августа 2018, 00:49
- ↑
- ↓
Си — отличный язык, который позволяет выстрелить себе в ногу, при этом попасть в голову. С одной стороны — это требует большой внимательности и опыта от программиста, с другой — дает большую гибкость.
Если заглянуть в учебник — int занимает 2 или 4 байта, в зависимости от архитектуры и флагов компилятора. Чтобы на наступать на грабли, лучше забыть все эти int/word/char и их unsigned братьев навсегда, а использовать типы int32_t/uint32_t итп из библиотеки stdint.h. Тогда не надо будет вспоминать, сколько же там бит занимает тип в текущем проекте. Знание языков высокого уровня не освобождает от знания ассемблера и архитектуры процессора, для которого создается приложение.
Если заглянуть в учебник — int занимает 2 или 4 байта, в зависимости от архитектуры и флагов компилятора. Чтобы на наступать на грабли, лучше забыть все эти int/word/char и их unsigned братьев навсегда, а использовать типы int32_t/uint32_t итп из библиотеки stdint.h. Тогда не надо будет вспоминать, сколько же там бит занимает тип в текущем проекте. Знание языков высокого уровня не освобождает от знания ассемблера и архитектуры процессора, для которого создается приложение.
- coredumped
- 16 августа 2018, 09:45
- ↑
- ↓
Хороший набор MCU СНИППЕТОВ на Си запостил камрад Лайфловер, спецом для ASS'ногоемблерного старичья, как Гнусмас, как и для прочих более молодых 30-40-летних «дебилов» (которые младше самого языка Си !), срочно ринувшихся в последние годы в освоение стм32 говнокодирования и поставивших ворованный Кейл или же Сеггер ембеддед студио.
Интересно, что за последние 20-25 или даже 10 лет расплодилось столько прафиссиАнального и распальцованного «быдла» говнокодировавшего на паскале/дельфи/жабе/жаваскрипте/си_шарпе и т.п. и даже на C++, но не умеющего кодировать на Си, тем более для MCU.
Интересно, что за последние 20-25 или даже 10 лет расплодилось столько прафиссиАнального и распальцованного «быдла» говнокодировавшего на паскале/дельфи/жабе/жаваскрипте/си_шарпе и т.п. и даже на C++, но не умеющего кодировать на Си, тем более для MCU.
- well-man2000
- 16 августа 2018, 14:32
- ↑
- ↓
Лайф, а у меня на пивоварне до сих пор крутится это:
Я делал так:
#define F_CPU 8000000UL //Тактовая частота 8 мегагерц
unsigned long millis_ctr; //4 байта на счётчик милисекунд — сможем считать 49 дней
//(49 дней х 24 часа х 60 минут х 60 сек х 1 000 мс = 4 233 600 000 мс
//unsigned long может хранить положительные числа до 4 294 967 295 — чуть меньше 50 дней.
interrupt [TIM2_COMP] void timer2_comp_isr(void) //прерывание по совпадению счётного регистра с регистром сравнения — там 124, т.е. каждую милисекунду
{
millis_ctr++; //увеличиваем переменную на 1, через 49 с небольшим суток она обнулится, но нам этого более чем.
}
#define T_SEC(x) ((x) * 1000UL)
#define T_MIN(x) (T_SEC(x) * 60)
#define T_HOUR(x) (T_MIN(x) * 60)
enum
{
START,
SETUP,
HEAT_ON,
HEAT_OFF,
ERROR
};
void main(void)
{
unsigned long m_ctr;
// Это переменная для работы с милисекундами — переменную millis_ctr нельзя трогать, потому что в ней постоянно меняются значения. 4 байта.
unsigned long m_last_keypoll, m_heat_start, m_heat_off;
//время последнего нажатия на кнопку, время начала нагрева, время окончания нагрева — все по 4 байта, потому что туда кидаем значения из счётчика в мс
unsigned char plus_state, start_state, stop_state;
unsigned char plus_pressed, start_pressed, stop_pressed;
unsigned char state;
unsigned int liters;
ASSR=0<<AS2;
TCCR2=(0<<PWM2) | (0<<COM21) | (0<<COM20) | (0<<CTC2) | (0<<CS22) | (1<<CS21) | (1<<CS20);
TCNT2=0x00;
OCR2=0x7C;
TIMSK=(1<<OCIE2) | (0<<TOIE2) | (0<<TICIE1) | (0<<OCIE1A) | (0<<OCIE1B) | (0<<TOIE1) | (0<<TOIE0);
lcd_init(16);
#asm(«sei»);
state = START; /* начальное состояние автомата */
liters = 20;
m_last_keypoll = 0;
while (1)
{
/* считали значение счётчика для этого прохода */
#asm(«cli»);
m_ctr = millis_ctr;
#asm(«sei»);
/* каждые 20 мс выполняем опрос кнопок */
if(m_ctr — m_last_keypoll >= 20)
{
/* запомним когда последний раз опрашивали */
m_last_keypoll = m_ctr;
/* опросим кнопку "+" */
plus_pressed = !plus_state && PINB.3;
plus_state = PINB.3;
/* опросим кнопку «старт» */
start_pressed = !start_state && PINB.4;
start_state = PINB.4;
/* опросим кнопку «стоп» */
start_pressed = !stop_state && PINB.2;
stop_state = PINB.2;
}
ну и так далее.
Я делал так:
#define F_CPU 8000000UL //Тактовая частота 8 мегагерц
unsigned long millis_ctr; //4 байта на счётчик милисекунд — сможем считать 49 дней
//(49 дней х 24 часа х 60 минут х 60 сек х 1 000 мс = 4 233 600 000 мс
//unsigned long может хранить положительные числа до 4 294 967 295 — чуть меньше 50 дней.
interrupt [TIM2_COMP] void timer2_comp_isr(void) //прерывание по совпадению счётного регистра с регистром сравнения — там 124, т.е. каждую милисекунду
{
millis_ctr++; //увеличиваем переменную на 1, через 49 с небольшим суток она обнулится, но нам этого более чем.
}
#define T_SEC(x) ((x) * 1000UL)
#define T_MIN(x) (T_SEC(x) * 60)
#define T_HOUR(x) (T_MIN(x) * 60)
enum
{
START,
SETUP,
HEAT_ON,
HEAT_OFF,
ERROR
};
void main(void)
{
unsigned long m_ctr;
// Это переменная для работы с милисекундами — переменную millis_ctr нельзя трогать, потому что в ней постоянно меняются значения. 4 байта.
unsigned long m_last_keypoll, m_heat_start, m_heat_off;
//время последнего нажатия на кнопку, время начала нагрева, время окончания нагрева — все по 4 байта, потому что туда кидаем значения из счётчика в мс
unsigned char plus_state, start_state, stop_state;
unsigned char plus_pressed, start_pressed, stop_pressed;
unsigned char state;
unsigned int liters;
ASSR=0<<AS2;
TCCR2=(0<<PWM2) | (0<<COM21) | (0<<COM20) | (0<<CTC2) | (0<<CS22) | (1<<CS21) | (1<<CS20);
TCNT2=0x00;
OCR2=0x7C;
TIMSK=(1<<OCIE2) | (0<<TOIE2) | (0<<TICIE1) | (0<<OCIE1A) | (0<<OCIE1B) | (0<<TOIE1) | (0<<TOIE0);
lcd_init(16);
#asm(«sei»);
state = START; /* начальное состояние автомата */
liters = 20;
m_last_keypoll = 0;
while (1)
{
/* считали значение счётчика для этого прохода */
#asm(«cli»);
m_ctr = millis_ctr;
#asm(«sei»);
/* каждые 20 мс выполняем опрос кнопок */
if(m_ctr — m_last_keypoll >= 20)
{
/* запомним когда последний раз опрашивали */
m_last_keypoll = m_ctr;
/* опросим кнопку "+" */
plus_pressed = !plus_state && PINB.3;
plus_state = PINB.3;
/* опросим кнопку «старт» */
start_pressed = !start_state && PINB.4;
start_state = PINB.4;
/* опросим кнопку «стоп» */
start_pressed = !stop_state && PINB.2;
stop_state = PINB.2;
}
ну и так далее.
- VeniaminCaver
- 20 ноября 2018, 20:24
- ↓
Не надо забывать тег <code>. Без него малочитабельно.
И на чем ты там таком пишешь, с PINB.2? Не mikroC хоть?
#asm(«cli»);Атомарное чтение счетчика — это правильно. Но вот сам счетчик должен быть volatile.
m_ctr = millis_ctr;
#asm(«sei»);
И на чем ты там таком пишешь, с PINB.2? Не mikroC хоть?
Надо в этом говнодвижке бложика уже функцию редактирования сделать. А то твои советы — как мёртвому припарки — исправить ничего уже нельзя.
- coredumped
- 23 ноября 2018, 10:25
- ↑
- ↓
Это codevision. Ещё кто-то такое помнит? Хм теги извиняйте, давно не писал — всё забыл.
- VeniaminCaver
- 23 ноября 2018, 00:00
- ↓
Комментарии (34)
RSS свернуть / развернуть