LINUX.ORG.RU

Осваиваем STM32 снизу: часть 6 - Мигаем с таймером

 ,


0

1

Часть 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 раз в минуту.

Довольно теории, перейдём к практике. Для нашей программы нужно сделать следующие действия:

  1. Как и в прошлых частях, нужно сконфигурировать GPIO порт и вывод.

  2. Мы настроим SysTick таймер и перейдём в вечный цикл.

  3. Мы добавим обработчик исключения 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

На этом всё, полученный код можно прошить в устройство и наблюдать моргание ровно раз в секунду.

Полный код доступен на гитхабе.

★★★

Проверено: hobbit ()
Последнее исправление: vbr (всего исправлений: 3)

Ого, что тут в неподтвержденных, целых шесть частей, круто

GREAT-DNG ★★★
()

Я конечно считаю что подход автора переусложнен, в реальности всё гораздо проще на Си без всего этого

Но все равно, благодарю автора за труды, может кому то углубленное представление будет необходимо и полезно

I-Love-Microsoft ★★★★★
()
Ответ на: комментарий от I-Love-Microsoft

Мне как раз наоборот подход автора очень понравился своей простотой и лаконичностью.

Вместо переусложненных IDE с кучей мастеров, конфигураторов, генераторов и прочего шлака на простых примерах с использованием привычных юниксовых утилит показать, как это все работает на самом базовом и низком уровне - однозначно пять.

Barracuda72 ★★
()
27 октября 2023 г.

Почему-то на моём устройстве этот пример не работает. Если пытаться использовать HAL_Delay() из HAL — результат аналогичный. При этом через цикл всё работает(то есть глючит именно таймер). Доступа к программатору и отладчику соответственно нет. Что это может быть?

Werenter ★★★
()
Ответ на: комментарий от Werenter

Подземный стук. Какое устройство, какие на нем доступны таймеры? HAL требует настройки если вы не подключили нужный заголовок и не прописали нужные значения в define’ах.

PPP328 ★★★★★
()
Ответ на: комментарий от PPP328

Устройство на основе STM32F103RE. Таймер SysTick точно имеется аппаратно(он есть на всех Cortex-M3), но почему-то не работает. Остальную периферию, не расположенную прямо в чипе, сообщить не могу, так как не знаю. Устройство — CM530.

И дело скорее всего не в HAL, так как код автора тоже не работает(при этом версия от автора с использованием цикла работает нормально).

Вообще, у этого контроллера есть дополнительный загрузчик, залитый в первые 12кб flash, который делает возможной загрузку прошивки с USART. Возможно, что он что-то криво инициализирует до старта программы, и потому не работает.

Werenter ★★★
()
Последнее исправление: Werenter (всего исправлений: 4)
Ответ на: комментарий от Werenter

Использовать таймер можно не только через прерывание но и через обычный polling цикл. Когда таймер проходит через 0, то выставляется бит COUNT FLAG в регистре STK_CTRL. Для начала стоит попробовать этот вариант, чтобы убедиться, что этот таймер работает (уж не знаю, почему он может не работать):

void start(void)
{
  *((volatile int *) 0xe000e010) = 0x00000001;
  *((volatile int *) 0xe000e014) = 500000;
  
  while (*((volatile int *) 0xe000e010) & 0x00010000 == 0) {
  }
  // led on
  
  while (*((volatile int *) 0xe000e010) & 0x00010000 == 0) {
  }
  // led off
}

(код пишу в браузере, не проверял)

Кроме того надо иметь в виду, что во-первых таймер тикает со скоростью, пропорциональной частоте процессора и если процессор работает на очень низкой частоте, то значение 500000 нужно уменьшить. Если наоборот на очень высокой, то увеличить, очень часто моргающий диод невооружённым глазом не отличить от просто включенного.

Если удастся «завести» код с polling режимом, значит проблема в вызове обработчика прерывания. Во-первых нужно перепроверить, что в прошивке стоит правильный адрес, во-вторых нужно убедиться, что загрузчик не меняет адрес таблицы обработчиков прерываний. Я сам это не пробовал делать, но насколько я знаю, это настраивается с помощью регистра SCB_VTOR, в-третьих нужно убедиться, что нужное прерывание не отключено (регистры NVIC_ISERx / NVIC_ICERx).

vbr ★★★
() автор топика
Последнее исправление: vbr (всего исправлений: 5)
Ответ на: комментарий от vbr

Спасибо за развернутый ответ, как будет возможность, попробую.

Werenter ★★★
()
Ответ на: комментарий от vbr

Да, такой код нормально работает. Буду разбираться с прерываниями

Werenter ★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.