Базовый шаблон [STM32, Часть 2]

9 Сентября 2012 К комментариям

При использовании платных компиляторов большую часть работы по созданию проекта за нас уже как правило сделали, т.е. нам достаточно указать тип МК, нажать ОК и мы получим заготовку проекта с файлами начальной инициализации и прочими сопутствующими данными. Конечно есть заготовки и для 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