Продолжим осваивать язык ассемблера для архитектуры ARM.
Сегодня мы попытаемся включить наш контроллер на полную мощность, настроив в нём механизм тактирования — модуль RCC. Настройка данного модуля даст нам возможность знать точно, какие шины и какая периферия на какой частоте будет у нас работать.
Благодаря данной настройке, мы также познакомимся со стеком, для чего он нужен, как он работает, а также научимся вызывать процедуры из других модулей и изучим некоторые новые для нас команды и директивы.
Схема урока наша не изменилась
А проект для урока мы сделаем из проекта прошлого урока с именем ASM_BLINK01 и присвоим ему имя ASM_BLINK01_RCC.
Так как по RCC кода ожидается много, то давайте работу с ним оформим в отдельном модуле, создав и добавив для этого файл rcc.s в группу user дерева проекта
Подключим в данном файле сразу файл со значениями
1 |
GET stm32f103C8.asm |
Добавим начало новой области
1 2 |
GET stm32f103C8.asm AREA __clk,CODE,READONLY |
Как мы знаем из урока 166 по RCC с использованием библиотеки CMSIS, что прежде чем начинать инициализацию RCC, желательно сначала сбросить его настройки, то есть проделать обратную операцию – деинициализацию.
Для этого добавим новую процедуру в нашем новом файле
1 2 3 4 5 |
AREA __clk,CODE,READONLY RCC_DEINIT PROC ENDP |
Также мы должны добавить окончание нашего кода в конце файла, перед этим применив директиву выравнивания
1 2 3 4 5 |
ENDP ALIGN END |
Так как процедуру деинициализации RCC мы можем вызвать и позже в любой момент, то желательно бы сохранить состояние регистров, которые мы планируем использовать в данной процедуре, чтобы после возврата вернуть их первоначальное значение для того, чтобы в коде, где данная подпрограмма будет вызвана, не было проблем с искажением значений регистров.
Для таких ситуаций удобнее всего использовать стек, хотя можно воспользоваться и обычной оперативной памятью. Стек работает быстрее по многим причинам, одна из которых та, что команда короче.
Что же такое стек?
В принципе, не следовало бы в таких уроках вообще подробно вести разговор о стеке, так как это элементарная информатика. Но немного всё же расскажу. Существует две основных модели хранилищ или буферов — FIFO и LIFO.
FIFO (First In, First Out) — это хранилище работающее по принципу «первым вошел, первым вышел». То есть, когда мы выбираем из такого буфера значение, то первым выбирается то значение, которое и пришло первым, затем следующее и так далее
LIFO (Last In, First Out) — данное хранилище уже работает по принципу «первым вошел, последним вышел». То есть, когда мы выбираем из такого буфера значение, то первым выбирается то значение, которое пришло туда последним, затем предпоследнее и так далее
Данные буферы можно сравнить с трубкой, в которую закатывают шарики по одному, так как у неё диаметр такой, что по два не закатишь и по два не вынешь, также шарики в трубке поменяться в связи с этим тоже не могут. Из первой трубки шарики забирать можно только из противоположного отверстия, а из приёмного нельзя, а у второй трубки противоположное отверстие запаяно, поэтому забирать шарики можно только из приёмного отверстия. Вот и получается, что из первой трубки мы забираем шарик, который из всех присутствующих там пришел первым, а из второй — последним.
Так вот стек (stack) относится ко второму типу хранилищ — LIFO.
Также стек ещё многие сравнивают с магазином автомата, из которого при стрельбе поступает в патронник тот патрон, который был помещён туда последним, а последним поступит тот патрон, который был помещён в магазин первым.
Ещё можно стек сравнить с бочкой, в которую погружаются круги и взять оттуда мы, соответственно, можем тот круг, который положили первым.
Только вот работа процессора со стеком происходит немного сложнее, чтобы не нужно было использовать какую-то пружину, как в магазине автомата, выталкивающую к вершине данные, как патроны, а также в случае бочки не наклоняться слишком низко за самым последним кругом, который лежит на дне и который пришел самым первым. Здесь происходит всё по принципу перемещения вершины. То есть здесь больше подходит бочка, так как регистр SP хранит адрес последнего помещённого в стек значения, а когда мы данное значение выбираем, оно остаётся в памяти и не удаляется, просто в регистр SP записывается адрес предыдущего помещённого в стек значения. А когда мы помещаем значение в стек, то в регистр SP записывается адрес помещённого значения, то есть следующий адрес памяти.
Это делается для того, чтобы не перемещать данные в памяти, засчёт этого экономится процессорное время.
Ну, думаю, со стеком теперь стало немного понятнее.
Для того чтобы поместить какие-то данные в стек, мы используем команду PUSH
PUSH{cond} reglist
А для того, чтобы данные забирать из стека, то мы используем команду POP
POP{cond} reglist
Данные команды работают сразу со списком регистров. Использоваться могут именно только регистры, с другими данными стек не работает. Также, как видно из описания команд, они могут использовать условия, только в результате выполнения которых команды выполнятся.
Давайте поместим значения регистров, с которыми мы собираемся работать в нашей подпрограмме, в стек с помощью следующей команды
1 2 |
RCC_DEINIT PROC PUSH {R0, R1, R2, R3, LR} |
Плюс ко всем регистрам общего назначения мы используем регистр LR, который хранит значение адреса команды, следующей за командой вызова подпрограммы. Делается это для того, чтобы сэкономить на использовании команды BX LR, которую нам вызывать впоследствии необязательно.
А в конце нашей подпрограммы мы заберём сохранённые значения из стека обратно в регистры
1 2 |
POP {R0, R1, R2, R3, PC} ENDP |
Хотя стек работает по принципу последним пришел — первым вышел, тем не менее в команде POP мы используем тот же порядок, что и в команде PUSH, так устроена работа команд. Причём мы можем например забрать данные в стек из одного регистра, а потом поместить их в другой, как мы поступили с регистром LR, значение которого мы возвращаем из стека уже не в него, а в регистр PC, в результате чего мы вернёмся в то место, адрес которого был в регистре LR, то есть будем выполнять команду, следующую за командой вызова подпрограммы.
Теперь мы должны вызвать нашу подпрограмму в модуле main.s. Только вызвать её будет не просто, потому что код подпрограммы находится в другом модуле. Для этого мы сначала должны экспортировать нашу процедуру. Добавим перед директивой окончания файла в файле rcc.s следующую директиву
1 2 3 4 5 |
ALIGN EXPORT RCC_DEINIT END |
А в файле main.s добавим директиву, с помощью которой мы произведём импорт имени нашей процедуры деинициализации
1 2 3 |
AREA |.text|, CODE, READONLY EXTERN RCC_DEINIT |
Вызовем в main.s нашу подпрограмму в самом начале кода
1 2 |
Start PROC BL RCC_DEINIT |
Соберём код, прошьём контроллер и в отладке попробуем попасть в нашу процедуру, установив предварительно точку останова здесь
Запустим отладку и посмотрим, как выглядит команда помещения значений регистров в стек, а заодно и команда извлечения их оттуда
Команды данные занимают в коде всего лишь по 2 байта. Думаю, если бы мы сохраняли значения такого состояния регистров в обычную память, то мы бы не отделались таким лёгким испугом.
Запомним состояние регистров, данные которых помещаются в стек, а также адрес указателя стека
Адрес указателя стека у нас соответствует адресу его вершины, так как мы пока ничего в стек не помещали.
Теперь прошагаем команду помещения значений в стек и посмотрим, что у нас изменится
Адрес указателя стека у нас переместился ниже на 20 байт, поэтому мы можем смело предположить, что данные сохранились в область памяти между адресами 0x200003EC и 0x20000400.
Давайте взглянем на содержимое памяти в этих адресах. Для удобного просмотра давайте данные будем отображать в словах
И теперь мы видим, что значения наших регистров надёжно улеглись в памяти, отведённой под стек
Выполним команду POP, сделав ещё один шаг и увидим, что указатель выполнения программы теперь находится на следующей команде, так как теперь значение, которое было в регистре LR, находится в регистре PC, а также значение адреса указателя стека вернулось к адресу вершины
Что ж, вернёмся в нашу процедуру RCC_DEINIT файла rcc.s и занесём в регистры R0 и R1 значения 0 и 1, так как мы процедуру вызвали до занесения таких данных, а также данную процедуру (подпрограмму) мы можем вызвать и позднее в нашем коде, где неизвестно, что содержится в данных регистрах
1 2 3 |
PUSH {R0, R1, R2, R3, LR} MOV R0, #0 MOV R1, #1 |
Далее действуем по алгоритму урока 166, только используя при этом язык ассемблер.
Включим для начала HSI (внутренний генератор 8 МГц), для чего нам надо будет добавить номер бита в файл stm32f103C8.asm. Давайте, чтобы не мучиться каждый раз с добавления туда значений, мы сразу добавим все значения, нужные для данного урока. После этого данный файл станет следующего содержания
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 43 44 45 46 47 48 49 50 |
PERIPH_BB_BASE EQU 0x42000000 PERIPH_BASE EQU 0x40000000 AFIO_MAPR EQU 0x40010004 RCC_CR EQU 0x40021000 RCC_CFGR EQU 0x40021004 RCC_APB2ENR EQU 0x40021018 RCC_CSR EQU 0x40021024 GPIOC_CRH EQU 0x40011004 GPIOC_ODR EQU 0x4001100C FLASH_ACR EQU 0x40022000 RCC_APB2ENR_AFIOEN_N EQU 0 RCC_APB2ENR_IOPCEN_N EQU 4 RCC_CR_HSION_N EQU 0 RCC_CR_HSIRDY_N EQU 1 RCC_CR_HSEON_N EQU 16 RCC_CR_HSERDY_N EQU 17 RCC_CR_HSEBYP_N EQU 18 RCC_CR_PLLON_N EQU 24 RCC_CR_HSITRIM EQU 0x000000F8 RCC_CR_HSEON EQU 0x00010000 RCC_CR_HSERDY EQU 0x00020000 RCC_CR_CSSON EQU 0x00080000 RCC_CR_PLLRDY EQU 0x02000000 RCC_CFGR_SW EQU 0x00000003 RCC_CFGR_SW_PLL EQU 0x00000002 RCC_CFGR_SWS EQU 0x0000000C RCC_CFGR_HPRE EQU 0x000000F0 RCC_CFGR_PPRE1 EQU 0x00000700 RCC_CFGR_PPRE1_DIV2 EQU 0x00000400 RCC_CFGR_PPRE2 EQU 0x00003800 RCC_CFGR_PLLSRC EQU 0x00010000 RCC_CFGR_PLLXTPRE EQU 0x00020000 RCC_CFGR_PLLMULL EQU 0x003C0000 RCC_CFGR_PLLMULL9 EQU 0x001C0000 RCC_CSR_RMVF_N EQU 24 AFIO_MAPR_SWJ_CFG EQU 0x07000000 AFIO_MAPR_SWJ_CFG_JTAGDISABLE_N EQU 25 GPIO_CRH_MODE13_0_N EQU 20 GPIO_ODR_ODR13_N EQU 13 FLASH_ACR_PRFTBE_N EQU 4 FLASH_ACR_LATENCY EQU 0x03 FLASH_ACR_LATENCY_2_N EQU 1 END |
Установим бит отвечающий за включение HSI
1 2 3 4 5 |
MOV R1, #1 ;HSI On LDR R2, =(PERIPH_BB_BASE + (RCC_CR - PERIPH_BASE) * 32 + RCC_CR_HSION_N * 4) STR R1, [R2] |
Теперь нам необходимо дождаться стабилизации HSI.
Здесь уже работаем без бит-бэндинга, с реальными адресами регистров и их битами
Занесём в регистр R2 адрес регистра RCC_CR
1 2 3 |
STR R1, [R2] LDR R2, =RCC_CR |
Добавим метку
1 2 |
LDR R2, =RCC_CR wait_hsirdy |
Прочитаем регистр RCC_CR в регистр R3
1 2 |
wait_hsirdy LDR R3,[R2] ; читаем регистр RCC_CR |
А теперь применим новую команду TST, которая проверяет значение битов по маске
TST{cond} Rn, Operand2
Здесь происходит логическое умножение регистра и второго операнда, только результат никуда не записывается и содержание регистра остаётся неизменным. Команда независимо от отсутствия префикса S всегда влияет на флаги.
С помощью такой команды мы проверим, включен ли у нас бит RCC_CR_HSIRDY
1 2 |
LDR R3,[R2] ; читаем регистр RCC_CR TST R3,#(1<<RCC_CR_HSIRDY_N) |
И если результат будет нулевой (Z=1, бит не включен), то мы перейдём назад на метку, выполнив следующую команду
1 2 |
TST R3,#(1<<RCC_CR_HSIRDY_N) BEQ wait_hsirdy |
Таким образом, мы будем крутиться в данном цикле, пока бит не включится.
Дальше нам нужно будет сбросить калибровку, для чего мы должны в регистре RCC_CR сбросить битовое поле RCC_CR_HSITRIM.
Сбросить биты по маске мы можем с помощью команды BIC
BIC{S}{cond} Rd, Rn, Operand2
Данная команда сбрасывает биты, установленные в значении операнда 2, в значении регистра Rn, при этом значение данного регистра остаётся неизменным, а результат записывается в регистр Rd.
Тем самым данная команда работает аналогично тому, когда мы в C для того, чтобы сбросить биты, сначала инвертировали маску, а затем использовали логическое И.
Сбросим с помощью данной команды все биты битового поля RCC_CR_HSITRIM
1 2 3 4 5 |
BEQ wait_hsirdy ;Reset internal high-speed clock trimming LDR R3, [R2] BIC R3, R3, #RCC_CR_HSITRIM |
Адрес регистра нам теперь не нужно заносить в регистр R2, он у нас там уже есть.
Теперь мы должны установить самый старший бит этого же битового поля.
Для этого существует арифметическая операция логического ИЛИ и выглядит она следующим образом
ORR{S}{cond} Rd, Rn, Operand2
Данная команда производит операцию ИЛИ между значением регистра Rn и операндом 2, а результат записывает в регистр Rd.
С помощью данной команды установим необходимый бит
1 2 |
BIC R3, R3, #RCC_CR_HSITRIM ORR R3, R3, #0x80 |
Данные операции можно, в принципе, объединить в одну. Для этого в ассемблере существуют операции над битовыми полями, но с ними мы будем, возможно, знакомиться в более поздних занятиях.
Также мы знаем, что произведя данные операции над значением регистра контроллера, мы это значение ещё не изменили, мы пока работаем в регистрах ядра. Чтобы применить изменения, нужно сохранить значение в регистр RCC_CR
1 2 |
ORR R3, R3, #0x80 STR R3, [R2] |
Далее нам нужно полностью очистить конфигурационный регистр RCC_CFGR.
Это сделать несложно
1 2 3 4 5 |
STR R3, [R2] ;Clear RCC_CFGR LDR R2, =RCC_CFGR STR R0, [R2] |
Теперь нам необходимо дождаться очистки бита SWS регистра RCC_CFGR.
Очистку битов мы ждём немного не так, как установку.
Для начала мы также запишем адрес регистра в R2, установим метку и считаем значение регистра в R3
1 2 3 4 5 |
STR R0, [R2] LDR R2, =RCC_CFGR wait_cfgrsws LDR R3,[R2] ; читаем регистр RCC_CFGR |
Чтобы узнать то, что бит пока ещё установлен, мы применяем операцию логического И. Она выглядит следующим образом
AND{S}{cond} Rd, Rn, Operand2
Выполняется данная команда аналогично команде ORR, только вместо логического ИЛИ выполняется логическое И. Результат также записывается в регистр Rd.
Выполним такую команду, чтобы узнать состояние нашего бита
1 2 |
LDR R3,[R2] ; читаем регистр RCC_CFGR AND R3, R3, #RCC_CFGR_SWS |
Далее для того, чтобы узнать, равен ли результат операции нулю, мы можем также применить команду сравнения CMP.
Также есть аналогичная команда, но с инвертированием результата CWN. Вот так выглядят данные команды
CMP{cond} Rn, Operand2
CMN{cond} Rn, Operand2
Данные команды результат никуда не записывают, они только влияют на флаги. Например, команда CMP работает аналогично команде вычитания SUBS, только результат она ни в какой регистр не записывает, а влияет только на флаги.
Применим данную команду, чтобы узнать, установлен ли бит по прежнему
1 2 |
AND R3, R3, #RCC_CFGR_SWS CMP R3, #0 |
Если бит установлен, то результат окажется положительным и флаг нуля не установится (Z=0). В этом случае мы перейдём к метке
1 2 |
CMP R3, #0 BNE wait_cfgrsws |
В следующей части урока мы настроим отладчик, FLASH и HSE.
Предыдущий урок Программирование МК STM32 Следующая часть
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Добавить комментарий