D写裸机2
原文
上一篇
这是之前用D编写RISC-V裸机文章的后续.这一次,在实际硬件上运行代码.使用VisionFive2板,这是最近发布的RISC-V的SBC;带4个(SiFiveU74)应用内核,时钟频率为1.5GHz,1个(SiFiveS7)监听器内核.U74和S7内核间主要区别是S7内核没有MMU,因此无法支持虚内存.
来弄清楚代码如何在VisionFive2裸机上运行,然后实现小型UART和GPIO驱动,并通过串行连接打印消息并闪烁LED.
博客代码仓库.更复杂的示例,请见Multiplix这里,我正在开发的操作系统,可在VisionFive2(和RaspberryPis)上运行.
安装
如果要继续操作,则需要以下硬件:
1,VisionFive2板(带USB-C电缆来供电).
2,USB到UART转换器.
3,发光二极管.
还需要以下软件:
1,LDC1.30和GNU工具链这里.
2,通过XMODEM加载文件的sx程序.
3,U-boot中的创建固件镜像文件的mkimage工具.
我写了些工具这里
1,rduart:从主机上读串行设备的程序.
2,vf2-imager:转换.bin文件为VisionFive2固件镜像的程序.
3,vf2:发送VisionFive2固件镜像到开发板,并自动浏览菜单来正确上传的程序.
要Go来安装我编写的自定义工具.要构建它们,请在每个工具目录中运行go build(或(go install)来安装到你的GOBIN).
安装自定义固件
编写代码前,最好检查是否可在VisionFive2上正确安装新的固件镜像.注意:会覆盖以前固件.如果要返回来运行Linux,见本文末尾,来重装默认固件.
你应该可构建的最终.bin文件:prog.bin,这里.
要安装此二进制文件,要从中创建固件镜像文件.可用vf2-imager工具完成,只需用正确输入调用U-boot的mkimage工具:
$ vf2-imager -i prog.bin -o prog.img
现在要上传该镜像文件.VisionFive2快速入门指南中,有一节描述了在覆盖或破坏主板闪存时,如何恢复引导加载程序(固件)(附录4.4).
它描述了如何重新上传默认固件,但可按这些说明上传自定义固件.首先,转换USB-UART器连接到电路板.可参考数据表中的引脚排列图.
接地到接地,TX连接到RX,RX连接到TX.保持电源断开,因为主板将通过USB-C电缆供电.
安装USB-UART转换器,并插入USB-C电缆打开主板电源后,可安装新固件.这是抽象步骤.不必手动,因为我编写了个工具来自动执行.
1,翻转开发板上的引导模式开关.这些开关很小,所以我一般使用LED支腿来帮助翻转它们.
2,重启主板(拔下并重新插入USB-C电缆,或按主板侧面的重启按钮).
3,使用sx发送jh7110-recovery.bin文件.这是StarFive分发的加载固件的程序,因此发送它,以便可在板上运行并加载实际固件.
4,传输完成后,会出现查询要更新哪种固件的菜单.想要2选项:在闪存中更新fw_verify/uboot.
5,使用sx发送vf2-hello.img文件.
6,翻转开关,并重启开发板.
很多步骤!幸好,编写了叫vf2的程序,(除了拨动开关),可自动完成所有操作.所以只需要跑:
$vf2prog.img
并等待它完成.完成后,拨动开关,开始运行rduart,来从UART读取,然后重启开发板.希望会看到屏幕上出现"闪烁:开"或"闪烁:关"(如果在61引脚插入LED,LED应该闪烁).
新入口
现在开始编写代码.从上一篇文章中继续.
VisionFive2和QEMU之间的一个区别是VisionFive2上的入口是0x40000000而不是0x80000000,因此必须在link.ld文件(完整link.ld)中更改该地址.
禁止其他内核
VisionFive2总共有五个内核,它们都同时跳转到CPU入口.简单示例,禁止除一个内核外的所有内核,这样就不必处理编写并行安全代码.在mhartid的CSR中存储每个内核的ID.S7监听器内核的ID为0,四个U7应用内核的ID为1-4.更改start.s来仅启动1核心:
.section ".text.boot"
.globl _start
_start:
csrr t0, mhartid
li t1, 1
bne t0, t1, _hlt #如果mhartid!=1,切换至_hlt
la sp, _stack_start
call dstart
_hlt:
wfi
j _hlt
指示灯闪烁
为了使LED闪烁,要延迟一段时间并切换GPIO引脚.最简单延迟方法是执行大量的nop指令.稍复杂方法是,访问系统时间寄存器,以便可可靠地等待精确时间.
虽然未记录大多数VisionFive2的设备,但该板有个Linux设备树,因此可查看它来取得有关板上设备,及其在内存映射中的位置信息.对编写简单的GPIO和UART驱动非常有用.可在这里找到jh7110.dtsi设备树.
访问系统计时器
VisionFive2用SiFiveU74复核.在0x200_0000地址处有个SiFive内核的(CLINT)本地中断控件.在CLINT中有个内存映射的固定频率递增的64位mtime寄存器.
RISC-V规范说,实现必须提供确定定时器频率的方法,但在VisionFive2上,我没有找到(似乎未记录?)
通过实验,我估计计时器的运行频率为4.8MHz.在CLINT内0xBFF8偏移映射,mtime寄存器.有关文档,请见SiFive手册.
可用内存映射来延迟一定数量微秒,来制作简单计时器模块.
module timer;
struct Timer {
static ulong mtime() {
return volatileLoad(cast(ulong*) (0x200_0000 + 0xBFF8));
}
enum mtime_freq = 4_800_000;
static void delay_time(ulong t) {
ulong rb = mtime;
while (true) {
ulong ra = mtime;
if ((ra - rb) >= t)
break;
}
}
static void delay_us(ulong us) {
delay_time(us * mtime_freq / 1_000_000);
}
}
此代码使用D的(UFCS)统一函数调用语法,不使用括号就调用mtime函数.
GPIO驱动
切换GPIO引脚要求控制GPIO设备.VisionFive2目前没有很好的文档记录,我找不到GPIO设备的文档.但是,有个Linux内核驱动(补丁),我只拉出了来打开或关闭引脚的部分.
VisionFive2上的GPIO器件,在0x0偏移处有16个4字节使能寄存器,在偏移0x40处有16个4字节输出值寄存器.每个寄存器包含分布在寄存器的4个字节上的4个引脚的使能/输出值.允许总共控制64个引脚.使能为低电平有效.要打开GPIO的n引脚,你必须:
1,启用引脚:找到en[n/4]寄存器,内部在n%4字节清理底层6位.
2,打开引脚输出:找到[n/4]寄存器输出,内部置n%4字节的底层7位为0b0000001.
module gpio;
struct Jh7110Gpio(uintptr base) {
enum doen_reg = cast(uint*)(base + 0x0);
enum dout_reg = cast(uint*)(base + 0x40);
enum dout_mask = 0x7f;
enum doen_mask = 0x3f;
static void set(uint pin) {
if (pin > 63) {
return;
}
uint offset = pin / 4;
uint shift = 8 * (pin % 4);
uint dout = volatileLoad(&dout_reg[offset]);
uint doen = volatileLoad(&doen_reg[offset]);
volatileStore(&dout_reg[offset], dout & ~(dout_mask << shift) | (1 << shift));
// 低位使能
volatileStore(&doen_reg[offset], doen & ~(doen_mask << shift));
}
static void clear(uint pin) {
if (pin > 63) {
return;
}
uint offset = pin / 4;
uint shift = 8 * (pin % 4);
uint dout = volatileLoad(&dout_reg[offset]);
volatileStore(&dout_reg[offset], dout & ~(dout_mask << shift));
}
static void write(uint pin, bool value) {
if (value) {
set(pin);
} else {
clear(pin);
}
}
}
alias Gpio = Jh7110Gpio!(0x13040000);
查看JH7110设备树,可看到GPIO设备在0x13040000地址:
gpio: gpio@13040000 {
compatible = "starfive,jh7110-sys-pinctrl";
reg = <0x0 0x13040000 0x0 0x10000>;
...
};
因此,添加:
alias Gpio = Jh7110Gpio!(0x13040000);
眨眼!
现在在kmain中,有了切换引脚的一切:
module main;
import gpio;
import timer;
void kmain() {
bool val = true;
enum pin = 61;
while (true) {
Gpio.write(pin, val);
val = !val;
// 延迟500 ms
Timer.delay_us(500 * 1000);
}
}
构建进.bin文件中,并用vf2-imager和vf2将其复制到VisionFive2上.
现在连接LED到GPIO61(另一端连接到GND),你应该会看到它闪烁!(如果不管用,请试翻转LED的末端,LED一般有极性)
小型UART驱动
包括UART的VisionFive2上的大多数设备,都没有很好的记录.幸好,它的Linux设备树非常详细,并且有个UART项.
uart0: serial@10000000 {
compatible = "snps,dw-apb-uart";
reg = <0x0 0x10000000 0x0 0x10000>;
...
};
VisionFive2的UART显然与Synopsys的DW8250UART设备兼容,后者确实有在线手册.与上篇文章中默认的QEMUUART一样,此UART设备上的传输寄存器也是列表中的第一个寄存器.表明,固件以115200的波特率来初化设备,因此不必弄清如何配置时钟.
QEMUUART和真正的UART间的一个区别是,数据从传输寄存器中传输出去要耗时,因此写入传输寄存器前,必须等待它为空."行状态寄存器"可报告此信息.
module uart;
struct Dw8250(uintptr base) {
// 行状态寄存器
static uint lsr() {
enum off = base + 0x14;
return volatileLoad(cast(uint*) off);
}
// 传输持有寄存器
static void thr(uint b) {
enum off = base + 0x0;
volatileStore(cast(uint*) off, b);
}
enum Lsr {
thre = 5,
//如果`THR`为空,则置`LSR`的第5位
}
static void tx(ubyte b) {
// 等待`THR`为空
while (((lsr >> Lsr.thre) & 1) != 1) {
}
thr = b;
}
}
alias Uart = Dw8250!(0x10000000);
这段代码使用了UFCS,其中lsr是调用lsr(),thr=b是调用thr(b).
在UART上打印
现在可加到眨眼程序中:
module main;
import gpio;
import timer;
void kmain() {
bool val = true;
enum pin = 61;
while (true) {
// 可重用上一篇文章中的`println`实现
println("blink: ", val ? "on" : "off");
Gpio.write(pin, val);
val = !val;
// 延迟`500`毫秒
Timer.delay_us(500 * 1000);
}
}
如果运行rduart,则应看到通过串行连接打印的文本.
重装默认固件
很容易重装默认固件.只需要取默认的.img文件并用vf2工具刷新它.
默认固件镜像为可从starfive-tech/VisionFive2下载的visionfive2_fw_payload.img.
刷新固件大约需要10分钟.
结语
在实际硬件上裸机运行代码总是很有趣.下一步可能是实现UART引导加载程序,这样就不必使用vf2工具(并翻转开关)来上传新程序.
可上传一次引导加载程序,然后让它轮询新程序以通过UART加载.另一个下一步可能是启用更多硬件功能,如MMU(对虚内存)或中断.参见Multiplix来了解,我正在开发的在VisionFive2上运行的一个小示例内核.
运行裸机时,还可真正有效的微基准测试,如准确确定执行系统调用指令(RISC-V中的ecall)需要的周期.
可惜,VisionFive2上的设备没有很好的记录,因此在板上为更先进设备,构建控制器似乎不行.我希望HiFivePro会有更好的文档,但似乎不太可能.尽管如此,UART和GPIO设备可制作一些有趣程序.
浙公网安备 33010602011771号