Всем датагорцам привет! Продолжим изучение микроконтроллеров и языка Си. Эффективность программы можно оценить по двум критериям — размер и скорость работы. Если о занимаемой памяти мы можем пока не беспокоиться, то понимать, как быстро работает код, лучше с первых его строк. Проще всего это сделать, определив время исполнение одного круга цикла while(1), чем мы и займёмся для примера из предыдущей части статьи.
Содержание статьи / Table Of Contents
↑ Библиотека вывода
Оформим библиотеку out, управляющую одноимённым выводом нашего устройства:ATmega8
#ifndef OUT_H_
#define OUT_H_
#include <avr/io.h>
#define OUT_TOGGLE PORTB ^= (1 << PB0)
void outInit();
#endif
#include "out.h"
void outInit()
{
DDRB |= (1 << PB0);
}
STM32F401
#ifndef OUT_H_
#define OUT_H_
#include "stm32f4xx.h"
#define OUT_TOGGLE GPIOB->ODR ^= GPIO_ODR_ODR_12
void outInit();
#endif
#include "out.h"
void outInit()
{
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;
GPIOB->MODER |= GPIO_MODER_MODER12_0;
}
nRF52832
#ifndef OUT_H_
#define OUT_H_
#include "nrf.h"
#define OUT_TOGGLE NRF_GPIO->OUT ^= (GPIO_OUT_PIN25_High << GPIO_OUT_PIN25_Pos)
void outInit();
#endif
#include "out.h"
void outInit()
{
NRF_GPIO->PIN_CNF[25] |= (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos);
}
Подключим библиотеку в main.c и добавим в основную функцию main():
а) функцию outInit() — до цикла while(1),
б) макрос OUT_TOGGLE — в самом начале цикла while(1).
#include "leds.h"
#include "buttons.h"
#include "out.h"
void SystemInit() // для STM32F401 и nRF52832
{
}
int main()
{
ledsInit();
buttonsInit();
outInit();
while(1)
{
OUT_TOGGLE;
if(BUTTON_1_PUSHED && BUTTON_2_RELEASED)
{
LED_1_ON;
LED_2_OFF;
LED_3_OFF;
}
else if(BUTTON_1_RELEASED && BUTTON_2_PUSHED)
{
LED_1_OFF;
LED_2_ON;
LED_3_OFF;
}
else if(BUTTON_1_PUSHED && BUTTON_2_PUSHED)
{
LED_1_OFF;
LED_2_OFF;
LED_3_ON;
}
else
{
LED_1_OFF;
LED_2_OFF;
LED_3_OFF;
}
}
}
Откомпилировав и загрузив программу в МК, мы получим ту же реакцию на нажатие кнопок и, вдобавок, сигнал на выводе OUT с полупериодом около:
• 4 мкс — для ATmega8,
• 4 мкс — STM32F401,
• 1 мкс — для nRF52832.
В плоскости работы устройства это — период опроса кнопок при данном объёме кода цикла while(1). Обратите внимание, что на исполнение этого кода у ATmega8 и STM32F401 уходит одинаковое количество времени при двукратной разнице в частоте тактирования процессора (8 и 16МГц, соответственно). Объясняется это следующими причинами:
1. Ассемблерный код, в который преобразуется программа, для AVR-8 несколько короче, в том числе и вследствие использования прямой адресации.
2. Некоторые инструкции ARM исполняются за большее количество тактов, чем AVR-8.
Заставим оставшийся светодиод LED_4 мигать с видимой для глаза задержкой, причём последнюю реализуем двумя способами: сначала через цикл, а затем, выяснив минусы такого варианта, посредством таймера.
В Си предусмотрены два практически равнозначных вида циклов — for и уже знакомый вам while.
Общий вид цикла while следующий:
while(выражение)
инструкция
Инструкция цикла будет выполняться до тех пор, пока выражение в круглых скобках не станет ложью или, что тоже самое, равным нулю. В частности, цикл while(1) основной функции main() — бесконечный, поскольку единица никогда не обратится в ноль. Если инструкций цикла больше, чем одна, их заключают в фигурные скобки.
Синтаксис цикла for выглядит так:
for(выражение_1; выражение_2; выражение_3)
инструкция
Чаще всего, выражение_1 и выражение_2 являются начальным и конечным значениями переменной-счётчика, а выражение_3 — её инкремент или декремент. Так же, как и в случае с while, инструкции количеством более одной обрамляются фигурными скобками.
Добавим в:
а) leds.h — макроопределения количества кругов цикла для случаев включённого (LED_4_ON_DELAY) и выключенного (LED_4_OFF_DELAY) светодиода с таким расчётом, чтобы соответствующее время задержки составляло приблизительно 3 и 1 секунды.
#define LED_4_ON_DELAY 2000000 // для ATmega8, 5000000 — для STM32F401, 15000000 — для nRF52832
#define LED_4_OFF_DELAY 750000 // для ATmega8, 2000000 — для STM32F401, 6000000 — для nRF52832
б) main.c — код блинка, применив для разнообразия оба вида циклов с ассемблерной инструкцией NOP, которая обязывает процессор ничего не делать в течение одного тактового импульса.
#include "leds.h"
#include "buttons.h"
#include "out.h"
void SystemInit() // для STM32F401 и nRF52832
{
}
int main()
{
// Переменная-счётчик
unsigned long ledDelay = 0;
ledsInit();
buttonsInit();
outInit();
while(1)
{
OUT_TOGGLE;
if(BUTTON_1_PUSHED && BUTTON_2_RELEASED)
{
LED_1_ON;
LED_2_OFF;
LED_3_OFF;
}
else if(BUTTON_1_RELEASED && BUTTON_2_PUSHED)
{
LED_1_OFF;
LED_2_ON;
LED_3_OFF;
}
else if(BUTTON_1_PUSHED && BUTTON_2_PUSHED)
{
LED_1_OFF;
LED_2_OFF;
LED_3_ON;
}
else
{
LED_1_OFF;
LED_2_OFF;
LED_3_OFF;
}
// Включить светодиод LED_4
LED_4_ON;
// Задержка около 3 секунд
for(ledDelay = 0; ledDelay < LED_4_ON_DELAY; ledDelay++)
__asm("NOP");
// Выключить светодиод LED_4
LED_4_OFF;
// Задержка около 1 секунды
ledDelay = LED_4_OFF_DELAY;
while(ledDelay > 0)
{
__asm("NOP");
ledDelay--;
}
}
}
После загрузки программы в МК LED_4 будет исправно мигать, однако вы обнаружите, что:
• Полупериод сигнала на выводе OUT увеличился на 3 + 1 = 4 секунды.
• Устройство не реагирует на нажатие кнопок во время блинка. И вообще, получить такую реакцию можно только, если повезёт нажать кнопку в те микросекунды, когда процессор её опрашивает, либо надо удерживать кнопку нажатой, пока не будет достигнут нужный результат.
Очевидно, всё это обусловлено тем, что большую часть времени процессор занят бесполезным простаиванием в циклах. Отсюда — простой вывод: избегайте использования в программе циклических задержек. Могу назвать только пару случаев, когда я их применяю:
1. До цикла while(1) при условии, что время инициализации устройства не принципиально.
2. В программных версиях «медленных» протоколов обмена (1-Wire, I2C), где могут потребоваться микро- или, в крайнем случае, миллисекундные задержки.
Чтобы вернуть девайс в адекватное состояние, перепишем код, организовав задержки через таймер.
↑ Таймер
Подробную информацию о тактировании и регистрах таймера вы можете почерпнуть из четвёртой и пятой частей статьи «Ассемблер...», здесь же мы сосредоточимся на следующих его полезных свойствах:• Таймер может считать поступающие на него тактовые импульсы, перемножение периода и количества которых даёт общее время счёта.
• Тактирование таймера — независимое, т.е он работает параллельно процессору. Следовательно, можно передать функцию отсчёта временных интервалов таймеру, освободив процессор для более важных задач, например, опроса кнопок.
• По достижению заданного количества учитываемых тактовых импульсов устанавливается в единицу флаг-бит одного из регистров таймера (в частности, бит OCF1A регистра TIFR — для ATmega8, бит UIF регистра SR — для STM32F401, EVENTS_COMPARE — для nRF52832), опрашивая который мы сможем зафиксировать факт истечения требуемого отрезка времени.
Ничто не мешает настроить таймер таким образом, чтобы он отсчитывал упомянутые выше 1 и 3 секунды, но, это будет нерациональное использование ресурсов, поскольку целый таймер будет занят обслуживанием блинка единственного светодиода. Поэтому, выделим один из таймеров МК в качестве системных часов, устанавливающих флаг-бит с периодом, заведомо большим периода цикла while(1), чтобы не пропустить факт поднятия флага (к примеру, 1 мс).
Далее, посредством переменной-счётчика будем, при необходимости, считать миллисекунды и принимать решения по достижению заданного времени. Такой поход позволяет посредством одного таймера организовать тайминг множества процессов, в чём вы убедитесь чуть позже.
Алгоритм фрагмента управления светодиодом LED_4 будет следующим:
Если флаг таймера-часов поднят
сбросить флаг
если LED_4 выключен
инкрементировать переменную ledDelay
если значение ledDelay достигло 1000 (т.е. прошла 1 секунда)
обнулить ledDelay
включить LED_4
если же LED_4 включен
инкрементировать переменную ledDelay
если значение ledDelay достигло 3000 (т.е. прошло 3 секунды)
обнулить ledDelay
выключить LED_4
Реализуем это в коде, для чего в leds.h изменим значения задержек и добавим макросы, позволяющие определить текущее состояние пина LED_4:
ATmega8
#define LED_4_ON_DELAY 3000
#define LED_4_OFF_DELAY 1000
#define LED_4_IS_OFF (PINB & (1 << PB2)) != 0
#define LED_4_IS_ON (PINB & (1 << PB2)) == 0
STM32F401
#define LED_4_ON_DELAY 3000
#define LED_4_OFF_DELAY 1000
#define LED_4_IS_OFF (GPIOB->IDR & GPIO_IDR_IDR_2) != 0
#define LED_4_IS_ON (GPIOB->IDR & GPIO_IDR_IDR_2) == 0
nRF52832
#define LED_4_ON_DELAY 3000
#define LED_4_OFF_DELAY 1000
#define LED_4_IS_OFF (NRF_GPIO->OUT & (GPIO_OUT_PIN16_High << GPIO_OUT_PIN16_Pos)) != 0
#define LED_4_IS_ON (NRF_GPIO->OUT & (GPIO_OUT_PIN16_High << GPIO_OUT_PIN16_Pos)) == 0
Далее, для ATmega8 создадим библиотеку clock (и подключим её в main.c), содержащую:
• Макросы, которые определяют факт установки в 1 флаг-бита (CLOCK_FLAG_IS_SET) и сбрасывают его (CLEAR_CLOCK_FLAG).
• Функцию clockInit() настройки TIMER1.
clock.h
#ifndef CLOCK_H_
#define CLOCK_H_
#include <avr/io.h>
void clockInit();
#define CLOCK_FLAG_IS_SET TIFR & (1 << OCF1A)
#define CLEAR_CLOCK_FLAG TIFR |= (1 << OCF1A)
#endif
clock.c
#include "clock.h"
void clockInit()
{
// Установить верхний предел счёта
OCR1A = 1000;
/* Включить счётчик с делителем 8, что даст
частоту 8МГц/1000/8 = 1 кГц или период 1 мс */
TCCR1B = (1 << WGM12) | (1 << CS11);
}
В случае с STM32F401 и nRF52832 используем для настроек таймера пустующую функцию systemInit(), выше которой поместим макросы по работе с флаг-битом:
STM32F401
#define CLOCK_FLAG_IS_SET TIM2->SR & TIM_SR_UIF
#define CLEAR_CLOCK_FLAG TIM2->SR &= ~TIM_SR_UIF
void SystemInit()
{
// Включить тактирование TIMER2
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
/* Установить верхний предел счёта 1000 и деление на 16,
что даст частоту 16МГц/1000/16 = 1 кГц или период 1 мс*/
TIM2->ARR = 1000;
TIM2->PSC = 16;
// Включить счётчик
TIM2->CR1 |= TIM_CR1_CEN;
}
nRF52832
#define CLOCK_FLAG_IS_SET NRF_TIMER2->EVENTS_COMPARE[0] == 1
#define CLEAR_CLOCK_FLAG NRF_TIMER2->EVENTS_COMPARE[0] = 0
void SystemInit()
{
// Установить коэффициент деления 6
NRF_TIMER2->PRESCALER = 6;
// Установить разрядность 8 счётчика
NRF_TIMER2->BITMODE = TIMER_BITMODE_BITMODE_08Bit << TIMER_BITMODE_BITMODE_Pos;
// Включить счётчик
NRF_TIMER2->TASKS_START = 1;
}
Чтобы избежать дублирования кода, практически одинакового для всех трёх МК (за исключением вызова функции clockInit() в случае с ATmega8), с этого момента я буду приводить для main.c лишь код основной функции, подразумевая при этом обязательную инициализацию таймера в SystemInit() в случае с STM32F401 и nRF52832:
int main()
{
unsigned int ledDelay = 0;
ledsInit();
buttonsInit();
outInit();
clockInit(); // только для ATmega8
while(1)
{
OUT_TOGGLE;
if(BUTTON_1_PUSHED && BUTTON_2_RELEASED)
{
LED_1_ON;
LED_2_OFF;
LED_3_OFF;
}
else if(BUTTON_1_RELEASED && BUTTON_2_PUSHED)
{
LED_1_OFF;
LED_2_ON;
LED_3_OFF;
}
else if(BUTTON_1_PUSHED && BUTTON_2_PUSHED)
{
LED_1_OFF;
LED_2_OFF;
LED_3_ON;
}
else
{
LED_1_OFF;
LED_2_OFF;
LED_3_OFF;
}
// Если флаг таймера-часов поднят
if(CLOCK_FLAG_IS_SET)
{
// сбросить флаг
CLEAR_CLOCK_FLAG;
// если LED_4 выключен
if(LED_4_IS_OFF)
{
// инкрементировать переменную ledDelay
ledDelay++;
// если значение ledDelay достигло 1000 (т.е. прошла 1 секунда)
if(ledDelay == LED_4_OFF_DELAY)
{
// обнулить ledDelay
ledDelay = 0;
// включить LED_4
LED_4_ON;
}
}
// если же LED_4 включен
else if(LED_4_IS_ON)
{
// инкрементировать переменную ledDelay
ledDelay++;
// если значение ledDelay достигло 3000 (т.е. прошло 3 секунды)
if(ledDelay == LED_4_ON_DELAY)
{
// обнулить ledDelay
ledDelay = 0;
// выключить LED_4
LED_4_OFF;
}
}
}
}
}
Как видите, размерность ledDelay уменьшена до unsigned int, поскольку максимальное значение, принимаемое ею, теперь составляет 3000 и нет смысла занимать лишнюю операционную память под 32-битную переменную.
Загрузим программу в МК и убедимся, что:
а) Светодиод LED_4 мигает с требуемыми задержками.
б) Полупериод сигнала на выводе OUT вернулся к первоначальным значениям (4 мкс — для ATmega8 и STM32F401, 1 мкс — для nRF52832) и лишь на доли микросекунд увеличился в связи с исполнением добавленного нами кода.
в) Устройство своевременно реагирует на нажатие кнопок.
↑ Прерывания
Информацию о преимуществах использования прерываний и требованиях к их настройке для основных модулей МК можно найти в четвёртой части «Ассемблер...».Подключим в main.c библиотеку Serial и добавим в основную функцию:
• Инициализацию UART — до цикла while(1)
ledsInit();
buttonsInit();
outInit();
clockInit(); // только для ATmega8
serialBegin();
• Печать информации о состоянии кнопок — в соответствующие ветки их проверки
if(BUTTON_1_PUSHED && BUTTON_2_RELEASED)
{
serialPrintln("BUTTON_1 pushed");
LED_1_ON;
LED_2_OFF;
LED_3_OFF;
}
else if(BUTTON_1_RELEASED && BUTTON_2_PUSHED)
{
serialPrintln("BUTTON_2 pushed");
LED_1_OFF;
LED_2_ON;
LED_3_OFF;
}
else if(BUTTON_1_PUSHED && BUTTON_2_PUSHED)
{
serialPrintln("Both buttons pushed");
LED_1_OFF;
LED_2_OFF;
LED_3_ON;
}
Нажав кнопку устройства с таким кодом, вы увидите в мониторе порта поток строк, который останавливается только после отпускания кнопки. Даже короткое нажатие приводит к печати нескольких строк, поскольку длится оно миллисекунды и процессор успевает за это время сделать множество кругов цикла while(1).
Светодиоды, кстати говоря, точно так же многократно включаются и выключаются, только мы этого не замечаем. И если в случае со светодиодами на такое положением вещей можно закрыть глаза, то, например, для шагового двигателя нажатие кнопки обусловит несколько его шагов, вместо одного. Да и в целом, назвать удовлетворительной такую работу устройства затруднительно.
Обойти неприятность можно, если заставить МК реагировать не на низкий уровень сигнала на выводе кнопки, а на фронт (в нашем случае — ниспадающий, поскольку имеем дело с активно-низкой кнопкой). Но, тут возникнет ещё один требующий решения вопрос — дребезг кнопки, который длится миллисекунды и каждый его ниспадающий фронт будет трактоваться микроконтроллером как нажатие кнопки.
Учитывая изложенное:
а) Обеспечим требуемую реакцию на нажатие кнопок через соответствующее внешнее прерывание, а заодно перепишем код таймера, чтобы воспользоваться и его прерыванием.
б) Программно устраним влияние дребезга посредством задержки в несколько десятков миллисекунд после нажатия, организованной с помощью того же таймера.
Первым делом, внесём дополнения в библиотеку buttons:
ATmega8
#ifndef BUTTONS_H_
#define BUTTONS_H_
#include <avr/io.h>
#include <avr/interrupt.h>
#define BUTTON_1_PUSHED (PIND & (1 << PD2)) == 0
#define BUTTON_1_RELEASED (PIND & (1 << PD2)) != 0
#define BUTTON_1_OFF GICR &= ~(1 << INT0)
#define BUTTON_1_ON GIFR |= (1 << INTF0); GICR |= (1 << INT0)
#define BUTTON_1_FLAG_BIT 0
#define SET_BUTTON_1_FLAG buttonsFlags |= (1 << BUTTON_1_FLAG_BIT)
#define CLEAR_BUTTON_1_FLAG buttonsFlags &= ~(1 << BUTTON_1_FLAG_BIT)
#define BUTTON_2_PUSHED (PIND & (1 << PD3)) == 0
#define BUTTON_2_RELEASED (PIND & (1 << PD3)) != 0
#define BUTTON_2_OFF GICR &= ~(1 << INT1)
#define BUTTON_2_ON GIFR |= (1 << INTF1); GICR |= (1 << INT1)
#define BUTTON_2_FLAG_BIT 1
#define SET_BUTTON_2_FLAG buttonsFlags |= (1 << BUTTON_2_FLAG_BIT)
#define CLEAR_BUTTON_2_FLAG buttonsFlags &= ~(1 << BUTTON_2_FLAG_BIT)
#define ANY_BUTTON_PUSHED buttonsFlags != 0
#define BOUNCE_DELAY 30
/* Прототип функции инициализации пинов */
void buttonsInit();
/* Прототип переменной флагов */
extern volatile unsigned char buttonsFlags;
#endif
#include "buttons.h"
volatile unsigned char buttonsFlags = 0;
ISR(INT0_vect)
{
// Выключить кнопу BUTTON_1
BUTTON_1_OFF;
// Установить в 1 бит BUTTON_1_FLAG_BIT переменной buttonsFlags
SET_BUTTON_1_FLAG;
}
ISR(INT1_vect)
{
// Выключить кнопу BUTTON_2
BUTTON_2_OFF;
// Установить в 1 бит BUTTON_2_FLAG_BIT переменной buttonsFlags
SET_BUTTON_2_FLAG;
}
void buttonsInit()
{
// Настроить пины PD3 и PD2 как входы
DDRD &= ~((1 << PD3) | (1 << PD2));
// и подтянуть к питанию
PORTD |= (1 << PD3) | (1 << PD2);
// Выбрать триггером прерывания ниспадающий фронт
MCUCR = (1 << ISC11) | (1 << ISC01);
// Разрешить прерывания локально
BUTTON_1_ON;
BUTTON_2_ON;
// и глобально
sei();
}
STM32F401
#ifndef BUTTONS_H_
#define BUTTONS_H_
#include "stm32f4xx.h"
#define BUTTON_1_PUSHED (GPIOA->IDR & GPIO_IDR_IDR_9) == 0
#define BUTTON_1_RELEASED (GPIOA->IDR & GPIO_IDR_IDR_9) != 0
#define BUTTON_1_OFF EXTI->IMR &= ~EXTI_IMR_MR9
#define BUTTON_1_ON EXTI->IMR |= EXTI_IMR_MR9
#define BUTTON_1_FLAG_BIT 0
#define SET_BUTTON_1_FLAG buttonsFlags |= (1 << BUTTON_1_FLAG_BIT)
#define CLEAR_BUTTON_1_FLAG buttonsFlags &= ~(1 << BUTTON_1_FLAG_BIT)
#define BUTTON_2_PUSHED (GPIOA->IDR & GPIO_IDR_IDR_10) == 0
#define BUTTON_2_RELEASED (GPIOA->IDR & GPIO_IDR_IDR_10) != 0
#define BUTTON_2_OFF EXTI->IMR &= ~EXTI_IMR_MR10
#define BUTTON_2_ON EXTI->IMR |= EXTI_IMR_MR10
#define BUTTON_2_FLAG_BIT 1
#define SET_BUTTON_2_FLAG buttonsFlags |= (1 << BUTTON_2_FLAG_BIT)
#define CLEAR_BUTTON_2_FLAG buttonsFlags &= ~(1 << BUTTON_2_FLAG_BIT)
#define ANY_BUTTON_PUSHED buttonsFlags != 0
#define BOUNCE_DELAY 30
/* Прототип функции инициализации пинов */
void buttonsInit();
/* Прототип переменной флагов */
extern volatile unsigned char buttonsFlags;
#endif
#include "buttons.h"
volatile unsigned char buttonsFlags = 0;
void EXTI9_5_IRQHandler()
{
EXTI->PR |= EXTI_PR_PR9;
// Выключить кнопу BUTTON_1
BUTTON_1_OFF;
// Установить в 1 бит BUTTON_1_FLAG_BIT переменной buttonsFlags
SET_BUTTON_1_FLAG;
}
void EXTI15_10_IRQHandler()
{
EXTI->PR |= EXTI_PR_PR10;
// Выключить кнопу BUTTON_2
BUTTON_2_OFF;
// Установить в 1 бит BUTTON_2_FLAG_BIT переменной buttonsFlags
SET_BUTTON_2_FLAG;
}
void buttonsInit()
{
// Включить тактирование порта A
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
// Настроить пины PA10 и PA9 как входы
GPIOA->MODER &= ~(GPIO_MODER_MODER10 | GPIO_MODER_MODER9);
// и подтянуть к питанию
GPIOA->PUPDR |= GPIO_PUPDR_PUPDR10_0 | GPIO_PUPDR_PUPDR9_0;
// Включить тактирование модуля SYSCFG
RCC->AHB2ENR |= RCC_APB2ENR_SYSCFGEN;
// Выбрать пины PA10-9 как источники прерываний EXTI10-9
SYSCFG->EXTICR[3] |= SYSCFG_EXTICR3_EXTI10_PA | SYSCFG_EXTICR3_EXTI9_PA;
// Выбрать триггером прерывания ниспадающий фронт
EXTI->FTSR |= EXTI_FTSR_TR10 | EXTI_FTSR_TR9;
// Разрешить прерывания локально
EXTI->IMR |= EXTI_IMR_MR10 | EXTI_IMR_MR9;
// и глобально
NVIC_EnableIRQ(EXTI15_10_IRQn);
NVIC_EnableIRQ(EXTI9_5_IRQn);
}
nRF52832
#ifndef BUTTONS_H_
#define BUTTONS_H_
#include "nrf.h"
#define BUTTON_1_PUSHED (NRF_GPIO->IN & (GPIO_IN_PIN14_High << GPIO_IN_PIN14_Pos)) == 0
#define BUTTON_1_RELEASED (NRF_GPIO->IN & (GPIO_IN_PIN14_High << GPIO_IN_PIN14_Pos)) != 0
#define BUTTON_1_OFF NRF_GPIOTE->INTENSET &= ~(GPIOTE_INTENSET_IN1_Enabled << GPIOTE_INTENSET_IN1_Pos)
#define BUTTON_1_ON NRF_GPIOTE->EVENTS_IN[1] = 0; NRF_GPIOTE->INTENSET |= (GPIOTE_INTENSET_IN1_Enabled << GPIOTE_INTENSET_IN1_Pos)
#define BUTTON_1_FLAG_BIT 0
#define SET_BUTTON_1_FLAG buttonsFlags |= (1 << BUTTON_1_FLAG_BIT)
#define CLEAR_BUTTON_1_FLAG buttonsFlags &= ~(1 << BUTTON_1_FLAG_BIT)
#define BUTTON_2_PUSHED (NRF_GPIO->IN & (GPIO_IN_PIN13_High << GPIO_IN_PIN13_Pos)) == 0
#define BUTTON_2_RELEASED (NRF_GPIO->IN & (GPIO_IN_PIN13_High << GPIO_IN_PIN13_Pos)) != 0
#define BUTTON_2_OFF NRF_GPIOTE->INTENSET &= ~(GPIOTE_INTENSET_IN2_Enabled << GPIOTE_INTENSET_IN2_Pos)
#define BUTTON_2_ON NRF_GPIOTE->EVENTS_IN[2] = 0; NRF_GPIOTE->INTENSET |= (GPIOTE_INTENSET_IN2_Enabled << GPIOTE_INTENSET_IN2_Pos)
#define BUTTON_2_FLAG_BIT 0
#define SET_BUTTON_2_FLAG buttonsFlags |= (1 << BUTTON_2_FLAG_BIT)
#define CLEAR_BUTTON_2_FLAG buttonsFlags &= ~(1 << BUTTON_2_FLAG_BIT)
#define ANY_BUTTON_PUSHED buttonsFlags != 0
#define BOUNCE_DELAY 30
/* Прототип функции инициализации пинов */
void buttonsInit();
/* Прототип переменной флагов */
extern volatile unsigned char buttonsFlags;
#endif
#include "buttons.h"
volatile unsigned char buttonsFlags = 0;
void GPIOTE_IRQHandler()
{
if(NRF_GPIOTE->EVENTS_IN[1])
{
NRF_GPIOTE->EVENTS_IN[1] = 0;
// Выключить кнопу BUTTON_1
BUTTON_1_OFF;
// Установить в 1 бит BUTTON_1_FLAG_BIT переменной buttonsFlags
SET_BUTTON_1_FLAG;
}
else if(NRF_GPIOTE->EVENTS_IN[2])
{
NRF_GPIOTE->EVENTS_IN[2] = 0;
// Выключить кнопу BUTTON_2
BUTTON_2_OFF;
// установить в 1 бит BUTTON_2_FLAG_BIT переменной buttonsFlags
SET_BUTTON_2_FLAG;
}
}
void buttonsInit()
{
// Настроить как вход и подтянуть к питанию пин P0.13
NRF_GPIO->PIN_CNF[13] = (GPIO_PIN_CNF_DIR_Input << GPIO_PIN_CNF_DIR_Pos) | (GPIO_PIN_CNF_PULL_Pullup << GPIO_PIN_CNF_PULL_Pos);
// Выбрать триггером прерывания ниспадающий фронт
NRF_GPIOTE->CONFIG[2] = (GPIOTE_CONFIG_MODE_Event << GPIOTE_CONFIG_MODE_Pos) |
(GPIOTE_CONFIG_POLARITY_HiToLo << GPIOTE_CONFIG_POLARITY_Pos) |
(13 << GPIOTE_CONFIG_PSEL_Pos);
// Настроить как вход и подтянуть к питанию пин P0.14
NRF_GPIO->PIN_CNF[14] = (GPIO_PIN_CNF_DIR_Input << GPIO_PIN_CNF_DIR_Pos) | (GPIO_PIN_CNF_PULL_Pullup << GPIO_PIN_CNF_PULL_Pos);
// Выбрать триггером прерывания ниспадающий фронт
NRF_GPIOTE->CONFIG[1] = (GPIOTE_CONFIG_MODE_Event << GPIOTE_CONFIG_MODE_Pos) |
(GPIOTE_CONFIG_POLARITY_HiToLo << GPIOTE_CONFIG_POLARITY_Pos) |
(14 << GPIOTE_CONFIG_PSEL_Pos);
// Разрешить прерывания локально
BUTTON_1_ON;
BUTTON_2_ON;
// и глобально
NVIC_EnableIRQ(GPIOTE_IRQn);
}
Поясню некоторые моменты:
1. В с-файлах объявлена переменная buttonsFlags, 0-й и 1-й биты которой будут служить флагами, оповещающими о нажатии кнопок BUTTON_1 и BUTTON_2, соответственно. Ключевое слово volatile принуждает компилятор использовать актуальное значение, хранящееся в самой переменной, а не в РОН. До этого момента мы использовали локальные переменные, видимость которых ограничивается фигурными скобкам, внутри которых они объявлены. В случае, когда нужно, чтобы переменная стала доступной из другого файла (в нашем случае — из main.c), т.е. была глобальной, необходимо:
• объявлять её вне кода, в самом верху с-файла,
• в хэдер-файле помещать прототип переменной, предваряемый ключевым словом extern.
2. В функцию инициализации выводов, обслуживающих кнопки, добавлены фрагменты выбора ниспадающего фронта в качестве триггера прерываний, а также локального и глобального разрешения последних.
3. В обработчиках отключается кнопка посредством локального запрещения прерывания, а также устанавливается в 1 соответствующий бит buttonsFlags.
4. В хэдер-файлы добавлены:
• макроопределения битов buttonsFlags, времени дребезга кнопок,
• макросы для включения/выключения кнопок, установки/сброса битов buttonsFlags, определения текущего состояния кнопок (ANY_BUTTON_PUSHED).
Далее необходимо подкорректировать код, связанный с таймером-часами:
ATmega8
#ifndef CLOCK_H_
#define CLOCK_H_
#include <avr/io.h>
#include <avr/interrupt.h>
#define LED_DELAY_FLAG 0
#define LED_DELAY_FLAG_IS_SET (clockFlags & (1 << LED_DELAY_FLAG)) != 0
#define CLEAR_LED_DELAY_FLAG clockFlags &= ~(1 << LED_DELAY_FLAG)
#define BUTTON_DELAY_FLAG 1
#define BUTTON_DELAY_FLAG_IS_SET (clockFlags & (1 << BUTTON_DELAY_FLAG)) != 0
#define CLEAR_BUTTON_DELAY_FLAG clockFlags &= ~(1 << BUTTON_DELAY_FLAG)
void clockInit();
extern volatile unsigned char clockFlags;
#endif
#include "clock.h"
volatile unsigned char clockFlags = 0;
ISR(TIMER1_COMPA_vect)
{
// Установить в 1 все биты clockFlags
clockFlags = 0xff;
}
void clockInit()
{
// Установить верхний предел счёта
OCR1A = 1000;
// Разрешить прерывание локально
TIMSK = (1 << OCIE1A);
// и глобально
sei();
/* Включить счётчик с делителем 8, что даст
частоту 8МГц/1000/8 = 1 кГц или период 1 мс */
TCCR1B = (1 << WGM12) | (1 << CS11);
}
STM32F401
#define LED_DELAY_FLAG_BIT 0
#define LED_DELAY_FLAG_IS_SET (clockFlags & (1 << LED_DELAY_FLAG_BIT)) != 0
#define CLEAR_LED_DELAY_FLAG clockFlags &= ~(1 << LED_DELAY_FLAG_BIT)
#define BUTTON_DELAY_FLAG_BIT 1
#define BUTTON_DELAY_FLAG_IS_SET (clockFlags & (1 << BUTTON_DELAY_FLAG_BIT)) != 0
#define CLEAR_BUTTON_DELAY_FLAG clockFlags &= ~(1 << BUTTON_DELAY_FLAG_BIT)
volatile unsigned char clockFlags = 0;
void TIM2_IRQHandler()
{
TIM2->SR &= ~TIM_SR_UIF;
// Установить в 1 все биты clockFlags
clockFlags = 0xff;
}
void SystemInit()
{
// Включить тактирование TIMER2
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
/* Установить верхний предел счёта 1000 и деление на 16,
что даст частоту 16МГц/1000/16 = 1 кГц или период 1 мс*/
TIM2->PSC = 16;
TIM2->ARR = 1000;
// Разрешить прерывание локально
TIM2->DIER |= TIM_DIER_UIE;
// и глобально
NVIC_EnableIRQ(TIM2_IRQn);
// Включить счётчик
TIM2->CR1 |= TIM_CR1_CEN;
}
nRF52832
#define LED_DELAY_FLAG_BIT 0
#define LED_DELAY_FLAG_IS_SET (clockFlags & (1 << LED_DELAY_FLAG_BIT)) != 0
#define CLEAR_LED_DELAY_FLAG clockFlags &= ~(1 << LED_DELAY_FLAG_BIT)
#define BUTTON_DELAY_FLAG_BIT 1
#define BUTTON_DELAY_FLAG_IS_SET (clockFlags & (1 << BUTTON_DELAY_FLAG_BIT)) != 0
#define CLEAR_BUTTON_DELAY_FLAG clockFlags &= ~(1 << BUTTON_DELAY_FLAG_BIT)
volatile unsigned char clockFlags = 0;
void TIMER2_IRQHandler(void)
{
NRF_TIMER2->EVENTS_COMPARE[0] = 0;
// Установить в 1 все биты clockFlags
clockFlags = 0xff;
}
void SystemInit()
{
// Установить коэффициент деления 6
NRF_TIMER2->PRESCALER = 6;
// Установить разрядность 8 счётчика
NRF_TIMER2->BITMODE = TIMER_BITMODE_BITMODE_08Bit << TIMER_BITMODE_BITMODE_Pos;
// Разрешить прерывание локально
NRF_TIMER2->INTENSET |= TIMER_INTENSET_COMPARE0_Enabled << TIMER_INTENSET_COMPARE0_Pos;
// и глобально
NVIC_EnableIRQ(TIMER2_IRQn);
NRF_TIMER2->TASKS_START = 1;
}
Отмечу, что:
1. Теперь мы будем отслеживать миллисекундные интервалы не через флаг-бит регистра таймера, а с помощью глобальной переменной clockFlags. На данный момент тип переменной — unsigned char, т.е. она позволяет организовать тайминг восьми процессов.
2. В код инициализации таймера добавлено локальное и глобальное разрешение прерывания.
3. Обработчик прерывания устанавливает биты clockFlags, оповещая все обслуживаемые переменной процессы об истечении 1 миллисекунды. Вообще, желательно максимально ограничивать код любого обработчика прерываний, например, как в нашем случае, установкой какого-либо флага, поскольку при генерации прерывания процессор немедленно переходит в его обработчик и чем больше размер последнего, там дольше простаивают другие процессы. К исключениям относятся случаи, когда критически важно время отклика процессора. Например, в программном варианте протокола UART код приёма байта полностью прописывается в обработчике внешнего прерывания RX-пина во избежание искажений данных.
В итоге, код основной функции примет следующий вид:
int main()
{
unsigned int ledDelay = 0;
unsigned char buttonDelay = 0;
ledsInit();
buttonsInit();
outInit();
clockInit(); // только для ATmega8
serialBegin();
while(1)
{
OUT_TOGGLE;
// Если нажата любая из кнопок, т.е. установлен в 1 один из битов переменной buttonsFlags
if(ANY_BUTTON_PUSHED)
{
// если установлен в 1 бит BUTTON_DELAY_FLAG_BIT переменной clockFlags
if(BUTTON_DELAY_FLAG_IS_SET)
{
// обнулить бит BUTTON_DELAY_FLAG_BIT переменной clockFlags
CLEAR_BUTTON_DELAY_FLAG;
// инкрементировать переменную buttonDelay
buttonDelay++;
// если значение buttonDelay достигло 30
if(buttonDelay == BOUNCE_DELAY)
{
// обнулить buttonDelay
buttonDelay = 0;
// если нажата кнопка BUTTON_1
if(BUTTON_1_PUSHED && BUTTON_2_RELEASED)
{
// вывести на печать соответствующую строку
serialPrintln("BUTTON_1 pushed");
// обнулить бит BUTTON_1_FLAG_BIT переменной buttonsFlags
CLEAR_BUTTON_1_FLAG;
// включить кнопку BUTTON_1
BUTTON_1_ON;
}
// если же нажата кнопка BUTTON_2
else if(BUTTON_1_RELEASED && BUTTON_2_PUSHED)
{
// вывести на печать соответствующую строку
serialPrintln("BUTTON_2 pushed");
// обнулить бит BUTTON_2_FLAG_BIT переменной buttonsFlags
CLEAR_BUTTON_2_FLAG;
// включить кнопку BUTTON_2
BUTTON_2_ON;
}
// если же нажаты обе кнопки
else if(BUTTON_1_PUSHED && BUTTON_2_PUSHED)
{
// вывести на печать соответствующую строку
serialPrintln("Both buttons pushed");
// обнулить оба бита переменной buttonsFlags
CLEAR_BUTTON_1_FLAG;
CLEAR_BUTTON_2_FLAG;
// включить обе кнопки
BUTTON_1_ON;
BUTTON_2_ON;
}
// если же это был дребезг
else
{
// обнулить оба бита переменной buttonsFlags
CLEAR_BUTTON_1_FLAG;
CLEAR_BUTTON_2_FLAG;
// включить обе кнопки
BUTTON_1_ON;
BUTTON_2_ON;
}
}
}
}
// Если установлен в 1 бит LED_DELAY_FLAG_BIT переменной clockFlags
if(LED_DELAY_FLAG_IS_SET)
{
// обнулить бит LED_DELAY_FLAG_BIT переменной clockFlags
CLEAR_LED_DELAY_FLAG;
// если LED_4 выключен
if(LED_4_IS_OFF)
{
// инкрементировать переменную ledDelay
ledDelay++;
// если значение ledDelay достигло 1000 (т.е. прошла 1 секунда)
if(ledDelay == LED_4_OFF_DELAY)
{
// обнулить ledDelay
ledDelay = 0;
// включить LED_4
LED_4_ON;
}
}
// если же LED_4 включен
else if(LED_4_IS_ON)
{
// инкрементировать переменную ledDelay
ledDelay++;
// если значение ledDelay достигло 3000 (т.е. прошло 3 секунды)
if(ledDelay == LED_4_ON_DELAY)
{
// обнулить ledDelay
ledDelay = 0;
// выключить LED_4
LED_4_OFF;
}
}
}
}
}
Как видите:
1. Для учёта времени дребезга кнопки выделена отдельная переменная buttonDelay. Сделано это по той причине, что процессы отсчёта задержек блинка светодиода и дребезга могут протекать одновременно. В противном случае можно использовать одну переменную-счётчик.
2. В ветке, отражающей отпущенное состояние обеих кнопок, отсутствует фрагмент выключения всех светодиодов, поскольку теперь устройство реагирует на фронт сигнала на пинах BUTTON_1 и BUTTON_2, а не низкий уровень и указанный фрагмент будет выключать светодиоды через миллисекунду после их включения так, что нам будет казаться, будто светодиоды всегда выключены и не реагируют на нажатие кнопок.
3. Практически каждая строка программы прокомментирована, поэтому нет необходимости, как мне кажется, в пояснениях.
Дополнительным бонусом от применения прерываний является сокращение времени круга цикла while(1):
• менее 2 мкс — для ATmega8,
• менее 2 мкс — для STM32F401,
• около 560 нс — для nRF52832.
Обусловлен этот факт тем, что теперь проверка состояния кнопок происходит не на каждом круге цикла, а только по факту нажатия какой-либо из них и то лишь единожды — после истечения времени дребезга.
↑ Файлы
🎁Исходники: codes-part-03.zip 88.36 Kb ⇣ 29Спасибо за внимание!
Продолжение следует!