Базовый шаблон [STM32, Часть 2]
При использовании платных компиляторов большую часть работы по созданию проекта за нас уже как правило сделали, т.е. нам достаточно указать тип МК, нажать ОК и мы получим заготовку проекта с файлами начальной инициализации и прочими сопутствующими данными. Конечно есть заготовки и для GCC, но так как мне интересен весь процесс я попробую разобраться что к чему.
Введение
В состав GCC входят компилятор и компоновщик (линкер), задача компилятора прочитать наш программный код и сформировать из них набор инструкций, которые компоновщик соберет в конечный выходной файл годный для заливки в МК.
Исходя из того нам нужно каким-то образом объяснить компоновщику как устроен наш МК, т.е. написать некий сценарий, т.е. указать сколько у нас флеш и ОЗУ, по каким адресам расположены, а так же указать куда что «положить».
Кроме того нужно выполнить начальную инициализацию МК. Почему эти действия не заложены на автомате как например в AVR-GCC, я думаю из-за разных реализаций микроконтроллеров с ARM архитектурой.
Сценарий
Необходимый минимум информации который должен содержать сценарий:
- расположение и размер флеш и ОЗУ
- расположение кода, данных
- размер и расположение стека
- имя точки входа
Первым делом указываем расположение и размер флеш и ОЗУ:
MEMORY
{
flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K
ram (rwx) : ORIGIN = 0x20000000, LENGTH = 8K
}
Рассмотрим подробнее указание флеш-памяти:
flash | имя области памяти |
(rx) | атрибут означающий что область памяти доступна только на чтение (r), а так же можно запускать код на исполнение (x) |
ORIGIN = 0×08000000 | начальный адрес области |
LENGTH = 128K | размер области, при компоновке если будет превышен размер компоновщик сообщит об ошибке |
Далее необходимо описать карту распределения памяти.
Скажу сразу что вся область памяти для простоты разбивается на секции (программный код, константы, вектора прерываний, область загрузчика и т. д.), по умолчанию расположение секций в памяти идет согласно их порядку при описании, можно менять порядок (позже при указании стека мы этим воспользуемся).
Вначале указываем расположение кода программы, таблицу векторов прерываний и данных (константы):
.text :
{
. = ALIGN(4);
KEEP(*(.interrupt_vector))
KEEP(*(.isr_vector))
*(.text)
*(.text*)
*(.rodata)
*(.rodata*)
*(.glue_7)
*(.glue_7t)
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
} > flash
Рассмотрим подробнее:
.text {} > flash | оператор размещает все его элементы во флеш память с начального адреса |
. = ALIGN(4) | оператор точка определяет текущий адрес, оператор ALIGN(4) – предписываем компоновщику выравнивать адреса по 4 байта |
KEEP(*(.interrupt_vector)) | в самом начале флеш-памяти необходимо разместить таблицу прерываний (согласно документации для данного МК), оператор KEEP() говорит компоновщику что эту секцию нужно пропустить, а то ещё с оптимизирует ;) |
*(.text)*(.text*) | после таблицы прерываний как правило располагается программный код |
*(.rodata)*(.rodata*) | константы как правило располагаются после программного кода (rodata – сокращение от read-only-data) |
Далее небольшой нюанс, так как у нас в программе возможно будут использоваться переменные которые могут иметь начальные значение, то не плохо бы хранить эти значения во флеш, а так же вынести в отдельную секцию и при старте просто копировать значения из флеш-памяти в ОЗУ. Целесообразно расположить эти данные в конце используемого пространства флеш-памяти (замечу не в конце флеш-памяти, можно было бы и в конце, но тогда бы пришлось прошивать данные в разные места флеш-памяти что согласитесь не очень удобно), для этого запомним последний используемый адрес флеш-памяти:
_data_flash = .;
Раннее я писал что оператор «точка» определяет текущий адрес.
И сразу опишем секцию:
.data : AT ( _data_flash )
{
. = ALIGN(4);
_data_begin = .;
_sdata = _data_begin;
*(.data)
*(.data*)
. = ALIGN(4);
_data_end = .;
_edata = _data_end;
} > ram
Так же для того что бы при старте знать что куда копировать «запоминаем» начальный и конечный адрес области (_data_begin, _data_end).
Аналогично задаем секцию для переменных начальные значение которых равны нулю (обнуляем их сами при старте):
.bss :
{
. = ALIGN(4);
_bss_begin = .;
_sbss = _bss_begin;
__bss_start__ = _bss_begin;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_bss_end = .;
_ebss = _bss_end;
__bss_end__ = _bss_end;
} > ram
Ну и последним этапом необходимо указать размер и расположение стека:
_stack_size = 256;
_stack_end = 0x20000000 + 8K;
_estack = _stack_end;
_stack_begin = _stack_end - _stack_size;
. = _stack_begin;
._stack :
{
. = . + _stack_size;
} > ram
Можно было бы не указывать размер стека, а просто задать указатель на конец ОЗУ, но тогда компоновщик не предупредить нас нам не хватить памяти под стек. (Если быть точным то мы указали не фиксированный размер и минимальный, так как реально данные как правило занимает не всю доступную область ОЗУ, а лишь часть и стек может иметь больший размер).
Осталось указать точку входа (адрес с которого начинается выполнение программы), в нашем случае при старте нам необходимо произвести инициализацию переменных, а уже потом переходить к основной программе:
ENTRY(Reset_Handler)
Что бы лучше понять распределение памяти я нарисовал вот такую схемку:
[caption id=”attachment_42” align=”aligncenter” width=”291”] Распределение памяти[/caption]
Инициализация
Осталось выполнить начальную инициализацию МК. Почему эти действия не заложены на автомате как например в AVR-GCC, я думаю из-за разных реализаций микроконтроллеров с ARM архитектурой.
Основные действия при инициализации:
- инициализация переменных
- указание стека
- передача управления основной программе
Когда я первый раз писал эту статью инициализацию делал сам на Си, но сейчас не вижу в этом смысла. Готовый шаблон сценария и инициализации, написанный на ассемблере, необходимо взять из стандартной библиотеки предлагаемой производителем. Для этого на сайте (www.st.com), выбираем нужный микроконтроллер и скачиваем архив с библиотекой.
Для примера я возьму микроконтроллер STM32L152RBT6 из серии STM32L1.
На текущий момент для него доступна библиотека STM32L1xx standard peripherals library версии 1.1.1 (скачать). Необходимый нам файл startup_stm32l1xx_md.S находится в папке:
stm32l1_stdperiph_lib\STM32L1xx_StdPeriph_Lib_V1.1.1\Libraries\CMSIS\Device\ST\STM32L1xx\Source\Templates\gcc_ride7
Индекс md в названии файла означает одну из трех серий:
- High-density Devices
- Medium-density Devices
- Medium-density Plus Devices
Серии отличаются “фаршированностью”, т.е. чем богаче периферия микроконтроллера тем выше индекс.
В коде инициализации вызывается внешняя функция SystemInit, на данном этапе сделаем простую заглушку для нее:
//------------------------------------------------------------------------------
void SystemInit(void)
{
}
Makefile
Можно компилировать каждый отдельный файл в ручную, но гораздо удобнее автоматизировать этот процесс, для этого есть замечательная утилита GNU Make. Сценарий согласно которому она производит сборку проекта принято располагать в файле с именем Makefile, если не указывать на прямую пусть к файлу, то при запуске она ищет в текущей директории данный файл.
Формат файла достаточно своеобразен, поэтому лучше конечно почитать о нем в документации к make.
Я составил минимально возможный Makefile для сборки проекта:
#-------------------------------------------------------------------------------
# Makefile @ Denis Zheleznyakov http://ziblog.ru
#-------------------------------------------------------------------------------
OPTIMIZATION = s
#-------------------------------------------------------------------------------
SRC_C = main.c
SRC_C += system_init.c
SRC_ASM += startup_stm32l1xx_md.s
#-------------------------------------------------------------------------------
CROSS_PATH = C:/Tools/CodeSourcery/
CROSS_VERSION = 2012-03-56
CROSS = $(CROSS_PATH)$(CROSS_VERSION)/bin/arm-none-eabi-
INCLUDES += -I$(CROSS_PATH)/arm-none-eabi/include
INCLUDES += -I$(CROSS_PATH)/arm-none-eabi/include/lib
INCLUDES += -Imcu
INCLUDES += -Imcu/core
INCLUDES += -Imcu/startup
INCLUDES += -Imcu/peripherals
INCLUDES += -Imcu/std_lib/inc
INCLUDES += -Imcu/std_lib/src
INCLUDES += -Iutility
VPATH += mcu
VPATH += mcu/core
VPATH += mcu/startup
VPATH += mcu/peripherals
VPATH += mcu/std_lib/inc
VPATH += mcu/std_lib/src
VPATH += utility
#-------------------------------------------------------------------------------
FLAGS_C = $(INCLUDES) -I.
FLAGS_C += -O$(OPTIMIZATION)
FLAGS_C += -gdwarf-2
FLAGS_C += -Wall
FLAGS_C += -c
FLAGS_C += -fmessage-length=0
FLAGS_C += -fno-builtin
FLAGS_C += -ffunction-sections
FLAGS_C += -fdata-sections
FLAGS_C += -msoft-float
FLAGS_C += -mapcs-frame
FLAGS_C += -D__thumb2__=1
FLAGS_C += -mno-sched-prolog
FLAGS_C += -fno-hosted
FLAGS_C += -mtune=cortex-m3
FLAGS_C += -mcpu=cortex-m3
FLAGS_C += -mthumb
FLAGS_C += -mfix-cortex-m3-ldrd
FLAGS_C += -fno-strict-aliasing
FLAGS_C += -ffast-math
FLAGS_C += -std=c99
FLAGS_LD = -Xlinker -Map=target/target.map
FLAGS_LD += -Wl,--gc-sections
FLAGS_LD += -mcpu=cortex-m3
FLAGS_LD += -mthumb
FLAGS_LD += -static
FLAGS_LD += -stdlib
#LIB_LD = -lm
FLAGS_ASM = -D__ASSEMBLY__
FLAGS_ASM += -g $(FLAGS_C)
FLAGS_ASM += -I. -x assembler-with-cpp
#-------------------------------------------------------------------------------
all: sperator target.elf
mcu_all: sperator target.elf mcu_prog
%.elf: $(SRC_ASM:%.S=target/%.o) $(SRC_C:%.c=target/%.o)
@echo Linking: $@
@$(CROSS)gcc $(FLAGS_LD) -T'mcu/startup/stm32l152rb.lsf' -o 'target/$@' $^ $(LIB_LD)
@echo '--------------------------------------------------------------------'
@$(CROSS)size 'target/target.elf'
@$(CROSS)size 'target/target.elf' > 'target/target.log'
@$(CROSS)objcopy -O binary 'target/target.elf' 'target/target.bin'
@$(CROSS)objcopy -O ihex 'target/target.elf' 'target/target.hex'
@$(CROSS)objdump -h -S -z 'target/target.elf' > 'target/target.lss'
@$(CROSS)nm -n 'target/target.elf' > 'target/target.sym'
@rm -f target/*.o
$(SRC_C:%.c=target/%.o): target/%.o: %.c
@echo Compiling: $>
@$(CROSS)gcc $(FLAGS_C) -c $> -o $@
$(SRC_ASM:%.s=target/%.o): target/%.o: %.s
@echo Compiling asm: $>
@$(CROSS)gcc $(FLAGS_ASM) -c $> -o $@
mcu_prog:
@$(PROGRAMMATOR) -c SWD -ME @$(PROGRAMMATOR) -c SWD -P "target/target.hex" -V "target/target.hex" -Q -Rst -Run
mcu_reset:
@$(PROGRAMMATOR) -c SWD -Rst -Run
clean:
@echo '--------------------------------------------------------------------'
@echo > target/dummy.txt
@rm -f target/*.*
sperator:
@echo '--------------------------------------------------------------------'
.PHONY : all clean mcu_prog mcu_reset
Кратенько о содержании:
OPTIMIZATION = s | задаем уровень оптимизации, может принимать [0,1,2,3,s] в данном случае оптимизировать по размеру выходного кода, как правило его не так часто нужно менять |
SRC_C = startup.c SRC_C += main.c | указываем какие файлы нужно компилировать, при добавлении новых нужно добавить в ручную, например создали файл test.c, то добавить нужно строчку SRC_C += test.c |
CROSS_PATH = C:/Tools/CodeSourcery/2010-09-51/ CROSS = $(CROSS_PATH)/bin/arm-none-eabi- INCLUDES += -I$(CROSS_PATH)/arm-none-eabi/include INCLUDES += -I$(CROSS_PATH)/arm-none-eabi/include/lib INCLUDES += -Imcu INCLUDES += -Imcu/peripherals INCLUDES += -Imcu/startup INCLUDES += -Iutility VPATH += mcu VPATH += mcu/startup VPATH += mcu/peripherals VPATH += utility | указываем пути до GCC, а так же пути где компилятору искать файлы исходных кодов и заголовочные файлы. |
FLAGS_C = $(INCLUDES) -I. FLAGS_C += -O$(OPTIMIZATION) FLAGS_C += -Wall FLAGS_C += -c FLAGS_C += -fmessage-length=0 FLAGS_C += -fno-builtin FLAGS_C += -ffunction-sections FLAGS_C += -fdata-sections FLAGS_C += -msoft-float FLAGS_C += -mapcs-frame FLAGS_C += -D__thumb2__=1 FLAGS_C += -mno-sched-prolog FLAGS_C += -fno-hosted FLAGS_C += -mtune=cortex-m3 FLAGS_C += -mcpu=cortex-m3 FLAGS_C += -mthumb FLAGS_C += -mfix-cortex-m3-ldrd FLAGS_LD = -Xlinker FLAGS_LD += –gc-sections FLAGS_LD += -mcpu=cortex-m3 FLAGS_LD += -mthumb FLAGS_LD += -static FLAGS_LD += -nostdlib | ключи для правильно компиляции и сборки |
all: clean target.elf … | в этой части описаны так называемые цели, т.е. сам сценарий сборки |
Основа проекта
Осталось только всё созданные файлы разложить по “полочкам”, мне нравиться вот такая структура проекта:
target | директория выходных файлов (прошивка) |
mcu | содержит файл инициализации, файл сценария для компоновщика и т.п., а так же файлы для работы с периферией |
utility | файлы вспомогательного кода |
корневая директория | содержит основной файл программы (main), файл конфигурации (config.h) и Makefile |
Так как данная статья отличается от первоначально опубликованного оригинала, мне придется отложить примеры до следующего раза.
В следующей статье приведу примеры под имеющиеся у меня платы:
- STM32VL-Discovery
- STM32L-Discovery
- STM32F0-Discovery
- STM32F4-Discovery
Последние две платы мной ещё не включались, значит будут интересно :)
comments powered by Disqus