目录
前言
在近期的工作需求中,我需要对实时操作系统(RTOS)的底层代码进行一些必要的调整。为此,我重新深入研究了ARM架构的相关知识点。在这一过程中,我回顾了之前一直让我感到困惑的一个技术问题:在几乎所有的单片机项目代码库中,都存在一个名为start.s
的汇编语言启动代码文件。由于我并没有系统地学习过汇编语言,因此对于这段代码的理解总是停留在一知半解的层面。
在整个项目中,通常只有一个汇编文件,专门去学习汇编语言似乎显得有些不必要。因此,我考虑是否可以使用C语言来实现这一汇编代码的功能。这样的做法不仅可以避免学习新的语言,而且可能在某些情况下提供更高的代码可读性和可维护性。
实现思路
单片机启动流程
单片机的启动流程可以大致归纳为以下几步:
- 上电或复位信号触发,单片机开始启动过程。
- 硬件检查栈指针初始化,通常指向预设的栈顶地址。
- 硬件从中断向量表中读取复位处理程序的入口地址。
- 执行复位处理程序(Reset_Handler),进行系统初始化。
- 复位处理程序从 Flash 复制初始化数据到 RAM。
- 复位处理程序清零 BSS 段,初始化未初始化的数据段。
- 复位处理程序跳转到
main
函数,开始执行主程序。
因此理论上,只要能完成以上步骤,无论使用用什么编程语言,都可以实现,其中 2 中栈起始位置的设置,4 - 7 Reset_Handler
函数的实现是我们需要做的,其余是硬件已经做好的。
汇编语言分析
先看一段CubeMX生成的项目中的startup.s
.syntax unified
.cpu cortex-m3
.fpu softvfp
.thumb
.global g_pfnVectors
.global Default_Handler
.word _sidata
.word _sdata
.word _edata
.word _sbss
.word _ebss
.equ BootRAM, 0xF108F85F
.section .text.Reset_Handler
.weak Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
ldr r0, =_sdata
ldr r1, =_edata
ldr r2, =_sidata
movs r3, #0
b LoopCopyDataInit
CopyDataInit:
ldr r4, [r2, r3]
str r4, [r0, r3]
adds r3, r3, #4
LoopCopyDataInit:
adds r4, r0, r3
cmp r4, r1
bcc CopyDataInit
ldr r2, =_sbss
ldr r4, =_ebss
movs r3, #0
b LoopFillZerobss
FillZerobss:
str r3, [r2]
adds r2, r2, #4
LoopFillZerobss:
cmp r2, r4
bcc FillZerobss
bl __libc_init_array
bl main
bx lr
.size Reset_Handler, .-Reset_Handler
.section .text.Default_Handler,"ax",%progbits
Default_Handler:
Infinite_Loop:
b Infinite_Loop
.size Default_Handler, .-Default_Handler
.section .isr_vector,"a",%progbits
.type g_pfnVectors, %object
.size g_pfnVectors, .-g_pfnVectors
g_pfnVectors:
.word _estack
.word Reset_Handler
.word NMI_Handler
.word HardFault_Handler
/* 省略后面的项 */
/* ... */
.weak NMI_Handler
.thumb_set NMI_Handler,Default_Handler
.weak HardFault_Handler
.thumb_set HardFault_Handler,Default_Handler
/* 省略后面的项 */
/* ... */
在这段代码中,其实最主要事就是两件
- 定义了入口函数
Reset_Handler
,通过它最终进入main
函数 - 定义中断向量表,其中最主要的是前面两项:设置栈起始位置,设置入口函数
因此我们可以用C语言去做这两件事
开发环境
- 硬件平台
stm32f103c8t6最小系统板。便宜
- 编译器
gcc-arm-none-eabi + make。由于要保证底层的运行都是可控的,因此使用这一套工具链使用起来会更加方便。
- 调试器
openocd + stlink。用于烧录调试代码
- 代码编辑器
vscode。好用,懂的都懂
代码分析
废话不多说,上代码。总共有以下三个代码文件main.c
,linker.ld
和Makefile
-
main.c
实现功能的主要代码。-
实现了reset_handler函数
-
定义了中断向量表
-
typedef unsigned int uint32_t;
/* 声明所需寄存器 */
#define RCC_APB2ENR *(uint32_t*)0x40021018
#define GPIOC_CRH *(uint32_t*)0x40011004
#define GPIOC_ODR *(uint32_t*)0x4001100c
/* 在ld文件中声明的地址 */
extern uint32_t _estack; /* 栈顶地址 */
extern uint32_t _sdata; /* 已初始化数据段起始地址(Flash)*/
extern uint32_t _edata; /* 已初始化数据段结束地址(Flash)*/
extern uint32_t _sidata; /* 已初始化数据段起始地址(SRAM)*/
extern uint32_t _sbss; /* 未初始化数据段(BSS)起始地址 */
extern uint32_t _ebss; /* 未初始化数据段(BSS)结束地址 */
/* 主函数声明 */
int main(void);
/* 硬件复位后开始执行的启动代码 */
void reset_handler(void)
{
uint32_t *src, *dest;
uint32_t *end;
/* 复制数据段初始化值从Flash到SRAM */
src = (uint32_t *)&_sidata;
dest = (uint32_t *)&_sdata;
end = (uint32_t *)&_edata;
while (dest < end) {
*dest++ = *src++;
}
/* 清零BSS段 */
dest = (uint32_t *)&_sbss;
end = (uint32_t *)&_ebss;
while (dest < end) {
*dest++ = 0;
}
/* 进入main */
main();
/* 以防main函数返回 */
while (1) {
;
}
}
/* 设置中断向量表 */
typedef void (*irq_handler)(void); /* 中断向量类型 */
/* 将中断向量表放在.isr_vector段 */
__attribute__((section(".isr_vector")))
__attribute__((aligned(4)))
irq_handler g_pfn_vectors[] = {
(irq_handler) &_estack,
reset_handler,
};
/* 延时1秒 */
void delay(void)
{
volatile int times = 1000000;
while (times--) {
;
}
}
/* LED(PC13)闪烁,间隔1秒 */
int main(void)
{
RCC_APB2ENR |= (1 << 4);
GPIOC_CRH |= (1 << 20); /* GIPOC13 */
while (1) {
GPIOC_ODR |= (1 << 13); /* 拉高默认低 */
delay();
GPIOC_ODR &= ~(1 << 13); /* 拉低 */
delay();
}
}
linker.ld
规定将生成的二进制文件放在内存中对应的位置,比如中断向量表存放在0x08000000这个位置,就是在这个文件中规定的。- 基本都是固定写法,用汇编和C区别不大。定义了内存中各个段的位置变量,方便在其它文件中进行操作,如
main.c
对各个段的初始化操作都需要用到这里定义的变量,中断向量表存放的段也在这里声明。
- 基本都是固定写法,用汇编和C区别不大。定义了内存中各个段的位置变量,方便在其它文件中进行操作,如
ENTRY(reset_handler) /* 程序入口地址(可以不写,为了可读性) */
_estack = ORIGIN(RAM) + LENGTH(RAM); /* 栈向下生长,起始位置为RAM地址最后 */
_Min_Heap_Size = 0x200; /* 堆内存大小 */
_Min_Stack_Size = 0x400; /* 栈内存大小 */
/* 内存分布 */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 64K
}
/* 定义输出段 */
SECTIONS
{
/* 启动代码从FLASH的第一段开始 */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* 向量表 */
. = ALIGN(4);
} >FLASH
/* 程序代码和其他数据进入FLASH */
.text :
{
. = ALIGN(4);
*(.text)
*(.text*) /* 代码 */
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext = .;
} >FLASH
/* 只读数据段存放在FLASH中 */
.rodata :
{
. = ALIGN(4);
*(.rodata) /* 只读数据 */
*(.rodata*)
. = ALIGN(4);
} >FLASH
/* 用于初始化数据段 */
_sidata = LOADADDR(.data);
/* 已初始化的数据段会加载到RAM中,加载内存地址副本会在代码段之后被创建 */
.data :
{
. = ALIGN(4);
_sdata = .; /* create a global symbol at data start */
*(.data) /* .data sections */
*(.data*) /* .data* sections */
. = ALIGN(4);
_edata = .; /* define a global symbol at data end */
} >RAM AT> FLASH
/* 未初始化数据段 */
. = ALIGN(4);
.bss :
{
_sbss = .;
__bss_start__ = _sbss;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
__bss_end__ = _ebss;
} >RAM
/*
* 此段不占内存,用于检查RAM中是否足够存储stack和heap
* 如果内存不够,链接器会报错
*/
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} >RAM
/* 移除标准库中没用使用的信息,减少最后生成文件的内存 */
/DISCARD/ :
{
libc.a ( * )
libm.a ( * )
libgcc.a ( * )
}
.ARM.attributes 0 : { *(.ARM.attributes) }
}
Makefile
文件多的时候方便构建所有代码,本次由于编译选项较多,为了方便用make构建,也可以不用。
#编译器
PREFIX = arm-none-eabi-
CC = $(PREFIX)gcc
AS = $(PREFIX)gcc -x assembler-with-cpp
OBJCOPY = $(PREFIX)objcopy
OBJDUMP = $(PREFIX)objdump
SZ = $(PREFIX)size
#生成目标
TARGET = main
#可选
OPT = -Og
# C文件
C_SOURCES = main.c
ASM_SOURCES =
HEX = $(OBJCOPY) -O ihex
BIN = $(OBJCOPY) -O binary -S
CPU = -mcpu=cortex-m3
MCU = $(CPU) -mthumb $(FPU) $(FLOAT-ABI)
ASFLAGS = $(MCU) $(OPT) -Wall -fdata-sections -ffunction-sections
CFLAGS += $(MCU) $(OPT) -Wall -fdata-sections -ffunction-sections
CFLAGS += -g -gdwarf-2 -MMD -MP -MF"$(@:%.o=%.d)"
#链接脚本
LDSCRIPT = c8t6.ld
LIBS = -lc -lm -lnosys
LIBDIR =
LDFLAGS = $(MCU) -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(TARGET).map,--cref -Wl,--gc-sections -nostartfiles
all: $(TARGET).elf $(TARGET).hex $(TARGET).bin
OBJECTS = main.o
%.o: %.c $(LDSCRIPT)
$(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(notdir $(<:.c=.lst)) $< -o $@
%.o: %.s $(LDSCRIPT)
$(AS) -c $(CFLAGS) $< -o $@
$(TARGET).elf: $(OBJECTS) Makefile
$(CC) $(OBJECTS) $(LDFLAGS) -o $@
$(SZ) $@
%.hex: %.elf
$(HEX) $< $@
%.bin: %.elf
$(BIN) $< $@
#下载命令
install: $(TARGET).hex
openocd -f openocd.cfg -c "program main.hex verify reset" -c "reset run" -c "exit"
.PHONY: clean
clean:
-rm -f $(OBJECTS) $(TARGET).elf $(TARGET).hex $(TARGET).lst $(TARGET).bin $(TARGET).map ./*.d
总结
第一次发帖,可能讲不够细致,欢迎大家评论,我都会一一回复。
标签:初始化,代码,ALIGN,C语言,单片机,代码优化,Handler,main,uint32 From: https://blog.csdn.net/weixin_44884878/article/details/140905771