В начало | Зарегистрироваться | Заказать наши киты почтой
 
 
 
 

Программирование микроконтроллеров на языке C. Часть 3

📆5 июля 2023   ✒️erbol   🔎1.409   💬0  
Программирование микроконтроллеров на языке C. Часть 3

Всем датагорцам привет! Продолжим изучение микроконтроллеров и языка Си. Эффективность программы можно оценить по двум критериям — размер и скорость работы. Если о занимаемой памяти мы можем пока не беспокоиться, то понимать, как быстро работает код, лучше с первых его строк. Проще всего это сделать, определив время исполнение одного круга цикла 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 ⇣ 22

Спасибо за внимание!
Продолжение следует!
 

Читательское голосование

Нравится

Статью одобрили 15 читателей.

Для участия в голосовании зарегистрируйтесь и войдите на сайт с вашими логином и паролем.
 

Поделись с друзьями!

 

 

Связанные материалы

 

Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 2. Шаблонные файлы и инструкции МК... В предыдущей части статьи мы провели подготовительную работу и вкратце разобрали принципы работы...
Схема на Датагоре. Новая статья Программирование микроконтроллеров на языке C. Часть 2... Добрый день, уважаемые камрады-датагорцы! Сегодня, рассмотрев некоторые общие моменты, мы займёмся...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 5. Периферия МК.... Сегодня мы рассмотрим работу следующих модулей периферии: • порта ввода-вывода, • таймера •...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 3. Макросы и функции... Привет, датагорцы — любители Ассемблера! В пункте 2.5.2 «Инструкции условного перехода» предыдущей...
Схема на Датагоре. Новая статья Програмирование в AVR Studio 5 с самого начала. Часть 8... Перейдем к изучению встроенных таймеров. Изучение прерываний и особенно таймеров в...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 7. Компиляция, отладка, загрузка... Привет датагорцам и гостям нашего кибер-города! В предыдущих частях материала по Ассемблеру...
Схема на Датагоре. Новая статья Программирование микроконтроллеров в AtmelStudio 6. Часть 2. Одна программа на разных языках.... Для радиолюбителей, которые до определенного времени не использовали микроконтроллеры в своих...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 4. Система адресации памяти, назначение выводов, тактирование и прерывания МК... Привет датагорцам! Сегодня мы остановимся на следующих вопросах касательно рассматриваемых нами...
Схема на Датагоре. Новая статья Визуализация для микроконтроллера. Часть 5. Графика... В предыдущих частях статьи нами более или менее подробно были рассмотрены основные принципы работы...
Схема на Датагоре. Новая статья Визуализация для микроконтроллера. Часть 3. TFT дисплей 2.8" (240х320) на ILI9341... Битва за урожай закончена, можно продолжить повествование. Полноцветный TFT-дисплей 240×320 ILI9341...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 6. Протоколы обмена данными I2C и SPI... В проекте из предыдущей части нашей ассемблерной эпопеи мы подключали к микроконтроллеру светодиод...
Схема на Датагоре. Новая статья Электронные часы-термометр с беспроводным датчиком через радиомодуль nRF24L01... Здравствуйте, уважаемые Датагорцы! Представляю вашему вниманию электронные часы с функцией...
 

Комментарии, вопросы, ответы, дополнения, отзывы

 

Добавить комментарий, вопрос, отзыв 💬

Камрады, будьте дружелюбны, соблюдайте правила!

  • Смайлы и люди
    Животные и природа
    Еда и напитки
    Активность
    Путешествия и места
    Предметы
    Символы
    Флаги
 
 
В начало | Зарегистрироваться | Заказать наши киты почтой