本节是以 Golang 上游 7ee4c1665477c6cf574cb9128deaf9d00906c69f 为基准做的实验
予焦啦!针对外部中断的机制,我们从 RISC-V CPU 内部的控制暂存器看起,到达 PLIC 作为中断控制器的用法,昨天也进行了若干实验,因此回头参考 PLIC 的规格书的话能够有更加具体的了解。今天则是要再往更外部装置的方向走去;以我们现在的目标而言,如果能够看到输出是还不错,但如果能够有个输入输出的命令列可用,那就更好了不是吗?
fmt.Println
函数的基本流程笔者没有打算详细介绍 UART 装置的原理和概念。一来是笔者其实对此不熟,二来是网路上已经有太多优秀的解说,如 1、2、这次铁人赛也有的3之类的。笔者猜测,随着 ARM 也加入今年的主题,应该也会有其它相关的解说。
除了上面引用的几篇 UART 机制教学文之外,笔者用来当作参考的是上面的 wikibook 连结。里面有详细的暂存器资讯,虽然和我们经手的 OpenSBI 使用的名称有些微出入,但仍是极佳的参照。
我们先前已经实作过 exit
等执行绪收尾的函数,所以我们知道 ethanol/main.go
的 Hello Wrold!
讯息只是被跳过了;实际上,fmt.Println
函数本身有被执行到,否则 Golang 怎麽能够结束 main
组件的 main
函数,擅自回到 runtime.main
函数之中收尾这个执行绪?所以现在的目标就是,我们现在必须追究,为什麽 fmt.Println
没有产生效果。这对笔者来说不太直觉,因为我们在第二章当中已经打通 throw
之类的错误函数的输出;当时是透过自行建立 runtime.write2
函数呼叫,连结到 OpenSBI 提供的字元输出功能。为什麽 fmt.Println
没有办法直接接上那个部份呢?
要理解背後的原因,请读者回顾一下笔者前年的铁人赛文章:
这两篇应该视作一个整体。作为一个 fmt
组件的函数,Println
只是指定了输出管道为控制台(console)标准输出(standard output)、在 Fprintln
函数之上又包装一层的函数;fmt
组件里面提供给格式化输出需求的真正核心函数,其实是 Fprintln
。
对於 Golang 的标准输出的初始设定有兴趣的读者,可以往前回溯一篇,阅读追踪
os.Stdout
之篇章。
於是我们可以理解到这两者(runtime.write
与 fmt.Fprintln
)之间的差异。前者是,runtime
组件在执行期的时候遇到输出需求时使用的函数,类似的需求有我们已经走访过的 throw
函数。也就是说,其实 runtime.write
不需要很复杂,只要能够从标准错误(standard error)当中显示出有意义的资讯即可。相对的,後者则是格式化输出的核心函数,它需考量到很多不同的输出场景(比方说控制台、I/O 装置、档案等等)的各式需求,然後特定情况还需要处理资料格式化。前者不能使用後者,因为 runtime
组件是 Golang 生命周期之始,所有後续的组件,当然包含 fmt
,都需仰赖它完成的执行期初始化。但为何後者不能使用前者呢?这是笔者感到比较不直觉的部份,不过也许是考量到跨组件的相依性吧?
完全不知道标准输出或标准错误、或是对於相关名词感到懵懂的读者,请参考维基百科。
fmt.Println
到印出讯息的部份虽然前段已经提供详尽的参考,但如果读者想要懒人包的话,这里直接提供。谜底是我们可以在 src/os/file_opensbi.go
的 write
函数里面埋个 panic
呼叫,
diff --git a/src/os/file_opensbi.go b/src/os/file_opensbi.go
index 2c9a194ec1..090adcd269 100644
--- a/src/os/file_opensbi.go
+++ b/src/os/file_opensbi.go
@@ -77,7 +77,9 @@ func (f *File) pread(b []byte, off int64) (n int, err error) {
}
func (f *File) write(b []byte) (n int, err error) {
- return 0, nil
+ print(string(b))
+ panic("??")
+ return 13, nil
}
编译并执行,可以取得回溯讯息:
...
Hello World!
panic: ??
goroutine 1 [running]:
os.(*File).write(...)
/home/noner/FOSS/hoddarla/ithome/go/src/os/file_opensbi.go:81
os.(*File).Write(0x0, {0xffffffcf04014020, 0xd, 0x10})
/home/noner/FOSS/hoddarla/ithome/go/src/os/file.go:176 +0x98
fmt.Fprintln({0xffffffc0000b0620, 0x0}, {0xffffffcf04056f70, 0x1, 0x1})
/home/noner/FOSS/hoddarla/ithome/go/src/fmt/print.go:265 +0x7c
fmt.Println(...)
/home/noner/FOSS/hoddarla/ithome/go/src/fmt/print.go:274
main.main()
/home/noner/FOSS/hoddarla/ithome/ethanol/main.go:6 +0x70
自从第一章解消了编译过程找不到符号定义的问题之後,我们就没有再重访过
os
组件里面的东西了。都在处理执行期的事情。
为什麽是埋
panic
而不是throw
?因为panic
和throw
是runtime
组件特定的。
也就是说,只要我们拔掉
panic
,Hoddarla/ethanol 就算是正式 Hello World 了!这当然是一个里程碑。最後的细节是,回传所印出的字元不能总是写死13
个。这个部分我们可以使用len(string(b))
,因为 Golang 的这些功能都接上了。相关部分已经更新在今天上传的 Hoddarla repo。
这里引用了 print
函数搭配字元阵列到字串的处理,我们就成功地看到了 Hello World!
的输出了。只是,这个透过 print
函数的输出,其实还是透过 runtime.write
,也就是利用 OpenSBI 使之输出。至於到底是怎麽输出的?答案就是昨日我们在观察 PLIC 时看到的 UART 装置。
之所以说简单模式,是因为显然 OpenSBI 有一套方法可以驾驭 UART 装置,但不是透过外部中断的方法;否则的话,我们就不需要在先前几天的实验当中,使用 Ctrl+A X
组合键去强制关闭 QEMU,且随意敲击按键也没有任何反应。所以我们先看看 OpenSBI 如何使用以及如何初始化 UART 装置吧。
回顾一下我们在 runtime.write2
的作为,
// func write2(p uintptr)
TEXT runtime·write2(SB),NOSPLIT|NOFRAME,$0-8
MOV p+0(FP), A0
LB 0(A0), A0
MOV $1, A7
MOV $0, A6
ECALL
RET
对於相关规格有兴趣的读者可以参考 SBI 规格书。
A6=0
与 A7=1
的组合,构成 legacy_console_putchar
的环境呼叫(evironment call,RISC-V 系统中的较低权限等级向较高权限等级索取服务时的呼叫)。当然,这会进到 OpenSBI 的陷阱向量,经过一些手续之後,最终会导到 lib/sbi/sbi_ecall_legacy.c
(OpenSBI 的原始码资料夹下)里面的 sbi_ecall_legacy_handler
。在主要的 switch
区块内,进入
case SBI_EXT_0_1_CONSOLE_PUTCHAR:
sbi_putc(regs->a0);
break;
sbi_putc
位在 lib/sbi/sbi_console.c
之中,
void sbi_putc(char ch)
{
if (console_dev && console_dev->console_putc) {
if (ch == '\n')
console_dev->console_putc('\r');
console_dev->console_putc(ch);
}
}
搜寻一下这个 console_putc
函数指标的话,可以看到有 5 组串列装置(serial device)。其中 3 组看起来和特定的平台有关,剩下两个分别是 uart8250_putc
,另一个则是 htif_putc
。虽然这里省略了具体的执行步骤,但笔者在 GDB 里面设了这两个断点并观察之後可以断定,这里的呼叫使用到的是前者。在 lib/utils/serial/uart8250.c
之中:
72 static void uart8250_putc(char ch)
73 {
74 while ((get_reg(UART_LSR_OFFSET) & UART_LSR_THRE) == 0)
75 ;
76
77 set_reg(UART_THR_OFFSET, ch);
78 }
其中,LSR
与 THR
都是 UART 的暂存器。这个函数希望在 LSR
暂存器的 THRE
位元是 0 的时候卡住;通过了之後,才去将传进来的字元写到 THR
暂存器去。
LSR
是线路状态暂存器(Line Status Register),它的 THRE
位元是发射器保留暂存器(Transmitter Holding Register)状态是否余有空间(Empty)的意思。如果这个位元真的是 0,就表示这里我们就算想要写入字元给 UART(正确来说是 THR
,也就是发射器保留暂存器)请它输出,它也没有办法做到。
set_reg
也有值得一提的部分:
static void set_reg(u32 num, u32 val)
{
u32 offset = num << uart8250_reg_shift;
if (uart8250_reg_width == 1)
writeb(val, uart8250_base + offset);
else if (uart8250_reg_width == 2)
writew(val, uart8250_base + offset);
else
writel(val, uart8250_base + offset);
}
一开始,真正的偏移量(offset
)计算是将传入的暂存器号码(num
)做左移 uart8250_reg_shift
的运算。之後,再针对这个 UART 装置的暂存器宽度(uart8250_reg_width
)来决定,这个 I/O 的写入动作是针对一个位元组(b
後缀)、双字元(w
後缀)、还是一个字组(l
後缀)。
这些
write*
呼叫,就是 MMIO 暂存器写入呼叫。除了最後对应到sb
、sh
之类的写入指令之外,还需要有前置的写入屏障(write barrier),由 RISC-V 指令fence w,o
确保,代表在真正进行这个输出(o
)之前,所有的记忆体写入(w
)行为都必须已经完成了。
uart8250_reg_*
这些量,又是怎麽决定的呢?这就不得不提 UART 的初始化。
同样在 lib/utils/serial/fdt_serial_uart8250.c
里面,函数 serial_uart8250_init
,
static int serial_uart8250_init(void *fdt, int nodeoff,
const struct fdt_match *match)
{
int rc;
struct platform_uart_data uart;
rc = fdt_parse_uart8250_node(fdt, nodeoff, &uart);
if (rc)
return rc;
return uart8250_init(uart.addr, uart.freq, uart.baud,
uart.reg_shift, uart.reg_io_width);
}
这里有装置树剖析(parse),透过 fdt_parse_uart8250_node
来完成,这当然会比笔者在第二章当中只负责剖析记忆体节点与断章中剖析核心参数的部份要正式许多。它透过传入 platform_uart_data
结构指标,在该函数内完成大部份成员的赋值。然後将其中五个值传入 uart8250_init
函数。最後两个是我们在上一小节已经看过的暂存器偏移量与暂存器宽度,所以这里先略过。第一个是则是 UART 本身在装置树中显示的基底位址,QEMU 所使用的 UART 的话是在 0x10000000
,可见:
uart@10000000 {
interrupts = <0x0a>;
interrupt-parent = <0x09>;
clock-frequency = "\08@";
reg = <0x00 0x10000000 0x00 0x100>;
compatible = "ns16550a";
};
无论是节点本身的示意或是 reg
属性的第一组数字,都显示了这个事实。至於第二与第三个值,分别代表频率与鲍率(baud rate);这两个值与 UART 运作时的时间特性有关,但笔者这里不打算深入研究。使用 GDB 设法断在 uart8250_init
呼叫之前,可以观察到这几个数字的值:
(gdb) p/x $a0
$1 = 0x10000000
(gdb) p/x $a1
$2 = 0x384000
(gdb) p/x $a2
$3 = 0x1c200
(gdb) p/x $a3
$4 = 0x0
(gdb) p/x $a4
$5 = 0x1
其中,uart.freq
就来自 clock-frequency
的怪字串 \08@
,表示成四个位元组(加上结尾的 \0
)的数字之後,恰好就是 0x00
(第一个字元 \0
)、0x38
(字元 8
)、0x40(字元 @
)、结尾字元。
uart.baud
、uart.reg_shift
、uart.reg_io_width
三个值都是来自 lib/utils/fdt/fdt_helper.c
里面的定义,
19 #define DEFAULT_UART_BAUD 115200
20 #define DEFAULT_UART_REG_SHIFT 0
21 #define DEFAULT_UART_REG_IO_WIDTH 1
uart8250_init
的初始化内容这段显得平铺直述、与作业系统模式的设定较无关,且又尚未设置我们最感兴趣的中断。没有兴趣的读者跳过亦无所谓。
这个函数在 lib/utils/serial/uart8250.c
当中。我们可以据此观察 OpenSBI 所进行的初始化,顺便对照先前的 UART 8250 详解,学习 MMIO 暂存器名称与功能。
/* 停用所有的中断 */
set_reg(UART_IER_OFFSET, 0x00);
IER
MMIO 暂存器全称为 `Interrupt Enable Register,它的 4 个低位元都代表不同的中断事件。OpenSBI 并没有要使用中断。
bdiv = uart8250_in_freq / (16 * uart8250_baudrate);
...
/* 启用 DLAB(Divisor Latch Access Bit,除数锁存器存取位元) */
set_reg(UART_LCR_OFFSET, 0x80);
if (bdiv) {
/* Set divisor low byte */
set_reg(UART_DLL_OFFSET, bdiv & 0xff);
/* Set divisor high byte */
set_reg(UART_DLM_OFFSET, (bdiv >> 8) & 0xff);
}
先从 bdiv
说起,这个值是频率除以鲍率再除以 16 的一个比值。要让 UART 正确运作,需要将这个比值拆分为低位元组与高位元组,分别存入 DLL
(代表 Divisor Latch Low Byte,除数锁存器的低位元组)与 DLH
(不知道为什麽 OpenSBI 这里使用 DLM
来命名?总之是代表除数锁存器的高位元组)。
这两个 MMIO 暂存器的偏移量实际上与其它 MMIO 暂存器重叠,所以为了精确的控制,需要额外的一个开关,DLAB
。这个控制位元位在 LCR
(Line Control Register,线路控制暂存器)MMIO 暂存器里面,仅有在它启用时,才能够写入上述的 DLL
与 DLH
。至於为什麽除数与比值如此设计,笔者目前没有兴趣追究。
/* 资料位元数为 8,没有奇偶值检查,1 个停止位元 */
set_reg(UART_LCR_OFFSET, 0x03);
在 LCR
线路控制暂存器设置这些其他的 UART 属性。资料位元可以设置 5 到 8 的范围,奇偶值也有不同模式可以设定。停止位元则是可以指定 1 或 2 个。
DLAB
位元也因此被清空了,所以重叠范围的 MMIO 暂存器因此可以正常使用了。
/* 启用 FIFO */
set_reg(UART_FCR_OFFSET, 0x01);
FCR
是 FIFO 控制暂存器。FIFO 是 8250 之後的 UART 较常采用的内部机制,它可以容纳多个位元组存在,并以先进先出的模式读写。当然,这个暂存器有其它位元可以控制 FIFO 行为,在此先略过了。
/* 无须控制数据机 DTR RTS */
set_reg(UART_MCR_OFFSET, 0x00);
MCR
是数据机控制暂存器,大致上的场合应该都是不需要的状态,笔者就略过了。
/* 清除线路状态 */
get_reg(UART_LSR_OFFSET);
/* 读取接收缓冲区 */
get_reg(UART_RBR_OFFSET);
/* 设置暂用空间 */
set_reg(UART_SCR_OFFSET, 0x00);
LSR
在前面的印出字元函数内观察过了,是代表线路状态暂存器;不确定为什麽这里使用 get_reg
函数?前述的 UART 使用指南里面并没有提到这个暂存器有读取附带清空效果的说明,而且每个位元展示的行为也不同。
RBR
是接收缓冲区暂存器(Recieve Buffer Register)。SCR
是暂用空间暂存器(SCratch Register),同我们前几天介绍的 RISC-V scratch 系列控制暂存器一样,可用作暂存空间。
如此的设置之後,OpenSBI 的部份就算结束了。但这不可能就是 UART 正常使用的方式的全部。目前为止,Hoddarla 都还没有办法接收 UART 的输入,但 debian 虚拟机却可以做到。至少输入,应该要给我们一些中断来用吧?不然将它接到 PLIC 去又有什麽意义呢?
由於笔者不知道怎麽除错先前使用的 debian 映像,所以决定自己简单的编 Linux 来用。懒人包(拉取 Linux 核心会非常久):
git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
cd linux
make ARCH=riscv defconfig
make ARCH=riscv menuconfig # 将 Kernel Hacking 里面的 Debug Info 勾选起来,储存
make ARCH=riscv CROSS_COMPILE=riscv64-buildroot-linux-musl- -j12 # 假设先前的工具链路径已经在环境变数 PATH 中
...
Kernel: arch/riscv/boot/Image.gz is ready # 成功之後会显示这个
直接拿来试跑的指令:
qemu-system-riscv64 \
-smp 4 \
-M virt \
-m 256M \
-nographic \
-kernel ./arch/riscv/boot/Image
...
[ 0.317606] Serial: 8250/16550 driver, 4 ports, IRQ sharing disabled
[ 0.329343] 10000000.uart: ttyS0 at MMIO 0x10000000 (irq = 2, base_baud = 230400) is a 16550A
...
当然最後会遇到错误,因为我们没有给它能用的使用者空间,但这不是重点。重点是,我们看到 UART 相关的部份出现在 Linux 的启动讯息内了。检索一番之後,可以发现这些讯息在 tty/serial/8250/8250_core.c
里的 serial8250_init
函数。不如就开个 GDB,从那里开始观察吧:
... # 锁定虚拟记忆体开启之时刻
0x8020103c: sfence.vma
0x80201040: csrw satp,a0
...
Continuing.
[Switching to Thread 1.2]
Thread 2 hit Breakpoint 1, 0x0000000080201040 in ?? ()
(gdb) d 1
(gdb) si
0x0000000080201044 in ?? ()
(gdb) set $pc=0xffffffff80001044
(gdb) si
0xffffffff80001048 in ?? ()
(gdb) add-symbol-file ../../aur/linux/vmlinux
add symbol table from file "../../aur/linux/vmlinux"
(y or n) y
(gdb) b serial8250_init
Cannot access memory at address 0xffffffff8081a2d0
(gdb)
但是,Linux 的早期分页做得比较多层次一点,这时候还是没有办法直接设置断点於我们想要观察的函数。无奈,只能多推进一点,
_start_kernel () at arch/riscv/kernel/head.S:327
327 call soc_early_init
(gdb)
328 tail start_kernel
(gdb) b serial8250_init
Breakpoint 2 at 0xffffffff8081a2d0: file drivers/tty/serial/8250/8250_core.c, line 1161.
这时候也可以简单确认一下 PLIC 的状态,完全没有启用任何来自其它装置的中断:
(gdb) p/x $satp
$1 = 0x8000000000080c04
(gdb) set $satp=0x0
(gdb) x/wx 0xc002080
0xc002080: 0x00000000
(gdb) x/wx 0xc002180
0xc002180: 0x00000000
(gdb) x/wx 0xc002280
0xc002280: 0x00000000
(gdb) x/wx 0xc002380
0xc002380: 0x00000000
(gdb) set $satp=$1
幸好,n
个几次之後,Linux 应该就已经设置完成了对应的页表,所以我们可以断在这里。但就算整个 serial8250_init
走完,也看不到 UART 的状态有被设置得与 OpenSBI 有何不同。话说回来,笔者还没有展示过怎麽从 GDB 观察 UART 的这些 MMIO 暂存器,但概念与上述偷看 PLIC 类似:
(gdb) p/x $satp
$1 = 0x80000000000814ee
(gdb) set $satp=0x0
(gdb) x/8bx 0x10000000
0x10000000: 0x00 0x00 0xc1 0x03 0x00 0x60 0xb0 0x00
一样是没有中断设置的状态:偏移量 1 的值(第二组)是 0x00
。那又该去哪里找 UART 中断开启的地方呢?
这寻宝的过程是最没有头绪的部份,但有一种开放世界的探索感。总之得用上各种线索,也需善用模式比对工具如
grep
。
用中断启用暂存器,也就是 IER
当作关键字去 driver/serial
底下乱搜一通,在 ./drivers/tty/serial/8250/8250.h
看到了这个函数:
135 static inline bool serial8250_set_THRI(struct uart_8250_port *up)
136 {
137 if (up->ier & UART_IER_THRI)
138 return false;
139 up->ier |= UART_IER_THRI;
140 serial_out(up, UART_IER, up->ier);
141 return true;
142 }
字面上的意思就像是要设置传输暂存器(Transmition Holder Register)的中断,并且透过 serial_out
去写入 MMIO 暂存器的感觉。确实,UART_IER
是定义为 1 的巨集,代表偏移量;UART_IER_THRI
代表 2
,对照一下也没错,就是 IER
内对应到传输暂存器的中断位元。所以我们往 serial_out
这个里面看去,
118 static inline void serial_out(struct uart_8250_port *up, int offset, int value)
119 {
120 up->port.serial_out(&up->port, offset, value);
121 }
出现函数指标时最懒人的追踪方法,果然还是回到 GDB 然後设断点下去看会跳到哪里。直接公布答案,是 mem_serial_out
函数:
407 static void mem_serial_out(struct uart_port *p, int offset, int value)
408 {
409 offset = offset << p->regshift;
410 writeb(value, p->membase + offset);
411 }
如果以
mem_serial_out
为断点观察,会发现 Linux 这里在做串列装置探测(probe)的时候,还是会重设DLL
和DLH
。
这个 writeb
函数是一个 I/O 界面。在 RISC-V 的实作的话,真正执行写入单一位元组指令(sb
)之前,有一个屏障指令 fence w,o
,代表所有的记忆体写入效应必须先於之後的任何输出效应完成。总之,只要持续观察这个 mem_serial_out
函数,就可以监控到 Linux 对 UART 的控制了。当然也可以从回溯堆叠观察是怎麽呼叫进来的,但讯息量不大,所以笔者也是将之略过。
在 QEMU 端使用 -append earlycon=sbi console=ttyS0
参数的话,有大半 Linux 的早期开机讯息就都会是经由 OpenSBI 去印出的。但是接下来,如果断点设在 mem_serial_out
,会因为两种状况而触发太多次:
所以最後笔者在 GDB 的除错条件是这样下的:
riscv64-elf-gdb -ex 'target remote :1234'
-ex 'b *0x0000000080201040' # 断在启用虚拟记忆体之前
-ex "c" -ex "d 1" -ex "si" # 删除实体记忆体断点
-ex "set \$pc=0xffffffff80001044" # 推进之後直接移动程序指标
-ex "add-symbol-file ../../aur/linux/vmlinux"
-ex "si 6" -ex "n 6" # 需要等到正式的页表建立,龟毛的 Linux ...
-ex 'b mem_serial_out if $a1 != 0 && ($a1 != 1 || $a2 != 0)'
# 回避上述两种状况
这麽一来,会停下来的条件大部份是对偏移量 4 的控制(MCR
,数据机控制暂存器),也终於可以抓到第一次中断设置。
Thread 4 hit Breakpoint 2, mem_serial_out (p=0xffffffff81324010 <serial8250_ports>, offset=1, value=2)
at drivers/tty/serial/8250/8250_port.c:409
409 offset = offset << p->regshift;
(gdb) bt
#0 mem_serial_out (p=0xffffffff81324010 <serial8250_ports>, offset=1, value=2) at drivers/tty/serial/8250/8250_port.c:409
#1 0xffffffff80356248 in serial_port_out_sync (offset=1, value=2, p=0xffffffff81324010 <serial8250_ports>)
at drivers/tty/serial/8250/8250_port.c:524
#2 serial_port_out_sync (p=p@entry=0xffffffff81324010 <serial8250_ports>, value=2, offset=1)
at drivers/tty/serial/8250/8250_port.c:516
#3 0xffffffff803576e0 in serial8250_do_startup (port=0xffffffff81324010 <serial8250_ports>)
at drivers/tty/serial/8250/8250_port.c:2307
value
的 2 代表设置传输暂存器变成空的的时候的中断。但追踪 serial8250_do_startup
的相关内容,又似乎是在测试 UART 而已。之後偶尔设成 5(两个位元都与接收暂存器有关),但笔者发现都没有真正触发外部中断。
先前我们都看不到外部中断,可能是因为 Linux 自己印出系统讯息的时候,都会希望不要干扰到串列装置的状态,大部分都是以直接写到 THR
的方式去输出。这个直接输出的前後会存取 IER
,也就是中断启用暂存器的状态,并做相对应的储存与回复动作。所以就算我们观测到一直有针对 IER
的写入,可是实际上还是都没有任何触发的中断。
这只是笔者的猜测,至少从实验看起来是如此。也许实际上理解有误,但不影响接下来的实验的正确性。
所以,还是乖乖把使用者空间创起来吧。参考以前参赛时留下的纪录,busybox 部份还是完全可以参照使用(连附上的 inittab
也完全可以续用),然後在 Linux 资料夹下调整一下 .config
档案,新增组态:
CONFIG_INITRAMFS_SOURCE="rootfs"
之後再回到 Linux 资料夹下 make ARCH=riscv
重编,就可以拿来开进 busybox 的使用者空间了。我们可以使用 GDB 先无条件放行整个系统,等到进入命令列之後,再试着下断点在 mem_serial_out/in
等两个函数,观察看看 UART 有哪些动态。
闲置时,对於 UART MMIO 暂存器,完全没有读写的动态。正好可以试一个实验,在 QEMU 端随意敲击一次输入键,我们来分析之後发生的每一件事情。记得,这时候的断点只有上述两个函数。
IIR
这时候的回溯堆叠为
Thread 2 hit Breakpoint 1, 0xffffffff803554ae in __raw_readb (addr=<optimized out>) at ./arch/riscv/include/asm/mmio.h:49
49 asm volatile("lb %0, 0(%1)" : "=r" (val) : "r" (addr));
(gdb) bt
#0 0xffffffff803554ae in __raw_readb (addr=<optimized out>) at ./arch/riscv/include/asm/mmio.h:49
#1 mem_serial_in (p=<optimized out>, offset=<optimized out>) at drivers/tty/serial/8250/8250_port.c:404
#2 0xffffffff80357cd0 in serial_port_in (offset=2, up=0xffffffff81324010 <serial8250_ports>) at ./include/linux/serial_core.h:263
#3 serial8250_default_handle_irq (port=0xffffffff81324010 <serial8250_ports>) at drivers/tty/serial/8250/8250_port.c:1948
#4 0xffffffff80354456 in serial8250_interrupt (irq=<optimized out>, dev_id=0xffffffe00185bbc0)
at drivers/tty/serial/8250/8250_core.c:126
#5 0xffffffff8005309c in __handle_irq_event_percpu (desc=desc@entry=0xffffffe00164a600, flags=flags@entry=0xffffffff81203cc4)
at kernel/irq/handle.c:156
#6 0xffffffff80053230 in handle_irq_event_percpu (desc=0xffffffe00164a600) at kernel/irq/handle.c:196
#7 handle_irq_event (desc=desc@entry=0xffffffe00164a600) at kernel/irq/handle.c:213
#8 0xffffffff80056c2e in handle_fasteoi_irq (desc=0xffffffe00164a600) at kernel/irq/chip.c:717
#9 0xffffffff80052684 in generic_handle_irq_desc (desc=<optimized out>) at ./include/linux/irqdesc.h:158
#10 handle_irq_desc (desc=<optimized out>) at kernel/irq/irqdesc.c:646
#11 generic_handle_domain_irq (domain=<optimized out>, hwirq=<optimized out>) at kernel/irq/irqdesc.c:675
#12 0xffffffff802f1114 in plic_handle_irq (desc=0xffffffe00160b200) at drivers/irqchip/irq-sifive-plic.c:236
#13 0xffffffff80052a10 in generic_handle_irq_desc (desc=<optimized out>) at ./include/linux/irqdesc.h:158
#14 handle_irq_desc (desc=<optimized out>) at kernel/irq/irqdesc.c:646
#15 handle_domain_irq (domain=<optimized out>, hwirq=<optimized out>, regs=<optimized out>) at kernel/irq/irqdesc.c:701
#16 0xffffffff802f0f0a in riscv_intc_irq (regs=<optimized out>) at drivers/irqchip/irq-riscv-intc.c:40
#17 0xffffffff8000304c in handle_exception () at arch/riscv/kernel/entry.S:232
可见我们从 Linux 的陷阱向量进入 PLIC 的处理函数,然後又导到 serial8250_interrupt
去。然後我们读取偏移为 2 的中断识别暂存器,这会显示哪个中断是这次触发的原因。
这个读取相关的资讯为
(gdb) p/x $a0
$1 = 0xffffffd000245002
(gdb) si
0xffffffff803554b2 49 asm volatile("lb %0, 0(%1)" : "=r" (val) : "r" (addr));
(gdb) p/x $a0
$2 = 0xffffffffffffffcc
第一个位址之所以是一个看起来很正常的虚拟位址,而不是 0x10000002
这个 UART MMIO 真正的物理位址,是因为在作业系统模式运行的 Linux 也没办法直接存取物理位址。在稍早初始化时,就已经建立好这个虚拟位址对应到 UART 去了。
读取回的值这里为 0xcc
,对应到维基的解释,是一个过期的中断搁置(Time-out Interrupt Pending)。
LSR
读回来的值显示 0x61
,分别代表
RBR
这里就真的将刚才的输入键读出来了,得到的值是 0xd
,也就是 \r
字元。
LSR
,再来一次这次就观察不到资料就绪了,显示 0x60
。
MCR
读取得 0xb0
,代表
这其实与前面小节看到的状态一样。
IIR
,再读一次这次得到的是 0xc1
。最低位元代表中断搁置位元,但语意上是前述 0xcc
为 0 时代表有中断搁置,而现在为 1 代表中断已处理。
IER
:写入值为 7值得一提的是,这里已经不是在前述的回溯堆叠,也就是 PLIC 中断处理或是 UART 中断处理函数之内了。因为後续处理的行为,不应该占用被中断的系统太久时间,所以是将相关资讯存下来,等到系统有空之後再慢慢去处理就好。
重新启用中断。
IIR
,再读一次这次得到的是 0xc2
,代表的是传输暂存器空了所发生的中断。
LSR
,再来一次这次也还是没有资料就绪,显示 0x60
。
MCR
还是一样 0xb0
。
IER
:写入值为 5重新启用中断。
IIR
,再读一次这次得到的是 0xc1
,也是没事。
IER
:写入值为 7重新启用中断。
IIR
,再读一次这次得到的是 0xc2
,代表的是传输暂存器空了所发生的中断。
LSR
,再来一次这次也还是没有资料就绪,显示 0x60
。
MCR
还是一样 0xb0
。
THR
:写入值为 \r
,终於!纪录一下这时的回溯堆叠:
#0 0xffffffff8035641a in __raw_writeb (addr=<optimized out>, val=13 '\r') at ./arch/riscv/include/asm/mmio.h:21
#1 mem_serial_out (p=0xffffffff81324010 <serial8250_ports>, offset=<optimized out>, value=13)
at drivers/tty/serial/8250/8250_port.c:410
#2 0xffffffff80357a76 in serial_out (value=<optimized out>, offset=0, up=0xffffffff81324010 <serial8250_ports>)
at drivers/tty/serial/8250/8250.h:120
#3 serial8250_tx_chars (up=up@entry=0xffffffff81324010 <serial8250_ports>) at drivers/tty/serial/8250/8250_port.c:1818
#4 0xffffffff80357c9e in serial8250_handle_irq (port=port@entry=0xffffffff81324010 <serial8250_ports>, iir=<optimized out>)
at drivers/tty/serial/8250/8250_port.c:1932
#5 0xffffffff80357cec in serial8250_handle_irq (iir=<optimized out>, port=0xffffffff81324010 <serial8250_ports>)
at drivers/tty/serial/8250/8250_port.c:1952
#6 serial8250_default_handle_irq (port=0xffffffff81324010 <serial8250_ports>) at drivers/tty/serial/8250/8250_port.c:1949
#7 0xffffffff80354456 in serial8250_interrupt (irq=<optimized out>, dev_id=0xffffffe00185bbc0)
at drivers/tty/serial/8250/8250_core.c:126
#8 0xffffffff8005309c in __handle_irq_event_percpu (desc=desc@entry=0xffffffe00164a600, flags=flags@entry=0xffffffff81203cc4)
at kernel/irq/handle.c:156
#9 0xffffffff80053230 in handle_irq_event_percpu (desc=0xffffffe00164a600) at kernel/irq/handle.c:196
#10 handle_irq_event (desc=desc@entry=0xffffffe00164a600) at kernel/irq/handle.c:213
#11 0xffffffff80056c2e in handle_fasteoi_irq (desc=0xffffffe00164a600) at kernel/irq/chip.c:717
#12 0xffffffff80052684 in generic_handle_irq_desc (desc=<optimized out>) at ./include/linux/irqdesc.h:158
#13 handle_irq_desc (desc=<optimized out>) at kernel/irq/irqdesc.c:646
#14 generic_handle_domain_irq (domain=<optimized out>, hwirq=<optimized out>) at kernel/irq/irqdesc.c:675
#15 0xffffffff802f1114 in plic_handle_irq (desc=0xffffffe00160b200) at drivers/irqchip/irq-sifive-plic.c:236
#16 0xffffffff80052a10 in generic_handle_irq_desc (desc=<optimized out>) at ./include/linux/irqdesc.h:158
#17 handle_irq_desc (desc=<optimized out>) at kernel/irq/irqdesc.c:646
#18 handle_domain_irq (domain=<optimized out>, hwirq=<optimized out>, regs=<optimized out>) at kernel/irq/irqdesc.c:701
#19 0xffffffff802f0f0a in riscv_intc_irq (regs=<optimized out>) at drivers/irqchip/irq-riscv-intc.c:40
#20 0xffffffff8000304c in handle_exception () at arch/riscv/kernel/entry.S:232
为何说终於呢?因为先前的所有 UART 处理,都还只是作业系统在维护 UART 的正确状态,但这个输入的字元的效应,却一直没有真正出现,直到此时。至此,我们已经有足够的范本可以临摹了。
予焦啦!今天轻描淡写的让我们先前一直惦记的 Hello World
讯息印出来了,但这相比於 UART 的学习历程,实在是轻如鸿毛。我们有着更远大的目标,所以才从 Linux 控制 Serial/UART 串列装置的驱动程序里面偷学几招。过程中一度陷入没办法做实验的窘境,但也只是印证了笔者对於 Linux 系统的了解仍然不够透彻。
明日,我们就将迎来 Hoddarla 技术部份在这次铁人赛的结尾。各位读者,我们明天再会!
<<: 初探网路安全(一):密码大小事,存在服务器的密码安全吗?
资料视觉化 这边我们会用到seaborn来做一下简单的资料视觉化。 import seaborn ...
你是不是常常在编辑试算表时遇到”####”呢?别紧张,其实出现这个符号并不是你输入错误喔,而是你的栏...
在上一篇文章中,我们介绍作者如何分析MLB赛事,并找出影响比赛胜负较为重要的因子,而今天我们就来看看...
吼气鼠,萤幕没录到我打的文字哈哈~ 补充在这边~ When I was young. -> ...
前面几天连续介绍有线与无线的应用,多数家中或单位的网路就都从这些应用做拓展。让透天每层楼都有网路、w...