Продолжаем освоение ассемблера для архитектуры ARM.
На данном занятии мы попробуем задействовать такой механизм, как аппаратные таймеры.
Что это такое, мы давно знаем и давно используем в своих проектах. Поэтому вдаваться в подробности устройства аппаратных таймеров в STM32 и их настроек мы не будем.
Также в данном уроке мы познакомимся с таким понятием в ассемблере как табличные переходы. Познакомимся мы с ним при написании нашего кода, а пока лишь скажу то, что табличные переходы нам помогут организовать ветвление кода в зависимости от значения аргумента подобно конструкции switch в языке C. Также они нам помогут удобно управлять уровнями одновременно нескольких ножек портов, так как в схему мы теперь включим целых 10 светодиодов, а вернее светодиодную планку с десятью светодиодами, как мы делали, например, в уроке 165 с использованием библиотеки CMSIS
Проект за основу мы возьмём из прошлого урока с именем ASM_SYSTICK01 и назовём его ASM_TIM2.
Откроем проект в Keil и откроем в нём файл main.s. Первым делом нам нужно будет настроить ножки портов, задействованные под светодиоды.
Напомню, какие это ножки и каких портов
- LED1 — GPIOA2
- LED2 — GPIOA3
- LED3 — GPIOA4
- LED4 — GPIOA5
- LED5 — GPIOA6
- LED6 — GPIOA7
- LED7 — GPIOB0
- LED8 — GPIOB1
- LED9 — GPIOB10
- LED10 — GPIOB11
Предлагаю вынести процедуру настройки ножек GPIO в отдельную подпрограмму, поэтому пока удалим настройку GPIO в main.s, тем более настройка порта C и его 13 ножки нам не потребуется
LDR R2, =(PERIPH_BB_BASE + (RCC_APB2ENR — PERIPH_BASE) * 32 + RCC_APB2ENR_IOPCEN_N * 4)
STR R1, [R2]
LDR R2, =(PERIPH_BB_BASE + (GPIOC_CRH — PERIPH_BASE) * 32 + GPIO_CRH_MODE13_0_N * 4)
STR R1, [R2]
LDR R2, =(PERIPH_BB_BASE + (GPIOC_ODR — PERIPH_BASE) * 32 + GPIO_ODR_ODR13_N * 4)
STR R1, [R2]
Из бесконечного цикла также удалим всё его тело.
В файл с константами stm32f103C8.asm тоже нужно будет добавить ряд новых
1 2 3 4 5 6 7 8 9 |
PERIPH_BASE EQU 0x40000000 APB1PERIPH_BASE EQU PERIPH_BASE TIM2_BASE EQU APB1PERIPH_BASE + 0x0000 TIM2_CR1 EQU 0x00 TIM2_DIER EQU 0x0C TIM2_SR EQU 0x10 TIM2_PSC EQU 0x28 TIM2_ARR EQU 0x2C |
1 2 |
RCC_CFGR EQU 0x40021004 RCC_APB1ENR EQU 0x4002101C |
1 2 3 4 5 6 |
RCC_CSR EQU 0x40021024 GPIOA_CRL EQU 0x40010800 GPIOA_ODR EQU 0x4001080C GPIOB_CRL EQU 0x40010C00 GPIOB_CRH EQU 0x40010C04 GPIOB_ODR EQU 0x40010C0C |
1 2 |
SCB_BASE EQU 0xE000ED00 RCC_APB1ENR_TIM2EN_N EQU 0 |
1 2 3 |
RCC_APB2ENR_AFIOEN_N EQU 0 RCC_APB2ENR_IOPAEN_N EQU 2 RCC_APB2ENR_IOPBEN_N EQU 3 |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
AFIO_MAPR_SWJ_CFG_JTAGDISABLE_N EQU 25 GPIO_CRL_MODE0_0_N EQU 0 GPIO_CRL_MODE1_0_N EQU 4 GPIO_CRL_MODE2_0_N EQU 8 GPIO_CRL_MODE3_0_N EQU 12 GPIO_CRL_MODE4_0_N EQU 16 GPIO_CRL_MODE5_0_N EQU 20 GPIO_CRL_MODE6_0_N EQU 24 GPIO_CRL_MODE7_0_N EQU 28 GPIO_CRH_MODE8_0_N EQU 0 GPIO_CRH_MODE10_0_N EQU 8 GPIO_CRH_MODE11_0_N EQU 12 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
GPIO_CRH_MODE13_0_N EQU 20 GPIO_CRL_CNF0_0_N EQU 2 GPIO_CRL_CNF1_0_N EQU 6 GPIO_CRL_CNF2_0_N EQU 10 GPIO_CRL_CNF3_0_N EQU 14 GPIO_CRL_CNF4_0_N EQU 18 GPIO_CRL_CNF5_0_N EQU 22 GPIO_CRL_CNF6_0_N EQU 26 GPIO_CRL_CNF7_0_N EQU 30 GPIO_CRH_CNF10_0_N EQU 10 GPIO_CRH_CNF11_0_N EQU 14 GPIO_ODR_ODR0_N EQU 0 GPIO_ODR_ODR2_N EQU 2 |
1 2 3 4 5 |
FLASH_ACR_LATENCY_2_N EQU 1 TIM_DIER_UIE_N EQU 0 TIM_CR1_CEN_N EQU 0 TIM_SR_UIF_N EQU 0 |
1 2 3 4 |
STK_VAL EQU 0x00000008 SETENA0 EQU 0xE000E100 TIM2_IRQn EQU 0x10000000 |
Процедуру инициализации GPIO добавим после процедуры START
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
GPIO_INIT PROC PUSH {R0, R1, R2, R3, R4, LR} MOV R0, #0 MOV R1, #1 LDR R2, =(PERIPH_BB_BASE + (RCC_APB2ENR - PERIPH_BASE) * 32 + RCC_APB2ENR_IOPAEN_N * 4) STR R1, [R2] LDR R2, =(PERIPH_BB_BASE + (RCC_APB2ENR - PERIPH_BASE) * 32 + RCC_APB2ENR_IOPBEN_N * 4) STR R1, [R2] LDR R2,=GPIOA_CRL LDR R3,=(1<<GPIO_CRL_MODE7_0_N) + (1<<GPIO_CRL_MODE6_0_N) + (1<<GPIO_CRL_MODE5_0_N) + (1<<GPIO_CRL_MODE4_0_N) \ + (1<<GPIO_CRL_MODE3_0_N) + (1<<GPIO_CRL_MODE2_0_N) LDR R3,=(1<<GPIO_CRL_MODE7_0_N) + (1<<GPIO_CRL_MODE6_0_N) + (1<<GPIO_CRL_MODE5_0_N) + (1<<GPIO_CRL_MODE4_0_N) \ + (1<<GPIO_CRL_MODE3_0_N) + (1<<GPIO_CRL_MODE2_0_N) LDR R2,=GPIOA_CRL LDR R4, [R2] ORR R4, R4, R3 LDR R3,=(1<<GPIO_CRL_CNF7_0_N) + (1<<GPIO_CRL_CNF6_0_N) + (1<<GPIO_CRL_CNF5_0_N) + (1<<GPIO_CRL_CNF4_0_N) \ + (1<<GPIO_CRL_CNF3_0_N) + (1<<GPIO_CRL_CNF2_0_N) BIC R4, R4, R3 STR R4, [R2] LDR R3,=(1<<GPIO_CRL_MODE1_0_N) + (1<<GPIO_CRL_MODE0_0_N) LDR R2,=GPIOB_CRL LDR R4, [R2] ORR R4, R4, R3 LDR R3,=(1<<GPIO_CRL_CNF1_0_N) + (1<<GPIO_CRL_CNF0_0_N) BIC R4, R4, R3 STR R4, [R2] LDR R2,=GPIOB_CRH LDR R3,=(1<<GPIO_CRH_MODE10_0_N) + (1<<GPIO_CRH_MODE11_0_N) LDR R4, [R2] ORR R4, R4, R3 LDR R3,=(1<<GPIO_CRH_CNF10_0_N) + (1<<GPIO_CRH_CNF11_0_N) BIC R4, R4, R3 STR R4, [R2] POP {R0, R1, R2, R3, R4, PC} ENDP |
В данной процедуре всё стандартно, настройки такие же как у наст были для ножки PC13.
Вызовем данную процедуру в процедуре START
1 2 |
BL SYSTICK_START BL GPIO_INIT |
А вот для работы с таймером мы создадим отдельный модуль. Добавим новый файл tim.s в группу user пока со стандартным содержимым
1 2 3 4 5 6 7 8 |
GET stm32f103C8.asm AREA __data, DATA, READWRITE AREA __tim,CODE,READONLY ALIGN END |
Добавим в данном файле переменную для пользовательского счётчика срабатываний таймера
1 2 3 4 |
AREA __data, DATA, READWRITE TIM2_COUNTER DCD 0 |
Далее добавим процедуру инициализации, пока почти без кода
1 2 3 4 5 6 7 8 9 10 |
AREA __tim,CODE,READONLY TIM2_INIT PROC PUSH {R0, R1, R2, R3, LR} MOV R0, #0 MOV R1, #1 POP {R0, R1, R2, R3, PC} ENDP |
Произведём её экспорт и импортируем её в фале main.s, а затем вызовем её в процедуре START
1 2 |
BL GPIO_INIT BL TIM2_INIT |
С тем же самым таймером мы работали в уроке 168, поэтому нам будет написать процедуру инициализации, а впоследствии и процедуру обработки прерывания значительно легче.
Перейдём в файл tim.s и первым делом в процедуре TIM2_INIT организуем тактирование нашего таймера
1 2 3 4 |
PUSH {R0, R1, R2, R3, LR} LDR R2, =(PERIPH_BB_BASE + (RCC_APB1ENR - PERIPH_BASE) * 32 + RCC_APB1ENR_TIM2EN_N * 4) STR R1, [R2] |
Обнулим пользовательский счётчик
1 2 3 4 |
STR R1, [R2] LDR R3 , =TIM2_COUNTER STR R0, [R3] |
Произведём основные настройки делителя и также регистра с числом, до которого будет наш таймер считать
1 2 3 4 5 6 7 8 |
STR R0, [R3] LDR R2 , =TIM2_BASE LDR R3 , =3600 - 1 STR R3 , [R2 , #TIM2_PSC] LDR R3 , =2000 STR R3 , [R2 , #TIM2_ARR] |
Включим глобальные прерывания таймера, иначе локальные без них работать не будут
1 2 3 4 5 |
STR R3 , [R2 , #TIM2_ARR] LDR R2 , =SETENA0 LDR R3 , =TIM2_IRQn STR R3, [R2] |
Теперь разрешим прерывания от таймера по событию обновления счётчика
1 2 3 4 |
STR R3, [R2] LDR R2, =(PERIPH_BB_BASE + TIM2_DIER * 32 + TIM_DIER_UIE_N * 4) STR R1, [R2] |
Запустим наш таймер
1 2 3 4 |
STR R1, [R2] LDR R2, =(PERIPH_BB_BASE + TIM2_CR1 * 32 + TIM_CR1_CEN_N * 4) STR R1, [R2] |
А вот светодиодами по очереди мы будем мигать в обработчике прерываний, который мы добавим выше процедуры прерывания, пока также практически без кода
1 2 3 4 5 6 7 8 |
AREA __tim,CODE,READONLY TIM2_IRQHandler PROC MOV R0, #0 MOV R1, #1 BX LR ENDP |
Произведём также экспорт данной процедуры и импортируем её в main.s, в котором мы также должны добавить вектор данного прерывания
Чтобы вектор добавился в правильном месте мы должны до него добавить несколько адресов меток бесконечного цикла на случай срабатывания других прерываний
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
DCD ISR_SYSTICK DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD int_vect_terminator DCD TIM2_IRQHandler |
Вернёмся в файл tim.s обработчик прерывания TIM2_IRQHandler и сбросим флаг прерывания
1 2 3 4 |
MOV R1, #1 LDR R2, =(PERIPH_BB_BASE + TIM2_SR * 32 + TIM_SR_UIF_N * 4) STR R0, [R2] |
Наш обработчик будет инкрементировать значение пользовательского счётчика от 0 до 9 и в зависимости от данного значения зажигать соответствующий светодиод, а предыдущий тушить.
Загрузим в регистры процессора адреса ODR второй ножки порта A и нулевой ножки порта B, вернее не сами адреса, а их алиасы для битбэндинга
1 2 3 4 |
STR R0, [R2] LDR R2, =(PERIPH_BB_BASE + (GPIOA_ODR - PERIPH_BASE) * 32 + GPIO_ODR_ODR2_N * 4) LDR R5, =(PERIPH_BB_BASE + (GPIOB_ODR - PERIPH_BASE) * 32 + GPIO_ODR_ODR0_N * 4) |
В другой регистр загрузим значение нашего пользовательского счётчика
1 2 3 4 |
LDR R5, =(PERIPH_BB_BASE + (GPIOB_ODR - PERIPH_BASE) * 32 + GPIO_ODR_ODR0_N * 4) LDR R3 , =TIM2_COUNTER LDR R4 , [R3] |
А вот дальше самое интересное.
Мы должны в зависимости от значения счётчика выполнить соответствующий участок кода, который не очень маленький, можно конечно написать серию условий и переходов, но есть более интересное и оптимальное решение — механизм табличных переходов.
Давайте пока пропустим использование данного механизма и напишем сами участки кодов с метками, иначе нам будет некуда переходить. Причём из этих участков по их окончанию мы должны также перейти в какую-то одну и ту же точку программы, поэтому пропустим и данные участки, добавим пока эту точку — метку и дальнейший код.
В данном коде мы будем инкрементировать значение пользовательского счётчика и сравнивать его с числом 10
1 2 3 4 5 |
LDR R4 , [R3] EXIT01 ADD R4, R4, #1 CMP R4, #10 |
Введём команду условия
1 2 |
CMP R4, #10 IT EQ |
Мы уже знаем из прошлого урока как работает данное условие. Правда, тогда мы использовали NE, которое проверяло, что флаг нуля для срабатывания условия должен быть сброшен. А вот EQ проверит здесь, что флаг нуля установлен.
И если он установлен, то есть результат операции равен нулю, а в случае сравнения результат совпал, то мы тогда занесём в регистр R4 ноль, то есть сбросим наш счётчик и затем занесём значение регистра R4 обратно в счётчик, а если флаг нуля не установится, то мы пропустим команду сброса счётчика и занесём значение неизменённого регистра R4 в переменную счётчика
1 2 3 |
IT EQ MOVEQ R4,#0 STR R4 , [R3] |
Теперь вернёмся назад и добавим все наши 10 участков кода, отвечающие за установку высокого уровня соответствующей значению счётчика ножки порта и установки низкого уровня предыдущей
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
LDR R3 , =TIM2_COUNTER LDR R4 , [R3] TIM2_COUNTER_CASE_0 STR R0, [R5,#44] STR R1, [R2] B EXIT01 TIM2_COUNTER_CASE_1 STR R0, [R2] STR R1, [R2,#4] B EXIT01 TIM2_COUNTER_CASE_2 STR R0, [R2,#4] STR R1, [R2,#8] B EXIT01 TIM2_COUNTER_CASE_3 STR R0, [R2,#8] STR R1, [R2,#12] B EXIT01 TIM2_COUNTER_CASE_4 STR R0, [R2,#12] STR R1, [R2,#16] B EXIT01 TIM2_COUNTER_CASE_5 STR R0, [R2,#16] STR R1, [R2,#20] B EXIT01 TIM2_COUNTER_CASE_6 STR R0, [R2,#20] STR R1, [R5] B EXIT01 TIM2_COUNTER_CASE_7 STR R0, [R5] STR R1, [R5,#4] B EXIT01 TIM2_COUNTER_CASE_8 STR R0, [R5,#4] STR R1, [R5,#40] B EXIT01 TIM2_COUNTER_CASE_9 STR R0, [R5,#40] STR R1, [R5,#44] |
Понятное дело, что в последнем кейсе мы не используем переход, так как метка EXIT01 следует за последней его командой.
Осталось дело за малым: перейти в зависимости от значения счётчика в соответствующий кейс.
Для этого нам нужно будет познакомиться с командами табличных переходов
В качестве первого операнда используется регистр, в котором находится адрес таблицы для наших ветвей, если таблица адресов, а вернее смещений относительно текущего места, следует сразу же за командой TBB или TBH, то мы используем регистр PC и в данном случае адресом таблицы является адрес инструкции + 4.
Различие между инструкциями TBB и TBH во том, что первая работает со смещением не больше байта, то есть использует короткие смещения (в данном случае адрес перехода — это значение регистра Rm, умноженное на 2 плюс адрес начала таблицы), а вторая — со смещением не больше полуслова, также в данном случае обязательным является использование ещё и сдвига влево. В качестве регистра Rm не могут выступать регистры PC и SP. Команда LSL #1 производит сдвиг влево значения регистра Rm, которое затем и используется в качестве смещения.
Думаю, что с длинными смещениями мы тоже когда-нибудь столкнёмся, а пока введём первую команду в нашем коде
1 2 3 |
LDR R4 , [R3] TBB [PC, R4] |
Мы отлично помним, что в регистре R4 на данный момент у нас содержится значение нашего счётчика.
Дальше добавим метку начала нашей таблицы переходов
1 2 |
TBB [PC, R4] TIM2_COUNTER_TABLE_START |
А теперь добавим значение смещения перового перехода. Нам придётся его вычислять прямо в коде, так как при изменении любого участка кода, следующего до данного значения адрес перехода изменится. Так как мы знаем, что значение смещения — это расстояние в байтах между началом таблицы и адресом перехода, умноженное на 2, то в таблицу мы должны занести значение, наоборот, разделённое на 2. Поэтому наш первый переход будет выглядеть примерно вот таким образом
1 2 |
TIM2_COUNTER_TABLE_START DCB (TIM2_COUNTER_CASE_0 - TIM2_COUNTER_TABLE_START)/2 |
Аналогичным образом будут выглядеть и смещения остальных переходов
1 2 3 4 5 6 7 8 9 10 |
DCB (TIM2_COUNTER_CASE_0 - TIM2_COUNTER_TABLE_START)/2 DCB (TIM2_COUNTER_CASE_1 - TIM2_COUNTER_TABLE_START)/2 DCB (TIM2_COUNTER_CASE_2 - TIM2_COUNTER_TABLE_START)/2 DCB (TIM2_COUNTER_CASE_3 - TIM2_COUNTER_TABLE_START)/2 DCB (TIM2_COUNTER_CASE_4 - TIM2_COUNTER_TABLE_START)/2 DCB (TIM2_COUNTER_CASE_5 - TIM2_COUNTER_TABLE_START)/2 DCB (TIM2_COUNTER_CASE_6 - TIM2_COUNTER_TABLE_START)/2 DCB (TIM2_COUNTER_CASE_7 - TIM2_COUNTER_TABLE_START)/2 DCB (TIM2_COUNTER_CASE_8 - TIM2_COUNTER_TABLE_START)/2 DCB (TIM2_COUNTER_CASE_9 - TIM2_COUNTER_TABLE_START)/2 |
Вот и всё. Надеюсь, объяснил нормально. Хотя я, признаться, подобного объяснения нигде не нашел, даже в англоязычной документации (наверно искал плохо).
Соберём наш код, прошьём контроллер и увидим, что наши светодиоды побегут друг за другом
Таким образом, сегодня мы научились использовать аппаратные таймеры в наших программах, также закрепили свои знания в области использования механизма прерываний и также познакомились с такой интересной возможностью, как использование в программах табличных переходов.
Всем спасибо за внимание!
Предыдущий урок Программирование МК STM32 Следующий урок
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
А возможны ли в данном ассемблере многострочные комментарии?
PS
С ассемблером начинаю чётко и ясно понимать CMSIS и RM0008.
Надо для себя многое комментировать чтобы не забыть.
на keil.com не нашел (может искал плохо)
/* */ не работает
%ifdef COMMENT
комментируемый код — не работает
%endif
COMMENT *
комментарий — не работает
*
Дорогой Автор. Спасибо вам за такой чудесный и здравый ресурс по стм нигде в англоязычном инете я такого не встречал. У меня есть тоже несколько проектов могу с вами поделиться.
Пожалуйста продолжайте это дело несите свет в массы. Искренне вам благодарен