Обезвреженный шаблон для STM32

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

Речь пойдет о том как написать минимальной код для STM32 (тестил только на F103xx), запустить его и отладить без JTAG/SWD. Когда-то я читал, что-то подобное для AVR, а вот для STM32 не нашел, все примеры обмазаны CMSIS и JTAG.

Hardware

Под этим заголовком ничего интересного не будет, все отлично рассказано в AN2586 `Getting started with STM32F10xxx hardware development` и в многочисленных постах здесь о том как кто-то спаял платку. Надо только заметить, что нам понадобятся RX/TX и BOOT0/BOOT1 выводы для доставки кода во flash или ram. Об этом можно узнать больше из AN2606 `STM32 microcontroller system memory boot mode`.

Toolchain

Насколько я понял подойдет любой новый gcc+binutils набор для arm. Генерация кода для cortex-m3 включается так же как например генерация кода для Atom на x86. А именно, вот так.


-mcpu=cortex-m3 -mthumb


У меня залежался вот такой toolchain.


armv5tel-softfloat-linux-gnueabi-


Дальше будем использовать именно его.

Makefile



TARGET	= bli
PREFIX	?= armv5tel-softfloat-linux-gnueabi-

CC      = $(PREFIX)gcc
AS      = $(PREFIX)as
LD      = $(PREFIX)ld
OC      = $(PREFIX)objcopy
OD      = $(PREFIX)objdump
RM      = rm -f

CFLAGS	= -mcpu=cortex-m3 -mthumb -fno-builtin -std=c99
CFLAGS  += -pipe -Wall -Os
AFLAGS	= -mcpu=cortex-m3 -mthumb
LFLAGS  = -T stm32.ld

OBJ	= $(patsubst %.s, %.o, $(wildcard *.s))
OBJ	+= $(patsubst %.c, %.o, $(wildcard *.c))

all: $(TARGET)

%.o: %.s
	@ echo "  AS    " $@
	@ $(AS) $(AFLAGS) -o $@ $<

%.o: %.c
	@ echo "  CC    " $@
	@ $(CC) -c $(CFLAGS) -MMD -o $@ $<

$(TARGET).elf: $(OBJ)
	@ echo "  LD    " $@
	@ $(LD) $(LFLAGS) -o $@ $^ 

$(TARGET): $(TARGET).elf
	@ echo "  OCOPY " $@
	@ $(OC) -j .text -j .data -O binary $< $@

tags: $(SRC)
	@ echo "  CTAGS "
	@ ctags --c-kinds=+px --fields=+Sn -R .

scope: $(SRC)
	@ echo "  CSCOPE"
	@ cscope -b -k -f scope -R -s .

tag: tags scope

ram: $(TARGET)
	@ echo "  RAM " $^
	@ stm32flash -fv -w $(TARGET) -a 0x20000200 -g 0x20000200 /dev/ttyUSB0

flash: $(TARGET)
	@ echo "  FLASH " $^
	@ stm32flash -fv -w $(TARGET) -g 0x0 /dev/ttyUSB0

dump: $(TARGET).elf
	@ echo "  ODUMP " $^
	@ $(OD) -D $(TARGET).elf

clean:
	@ echo "  CLEAN "
	@ $(RM) $(OBJ) $(TARGET).elf $(TARGET)
	@ $(RM) $(patsubst %.o, %.d, $(OBJ))
	@ $(RM) tags scope

include $(wildcard *.d)



Общая структура это файла почти всегда одинакова, если вы не пишете какое нибудь bloatware (тогда можно подумать о замене make на что-то более другое).

Украшения (@ echo " LD " и т.п.) можно вырезать, будет немного проще.

LD Script

В Makefile во флагах линкера указан файл stm32.ld, вот он.


MEMORY
{
	RAM (rwx)	: ORIGIN = 0x20000000, LENGTH = 6K
	FLASH (rx)	: ORIGIN = 0x08000000, LENGTH = 16K
}

ld_stack = 0x20000000 + 6K - 1;

SECTIONS
{
	.text 0x20000200 : ALIGN(4)
	{
		ld_sisr = . ;

		KEEP(*(.vectors))

		. = ALIGN(4);

		*(.text)
		*(.text.*)
		*(.rodata)
		*(.rodata.*)
 
		. = ALIGN(4);
		ld_etext = . ;

	} > RAM

	.data : AT (ld_etext) ALIGN(4)
	{
		ld_sdata = . ;

		*(.data)
		*(.data.*)

		. = ALIGN(4);
		ld_edata = . ;

	} > RAM

	.bss : ALIGN(4)
	{
		ld_sbss = . ;

		*(.bss)
		*(.bss.*)
		*(COMMON)

		. = ALIGN(4);
		ld_ebss = . ;

	} > RAM
}


Это указания для ld какой код из каких файлов или их секций куда помещать в итоговом elf файле.

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

ld_stack это адрес конца памяти, используется как начальное значение для регистра sp. Не обязательно объявлять его именно в скрипте ld. Я рассчитывал таким образом избавиться от указания адреса и размера RAM дважды, но как именно это красиво сделать так и не придумал.

Такое объявление приводит к появлению нового символа, адрес которого можно использовать в C или asm коде.

Дальше самое важное, это определение секций выходного файла. Приведенная версия скрипта предназначена для запуска кода из RAM. Загрузчик ST использует для своих целей первые 512 байт RAM, поэтому адрес после имени секции .text смещен от начала RAM на такое же количество байт. Это адрес начала этой секции, с которого начинается отсчет адресов всех символов в этой секции. В мануале он называется виртуальным (VMA / Virtual Memory Address). Есть ещё LMA (Load Address), который определяет куда ляжет секция в итоговом бинаре, который можно сделать с помощью objcopy. LMA задается после слова AT и как можно видеть используется для секции .data которая в бинаре лежит после .text хотя код обращается к ней по адресам в RAM. А в RAM начальные значения из флеша переносятся стартовым кодом, именно для него объявлены символы ld_etext, ld_sdata и т.д.

Если VMA секции не указан явно, то она располагается в свободной части указанной с помощью `>` области памяти. Если не указан LMA то он считается равным VMA. Если например не указать соответствующий LMA для секции .data а секцию .text направить во флеш, то можно получить бинарь огромных размеров включающий в себя данные начиная с 0x08000000 и до 0x20000000.

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

Если надо слинковать код для загрузки во флеш, то надо убрать адрес и поменять область памяти куда положить секцию.


	.text : ALIGN(4)
	{
		ld_sisr = . ;

		KEEP(*(.vectors))

		. = ALIGN(4);

		*(.text)
		*(.text.*)
		*(.rodata)
		*(.rodata.*)
 
		. = ALIGN(4);
		ld_etext = . ;

	} > FLASH


И ещё небольшая особенность. В секцию .bss включено содержимое COMMON секции. Не копал почему так, но не инициализированные глобальные переменные оказываются именно в ней, а не в .bss, если не включить, то ld_ebss будет иметь неверное значение.

И по поводу выравнивания, оно почти бесплатно (gcc неплохо пакует байты по словам) и позволяет выполнять копирование по словам а не по байтам.

Start Up

Это тот код с которого начинается выполнение. Независимо от того, загрузка ли это с флеша или команда GO загрузчика с адресом в RAM, происходит следующее:

— Инициализация стека, sp = *(start_addr)
— Переход на обработчик ресета, pc = *(start_addr+4)

Надо заметить, что адрес обработчика ресета должен иметь младший бит равный еденице, так же как и адреса для инструкций bl/bx. Для этого имеется следующая строка.


.type isr_reset, function


Я довольно долго ковырялся с этим пока не понял в чем дело. start_addr это либо 0 в случае старта из флеша, либо любое указанное число при старте через GO.


.syntax unified
.cpu cortex-m3
.thumb

.section .vectors

	.word	ld_stack
	.word	isr_reset
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0
	.word	0

.section .text

.global isr_reset
.type isr_reset, function

isr_reset:

/* Copying .data section from flash to ram */
/*
	ldr	r2, =(ld_sdata)
	ldr	r1, =(ld_etext)
	ldr	r3, =(ld_edata)

	b.n	__ir_data_comp

__ir_data_loop:

	ldr.w	r0, [r1], #4
	str.w	r0, [r2], #4

__ir_data_comp:

	cmp	r2, r3
	bne.n	__ir_data_loop
*/
/* Filling .bss section */

	ldr	r2, =(ld_sbss)
	ldr	r1, =(ld_ebss)
	mov	r0, #0

	b.n	__ir_bss_comp

__ir_bss_loop:

	str.w	r0, [r2], #4

__ir_bss_comp:

	cmp	r2, r1
	bne.n	__ir_bss_loop

/* Jump to C code entry */

	b.w	bli_entry




Копирование данных и зануление .bss как у всех, это не специфично для STM32. Это пример загрузки кода в RAM поэтому копирование .data закомменчено, данные уже в RAM, нет смысла копировать когда src == dst.

C code

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


void bli_entry()
{
	bli_board_init();
	dbg_init();

	while (1) {

		sleep(2);

		GPIOA->BSRR |= BIT(14);

		dbg_printf("LED%d %s\r\n", 14, "ON");

		sleep(2);

		GPIOA->BSRR |= BIT(30);

		dbg_printf("LED%d %s\r\n", 14, "OFF");
	}
}


Кода там мало и он простой, комментить нечего, дальше надо читать reference manual.

stm32flash

Оригинал со страницы гуглокода не годится для загрузки кода в RAM, указать адрес там нельзя, только номер страницы флеша. Поэтому был написан патч добавляющий возможность указать адрес куда производить запись и отключать стирание флеша перед записью.

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

TODO

Что ещё было не сделано, хотя можно было и сделать.

— Инициализации PLL и делителей
— Указание смещения таблицы векторов (VTOR)
— Как-то совместить использование stm32flash и picocom на одном UART интерфейсе
— Форкнуть stm32flash

Если я забыл что-то важное, то жду комментов.

Файлики

Прицепляю, патч для stm32flash, мануал по ld (нигде не нашел его в pdf, собрал сам из исходников). Ну и сам шаблон.

Errata

— Не нужно вычитать единицу из адреса конца памяти для инициализации указателя стека. Когда делается push то sp сначала декрементится а потом адрес уже используется, push(x) {*--sp = x;}.

ld_stack = 0x20000000 + 6K — 1;

Хотя это работает, из-за того, что видимо аппаратно производится выравнивание, и в результате sp равен вот чему.

SP = 200017FC

Проверил вот так.


	int	sp;

	__asm__ __volatile__ (
		"mov %0, sp"	"\n\t"
		: "=r" (sp)
		: :
	);

	sleep(100);

	dbg_printf("SP = %x", sp);
  • +2
  • 13 мая 2012, 17:22
  • amaora
  • 3
Файлы в топике: e4r.diff.gz, ld.pdf.gz, bli.tar.gz

Комментарии (13)

RSS свернуть / развернуть
Уберите лишние переносы строки
0
Благодарю, так на много лучше :)
0
«Не знаю есть ли ещё такие как я, кто не пользуется аппаратной отладкой и готовыми либами.»
А про меня забыл? :)

ziblog.ru/2011/01/06/pervyiy-start-s-stm32-discovery-chast-4/
0
  • avatar
  • ZiB
  • 15 мая 2012, 08:28
Я чет-то не понял, а как вы отлаживаете без SWG и что за интересная функция такая dbg_printf?
0
*SWD
0
Так и отлаживаю, надо узнать значание переменной или выяснить выполняется ли этот код, ставлю printf и смотрю в терминал. В таком способе главное написать стартовый минимум, когда printf уже начинает работать. До этого все вслепую, ну или с помощью led.
0
А зачем такой изврат, если есть доступная, быстрая и удобная аппаратная отладка?
+1
Это вопрос `simple vs. easy`, а jtag/swd для меня в категории easy т.к. я их не понимаю полностью и считаю слишком сложными.
0
Прошивку ведь все равно нужно как-то заливать, вот через SWD и заливать. Для начала. А потом настроить работу с отладчиком и попробовать пошагам пройтись по программе. После этого желание извращаться с «отладкой» на printf отпадет раз и навсегда.
0
Ну я работал (немного) с фрискейловскими dsp через jtag. Ничего не отпало, printf остается более простым.

Припоминаю, что даже gdb я чаще использовал таким образом, чтобы остановаки не происходило, только распечатать инфу и работать дальше (можно писать скрипт который выполяется по бряку). То есть почти printf, только без пересборки. Но это уже другая причина. Во многих случах поведение программы меняется если позволять ей стоять на бряках.
0
printf-ы тоже влияют на тайминги, так что тут у них никакого преимущества нет.

Касательно простоты: printf привычнее, потому и кажется более простым.
0
То есть функция dbg_printf не имеет никакого отношения к gdb?
0
Да. Кстати надо попробовать привернуть gdb без jtag и swd :) Хотя он мне и не нужен, но было бы забавно, и может кому (или мне когда нибудь) будет надо.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.