第一章:RISC-V 裸机开发环境搭建

各位同学,咱们今天正式开始搞 D1s 的裸机开发。说实话,裸机开发这事儿,说难不难,说简单也不简单。关键是把地基打牢——环境搭好了,后面写中断、搞定时器,那都是水到渠成的事。

我个人习惯,拿到一块新芯片,第一件事不是看数据手册,而是先把工具链跑通。你想想看,代码写得再好,编译不过、烧录不了,那都是白搭。所以这一章,咱们就把交叉编译工具链、Makefile、链接脚本、启动流程这些基础活儿,一个一个捋清楚。

1.1 交叉编译工具链安装

D1s 用的是 RISC-V 64 位内核,咱们的电脑通常是 x86 架构。所以得用交叉编译——在 x86 上编译出能在 RISC-V 上跑的程序。

我推荐用 xpack-riscv-none-embed-gcc 这个工具链。为啥?因为它是专门为裸机开发优化的,不带 Linux 那套复杂的 glibc,干净利落。

安装步骤(以 Ubuntu 20.04 为例):

  1. 下载工具链: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
  2. 解压到 /opt 目录:sudo tar -xzf xpack-riscv-none-embed-gcc-12.3.0-1-linux-x64.tar.gz -C /opt
  3. 添加环境变量:export PATH=$PATH:/opt/xpack-riscv-none-embed-gcc-12.3.0-1/bin
  4. 验证安装: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 的启动流程大致是这样的:

  1. 上电复位,CPU 从 0x00000000 取指
  2. BROM 初始化基本硬件(时钟、SPI、SDIO 等)
  3. BROM 尝试从外部存储介质加载程序:
    • 优先从 SD 卡(MMC0)启动
    • 如果 SD 卡不行,尝试 SPI NAND/NOR Flash
    • 最后尝试 USB 烧录模式(FEL 模式)
  4. 加载成功后,跳转到 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 的中断机制。到时候你会看到,有了今天的基础,写中断处理程序其实没那么复杂。

对了,如果你在搭建环境时遇到问题,欢迎来交流。我的联系方式在页脚。