STM32F4: GNU AS: Мигаем светодиодом (Оживление) (Часть 2)
STM32F4: GNU AS: Мигаем светодиодом (Версия для STM32F4 Discovery, Оптимизация) (Часть 3)
STM32F4: GNU AS: Настраиваем среду компиляции (Часть 4)
STM32F4: GNU AS: Настройка тактирования микроконтроллера (Часть 5)
В первых публикациях — мы тактировали микроконтроллер от внутреннего тактового генератора (HSI)… ну если быть совсем точным, то мы вообще не настраивали тактирование микроконтроллера, и пользовались тем состоянием которое было у микроконтроллера при старте (включении питания, сбросе). Для первых программ это вполне допустимо, но для будущих проектов этого не достаточно, поэтому в пятой части публикации я предложил модуль настройки тактирования микроконтроллера (sysclk) на его «родную» (определенную производителем) частоту в 168 МГц.
Теперь организуем задержку на заданное количество миллисекунд при помощи системного таймера (SYSTICK).
До настоящей публикации, в своей первой программе «мигалка», для того чтобы организовать задержку при мигании светодиодом мы делали цикл с достаточно большим периодом повторения:
DELAY:
LDR R3, =0x00100000 @ повтор цикла 0x0010 0000 раз.
Delay_loop:
SUBS R3, R3, 1
BNE Delay_loop
BX LR
Такой подход более менее допустим при тактовой частоте микроконтроллера в 10-20 МГц и при отсутствии требования о жестком задании временных интервалов и их соответствии каким то общеупотребимым значениям (например долям секунды).
Теперь же (после настройки тактирования микроконтроллера на 168 МГц) переменную цикла, для сохранения прежней частоты мигания светодиодом (значение регистра R3), нужно увеличить примерно в 10 раз — значение должно быть равно, примерно, 0x00119999. На глаз — частота мигания светодиода станет прежней, но точно подобрать интервал примерно равный полу- или одной секунде, что называется «на глаз», без большой погрешности просто не возможно… В программе мигалка это не важно, но в дальнейшем такой подход использовать нельзя, и нужно использовать другое решение:
В микроконтроллерах STM32F4 есть системный таймер на основе которого получается хороший способ генерации задержек.
Описание регистров системного таймера (SYSTICK) приведено в PM214 Programming manual, стр. 245 (и далее), можно поискать в интернете, например, вот таким запросом в googlе.ru: systick stm32f4 пример, я лишь немного резюмирую информацию по регистрам и «переложу» все это в код программы на языке ассемблера:
У системного таймера есть несколько управляющих регистров:
-STK_CTRL — регистр управления, смещение относительно базы — 0х00
Расшифровка управляющих битов:
- COUNTFLAG Бит устанавливается в «1» если счетчик досчитал до нуля и этот бит еще не проверялся
- CLKSOURCE — Бит выбора источника для тактирования SYSTICK:
0: AHB/8
1: AHB
Частота AHB равна тактовой частоте процессора, после работы модуля тактирования частота AHB будет равна 168 МГц - TICKINT — Бит включения прерывания таймера при достижении нуля:
0: При достижении «0» прерывание не генерируется
1: При достижении «0» генерируется прерывание - ENABLE — Включить системный таймер
-STK_LOAD — регистр первоначального значения, смещение относительно базы — 0х04
В регистре содержится первоначальное значение счетчика, значение 24-ех битное, от 1 до 0xFFFFFF (16777215). Стартовое значение равное «0» не имеет смысла, так как бит COUNTFLAG регистра STK_CTRL выставляется в «1» при уменьшении значения счетчика с «1» на «0».
-STK_VAL — регистр текущего значения, смещение относительно базы — 0х08
Значение счетчика так же как и значение регистра STK_LOAD 24-ех битное. При чтении регистра — считывается его текущее значение, при любой записи регистра — значение регистра сбрасывается в «0»
-STK_CALIB — регистр калибровки, смещение относительно базы — 0х0C
В этом регистре не разбирался. Судя по описанию калибровочное значение нужно при использовании частоты такта AHB/8.
Использование системного таймера:
- Установить значение счетчика STK_LOAD
- Установить управляющее значение регистра STK_CTRL:
- Выбрать частоту таймера, для системной CLKSOURCE=1
- Выбрать необходимость генерации прерывания, TICKINT=1
- Включить таймер, ENABLE=1
Обычно значение счетчика STK_VAL / STK_LOAD выбирают таким образом чтобы получить удобную для обработки частоту. Например, для удобства расчета долей секунды удобно выбрать, например, значение позволяющее получить частоту 1000 гц, тогда подождав в цикле 1000 прерываний — мы получим задержку в 1 секунду…
Системная частота у нас равна 168 000 000 Гц, значит значение STK_LOAD установим равным (168 000 -1) — в итоге получим генерацию прерываний с частотой 168 000 000 Гц / 168 000 = 1000 Гц (то есть каждую 1 миллисекунду)
Почему загружаем значение (168 000 -1) а не просто 168 000? — потому что загрузка значения STL_LOAD в STK_VAL происходит при следующем цикле после установки значения счетчика равным «0», и следующее уменьшение значения еще при следующем такте — таким образом количество тактов как бы увеличивается на «1» относительно значения заданного STK_LOAD.
Оформим алгоритм настройки, приведенный выше, программой на языке ассемблера:
.global SYSTICK_START
SYSTICK_START:
PUSH { R0 , R1 , LR }
LDR R0 , =SYSTICK_BASE
@ установка значения пересчета для получения частоты 1000 гц
LDR R1 , =(168000 - 1)
STR R1 , [ R0 , STK_LOAD]
@ источник частоты AHB (168 мгц) + прерывания + включаем SYSTICK
LDR R1 , =(STK_CTRL_CLKSOURSE + STK_CTRL_TICKINT + STK_CTRL_ENABLE)
STR R1 , [ R0 , STK_CTRL]
После включения таймера произойдет следующее:
1) значение из STK_LOAD будет записано в STK_VAL
2) значение STK_VAL будет уменьшаться с частотой заданной CLKSOURSE [1: 168 мгц / 0: 21 мгц (168/8)], в мое примере выше CLKSOURSE=1 (такт системного таймера от AHB =168 МГц)
3) как только значение STK_VAL будет уменьшаться с 1 на 0 произойдет:
а) установка флага COUNTFLAG=1
б) если бит настройки TICKINT=1, то произойдет вызов подпрограммы обработки прерывания системного таймера
в) значение из STK_LOAD будет загружено в STK_VAL
Думаю с настройкой системного таймера (SYSTICK) теперь все более менее понятно (за исключением калибровки), теперь посмотрим как строится обработчик прерываний системного таймера.
Расположение векторов таблицы прерываний можно посмотреть на стр. 37 PM0214 Programming manual
Эти указатели размещаются сразу после указателя стека загружаемого при старте микроконтроллера.
Я поместил эту таблицу указателей в файл isr_vector.inc
@ Таблица исключений для микроконтроллеров STM32F40x - F41x
.word int_vect_terminator+1 @ NMI
.word int_vect_terminator+1 @ Hard Fault
.word int_vect_terminator+1 @ MPU Fault
.word int_vect_terminator+1 @ Bus Fault
.word int_vect_terminator+1 @ Usage Fault
.word 0 @ Reserved
.word 0 @ Reserved
.word 0 @ Reserved
.word 0 @ Reserved
.word int_vect_terminator+1 @ SVCall
.word int_vect_terminator+1 @ Debug Monitor
.word 0 @ Reserved
.word int_vect_terminator+1 @ PendSV
.word ISR_SYSTICK+1 @ SysTick
.section .text
@ Заглушка для любых необрабатываемых прерываний
int_vect_terminator:
B int_vect_terminator
Как я описывал в первой части статьи — все вектора прерываний должны быть увеличены на 1.
Поскольку сейчас нам не нужно обрабатывать никакие прерывания кроме SYSTICK — то все вектора заполним адресом перехода на подпрограмму-заглушку int_vect_terminator, которая представляет собой просто зацикленный сам на себя переход B
Включение таблицы векторов прерываний выглядит как .include в секции .vectors после указателя стека и вектора сброса (RESET)
.section .vectors
@ таблица векторов прерываний
.word 0x20020000 @ Вершина стека
.word Start+1 @ Вектор сброса
.include "isr_vector.inc" @ таблица указателей векторов прерываний
Теперь более четко определим логику работы с системным таймером (SYSTICK)
В прерывании мы будем проверять некий счетчик (размещенный в SRAM) на равность нулю, и если его значение не равно «0», то будем уменьшать это значение на единицу (в текущем прерывании) и выходить… Поступая таким образом мы добьемся уменьшения значения счетчика на 1 каждые 1/1000 секунду (1 мс).
В программе: объявляем переменную в SRAM, согласно карты компоновки (stm32f40_def.ld) это секция .bss:
.section .bss
@ Переменная в ОЗУ
SYSTICK_COUNTER:
.word 0 @ Счетчик задержки
Еще один интересный момент: нам нужно загрузить адрес переменной SYSTICK_COUNTER в R1 (см. далее в программе), для этого мы создаем в секции .asmcode или .rodata константу ADR_SYSTICK_COUNTER со значением адреса переменной,
ADR_SYSTICK_COUNTER:
.word SYSTICK_COUNTER
и уже это значение загружаем в R1, после того как в R1 мы загрузили адрес счетчика — мы можем загрузить в R0 значение этого счетчика
LDR R1 , ADR_SYSTICK_COUNTER
LDR R0 , [R1 , 0]
Теперь нам нужно проверить значение R0 на ноль, и если R0<>0, то уменьшить его на 1:
ORRS R0 , R0 , 0 @ Проверка R0 на 0
ITT NE @ Если R0<>0 уменьшаем его на 1
SUBNE R0 , R0 , 1
STRNE R0 , [R1 , 0]
Способ проверки и изменения прост: сначала мы делаем логическое OR регистра R0 к самому себе, соответственно его значение не меняется, но вот суффикс "S" после команды указывает, что необходимо обновить флаги. Соответственно если R0 = 0, то после выполнения ORR R0, R0, 0 будет установлен флаг "Z". Далее идет блок IT который резервирует после себя две команды которые будут выполнены при сброшенном флаге "Z" (в нашей программе в случае если R0<>0).
Это команды:
— Уменьшения R0 на "1" (при сброшенном флаге "Z" — суффикс исполнения "NE")
— Записи нового значения R0 (так же при сброшенном флаге "Z" — суффикс исполнения "NE")
Поскольку в нашем прерывании мы используем два регистра R0 и R1 — которые возможно имели какие то значения в прерываемой программе- то необходимо их сохранить при входе в прерывание и восстановить при выходе из него — это делается командами PUSH / POP соответственно.
Весь код обработчика прерываний будет выглядеть следующим образом:
.section .bss
@ Переменная в ОЗУ
SYSTICK_COUNTER:
.word 0 @ Счетчик задержки
.section .asmcode
ADR_SYSTICK_COUNTER:
.word SYSTICK_COUNTER
@ Прерывание уменьшает значение счетчика SYSTICK_COUNTER на "1" (в случае если
@ значение счетчика больше "0"
.global ISR_SYSTICK
ISR_SYSTICK:
PUSH { R0 , R1 }
LDR R1 , ADR_SYSTICK_COUNTER
LDR R0 , [R1 , 0]
ORRS R0 , R0 , 0 @ Проверка R0 на 0
ITT NE @ Если R0<>0 уменьшаем его на 1, и сохраняем в SYSTICK_COUNTER
SUBNE R0 , R0 , 1
STRNE R0 , [R1 , 0]
POP { R0 , R1 }
BX LR
К этому моменту у нас есть:
1) Настройка системного таймера
2) Прерывание генерируемое 1000 раз в секунду
3) Уменьшаемый с частотой 1000 раз в секунду счетчик.
Осталось написать процедуру задержки на заданное время:
.global SYSTICK_DELAY
SYSTICK_DELAY:
PUSH {R1, R2, LR}
LDR R2, =SYSTICK_BASE @ сбросим текущий счетчик
STR R0, [R2, STK_VAL]
LDR R1, ADR_SYSTICK_COUNTER @ адрес счетчика
STR R0, [R1 , 0] @ сохраним начальное значение
DELAY_LOOP:
LDR R0, [R1 , 0] @ ждем обнуления счетчика
ORRS R0, R0 , 0
BNE DELAY_LOOP
POP { R1 , R2, PC }
Что мы делаем:
— сохраняем используемые регистры
— сбрасываем текущий счетчик STK_VAL в «0» (посмотрите описание, счетчик сбрасывается ПРИ ЛЮБОЙ ЗАПИСИ в него!)
PUSH {R1, R2, LR}
LDR R2, =SYSTICK_BASE @ сбросим текущий счетчик
STR R0, [R2, STK_VAL]
-сохраняем входное значение задержки в R0 в SYSTICK_COUNTER
LDR R1, ADR_SYSTICK_COUNTER @ адрес счетчика
STR R0, [R1 , 0] @ сохраним начальное значение
— Ждем в цикле пока счетчик не обнулится — это будет означать что [R0]-миллисекунд прошло и можно выходить из подпрограммы задержки
DELAY_LOOP:
LDR R0, [R1 , 0] @ ждем обнуления счетчика
ORRS R0, R0 , 0
BNE DELAY_LOOP
POP { R1 , R2, PC }
Весь код модуля мы разобрали, осталась лишь одна тонкость:
Дело в том что в микроконтроллерах STM32 существует возможность задания приоритетов прерываний. В AVR микроконтроллерах если процессор обрабатывает прерывание, то все остальные, возникшие за это время прерывания, ожидают завершения обработки текущего прерывания и потом будут исполняться в определенном порядке. В STM32 если при выполнении прерывания с приоритетом например 15, возникает прерывание с приоритетом меньшим — то процессор перейдет от выполнения текущего прерывания к выполнению прерывания с более высоким приоритетом. Насколько часто могут возникнуть подобные ситуации я не берусь сказать, скорее всего все зависит от контекста задачи. Но коль такой механизм есть, то настроим низший приоритет для нашего прерывания (низший приоритет — более высокое значение приоритета!).
Для того чтобы определить какой регистр к какому прерыванию задает приоритет нужно посмотреть на стр. 217 PM0214 Programming manual:
Для нашего системного таймера определен регистр SHPR3:
Для максимального приоритета задается номер «0», для минимального 0xFF, правда есть оговорка что принимаются только старшие 4 бита приоритета — таким образом у нас есть возможность задать старшими 4-мя битами 16 приоритетов (от 0x00 до 0xF0).
Я использовал системный таймер и без задания приоритета прерывания — так что можно особо не разбираться в этом механизме сейчас, но чтобы не забыть что это нужно настраивать — задаем приоритет 15 (самый низший)
@GNU AS
.syntax unified @ синтаксис исходного кода
.thumb @ тип используемых инструкций Thumb
.cpu cortex-m4 @ процессор
.fpu fpv4-sp-d16 @ сопроцессор
@ определение констант
.equ SCB_BASE ,0xE000ED00 @ System control block (SCB)
.equ SHPR3 ,0x20 @ System handler priority registers (SHPRx)
.equ SYSTICK_BASE ,0xE000E010 @ System timer
.equ STK_CTRL ,0x00 @ Регистр статуса и управления
.equ STK_LOAD ,0x00000004 @ Значение для перезагрузки счетчика
.equ STK_VAL ,0x00000008 @ Текущее значение счетчика
.equ STK_CTRL_CLKSOURSE ,0x00000004 @ (RW) источник тактирования: 0: AHB/8; 1: AHB
.equ STK_CTRL_TICKINT ,0x00000002 @ (RW) при установке генерирует прерывание при переходе в 0
.equ STK_CTRL_ENABLE ,0x00000001 @ (RW) включает счетчик.
@ ****************************************************************************
@ * Обработчик прерывания системного таймера SysTick *
@ ****************************************************************************
.section .bss
@ Переменная в ОЗУ
SYSTICK_COUNTER:
.word 0 @ Значение необходимой задержки
.section .asmcode
@ Прерывание уменьшает значение счетчика SYSTICK_COUNTER на "1" (в случае если
@ значение счетчика больше "0"
.global ISR_SYSTICK
ISR_SYSTICK:
PUSH {R0 , R1 , LR}
LDR R1 , ADR_SYSTICK_COUNTER
LDR R0 , [R1 , 0]
ORRS R0 , R0 , 0 @ Проверка R0 на 0
ITT NE @ Если R0<>0 уменьшаем его на 1
SUBNE R0 , R0 , 1
STRNE R0 , [R1 , 0]
POP {R0 , R1 , PC}
ADR_SYSTICK_COUNTER:
.word SYSTICK_COUNTER
@ ****************************************************************************
@ * Инициализация системного таймера SysTick *
@ ****************************************************************************
@ Для частоты AHB=168 Мгц
@ Частота счета 1000 Гц
@
@ Включение SysTick
.global SYSTICK_START
SYSTICK_START:
PUSH {R0 , R1 , LR}
LDR R0 , =SYSTICK_BASE
@ установка значения пересчета для получения частоты 1000 гц
LDR R1 , =168000 - 1
STR R1 , [R0 , STK_LOAD]
@ источник частоты AHB (168 мгц) + прерывания + включаем SYSTICK
LDR R1 , =(STK_CTRL_CLKSOURSE + STK_CTRL_TICKINT + STK_CTRL_ENABLE)
STR R1 , [R0 , STK_CTRL]
@ установка приоритета прерываний от SysTick
LDR R0 , =SCB_BASE
LDR R1 , [R0, SHPR3]
ORR R1 , R1 , 0xF0 << 24
STR R1 , [R0 , SHPR3]
POP {R0 , R1 , PC}
@ ****************************************************************************
@ * Задержка средствами системного таймера SysTick *
@ ****************************************************************************
@ Входной параметр: R0 - задержка в милисекундах
@ Выходной параметр: R0 = 0
@ Изменение других регистров: нет
.global SYSTICK_DELAY
SYSTICK_DELAY:
PUSH {R1, R2, LR}
LDR R2, =SYSTICK_BASE @ сбросим текущий счетчик
STR R0, [R2, STK_VAL]
LDR R1, ADR_SYSTICK_COUNTER @ адрес счетчика
STR R0, [R1 , 0] @ сохраним начальное значение
DELAY_LOOP:
LDR R0, [R1 , 0] @ ждем обнуления счетчика
ORRS R0, R0 , 0
BNE DELAY_LOOP
POP { R1 , R2, PC }
Использование модуля в программе «мигалка»
Я не буду приводить весь текст программы приведу только текст процедуры DELAY которая отвечает за задержку:
DELAY:
PUSH { R0, LR }
MOV R0, 250 @ задержка 250 мс.
BL SYSTICK_DELAY
POP { R0, PC }
В переменной R0 мы задаем желаемую задержку в миллисекундах, и потом вызываем SYSTICK_DELAY из получившегося у нас модуля systick. Удобство в том, что теперь мы можем задавать период мигания светодиода с точностью до 1 мс, а не подбирать «на глаз» :-)
Проект с использованием модулей sysclk и systick можно скачать здесь
Дополнительно в скрипт компиляции я внес небольшое усовершенствование: теперь при включении файлов директивой .include путь задается относительно .asm файла в котором делается «включение». Так что теперь, если в модуль входят несколько файлов, то можно делать .include просто по имени включаемого файла при условии, что он «лежит» рядом с .asm файлом — это удобно — так как не нужно знать относительный путь папки модуля в проекте. К слову, старый способ включения файлов с указанием относительно пути тоже работает, можете делать так как вам удобно.
P.s. если в статье где-то не очень понятно выразился — то вы всегда можете написать мне в «личку» здесь на хабре или на gorbukov @ «тот кто знает все !»
Автор: VitGo