从零开始创建GD32VF103 Makefile工程
目录
本文简单介绍如何从零开始创建一个GD32VF103 Makefile 工程,基于RV-STAR开发板。
写在开头
在本站文章使用GD32VF103固件库创建Makefile工程 中,创建Makefile工程所使用的链接脚本、启动代码等文件 均是固件库提供的。然而在嵌入式开发中,理解一个最小的裸机工程是非常重要的。 本文将通过一个基于GD32VF103 MCU的示例,展示如何从链接脚本、 启动代码到 main.c 程序,构建一个可以点亮通过GPIO控制的LED的最小工程。
工程组成
该工程包含以下几个核心部分:
- 链接脚本 (linker.ld): 用来描述程序如何被放置到 MCU 的存储器中。
- 启动代码 (start.s): 上电复位后,处理器执行的第一段汇编代码,主要负责初始化栈、数据段和bss段。
- 应用程序 (main.c): 开发者真正编写的业务逻辑,例如这里的 GPIO 点灯。
- Makefile文件: 指定工具链、编译选项、依赖关系和最终生成的 elf/bin 文件。
链接脚本linker.ld
作为一个 C 项目的链接脚本,即使是最小化的裸机程序, 链接脚本通常至少应包含以下几个基本段(sections),以确保程序可以正确加载和运行:
- .text: 存放代码(函数、指令)
- .rodata: 只读数据(如字符串字面量)
- .data: 已初始化的全局变量(运行时读写)
- .bss: 未初始化的全局变量(运行时自动清零)
- .stack(或 _stack_top 标记): 标记栈空间(通常通过符号定义,非真实段)
链接脚本如下:
OUTPUT_ARCH("riscv")
ENTRY(_start)
_stack_size = 2048;
MEMORY
{
FLASH (rxai!w) : ORIGIN = 0x8000000, LENGTH = 128k
RAM (wxa!ri) : ORIGIN = 0x20000000, LENGTH = 32k
}
SECTIONS
{
.text :
{
*(.init)
*(.text*)
*(.rodata*)
} > FLASH
.data : AT (ADDR(.text) + SIZEOF(.text))
{
_data_load = LOADADDR(.data);
_data_start = .;
*(.data*)
_data_end = .;
} > RAM
.bss :
{
_bss_start = .;
*(.bss*)
*(COMMON)
_bss_end = .;
} > RAM
. = ORIGIN(RAM) + LENGTH(RAM);
. = . - _stack_size;
_stack_top = .;
}
- OUTPUT_ARCH(“riscv”):指定目标架构为 RISC-V。
- ENTRY(_start):指定程序入口点 _start。
- MEMORY 段:描述了 MCU 的 FLASH 和 RAM 空间。
- .text 段:代码和常量放在 FLASH 中。
- .data 段:存放已初始化的全局变量,运行时需要从 FLASH 拷贝到 RAM。
- .bss 段:存放未初始化的全局变量,运行时需要清零。
- 栈空间:在 RAM 顶部分配 2KB 栈,并导出符号 _stack_top。
启动代码start.S
.section .init
vector_start:
j _start
.globl _start
_start:
la sp, _stack_top
/* load .data section */
la a0, _data_load
la a1, _data_start
la a2, _data_end
1:
beq a1, a2, 2f
lw t0, 0(a0)
sw t0, 0(a1)
addi a0, a0, 4
addi a1, a1, 4
j 1b
2:
/* clear .bss section */
la a0, _bss_start
la a1, _bss_end
1:
beq a0, a1, 2f
sw zero, 0(a0)
addi a0, a0, 4
j 1b
2:
call main
1:
j 1b
- 上电复位时,从vector_start处取第一条指令开始执行。
- vector_start 直接跳转到_start,这是整个程序的逻辑入口。
- _start 完成以下任务:
- 设置栈顶寄存器 sp。
- 把 .data 段从 FLASH 拷贝到 RAM。
- 将 .bss 段清零。
- 跳转到 main() 进入用户程序。
应用代码main.c
#include <stdint.h>
#define RCU_APB2EN 0x40021018
#define GPIOA_CTL0 0x40010800
#define GPIOA_OCTL 0x4001080C
int main()
{
// enable GPIOA clock
*(uint32_t *)RCU_APB2EN |= (uint32_t)1 << 2;
// set PA1 as output push pull
*(uint32_t *)GPIOA_CTL0 = *(uint32_t *)GPIOA_CTL0 & ((uint32_t)0xffffff0f) |
(uint32_t)1 << 4;
// PA1 output low
*(uint32_t *)GPIOA_OCTL &= (uint32_t)0xfffffffd;
while(1);
return 0;
}
- RCU_APB2EN 控制外设时钟,打开 GPIOA 时钟。
- GPIOA_CTL0 配置 GPIOA 引脚模式,将 PA1 配置为推挽输出。
- GPIOA_OCTL 控制 GPIOA 输出电平,这里拉低 PA1,点亮LED。
Makefile文件
PREFIX = riscv64-unknown-elf-
CC = $(PREFIX)gcc
OBJCOPY = $(PREFIX)objcopy
OBJDUMP = $(PREFIX)objdump
SIZE = $(PREFIX)size
TARGET = test
OPT = -Og
DEBUG = 1
BUILD_DIR = build
C_SOURCES = main.c
ASM_SOURCES = start.S
C_INCLUDES = -I.
LDSCRIPT = link.ld
ARCH = -march=rv32imac -mabi=ilp32 -mcmodel=medlow
CFLAGS = $(ARCH) $(OPT) -ffunction-sections -fdata-sections -std=gnu11 $(C_INCLUDES)
LDFLAGS= -T$(LDSCRIPT) -nostartfiles -Wl,-Bstatic -Wl,--gc-sections -Wl,-Map=$(BUILD_DIR)/$(TARGET).map
ifeq ($(DEBUG), 1)
CFLAGS += -g -gdwarf-2
endif
OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o)))
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(ASM_SOURCES:.S=.o)))
all: $(BUILD_DIR)/$(TARGET).elf \
$(BUILD_DIR)/$(TARGET).bin \
$(BUILD_DIR)/$(TARGET).hex \
$(BUILD_DIR):
mkdir -p $@
$(BUILD_DIR)/%.o: %.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/%.o: %.S | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/$(TARGET).elf: $(OBJECTS)
$(CC) $(OBJECTS) $(CFLAGS) -o $@ $(LDFLAGS)
$(SIZE) $@
$(OBJDUMP) -xS $@ > $(BUILD_DIR)/$(TARGET).s
$(BUILD_DIR)/%.bin: $(BUILD_DIR)/%.elf
$(OBJCOPY) -O binary $< $@
$(BUILD_DIR)/%.hex: $(BUILD_DIR)/%.elf
$(OBJCOPY) -O ihex $< $@
clean:
rm -rf $(BUILD_DIR)
flash: all
-openocd -f ./openocd_gd32vf103.cfg -c "program {$(BUILD_DIR)/$(TARGET).elf} verify reset exit"
debug:
-openocd -f ./openocd_gd32vf103.cfg
.PHONY: all clean flash debug