STM32F030. Светодиодоморгание

Мы вплотную подошли к написанию программы «Здравствуй, Мир!». В отношении железа и микроконтроллеров эта программы вырождается в моргание светодиодом. Этим и займёмся.

Давайте, чтобы не порушить предыдущие наработки (мало-ли — может придется к ним вернуться!), создадим еще одну директорию и поместим в нее несколько файлов.

Один из них будет файл start-0.S, только я его у себя на компе переименую в start-up.S. Мы его немного изменим под наш проект.

Следующий файл, который нам понадобится — linked.ld. Мы его изменять не будем.

Третий файл будет называться blinky.S. Создайте его пока пустым. Через несколько минут мы наполним его содержанием.

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

blinky-dir

Начнем с файла linked.ld. Вот его содержимое:

/* Зададим выходной формат поумолчанию elf32-littlearm */
OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm", "elf32-littlearm")

/* Определим целевую архитектуру */
OUTPUT_ARCH(arm)

/* Зададим точку входа */
ENTRY(Reset_Handler)

/* Теперь займемся распределением памяти */
SECTIONS
{
/* Флешь в STM32F030 начинается с адреса 0x08000000 */
.flash 0x08000000 :
{
/* Скажем Линковщику собрать таблицу обработчиков векторов прерываний на этот адрес */
KEEP(*(.vector_table));

/* Затем укажем Линковщику собрать все секции исполняемого кода в одну кучку */
*(.text .text.*);

/* Затем -- собрать все константы в другую кучку */
*(.rodata .rodata.*);
}

/* Оперативная память в STM32F030 начинается с адреса 0x20000000. */
/* Отсюда и начнем размечать память для размещения переменных */

/* Сначала должна разместиться секция инициализированных данных. */
/****************************************************************************************
Понятно, что начальные значения (значения для инициализации) должны где-то
храниться, когда питание микроконтроллера выключено. Они и храняться во флешь-пямяти.
Но флешь-память -- постоянная память, а нам для размещения переменых нужна опреативная
память. Вот, в оперативной память мы и разместим переменные, а проинициализируем их
значениями из флешь-памяти. Хитрожопо, но по другому никак!
*****************************************************************************************/
.data_at_ram 0x20000000: AT (LOADADDR(.flash) + SIZEOF(.flash))
{
*(.data .data.*);
}

.ram :
{
/* Теперь скажем Линковщику собрать в этом месте все неинициализированные глобальные и
статические переменные */
*(.bss .bss.*);
}

/* Теперь установим указатель стека на конец оперативной памяти.
TOS = top of stack -- вершина стека.
BOS = bottom of stack -- дно стека.
В момент инициализации вершина стека совпадает с его дном.
Конец оперативной памяти зависит от конкретного типа микроконтроллера.
Например, у меня SRM32F030F4, который имеет 4 кБ оперативы.
*/
__Initial_Stack_Value = 0x20000000 + (4 * 1024); /* Нехитрые вычисления дают верный результат*/
}

Следующий файл start-up.S. Назначений этого файла — отделить работу по инициализации микроконтроллера (установка стека, установка тактовой частоты, установка обработчиков прерываний и т.д.) от той работы, ради которой, собственно, был задуман проект. Этот тип работы часто называют — бизнес-логикой.

Файл start-0.S из предыдущего проекта нам не очень подходит. В нем уровень бизнес логики отсутствует, а то место, где должен быть переход на функцию main(), вырожден в бесконечный цикл.

Давайте в качестве основы возьмем start-0.S, допишем в него следующий фрагмент:

Reset_Handler:
/* Пока не делаем никакую предварительную подготовку, а тупо вызываем функцию main() */
LDR R0, =main /* В регистр R0 помещаем адрес функции */
BLX R0 /* Осуществляем переход на адрес, записанный в R0 */

/* Если даже main() случайно вернет управление, те есть произойдет из нее выход,
мы должны остановить дальнейшее исполнение кода. */
B . /* Тупо зациклимся на месте */

Вот содержимое файла start-up.S полностью:

/* Самый простой start-up файл. */
/* Из него вызывается другой файл, который содержит функцию main() */

/* Инструкции Thumb-2 поддерживаются только в режиме синтаксиса unified */
.syntax unified

/* Таблица векторов обработчиков прерываний */
.section ".vector_table"
.long __Initial_Stack_Value /* Вершина стека */
.long Reset_Handler /* Обработчик Reset */
.long NMI_Handler /* Обработчик NMI */
.long HardFault_Handler /* Обработчик Hard Fault */
.long MemManage_Handler /* Обработчик MPU Fault */
.long BusFault_Handler /* Обработчик Bus Fault */
.long UsageFault_Handler /* Обработчик Usage Fault */
.long 0 /* Не используется */
.long 0 /* Не используется */
.long 0 /* Не используется */
.long 0 /* Не используется */
.long SVC_Handler /* Обработчик SVCall Handler */
.long DebugMon_Handler /* Обработчик Debug Monitor Handler */
.long 0 /* Не используется */
.long PendSV_Handler /* Обработчик PendSV*/
.long SysTick_Handler /* Обработчик SysTick */

/* Секция программного кода */
.section ".text"

@-----------------------------------------------------------------------------
/* Отсюда начнет выполняться наша программа после сброса */
/* Объявим функцию как thumb_func. Иначе линковка не будет успешной */
.thumb_func

/* Чтобы Линковщик увидел символ Reset_Handler мы должны объявить его как global */
.global Reset_Handler

Reset_Handler:
/* Пока не делаем никакую предварительную подготовку, а тупо вызываем функцию main() */
LDR R0, =main /* В регистр R0 помещаем адрес функции */
BLX R0 /* Осуществляем переход на адрес, записанный в R0 */

/* Если даже main() случайно вернет управление, те есть произойдет из нее выход,
мы должны остановить дальнейшее исполнение кода. */
B . /* Тупо зациклимся на месте */

@-----------------------------------------------------------------------------
/* Обработчики прерываний */
/* Сделаем как делают ленивые парни -- направим все прерывания на один обработчик */
/* Объявляем все функции как thumb_func */
.thumb_func
NMI_Handler:

.thumb_func
HardFault_Handler:

.thumb_func
MemManage_Handler:

.thumb_func
BusFault_Handler:

.thumb_func
UsageFault_Handler:

.thumb_func
SVC_Handler:

.thumb_func
DebugMon_Handler:

.thumb_func
PendSV_Handler:

.thumb_func
SysTick_Handler:
B . /* и тоже тупо замкнем его в бесконечном цикле */

Как видите, ничего сложного нет. Теперь перейдем к созданию файла blinky.S. Вот его исходный текст. Я предлагаю вам, сначала пробежаться по нему глазами, а потом я немного поясню тонкие моменты — откуда что взялось.

/* blinky.S */

/* Инструкции Thumb-2 поддерживаются только в режиме синтаксиса unified */
	.syntax unified

/* Все следующие команды должны попасть в секцию .text */
	.section ".text"

@-----------------------------------------------------------

/* Объявим main() как глобальное имя. Иначе Линковщик не сможет его найти */
	.global main

/* Объявим функцию как thumb_func. Иначе линковка не будет успешной */
	.thumb_func

/* Это еще не аналог С-шной функции main(), но механизм вызова примерно такой же */
main:
	PUSH	{LR}             /* Сохраним в стеке адрес возврата */

	LDR     R0, =init_gpio   /* В регистр R0 поместим адрес подпрограммы */
	BLX     R0               /* Вызовем эту подпрограмму */
loop:
	LDR     R0, =clear_led  /* Далее -- аналогично */
	BLX     R0
	LDR     R0, =delay
	BLX     R0
	LDR     R0, =set_led
	BLX     R0
	LDR     R0, =delay
	BLX     R0
	B       loop               /* Безусловный переход. Это бесконечный цикл */

/* Сюда мы не должны попасть. Но для безопасности допишем следующий код */
/* Если все-таки мы сюда попали, то вернемся в то место, откуда функция main() была вызвана */
   	POP	{PC}

@-----------------------------------------------------------
/* Произведем инициализацию портов  */
	.thumb_func
init_gpio:
	/* Включим тактирования порта B */
	LDR	R0, =0x40021014  /* адрес регистра RCC.AHBENR */
	LDR	R1, =0x00020000  /* бит IOPAEN */
	STR	R1, [R0]

	/* Сконфигурируем вывод порта B на вывод */
	LDR	R0, =0x48000000  /* адрес регистра GPIOA.MODER */
	LDR	R1, =0x00000010  /* bit2 на вывод */
	STR	R1, [R0]

	/* Другие регистры порта не трогаем. */

	BX      LR	/* Возвращаемся */

@--------------------------------------------------------------------
/* Зажжем светодиод */
	.thumb_func
set_led:
	LDR	R0, =0x48000018  /* адрес регистра GPIO.BSRR */
	LDR	R1, =0x00000004  /* бит 2 */
	STR	R1, [R0]
	BX      LR

@---------------------------------------------------------------------
/* Погасим светодиод */
	.thumb_func
clear_led:
	LDR	R0, =0x48000028  /* адрес регистра GPIO.BRR */
	LDR	R1, =0x00000004  /* бит 2 */
	STR	R1, [R0]
	BX      LR

@----------------------------------------------------------
/* Пока мы ничего делать не умеем, поэтому тупо потратим процессорное время на */
/* большой цикл.  */
	.thumb_func
delay:
	LDR	R3, =0x00040000  /* Загружаем в регистр R3 количество циклов */

delay_loop:
	SUBS	R3, #1       /* Декремент регистра R3 */
	BEQ	delay_exit   /* Если R3 окажется равен нулю, то перейдем на метку */
	B       delay_loop   /* В противном случае -- безусловный переход на другую метку */

delay_exit:
                             /* В регистре LR содержится адрес возврата */
	BX      LR           /* Переход по адресу возврата */

Прежде всего следует заметить, что в файле описаны пять функций:

  • main
  • init_gpio
  • set_led
  • clear_led
  • delay

Все пять функций объявлены как .thumb_func. Это значит, что код в этих функциях будет thumb-2.

Кроме того, имя функции main объявлено ещё и как глобальное имя .global main. Это сделано для того, чтобы это имя было видно не только в этом файле, а вообще — на уровне Линковщика. Тогда Линковщик будет способен связать адрес этой функции и ее вызов из файла start-up.S.

Поскольку к остальным четырем функциям в файле blinky.S нет обращений из других файлов, а есть (локальные) обращения только внутри этого файла, то объявлять их глобальными нет никакого смысла. Внутри же файла они и так «видятся».

Проследим как происходит выполнение кода.

/* Таблица векторов обработчиков прерываний */
.section ".vector_table"
.long __Initial_Stack_Value /* Вершина стека */
.long Reset_Handler /* Обработчик Reset */
...

После сброса микроконтроллер «подхватывает» второе значение из таблицы векторов, интерпретирует его как адрес перехода. Другими словами — выполняет код по этому адресу. Первое же значение в таблице векторов — ничто иное как начальное значение для указателя стека (SP, Stack Pointer). Вот эти два числа (адрес переходя и начальное значение SP) после сброса одновременно попадают в соответствующие регистры ядра микроконтроллера.

Однако, если присмотреться к коду, то можно заметить, что таблица векторов располагается в секции .vector_table. А теперь идем в файл linked.ld и смотрим, сюда:

/* Теперь займемся распределением памяти */
SECTIONS
{
  /* Флешь в STM32F030 начинается с адреса 0x08000000 */
  .flash 0x08000000 :
  {
    /* Скажем Линковщику собрать таблицу обработчиков векторов прерываний на этот адрес */
    KEEP(*(.vector_table));

    /* Затем укажем Линковщику собрать все секции исполняемого кода в одну кучку */
    *(.text .text.*);
...

Здесь мы видим, что описание секций начинается с метки .flash, которая соответствует адресу начала флешь-памяти — 0x08000000. И сразу же за ней следует секция .vector_table, которая будет располагаться на этом же адресе. Таким образом, мы можем гарантировать, что в самом начале флешь-памяти располагается таблица векторов, что и требуется для правильного старта микроконтроллера.

Если просмотреть чуть далее, то можно увидеть, что за таблицей векторов (впритык) располагается секция исполняемых кодов — .text. Перейдите сначала в файл start-up.S, а затем в blinky.S, и убедитесь, что весь исполняемый код находится именно в этой секции.

После ассемблирования (или после компиляции С-шных файлов) в объектных файлах содержаться коды, разделенные на секции. В частности это могут быть исполняемые коды (.text), коды инициализированных данных (.data), кода неинициализированных данных (.bss) и так далее.

Линковщик тем и занимается, что уточняет значения глобальных имен (функций и переменных) — говорят «разрешает ссылки». (Не в смысле «позволяет», а в смысле «вычисляет». Ну уж такой профессиональный жаргон!). Кроме этого Линковщик собирает из всех объектных файлов (*.o) одноименные секции и складывает их в соответствующие кучки. Так, например, Линковщик, пробегая по всем объектным файлам, «вытаскивает» из них куски кода из секции .text и размещает их последовательно в общей секции .text целевого файла *.elf.

Вы даже можете назначить какое-нибудь имя вашей самопальной секции! В исходном тексте объявите свою секцию, а потом Линковщику укажите куда (на какие адреса) код этой секции размещать. Ничего сверхсложного в этом нет! Это просто нужно осознать и свыкнуться.

Итак, после того как второй элемент векторной таблицы попал в программный счетчик (PC), микроконтроллер будет пытаться выполнить код по этому адресу. Поскольку в нашей таблице векторов этот адрес имеет имя Reset_Handler, то давайте перейдем на место.

Мы видим следующий код:

Reset_Handler:
/* Пока не делаем никаую предварительную подготовку, а тупо вызываем фунцию main() */
	LDR     R0, =main  /* В регистр R0 помещаем адрес функции */
	BLX     R0         /* Осуществляем переход на адрес, записанный в R0 */

	/* Если даже main() случайно вернет управление, те есть произойдет из нее выход,
	   мы должны остановить дльнейшее исполнение кода. */
	B       .	/* Тупо зациклимся на месте */

Команда LDR R0, =main загружает адрес main в регистр R0. Следующая команда BLX R0 осуществляет переход на этот адрес. При этом в регистр связи LR помещается адрес следующей команды. Это походит на вызов подпрограмм call у других типов микроконтроллеров, но отличие заключается в том, что у них адрес возврата заносится в стек, а в микроконтроллерах Cortex адрес возврата записывается в регистр. Запись в регистр осуществляется значительно быстрее, чем обращение к памяти. Таким образом, Cortex-ы работают быстрее.

К стати, и это очень важно отметить: подпрограмма сама решает — нужно ли ей переносить адрес возврата из регистра LR в стек или нет. И в самом деле, если в подпрограмме нет вложенных вызовов других подпрограмм, то регистр LR не будет изменен, а это значит, что по окончании работы подпрограммы можно перенести его значение обратно в программный счетчик.

Это необычно, но с этим нужно просто свыкнуться и все станет прозрачно и понятно.

Идем дальше. Мы передали управление на адрес main. Метка с этим адресом находится в файле blinky.S.

/* Это еще не аналог С-шной функции main(), но механизм вызова примерно такой же */
main:
	PUSH	{LR}             /* Сохраним в стеке адрес возврата */

	LDR     R0, =init_gpio   /* В регистр R0 поместим адрес подпрограммы */
	BLX     R0               /* Вызовем эту подпрограмму */
loop:
	LDR     R0, =clear_leds  /* Далее -- аналогично */
	BLX     R0
	LDR     R0, =delay
	BLX     R0
	LDR     R0, =set_leds
	BLX     R0
	LDR     R0, =delay
	BLX     R0
	B       loop               /* Безусловный переход. Это бесконечный цикл */

Поскольку в функции main имеются вызовы других подпрограмм, мы сохраняем значение регистра связи LR в стеке. Таким образом мы «помним» путь домой. В принципе, если предъявляются очень жесткие требования к скорости вызова подпрограмм, а сам код достаточно компактный, то сохранить содержимое регистра связи можно не в стеке, а в каком-нибудь другом свободном регистре общего назначения, например, в регистре R7. Только потом, по возврату из этой функции нужно не забыть что адрес возврата находится в R7.

Да, это хакерские трюки. Да, это мастерство Джедая. Кому тяжело, пишите на Пых-Пыхе и программируйте мышко-тыканием по иконкам. И будьте счастливы на своем «высоком» уровне. Остальных, прошу проследовать за мной на следующий уровень…

Последующий код в функции main уже не представляет большого интереса. Всё строится на шаблоне:

	LDR     R0, =init_gpio   /* В регистр R0 поместим адрес подпрограммы */
	BLX     R0               /* Вызовем эту подпрограмму */

В регистр R0 закладываем адрес вызываемой функции и осуществляем переход на нее с занесением адреса возврата в регистр связи.

Теперь рассмотрим код функции инициализации порта Init_gpio. Следующими действиями мы включаем тактирование порта:

	/* Включим тактирования порта B */
	LDR	R0, =0x40021014  /* адрес регистра RCC.AHBENR */
	LDR	R1, =0x00020000  /* бит IOPAEN */
	STR	R1, [R0]

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

Включение тактовой частоты на порты осуществляется регистром-«рубильником» AHBENR, который находится системе управления сбросом и тактированием — Reset and clock control (RCC).

Как я уже ранее говорил, в Cortex-ах не стоит проблема нехватки адресного пространства. Поэтому и согласно идее поддержания понятной структуры, всё адресное пространство условно разбито на фрагменты — диапазоны адресов. Так например, системе управления сбросом и тактированием отведен диапазон адресов с 0x40021000 по 0x400213FF включительно. Базовый адрес — 0x40021000.

Еще примеры. Порту ввода-вывода GPIOA выделен диапазон адресов с 0x48000000 по 0x480003FF
, базовый адрес — 0x48000000. А порту ввода-вывода GPIOB выделен диапазон адресов с 0x48000400 по 0x480007FF, базовый адрес — 0x48000400. Ну и так далее. Все это описано в файлах DM00088500.pdf и DM00091010.pdf. К сожалению, базовые адреса групп и смещения регистров разнесены по разным документам, приходится под рукой держать оба.

таким образом мы можем говорить о базовом адресе группы регистров и смещении относительно этого адреса для конкретного регистра. Например, в группе RCC У нас имеется регистр AHBENR. Таким образом, точный адрес регистра складывается из базового адреса группы (0x40021000) и смещения (0x00000014) и равен 0x40021014, что мы и помещаем в регистр R0.

Далее, в регистр R1 мы заносим бит, который отвечает за включение тактирования на порт GPIOA. Согласно документу RM0360 Reference manual ( файл DM00091010.pdf) этот бит называется IOPAEN и имеет порядковый номер 17. Вот его-то (17-ый) бит мы и установим в единичное состояние, это будет число 0x00020000.

Следующим шагом (команда STR R1, [R0]) мы запишем значение из регистра R1 по адресу R0. Таким образом, после этих действий в работу включилась целая группа регистров, отвечающая за порт GPIOA.

Что дальше? А дальше нам нужно сконфигурировать порт таким образом, чтобы он мог зажигать подключенный к нему светодиод. Для этого нам достаточно «развернуть» направление порта, сделать его «выходом». Все разряды порта мы трогать не будем, развернем лишь второй разряд. Для того, чтобы убедиться в работоспособности нашей проги, нам этого будет вполне достаточно. Более того, мы так же не будем производить другие не существенные для нас настройки порта, дабы не напускать тумана и загадочности в прогу.

Зная, как формируются конкретные значения для адресов регичтров, попробуйте самостоятельно сформировать адрес для регистра MODER для порта GPIOA.

	/* Сконфигурируем вывод порта B на вывод */
	LDR	R0, =0x48000000  /* адрес регистра GPIOA.MODER */
	LDR	R1, =0x00000010  /* bit2 на вывод */
	STR	R1, [R0]

Заметьте, что для управления одним и тем же разрядом порта, для регистра MODER и для регистров BSRR и BRR получаются разные числа. Это связано с тем, что у регистров BSRR и BRR каждый их бит прямо отображется на бит разряда порта. А регистр MODER имеет по два смежных бита для управления одним битом порта, поэтому значение получается другое.

С вашего молчаливого согласия, я не буду комментировать дальнейший код. Основную идею создания программ для Cortex-ов вы уже уловили. И теперь для вас не составит особого труда понять код в функциях включения и выключения светодиода, и в функции задержки времени delay.

Я же хочу сейчас перейти к самой волнительно части — компиляции проекта.

Будем считать, что вы без ошибок создали три файла и без ошибок закопипастили в них код отсюда. Теперь давайте откомпилируем (правильнее говорить — отассемблируем, так как компиляция — это процесс трансляции с языка высокого уровня, например с С, но никак не с ассемблера.) ассемблерные файлы:

$ arm-none-eabi-as -mcpu=cortex-m0 -o start-up.o start-up.S
$ arm-none-eabi-as -mcpu=cortex-m0 -o blinky.o blinky.S

Я надеюсь у вас тоже все получилось без ошибок. Отсутствие криков в консоли — признак, что все прошло удачно.

Далее нам нужно слинковать полученные объектные файлы вместе:

$ arm-none-eabi-ld --gc-sections -Map blinky.map -T linked.ld -o blinky.elf start-up.o blinky.o

Посмотрим на получившийся размер секций:

$ arm-none-eabi-size blinky.elf

Согласитесь, приятно осознавать, что вы уже понимаете что означают эти цифры!

Теперь для заливки в микроконтроллер нам нужно из elf-а вытащить код и превратить его в бинарный формат:

$ arm-none-eabi-objcopy -j .flash -j .data_at_ram -O ihex blinky.elf blinky.hex
$ arm-none-eabi-objcopy -Obinary blinky.elf blinky.bin

Собственно, вот так бестактно я вынужден прервать свое повествование. Далее начинается работа с целевым (target) железом и программатором, который собственно представляет собой три проводка к последовательному порту компьютера. А так же компьютерной программы, которая управляя последовательным портом, осуществит заливку кода в микроконтроллер. Мне также хотелось бы сделать фотографии девайса и нарисовать его схему. Все это тянет на еще одну публикацию.

А на сегодня, если вы сумели успешно откомпилировать исходные файлы и разобрались откуда что берется, то это — всё! Приятного вам времени препровождения!

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

Advertisements

3 responses to “STM32F030. Светодиодоморгание

  1. Гротескно 🙂

  2. Хе, а вот я не врубился. Точнее проблема. Есть обработчик прерывания, всё замечательно, залетел в него, но в обработчике вставил вызов следующей подпрограммы. Вызвал её, что-то сделал, вернулся в обработчик прерывания нормально, а вот из прерывания никак. Получается LR нужно стЭкать ручками? Ну ладно. Типа push{LR}, pop{LR} но оказывается что регистр LR не попадает в список инструкций пуш и поп. Что делать?

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

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s