Часть 1 Часть 2 Часть 3 Часть 4 Часть 5 Часть 6 Часть 7 Часть 8 Часть 9
Часть 6. Мигаем с таймером
До сих пор наши программы для реализации временной задержки использовали пустые циклы с примерно подобранным числом итераций. Другой подход - использовать таймеры.
Мы упоминали о том, что у процессора имеется таблица векторов, в которой хранятся адреса функций-обработчиков исключений. Исключение (exception) это более общий термин, который включает в себя прерывания (interrupts) и ошибки (faults). Подробное описание механизма работы исключений можно почитать в Progamming Manual, разделе 2.3. Ниже будет дана краткая и упрощённая выжимка.
Исключение в процессоре может возникать как синхронно, например если при очередном шаге процессор прочитал несуществующую инструкцию, так и асинхронно, например если во время выполнения программы пользователь нажал кнопку, подключенную к GPIO выводам. Когда происходит исключение, процессор считывает адрес обработчика исключений из таблицы векторов. Справочную информацию по списку всех возможных исключений можно посмотреть в Reference Manual, разделе 10.1.2, таблице 63. Если обработчик исключений не задан (например его адрес является чётным, мы обсудили в первой части, адреса должны иметь выставленный младший бит), это приведёт к возникновению ошибки hard fault, которая в свою очередь вызовет соответствующий обработчик исключений.
Если исключение возникает во время обработки другого исключения, то новое исключение может приостановить обработку текущего исключения или же дождаться его завершения. Это зависит от приоритетов исключений.
Одним из видов периферийных устройств в микроконтроллере являются таймеры. Обычно доступно несколько разных таймеров, в Datasheet, можно увидеть, что в нашем микроконтроллере доступно 3 таймера общего назначения (general purpose), 1 таймер для расширенного управления (advanced control), часы реального времени (real time clock), два сторожевых таймера (watchdog) и SysTick таймер.
Таймеры используют источники тактирования (clock source) для своей работы. Это могут быть внешний кварцевый генератор, внутренний RC генератор и тд. Процессор для своей работы также использует источник тактирования, и если настроить его частоту, то это позволит изменять скорость работы процессора (конечно в определённых пределах). На нашей плате установлен внешний кварцевый генератор на 8 МГц, процессор после инициализации использует его, это значит, что он работает со скоростью 8 миллионов тактов в секунду. Большинство инструкций занимают 1-2 такта. К примеру в третьей части наш цикл состоял из двух инструкций и выполнялся миллион раз. Инструкция subs занимает 1 такт, а инструкция bne 2 такта. Это значит, что время его выполнения было равно 375 мс, а полный цикл включения-выключения светодиода занимал 750 мс, иными словами он моргал 80 раз в минуту.
Довольно теории, перейдём к практике. Для нашей программы нужно сделать следующие действия:
-
Как и в прошлых частях, нужно сконфигурировать GPIO порт и вывод.
-
Мы настроим SysTick таймер и перейдём в вечный цикл.
-
Мы добавим обработчик исключения SysTick, который будет вызываться каждый раз, когда SysTick таймер достигнет нуля. В этом обработчике мы переключим светодиод. Когда придёт время вызывать обработчик исключения, процессор перестанет выполнять вечный цикл и вызовет обработчик исключения. После завершения процессор вернётся в выполнение вечного цикла.
Для сборки и компоновки будут использоваться скрипты из предыдущих частей. Нужно будет лишь добавить новый обработчик исключения в таблицу векторов.
Подробную информацию о таймере SysTick можно найти в Programming Manual, раздел 4.5. Вратце: нам нужно настроить таймер, используя регистры STK_CTRL, STK_LOAD. Код приведён ниже:
static void configure_sys_tick_timer(void)
{
uint32_t stk_base_address = 0xE000E010;
uint32_t stk_ctrl_address = stk_base_address + 0x00;
uint32_t stk_ctrl_enable = 1 << 0;
uint32_t stk_ctrl_tickint = 1 << 1;
uint32_t stk_load_address = stk_base_address + 0x04;
// enable systick exception, enable systick counter
volatile uint32_t *stk_ctrl_pointer = (uint32_t *)stk_ctrl_address;
uint32_t stk_ctrl_value = *stk_ctrl_pointer;
stk_ctrl_value |= stk_ctrl_enable;
stk_ctrl_value |= stk_ctrl_tickint;
*stk_ctrl_pointer = stk_ctrl_value;
// set reload value to 500 ms
volatile uint32_t *stk_load_pointer = (uint32_t *)stk_load_address;
uint32_t stk_load_value;
stk_load_value = 8000000 / 8 / 2;
*stk_load_pointer = stk_load_value;
}
По умолчанию systick таймер использует частоту AHB/8. AHB это частота шины AHB (Advanced High-performance Bus), по умолчанию она равна 8 МГц, т.е. частота systick таймера равна 1 МГц.
Когда таймер достигнет нуля, произойдёт исключение SysTick и таймер инициализируется заново значением stk_load.
После того, как мы настроим периферийные устройства, мы перейдём в вечный цикл в
функции start
. Для этого мы напишем следующий код:
for (;;)
{
}
В этом простом цикле есть небольшой нюанс. Для языка С тут проблем никаких нет, он допускает существование в программе вечных циклов. Однако если вы используете С++, то компилятор может просто выбросить этот цикл, т.к. согласно актуального стандарта С++ эта программа не является корректной. Мы используем С, поэтому далее на этот момент не будем обращать внимания.
Ну а обработчик исключения в нашем случае будет самый, что ни на есть, простой:
void sys_tick_exception_handler(void)
{
toggle_pin();
}
Его мы добавим в таблицу векторов:
.isr_vector :
{
LONG(0x20000000 + 20K);
. = 0x00000004;
LONG(_reset_exception_handler | 1);
. = 0x0000003c;
LONG(sys_tick_exception_handler | 1);
. = 0x00000130;
} > Flash
На этом всё, полученный код можно прошить в устройство и наблюдать моргание ровно раз в секунду.
Полный код доступен на гитхабе.