В прошлых публикациях я показал от чего можно оттолкнуться при написании программ для STM32F4, настроили среду компиляции, определили файл компоновщика, получили шаблонный файл программы на языке ассемблера, попробовали настроить GPIO микроконтроллера и помигать светодиодом.
Ссылки на прошлые публикации:
STM32F4: GNU AS: Программирование на ассемблере (Часть 1)
STM32F4: GNU AS: Мигаем светодиодом (Оживление) (Часть 2)
STM32F4: GNU AS: Мигаем светодиодом (Версия для STM32F4 Discovery, Оптимизация) (Часть 3)
Теперь пришло время разобраться в компиляции программ состоящих из нескольких файлов, разобрать способ написания программ для нескольких отладочных плат, научиться выносить настройки программы для удобного их изменения
Начнем с самого простого: механизм условной компиляции
У нас сейчас как раз подходящая ситуация: у большинства из читающих мои публикации (причем от публикации к публикации это число неуклонно уменьшается :-) в качестве отладочной платы используется STM32F4 Discovery, у меня же отладочная плата Open407I-C, соответственно отличается подключением внешней периферии, например, светодиод который используется в программе мигалка подключен:
В ассемблере GNU AS есть механизм условной компиляции.
Нашел команду .def — согласно документации она должна создавать значение, однако заставить ее работать не удалось, поэтому я использовал директиву ".set".
Первоначально мы присваиваем переменной компилятора (не программы!) некоторое значение, я выбрал имя переменной DEVBOARD:
И далее в своей программе просто проверяю — если переменная DEVBOARD существует, то для компиляции предлагаю один текст программы, если не существует (то есть не была объявлена например потому что была закомментирована) — то другой текст программы.
Каждое подключение светодиода требует указания в программе (и эти параметры зависят от платы разработки):
Настройка этих параметров при условной компиляции выглядит следующим образом:
В тексте самой программы все «прямые» упоминания GPIOD (GPIOH) теперь изменяем на GPIO_LED, значение для RCC_AHB1ENR заменим на RCC_GPIO_EN и так далее.
Получим следующую программу:
Это тест программы для компиляции под STM32F4 Discovery, если же у кого то будет Open407I-C то достаточно просто закомментировать строку .set DEVBOARD, STM32F4DISCO и компиляция будет осуществлена со значениями настройки для Open407I-C
Я всегда стараюсь выносить параметры настройки подпрограмм и модулей в начало файла — лишний раз разбираться что написано в исходниках и не удобно с точки зрения затрачиваемого времени, и не всегда безопасно если в коде есть зависимости от настроек — гораздо проще «вытащить» в начало файла все настройки которые можно изменять, описать их, и больше не задумываться о том что же происходит в «недрах» подпрограммы или модуля
Теперь задумаемся об относительно больших по размеру программах, которые вряд ли будет удобно писать в одном файле, и которые будут состоять из нескольких (возможно даже нескольких десятков) файлов.
рассмотрим это на примере нашей программы «мигалка»: в ней есть подпрограмма которая реализует паузу между включением/выключением светодиода, сейчас это просто цикл с фиксированным числом циклического ожидания, но в будущем подпрограмму можно будет переписать на использование системного таймера — и эту подпрограмму будет правильнее разместить в другом файле.
Включать файлы можно директивой ".include" ассемблера:
это решение очень простое и достаточно эффективное, но у него есть один, по началу кажущийся незначительным, недостаток — общее пространство имен меток.
Пока программа не перевалит за 150-300 инструкций — у вас не будет особых проблем в «придумывании» имен меток, например, следующую подпрограмму задержки можно будет начать с метки "DELYA1:", а следующую с еще какой нибудь комбинацией, но все не так просто: как только вы начнете создавать собственную библиотеку подпрограмм и начнете их использовать в различных сочетаниях — то обязательно начнутся пересечения в именах меток.
Один из способов решения — введение в имя метки префикса имени файла исходника, например для нашей подпрограммы задержки можно задать следующее имя метки "DELAY_ASM_DELAY:", я опробовал такой путь, правда опять не без оговорок — ведь в процессе разработки некоторые модули могут находиться в подкаталогах проекта, и чтобы не было пересечений имя метки должно включать в себя не только имя файла, но и имя каталога "DIRECTORY_DELAY_ASM_DELAY:" (пример для каталога с именем "DIRECTORY", в итоге, если нам еще понадобятся метки имя которых состоит из 2-3 слов для понятности — мы получим совсем уж монстроидальные имена меток: "DIRECTORY_DELAY_ASM_DELAY_LOOP_0x10000:"…
Еще один способ это раздельная компиляция исходных файлов программы.
Фактически ассемблер может компилировать отдельно файл main.asm и delay.asm, и затем на этапе компоновки — эти две откомпилированных части будут объединяться в итоговой прошивке.
Поскольку каждый .asm файл будет компилироваться отдельно — в начало каждого нужно включать указания настроек компиляции и, если есть необходимость, включать файл констант.
Для того чтобы сообщить компилятору какую метку из этого файла можно использовать извне — используется директива ".global".
Рассмотрим текст подпрограммы при размещении в отдельном файле для раздельной компиляции:
Соответственно, метка DELAY объявленная как .global будет доступна для вызова из других файлов исходников программы, а вот метка DELAY_loop доступна только внутри файла delay.asm и фактически имя такой метки может без каких либо последствий использоваться в любых других частях программы — пересечения не будет!
в текст основной программы согласно де-факто сложившихся правил нужно включать директиву ".extern" которая указывает на метку которая определена в другом файле и будет связана на этапе компоновки, но в документации на GNU AS указано что данная директива пропускается для обеспечения совместимости, и любые не найденные в исходном файле компиляции метки и константы считаются глобальными. Подход на мой взгляд спорный, все таки внешние значения меток и констант лучше обозначать явно.
как видно из приведенного листинга — никаких дополнительных ".include" для включаемого файла делать не нужно! включение будет обрабатывать компоновщик.
процесс компиляции и сборки проекта должен выглядеть следующим образом:
в дальнейшем получение файлов для прошивки в форматах .hex или .bin производится как это было описано в первой части публикации
Теперь явно видно неудобство такой схемы для конкретного программиста: необходимо проводить компиляцию отдельных файлов и потом так же при сборке указывать получившиеся откомпилированные файлы. Делать это в ручном режиме, постоянно редактируя файл make_project.bat, достаточно рутиное занятие, поэтому данный процесс нужно переложить на чьи-то другие «могучие плечи»
Немного по эксперементировав «в субботу», я написал новый .bat файл собирающий проект в автоматическом режиме.
Пришлось внести некоторые изменения в структуру каталогов проекта и в «соглашение» об именовании файлов:
Ссылки на прошлые публикации:
STM32F4: GNU AS: Программирование на ассемблере (Часть 1)
STM32F4: GNU AS: Мигаем светодиодом (Оживление) (Часть 2)
STM32F4: GNU AS: Мигаем светодиодом (Версия для STM32F4 Discovery, Оптимизация) (Часть 3)
Теперь пришло время разобраться в компиляции программ состоящих из нескольких файлов, разобрать способ написания программ для нескольких отладочных плат, научиться выносить настройки программы для удобного их изменения
Начнем с самого простого: механизм условной компиляции
У нас сейчас как раз подходящая ситуация: у большинства из читающих мои публикации (причем от публикации к публикации это число неуклонно уменьшается :-) в качестве отладочной платы используется STM32F4 Discovery, у меня же отладочная плата Open407I-C, соответственно отличается подключением внешней периферии, например, светодиод который используется в программе мигалка подключен:
- к выводу 15 GPIO_D для STM32F4 Discovery
- к выводу 2 GPIO_H для Open407I-C
В ассемблере GNU AS есть механизм условной компиляции.
Нашел команду .def — согласно документации она должна создавать значение, однако заставить ее работать не удалось, поэтому я использовал директиву ".set".
Первоначально мы присваиваем переменной компилятора (не программы!) некоторое значение, я выбрал имя переменной DEVBOARD:
@ Если определение ниже закомментировано, то предполагается что
@ использована плата Open407I-C, иначе STM32F4 Discovery
.set DEVBOARD, STM32F4DISCO
И далее в своей программе просто проверяю — если переменная DEVBOARD существует, то для компиляции предлагаю один текст программы, если не существует (то есть не была объявлена например потому что была закомментирована) — то другой текст программы.
Каждое подключение светодиода требует указания в программе (и эти параметры зависят от платы разработки):
- Бита регистра включения GPIO в RCC_AHB1ENR
- Адреса GPIOx_BASE через который осуществляется вычисление всех остальных управляющих регистров
- Номера вывода GPIOx для управления им в режиме BitBand
- Битового поля для настройки режима порта в регистре GPIOx_MODER
Настройка этих параметров при условной компиляции выглядит следующим образом:
@ Определения отладочной платы
.ifdef DEVBOARD @ если переменная существует, то STM32F4 Discovery
.equ GPIO_LED ,GPIOD_BASE @ порт подключения светодиода
.equ RCC_GPIO_EN ,RCC_AHB1ENR_GPIODEN_N @ бит включения GPIO
.equ GPIO_ODR_NUM ,15 @ номер пина GPIO
.else @ если переменная не существует, то Open407I-C
.equ GPIO_LED ,GPIOH_BASE
.equ RCC_GPIO_EN ,RCC_AHB1ENR_GPIOHEN_N
.equ GPIO_ODR_NUM ,2
.endif
@ общий параметр просто вычисляется в зависимости от входных параметров
.equ GPIO_MODER_MDR ,1<<(GPIO_ODR_NUM*2)
В тексте самой программы все «прямые» упоминания GPIOD (GPIOH) теперь изменяем на GPIO_LED, значение для RCC_AHB1ENR заменим на RCC_GPIO_EN и так далее.
Получим следующую программу:
@GNU AS
@ Настройки компилятора
.syntax unified @ тип синтаксиса
.thumb @ тип используемых инструкций Thumb
.cpu cortex-m4 @ микроконтроллер
@ Если определение ниже закомментировано, то предполагается что
@ использована плата Open407I-C, иначе STM32F4 Discovery
.set DEVBOARD, STM32F4DISCO
.include "stm32f40x.inc" @ определения микроконтроллера
@ Определения в зависимости от отладочной платы
.ifdef DEVBOARD
.equ GPIO_LED ,GPIOD_BASE @ порт подключения светодиода
.equ RCC_GPIO_EN ,RCC_AHB1ENR_GPIODEN_N @ бит включения GPIO
.equ GPIO_ODR_NUM ,15 @ номер пина GPIO
.else
.equ GPIO_LED ,GPIOH_BASE
.equ RCC_GPIO_EN ,RCC_AHB1ENR_GPIOHEN_N
.equ GPIO_ODR_NUM ,2
.endif
.equ GPIO_MODER_MDR ,1<<(GPIO_ODR_NUM*2)
@ таблица векторов прерываний
.section .text
.word 0x20020000 @ Вершина стека
.word Reset+1 @ Вектор сброса
Reset:
MOV R0, 0 @ Значение 0, будет использоваться для bitband
MOV R1, 1 @ значение 1, будет использоваться для bitband
@ включим тактирование GPIO_LED
LDR R2, =(PERIPH_BB_BASE + (RCC_BASE + RCC_AHB1ENR) * 32 + RCC_AHB1ENR_GPIO_EN * 4) @ адрес
STR R1, [R2] @ запись R1 ("1") по адресу бита указанному в R2
@ установим режим GPIO_LED pin_15
LDR R2, =(PERIPH_BASE + GPIO_LED + GPIO_MODER) @ адрес
LDR R3, =GPIO_MODER_MDR @ значение
LDR R4, [R2] @ прочитали значение регистра
ORR R3, R3, R4 @ логическое, побитовое ИЛИ
STR R3, [R2] @ запись обновленного значения в GPIOD_MODER
LDR R2, =(PERIPH_BB_BASE + (GPIO_LED + GPIO_ODR) * 32 + GPIO_ODR_NUM*4) @ адрес бита
BLINK_LOOP:
@ включим светодиод
STR R1, [R2] @ запись R1 ("1") по адресу указанному в R2
BL DELAY @ пауза
@ выключим светодиод
STR R0, [R2] @ запись R0 ("0") по адресу указанному в R2
BL DELAY @ пауза
B BLINK_LOOP @ делаем цикл
DELAY:
LDR R3, =0x00100000 @ повтор цикла 0x0010 0000 раз.
Delay_loop:
SUBS R3, R3, 1
BNE Delay_loop
BX LR
Это тест программы для компиляции под STM32F4 Discovery, если же у кого то будет Open407I-C то достаточно просто закомментировать строку .set DEVBOARD, STM32F4DISCO и компиляция будет осуществлена со значениями настройки для Open407I-C
Я всегда стараюсь выносить параметры настройки подпрограмм и модулей в начало файла — лишний раз разбираться что написано в исходниках и не удобно с точки зрения затрачиваемого времени, и не всегда безопасно если в коде есть зависимости от настроек — гораздо проще «вытащить» в начало файла все настройки которые можно изменять, описать их, и больше не задумываться о том что же происходит в «недрах» подпрограммы или модуля
Теперь задумаемся об относительно больших по размеру программах, которые вряд ли будет удобно писать в одном файле, и которые будут состоять из нескольких (возможно даже нескольких десятков) файлов.
рассмотрим это на примере нашей программы «мигалка»: в ней есть подпрограмма которая реализует паузу между включением/выключением светодиода, сейчас это просто цикл с фиксированным числом циклического ожидания, но в будущем подпрограмму можно будет переписать на использование системного таймера — и эту подпрограмму будет правильнее разместить в другом файле.
Включать файлы можно директивой ".include" ассемблера:
файл main.asm
@GNU AS
@ Настройки компилятора
.syntax unified @ тип синтаксиса
.thumb @ тип используемых инструкций Thumb
.cpu cortex-m4 @ микроконтроллер
@ Если определение ниже закомментировано, то предполагается что
@ использована плата Open407I-C, иначе STM32F4 Discovery
.set DEVBOARD, STM32F4DISCO
.include "stm32f40x.inc" @ определения микроконтроллера
@ Определения в зависимости от отладочной платы
.ifdef DEVBOARD
.equ GPIO_LED ,GPIOD_BASE @ порт подключения светодиода
.equ RCC_GPIO_EN ,RCC_AHB1ENR_GPIODEN_N @ бит включения GPIO
.equ GPIO_ODR_NUM ,15 @ номер пина GPIO
.else
.equ GPIO_LED ,GPIOH_BASE
.equ RCC_GPIO_EN ,RCC_AHB1ENR_GPIOHEN_N
.equ GPIO_ODR_NUM ,2
.endif
.equ GPIO_MODER_MDR ,1<<(GPIO_ODR_NUM*2)
@ таблица векторов прерываний
.section .text
.word 0x20020000 @ Вершина стека
.word Reset+1 @ Вектор сброса
Reset:
MOV R0, 0 @ Значение 0, будет использоваться для bitband
MOV R1, 1 @ значение 1, будет использоваться для bitband
@ включим тактирование GPIO_LED
LDR R2, =(PERIPH_BB_BASE + (RCC_BASE + RCC_AHB1ENR) * 32 + RCC_AHB1ENR_GPIO_EN * 4) @ адрес
STR R1, [R2] @ запись R1 ("1") по адресу бита указанному в R2
@ установим режим GPIO_LED pin_15
LDR R2, =(PERIPH_BASE + GPIO_LED + GPIO_MODER) @ адрес
LDR R3, =GPIO_MODER_MDR @ значение
LDR R4, [R2] @ прочитали значение регистра
ORR R3, R3, R4 @ логическое, побитовое ИЛИ
STR R3, [R2] @ запись обновленного значения в GPIOD_MODER
LDR R2, =(PERIPH_BB_BASE + (GPIO_LED + GPIO_ODR) * 32 + GPIO_ODR_NUM*4) @ адрес бита
BLINK_LOOP:
@ включим светодиод
STR R1, [R2] @ запись R1 ("1") по адресу указанному в R2
BL DELAY @ пауза
@ выключим светодиод
STR R0, [R2] @ запись R0 ("0") по адресу указанному в R2
BL DELAY @ пауза
B BLINK_LOOP @ делаем цикл
.include "delay.asm" @ добавляем файл delay.asm в это место нашей программы
файл delay.asm
DELAY:
LDR R3, =0x00100000 @ повтор цикла 0x0010 0000 раз.
Delay_loop:
SUBS R3, R3, 1
BNE Delay_loop
BX LR
это решение очень простое и достаточно эффективное, но у него есть один, по началу кажущийся незначительным, недостаток — общее пространство имен меток.
Пока программа не перевалит за 150-300 инструкций — у вас не будет особых проблем в «придумывании» имен меток, например, следующую подпрограмму задержки можно будет начать с метки "DELYA1:", а следующую с еще какой нибудь комбинацией, но все не так просто: как только вы начнете создавать собственную библиотеку подпрограмм и начнете их использовать в различных сочетаниях — то обязательно начнутся пересечения в именах меток.
Один из способов решения — введение в имя метки префикса имени файла исходника, например для нашей подпрограммы задержки можно задать следующее имя метки "DELAY_ASM_DELAY:", я опробовал такой путь, правда опять не без оговорок — ведь в процессе разработки некоторые модули могут находиться в подкаталогах проекта, и чтобы не было пересечений имя метки должно включать в себя не только имя файла, но и имя каталога "DIRECTORY_DELAY_ASM_DELAY:" (пример для каталога с именем "DIRECTORY", в итоге, если нам еще понадобятся метки имя которых состоит из 2-3 слов для понятности — мы получим совсем уж монстроидальные имена меток: "DIRECTORY_DELAY_ASM_DELAY_LOOP_0x10000:"…
Еще один способ это раздельная компиляция исходных файлов программы.
Фактически ассемблер может компилировать отдельно файл main.asm и delay.asm, и затем на этапе компоновки — эти две откомпилированных части будут объединяться в итоговой прошивке.
Поскольку каждый .asm файл будет компилироваться отдельно — в начало каждого нужно включать указания настроек компиляции и, если есть необходимость, включать файл констант.
Для того чтобы сообщить компилятору какую метку из этого файла можно использовать извне — используется директива ".global".
Рассмотрим текст подпрограммы при размещении в отдельном файле для раздельной компиляции:
файл delay.asm
@GNU AS
@ Настройки компилятора
.syntax unified @ тип синтаксиса
.thumb @ тип используемых инструкций Thumb
.cpu cortex-m4 @ микроконтроллер
.section .text
.global DELAY @ по метке DELAY к подпрограмме можно обращаться из других частей программы
DELAY:
LDR R3, =0x00100000 @ повтор цикла 0x0010 0000 раз.
Delay_loop: @ метка доступна только в данном файле, извне не видна !
SUBS R3, R3, 1
BNE Delay_loop
BX LR
Соответственно, метка DELAY объявленная как .global будет доступна для вызова из других файлов исходников программы, а вот метка DELAY_loop доступна только внутри файла delay.asm и фактически имя такой метки может без каких либо последствий использоваться в любых других частях программы — пересечения не будет!
в текст основной программы согласно де-факто сложившихся правил нужно включать директиву ".extern" которая указывает на метку которая определена в другом файле и будет связана на этапе компоновки, но в документации на GNU AS указано что данная директива пропускается для обеспечения совместимости, и любые не найденные в исходном файле компиляции метки и константы считаются глобальными. Подход на мой взгляд спорный, все таки внешние значения меток и констант лучше обозначать явно.
файл main.asm
@GNU AS
@ Настройки компилятора
.syntax unified @ тип синтаксиса
.thumb @ тип используемых инструкций Thumb
.cpu cortex-m4 @ микроконтроллер
@ Если определение ниже закомментировано, то предполагается что
@ использована плата Open407I-C, иначе STM32F4 Discovery
.set DEVBOARD, STM32F4DISCO
.include "stm32f40x.inc" @ определения микроконтроллера
@ Определения в зависимости от отладочной платы
.ifdef DEVBOARD
.equ GPIO_LED ,GPIOD_BASE @ порт подключения светодиода
.equ RCC_GPIO_EN ,RCC_AHB1ENR_GPIODEN_N @ бит включения GPIO
.equ GPIO_ODR_NUM ,15 @ номер пина GPIO
.else
.equ GPIO_LED ,GPIOH_BASE
.equ RCC_GPIO_EN ,RCC_AHB1ENR_GPIOHEN_N
.equ GPIO_ODR_NUM ,2
.endif
.equ GPIO_MODER_MDR ,1<<(GPIO_ODR_NUM*2)
@ таблица векторов прерываний
.section .text
.word 0x20020000 @ Вершина стека
.word Reset+1 @ Вектор сброса
Reset:
MOV R0, 0 @ Значение 0, будет использоваться для bitband
MOV R1, 1 @ значение 1, будет использоваться для bitband
@ включим тактирование GPIO_LED
LDR R2, =(PERIPH_BB_BASE + (RCC_BASE + RCC_AHB1ENR) * 32 + RCC_AHB1ENR_GPIO_EN * 4) @ адрес
STR R1, [R2] @ запись R1 ("1") по адресу бита указанному в R2
@ установим режим GPIO_LED pin_15
LDR R2, =(PERIPH_BASE + GPIO_LED + GPIO_MODER) @ адрес
LDR R3, =GPIO_MODER_MDR @ значение
LDR R4, [R2] @ прочитали значение регистра
ORR R3, R3, R4 @ логическое, побитовое ИЛИ
STR R3, [R2] @ запись обновленного значения в GPIOD_MODER
LDR R2, =(PERIPH_BB_BASE + (GPIO_LED + GPIO_ODR) * 32 + GPIO_ODR_NUM*4) @ адрес бита
BLINK_LOOP:
@ включим светодиод
STR R1, [R2] @ запись R1 ("1") по адресу указанному в R2
.extern DELAY @ объявляем о внешнем значении метки
BL DELAY @ пауза
@ выключим светодиод
STR R0, [R2] @ запись R0 ("0") по адресу указанному в R2
BL DELAY @ пауза
B BLINK_LOOP @ делаем цикл
как видно из приведенного листинга — никаких дополнительных ".include" для включаемого файла делать не нужно! включение будет обрабатывать компоновщик.
процесс компиляции и сборки проекта должен выглядеть следующим образом:
:: компилируем исходные тексты в отдельные файлы
arm-none-eabi-as.exe -o main.o main.asm
arm-none-eabi-as.exe -o delay.o delay.asm
:: компонуем откомпилированные файлы в единую прошивку
arm-none-eabi-ld.exe -T stm32f40_def.ld -o compile\sys.elf main.o delay.o
в дальнейшем получение файлов для прошивки в форматах .hex или .bin производится как это было описано в первой части публикации
Теперь явно видно неудобство такой схемы для конкретного программиста: необходимо проводить компиляцию отдельных файлов и потом так же при сборке указывать получившиеся откомпилированные файлы. Делать это в ручном режиме, постоянно редактируя файл make_project.bat, достаточно рутиное занятие, поэтому данный процесс нужно переложить на чьи-то другие «могучие плечи»
Немного по эксперементировав «в субботу», я написал новый .bat файл собирающий проект в автоматическом режиме.
Пришлось внести некоторые изменения в структуру каталогов проекта и в «соглашение» об именовании файлов:
- исходный текст программы размещается в подкаталоге src/ проекта:
- в подкаталоге compile/ размещаются готовые файлы для прошивки, в подкаталоге compile/temp/ размещаются справочные файлы откомпилированных файлов, файл значений и т.д.
- компилироваться будут все файлы находящиеся начиная с подкаталога /src и имеющие расширение .asm. При этом, те файлы которые будут включаться директивой .include не должны иметь расширение .asm — поскольку в этом случае они будут откомпилированы дважды — первый раз в тексте программы в которую они включались директивой .include, и отдельно по расширению.
- путь всех файлов которые включаются в проект командой .include должен начинаться с указания каталога src/, например, для файла констант который лежит в каталоге src/ директива должна быть написана следующим образом .include «src/const_file.inc»
Компиляцию делает следующий скрипт размещенный вmake_project.bat:: Файл компиляции и сборки проектов GNU AS echo off cls :: Удалим файлы прежних попыток сборки проекта ------------------------------ del /Q compile\*.* del /Q compile\temp\*.* :: Назначаем переменные (каталог запуска и компилятора) --------------------- set pth=%~dp0 set path=%~dp0binecho Текущий путь запуска: %path% echo. echo Компиляция файлов: :: обходим все каталоги проекта и компилируем asm файлы в .o ---------------- Setlocal EnableDelayedExpansion for /r /d %%i in (*) do ( cd %%i for %%b in (*.asm) do ( set a=%%~fb set o=%%~db%%~pb%%~nb.o set l=compile/temp/%%~nb.lst echo !a! cd !pth! %path%arm-none-eabi-as.exe -o !o! !a! %path%arm-none-eabi-objdump.exe -j .vectors -j .asmcode -j .bss -j .rodata -d -t -w !o! > !l! cd %%i ) ) :: обходим все каталоги проекта и строим список .o файлов ------------------- cd %pth% set ofiles= Setlocal EnableDelayedExpansion for /r /d %%i in (*) do ( cd %%i for %%b in (*.o) do set ofiles=!ofiles! %%~fb ) ) echo. echo Компоновка файлов: echo %ofiles% echo. :: компонуем полученные .o файлы в прошивку --------------------------------- cd %pth% %path%arm-none-eabi-ld.exe -T src\stm32f40_map.ld -o compile\sys.elf %ofiles% :: Если при компиляции были ошибки и выходного файла нет - выходим ! set ou=compile\sys.elf IF NOT exist %ou% ( echo Ошибка при компиляции ! PAUSE goto exit ) :: из .elf файла делаем файлы прошивок .bin и .hex -------------------------- cd %pth% %path%arm-none-eabi-objcopy.exe -O binary compile\sys.elf compile\output.bin %path%arm-none-eabi-objcopy.exe -O ihex compile\sys.elf compile\output.hex :: Адреса меток и значения переменных, вывод в файл %path%arm-none-eabi-nm.exe -A -p compile\sys.elf > compile\labels.lst echo. echo Файлы прошивки находятся в папке \compile :exit :: удаляем .o файлы чтобы не мешались---------------------------------------- cd %pth% Setlocal EnableDelayedExpansion for /r /d %%i in (*) do ( cd %%i for %%b in (*.o) do (del /Q %%~fb) )
Не буду разбирать работу предложенного мною файла сборки проекта, думаю кому будет интересно он сам без труда найдет информацию (начать можно с командной строки по for /?) как реализован обход каталогов и файлов в папках. Функциональность программ пакета компилятора мы разбирали в первой части публикации.
Папку проекта нужно размещать в папке в которой и в ее пути нет пробелов в именах. Русские буквы в именах каталогов допускаются.
Таким образом путь запуска: D:/МойПроект/gnuas/Первый/ — допустим, а вот D:/Мой Проект/gnus/Первый/ — не допустим (Имя папки «Мой Проект» содержит пробел)
Изменение в компиляции и сборке проекта наряду к удобству работу с метками которые видны только внутри отдельных файлов-исходников привели и к необходимости введения дополнительных секций в карту компоновщика.
Дело в том что если у нас в проекте после компиляции есть два файла: main.о и delay.о — то при компоновке файлов, если их добавить на компоновку по порядку:
arm-none-eabi-ld.exe -T src\stm32f40_map.ld -o compile\sys.elf main.o delay.o
то проект будет собран правильно, таблица прерываний будет на своем месте (в начале программы), если же на компоновку файлы послать в ином порядке, например:
arm-none-eabi-ld.exe -T src\stm32f40_map.ld -o compile\sys.elf delay.o main.o
то таблица прерываний окажется в середине программы!
Совершенно очевидно что нужно выделять отдельную секцию программы для указания векторов прерываний, дополнительно нужно добавить наконец и иные области компоновки.
Мы уже разбирали в первой части публикации формат областей памяти и секций компоновки, нужно просто воспользоваться этой информацией, добавить новые секции исходных файлов и областей RAM:
/* STM32F40x, flash 1 mb, sram 192 kb, bkpsram 4 kb */ MEMORY { /* FLASH - Программная flash память */ FLASH (RX) : ORIGIN = 0x08000000, LENGTH = 1024K /* SRAM - ОЗУ общего назначения, п.2.3.1 RM0090, стр.68 */ SRAM (RW) : ORIGIN = 0x20000000, LENGTH = 128K /* CCM - быстрая память ядра, п.2.3.1 RM0090, стр.68 */ CCMDATARAM (RWX) : ORIGIN = 0x10000000, LENGTH = 64K /* BKPSRAM- backup sram, п.2.3 RM0090, стр.65 */ BKPSRAM (RW) : ORIGIN = 0x40024000, LENGTH = 4K } SECTIONS { .text : { *(.vectors); /* Указатели векторов прерываний */ *(.text); /* */ *(.asmcode) /* Текст программы */ *(.rodata); /* Read only DATA (константы в flash) */ } > FLASH .bss : { *(.bss); /* Переменные в SRAM */ } > SRAM .ccmdataram : { *(.ccmdataram); /* SRAM ядра, для кода и переменных */ } > CCMDATARAM .bkpsram : { *(.bkpsram) /* SRAM с энергонезависимым питанием */ } > BKPSRAM }
По секции прошивки .text сделаю пояснение:
.text : { *(.vectors); /* Указатели векторов прерываний */ *(.text); /* */ *(.asmcode) /* Текст программы */ *(.rodata); /* Read only DATA (константы в flash) */ } > FLASH
Такая запись указывает компоновщику что секции должны быть размещены именно в том порядке в каком описаны.
Для векторов прерываний я ввел секцию .vectors, это секция должна встречаться в программе только один раз (что логично, так как таблица векторов в наших программах пока только одна).
Далее я объявляю секции в которых будет размещаться текст программы: это .text и .asmcode — по «задумке» секция .text будет гарантировано размещена после таблицы векторов — в этой секции я размещаю различные служебные подпрограммы (например, «заглушки» прерываний), а вот в секции .asmcode я пишу сам текст программы.
Таким образом отметив в программе секцию .vectors я гарантирую ее размещение в начале программы вне зависимости от того каким по счету файлом будет указан при компоновке main.o
Порядок размещения нескольких секций .asmcode, относительно друг друга, как правило уже роли не играет (ну если быть совсем честным — то конечно играет, но это только для совсем больших программ, но об этом пока задумываться рано)
Секция .rodata — это секция констант во flash, отдельно выделил для удобства, объяснить зачем она нужна проще на примере:
.asmcode . . . . . . LDR R0, CONST @ загружаем значение из ячейки с адресом CONST .rodata CONST: .word 0x12345678 @ значение для загрузки, при компоновке будет помещено в @ секцию .rodata после всех секций .asmcode .asmcode STR R0, [R1] . . .
Если бы мы это сделали выделение памяти «посреди» программы в одной секции, то получили бы ошибку при исполнении, так как команда LDR R0, CONST загрузила бы значение, а потом процессор бы попытался выполнить инструкцию которая закодирована значением .word 0x12345678 (!!). Чтобы такого не было, нам нужно было бы описывать эту константу где то в подходящем месте исходного файла который не будет исполняться (например после команды B, или в конце исходного файла, вспомните куда компилятор размещал константные значения при использовании команды LDR Rx,=CONST в третьей части публикации)…
Применение же отдельной секции гарантирует что программа будет скомпилирована в виде:
.asmcode . . . . . . LDR R0, CONST @ загружаем значение из ячейки с адресом CONST STR R0, [R1] . . .
а константа размещена в отдельной секции .rodata после секций .asmcode
Мелочь ?! — Да! Но иногда очень удобно!
Надеюсь основные идеи которые двигали мною при формировании секций стали понятны, и теперь осталось внести изменения в наш проект «мигалка», с двумя файлами исходной программы, касающиеся именования секций
@GNU AS @ Настройки компилятора .syntax unified @ тип синтаксиса .thumb @ тип используемых инструкций Thumb .cpu cortex-m4 @ микроконтроллер @ Если определение ниже закомментировано, то предполагается что @ использована плата Open407I-C, иначе STM32F4 Discovery .set DEVBOARD, STM32F4DISCO .include "stm32f40x.inc" @ определения микроконтроллера @ Определения в зависимости от отладочной платы .ifdef DEVBOARD .equ GPIO_LED ,GPIOD_BASE @ порт подключения светодиода .equ RCC_GPIO_EN ,RCC_AHB1ENR_GPIODEN_N @ бит включения GPIO .equ GPIO_ODR_NUM ,15 @ номер пина GPIO .else .equ GPIO_LED ,GPIOH_BASE .equ RCC_GPIO_EN ,RCC_AHB1ENR_GPIOHEN_N .equ GPIO_ODR_NUM ,2 .endif .equ GPIO_MODER_MDR ,1<<(GPIO_ODR_NUM*2) @ таблица векторов прерываний, в секции .vectors .section .vectors .word 0x20020000 @ Вершина стека .word Reset+1 @ Вектор сброса .section .asmcode @ секция программы Reset: MOV R0, 0 @ Значение 0, будет использоваться для bitband MOV R1, 1 @ значение 1, будет использоваться для bitband @ включим тактирование GPIO_LED LDR R2, =(PERIPH_BB_BASE + (RCC_BASE + RCC_AHB1ENR) * 32 + RCC_AHB1ENR_GPIO_EN * 4) @ адрес STR R1, [R2] @ запись R1 ("1") по адресу бита указанному в R2 @ установим режим GPIO_LED pin_15 LDR R2, =(PERIPH_BASE + GPIO_LED + GPIO_MODER) @ адрес LDR R3, =GPIO_MODER_MDR @ значение LDR R4, [R2] @ прочитали значение регистра ORR R3, R3, R4 @ логическое, побитовое ИЛИ STR R3, [R2] @ запись обновленного значения в GPIOD_MODER LDR R2, =(PERIPH_BB_BASE + (GPIO_LED + GPIO_ODR) * 32 + GPIO_ODR_NUM*4) @ адрес бита BLINK_LOOP: @ включим светодиод STR R1, [R2] @ запись R1 ("1") по адресу указанному в R2 BL DELAY @ пауза @ выключим светодиод STR R0, [R2] @ запись R0 ("0") по адресу указанному в R2 BL DELAY @ пауза B BLINK_LOOP @ делаем цикл
файл delay.asm@GNU AS @ Настройки компилятора .syntax unified @ тип синтаксиса .thumb @ тип используемых инструкций Thumb .cpu cortex-m4 @ микроконтроллер .section .asmcode @ секция кода программы, идет после .text в прошивке .global DELAY @ по метке DELAY к подпрограмме можно обращаться из других частей программы DELAY: LDR R3, =0x00100000 @ повтор цикла 0x0010 0000 раз. Delay_loop: @ метка доступна только в данном файле, извне не видна ! SUBS R3, R3, 1 BNE Delay_loop BX LR
Теперь, в каком бы мы порядке не отдавали файлы на компоновку — секция .vectors всегда будет в начале исходного файла, а секция .asmcode после нее… А вот какая часть программы будет идти сразу после таблицы указателей прерываний (main.asm или delay.asm) — будет зависеть от порядка их компоновки (кто раньше встал — тогои тапкираньше и разместили)
Что еще нужно дляполного счастья ?написания программ на ассемблере для STM32F4?
p.s. чуть не забыл архив прикрепить blink4.zip
Развиваемся дальше: STM32F4: GNU AS: Настройка тактирования микроконтроллера (Часть 5)