На примере микроконтроллера К1986ВЕ92QI (MDR32F9Q2I) и отладочной платы LDM-K1986BE92QI рассмотрим применение прерываний и системного таймера, описанных в пунктах 12, 29 и 30 спецификации на МК.
В предыдущих примерах мы моргали светодиодом делая задержку пустым вращением цикла for. Это не есть хорошо, так как ресурсы процессора можно и нужно использовать для более полезных вещей.
Тут нам на помощь приходят прерывания - механизм вызова функции, которая вызывается по каким-то внешним или внутренним событиям. Таким образом, до прихода события, процессор может заниматься выполнением полезной работы, как только событие произойдет, процессор сохраняет свой контекст (значения) и выполняет функцию прерывания. По завершении выполнения функции прерывания, процессор продолжает выполнять свою полезную работу, как будто ничего его и не прерывало (ну кроме задержки на выполнения функции прерывания).
Для включения механизма прерываний нужно:
1) Вызывать функцию NVIC_EnableIRQ(), в качестве единственного параметра которой передать ей одно из предопределенных макроопределений с индексом прерывания.
2) Определить функцию предопределенного обработчика прерываний.
3) Настроить периферию МК, в приведенном ниже коде примера, это делается в функции init_SysTick() и там же выполняется пункт 1 - вызов NVIC_EnableIRQ().
Алгоритм поиска нужных наименований:
1) Вариант 1. В IDE Keil нажать Ctrl + F и в появившемся диалоговом окне поиска в поле "Find what:" вставить SysTick_IRQn, а в поле "Look in" выбрать в списке "Current Project" и нажимая кнопку "Find Next" поискать рядом подходящие прерывания и обработчики. Повторить поиск для "SysTick_Handler".
2) Вариант2. Воспользоваться таблицей ниже.
Таблица 1. Наименования для вызова всех имеющихся прерываний МК К1986ВЕ92QI (MDR32F9Q2I).
id | Макроопределение прерывания параметр NVIC_EnableIRQ() | Функция обработчика прерывания |
---|---|---|
__initial_sp | ||
Reset_Handler | ||
-14 | NonMaskableInt_IRQn | NMI_Handler |
-13 | HardFault_IRQn | HardFault_Handler |
-12 | MemoryManagement_IRQn | MemManage_Handler |
-11 | BusFault_IRQn | BusFault_Handler |
-9 (-10?) | UsageFault_IRQn | UsageFault_Handler |
-5 | SVCall_IRQn | SVC_Handler |
-4 | DebugMon_Handler | |
-2 | PendSV_IRQn | PendSV_Handler |
-1 | SysTick_IRQn | SysTick_Handler |
0 | CAN1_IRQn | CAN1_IRQHandler |
1 | CAN2_IRQn | CAN2_IRQHandler |
2 | USB_IRQn | USB_IRQHandler |
5 | DMA_IRQn | DMA_IRQHandler |
6 | UART1_IRQn | UART1_IRQHandler |
7 | UART2_IRQn | UART2_IRQHandler |
8 | SSP1_IRQn | SSP1_IRQHandler |
10 | I2C_IRQn | I2C_IRQHandler |
11 | POWER_IRQn | POWER_IRQHandler |
12 | WWDG_IRQn | WWDG_IRQHandler |
14 | Timer1_IRQn | Timer1_IRQHandler |
15 | Timer2_IRQn | Timer2_IRQHandler |
16 | Timer3_IRQn | Timer3_IRQHandler |
17 | ADC_IRQn | ADC_IRQHandler |
19 | COMPARATOR_IRQn | COMPARATOR_IRQHandler |
20 | SSP2_IRQn | SSP2_IRQHandler |
27 | BACKUP_IRQn | BACKUP_IRQHandler |
28 | EXT_INT1_IRQn | EXT_INT1_IRQHandler |
29 | EXT_INT2_IRQn | EXT_INT2_IRQHandler |
30 | EXT_INT3_IRQHandler | |
31 | EXT_INT4_IRQn | EXT_INT4_IRQHandler |
Пример программы использующей прерывание по системному таймеру для моргания светодиодом.
// Моргаем светодиодом по прерыванию
#include "MDR32F9Q2I.h" // Заголовок с наименованиями CMSIS
#define PIN_LED 3 // Номер вывода светодиода
#define PORT_LED MDR_PORTB // Порт светодиода
void SysTick_Handler(void); // Прототип обработчика прерывания (чтобы убрать предупреждения Keil)
static void init_GPIO(void){ // Инициализация порта
MDR_RST_CLK->PER_CLOCK |= (1 << 22); // Разрешение тактирования порта B (п.14.7.8)
PORT_LED->OE |= (1 << PIN_LED); // Вход или выход. 0 - вход, 1 - выход (п.16.1.2)
PORT_LED->ANALOG |= (1 << PIN_LED); // Аналог или цифра. 0 - аналоговый, 1 - цифровой (п.16.1.4) (п.4 т.2)
PORT_LED->PWR |= (1 << PIN_LED * 2); // Передатчик. 0 - отключен, 1 - медленный, 2 - средний, 3 - быстрый (п.16.1.7)
}
static void init_SysTick(void){ // Инициализация системного таймера SysTick
SysTick->CTRL = ( // Параметры системного таймера (п.12.1.1)
(1 << 0x00) // Работа таймера. 0 - отключен, 1 - включен
|(1 << 0x01) // Разрешение прерывания по достижению счетчика. 0 - запрещено, 1 - разрешено
|(1 << 0x02) // Источник тактового сигнала. 0 - LSI (внутренний RC 40 кГц), 1 - HCLK (системная частота)
);
SysTick->LOAD = 8000000 - 1; // Загружаем значение счетчика (для 1 секунды, исходя из тактовой частоты 8 МГц) (п.12.1.2)
NVIC_EnableIRQ(SysTick_IRQn); // Разрешаем прерывание от SysTick
}
int main(void)
{
init_GPIO(); // Инициализация порта
init_SysTick(); // Инициализация системного таймера SysTick
while(1){ // Основной бесконечный цикл. В нем делаем полезную работу.
}
}
void SysTick_Handler(void){ // Обработчик прерывания
PORT_LED->RXTX ^= (1 << PIN_LED); // Инвертировать значение вывода (п.16.1.1)
}
На практике, в программе, требуются множество различных временных отсчетов, а таймеров в МК не так много. Один из подходов это настроить таймер на достаточно маленький отсчет времени, например 1 мс, а в обработчике прерывания таймера проверять требуемые отсчеты времени использую счетчики или операцию деления по модулю.
В программе ниже я приведу оба этих метода. Следует помнить что в часе 3600 секунд, а в сутках 3600 * 24 = 86 400 секунд (или 86 400 000 миллисекунд). Максимальное значение счетчика типа unsigned int 4 294 967 295 и примерно через 49 суток непрерывной работы ПО возникнет переполнение счетчика - переход через максимальное значение и опять счет от нуля, в этот момент времени могут возникать неправильные интервалы времени. А при использовании в ПО значений счетчика, с последующим их вычитанием для получения интервала времени и более грубые ошибки.
// Моргаем четырьмя светодиодами с разной частотой по прерыванию системного таймера
#include "MDR32F9Q2I.h" // Заголовок с наименованиями CMSIS
// Макроопределения портов и выводов светодиодов. Если нужно будет их поменять, это достаточно будет сделать здесь, а не лазить по всему коду.
#define PIN_LED_1 0 // Номер вывода светодиода 1
#define PORT_LED_1 MDR_PORTB // Порт светодиода 1
#define PIN_LED_2 1 // Номер вывода светодиода 2
#define PORT_LED_2 MDR_PORTB // Порт светодиода 2
#define PIN_LED_3 2 // Номер вывода светодиода 3
#define PORT_LED_3 MDR_PORTB // Порт светодиода 3
#define PIN_LED_4 3 // Номер вывода светодиода 4
#define PORT_LED_4 MDR_PORTB // Порт светодиода 4
static uint32_t systick_counter = 1; // Счетчик миллисекунд. Ставим в 1 чтобы не было срабатывания при опереции % в первом прерывании.
const static uint16_t TIME_LED_1 = 100; // Через сколько миллисекунд менять цвет светодиода 1
const static uint16_t TIME_LED_2 = 230; // Через сколько миллисекунд менять цвет светодиода 2
const static uint16_t TIME_LED_3 = 410; // Через сколько миллисекунд менять цвет светодиода 3
const static uint16_t TIME_LED_4 = 670; // Через сколько миллисекунд менять цвет светодиода 4
void SysTick_Handler(void); // Прототип обработчика прерывания (чтобы убрать предупреждения Keil)
static void init_GPIO(void){ // Инициализация порта
MDR_RST_CLK->PER_CLOCK |= (1 << 22); // Разрешение тактирования порта B (п.14.7.8)
PORT_LED_1->OE |= (1 << PIN_LED_1); // Вход или выход. 0 - вход, 1 - выход (п.16.1.2)
PORT_LED_1->ANALOG |= (1 << PIN_LED_1); // Аналог или цифра. 0 - аналоговый, 1 - цифровой (п.16.1.4) (п.4 т.2)
PORT_LED_1->PWR |= (1 << PIN_LED_1 * 2); // Передатчик. 0 - отключен, 1 - медленный, 2 - средний, 3 - быстрый (п.16.1.7)
PORT_LED_2->OE |= (1 << PIN_LED_2); // Вход или выход. 0 - вход, 1 - выход (п.16.1.2)
PORT_LED_2->ANALOG |= (1 << PIN_LED_2); // Аналог или цифра. 0 - аналоговый, 1 - цифровой (п.16.1.4) (п.4 т.2)
PORT_LED_2->PWR |= (1 << PIN_LED_2 * 2); // Передатчик. 0 - отключен, 1 - медленный, 2 - средний, 3 - быстрый (п.16.1.7)
PORT_LED_3->OE |= (1 << PIN_LED_3); // Вход или выход. 0 - вход, 1 - выход (п.16.1.2)
PORT_LED_3->ANALOG |= (1 << PIN_LED_3); // Аналог или цифра. 0 - аналоговый, 1 - цифровой (п.16.1.4) (п.4 т.2)
PORT_LED_3->PWR |= (1 << PIN_LED_3 * 2); // Передатчик. 0 - отключен, 1 - медленный, 2 - средний, 3 - быстрый (п.16.1.7)
PORT_LED_4->OE |= (1 << PIN_LED_4); // Вход или выход. 0 - вход, 1 - выход (п.16.1.2)
PORT_LED_4->ANALOG |= (1 << PIN_LED_4); // Аналог или цифра. 0 - аналоговый, 1 - цифровой (п.16.1.4) (п.4 т.2)
PORT_LED_4->PWR |= (1 << PIN_LED_4 * 2); // Передатчик. 0 - отключен, 1 - медленный, 2 - средний, 3 - быстрый (п.16.1.7)
}
static void init_SysTick(void){ // Инициализация системного таймера SysTick
SysTick->CTRL = ( // Параметры системного таймера (п.12.1.1)
(1 << 0x00) // Работа таймера. 0 - отключен, 1 - включен
|(1 << 0x01) // Разрешение прерывания по достижению счетчика. 0 - запрещено, 1 - разрешено
|(1 << 0x02) // Источник тактового сигнала. 0 - LSI (внутренний RC 40 кГц), 1 - HCLK (системная частота)
);
SysTick->LOAD = 8000 - 1; // Загружаем значение счетчика (для 1 миллисекунды, исходя из тактовой частоты 8 МГц) (п.12.1.2)
NVIC_EnableIRQ(SysTick_IRQn); // Разрешаем прерывание от SysTick
}
int main(void)
{
init_GPIO(); // Инициализация порта
init_SysTick(); // Инициализация системного таймера SysTick
while(1){ // Основной бесконечный цикл. В нем делаем полезную работу.
}
}
void SysTick_Handler(void){ // Обработчик прерывания
static uint16_t cnt_led_1 = 0; // Счетчик миллисекунд для светодиода 1
static uint16_t cnt_led_2 = 0; // Счетчик миллисекунд для светодиода 2
cnt_led_1++; // Инкрементируем счетчик светодиода 1
cnt_led_2++; // Инкрементируем счетчик светодиода 2
if(cnt_led_1 == TIME_LED_1){ // Настало время смены цвета светодиода 1
PORT_LED_1->RXTX ^= (1 << PIN_LED_1); // Меняем цвет светодиода 1 (п.16.1.1)
cnt_led_1 = 0; // Сбрасываем счетчик
}
if(cnt_led_2 == TIME_LED_2){ // Настало время смены цвета светодиода 2
PORT_LED_2->RXTX ^= (1 << PIN_LED_2); // Меняем цвет светодиода 2 (п.16.1.1)
cnt_led_2 = 0; // Сбрасываем счетчик
}
if(systick_counter % TIME_LED_3 == 0){ // Настало время смены цвета светодиода 3
PORT_LED_3->RXTX ^= (1 << PIN_LED_3); // Меняем цвет светодиода 3 (п.16.1.1)
}
if(systick_counter % TIME_LED_4 == 0){ // Настало время смены цвета светодиода 3
PORT_LED_4->RXTX ^= (1 << PIN_LED_4); // Меняем цвет светодиода 3 (п.16.1.1)
}
systick_counter++; // Инкрементируем счетчик миллисекунд
}