Таймеры и задержки: сборник рецептов

AVR
Увидел в статье DI HALT "Простой программный таймер для конечных автоматов" некий кусок кода и решил накатать статью…

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();

		/* ... */
	}
}

Что делать НЕ надо:
  • Городить развесистые автоматы, когда можно обойтись одним флажком...

Комментарии (27)

RSS свернуть / развернуть
А интересно ли это кому-нибудь?
Да, интересно.
+5
+1
0
+1
0
+1
0
Ой кольцевые буферы это да… я что там только не видел.
0
Да, очень интересно.
0
Конечно интересно! Особенно про TWI.
0
Не читал, но плюсую.
0
Добавил пример с кнопочками
+1
Чет регистры какие-то странные. У ARM таких нету.
Upd.: пролистал, это для каких-то допотопных микроконтроллеров, типа AVR
0
  • avatar
  • igorp
  • 01 августа 2018, 17:37
Интересная у вас логика, сэр, если ждёте про свои сраные армы в блоге «AVR»… Наверное, надо в следующий раз в арм писать.
Или имеется в виду, что всё должно быть про арм по умолчанию? Не дождётесь.
0
В прицнипе, это вообще надо было в «алгоритмы» публиковать, из платформозависимого здесь только регистры таймера в примерах, сами идеи и рецепты отлично работают на любом МК, где есть таймер.
0
VGA +++. Процессорозависимой части здесь нет, наверное эти алгоритмы могут быть не так эффективны на других архитектурах, но познавательной значимости это их не лишает. С точки зрения ассемблерщика один и тот же алгоритм реализации флагового автомата на разных архитектурах может значительно отличаться в деталях реализации.
Но с точки зрения языка С все они могут быть описаны в одних и тех же командах. Полезная статья.
0
Задача 2: выполнять прерывание с заданным периодом
Что делать НЕ надо: Использовать CTC режим. Он похоронит второй канал и другие ресурсы таймера.
А если используется АТмега 8535,16,32 и Timer0, у которого всего 1 канал, то CTC режим режим удачное решение?
0
Счётчик и переполнение можно для системного тика использовать или ещё как-нибудь. CTC годится когда таймеров куры не клюют…
0
Задача 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
0
Опечаточка получилась. Указывамый размер конечно относится к НЕХ-файлу, а не исходнику))
0
Прошу прощения, в предыдущем посте я соврал. Закомментировал все 15 установок задержек в своей программе, поочередно раскоменчивал, компилил и смотрел результат. И в 1 месте я использовал макрос для передачи переменной. Одна эта строчка и давала +3 кб кода))))))))) Эмпирически подтвердил Ваше «Что делать НЕ надо». Там надо написать «НЕ НАДО !!!!!!!!!!!!!!!!111111111111» )))
А так, при использовании эти макросов для констант, я имел для каждого вызова от: «ни байта кода не добавилось» до "+32 байта" при установке задержки длительностью в 750 мс. + кое-где сжирает по 1-2 байта ОЗУ на вызов. В общем заметил, что чем меньше задержка, тем меньше кода добавляет макрос.
0
Макрос вообще никакого кода не добавляет. Это константное выражение, компилятор заменит его на целое число.
0
Если задержка задана переменной в миллисекундах, лучше её и использовать со вторичным счётчикам в миллисекундах, а не пересчитывать в тики…
0
Спасибо.
0
Задача 6: увеличить диапазон задержек программного таймера
… Для данных большей разрядности — заменить 240 на нужный диапазон и убрать приведение к uint8_t...
Не знаю почему, но для «сраных армов» в Keil, пришлось делать приведение к uint16_t, иначе периодически возникал длительный затуп в работе таймера (светодиодик не мигал).
0
Если разрядность данных меньше разрядности int, то нужно приводить. Сказано же в нескольких местах. В арме int — 32 бита.
0
Взрослый и упитанный дядя стэмщик32(со сраных армов быдлочипщик) облажался? Там же все тебе товарищ студент уже написал для 16 бит, ты что там наделал? Убрал приведение к uint8_t, а переменную определил как int или что? Что ты там наТВОРИЛ?
0
Откуда столько злобы в таком маленьком тельце ?)) С чего Вы взяли, что я «дядя стэмщик32»? Я вообще пики 12-16 да 8051 люблю по вечерам после работы ковырять. Я облажался? Еще как!!! Я Си только взялся изучать, и я еще столько раз облажаюсь, что просто жуть.
Нормально про «разрядность данных меньше разрядности int» я понял только после прочтения Задачи 12, в которой четко написано «Главное — не забывать приводить разность к нужному размеру если он меньше чем разрядность уменьшаемого, вычитаемого или int». А читать я ее начал гораздо позже, только после того, как попылася сделать задачи 5,6 на «сраном АРМЕ». А делать я это начал только после того, как сделал их на АВР.
0
Прошу любителей АРМов не обижатся, на самом деле они не «сраные», просто я еще до них мозгом не дорос.
0
Нормально про «разрядность данных меньше разрядности int»
Нет такой разрядности int (по крайней мере при говнокодировании для MCU), так что про int забудь.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.