2

RISC-V from Scratch 3

 2 years ago
source link: https://dingfen.github.io/risc-v/2020/07/27/riscv-from-scratch-3.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

RISC-V from scratch 3: 写 UART 驱动Permalink

今天为大家继续翻译 RISC-V from scratch 系列博客,接着上一部分内容,我们本此的目标是实现 UART 协议的驱动程序,继续完善 RISC-V 的内核。本文译自 RISC-V from scratch 3: Writing a UART driver in nasm (1 / 3)

由于我发现该系列的原作者貌似没有把这一系列完成就咕咕了,因此从本文开始,我将加上一些自己实践的内容,以及一些自己的想法,同大家探讨,算是狗尾续貂,弥补遗憾

简介Permalink

欢迎再次来到 RISC-V from scratch ,先快速回顾一下我们之前做过的内容,我们之前已经探索了很多与 RISC-V 及其生态相关的底层概念(例如编译、链接、原语运行时、汇编等)。具体来说,在上一篇文章中,我们使用 dtc 工具检查了 virt QEMU 虚拟机中的硬件布局,确定了 RAM 在该计算机中的存放地址,如果你观察仔细的话,会发现 virt 还有很多有趣的地方,其中一个是 UART

为了进一步学习 RISC-V 汇编的知识,我们将在接下来的三篇文章中为该 UART 编写驱动程序,深入探索 ABI,函数以及其中的底层堆栈操作等重要概念。

译注:由于原作者说的三篇文章中的最后一篇还未完成,而译者认为使用 RISC-V 汇编写 UART 驱动程序是吃力不讨好的行为,因此,译者使用 C 语言完成了驱动的编写,以后的内容也会介绍。

搭建环境Permalink

如果你还未看本系列博客的第一部分,没有安装 riscv-qemu 和 RISC-V 工具链,那么赶紧点击上面标题的链接,跳转到 “QEMU and RISC-V toolchain setup”

之后,再将博主创建的 github 库下载下来,作为我们的工作点。

git clone [email protected]:twilco/riscv-from-scratch.git
# or `git clone https://github.com/twilco/riscv-from-scratch.git` to clone
# via HTTPS rather than SSH
# alternatively, if you are a GitHub user, you can fork this repo.
# https://help.github.com/en/articles/fork-a-repo
cd riscv-from-scratch/work

译注:亲测无需下载 github 库也可实现下面的实验。

什么是 UARTPermalink

UART 是 “Universal Asynchronous Receiver-Transmitter” 的缩写,它是用于传输、接收系列数据的硬件设备。串行数据传输是逐位顺序发送数据的过程。 相反,并行数据传输是一次发送多个位的过程。 关于串行并行通信,此图很好地说明了差异:

Parallel_and_Serial_Transmission.gif

UART 从不指定数据接收或发送的速率(也称为时钟速率或时钟信号),这是它们异步而不是同步的原因。正因为异步的要求,UART 使用开始和停止位来将数据截断为帧,开始位和停止位会告诉 UART 何时开始和停止读取数据。

你可能听说过 USARTs (Universal Synchronous/Asynchronous Receiver-Transmitter) ,该设备既可以同步也可以异步工作,当同步工作时,USART 会放弃使用开始位和停止位,而是在单独的线路上发送时钟信号,实现发送与接受的同步。

事实上,UART和USART随处可见。 它们内置于几乎所有现代微控制器(包括我们的虚拟机)中。 这些设备工作在交通信号灯、冰箱以及绕地球轨道运行了多年的卫星上。

硬件布局回顾Permalink

在我们正式开始写驱动前,我们需要一些额外的信息来解决一些问题。我们如何配置虚拟机的 UART ? 我们可以在哪个内存地址找到接收和发送缓冲区?

接下来,我们使用 dtc 工具,回顾一下 uart 的 devicetree 节点的一些信息。

# Install 'dtc' if you don't already have it.
# I use 'brew' for MacOS - you may need to do something else.
brew install dtc
# Use qemu to dump info about the 'virt' machine in dtb (device tree blob) 
# format.
# The data in this file represents hardware components of a given 
# machine / device / board.
qemu-system-riscv64 -machine virt -machine dumpdtb=riscv64-virt.dtb
# Convert our .dtb into a human-readable .dts (device tree source) file.
dtc -I dtb -O dts -o riscv64-virt.dts riscv64-virt.dtb
# Search for 'uart' and display 2 lines before and 6 lines after each match.
grep uart riscv64-virt.dts -B 2 -A 6
        chosen {
                bootargs = [00];
                stdout-path = "/uart@10000000";
        };
--
        };

        uart@10000000 {
                interrupts = <0x0a>;
                interrupt-parent = <0x02>;
                clock-frequency = <0x384000>;
                reg = <0x00 0x10000000 0x00 0x100>;
                compatible = "ns16550a";
        };

grep 输出的最上面,chosen 节点出现了,该节点内容表明,输出信息会通过 UART 设备打印出来。根据此篇文档chosen 节点不代表任何物理硬件设备,通常用于在固件和运行在裸机上的程序(比如操作系统)之间的数据交换,我们接下来的操作不需要用到该节点,不必理会。

接下来才是我们想要的东西—— uart 节点。根据前面的知识,我们很容易就发现 UART 的内存地址位于 0x10000000 ,还有 interruptsinterrupt-parent 属性,表示 UART 是会产生中断的。

可能有读者不太熟悉计算机系统,因此我这里简单介绍一下中断 interrupt,中断是硬件或软件向处理器发出的信号,指示事件需要立即处理执行。例如,在以下情况下,UART 可能会产生中断:

  • 新的数据进入了接收缓存
  • 数据传送机 (transmitter) 完成了缓存中数据的发送
  • UART 遇到了发送错误的情况

这些中断行为充当 hook ,程序员可编写代码适当地响应这些事件,不过接下来的内容我们不会用到中断,因此先忽略到这些内容吧。

再来看一下 clock-frequency = <0x38400> ,参考 devicetree specificationclock-frequency 代表了时钟的初始频率,其值为十六进制的 0x38400 Hz ,即3.6864 MHz,每秒36.864百万个时钟滴答,这是标准的晶体振荡器频率。

下一个属性就很熟悉了 reg = <0x00 0x10000000 0x00 0x100> ,决定了 UART 的内存位置,以及它的长度,在上一篇文章中,我们知道有两个 32-bit 的值在描述信息。通过给的信息来看,不难得出 UART 的内存位置起始于 0x00 + 0x10000000 = 0x10000000 ,且长度为 0x00 + 0x100 = 0x100 字节。

uart 节点的最后一个属性,compatible =“ ns16550a” ;,它告知我们 UART 与哪种编程模型兼容。 操作系统使用此属性来确定其可用于外围设备的设备驱动程序。网上有很多的实现与 NS16550A 兼容的 UART 所需的资料,这篇是本文所引用的。

驱动程序的基本框架Permalink

现在,我们创建新文件,取名 ns16550a.s ,在这里我们开始构建驱动程序的基本框架,首先,我们仅仅先实现一个读写字符的函数,不管那些复杂的中断。

.global uart_put_char
.global uart_get_char

uart_get_char:
    .cfi_startproc
    .cfi_endproc

uart_put_char:
    .cfi_startproc
    .cfi_endproc

.end

我们从 .global 汇编指令开始,将 uart_put_charuart_get_char 声明为其他文件可访问的符号。以 . 开头的指令都是伪指令,它们只向汇编器提供信息,不是可执行代码。所有基本 GNU 汇编器指令的详细说明都可以在这里找到。

接下来,将会有每个符号的定义,当前仅包含 .cfi 汇编程序指令。这些 .cfi 指令将框架的结构及其展开方法通知工具(例如汇编器或异常展开器)。.cfi_startproc.cfi_endproc 分别表示函数的开始和结束。

尽管我们还没有完全开始写驱动(你肯定能察觉到我们只是搭建了个框架),我们先把他编译一下,看看这个框架是否可用。

riscv64-unknown-elf-gcc -g -ffreestanding -O0 -Wl,--gc-sections \
    -nostartfiles -nostdlib -nodefaultlibs -Wl,-T,riscv64-virt.ld \
    crt0.s ns16550a.s

如果你很想知道这些编译选项是什么意思,建议参考这里

然后,我们得到了一个错误:

/Users/twilco/usys/riscv/riscv64-unknown-elf-gcc-8.2.0-2019.02.0-x86_64-apple-darwin/bin/../lib/gcc/riscv64-unknown-elf/8.2.0/../../../../riscv64-unknown-elf/bin/ld: /var/folders/rg/hbr8vy7d13z9k7pdn0l_n9z51y1g13/T//ccjYQiJc.o: in function `.L0 ':
/Users/twilco/projects/riscv-from-scratch/work/crt0.s:12: undefined reference to `main'
collect2: error: ld returned 1 exit status

不过,放轻松,只是缺少 main 函数而已。这是因为在 crt0.s 文件中,我们曾经用到过 main 函数的地址:

.section .init, "ax"
.global _start
_start:
    .cfi_startproc
    .cfi_undefined ra
    .option push
    .option norelax
    la gp, __global_pointer$
    .option pop
    la sp, __stack_top
    add s0, sp, zero
    jal zero, main # <~~~~~~~~~~
    .cfi_endproc
    .end

那么,为了简单起见,先创建个文件 main.c ,然后把 main 函数的定义写出来:

int main() {
    uart_put_char();
}

最后,将这几个文件一起编译,就不会报错了:

riscv64-unknown-elf-gcc -g -ffreestanding -O0 -Wl,--gc-sections \
    -nostartfiles -nostdlib -nodefaultlibs -Wl,-T,riscv64-virt.ld \
    crt0.s ns16550a.s main.c

除此之外,我们可以使用 nm 工具,查看一下 a.out 文件里面符号定义的情况:

riscv64-unknown-elf-nm a.out

00000000800010a0 R __BSS_END__
000000008000109c R __DATA_BEGIN__
000000008000109c R __SDATA_BEGIN__
000000008000109c R __bss_start
000000008000189c A __global_pointer$
0000000088000000 T __stack_top
000000008000109c R _edata
00000000800010a0 R _end
0000000080000000 T _start
0000000080000018 T main
0000000080000018 T uart_get_char
0000000080000018 T uart_put_char

设置基础地址Permalink

从这篇资料得知,NS16550A UART 有十二个寄存器,访问每个寄存器只需要在基址的基础上加上若干字节的偏移量即可。为了能方便地访问这些寄存器,我们首先需要定义一个代表该基址的符号。 正如我们从 riscv64-virt.dts 中发现的那样,基址位于 0x00 + 0x10000000 = 0x10000000,这就是 reg 属性中的内容:

uart@10000000 {
    interrupts = <0x0a>;
    interrupt-parent = <0x02>;
    clock-frequency = <0x384000>;
    reg = <0x00 0x10000000 0x00 0x100>;
    compatible = "ns16550a";
};

riscv64-virt.ld 文件中,加入这个符号:

...more above...
SECTIONS
{
  /* Read-only sections, merged into text segment: */
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x10000));
  . = SEGMENT_START("text-segment", 0x10000) + SIZEOF_HEADERS;
  PROVIDE(__stack_top = ORIGIN(RAM) + LENGTH(RAM));
  /* >>>>>> Our newest addition. <<<<<< */
  PROVIDE(__uart_base_addr = 0x10000000);
  /* >>>>>> End of our addition. <<<<<< */
  .interp         : { *(.interp) }
...more below...

__uart_base_addr 定义完成后,我们就可以很轻松地访问 NS16550A 的寄存器了!

接下来Permalink

今天,我们了解了 UART 和 USART 、NS16550A 规范,中断以及一些其他 devicetree 属性。 我们还为UART 组装驱动程序创建了基础框架,并已将 __uart_base_addr 编码为链接器文件中的符号,以方便对 UART 寄存器访问。

在下一篇文章中,我们将讨论和实现两个驱动程序函数 uart_get_charuart_put_char 。 函数是在汇编世界中使函数调用成为可能的重要部分。 我们将逐步介绍函数的序幕,并提供详细说明堆栈更改和每条指令寄存器的图表。


我的尝试Permalink

OK!原博文翻译到此结束!现在介绍一下我的实验方案:

事实上,在跟着写完 crt0.s 文件,并将他们编译、链接,运行在虚拟机上时,我的思想就与原博主最初的想法不太一样了,原博主只是想要探究一下 RISC-V 的底层技术,但我想要做的却是一个 RISC-V 内核。

原博主的实验步骤中,创建 crt0.s 以及它的前因后果解释非常详细,让我受益良多。但同时我也马上明白,这些步骤只要再稍加调整,就完全可以当作操作系统的启动工作了!那么接下来,我将会继续我自己的实验,敬请期待。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK