STM Урок 203. Assembler. RCC. Стек. Новые команды. Часть 1



Продолжим осваивать язык ассемблера для архитектуры ARM.

Сегодня мы попытаемся включить наш контроллер на полную мощность, настроив в нём механизм тактирования — модуль RCC. Настройка данного модуля даст нам возможность знать точно, какие шины и какая периферия на какой частоте будет у нас работать.

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

Схема урока наша не изменилась

 

 

А проект для урока мы сделаем из проекта прошлого урока с именем ASM_BLINK01 и присвоим ему имя ASM_BLINK01_RCC.

Так как по RCC кода ожидается много, то давайте работу с ним оформим в отдельном модуле, создав и добавив для этого файл rcc.s в группу user дерева проекта

 

 

Подключим в данном файле сразу файл со значениями

 

 

Добавим начало новой области

 

 

Как мы знаем из урока 166 по RCC с использованием библиотеки CMSIS, что прежде чем начинать инициализацию RCC, желательно сначала сбросить его настройки, то есть проделать обратную операцию – деинициализацию.

Для этого добавим новую процедуру в нашем новом файле

 

 

Также мы должны добавить окончание нашего кода в конце файла, перед этим применив директиву выравнивания

 

 

Так как процедуру деинициализации 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

 

Данные команды работают сразу со списком регистров. Использоваться могут именно только регистры, с другими данными стек не работает. Также, как видно из описания команд, они могут использовать условия, только в результате выполнения которых команды выполнятся.

Давайте поместим значения регистров, с которыми мы собираемся работать в нашей подпрограмме, в стек с помощью следующей команды

 

 

Плюс ко всем регистрам общего назначения мы используем регистр LR, который хранит значение адреса команды, следующей за командой вызова подпрограммы. Делается это для того, чтобы сэкономить на использовании команды BX LR, которую нам вызывать впоследствии необязательно.

А в конце нашей подпрограммы мы заберём сохранённые значения из стека обратно в регистры

 

 

Хотя стек работает по принципу последним пришел — первым вышел, тем не менее в команде POP мы используем тот же порядок, что и в команде PUSH, так устроена работа команд. Причём мы можем например забрать данные в стек из одного регистра, а потом поместить их в другой, как мы поступили с регистром LR, значение которого мы возвращаем из стека уже не в него, а в регистр PC, в результате чего мы вернёмся в то место, адрес которого был в регистре LR, то есть будем выполнять команду, следующую за командой вызова подпрограммы.

Теперь мы должны вызвать нашу подпрограмму в модуле main.s. Только вызвать её будет не просто, потому что код подпрограммы находится в другом модуле. Для этого мы сначала должны экспортировать нашу процедуру. Добавим перед директивой окончания файла в файле rcc.s следующую директиву

 

 

 

А в файле main.s добавим директиву, с помощью которой мы произведём импорт имени нашей процедуры деинициализации

 

 

Вызовем в main.s нашу подпрограмму в самом начале кода

 

 

Соберём код, прошьём контроллер и в отладке попробуем попасть в нашу процедуру, установив предварительно точку останова здесь

 

 

Запустим отладку и посмотрим, как выглядит команда помещения значений регистров в стек, а заодно и команда извлечения их оттуда

 

 

Команды данные занимают в коде всего лишь по 2 байта. Думаю, если бы мы сохраняли значения такого состояния регистров в обычную память, то мы бы не отделались таким лёгким испугом.

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

 

 

Адрес указателя стека у нас соответствует адресу его вершины, так как мы пока ничего в стек не помещали.

Теперь прошагаем команду помещения значений в стек и посмотрим, что у нас изменится

 

 

Адрес указателя стека у нас переместился ниже на 20 байт, поэтому мы можем смело предположить, что данные сохранились в область памяти между адресами 0x200003EC и 0x20000400.

Давайте взглянем на содержимое памяти в этих адресах. Для удобного просмотра давайте данные будем отображать в словах

 

 

И теперь мы видим, что значения наших регистров надёжно улеглись в памяти, отведённой под стек

 

 

Выполним команду POP, сделав ещё один шаг и увидим, что указатель выполнения программы теперь находится на следующей команде, так как теперь значение, которое было в регистре LR, находится в регистре PC, а также значение адреса указателя стека вернулось к адресу вершины

 

 

Что ж, вернёмся в нашу процедуру RCC_DEINIT файла rcc.s и занесём в регистры R0 и R1 значения 0 и 1, так как мы процедуру вызвали до занесения таких данных, а также данную процедуру (подпрограмму) мы можем вызвать и позднее в нашем коде, где неизвестно, что содержится в данных регистрах

 

 

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

 

 

Включим для начала HSI (внутренний генератор 8 МГц), для чего нам надо будет добавить номер бита в файл stm32f103C8.asm. Давайте, чтобы не мучиться каждый раз с добавления туда значений, мы сразу добавим все значения, нужные для данного урока. После этого данный файл станет следующего содержания

 

 

Установим бит отвечающий за включение HSI

 

 

Теперь нам необходимо дождаться стабилизации HSI.

Здесь уже работаем без бит-бэндинга, с реальными адресами регистров и их битами

Занесём в регистр R2 адрес регистра RCC_CR

 

 

Добавим метку

 

 

Прочитаем регистр RCC_CR в регистр R3

 

 

А теперь применим новую команду TST, которая проверяет значение битов по маске

 

TST{cond} Rn, Operand2

 

Здесь происходит логическое умножение регистра и второго операнда, только результат никуда не записывается и содержание регистра остаётся неизменным. Команда независимо от отсутствия префикса S всегда влияет на флаги.

С помощью такой команды мы проверим, включен ли у нас бит RCC_CR_HSIRDY

 

 

И если результат будет нулевой (Z=1, бит не включен), то мы перейдём назад на метку, выполнив следующую команду

 

 

Таким образом, мы будем крутиться в данном цикле, пока бит не включится.

Дальше нам нужно будет сбросить калибровку, для чего мы должны в регистре RCC_CR сбросить битовое поле RCC_CR_HSITRIM.

Сбросить биты по маске мы можем с помощью команды BIC

 

BIC{S}{cond} Rd, Rn, Operand2

 

Данная команда сбрасывает биты, установленные в значении операнда 2, в значении регистра Rn, при этом значение данного регистра остаётся неизменным, а результат записывается в регистр Rd.

Тем самым данная команда работает аналогично тому, когда мы в C для того, чтобы сбросить биты, сначала инвертировали маску, а затем использовали логическое И.

Сбросим с помощью данной команды все биты битового поля RCC_CR_HSITRIM

 

 

Адрес регистра нам теперь не нужно заносить в регистр R2, он у нас там уже есть.

Теперь мы должны установить самый старший бит этого же битового поля.

Для этого существует арифметическая операция логического ИЛИ и выглядит она следующим образом

 

ORR{S}{cond} Rd, Rn, Operand2

 

Данная команда производит операцию ИЛИ между значением регистра Rn и операндом 2, а результат записывает в регистр Rd.

С помощью данной команды установим необходимый бит

 

 

Данные операции можно, в принципе, объединить в одну. Для этого в ассемблере существуют операции над битовыми полями, но с ними мы будем, возможно, знакомиться в более поздних занятиях.

Также мы знаем, что произведя данные операции над значением регистра контроллера, мы это значение ещё не изменили, мы пока работаем в регистрах ядра. Чтобы применить изменения, нужно сохранить значение в регистр RCC_CR

 

 

Далее нам нужно полностью очистить конфигурационный регистр RCC_CFGR.

Это сделать несложно

 

 

Теперь нам необходимо дождаться очистки бита SWS регистра RCC_CFGR.

Очистку битов мы ждём немного не так, как установку.

Для начала мы также запишем адрес регистра в R2, установим метку и считаем значение регистра в R3

 

 

Чтобы узнать то, что бит пока ещё установлен, мы применяем операцию логического И. Она выглядит следующим образом

AND{S}{cond} Rd, Rn, Operand2

Выполняется данная команда аналогично команде ORR, только вместо логического ИЛИ выполняется логическое И. Результат также записывается в регистр Rd.

Выполним такую команду, чтобы узнать состояние нашего бита

 

 

Далее для того, чтобы узнать, равен ли результат операции нулю, мы можем также применить команду сравнения CMP.

Также есть аналогичная команда, но с инвертированием результата CWN. Вот так выглядят данные команды

 

CMP{cond} Rn, Operand2
CMN{cond} Rn, Operand2

 

Данные команды результат никуда не записывают, они только влияют на флаги. Например, команда CMP работает аналогично команде вычитания SUBS, только результат она ни в какой регистр не записывает, а влияет только на флаги.

Применим данную команду, чтобы узнать, установлен ли бит по прежнему

 

 

Если бит установлен, то результат окажется положительным и флаг нуля не установится (Z=0). В этом случае мы перейдём к метке

 

 

В следующей части урока мы настроим отладчик, FLASH и HSE.

 

 

Предыдущий урок Программирование МК STM32 Следующая часть

 

Исходный код

 

 

Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6

Программатор недорогой можно купить здесь ST-Link V2

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

 

STM Assembler. RCC. Стек. Новые команды

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*