移植xv6到SpacemiT K1

本文记录了在SpacemiT K1上移植xv6的过程,包括启动流程、硬件配置、中断使能、文件系统挂载以及多核启动等过程。
源代码仓库:xv6-OrangePi_RV2 (GitHub)

准备

  • OrangePi-RV2(或者其他集成SpacemiT K1 SoC的板子)
  • 一张已经烧写好Bianbu os的SD card

启动方式

在u-boot调试模式下,通过printenv命令可以看到u-boot的环境变量,这里我们列出和加载内核镜像相关的环境变量:

mmc_rootfstype=ext4
bootfs_devname=mmc
boot_devnum=0
bootfs_part=5
kernel_addr_r=0x08000000

autoboot=if test ${boot_device} = nand; then run nand_boot; elif test ${boot_device} = nor; then run nor_boot; elif test ${boot_device} = mmc; then run mmc_boot; elif test ${boot_device} = nfs; then run nfs_boot; fi;

mmc_boot=echo "Try to boot from ${bootfs_devname}${boot_devnum} ..."; run commonargs; run add_bootarg; run set_mmc_args; run get_esp_index; if test -e ${bootfs_devname} ${boot_devnum}:${esp_index} ${grub_file}; then run boot_grub; else run boot_kernel; fi; echo "########### boot failed by default config, check your boot config #############"

boot_kernel=run set_root_arg; run detect_dtb; run loadknl; run loaddtb; run loadramdisk; run start_kernel;

loadknl=echo "Loading kernel..."; load ${bootfs_devname} ${boot_devnum}:${bootfs_part} ${kernel_addr_r} ${knl_name};

u-boot会根据启动介质选择相应的启动方式,这里启动介质是mmc(适用于emmc和sd card),在启动命令的最后会启动内核。启动内核的其中一个步骤是加载内核镜像,对应的命令为:

load ${bootfs_devname} ${boot_devnum}:${bootfs_part} ${kernel_addr_r} ${knl_name};

结合环境变量的定义以及mmc文件系统类型,可以得到具体的命令:

ext4load mmc 0:5 0x08000000 vmlinuz-6.6.63

如果想要加载自己的内核镜像,我们可以将内核镜像放到sd card的第五分区,然后通过ext4load加载内核镜像到指定位置。
这里需要注意在xv6的链接脚本中,所定义的入口地址是0x80000000,我们需要将其修改为0x08000000

-> % git --no-pager diff kernel/kernel.ld
diff --git a/kernel/kernel.ld b/kernel/kernel.ld
index c72aaac..ba84477 100644
--- a/kernel/kernel.ld
+++ b/kernel/kernel.ld
@@ -7,10 +7,9 @@ SECTIONS
    * ensure that entry.S / _entry is at 0x80000000,
    * where qemu's -kernel jumps.
    */
-  . = 0x80000000;
+  . = 0x08000000;

   .text : {
-    kernel/entry.o(_entry)
     *(.text .text.*)
     . = ALIGN(0x1000);
     _trampoline = .;
@@ -35,10 +34,12 @@ SECTIONS
   }

   .bss : {
+    PROVIDE(start_bss = .);
     . = ALIGN(16);
     *(.sbss .sbss.*) /* do not need to distinguish this from .bss */
     . = ALIGN(16);
     *(.bss .bss.*)
+    PROVIDE(end_bss = .);
   }

   PROVIDE(end = .);

传统的内核镜像在镜像头中存放MAGIC NUMBER。通过bootm或者bootz启动镜像时,需要检查对应的MAGIC是否符合规范。为了方便,我们可以通过go命令直接跳转到该地址运行。
至此,我们已经可以在板子上启动xv6了。

硬件配置

xv6在kermel/memlayout.h中定义了使用的硬件的寄存器和中断号。
这里我们需要根据K1的设备树配置或者数据手册查看相应的配置并做出修改。

-> % git --no-pager diff kernel/memlayout.h
diff --git a/kernel/memlayout.h b/kernel/memlayout.h
index 9bc9424..0613352 100644
--- a/kernel/memlayout.h
+++ b/kernel/memlayout.h
@@ -18,25 +18,37 @@
 // PHYSTOP -- end RAM used by the kernel

 // qemu puts UART registers here in physical memory.
-#define UART0 0x10000000L
-#define UART0_IRQ 10
+#define UART0 0xd4017000L
+#define UART0_IRQ 42
+
+#define SDHCI0 0xd4280000L
+#define SDHCI0_IRQ 99

 // virtio mmio interface
-#define VIRTIO0 0x10001000
-#define VIRTIO0_IRQ 1
+//#define VIRTIO0 0x10001000
+//#define VIRTIO0_IRQ 1

 // qemu puts platform-level interrupt controller (PLIC) here.
-#define PLIC 0x0c000000L
+#define PLIC 0xe0000000L
-#define PLIC_SENABLE(hart) (PLIC + 0x2080 + (hart)*0x100)
-#define PLIC_SPRIORITY(hart) (PLIC + 0x201000 + (hart)*0x2000)
-#define PLIC_SCLAIM(hart) (PLIC + 0x201004 + (hart)*0x2000)
+#define PLIC_SENABLE(hart)   (PLIC + 0x2000 + (hart)*0x80)
+#define PLIC_SPRIORITY(hart) (PLIC + 0x200000 + (hart)*0x1000)
+#define PLIC_SCLAIM(hart)    (PLIC + 0x200004 + (hart)*0x1000)

 // the kernel expects there to be RAM
 // for use by the kernel and user pages
 // from physical address 0x80000000 to PHYSTOP.
-#define KERNBASE 0x80000000L
+#define KERNBASE 0x08000000L
 #define PHYSTOP (KERNBASE + 128*1024*1024)

串口打印

xv6通过直接读写寄存器来实现串口功能。由于此时我们处于S模式下,我们尽量避免直接操作底层硬件,恰好此时我们已经有opensbi,因此我们通过opensbi来完成串口的打印输出。

-> % git --no-pager diff kernel/console.c
diff --git a/kernel/console.c b/kernel/console.c
index 4deeac6..041c57c 100644
--- a/kernel/console.c
+++ b/kernel/console.c
@@ -21,6 +21,7 @@
 #include "riscv.h"
 #include "defs.h"
 #include "proc.h"
+#include "sbi.h"

 #define BACKSPACE 0x100  // erase the last output character
 #define C(x)  ((x)-'@')  // Control-x
@@ -36,9 +37,15 @@ consputc(int c)
 {
   if(c == BACKSPACE){
     // if the user typed backspace, overwrite with a space.
-    uartputc_sync('\b'); uartputc_sync(' '); uartputc_sync('\b');
+    sbi_console_putchar('\b');
+    sbi_console_putchar(' ');
+    sbi_console_putchar('\b');
   } else {
-    uartputc_sync(c);
+    sbi_console_putchar(c);
   }
 }

@@ -69,7 +76,11 @@ consolewrite(int user_src, uint64 src, int n)
       nn = n - i;
     if(either_copyin(buf, user_src, src+i, nn) == -1)
       break;
-    uartwrite(buf, nn);
+    for(int j = 0; j < nn; j++) {
+      sbi_console_putchar(buf[j]);
+    }
     i += nn;
   }

内存分页

在实际的移植过程中,需要注意K1的MMU和QEMU的MMU行为是不一样的,页表初始化时需要给所有的leaf-PTE添加额外的标志位 PTE_A和PTE_D,表示该页可访问且数据为脏,否则将会一直停留在中断。

-> % git --no-pager diff kernel/riscv.h
diff --git a/kernel/riscv.h b/kernel/riscv.h
index ef3372c..64b6d4d 100644
--- a/kernel/riscv.h
+++ b/kernel/riscv.h
@@ -360,9 +360,12 @@ typedef uint64 *pagetable_t; // 512 PTEs
 #define PTE_W (1L << 2)
 #define PTE_X (1L << 3)
 #define PTE_U (1L << 4) // user can access
+#define PTE_A (1L << 6) // user can access
+#define PTE_D (1L << 7) // user can access

-> % git --no-pager diff kernel/vm.c
diff --git a/kernel/vm.c b/kernel/vm.c
index 28f4248..67cf240 100644
--- a/kernel/vm.c
+++ b/kernel/vm.c
@@ -7,50 +7,59 @@
 #include "spinlock.h"
 #include "proc.h"
 #include "fs.h"
+#include "sbi.h"

 // Return the address of the PTE in page table pagetable
@@ -164,7 +179,7 @@ mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
       return -1;
     if(*pte & PTE_V)
       panic("mappages: remap");
-    *pte = PA2PTE(pa) | perm | PTE_V;
+    *pte = PA2PTE(pa) | perm | PTE_V | PTE_A | PTE_D;
     if(a == last)
       break;
     a += PGSIZE;

中断管理

串口中断

xv6使用16650作为串口驱动芯片。K1使用8250芯片串口。
16550是第一种带先进先出(FIFO)功能的8250系列串口芯片。
Note:K1的串口的寄存器宽度是16位,而不是xv6中定义的8位。
当我们直接使用串口代码时,可以发现串口中断并没有如预期般发生。此时我们使用gdb调试逐步寻找问题。

(gdb) break main
Breakpoint 1 at 0x8000dc2: file kernel/main.c, line 15.
(gdb) continue
Continuing.
Disabling abstract command writes to CSRs.

Breakpoint 1, main () at kernel/main.c:15
15        if(cpuid() == 1){
(gdb) info registers sie mie
sie            0x220    544
mie            0x228    552
(gdb) continue
Continuing.
[k1.cpu_x60.0] Found 8 triggers
^C
Program received signal SIGINT, Interrupt.
scheduler () at kernel/proc.c:441
441         intr_on();
(gdb) source dump-uart.gdb
=== UART Registers ===
RBR/THR: 0x00000000
IER:     0x00000041
ISR/FCR: 0x000000c1
LCR:     0x00000003
MCR:     0x00000003
LSR:     0x00000060
MSR:     0x00000000
SCR:     0x00000000
=====================
(gdb) continue
Continuing.
^C
Program received signal SIGINT, Interrupt.
scheduler () at kernel/proc.c:441
441         intr_on();

此时在串口随便按下按键,以触发串口接收中断。

(gdb) source dump-uart.gdb
=== UART Registers ===
RBR/THR: 0x00000064
IER:     0x00000041
ISR/FCR: 0x000000c4
LCR:     0x00000003
MCR:     0x00000003
LSR:     0x00000061
MSR:     0x00000000
SCR:     0x00000000
=====================
(gdb) x /1wx 0xe0001004
0xe0001004:     0x00000040

可以看到,此时8250的LSR寄存器的接收中断位(BIT 1)已经被置位,且数据寄存器已经有相应的 值。但是PLIC的PENDING相应位(BIT 42)并没有被置1,那么说明串口中断并没有被真正的使能。
于是在网上找到了一些博客发现,对于8250,第三位OUT2可能是会被认为是中断使能位。
使能相应位,再重新进行gdb调试。

(gdb) break main
Breakpoint 1 at 0x8000dc2: file kernel/main.c, line 15.
(gdb) b main.c:28
Breakpoint 2 at 0x8000e48: file kernel/main.c, line 28.
(gdb) continue
Continuing.
Disabling abstract command writes to CSRs.

Breakpoint 1, main () at kernel/main.c:15
15        if(cpuid() == 1){
(gdb) continue
Continuing.
[k1.cpu_x60.0] Found 8 triggers

Breakpoint 2, main () at kernel/main.c:28
28          plicinithart();  // ask PLIC for device interrupts
(gdb) source dump-uart.gdb
=== UART Registers ===
RBR/THR: 0x00000000
IER:     0x00000041
ISR/FCR: 0x000000c1
LCR:     0x00000003
MCR:     0x0000000b
LSR:     0x00000060
MSR:     0x00000000
SCR:     0x00000000
=====================

此时在串口随便按下按键,以触发串口接收中断。

(gdb) source dump-uart.gdb
=== UART Registers ===
RBR/THR: 0x00000063
IER:     0x00000041
ISR/FCR: 0x000000c4
LCR:     0x00000003
MCR:     0x0000000b
LSR:     0x00000061
MSR:     0x00000000
SCR:     0x00000000
=====================
(gdb) continue
Continuing.
^C
Program received signal SIGINT, Interrupt.
scheduler () at kernel/proc.c:441
441         intr_on();
(gdb) continue
Continuing.
^C
Program received signal SIGINT, Interrupt.
scheduler () at kernel/proc.c:441
441         intr_on();
(gdb) x /1wx 0xe0001004
0xe0001004:     0x00000440

此时LSR寄存器(BIT 1)指示发生串口接收中断,并且此时PLIC对应的PENDING位已经置1,但是此时在串口交互端并没有实时的字符回显(字符回显的调用过程为usertrap()->devintr()->uartgetc()->consoleintr()->consputc()),串口也没有任何输出。说明相应的串口中断并没有被使能。问题停留在PLIC中断使能上。

结合opensbi中关于plic的定义,详见lib/utils/irpchip/plic.c。我们可以发现,对于这颗芯片来说,M模式和S模式下的这三个寄存器存在不同的基地址定义。这里关于PLIC,S模式下的ENABLE、PRIORITY以及CLAIM的定义需要修改。理论上的定义如下:

#define PLIC_SENABLE(hart)   (PLIC + 0x2000 + ((hart)*2 + 1)*0x80)
#define PLIC_SPRIORITY(hart) (PLIC + 0x200000 + ((hart)*2 + 1)*0x1000)
#define PLIC_SCLAIM(hart)    (PLIC + 0x200004 + ((hart)*2 + 1)*0x1000)

继续进行gdb调试,但是发现gdb调试结果还是和此前一致,没有字符回显。
那么我们可不可以通过直接触发M模式下的中断,从而让中断得到处理呢?
理论上是可以的,M模式下的中断一定要在M模式下进行处理有三种情况:
a) 当前特权级别为M-Mode且mstatus中的MIE打开,或当前特权级别低于M-Mode(中断总开关打开)
b) mip与mie中的第i位都被设置为1(表明中断待处理且使能位打开)
c) mideleg寄存器存在,且第i位没有被设置(中断没有被委托给S-Mode)
除此之外,可以在S模式下进行处理。

(gdb) info registers mideleg medeleg
mideleg        0x2222   8738
medeleg        0xb109   45321
(gdb) info registers mstatus mie
mstatus        0x8000000a00006680       SD:1 VM:00 MXR:0 PUM:0 MPRV:0 XS:0 FS:3 MPP:0 HPP:3 SPP:0 MPIE:1 HPIE:0 SPIE:0 UPIE:0 MIE:0 HIE:0 SIE:0 UIE:0
mie            0x228    552

Breakpoint 2, main () at kernel/main.c:28
28          plicinithart();  // ask PLIC for device interrupts
(gdb) continue
Continuing.

Breakpoint 3, main () at kernel/main.c:29
29          binit();         // buffer cache
(gdb) info registers sstatus sie
sstatus        0x8000000200006600       -9223372028264815104
sie            0x220    544
(gdb)

这里我们发现中断是实现了委托的,此时可以通过触发M模式下对应的中断,从而在S模式下进行处理。于是修改对应的宏定义。

#define PLIC_SENABLE(hart)   (PLIC + 0x2000 + ((hart)*2)*0x80)
#define PLIC_SPRIORITY(hart) (PLIC + 0x200000 + ((hart)*2)*0x1000)
#define PLIC_SCLAIM(hart)    (PLIC + 0x200004 + ((hart)*2)*0x1000)

再运行时发现还是没办法触发中断。
最后经过不断的修改尝试,发现修改成这组值时,中断是可以触发的,但是我目前还并不知道为什么这里的定义与plic中相应的定义对不上。

#define PLIC_SENABLE(hart)   (PLIC + 0x2000 + (hart)*0x80)
#define PLIC_SPRIORITY(hart) (PLIC + 0x200000 + (hart)*0x1000)
#define PLIC_SCLAIM(hart)    (PLIC + 0x200004 + (hart)*0x1000)

最终串口能够正常驱动且触发中断。

-> % git --no-pager diff kernel/uart.c
diff --git a/kernel/uart.c b/kernel/uart.c
index 248b9e4..316721a 100644
--- a/kernel/uart.c
+++ b/kernel/uart.c
@@ -9,153 +9,246 @@
 #include "spinlock.h"
 #include "proc.h"
 #include "defs.h"
+#include "sbi.h"

// the UART control registers.
// some have different meanings for read vs write.
// see http://byterunner.com/16550.html
-#define RHR 0                 // receive holding register (for input bytes)
-#define THR 0                 // transmit holding register (for output bytes)
-#define IER 1                 // interrupt enable register
-#define IER_RX_ENABLE (1<<0)
-#define IER_TX_ENABLE (1<<1)
-#define FCR 2                 // FIFO control register
-#define FCR_FIFO_ENABLE (1<<0)
-#define FCR_FIFO_CLEAR (3<<1) // clear the content of the two FIFOs
-#define ISR 2                 // interrupt status register
-#define LCR 3                 // line control register
-#define LCR_EIGHT_BITS (3<<0)
-#define LCR_BAUD_LATCH (1<<7) // special mode to set baud rate
-#define LSR 5                 // line status register
-#define LSR_RX_READY (1<<0)   // input is waiting to be read from RHR
-#define LSR_TX_IDLE (1<<5)    // THR can accept another character to send

+// 寄存器偏移(考虑 reg-shift = 2)
+#define RBR_OFFSET 0    // Receiver Buffer (read)
+#define THR_OFFSET 0    // Transmitter Holding (write)
+#define IER_OFFSET 4    // Interrupt Enable
+#define ISR_OFFSET 8    // Interrupt Status (read)
+#define FCR_OFFSET 8    // FIFO Control (write)
+#define LCR_OFFSET 12   // Line Control
+#define MCR_OFFSET 16   // Modem Control
+#define LSR_OFFSET 20   // Line Status
+// IER 位
+ #define IER_RX_ENABLE (1<<0)
+ #define IER_TX_ENABLE (1<<1)
 
-// for sending threads to synchronize with uart "ready" interrupts.
-static struct spinlock tx_lock;
-static int tx_busy;           // is the UART busy sending?
-static int tx_chan;           // &tx_chan is the "wait channel"
+// LSR 位
+#define LSR_RX_READY (1<<0)
+#define LSR_TX_IDLE  (1<<5)

-extern volatile int panicking; // from printf.c
-extern volatile int panicked; // from printf.c
+#define MCR_DTR  (1<<0)
+#define MCR_RTS  (1<<1)
+#define MCR_OUT2 (1<<3)  // ← 关键!中断输出使能

-void
-uartinit(void)
-{
-  // disable interrupts.
-  WriteReg(IER, 0x00);
-  // special mode to set baud rate.
-  WriteReg(LCR, LCR_BAUD_LATCH);
-
-  // LSB for baud rate of 38.4K.
-  WriteReg(0, 0x03);
-
-  // MSB for baud rate of 38.4K.
-  WriteReg(1, 0x00);
-
-  // leave set-baud mode,
-  // and set word length to 8 bits, no parity.
-  WriteReg(LCR, LCR_EIGHT_BITS);
-
-  // reset and enable FIFOs.
-  WriteReg(FCR, FCR_FIFO_ENABLE | FCR_FIFO_CLEAR);
-
-  // enable transmit and receive interrupts.
-  WriteReg(IER, IER_TX_ENABLE | IER_RX_ENABLE);
-
-  initlock(&tx_lock, "uart");
-}
-
+void uartinit(void)
 {
+  // 1. 禁用中断
+  WriteReg(IER_OFFSET, 0x00);
+
+  // 2. 清空所有残留数据(重要!)
+  int cleared = 0;
+  while(ReadReg(LSR_OFFSET) & LSR_RX_READY) {
+    (void)ReadReg(RBR_OFFSET);
+    cleared++;
+    if(cleared > 100) break;  // 防止死循环
+  }
+
+  // 3. 重置并启用 FIFO
+  WriteReg(FCR_OFFSET, 0x07);
+
+  // 4. 延迟一下
+  for(volatile int i = 0; i < 1000; i++);
+
+  // 5. 再次确保缓冲区空
+  while(ReadReg(LSR_OFFSET) & LSR_RX_READY) {
+    (void)ReadReg(RBR_OFFSET);
   }

+  WriteReg(MCR_OFFSET, MCR_DTR | MCR_RTS | MCR_OUT2);
+  //// 6. 最后启用接收中断
+  WriteReg(IER_OFFSET, 0x40 | IER_RX_ENABLE);
 }

文件系统

在xv6中,启动时会将文件系统挂载在虚拟磁盘virtio_disk中。但是对于真实的板子,存储介质往往是ramdisk、sd card、emmc等等,这里我使用sd card来驱动,将文件系统镜像放在sd card的指定分区,通过读写分区来实现文件系统的功能。

多核启动

当我们执行go命令让芯片跳转到指定地址运行xv6时,其实仅仅只有一个hart会来到这里。
这里我们在main函数中使用opensbi接口来实现对其他的hart的唤醒。

if (0 != sbi_hsm_hart_start(2, 0x08000000, 0))
	panic("hart 0 start fail\n");

至此,我们已经完成了xv6在SpacemiT K1上的移植。

移植总结

总体而言,将 xv6 移植到 SpacemiT K1 的过程并不复杂,关键在于结合 opensbi 的源码和厂商提供的代码,针对实际问题进行分析排查。
这次移植最大的收获,是通过灵活运用 gdb 和 objdump 工具,逐步定位和解决移植过程中遇到的问题。同时,对 RISC-V 架构的 CSR 寄存器也有了更深入的理解和认识。

4 个赞

非常详细的教程 :+1: 感谢大佬分享~