D写裸机2

原文
上一篇
这是之前用D编写RISC-V裸机文章的后续.这一次,在实际硬件上运行代码.使用VisionFive2板,这是最近发布的RISC-VSBC;带4个(SiFiveU74)应用内核,时钟频率为1.5GHz,1(SiFiveS7)监听器内核.U74S7内核间主要区别是S7内核没有MMU,因此无法支持虚内存.

来弄清楚代码如何在VisionFive2裸机上运行,然后实现小型UARTGPIO驱动,并通过串行连接打印消息并闪烁LED.

博客代码仓库.更复杂的示例,请见Multiplix这里,我正在开发的操作系统,可在VisionFive2(和RaspberryPis)上运行.

安装

如果要继续操作,则需要以下硬件:
1,VisionFive2板(带USB-C电缆来供电).
2,USBUART转换器.
3,发光二极管.
还需要以下软件:
1,LDC1.30GNU工具链这里.
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-bootmkimage工具:

$ 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应该闪烁).

新入口

现在开始编写代码.从上一篇文章中继续.
VisionFive2QEMU之间的一个区别是VisionFive2上的入口是0x40000000而不是0x80000000,因此必须在link.ld文件(完整link.ld)中更改该地址.

禁止其他内核

VisionFive2总共有五个内核,它们都同时跳转到CPU入口.简单示例,禁止除一个内核外的所有内核,这样就不必处理编写并行安全代码.在mhartidCSR存储每个内核的ID.S7监听器内核的ID0,四个U7应用内核的ID1-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设备树,因此可查看它来取得有关板上设备,及其在内存映射中的位置信息.对编写简单的GPIOUART驱动非常有用.可在这里找到jh7110.dtsi设备树.

访问系统计时器

VisionFive2SiFiveU74复核.在0x200_0000地址处有个SiFive内核的(CLINT)本地中断控件.在CLINT中有个内存映射固定频率递增的64mtime寄存器.
RISC-V规范说,实现必须提供确定定时器频率的方法,但在VisionFive2上,我没有找到(似乎未记录?)
通过实验,我估计计时器的运行频率为4.8MHz.在CLINT0xBFF8偏移映射,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偏移处有164字节使能寄存器,在偏移0x40处有164字节输出值寄存器.每个寄存器包含分布在寄存器的4个字节上的4个引脚使能/输出值.允许总共控制64个引脚.使能为低电平有效.要打开GPIOn引脚,你必须:

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-imagervf2将其复制到VisionFive2上.

现在连接LEDGPIO61(另一端连接到GND),你应该会看到它闪烁!(如果不管用,请试翻转LED的末端,LED一般有极性)

小型UART驱动

包括UARTVisionFive2上的大多数设备,都没有很好的记录.幸好,它的Linux设备树非常详细,并且有个UART项.

uart0: serial@10000000 {
    compatible = "snps,dw-apb-uart";
    reg = <0x0 0x10000000 0x0 0x10000>;
    ...
};

VisionFive2UART显然与SynopsysDW8250UART设备兼容,后者确实有在线手册.与上篇文章中默认的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会有更好的文档,但似乎不太可能.尽管如此,UARTGPIO设备可制作一些有趣程序.

posted @   zjh6  阅读(39)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示