6. Мигаем с таймером
До сих пор все наши программы загружали процессор 100% времени, используя пустые циклы с примерно подобранным числом итераций. Другой подход - использовать таймеры. В этом случае процессор будет находиться в режиме ожидания до тех пор, пока не получит прерывание от таймера.
Мы упоминали о том, что у процессора имеется таблица векторов, в которой хранятся адреса функций-обработчиков исключений. Исключение (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;
volatile uint32_t *stk_ctrl_pointer = (uint32_t *)stk_ctrl_address;
uint32_t stk_load_address = stk_base_address + 0x04;
volatile uint32_t *stk_load_pointer = (uint32_t *)stk_load_address;
// enable systick exception, enable systick counter
uint32_t stk_ctrl_value = *stk_ctrl_pointer;
stk_ctrl_value = stk_ctrl_value | 0x00000003;
*stk_ctrl_pointer = stk_ctrl_value;
// set reload value to 500 ms
*stk_load_pointer = 8000000 / 8 / 2;
}
По умолчанию systick таймер использует частоту AHB/8. AHB это частота шины AHB (Advanced High-performance Bus), по умолчанию она равна 8 МГц, т.е. частота systick таймера равна 1 МГц.
Когда таймер достигнет нуля, произойдёт исключение SysTick и таймер инициализируется заново значением stk_load.
После всех настроек мы перейдём в вечный цикл, в котором будем вызывать инструкцию wfi (wait for interrupt). Эта инструкция будет переключать процессор в режим сна до следующего прерывания.
for (;;)
{
asm("wfi");
}
Ну а обработчик исключения в нашем случае будет самый, что ни на есть, простой:
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
К сожалению есть одна проблема, которую мне пока не удалось решить. После
запуска этой программы светодиод мигает, но плата перестаёт быть видна в
программаторе, её уже нельзя перепрограммировать или отладить. Для решения этой
проблемы нужно заменить "wfi" на "nop" (это заставит процессор не засыпать, а
крутиться в вечном цикле), переключить джампер BOOT0 в состояние "1" и
перезагрузить плату и программатор. Процессор загрузится из бутлоадера и будет
доступен для перепрограммирования. После этого можно прошить программу без
инструкции wfi и вернуть джампер в исходное состояние.