OS - LA32R移植报告

这一部分由我与另一位同学共同完成。我主要完成的是SD卡部分的工作。仅此向Lab4就已探索出前三个Lab移植的他表示敬意…

指令集适配

这一部分只介绍较为关键的改动。有关Makefile中编译选项的修改,指令名称、格式变动引起的修改,在此不多加赘述。

例外有关

LA32R需要在状态控制寄存器(CSR)中,配置EENTRYTLBRENTRY两项,分别对应普通例外的入口地址,以及TLB重填例外的入口地址。不难注意到,LA32R的TLB重填例外,是与其他例外分开处理的。

此外,由于触发 TLB 重填例外之后,处理器核将进入直接地址翻译模式,TLBRENTRY处所填入口地址应当是物理地址EENTRY不受影F响。

配置两个入口的工作,也在start.S中进行:

1
2
3
4
5
6
7
8
9
       la      a0, exc_gen_entry  
       csrwr   a0, csr_eentry

       la      a1, tlb_miss_entry
       // 转换为物理地址
       li.w    a0, 0x1fffffff
       and     a1, a1, a0
       
       csrwr   a1, csr_tlbrentry

随后,是异常处理程序入口的获取。

这需要修改kern/entry.S

  • 在出现TLB有关异常时,LA32R核会直接由先前已经规定的TLB例外入口,进入对应的处理程序,我们在这里只需要关心其他例外的处理程序入口;
  • 与先前由CP0.CAUSE获取例外编号类似,我们由CSR.ESTAT获取例外编号,进入对应的处理程序。

虚拟地址翻译

LA32R引入了 “直接地址翻译模式”与“映射地址翻译模式”。在我们的实验中,我们的虚拟地址与物理地址位数相同,前者的虚拟地址直接就是物理地址,后者的虚拟地址则是由MMU进行管理,与常见实现类似。

LA32R还提供了“直接映射地址翻译模式”,允许在映射地址翻译模式下,按照CSR.DMW0/1中配置好的地址翻译窗口,将某个虚拟地址不通过页表,直接翻译为某个物理地址。

对于某些地址,我们希望其不通过Cache,直接访问内存(如MMIO有关)。LA32R提供了一致可缓存、强序非缓存两种模式,粗浅理解就是可否通过缓存访问。

LA32R并没有体系结构约定的虚拟地址空间。我们决定沿用原有的地址空间设计。因此,我们需要借助直接映射地址翻译,进行原设计中kseg0、kseg1的配置,是否可缓存的规则也与先前设计一致。

这一部分的配置工作在start.S中,于跳转到la32r_init()前进行:

1
2
3
4
5
6
7
8
       // Configure KSEG0/1  
       li.w a0, 0x8000000a
       csrwr a0, csr_dmw0
       li.w a0, 0xa0000011
       csrwr a0, csr_dmw1
       
       /* jump to la32r_init */
       b       la32r_init

此外,在start.S中,除了原有的禁用中断外,我们还需启用映射地址翻译模式:

1
2
       li.w    a0, 0xb0     // PLV = 0, ID = 0, DA = 0, PG = 1, DATF = DATM = 01 
       csrwr   a0, csr_crmd

LA32R亦对TLB/页表的表项结构产生了影响,大致可以归纳为以下两点:

  • 引入PLV位,对访问权限进行控制;这需要在涉及页表操作时,对应设置权限位(pmap.cenv.c中涉及更改居多。大部分为PLV3,即用户态)
  • 表项属性位置的变化。

TLB异常有关

正如前文所言,TLB重填的处理入口,在LA32R中是与普通异常独立的,这为我们的实现带来了便利。

  1. tlb_out被弃用,tlb_invalidate()使用invtlb指令即可实现
  • 原有设计,是去除指定asidva的TLB项目
  • 内存共享问题此处已被考虑,就是要去除这一被共享的TLB项
  • 查询LA32R手册,得知其对应模式标志为0x6
  • invtlb 0x6, a0, a1即可,$a0/1对应先前tlb_out参数中的asid/va
  1. tlb_asm.S内直接实现tlb_miss_entry中的重填逻辑。整体流程:
  • 将处理过程中,要用到的寄存器原始内容保存进CSR
  • 读取CSR中,一级页表地址
  • 按照BADV,算出页目录索引
  • 按页表索引,得出页目录项;检查之是否合法
  • 合法,继续读取页表,将页表项存入TLB
  • 不合法,则缺页,写两个空项进入TLB
  • 处理完后,恢复寄存器

原有的TLB项对应页无效逻辑变化不大,此处不加赘述。

对应,kern/tlbex.c受到影响。

计时器

首先,要配置计时器的中断使能,需要配置CSR.ECFG。这在进程创建时(env.c)进行;在进程切换,根据Trapframe恢复现场时,时钟中断对应就会启用。

1
e->env_tf.csr_ecfg = ECFG_TIE;

其次,是计时器的控制,需要配置CSR.TCFG。实现在include/kclock.h中:

1
2
3
4
.macro RESET_KCLOCK
li.w t0, 0xfffd // Enable = 1, Periodic = 0, Else as the init value of clock
csrwr t0, csr_tcfg
.endm

LA32R提供了Periodic的计时器复位方法:在时钟中断发生后,计时器自动复位成CSR.TVAL中预设值。为了简化设计,我们选择MIPS中原有的计时器复位思路,即时钟中断发生后,由软件手动配置计时器。

最后,则是时钟中断的处理。在LA32R中,我们需要手动写CSR.TICLR寄存器,进行时钟中断的清除。这部分逻辑在genex.S处实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
handle_int:    
 // 判断是否为时钟中断;
 // 与原有逻辑相似,
 // 但现在从CSR.ESTAT中获取异常信息                                      
    csrrd   t0, csr_estat  
    andi    t1, t0, ESTAT_TI          
    bne     t1, zero, timer_irq         
timer_irq:                                 
    li.w     t0,1                             
    csrwr    t0, csr_ticlr                  
    li.w      a0, 0
    b       schedule                           
END(handle_int)

上下文保存

首先,我们需要修改Trapframe结构体的定义,使得移植后,我们仍能得知异常处理所需要的CSR信息:

  • PRMD:出错时的当前状态;这主要是为了进程的创建,使得进程退出后能够正常让系统回到内核态
  • BADV:出错的访存地址
  • ESTAT:例外状态,旨在得知发生了什么异常
  • ERA:异常处理后,返回的程序PC
  • ECFG:旨在方便创建进程时,打开时钟中断

单独存储PRMD、BADV,而不是直接从CSR内读取,主要是为了处理异常的嵌套,单层异常的话,其实等价于此时CSR.PRMD与CSR.BADV中存储的值。

对应的,我们需要修改include/stackframe.h中的SAVE/RESTORE_ALL宏,更新指令为LA32R格式,并适应现在的Trapframe设计,使得上下文能够正常保存与恢复。

此外,在LA32R的寄存器定义下,发生了以下两个关键变动:

  • $sp以及返回值寄存器的编号发生了变化;
  • LA32R下,参数寄存器有5个,不再需要在栈帧中存放额外参数

这需要我们对设计这两个更改的地方进行修正。

SD卡驱动

这部分的工作,大体可以分为SD卡的初始化,以及SD卡的读写两个部分。

  • 自然,我们需要规定需要访问的SD Host Controller寄存器的地址;本实现选择跟串口地址的定义放在一起。
  • SD卡的初始化,本实现选择放在la32r_init()中。
  • SD卡的核心交互逻辑,存储在kern/sd.c中。
  • 对应有include/sd.h,规定了ADMA2描述符结构体adma2_desc_entry,SD Host Controller寄存器的访问宏SDREG(),以及向SD卡发指令操作的函数定义。

为方便表述,下称Host Controller为控制器。

初始化

这里主要分为三步:

  • 配置控制器与SD卡的时钟;
  • 配置控制器的一般/错误中断状态使能;
  • 给SD卡发送指令,初始化SD卡。

配置时钟控制,通过配置控制器的Clock Control Register实现。

1
2
3
4
5
6
7
8
void sd_clk_init() {
SDREG(MEGASOC_SD_CCR) = (
0x00 << 8 | // 直接使用可用的基础频率
0x1 << 0 // 启用控制器内部时钟
);
while (!(SDREG(MEGASOC_SD_CCR) & 0x0002)) {} // 等待控制器内部时钟稳定
SDREG(MEGASOC_SD_CCR) |= 0x1 << 2; // 启动SD卡的时钟
}

在时钟配置完成后,我们才能对SD卡发送指令,进行初始化。

一般/错误中断状态使能,则是通过配置控制器的Normal/Error Interrupt Status Enable Register实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
void sd_intr_init() {
SDREG(MEGASOC_SD_NISER) = (
1 << 0 | // CMD done
1 << 1 | // Transfer done
1 << 3 // DMA intr
);

SDREG(MEGASOC_SD_EISER) = (
1 << 0 | 1 << 1 | // CMD timeout/CRC
1 << 4 | 1 << 5 | // DAT timeout/CRC
1 << 9 // ADMA err
);
}

对于NISER,我们只需要知道指令完成、数据发送完成(用于SD卡读写时)、DMA中断(DMA出错时有用)三个中断。而EISER,只需配置ADMA错误,控制器的CMD/DAT两条线的超时/CRC校验错误即可。

这两步完成后,即可按照手册中的初始化步骤,对SD卡自身进行初始化了。
cf952bc27452f66f7a1a0fb562dfbb62.jpg


我们使用ADMA2方法进行SD卡的读写。本实现在SD卡初始化时,就对SD卡的传输方式就进行配置。这通过配置Host Control 1 Register实现。

1
SDREG_8(MEGASOC_SD_HC1R) = 0x2 << 3; // Setup ADMA2

在进行SD卡的读写前,我们还需要让SD卡进入传输状态。

  • 在上面的初始化过程中,我们通过CMD3的返回值,得知了当前已插入SD卡的相对地址RCA
  • 我们需要通过发送CMD7,以这一RCA为参数,使得被插入的卡被选中,进入传输状态
  • 只有在传输状态下,SD卡才能接收并执行CMD18/25两个读写指令,进行正常读写。
  • 同样,本实现在sd_init()中就发送CMD7,简化SD卡读写逻辑的编写

至此,整个SD卡的初始化流程就结束了。此后,我们即可调用SD卡的读写逻辑,对SD卡进行读写。

读写逻辑

整个读写逻辑,我们打包在sys_read/write_block()系统调用中。文件服务在需要进行读写时,直接调用syscall_(read/write)_block(secno, (dst/src), nsecs),即可对SD卡进行读写。增加系统调用的操作在此不加赘述。

具体的读写逻辑,在kern/syscall_all.c中定义。读、写过程十分相近,大体可以分为以下步骤:

  • 配置ADMA2标志符表,并将标志符表的物理地址,写入控制器的ADMA System Address寄存器,告知控制器标志符表的地址
  • 配置控制器的Transfer Mode寄存器,为读/写操作做准备
  • 配置控制器的块大小、块计数寄存器;
  • 发送多块读/写指令(CMD18/25)

此处以读操作的实现为例,进行具体分析。

描述符表项的结构体定义如下:

1
2
3
4
5
typedef struct {
uint16_t attribute;
uint16_t length;
uint32_t paddr;
} adma2_desc_entry;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int sys_read_block(u_int secno, void *dst, u_int nsecs) {
// 初始化描述符表;各描述符表项需连续分布在一段地址空间上
adma2_desc_entry adma2_dtable[512];
int i;
for (i = 0; i < nsecs; i++) {
uint32_t vaddr = dst + (i * 512);
uint32_t paddr = va2pa(curenv->env_pgdir, (u_long)vaddr); // 获取目标地址的物理地址;描述符表项的地址域,需要的是物理地址

adma2_dtable[i].paddr = ((uint32_t)(paddr));
adma2_dtable[i].length = 512; // 一块的大小

adma2_dtable[i].attribute = 1 << 5 | // 配置表项为传输模式;此处涉及描述符表项的其他设计,此处略
1 << 2 | // 出现错误时中断
1 << 0; // 表示该表项合法

if (i == nsecs - 1) {
adma2_dtable[i].attribute |= 1 << 1; // 描述符表的最后一项,需要配置一个终止标记
}
}

SDREG(MEGASOC_SD_TM) = 1 << 0 | // 启用DMA
1 << 1 |// 启用块计数寄存器;配合CMD12使用
0x1 << 2 | // 自动发送CMD12(停止传输);这样,我们就只需在发送CMD18/25后,等待“传输完成”中断
1 << 4 |// 数据方向为读
1 << 5; // 多块数据操作

// 将描述符表的物理地址告知控制器
uint32_t dtable_paddr = PADDR(adma2_dtable);
SDREG(MEGASOC_SD_ADMASAR_BASE) = ((uint16_t)(dtable_paddr & 0xffff));
SDREG(MEGASOC_SD_ADMASAR_BASE + 0x2) = ((uint16_t)(dtable_paddr >> 16));

// 配置块大小和块数
SDREG(MEGASOC_SD_BLKSIZE) = 512;
SDREG(MEGASOC_SD_BLKCNTR) = ((uint16_t) nsecs);

// 发送读/写指令,等待其完成
sd_send_cmd18(secno * 512); // CMD18/25传入的SD卡数据地址参数,是字节编址的,因此需要对参数做如此变换
return 0;
}

至此,我们就完成了一个基本的SD卡驱动。

作者

LajiPZ

发布于

2025-07-08

更新于

2025-07-08

许可协议

评论