从零开始创建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

参考