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
设备可制作一些
有趣程序.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现