这个5.1出不了门,正好重温一下嵌入式操作系统知识,手头有一块Xilinx的开发版,想着试一试将xv6移植到开发板上。
开发版使用Zynq UltraScale+ MPSoCs EV系列的系列的芯片,型号为 XCZU4EV-1SFVC784I。ZU4EV 芯片的PS 系统集成了 4 个ARM Cortex™-A53处理器,2个Cortex-R5 处理器。其中R5基于 ARMv7R ,是32位的指令集,A53基于ARMv8,是64为指令集,一般R5用于实时任务,A53用作高速任务。xv6 是MIT 开发的一个教学用的完整的类 Unix 操作系统,是 Unix Version 6(v6)操作系统的重新实现。xv6 在一定程度上遵守 v6 的结构和风格,用ANSI C实现,并且是基于 x86多核处理器的。包含了进程管理、内存管理、文件系统等组件,非常适合操作系统的学习。这回就尝试将xv6移植到ZYNQ的A53上运行。
网上有将xv6移植到qemu虚拟virt上的开源项目xv6-OS-for-arm-v8,就以这个为基础,主要解决如下几个问题。(1)开发环境搭建。 Zynq使用xilix的vitis开发与调试,首先要将xv6引入到vitis中才能进一步使用相关的调试手段。(2)系统引导程序。主要完成A53处理器异常等级配置,建立c语言的栈调用环境,然后跳转到c语言主程序。(3)串口驱动。在系统运行初期提供辅助调试手段和在系统运行时为控制台程序提供输入输出。(4)任务调度进程。(5)SD卡运行程序。
一、开发环境搭建
1.vitis
嵌入式 Linux 开发,一般需要一台 Linux 操作系统主机,用来编译 u-boot 或者 Linux-kernel。一般习惯先在windows平台安装虚拟机,然后在虚拟集中安装Linux主机,鉴于ubuntu Linux 桌面操作 系统的安装以及配置较为简单,选择了ubuntu 桌面操作系统。这里使用Ubuntu 18.04.2 LTS 64 位操作系统。在操作系统安装完成后安装Vivado 软件(2020.1,集成vitis)。
这样xv6在zynq下的调试环境就搭建完成。其中xv6的entry、start和内核页表从0X40100000的地址开始,xv6其他部分从0X40300000的地址开始。
二、系统引导程序
zynq复位后首先运行存储在rom中的boot程序,由该程序加载fsbl,全称为first stage boot loader,再由fsbl进一步引导uboot或目标系统。这里直接使用fsbl加载xv6。
A53复位后处于EL3异常等级,操作系统内核一般运行在EL1,用户程序运行在EL0。在复位后bootrom,完成ram、flash 的初始化后加载fsbl,fsbl分别加载PL的配置文件(bit),然后加载并跳转到PS部分即xv6。因此xv6首先要返回到EL1,然后初始化C语言的调用栈环境。
armv8使用eret从高一级的异常状态返回,这里需要配置SCR_EL3的NS=0,使EL1可以访问Secure memory,特别是gic控制器。
.text
.align 16
.global _start
#define SCTLR_RESERVED (3 << 28) | (3 << 22) | (1 << 20) | (1 << 11)
#define SCTLR_EE_LITTLE_ENDIAN (0 << 25)
#define SCTLR_EOE_LITTLE_ENDIAN (0 << 24)
#define SCTLR_I_CACHE_DISABLED (0 << 12)
#define SCTLR_D_CACHE_DISABLED (0 << 2)
#define SCTLR_MMU_DISABLED (0 << 0)
#define SCTLR_MMU_ENABLED (1 << 0)
#define SCTLR_VALUE_MMU_DISABLED (SCTLR_RESERVED | SCTLR_EE_LITTLE_ENDIAN | SCTLR_I_CACHE_DISABLED | SCTLR_D_CACHE_DISABLED | SCTLR_MMU_DISABLED)
#define HCR_RW (1 << 31)
#define HCR_VALUE HCR_RW
#define SCR_RESERVED (3 << 4)
#define SCR_RW (1 << 10)
#define SCR_NS (0 << 0)
#define SCR_VALUE (SCR_RESERVED | SCR_RW | SCR_NS)
#define SPSR_MASK_ALL (7 << 6)
#define SPSR_EL1h (5 << 0)
#define SPSR_VALUE (SPSR_MASK_ALL | SPSR_EL1h)
_start:
Loop_el:
mov x0,0
mrs x0, currentEL
cmp x0, #0xC
beq InitEL3
cmp x0, #0x8
beq InitEL2
cmp x0, #0x4
beq InitEL1
InitEL3:
adr x0, Loop_el
msr elr_el3,x0
ldr x1, =SCTLR_VALUE_MMU_DISABLED
msr sctlr_el1, x1
ldr x1, =HCR_VALUE
msr hcr_el2, x1
ldr x1, =SCR_VALUE
msr scr_el3, x1
ldr x1, =SPSR_VALUE
msr spsr_el3, x1
msr elr_el3, x0
eret
初始化C语言的栈帧环境及跳转。
Init_el0:
adr x0, Loop_el
msr elr_el1,x0
mov x0,0
msr spsr_el1,x0
eret
entry_0:
adrp x0, init_stktop
mov sp, x0
# clear the entry bss section, the svc stack, and kernel page table
LDR x1, =edata_entry
LDR x2, =end_entry
MOV x3, #0x00
1:
CMP x1, x2
B.GT 2f
STR x3, [x1]
ADD x1, x1, #0x08
BLT 1b
2:
BL start
其中init_stktop、edata_entry、end_entry 均定义在链接文件lscript.ld 中,sp=init_stktop为栈顶,栈空间大小为0x1000 ,将从edata_entry到end_entry的空间清零,包含了xv6自身引导程序的bss段,栈和内核页表空间。
最后跳转到C语言入口start函数地址。
三、串口驱动
在xv6启动初始化阶段,仅用到了UART0输出,用于打印内核信息,该阶段工作在实地址模式。UART0的基址为0XFF000000.
UART0输出相关寄存器的偏移。
当判断TXFULL为空时,就可以向TXFIFO写发送数据。使用volatile 关键字定义寄存器变量,防止缓存。重写_uart_putc(int c)函数如下:
#define XUARTPS_SR_OFFSET 0x002CU /**< Channel Status [14:0] */
#define STDIN_BASEADDRESS 0xFF000000
#define XUARTPS_FIFO_OFFSET 0x0030U /**< FIFO [7:0] */
#define XUARTPS_SR_TXFULL 0x00000010U /**< TX FIFO full */
void _uart_putc(int c)
{
while(1)
{
uint v_sr=0;
volatile uint8 * uartsr = (uint8*)(STDIN_BASEADDRESS+XUARTPS_SR_OFFSET);
v_sr =*uartsr;
if((v_sr & XUARTPS_SR_TXFULL) == XUARTPS_SR_TXFULL)
{
continue;
}
else
{
break;
}
}
/* Write the byte into the TX FIFO */
volatile uint8 * uart0 = (uint8*)(STDIN_BASEADDRESS+XUARTPS_FIFO_OFFSET);
*uart0 = (c&0x00ff);
}
当xv6完成内核页表初始化,开启mmu后,需要使用虚地址访问UART0,基址经TTBR1_EL1中的页表映射到地址0xFFFFFFFFFF000000,在EL1虚地址模式armv8对大于0x0000ffff_ffffffff的地址使用TTBR1_EL1作为页表基地址寄存器,不大于的使用TTBR0_EL1,一般情况下TTBR0用于存放用户空间的一级页表基址,TTBR1存放内核空间的一级页表基址。
UART0接受数据使用中断,通过GIC的IRQ中断,中断号53。首先配置GIC中断及对应中断号的处理函数,然后开启UART0接收数据中断。详细可参考UG1085.。
#define V_STDIN_BASEADDRESS 0xFFFFFFFFFF000000
void uart_enable_rx ()
{
u32 TempMask ;
volatile u32 *uart_RXWM = (u32 *)(V_STDIN_BASEADDRESS+XUARTPS_RXWM_OFFSET);
*uart_RXWM = 1;
volatile u32 *uart_TXWM = (u32 *)(V_STDIN_BASEADDRESS+XUARTPS_TXWM_OFFSET);
*uart_TXWM = 1;
TempMask = XUARTPS_IXR_RXOVR|XUARTPS_IXR_RXEMPTY;
volatile u32 *uart_IER=(u32 *)(V_STDIN_BASEADDRESS+XUARTPS_IER_OFFSET);
*uart_IER =TempMask;
pic_enable(PIC_UART0, isr_uart);
}
中断号处理函数isr_uart:
int uartgetc (void)
{
volatile u32 *uartsr = (u32 *)(V_STDIN_BASEADDRESS+XUARTPS_SR_OFFSET);
int CsrRegister = *uartsr;
if(((CsrRegister & XUARTPS_SR_RXEMPTY) == (u32)0))
{
volatile u32 *uartfifo = (u32 *)(V_STDIN_BASEADDRESS+XUARTPS_FIFO_OFFSET);
return *uartfifo;
}
else
{
volatile u32 *uartisr = (u32 *)(V_STDIN_BASEADDRESS+XUARTPS_ISR_OFFSET);
*uartisr = XUARTPS_IXR_RXEMPTY;
return -1;
}
}
void isr_uart (struct trapframe *tf, int idx)
{
volatile u32 *uartsr = (u32 *)(V_STDIN_BASEADDRESS+XUARTPS_SR_OFFSET);
int CsrRegister = *uartsr;
if((CsrRegister & XUARTPS_SR_RXOVR) && ((CsrRegister & XUARTPS_SR_RXEMPTY) == (u32)0))
{
consoleintr(uartgetc);
}
volatile u32 *uartisr = (u32 *)(V_STDIN_BASEADDRESS+XUARTPS_ISR_OFFSET);
*uartisr = XUARTPS_IXR_RXOVR;
}
四、任务调度进程
在进程调度时需要保存调度出的进程的上下文的环境(一组寄存器),并将要取得cpu的进程的上下文更新到cpu。xv6声明的上下文数据结构如下:
struct context {
// svc mode registers
uint64 r3;
uint64 r4;
uint64 r5;
uint64 r6;
uint64 r7;
uint64 r8;
uint64 r9;
uint64 r10;
uint64 r11;
uint64 r12;
uint64 r13;
uint64 r14;
uint64 r15;
uint64 r16;
uint64 r17;
uint64 r18;
uint64 r19;
uint64 r20;
uint64 r21;
uint64 r22;
uint64 r23;
uint64 r24;
uint64 r25;
uint64 r26;
uint64 r27;
uint64 r28;
uint64 r29;
uint64 lr; // x30
};
在AARCH64 C语言栈回溯结构中,一般使用x30作为返回地址,x29保存sp。x0用作第一个参数,x1用作第二个参数。xv6的调度程序如下。
void scheduler(void)
{
struct proc *p;
for(;;){
// Enable interrupts on this processor.
sti();
// Loop over process table looking for process to run.
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->state != RUNNABLE) {
//_puts("scheduler3\n");
continue;
}
// Switch to chosen process. It is the process's job
// to release ptable.lock and then reacquire it
// before jumping back to us.
proc = p;
switchuvm(p);
p->state = RUNNING;
swtch(&cpu->scheduler, proc->context);
// Process is done running for now.
// It should have changed its p->state before coming back.
proc = 0;
}
release(&ptable.lock);
}
}
这里x0=&cpu->scheduler,x1=proc->context。swtch在swtch.S中实现。
.global swtch
swtch:
stp x29, x30, [sp, #-16]!
stp x27, x28, [sp, #-16]!
stp x25, x26, [sp, #-16]!
stp x23, x24, [sp, #-16]!
stp x21, x22, [sp, #-16]!
stp x19, x20, [sp, #-16]!
stp x17, x18, [sp, #-16]!
stp x15, x16, [sp, #-16]!
stp x13, x14, [sp, #-16]!
stp x11, x12, [sp, #-16]!
stp x9, x10, [sp, #-16]!
stp x7, x8, [sp, #-16]!
stp x5, x6, [sp, #-16]!
stp x4, x3, [sp, #-16]!
_sw_1:
# switch the stack
mov x21, sp
str x21, [x0]
mov sp, x1
_sw_2:
ldp x3, x4, [sp], #16
ldp x5, x6, [sp], #16
ldp x7, x8, [sp], #16
ldp x9, x10, [sp], #16
ldp x11, x12, [sp], #16
ldp x13, x14, [sp], #16
ldp x15, x16, [sp], #16
ldp x17, x18, [sp], #16
ldp x19, x20, [sp], #16
ldp x21, x22, [sp], #16
ldp x23, x24, [sp], #16
ldp x25, x26, [sp], #16
ldp x27, x28, [sp], #16
ldp x29, x30, [sp], #16
_sw_3:
# return to the caller
br x30
将所有入栈栈帧sp(x29)进行16字节对齐。
sp &= (~15);
第一个进程的栈结构如下。
第一个进程的启动过程参考xv6中文文档。
五、SD卡运行程序
在vitis中通过jtag调试好程序后,可以通过create boot image生成BOOT.bin文件拷贝到U盘根目录下,同时通过拨码开关调整启动方式一般就可以了,但是却遇到了意外,xv6在运行到cprinf函数时产生了同步异常,在jtag调试时没有任何问题,怀疑是系统配置导致。
通过gcc -S 生成void cprintf (char *fmt, ...)函数的汇编代码:
cprintf:
stp x29, x30, [sp, -256]!
add x29, sp, 0
str x0, [x29, 24]
str x1, [x29, 200]
str x2, [x29, 208]
str x3, [x29, 216]
str x4, [x29, 224]
str x5, [x29, 232]
str x6, [x29, 240]
str x7, [x29, 248]
str q0, [x29, 64]
str q1, [x29, 80]
str q2, [x29, 96]
str q3, [x29, 112]
str q4, [x29, 128]
str q5, [x29, 144]
str q6, [x29, 160]
str q7, [x29, 176]
adrp x0, cons
系统初始化阶段已经配置了CPACR_EL1寄存器。
// no trapping on FP/SIMD instructions
val32 = 0x03 << 20;
asm("MSR CPACR_EL1, %[v]": :[v]"r" (val32):);
暂时不去深究原因,暴力解决。手工去掉 str q0,~str q7相关代码,重新编译后,生成BOOT.bin,系统正常启动:
================= In Stage 4 ============
PMU-FW is not running, certain applications may not be supported.
Protection configuration applied
Exit from FSBL
starting-xv6-for-ARMv8..
Implementer: ARM Limited
Current EL: EL1
Flushing TLB and Instr Cache
clearing BSS section for the main kernel
**************************************************************************
** **
** **
** xv6 on ARMv8-A (64-bit) Architecture **
** **
** **
**************************************************************************
*
init: Starting Shell
$ ls
. 1 1 512
.. 1 1 512
cat 2 2 13592
echo 2 3 13088
grep 2 4 15176
info 2 5 13032
init 2 6 13808
kill 2 7 13064
ln 2 8 13064
ls 2 9 14832
mkdir 2 10 13144
rm 2 11 13136
sh 2 12 22048
stressfs 2 13 13552
usertests 2 14 47064
wc 2 15 14024
zombie 2 16 12776
UNIX 2 17 7828
console 3 18 0
最后的vitis项目文件, 提取码:64pb 。