第一章:RISC-V 裸机开发环境搭建
各位同学,咱们今天正式开始搞 D1s 的裸机开发。说实话,裸机开发这事儿,说难不难,说简单也不简单。关键是把地基打牢——环境搭好了,后面写中断、搞定时器,那都是水到渠成的事。
我个人习惯,拿到一块新芯片,第一件事不是看数据手册,而是先把工具链跑通。你想想看,代码写得再好,编译不过、烧录不了,那都是白搭。所以这一章,咱们就把交叉编译工具链、Makefile、链接脚本、启动流程这些基础活儿,一个一个捋清楚。
1.1 交叉编译工具链安装
D1s 用的是 RISC-V 64 位内核,咱们的电脑通常是 x86 架构。所以得用交叉编译——在 x86 上编译出能在 RISC-V 上跑的程序。
我推荐用 xpack-riscv-none-embed-gcc 这个工具链。为啥?因为它是专门为裸机开发优化的,不带 Linux 那套复杂的 glibc,干净利落。
安装步骤(以 Ubuntu 20.04 为例):
- 下载工具链:
wget https://github.com/xpack-dev-tools/riscv-none-embed-gcc-xpack/releases/download/v12.3.0-1/xpack-riscv-none-embed-gcc-12.3.0-1-linux-x64.tar.gz - 解压到
/opt目录:sudo tar -xzf xpack-riscv-none-embed-gcc-12.3.0-1-linux-x64.tar.gz -C /opt - 添加环境变量:
export PATH=$PATH:/opt/xpack-riscv-none-embed-gcc-12.3.0-1/bin - 验证安装:
riscv-none-embed-gcc --version
小提示: 我建议把环境变量写到 ~/.bashrc 里,这样每次打开终端就不用重新 export 了。我曾经因为忘了这步,折腾了半小时才发现是环境变量没生效。
验证一下,如果看到类似 riscv-none-embed-gcc (xPack) 12.3.0 的输出,那就说明装好了。
1.2 Makefile 基础
裸机开发不像 Linux 下有 autotools、CMake 这些高级工具。咱们通常手写 Makefile,简单直接。
一个典型的裸机 Makefile 长这样:
# 工具链定义
CROSS_COMPILE = riscv-none-embed-
CC = $(CROSS_COMPILE)gcc
LD = $(CROSS_COMPILE)ld
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
# 编译选项
CFLAGS = -march=rv64imafc -mabi=lp64f -nostdlib -ffreestanding -O2
LDFLAGS = -T link.lds -nostartfiles
# 源文件
SRCS = start.S main.c uart.c
OBJS = $(SRCS:.S=.o)
OBJS := $(OBJS:.c=.o)
# 目标
TARGET = d1s_demo
all: $(TARGET).bin
$(TARGET).elf: $(OBJS)
$(LD) $(LDFLAGS) -o $@ $^
%.o: %.S
$(CC) $(CFLAGS) -c -o $@ $<
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
$(TARGET).bin: $(TARGET).elf
$(OBJCOPY) -O binary $< $@
clean:
rm -f *.o *.elf *.bin
.PHONY: all clean
这里有几个关键点:
- -nostdlib:不链接标准库,裸机下用不到那些东西
- -ffreestanding:告诉编译器这是独立环境,没有操作系统
- -march=rv64imafc:指定架构,D1s 支持 I(整数)、M(乘除)、A(原子)、F(单精度浮点)、C(压缩指令)
注意: 别漏了 -mabi=lp64f。这个选项指定了 ABI 是 LP64,浮点用 F 扩展。如果配错了,链接时会出现奇怪的符号错误。我刚开始学 RISC-V 时就踩过这个坑,折腾了一天才发现是 ABI 不匹配。
1.3 链接脚本 (.lds) 解析
链接脚本,说白了就是告诉链接器:你的代码该放哪儿,数据该放哪儿。D1s 的 SRAM 地址范围是 0x20000000 - 0x2003FFFF(256KB),BROM 在 0x00000000 开始的地方。
咱们的裸机程序,通常从 SRAM 的起始地址开始运行。一个典型的链接脚本如下:
OUTPUT_ARCH(riscv)
ENTRY(_start)
MEMORY
{
SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 256K
}
SECTIONS
{
. = 0x20000000;
.text : {
*(.text._start)
*(.text*)
} > SRAM
.rodata : {
*(.rodata*)
} > SRAM
.data : {
*(.data*)
} > SRAM
.bss : {
*(.bss*)
*(COMMON)
} > SRAM
. = ALIGN(16);
_end = .;
}
解释一下:
- ENTRY(_start):指定入口函数是
_start,这是汇编里定义的第一个函数 - MEMORY:定义内存区域,这里只有 SRAM
- .text:代码段,
_start要放在最前面 - .bss:未初始化的全局变量,运行时需要清零
经验之谈: 我习惯在链接脚本最后定义 _end 符号,这样在 C 代码里可以知道堆的起始地址。后面做内存管理时特别有用。
1.4 D1s 启动流程与 BROM 加载
D1s 上电后,CPU 会从 0x00000000 开始执行。这个地址对应的是芯片内部的 Boot ROM(BROM)。BROM 是出厂固化的,咱们改不了。
BROM 的启动流程大致是这样的:
- 上电复位,CPU 从
0x00000000取指 - BROM 初始化基本硬件(时钟、SPI、SDIO 等)
- BROM 尝试从外部存储介质加载程序:
- 优先从 SD 卡(MMC0)启动
- 如果 SD 卡不行,尝试 SPI NAND/NOR Flash
- 最后尝试 USB 烧录模式(FEL 模式)
- 加载成功后,跳转到 SRAM 的
0x20000000执行
这里有个关键点:BROM 加载的镜像大小不能超过 SRAM 的大小(256KB)。而且镜像的前 64 字节有特殊格式,包含签名、长度、校验等信息。
重要: D1s 的 BROM 要求镜像文件的前 64 字节是 eGON 头部。这个头部定义了镜像的加载地址、大小、校验和等。咱们的裸机程序,需要先用 mksunxi 工具加上这个头部,才能被 BROM 识别。
一个典型的 eGON 头部结构如下:
typedef struct {
uint32_t magic; // 魔数,固定为 0x4F4E4745 ("EGON")
uint32_t check_sum; // 校验和
uint32_t length; // 镜像总长度(包含头部)
uint32_t load_addr; // 加载地址,通常是 0x20000000
uint32_t entry_point; // 入口地址,通常也是 0x20000000
uint32_t reserved[11]; // 保留
} egon_header_t;
生成带 eGON 头部的镜像,可以用这个命令:
mksunxi d1s_demo.bin d1s_demo_egon.bin
如果没有 mksunxi 工具,也可以自己写个 Python 脚本算校验和。嗯,这个咱们后面章节会详细讲。
1.5 实战:第一个裸机程序
理论说完了,咱们动手写个最简单的程序——点亮 LED。D1s 的 LED 通常接在 GPIO 上,比如 PD22。
先写汇编启动文件 start.S:
.section .text._start
.global _start
_start:
/* 设置栈指针 */
li sp, 0x2003FFFF
/* 跳转到 C 语言 main 函数 */
call main
/* 如果 main 返回,死循环 */
loop:
wfi
j loop
再写 C 文件 main.c:
#define GPIO_BASE 0x02000000
#define GPIO_CFG0 (*(volatile uint32_t *)(GPIO_BASE + 0x30))
#define GPIO_DAT (*(volatile uint32_t *)(GPIO_BASE + 0x34))
void delay(volatile int count) {
while (count--);
}
int main(void) {
// 配置 PD22 为输出
GPIO_CFG0 = (GPIO_CFG0 & ~(0xF << 24)) | (0x1 << 24);
while (1) {
GPIO_DAT |= (1 << 22); // 点亮 LED
delay(1000000);
GPIO_DAT &= ~(1 << 22); // 熄灭 LED
delay(1000000);
}
return 0;
}
编译、链接、生成镜像:
make
mksunxi d1s_demo.bin d1s_demo_egon.bin
把生成的 d1s_demo_egon.bin 烧录到 SD 卡,插到 D1s 开发板上电,LED 应该就开始闪烁了。
避坑指南: 我曾经犯过一个低级错误——忘了在链接脚本里把 _start 放在最前面。结果 BROM 跳转到 0x20000000 后,执行的不是 _start,而是其他乱七八糟的代码。板子直接死机。所以一定要确认 ENTRY(_start) 和 .text._start 段的位置。
小结
这一章咱们把环境搭好了,工具链、Makefile、链接脚本、启动流程都过了一遍。说实话,这些基础工作看着琐碎,但真不能马虎。我见过太多人一上来就写中断、搞外设,结果环境没配好,折腾半天连个 LED 都点不亮。
下一章,咱们开始深入 RISC-V 的中断机制。到时候你会看到,有了今天的基础,写中断处理程序其实没那么复杂。
对了,如果你在搭建环境时遇到问题,欢迎来交流。我的联系方式在页脚。