oz1010's blog

记录生活或技能

系统概览

平台是由一个或多个元件组成的单个集成电路。有些组件可能是RISC-V内核,而其他组件可能具有不同的功能。通常,它们都将连接到单个系统总线。单个RISC-V核心包含一个或多个硬件线程,称为hart。

用户与运行调试器(如gdb)的调试主机(如笔记本电脑)交互。调试器与调试转换器(例如OpenOCD,它可能包括一个硬件驱动程序)通信,以与调试传输硬件(例如Olimex USB-JTAG适配器)通信。调试传输硬件将调试主机连接到平台的调试传输模块(Debug Transport Module, DTM)。DTM使用调试模块接口(DMI)提供对一个或多个调试模块(dm)的访问。

平台中每个hart都由一个DM控制。对hart-DM映射没有进一步的限制,但通常单个核心中的所有hart都由同一个DM控制。在大多数平台中,只有一个DM控制平台中的所有hart。

2024-09-14T09:34:55.png

调试模块DM

调试模块接口DMI

调试模块从属于称为调试模块接口(DMI)的总线。总线的主人是调试传输模块(Debug Transport Module)。调试模块接口可以是一个普通的总线,有一个主总线和一个从总线,也可以使用功能更全面的总线,如TileLink或AMBA高级外围总线。

DMI使用7到32位地址位。如果在这个DMI上有额外的DM,DMI地址空间中下一个DM的基址在nextdm中给出。调试模块自己的状态和寄存器应该只在上电时重置,而dmcontrol中的dmactive为0。如果dmactive为1,即使会引起CSRs被清零,所有hart在系统重置期间也要保持halt状态。

重置控制

调试模块控制一个全局复位信号ndmreset(非调试模块复位),它可以复位或保持复位平台中的每个组件,除了调试模块和调试传输模块。

选择hart

一个单一的DM可以管理高达2^20个hart。

要枚举所有hart,调试器必须首先通过将所有hart写入hartsel(假设最大大小)并回读值来确定HARTSELLEN,以查看实际设置了哪些位。然后,它从0开始选择每个hart,直到在dmstatus中anynonexistent为1,或者达到最高索引(取决于HARTSELLEN)。

抽象命令

DM支持一组抽象命令,其中大部分是可选的。根据实现的不同,调试器可能能够执行一些抽象命令,即使所选的hart没有停止。调试器只能通过尝试特定状态下的特定hart支持哪些抽象命令,然后查看abstractcs中cmderr以确定它们是否成功。

如果命令接受参数,调试器必须在写入命令之前将它们写入数据寄存器。

每个抽象命令都是32位值。前8位包含cmdtype,它决定了命令的类型。

cmdtypeCommand
0Access Register
1Quick Access
2Access Memory

Access Register

3.6.1.1 Access Register

遵循以下操作顺序:

  1. 若write为0且transfer为1,则将regno指定的寄存器的数据拷贝到数据区arg0中,并执行从m模式读取该寄存器时发生的任何副作用。
  2. 若write为1且transfer为1,则将数据区arg0的数据拷贝到regno指定的寄存器中,并执行从m模式写入该寄存器时发生的任何副作用。
  3. 若aarpostincrement为1,则将增加regno。
  4. 若postexec为1,执行Program Buffer.

抽象寄存器号表

地址范围说明
0x0000 - 0x0fffCSRs,可以通过dpc访问PC
0x1000 - 0x101fGPRs
0x1020 - 0x103f浮点寄存器
0xc000 - 0xffff保留给非标扩展和内部使用的

Quick Access

3.6.1.2 Quick Access

遵循以下操作顺序:

  1. 若hart已经暂停,命令将会设置cmderr为”halt/resume”并不会继续执行。
  2. 暂停hart。若hart因某些其他原因(如breakpoint)暂停,命令将会设置cmderr为”halt/resume”并不会继续执行。
  3. 执行Program Buffer。若发生异常,cmderr会被设置为”exception”并且program buffer结束执行,但快速访问命令会继续。
  4. 恢复hart。

Access Memory

3.6.1.3 Access Memory

遵循以下操作顺序:

  1. 若write为0,从arg1指定的内存位置拷贝数据到数据区arg0中。
  2. 若write为1,从数据区arg0拷贝数据到arg1指定的内存位置中。
  3. 若aampostincrement为1,则将增加arg1.

若任何操作失败,设置cmderr并不会执行任何剩余步骤。

程序缓冲区

3.7 Program Buffer

为支持在已停止的hart上执行任意指令,调试模块可以包括程序缓冲区,调试器可以将小程序写入其中。

调试器可以将一个小程序写入程序缓冲区,然后使用Access Register抽象命令执行它一次,命令中postexec位为1。调试器可以编写任何它喜欢的程序(包括跳出程序缓冲区),但是程序必须以ebreak或c.ebreak结束。

若progbufsize为1,则impebreak必须为1. 程序缓冲区可容纳一条32位或16位指令,所以在这种情况下,调试器必须只写一条指令,而不管它的大小。

执行程序缓冲区可能会破坏dpc。如果是这种情况,必须可以使用抽象命令读取/写入dpc,而不设置postexec。调试器必须尝试在停止和执行程序缓冲区之间保存dpc,然后在离开调试模式之前恢复dpc。

状态概览

下图显示了运行/停止调试期间hart经过的状态的概念视图,这些状态受到dmcontrol、abstractcs、abstractauto和command的不同字段的影响。

image-20240914114316238.png

系统总线访问

3.9 System Bus Access

调试器可以使用Program Buffer或Access Memory抽象命令从hart的角度访问内存。一个调试模块也可以包括一个System Bus Access块来提供内存访问而不涉及hart,而不管是否实现了Program Buffer。

System Bus Access块支持8, 16, 32, 64和128位访问。下表显示在sbdata中每个访问大小可以访问的位。

访问大小数据位
8sbdata0位7:0
16sbdata0位15:0
32sbdata0
64sbdata1, sbdata0
128sbdata3, sbdata2, sbdata1, sbdata0

DM寄存器

3.12 Debug Module Registers

本节中描述的寄存器是通过DMI总线访问的。每个DM都有一个基址(第一个DM为0)。下面的寄存器地址是这个基址的偏移量。

调试模块调试总线寄存器表

地址名称
0x04Abstract Data 0 (data0)
0x0fAbstract Data 11 (data11)
0x10Debug Module Control (dmcontrol)
0x11Debug Module Status (dmstatus)
0x12Hart Info (hartinfo)
0x13Halt Summary 1 (haltsum1)
0x14Hart Array Window Select (hawindowsel)
0x15Hart Array Window (hawindow)
0x16Abstract Control and Status (abstractcs)
0x17Abstract Command (command)
0x18Abstract Command Autoexec (abstractauto)
0x19Configuration String Pointer 0 (confstrptr0)
0x1aConfiguration String Pointer 1 (confstrptr1)
0x1bConfiguration String Pointer 2 (confstrptr2)
0x1cConfiguration String Pointer 3 (confstrptr3)
0x1dNext Debug Module (nextdm)
0x20Program Buffer 0 (progbuf0)
0x2fProgram Buffer 15 (progbuf15)
0x30Authentication Data (authdata)
0x34Halt Summary 2 (haltsum2)
0x35Halt Summary 3 (haltsum3)
0x37System Bus Address 127:96 (sbaddress3)
0x38System Bus Access Control and Status (sbcs)
0x39System Bus Address 31:0 (sbaddress0)
0x3aSystem Bus Address 63:32 (sbaddress1)
0x3bSystem Bus Address 95:64 (sbaddress2)
0x3cSystem Bus Data 31:0 (sbdata0)
0x3dSystem Bus Data 63:32 (sbdata1)
0x3eSystem Bus Data 95:64 (sbdata2)
0x3fSystem Bus Data 127:96 (sbdata3)
0x40Halt Summary 0 (haltsum0)

dmstatus, 0x11

3.12.1 Debug Module Status (dmstatus, at 0x11)

这个寄存器报告整个调试模块以及由hasel所选hart的状态。它的地址在将来不会改变,因为它包含了version。

整个寄存器是只读的。

dmcontrol, 0x10

3.12.2 Debug Module Control (dmcontrol, at 0x10)

这个寄存器控制整个调试模块以及由hasel所选的hart。

在本文档中,我们提到hartsel,它是hartselhi和hartsello的组合。虽然规范允许20个hartsel位,但实现可能会选择实现少于这个数。hartsel的实际宽度被称为HARTSELLEN。它最少为0,最多为20。调试器应该通过将所有的1写入HARTSELLEN(假设最大大小)并回读值以查看实际设置了哪些位来发现HARTSELLEN。

在任何给定的写操作中,调试器最多只能将1写入以下位中的一个:resumereq、hartreset、ackhavereset、setresethaltreq和clrresethaltreq。其他的必须写成0。

resethaltreq是每个hart可选内部状态位,不能读取,但可以使用setresethaltreq和clrresethaltreq写入。

hartinfo, 0x12

3.12.3 Hart Info (hartinfo, at 0x12)

这个寄存器能获取hartsel选择的hart的信息。

这个寄存器是可选的。若它不可用它会读到全0。

如果包含这个寄存器,调试器可以通过编写显式访问data或dscratch寄存器的程序来对程序缓冲区做更多的工作。

hawindowsel, 0x14

3.12.4 Hart Array Window Select (hawindowsel, at 0x14)

hawindowsel有效可写位取决于具体实现,最多为15位。比如单个DM支持48个hart,则此寄存器只有bit0可以写入,其余位均为0.

hawindow, 0x15

3.12.5 Hart Array Window (hawindow, at 0x15)

hart数组掩码寄存器,与hawindowsel一起选择hart数组。比如bit0代表hawindowsel * 32索引的hart,bit31代表hawindowsel * 32 + 31索引的hart。

abstractcs, 0x16

3.12.6 Abstract Control and Status (abstractcs, at 0x16)

当一个抽象命令正在执行时,写这个寄存器会造成cderr被设置为1(忙),如果它是0.

支持RV32,datacount至少为1;支持RV64,至少为2;支持RV128,至少为4.

command, 0x17

3.12.7 Abstract Command (command, at 0x17)

写这个寄存器会造成相关抽象命令被执行。

当一个抽象命令正在执行时,写这个寄存器会造成cderr被设置为1(忙),如果它是0.

如果cmderr为非0,写这个寄存器会被忽略。

abstractauto, 0x18

3.12.8 Abstract Command Autoexec (abstractauto, at 0x18)

这个寄存器是可选的。包含它允许更有效的突发访问。调试器可以通过设置位并读取它们来检测它是否被支持。

当一个抽象命令正在执行时,写这个寄存器会造成cderr被设置为1(忙),如果它是0.

confstrptr0, 0x19

3.12.9 Configuration String Pointer 0 (confstrptr0, at 0x19)

当设置confstrptrvalid时,读取该寄存器将返回配置字符串指针的第31:0位。读取其他的confstrptr寄存器返回地址的上位。

当实现系统总线主控时,这必须是一个可以与系统总线访问模块一起使用的地址。否则,这必须是一个可以用来访问ID0的hart的配置字符串的地址。

如果confstrptrvalid为0,则confstrptr寄存器保存标识符信息,该信息在本文档中没有进一步指定。

nextdm, 0x1d

3.12.10 Next Debug Module (nextdm, at 0x1d)

如果在DMI上有超过一个DM,这个寄存器包含链中下一个DM的基地址,如果是链中最后一个则为0.

data0, 0x04

3.12.11 Abstract Data 0 (data0, at 0x04)

data0到data11是基本的读/写寄存器,可以通过抽象命令读取或更改。dataccount表示实现了多少个,从data0开始,依次递增。

progbuf0, 0x20

3.12.12 Program Buffer 0 (progbuf0, at 0x20)

progbuf0到progbuf15提供对可选程序缓冲区的读/写访问。progbufsize表示从progbuf0开始实现的数目,依次递增。

authdata, 0x30

3.12.13 Authentication Data (authdata, at 0x30)

这个寄存器作为一个32位的串行端口进出认证模块。

当authbusy为0时,调试器可以通过读取或写入该寄存器与身份验证模块通信。没有单独的机制来指示溢出/下溢。

haltsum0, 0x40

3.12.14 Halt Summary 0 (haltsum0, at 0x40)

这个只读寄存器中的每个位表示一个特定的hart是否停止。不可用/不存在的hart不被认为是停止的。

LSB代表hart hartsel[19:5],5'h0的暂停状态,MSB代表harthartsel[19:5],5'h1f的暂停状态。

haltsum1, 0x13

3.12.15 Halt Summary 1 (haltsum1, at 0x13)

这个只读寄存器中的每个位表示一组(32个)hart是否停止。不可用/不存在的hart不被认为是停止的。

这个寄存器不能出现在少于33个hart的系统中。

LSB代表一组harthartsel[19:10],10'h0到harthartsel[19:10],10'h1f的暂停状态。MSB代表一组harthartsel[19:10],10'h3e0到harthartsel[19:10],10'h3ff的暂停状态。

haltsum2, 0x34

3.12.16 Halt Summary 2 (haltsum2, at 0x34)

这个只读寄存器中的每个位表示一组(1024个)hart是否停止。不可用/不存在的hart不被认为是停止的。

这个寄存器不能出现在少于1025个hart的系统中。

LSB代表一组harthartsel[19:15],15'h0到harthartsel[19:15],15'h3ff的暂停状态。MSB代表一组harthartsel[19:15],15'h7c00到harthartsel[19:15],15'h7fff的暂停状态。

haltsum3, 0x35

3.12.17 Halt Summary 3 (haltsum3, at 0x35)

这个只读寄存器中的每个位表示一组(32768个)hart是否停止。不可用/不存在的hart不被认为是停止的。

这个寄存器不能出现在少于32769个hart的系统中。

LSB代表一组hart20'h0到hart20'h7fff的暂停状态。MSB代表一组hart20'hf8000到hart20'hfffff的暂停状态。

sbcs, 0x38

3.12.18 System Bus Access Control and Status (sbcs, at 0x38)

sbaddress0, 0x39

3.12.19 System Bus Address 31:0 (sbaddress0, at 0x39)

如果sbasize为0,则这个寄存器不存在。

如果sberror为0,sbbusyerror为0,sbreadonaddr为1,然后开始向该寄存器写入以下内容:

  1. sbbusy置位。
  2. 从sbaddress的新值执行一次总线读取。
  3. 如果读成功且sbautoincrement为1,增加sbaddress。
  4. sbbusy清零。

sbaddress1, 0x3a

3.12.20 System Bus Address 63:32 (sbaddress1, at 0x3a)

如果sbasize少于33,则这个寄存器不存在。

sbaddress2, 0x3b

3.12.21 System Bus Address 95:64 (sbaddress2, at 0x3b)

如果sbasize少于65,则这个寄存器不存在。

sbaddress3, 0x37

3.12.22 System Bus Address 127:96 (sbaddress3, at 0x37)

如果sbasize少于97,则这个寄存器不存在。

sbdata0, 0x3c

3.12.23 System Bus Data 31:0 (sbdata0, at 0x3c)

如果sbc中的所有saccess位都是0,那么这个寄存器不存在。

对这个寄存器写入开始如下:

  1. sbbusy置位。
  2. 对sbdata的新值执行总线写入到sbaddress。
  3. 如果写成功且设置了sbautoincrement,增加sbaddress。
  4. sbbusy清零。

从这个寄存器读取开始如下:

  1. 返回数据。
  2. sbbusy置位。
  3. 如果设置了sbreadondata,则从saddress中包含的地址执行一次系统总线读取,将结果放入sbdata中。
  4. 如果设置了sbautoincrement,增加sbaddress。
  5. sbbusy清零。

sbdata1, 0x3d

3.12.24 System Bus Data 63:32 (sbdata1, at 0x3d)

如果sbaccess64和sbaccess128为0,那么这个寄存器不存在。

sbdata2, 0x3e

3.12.25 System Bus Data 95:64 (sbdata2, at 0x3e)

只有当sbaccess128为1时,这个寄存器才存在。

sbdata3, 0x3f

3.12.26 System Bus Data 127:96 (sbdata3, at 0x3f)

只有当sbaccess128为1时,这个寄存器才存在。

RISC-V调试

调试模式

调试模式是一种特殊的处理器模式,仅在hart暂停用于外部调试时使用。

当从可选的程序缓冲区执行代码时,hart保持在调试模式,并适用以下内容:

  1. 所有操作都在机器模式特权级别执行,除了根据mprven可以忽略处于状态的MPRV。
  2. 所有中断(包括NMI)被屏蔽。
  3. 异常不更新任何寄存器。包括cause, epc, tval, dpc和mstatus。它们终止程序缓冲区的执行。
  4. 如果一个触发器匹配上,则不采取任何操作。
  5. 计数器可能会停止,这取决于dcsr中的stopcount。
  6. 计时器可能会停止,这取决于dcsr中的stoptime。
  7. wfi指令起着nop的作用。
  8. 几乎所有改变特权级别的指令都有未定义的行为。这包括ecall、mret、sret和uret。(要更改特权级别,调试器可以在dcsr中写入prv)。唯一的例外是ebreak。当在调试模式下执行该命令时,它会再次停止hart,但不会更新dpc或dcsr。
  9. 完成Program Buffer的执行被认为是fence指令的输出。
  10. 如果所有的控制转移指令的目的地在程序缓冲区中,它们都可能被视为非法指令。如果其中一条指令是非法指令,那么所有这些指令都必须是非法指令。
  11. 如果控制转移指令的目的地在程序缓冲区之外,则所有控制转移指令都可能被视为非法指令。如果其中一条指令是非法指令,那么所有这些指令都必须是非法指令。
  12. 依赖于PC值的指令(例如auipc)可能作为非法指令。
  13. 有效的XLEN是DXLEN。

等待中断指令

如果在wfi执行时请求暂停,则hart必须离开已停止状态,完成该指令的执行,然后进入调试模式。

单步调试

调试器可以使暂停的hart执行一条指令,然后在设置resumereq之前通过设置step重新进入调试模式。

如果执行或获取该指令导致异常,则在PC更改为异常处理程序并更新相应的tval和cause寄存器后立即重新进入调试模式。

如果执行或获取指令导致触发器触发,则在触发器触发后立即重新进入调试模式。在这种情况下,cause被设置为2(trigger)而不是4(single step)。指令是否执行取决于触发器的具体配置。

如果被执行的指令导致PC改变到一个地址,在这个地址中指令获取会导致异常,那么这个异常直到下一次hart被恢复时才会发生。

类似地,新地址的触发器在hart实际尝试执行该指令之前不会触发。

如果正在跳过的指令是wfi并且通常会使hart停止,那么该指令将被视为nop。

重置

如果在hart从复位状态出来时断言了暂停信号(由hart的调试模块中的暂停请求位驱动)或resethaltreq, hart必须在执行任何指令之前进入调试模式,但在执行任何初始化之后,通常会在执行第一个指令之前发生

dret指令

为了从调试模式返回,定义了一条新指令:dret。编码为0x7b200073。在支持此指令的硬件上,在调试模式下执行dret会将pc更改为存储在dpc中的值。将当前权限级别更改为dcsr中prv指定的权限级别。hart不再处于调试模式。

核调试寄存器

地址名称
0x7b0Debug Control and Status (dcsr)
0x7b1Debug PC (dpc)
0x7b2Debug Scratch Register 0 (dscratch0)
0x7b3Debug Scratch Register 1 (dscratch1)

dcsr, 0x7b0

4.8.1 Debug Control and Status (dcsr, at 0x7b0)

dpc, 0x7b1

4.8.2 Debug PC (dpc, at 0x7b1)

dscratch0, 0x7b2

4.8.3 Debug Scratch Register 0 (dscratch0, at 0x7b2)

dscratch1, 0x7b3

4.8.4 Debug Scratch Register 1 (dscratch1, at 0x7b3)

虚拟调试寄存器

虚拟寄存器是一种不直接存在于硬件中的寄存器,但调试器将其显示为存在的寄存器。调试软件应该实现它们,但是硬件可以跳过这一节。虚拟寄存器的存在是为了让用户访问不属于标准调试器的功能,而不需要他们在调试器访问这些寄存器时仔细修改调试寄存器。

priv

4.9.1 Privilege Level (priv, at virtual)

用户可以读取此寄存器以检查hart停止时运行的特权级别。用户可以写这个寄存器来更改hart恢复时运行的特权级别。

编码特权等级
0User/Application
1Supervisor
3Machine

触发器模块

触发器可以导致断点异常、进入调试模式或跟踪操作,而无需执行特殊指令。这使得它们在从ROM中调试代码时非常宝贵。

触发器在调试模式下不会触发。

调试器可以构建所有触发器及其特性的列表,如下所示:

  1. 将0写入tselect。
  2. 回读tselect并检查它是否包含写入的值。如果不是,则退出循环。
  3. 读tinfo。
  4. 如果导致异常,调试器必须读取tdata1以发现类型。(如果type为0,则此触发器不存在。退出循环。)
  5. 如果info为1,则此触发器不存在。退出循环。
  6. 否则,所选触发器支持info中发现的类型。
  7. 重复,增加tselect中的值。

本地M-Mode触发器

触发器可用于本机调试。在一个功能齐全的系统上,触发器将使用u或s来设置,当触发它们时,可能会导致一个断点异常被捕获到一个更特权的模式。也可以将触发器本地设置为在M模式下触发。在这种情况下,没有更高的特权模式可以捕获。当这样的触发器在陷阱处理程序中导致断点异常时,这将使系统无法恢复正常执行。

简单的解决方案是在“M模式”和MIE在mstatus为0的状态下,让硬件防止触发器触发action=0。它的限制是,当用户想要触发触发器时,可能会在其他时间禁用中断。

触发器寄存器

这些寄存器是CSR,可以使用RISC-V csr操作码访问,也可以选择使用抽象调试命令。

action编码表

描述
0引发断点异常。(当软件想要在没有外部调试器的情况下使用触发模块时使用。)
1进入调试模式。(仅当触发器的dmode为1时支持。)
2 - 5保留供trace规范使用。
other保留以备将来使用。

触发器寄存器表

地址名称
0x7a0Trigger Select (tselect)
0x7a1Trigger Data 1 (tdata1)
0x7a1Match Control (mcontrol)
0x7a1Instruction Count (icount)
0x7a1Interrupt Trigger (itrigger)
0x7a1Exception Trigger (etrigger)
0x7a2Trigger Data 2 (tdata2)
0x7a3Trigger Data 3 (tdata3)
0x7a3Trigger Extra (RV32) (textra32)
0x7a3Trigger Extra (RV64) (textra64)
0x7a4Trigger Info (tinfo)
0x7a5Trigger Control (tcontrol)
0x7a8Machine Context (mcontext)
0x7aaSupervisor Context (scontext)

tselect, 0x7a0

5.2.1 Trigger Select (tselect, at 0x7a0)

这个寄存器确定哪个触发器可以通过其他触发器寄存器访问。可访问触发器集必须从0开始,并且是连续的。

tdata1, 0x7a1

5.2.2 Trigger Data 1 (tdata1, at 0x7a1)

tdata2, 0x7a2

5.2.3 Trigger Data 2 (tdata2, at 0x7a2)

tdata3, 0x7a3

5.2.4 Trigger Data 3 (tdata3, at 0x7a3)

tinfo, 0x7a4

5.2.5 Trigger Info (tinfo, at 0x7a4)

tcontrol, 0x7a5

5.2.6 Trigger Control (tcontrol, at 0x7a5)

mcontext, 0x7a8

5.2.7 Machine Context (mcontext, at 0x7a8)

scontext, 0x7aa

5.2.8 Supervisor Context (scontext, at 0x7aa)

mcontrol, 0x7a1

5.2.9 Match Control (mcontrol, at 0x7a1)

当type为2时,这个寄存器可以作为tdata1访问。

icount, 0x7a1

5.2.10 Instruction Count (icount, at 0x7a1)

当type为3时,这个寄存器可以作为tdata1访问。

itrigger, 0x7a1

5.2.11 Interrupt Trigger (itrigger, at 0x7a1)

当type为4时,这个寄存器可以作为tdata1访问。

etrigger, 0x7a1

5.2.12 Exception Trigger (etrigger, at 0x7a1)

当type为5时,这个寄存器可以作为tdata1访问。

textra32, 0x7a3

5.2.13 Trigger Extra (RV32) (textra32, at 0x7a3)

textra64, 0x7a3

5.2.14 Trigger Extra (RV64) (textra64, at 0x7a3)

调试传输模块DTM

调试传输模块通过一个或多个传输(例如JTAG或USB)提供对DM的访问。

单个平台中可能有多个dtm。理想情况下,与外部世界通信的每个组件都包含DTM,允许通过平台支持的每种传输对平台进行调试。例如,USB组件可以包含DTM。这样就可以轻松地通过USB调试任何平台。所需要的就是已经在使用的USB模块也可以访问调试模块接口。

JTAG调试传输模块

这个调试传输模块是基于一个正常的JTAG测试访问端口(TAP)。通过首先使用JTAG指令寄存器(IR)选择一个JTAG寄存器,然后通过JTAG数据寄存器(DR)访问它,JTAG TAP允许访问任意JTAG寄存器。

JTAG DTM寄存器

用作DTM的JTAG抽头必须具有至少5位的IR。当TAP复位时,IR必须默认为00001,选择IDCODE指令。调试器可能使用的唯一常规JTAG寄存器是BYPASS和IDCODE,但是该规范为许多其他标准JTAG指令留下了IR空间。未实现指令必须选择BYPASS寄存器。

JTAG DTM TAP寄存器表

地址名称描述
0x00BYPASSJTAG recommends this encoding
0x01IDCODEJTAG recommends this encoding
0x10DTM Control and Status (dtmcs)For Debugging
0x11Debug Module Interface Access (dmi)For Debugging
0x12Reserved (BYPASS)Reserved for future RISC-V debugging
0x13Reserved (BYPASS)Reserved for future RISC-V debugging
0x14Reserved (BYPASS)Reserved for future RISC-V debugging
0x15Reserved (BYPASS)Reserved for future RISC-V standards
0x16Reserved (BYPASS)Reserved for future RISC-V standards
0x17Reserved (BYPASS)Reserved for future RISC-V standards
0x1fBYPASSJTAG requires this encoding

IDCODE, 0x01

6.1.3 IDCODE (at 0x01)

当TAP状态机复位时,选择这个寄存器(在IR中)。其定义与IEEE标准1149.1-2013中的定义完全一致。

dtmcs, 0x10

6.1.4 DTM Control and Status (dtmcs, at 0x10)

在以后的版本中,这个寄存器的大小将保持不变,以便调试器始终可以确定DTM的版本。

dmi, 0x11

6.1.5 Debug Module Interface Access (dmi, at 0x11)

这个寄存器允许访问调试模块接口(DMI)。

在Update-DR中,DTM将启动op中指定的操作,除非op中报告的当前状态为sticky。 在Capture-DR中,DTM使用该操作的结果更新数据,如果当前操作不是sticky,则更新op。

BYPASS 0x1f

6.1.6 BYPASS (at 0x1f)

无效的1位寄存器。当调试器不想与此TAP通信时使用它。

FreeRTOS源码解析

基本配置

1
#define portSTACK_GROWTH			( -1 )

任务管理

任务控制块TCB

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
/*
* Task control block. A task control block (TCB) is allocated for each task,
* and stores task state information, including a pointer to the task's context
* (the task's run time environment, including register values)
*/
typedef struct tskTaskControlBlock /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
volatile StackType_t * pxTopOfStack; /**< Points to the location of the last item placed on the tasks stack. THIS MUST BE THE FIRST MEMBER OF THE TCB STRUCT. */

ListItem_t xStateListItem; /**< The list that the state list item of a task is reference from denotes the state of that task (Ready, Blocked, Suspended ). */
ListItem_t xEventListItem; /**< Used to reference a task from an event list. */
UBaseType_t uxPriority; /**< The priority of the task. 0 is the lowest priority. */
StackType_t * pxStack; /**< Points to the start of the stack. */
char pcTaskName[ configMAX_TASK_NAME_LEN ]; /**< Descriptive name given to the task when created. Facilitates debugging only. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */

UBaseType_t uxBasePriority; /**< The priority last assigned to the task - used by the priority inheritance mechanism. */
UBaseType_t uxMutexesHeld;

volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];

uint8_t ucDelayAborted;
} tskTCB;

/* The old tskTCB name is maintained above then typedefed to the new TCB_t name
* below to enable the use of older kernel aware debuggers. */
typedef tskTCB TCB_t;

动态创建任务

1
2
3
4
5
6
7
// tasks.c
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
const configSTACK_DEPTH_TYPE usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )

portSTACK_GROWTH < 0,栈向下增长;为避免栈增长到TCB中,需要先申请栈再申请TCB结构。

pxNewTCB->pxStack = pxStack;将栈赋值到TCB中。

prvInitialiseNewTask初始化TCB结构。

prvAddNewTaskToReadyList将任务添加到就绪列表中。

1
static void prvAddNewTaskToReadyList( TCB_t * pxNewTCB )

taskENTER_CRITICAL进入临界区,关闭中断。

prvAddTaskToReadyList将任务按优先级加入就绪队列中。

taskEXIT_CRITICAL退出临界区,开中断。

开始任务调度

1
2
// tasks.c
void vTaskStartScheduler( void )

xTaskCreate创建idle任务。

xTimerCreateTimerTask创建计时器任务。

portDISABLE_INTERRUPTS禁用中断。

xPortStartScheduler调用开启硬件调度接口,一般是启动tick计时器。

1
2
// port.c
BaseType_t xPortStartScheduler( void )

portDISABLE_INTERRUPTS禁用中断。

configSETUP_TICK_INTERRUPT配置tick计时器。

vPortRestoreTaskContext开始执行第一个任务。

汇编程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// portASM.S
/******************************************************************************
* vPortRestoreTaskContext is used to start the scheduler.
*****************************************************************************/
.align 8
.type vPortRestoreTaskContext, %function
vPortRestoreTaskContext:
.set freertos_vector_base, _freertos_vector_table

/* Install the FreeRTOS interrupt handlers. */
LDR X1, =freertos_vector_base
#if defined( GUEST )
MSR VBAR_EL1, X1
#else
MSR VBAR_EL3, X1
#endif
DSB SY
ISB SY

/* Start the first task. */
portRESTORE_CONTEXT

任务切换

MPS2_QEMU会调用汇编处理程序xPortPendSVHandler

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
void xPortPendSVHandler( void )
{
/* This is a naked function. */

__asm volatile
(
" mrs r0, psp \n"
" isb \n"
" \n"
" ldr r3, pxCurrentTCBConst \n"/* Get the location of the current TCB. */
" ldr r2, [r3] \n"
" \n"
" stmdb r0!, {r4-r11} \n"/* Save the remaining registers. */
" str r0, [r2] \n"/* Save the new top of stack into the first member of the TCB. */
" \n"
" stmdb sp!, {r3, r14} \n"
" mov r0, %0 \n"
" msr basepri, r0 \n"
" bl vTaskSwitchContext \n"
" mov r0, #0 \n"
" msr basepri, r0 \n"
" ldmia sp!, {r3, r14} \n"
" \n"/* Restore the context, including the critical nesting count. */
" ldr r1, [r3] \n"
" ldr r0, [r1] \n"/* The first item in pxCurrentTCB is the task top of stack. */
" ldmia r0!, {r4-r11} \n"/* Pop the registers. */
" msr psp, r0 \n"
" isb \n"
" bx r14 \n"
" \n"
" .align 4 \n"
"pxCurrentTCBConst: .word pxCurrentTCB \n"
::"i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY )
);
}

OpenAI辅助理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void PendSV_Handler(void) {
/* 保存当前任务的上下文 */
__asm volatile (
"MRS R0, PSP \n" /* 获取进程堆栈指针 */
"STMDB R0!, {R4-R11} \n" /* 保存R4-R11寄存器到堆栈 */
"LDR R1, =pxCurrentTCB \n" /* 加载当前TCB指针地址 */
"LDR R2, [R1] \n" /* 加载当前TCB指针 */
"STR R0, [R2] \n" /* 保存堆栈指针到当前TCB */
);

/* 切换到下一个任务 */
vTaskSwitchContext();

/* 恢复下一个任务的上下文 */
__asm volatile (
"LDR R1, =pxCurrentTCB \n" /* 加载当前TCB指针地址 */
"LDR R2, [R1] \n" /* 加载当前TCB指针 */
"LDR R0, [R2] \n" /* 加载堆栈指针 */
"LDMIA R0!, {R4-R11} \n" /* 恢复R4-R11寄存器 */
"MSR PSP, R0 \n" /* 恢复进程堆栈指针 */
"BX LR \n" /* 返回任务 */
);
}

然后调用切换上下文函数

1
2
// task.c
void vTaskSwitchContext( void )

taskSELECT_HIGHEST_PRIORITY_TASK选择最高优先级任务

时钟管理

时钟节拍

MPS2_QEMU会调用处理程序xPortSysTickHandler

1
2
// port.c
void xPortSysTickHandler( void )

portDISABLE_INTERRUPTS关闭中断。

xTaskIncrementTick尝试增加tick值,成功后触发PendSV中断,强行进行一次调度。

portENABLE_INTERRUPTS打开中断。

1
2
// tasks.c
BaseType_t xTaskIncrementTick( void )

taskSWITCH_DELAYED_LISTS系统tick值反转时,将延时任务队列与反转反转队列互换。

判断是否有超时的任务,并将其移入就绪队列中。

任务延时

1
void vTaskDelay( const TickType_t xTicksToDelay )

vTaskSuspendAll关闭调度器。

prvAddCurrentTaskToDelayedList将当前任务加到延时列表中。

xTaskResumeAll恢复调度器。

portYIELD_WITHIN_API当恢复调度时未进行再次调度,则强制进行调度并让任务进入睡眠状态。

1
2
3
static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait,
const BaseType_t xCanBlockIndefinitely )

uxListRemove将当前任务移除队列中。

xTimeToWake = xConstTickCount + xTicksToWait计算下次唤醒的时刻。需要考虑时间反转的场景。

基本信息

OpenOCD(Open On-Chip Debugger)

提供依赖JTAG接口的分层架构的片上编程和调试支持和TAP支持,包括:

  • (X)SVF回放,便于自动边界扫描和FPGA/CPLD编程;
  • 调试目标支持(如ARM、MIPS):单步调试、断点/监控点、gprof性能分析等;
  • Flash芯片驱动(如CFI、NAND、内部闪存);
  • 内嵌TCL解释器,方便脚本编写;

OpenOCD支持众多网络接口与之交互:包括:telnet、TCL和GDB。GDB server可以使得OpenOCD作为“远程目标”,使用标准的GNU GDB程序(和其他使用GDB协议的工具,如IDA Pro)源码级调试嵌入式系统。

官方手册

OpenOCD用户手册

OpenOCD开发手册

工程编译

源码解析

自动生成配置

配置命令

Linux环境中,执行./configure xxx配置后会返回

1
2
3
4
5
6
7
8
9
$ ./configure --prefix=/usr/local/OpenOCD --datarootdir=/usr/local/OpenOCD --enable-ftdi=no --enable-remote-bitbang=no --enable-ep93xx=no --enable-amtjtagaccel=no --enable-jtag_dpi=no --enable-vdebug=no --enable-jtag_vpi=no --enable-buspirate=no --enable-openjtag=no --enable-presto=no --enable-stlink=no --enable-ti-icdi=no --enable-ulink=no --enable-usb-blaster-2=no --enable-ft232r=no --enable-vsllink=no --enable-xds110=no --enable-osbdm=no --enable-opendous=no --enable-armjtagew=no --enable-rlink=no --enable-usbprog=no --enable-esp-usb-jtag=no --enable-nulink=no --enable-kitprog=no --enable-usb-blaster=no --enable-parport-giveio=no --disable-werror --enable-dummy=yes

OpenOCD configuration summary
--------------------------------------------------
Linux GPIO bitbang through libgpiod no
SEGGER J-Link Programmer yes (auto)
Dummy Adapter yes
RISC-V Link Adapter yes
Use Capstone disassembly framework no

新增rvlink适配器

configure.ac中新增RVLINK_ADAPTER配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
m4_define([DUMMY_ADAPTER],
[[[dummy], [Dummy Adapter], [DUMMY]]])

m4_define([RVLINK_ADAPTER],
[[[rvlink], [RISC-V Link Adapter], [RVLINK]]])
...
AC_ARG_ADAPTERS([DUMMY_ADAPTER],[no])

AC_ARG_ADAPTERS([RVLINK_ADAPTER],[yes])
...
PROCESS_ADAPTERS([DUMMY_ADAPTER], [true], [unused])
PROCESS_ADAPTERS([RVLINK_ADAPTER], [true], [unused])
...
m4_foreach([adapter], [USB1_ADAPTERS,
HIDAPI_ADAPTERS, HIDAPI_USB1_ADAPTERS, LIBFTDI_ADAPTERS,
LIBFTDI_USB1_ADAPTERS,
LIBGPIOD_ADAPTERS,
LIBJAYLINK_ADAPTERS, PCIE_ADAPTERS, SERIAL_PORT_ADAPTERS,
DUMMY_ADAPTER,
RVLINK_ADAPTER,
...

例如,对于rvlink适配器,其名称是RISC-V Link Adapter,编译宏为BUILD_RVLINK

JTAG驱动编控制文件src/jtag/drivers/Makefile.am中可以使用编译宏RVLINK控制编译的代码。

1
2
3
4
5
6
if DUMMY
DRIVERFILES += %D%/dummy.c
endif
if RVLINK
DRIVERFILES += %D%/rvlink.c
endif

Linux环境中编译会包含rvlink.c文件。

src/jtag/interfaces.c文件适配器驱动列表新增适配器。

1
2
3
4
5
6
7
8
struct adapter_driver *adapter_drivers[] = {
#if BUILD_DUMMY == 1
&dummy_adapter_driver,
#endif
#if BUILD_RVLINK == 1
&rvlink_adapter_driver,
#endif
};

数据结构

调试目标target

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/target/target.h
struct target {
struct target_type *type; // 包含目标名称和访问函数
struct jtag_tap *tap; // jtag扫描链
bool examined; // 检查目标标记
enum target_debug_reason debug_reason; // 目标进入调试状态原因枚举
enum target_state state; // 当前目标状态
struct reg_cache *reg_cache; // 寄存器缓存
struct breakpoint *breakpoints; // bp断点列表
struct watchpoint *watchpoints; // wp断点列表
struct trace *trace_info; // 跟踪信息
void *arch_info; // 架构信息,常用于target与架构互转
void *private_config; // 配置信息
struct rtos *rtos; // RTOS信息
struct list_head *smp_targets; // SMP列表节点
struct gdb_service *gdb_service; // SMP场景,所有调试目标使用相同的GDB
};

调试目标列表

1
2
3
4
5
6
// src/target/target.c
static struct target_type *target_types[] = {
&stm8_target,
&riscv_target,
NULL,
};

对于ARM架构处理器,可以通过container_of(target->arch_info, struct cortex_a_common, armv7a_common.arm),将target转换为cortex_a_common,然后可以获取ADI版本版本。

1
2
3
4
// struct adiv5_dap->adi_version
struct cortex_a_common *cortex_a = target_to_cortex_a(target);
struct armv7a_common *armv7a = &cortex_a->armv7a_common;
armv7a->debug_ap->dap->adi_version == 5; /** Indicates ADI version (5, 6 or 0 for unknown) being used */

适配器adapter

1
2
3
4
5
6
7
8
9
10
// icd_openocd/src/jtag/interface.h
struct adapter_driver {
const char * const *transports; // 传输列表
const struct command_registration *commands; // 命令列表
// JTAG设备操作接口
int (*init)(void); // 初始化资源
int (*reset)(int srst, int trst); // 控制设备SRST和TRST信号

struct jtag_interface *jtag_ops; // 低等级JTAG接口
};

JTAG接口

1
2
3
4
5
// icd_openocd/src/jtag/interface.h
struct jtag_interface {
unsigned supported; // 驱动支持能力的位表
int (*execute_queue)(struct jtag_command *cmd_queue); // 执行队列中的命令
};

服务service

服务service,主要用于管理服务。在创建服务时会复制对应的服务驱动接口。

1
2
3
4
5
6
7
8
// src/server/server.h
struct service {
char *name;
struct connection *connections;
int (*new_connection)(struct connection *connection);
int (*input)(struct connection *connection);
void (*keep_client_alive)(struct connection *connection);
};

服务驱动service_driver,主要定义基本结构

1
2
3
4
5
6
7
// src/server/server.h
struct service_driver {
const char *name;
int (*new_connection_handler)(struct connection *connection);
int (*input_handler)(struct connection *connection);
void (*keep_client_alive_handler)(struct connection *connection);
};

GDB服务驱动

1
2
3
4
5
6
7
8
9
// src/server/gdb_server.c
static const struct service_driver gdb_service_driver = {
.name = "gdb",
.new_connection_during_keep_alive_handler = NULL,
.new_connection_handler = gdb_new_connection,
.input_handler = gdb_input,
.connection_closed_handler = gdb_connection_closed,
.keep_client_alive_handler = gdb_keep_client_alive,
};

连接connection

1
2
3
4
5
6
7
8
9
10
11
// src/server/server.h
struct connection {
int fd;
int fd_out; /* When using pipes we're writing to a different fd */
struct sockaddr_in sin;
struct command_context *cmd_ctx;
struct service *service;
bool input_pending;
void *priv;
struct connection *next;
};

启动OpenOCD

运行命令./src/openocd -s tcl -f tcl/interface/cmsis-dap.cfg -f tcl/target/xxxx.cfg启动OpenOCD。

配置文件解析

适配器驱动处理,各tcl配置文件

1
2
3
4
5
6
7
8
9
10
// 适配器驱动文件 tcl/interface/cmsis-dap.cfg
adapter driver cmsis-dap
transport select jtag
adapter speed 2000

// 目标配置文件 tcl/target/xxxx.cfg
telnet_port 4444
gdb_port 3333
jtag newtap $_CHIPNAME cpu -enable -irlen 32
target create $_TARGETNAME riscv -chain-position $_TARGETNAME -coreid $_HARTID

解析tcl配置文件主流程

1
2
3
4
5
6
7
8
// 主流程 src/openocd.c
openocd_main
setup_command_handler // 注册命令处理函数
adapter_register_commands // 注册adapter命令处理服务
openocd_thread
parse_config_file // 解析配置文件
// 处理 tcl/interface/cmsis-dap.cfg 配置文件
// 处理 tcl/target/xxxx.cfg

adapter命令匹配"cmsis-dap"流程

1
2
3
4
// adapter命令回调,适配器配置解析匹配名称 src/jtag/adapter.c
interface_command_handlers // 匹配命令adapter
adapter_command_handlers // 匹配命令driver
handle_adapter_driver_command // 若驱动名称匹配,则切换adapter_driver,注册驱动的命令列表并更新allowed_transports列表

适配器列表

1
2
3
4
5
6
7
8
9
10
// 适配器驱动列表 src/jtag/interfaces.c
struct adapter_driver *adapter_drivers[] = {
#if BUILD_CMSIS_DAP_USB == 1 || BUILD_CMSIS_DAP_HID == 1
&cmsis_dap_adapter_driver,
#endif
};
// "cmsis-dap"适配器驱动
struct adapter_driver cmsis_dap_adapter_driver = {
.name = "cmsis-dap",
};

其他命令处理

1
2
3
4
// 解析目标配置文件,处理文件中各行命令
// 处理命令telnet_port,handle_telnet_port_command设置telnet_port
// 处理命令gdb_port,handle_gdb_port_command设置gdb_port
// 处理命令transport select,handle_transport_select在allowed_transports列表中匹配传输

主函数流程概览

openocd_main函数初始化逻辑

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
// src/openocd.c
int openocd_main(int argc, char *argv[])
{
...
// 注册各命令的处理函数
struct command_context *cmd_ctx;
cmd_ctx = setup_command_handler(NULL);
...
ret = openocd_thread(argc, argv, cmd_ctx);
...
}

// src/openocd.c
static int openocd_thread(int argc, char *argv[], struct command_context *cmd_ctx)
{
...
// 解析命令行参数
parse_cmdline_args(cmd_ctx, argc, argv);
...
// 解析配置文件,对调试器进行配置
ret = parse_config_file(cmd_ctx);
...
ret = server_init(cmd_ctx);
...
ret = command_run_line(cmd_ctx, "init");
...
ret = server_loop(cmd_ctx);
}

server_init函数初始化逻辑

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
// src/server/server.c
int server_init(struct command_context *cmd_ctx)
{
int ret = tcl_init();
ret = telnet_init("Open On-Chip Debugger");
}

// src/server/tcl_server.c
int tcl_init(void)
{
return add_service(&tcl_service_driver, tcl_port, CONNECTION_LIMIT_UNLIMITED, NULL);
}
static const struct service_driver tcl_service_driver = {
.name = "tcl",
.new_connection_during_keep_alive_handler = NULL,
.new_connection_handler = tcl_new_connection,
.input_handler = tcl_input,
.connection_closed_handler = tcl_closed,
.keep_client_alive_handler = NULL,
};

// src/server/telnet_server.c
int telnet_init(char *banner)
{
int ret = add_service(&telnet_service_driver, telnet_port, CONNECTION_LIMIT_UNLIMITED,
telnet_service);
}
static const struct service_driver telnet_service_driver = {
.name = "telnet",
.new_connection_during_keep_alive_handler = NULL,
.new_connection_handler = telnet_new_connection,
.input_handler = telnet_input,
.connection_closed_handler = telnet_connection_closed,
.keep_client_alive_handler = NULL,
};

server_loop主循环

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/server/server.c
int server_loop(struct command_context *command_context)
{
// 循环处理services中所有服务的连接列表
while(shutdown_openocd == CONTINUE_MAIN_LOOP)
{
// socket_select多路复用监听fd
// 轮询监听间隔为100ms
// 若服务有数据(service->fd),则接受连接并创建新的连接
// 若连接有数据(c->fd),则调用service->input进行处理
...
}
}

RISC-V架构目标示例

NEMU是RISC-V架构处理器硬件模拟器,OpenOCD通过TCP/51234接口连接它。

启动命令:./src/openocd -s tcl -f tcl/interface/rvlink.cfg -f tcl/target/simu/nemu.cfg

TCL配置文件

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
// tcl/interface/rvlink.cfg
adapter driver RV-LINK
transport select jtag
rvlink connect 127.0.0.1 51234

// tcl/target/simu/nemu.cfg
if { ![info exists _SMP_MODE] } {
set _SMP_MODE 1
}
if {![info exists _HARTID]} {
set _HARTID 0x00
}
if {![info exists _CHIPNAME]} {
set _CHIPNAME nemu
}
set _TARGETNAME $_CHIPNAME.cpu
adapter speed 2000
telnet_port 4444
gdb_port 3333
jtag newtap $_CHIPNAME cpu -enable -irlen 32
set _SMP_GROUP ""
target create $_TARGETNAME riscv -chain-position $_TARGETNAME -coreid $_HARTID
set _SMP_GROUP "$::_SMP_GROUP $_TARGETNAME"
if { $_SMP_MODE == 1 } {
eval "target smp $_SMP_GROUP"
}

成功启动并连接时的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ ./src/openocd -s tcl -f tcl/interface/rvlink.cfg -f tcl/target/simu/nemu.cfg
Open On-Chip Debugger 0.12.0+dev-03889-g5fefbc2da-dirty (2024-09-10-10:57)
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : Connected to server 127.0.0.1:51234
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : Note: The adapter "RV-LINK" doesn't support configurable speed
Info : JTAG tap: nemu.cpu tap/device found: 0x3ba0184d (mfg: 0x426 (Google Inc), part: 0xba01, ver: 0x3)
Info : [nemu.cpu] datacount=1 progbufsize=0
Warn : [nemu.cpu] We won't be able to execute fence instructions on this target. Memory may not always appear consistent. (progbufsize=0, impebreak=0)
Info : [nemu.cpu] Vector support with vlenb=0
Info : [nemu.cpu] S?aia detected with IMSIC
Info : [nemu.cpu] Core 0 made part of halt group 1.
Info : [nemu.cpu] Examined RISC-V core
Info : [nemu.cpu] XLEN=32, misa=0x40000000
[nemu.cpu] Target successfully examined.
Info : [nemu.cpu] Examination succeed
Info : [nemu.cpu] starting gdb server on 3333
Info : Listening on port 3333 for gdb connections

rvlink.cfg文件

adapter命令匹配"RV-LINK"流程

1
2
3
4
// adapter命令回调,适配器配置解析匹配名称 src/jtag/adapter.c
interface_command_handlers // 匹配命令adapter
adapter_command_handlers // 匹配命令driver
handle_adapter_driver_command // 若驱动名称匹配,则切换adapter_driver,注册驱动的命令列表并更新allowed_transports列表

适配器列表

1
2
3
4
5
6
7
8
9
10
// 适配器驱动列表 src/jtag/interfaces.c
struct adapter_driver *adapter_drivers[] = {
#if BUILD_RVLINK == 1
&rvlink_adapter_driver,
#endif
};
// "RV-LINK"适配器驱动 src/jtag/drivers/rvlink.c
struct adapter_driver rvlink_adapter_driver = {
.name = "RV-LINK",
};

transport命令选择jtag流程

1
2
3
4
5
// transport命令回调 src/transport/transport.c
transport_group // 匹配命令transport
transport_commands // 匹配命令select
handle_transport_select // 若名称匹配,则更新transport
transport_select // 调用select回调函数,并更新session

内置jtag传输

1
2
3
4
5
6
// src/jtag/core.c
static struct transport jtag_transport = {
.name = "jtag",
.select = jtag_select,
.init = jtag_init,
};

rvlink命令执行流程

1
2
3
4
// src/jtag/drivers/rvlink.c
rvlink_commands // 匹配命令rvlink
rvlink_subcommands // 匹配命令connect
rvlink_connect // 为目标建立TCP连接

nemu.cfg文件

telnet_port命令修改telnet端口

1
2
3
4
// src/server/telnet_server.c
telnet_command_handlers // 匹配命令telnet_port
handle_telnet_port_command // 直接调用函数
server_pipe_command // 修改telnet_port字符串的值

gdb_port命令修改gdb远程连接端口

1
2
3
// src/server/gdb_server.c
gdb_command_handlers // 匹配gdb_port
handle_gdb_port_command // 修改gdb_port字符串的值,并同步更新gdb_port_next

jtag命令创建新的tap

1
2
3
4
5
6
7
// jtag newtap nemu cpu -enable -irlen 32
// src/jtag/tcl.c
jtag_command_handlers // 匹配jtag
jtag_subcommand_handlers // 匹配newtap
handle_jtag_newtap // 创建nemu.cpu的tap对象
handle_jtag_newtap_args // 初始化tap对象,名称、ir配置
jtag_tap_init // 配置tap并将其加入tap列表__jtag_all_taps

target命令创建连接目标

1
2
3
4
5
6
// target create nemu.cpu riscv -chain-position nemu.cpu -coreid 0x00
// src/target/target.c
target_command_handlers // 匹配target
target_subcommand_handlers // 匹配create
jim_target_create // 调整参数并调用函数
target_create // 创建并匹配"riscv"调试目标,注册调试命令,更新all_targets

target命令设置smp

1
2
3
4
5
// target smp nemu.cpu
// src/target/target.c
target_command_handlers // 匹配target
target_subcommand_handlers // 匹配smp
handle_target_smp // 将所有target加入到smp列表中,每个组的smp编号从1开始

初始化命令

init命令初始化openocd,将会通过command_run_line函数触发一系列初始化函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/openocd.c
openocd_command_handlers // 匹配init
handle_init_command // 执行初始化函数。执行过程中jtag_poll_en为false,执行完成后恢复。
// target init命令
adapter_init // 适配器初始化 src/jtag/adapter.c
adapter_driver_gpios_init // 适配器驱动通用IO口初始化
// 特定(示例的rvlink)适配器初始化接口
// transport init命令
// dap init命令
target_examine // 循环检查调试目标列表;
// 延时检查
target_examine_one // 单次检查
target->type->examine(target) // 调用特定(示例为riscv)架构对应的检查接口
// flash init命令
// nand init命令
// pld init命令
// tpiu init命令
gdb_target_add_all // 循环调试目标列表,启动对应的GDB server
// _run_post_init_commands命令

target命令初始化所有调试目标

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
// target init 
// src/target/target.c
target_command_handlers // 匹配target
target_subcommand_handlers // 匹配init
handle_target_init_command // 直接调用startup.tcl脚本命令 init_targets init_target_events init_board

// init_targets 初始化函数为空
// src/target/startup.tcl
proc init_targets {} {
}

// init_target_events // 循环遍历目标列表,通过set_default_target_event为所有目标设置默认事件命令
// src/target/startup.tcl
proc init_target_events {} {
set targets [target names] // 获取到所有调试目标名称
foreach t $targets {
set_default_target_event $t gdb-flash-erase-start "reset init"
set_default_target_event $t gdb-flash-write-end "reset halt"
set_default_target_event $t gdb-attach "halt 1000"
}
}

// init_board 初始化函数为空
// src/target/startup.tcl
proc init_board {} {
}

transport传输初始化

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
// transport init
// src/transport/transport.c
transport_group // 匹配transport
transport_commands // 匹配init
handle_transport_init // 校验是否设置session变量,并调用特定(示例为jtag)的初始化接口

// jtag适配器初始化
// src/jtag/core.c
jtag_init // 初始化jtag适配器并执行jtag_init命令

// jtag_init
// src/jtag/startup.tcl
proc jtag_init {} {
if {[catch {jtag arp_init} err]!=0} {
# try resetting additionally
init_reset startup
}
}

// jtag arp_init
// src/jtag/tcl.c
jtag_command_handlers // 匹配jtag
jtag_subcommand_handlers // 匹配arp_init
handle_jtag_arp_init // 直接调用内部函数
jtag_init_inner // 执行jtag tap等初始化

dap初始化处理逻辑

1
2
3
4
5
6
// dap init
// src/target/arm_dap.c
dap_commands // 匹配dap
dap_subcommand_handlers // 匹配init
handle_dap_init // 直接调用函数
dap_init_all // 初始化所有dap对象

简单命令:flash init nand init pld init tpiu init

_run_post_init_commands初始化结束脚本命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/helper/startup.tcl
// 循环处理post_init_commands列表
proc _run_post_init_commands {} {
if {[info exists ::post_init_commands]} {
foreach cmd $::post_init_commands {
eval $cmd
}
}
}

// src/target/startup.tcl
// 将以下脚本函数加入到post_init_commands列表
lappend post_init_commands _post_init_target_array_mem
lappend post_init_commands _post_init_target_cortex_a_cache_auto

GDB交互

GDB连接处理

server_loop主循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/server/server.c
int server_loop(struct command_context *command_context)
{
// 循环处理services中所有服务的连接列表
while(shutdown_openocd == CONTINUE_MAIN_LOOP)
{
socket_select(fd_max + 1, &read_fds, NULL, NULL, &tv); // select处理所有服务连接,默认超时polling_period为100ms
target_call_timer_callbacks(); // 调试目标计时器回调;会调用keep_alive和server_keep_clients_alive处理各服务的保活逻辑
process_jim_events(command_context); // jim事件处理函数

// 循环遍历所有服务
for (service = services; service; service = service->next) {
// 若服务连接有数据,则创建连接
if (FD_ISSET(service->fd, &read_fds)) {
add_connection(service, command_context);
}
// 循环遍历服务下所有客户端连接,调用服务输入处理接口
for (c = service->connections; c; ) {
service->input(c);
}
}
}
}

创建连接

服务创建连接,并将其加入到服务连接队列中

1
2
3
4
5
6
// src/server/server.c
add_connection
// 创建connection对象
accept(service->fd, (struct sockaddr *)&service->sin, &address_size);
service->new_connection // gdb服务创建连接回调
// connection对象加入到service->connections尾部

GDB创建连接

1
2
3
4
5
6
7
8
// src/server/gdb_server.c
// service->new_connection
gdb_service_driver
gdb_new_connection // 初始化gdb连接目标
// 创建gdb连接对象
breakpoint_clear_target(target);
watchpoint_clear_target(target);
target_call_event_callbacks(target, TARGET_EVENT_GDB_ATTACH);

输入处理

GDB输入处理

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/server/gdb_server.c
// service->input
gdb_service_driver
gdb_input // gdb输入处理回调函数,直接调用函数
gdb_input_inner // gdb输入处理函数
gdb_get_packet // 从连接获取协议包
gdb_get_packet_inner // 从连接中读取数据包,若成功则回复ACK('+')
gdb_get_char // 读取数据包,返回第一个字符
fetch_packet // 解析剩余数据,并计算校验和;当切换到noack模式,则不会校验校验和,并跳过回复ACK('+')
/*
根据packet[0]第一个字符,执行对应的处理函数
一般小写为读操作函数,大写为写操作函数
*/

GDB远程协议

具体协议需要参考GDB远程协议章节(Appendix E gdb Remote Serial Protocol)。

连接OpenOCD

GDB执行命令连接OpenOCD。执行GDB命令set debug remote 1打开远程调试后,再连接远程目标可以看到通信日志。

1
2
(gdb) target remote :3333
0x80000004 in ?? ()

qSupported

探测OpenOCD支持哪些特性

1
2
3
4
5
6
7
8
9
  [remote] Sending packet: $qSupported:multiprocess+;swbreak+;hwbreak+;qRelocInsn+;fork-events+;vfork-events+;exec-events+;vContSupported+;QThreadEvents+;no-resumed+;memory-tagging+;xmlRegisters=i386#77
[remote] Received Ack
[remote] Packet received: PacketSize=4000;qXfer:memory-map:read-;qXfer:features:read+;qXfer:threads:read+;QStartNoAckMode+;vContSupported+
[remote] packet_ok: Packet qSupported (supported-packets) is supported

// src/server/gdb_server.c
gdb_query_packet // 处理q/Q消息
xml_printf // xml格式的回复消息
gdb_put_packet // 回复消息

vMustReplyEmpty

1
2
3
4
5
6
7
8
  [remote] Sending packet: $vMustReplyEmpty#3a
[remote] Received Ack
[remote] Packet received:

// src/server/gdb_server.c
gdb_v_packet // 处理v消息
// 无法匹配中任何命令
gdb_put_packet(connection, "", 0) // 直接回复空消息

QStartNoAckMode

设置不回复ACK模式

1
2
3
4
5
6
7
  [remote] Sending packet: $QStartNoAckMode#b0
[remote] Received Ack
[remote] Packet received: OK

// src/server/gdb_server.c
gdb_query_packet // 处理q/Q消息
gdb_put_packet // 回复OK

Hgxxx

1
2
3
4
5
6
7
8
  [remote] Sending packet: $Hg0#df
[remote] Packet received: OK

// src/server/gdb_server.c
gdb_thread_packet // 处理H消息
rtos_thread_packet // rtos线程包处理
// 若调试目标存在rtos,则设置当前线程
gdb_put_packet // 回复OK

qXfer:features:read

qXfer:features:read:target.xml:<offset,length>命令分页读取xml格式的架构描述文件,详细信息见riscv描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  [remote] Sending packet: $qXfer:features:read:target.xml:0,1000#0c
[remote] Packet received: m<?xml version="1.0"?>\n<!DOCTYPE target SYSTEM "gdb-target.dtd">\n<target version="1.0">\n<architecture>riscv:rv32</architecture>\n<feature name="org.gnu.gdb.riscv.cpu">\n<reg name="zero" bitsize="32" regnum="0" save-restore="yes" type="int" group="general"/>\n<reg name="ra" bitsize="32" regnum="1" save-restore="yes" type="int" group="general"/>\n<reg name="sp" bitsize="32" regnum="2" save-restore="yes" type="int" group="general"/>\n<reg name="gp" bitsize="32" regnum="3" save-restore="yes" type="int" group="genera [3585 bytes omitted]
...
[remote] Sending packet: $qXfer:features:read:target.xml:9000,1000#a5
[remote] Packet received: l type="int" group="csr"/>\n<reg name="hpmcounter27h" bitsize="32" regnum="3292" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter28h" bitsize="32" regnum="3293" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter29h" bitsize="32" regnum="3294" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter30h" bitsize="32" regnum="3295" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter31h" bitsize="32" regnum="3296" save-restore="no" type="int" group="csr"/>\n<reg [882 bytes omitted]

// src/server/gdb_server.c
gdb_query_packet // 处理q/Q消息
decode_xfer_read // 解析qXfer命令
gdb_get_target_description_chunk // 架构描述文件分块处理
gdb_generate_target_description // xml格式打印完整的架构描述文件
smp_reg_list_noread // 读取架构完整的寄存器列表
get_reg_features_list // 获取架构寄存器特性列表
xml_printf // 循环打印xml格式的描述文件
gdb_put_packet(connection, xml, strlen(xml)) // 回复架构描述文件xml数据;第一个字符为'm'表示后面有分页;第一个字符为'l'表示最后一页;

qTStatus

1
2
3
4
5
6
7
  [remote] Sending packet: $qTStatus#49
[remote] Packet received:

// src/server/gdb_server.c
gdb_query_packet // 处理q/Q消息
// 无法匹配中任何命令
gdb_put_packet(connection, "", 0) // 直接回复空消息

?

获取进入调试模式的原因对应的信号量

1
2
3
4
5
6
7
  [remote] Sending packet: $?#3f
[remote] Packet received: S00

// src/server/gdb_server.c
gdb_last_signal_packet 处理?消息
gdb_last_signal // 获取上次收到gdb的信号量(根据进入调试模式的原因转换)
gdb_put_packet(connection, sig_reply, 3); // 回复对应的信号量消息

qXfer:threads:read

获取线程列表

1
2
3
4
5
6
7
8
9
  [remote] Sending packet: $qXfer:threads:read::0,1000#92
[remote] Packet received: l<?xml version="1.0"?>\n<threads>\n</threads>\n

// src/server/gdb_server.c
gdb_query_packet // 处理q/Q消息
decode_xfer_read // 解析qXfer命令
gdb_get_thread_list_chunk // 线程列表线程信息分块处理
gdb_generate_thread_list // xml格式打印完整的线程列表信息
gdb_put_packet(connection, xml, strlen(xml)); // 回复线程列表信息xml数据;第一个字符为'm'表示后面有分页;第一个字符为'l'表示最后一页;

Hc-xxx

1
2
3
4
5
6
7
  [remote] Sending packet: $Hc-1#09
[remote] Packet received: OK

// src/server/gdb_server.c
gdb_thread_packet // 处理H消息
rtos_thread_packet // rtos线程包处理
gdb_put_packet // 回复OK

qC

1
2
3
4
5
6
  [remote] Sending packet: $qC#b4
[remote] Packet received: QC0

// src/server/gdb_server.c
rtos_thread_packet // 处理q消息
gdb_put_packet(connection, "QC0", 3); // 无rtos,直接回复QC0

qAttached

1
2
3
4
5
6
7
  [remote] Sending packet: $qAttached#8f
[remote] Packet received: 1
[remote] packet_ok: Packet qAttached (query-attached) is supported

// src/server/gdb_server.c
rtos_thread_packet // 处理q消息
gdb_put_packet(connection, "1", 1); // 直接回复"1"表示支持

g

获取通用寄存器列表

1
2
3
4
5
6
7
8
9
  [remote] Sending packet: $g#67
[remote] Packet received: 000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000080

// src/server/gdb_server.c
gdb_get_registers_packet // 处理g消息
target_get_gdb_reg_list // 获取特定架构寄存器列表
gdb_get_reg_value_as_str // 循环遍历所有寄存器,将寄存器的值格式化为字符串
gdb_str_to_target // 格式化寄存器的值,每个字节格式化为十六进制2位字符
gdb_put_packet(connection, reg_packet, reg_packet_size); // 返回寄存器值字符化后的数据

mxxx

m<address>,<length>读取指定地址的内存数据。默认分别读取当前PC和上一次PC对应的指令。

1
2
3
4
5
6
7
8
9
  [remote] Sending packet: $m80000004,4#59
[remote] Packet received: 93051005
[remote] Sending packet: $m80000000,4#55
[remote] Packet received: 13050005

// src/server/gdb_server.c
gdb_read_memory_packet // 处理m消息
target_read_buffer // 读取特定架构内存数据
gdb_put_packet // 返回内存数据字符化后的数据

附件

GDB通信日志

GDB远程连接日志

执行GDB命令set debug remote 1打开远程调试后,再连接远程目标可以看到通信日志

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
(gdb) target remote :3333
Remote debugging using :3333
[remote] start_remote_1: enter
[remote] Sending packet: $qSupported:multiprocess+;swbreak+;hwbreak+;qRelocInsn+;fork-events+;vfork-events+;exec-events+;vContSupported+;QThreadEvents+;no-resumed+;memory-tagging+;xmlRegisters=i386#77
[remote] Received Ack
[remote] Packet received: PacketSize=4000;qXfer:memory-map:read-;qXfer:features:read+;qXfer:threads:read+;QStartNoAckMode+;vContSupported+
[remote] packet_ok: Packet qSupported (supported-packets) is supported
[remote] Sending packet: $vMustReplyEmpty#3a
[remote] Received Ack
[remote] Packet received:
[remote] Sending packet: $QStartNoAckMode#b0
[remote] Received Ack
[remote] Packet received: OK
[remote] Sending packet: $Hg0#df
[remote] Packet received: OK
[remote] Sending packet: $qXfer:features:read:target.xml:0,1000#0c
[remote] Packet received: m<?xml version="1.0"?>\n<!DOCTYPE target SYSTEM "gdb-target.dtd">\n<target version="1.0">\n<architecture>riscv:rv32</architecture>\n<feature name="org.gnu.gdb.riscv.cpu">\n<reg name="zero" bitsize="32" regnum="0" save-restore="yes" type="int" group="general"/>\n<reg name="ra" bitsize="32" regnum="1" save-restore="yes" type="int" group="general"/>\n<reg name="sp" bitsize="32" regnum="2" save-restore="yes" type="int" group="general"/>\n<reg name="gp" bitsize="32" regnum="3" save-restore="yes" type="int" group="genera [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:1000,1000#9d
[remote] Packet received: m="sstateen0" bitsize="32" regnum="333" save-restore="no" type="int" group="csr"/>\n<reg name="sstateen1" bitsize="32" regnum="334" save-restore="no" type="int" group="csr"/>\n<reg name="sstateen2" bitsize="32" regnum="335" save-restore="no" type="int" group="csr"/>\n<reg name="sstateen3" bitsize="32" regnum="336" save-restore="no" type="int" group="csr"/>\n<reg name="snxti" bitsize="32" regnum="390" save-restore="no" type="int" group="csr"/>\n<reg name="sintstatus" bitsize="32" regnum="391" save-restore="no" ty [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:2000,1000#9e
[remote] Packet received: mno" type="int" group="csr"/>\n<reg name="mhpmevent8" bitsize="32" regnum="873" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmevent9" bitsize="32" regnum="874" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmevent10" bitsize="32" regnum="875" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmevent11" bitsize="32" regnum="876" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmevent12" bitsize="32" regnum="877" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmeven [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:3000,1000#9f
[remote] Packet received: m type="int" group="csr"/>\n<reg name="pmpcfg6" bitsize="32" regnum="999" save-restore="no" type="int" group="csr"/>\n<reg name="pmpcfg7" bitsize="32" regnum="1000" save-restore="no" type="int" group="csr"/>\n<reg name="pmpcfg8" bitsize="32" regnum="1001" save-restore="no" type="int" group="csr"/>\n<reg name="pmpcfg9" bitsize="32" regnum="1002" save-restore="no" type="int" group="csr"/>\n<reg name="pmpcfg10" bitsize="32" regnum="1003" save-restore="no" type="int" group="csr"/>\n<reg name="pmpcfg11" bitsize="32" r [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:4000,1000#a0
[remote] Packet received: m" save-restore="no" type="int" group="csr"/>\n<reg name="pmpaddr35" bitsize="32" regnum="1044" save-restore="no" type="int" group="csr"/>\n<reg name="pmpaddr36" bitsize="32" regnum="1045" save-restore="no" type="int" group="csr"/>\n<reg name="pmpaddr37" bitsize="32" regnum="1046" save-restore="no" type="int" group="csr"/>\n<reg name="pmpaddr38" bitsize="32" regnum="1047" save-restore="no" type="int" group="csr"/>\n<reg name="pmpaddr39" bitsize="32" regnum="1048" save-restore="no" type="int" group="csr"/>\n<reg n [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:5000,1000#a1
[remote] Packet received: m0h" bitsize="32" regnum="1629" save-restore="no" type="int" group="csr"/>\n<reg name="hstateen1h" bitsize="32" regnum="1630" save-restore="no" type="int" group="csr"/>\n<reg name="hstateen2h" bitsize="32" regnum="1631" save-restore="no" type="int" group="csr"/>\n<reg name="hstateen3h" bitsize="32" regnum="1632" save-restore="no" type="int" group="csr"/>\n<reg name="htval" bitsize="32" regnum="1668" save-restore="no" type="int" group="csr"/>\n<reg name="hip" bitsize="32" regnum="1669" save-restore="no" type="int [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:6000,1000#a2
[remote] Packet received: m" bitsize="32" regnum="2020" save-restore="no" type="int" group="csr"/>\n<reg name="tinfo" bitsize="32" regnum="2021" save-restore="no" type="int" group="csr"/>\n<reg name="tcontrol" bitsize="32" regnum="2022" save-restore="no" type="int" group="csr"/>\n<reg name="mcontext" bitsize="32" regnum="2025" save-restore="no" type="int" group="csr"/>\n<reg name="mscontext" bitsize="32" regnum="2027" save-restore="no" type="int" group="csr"/>\n<reg name="dcsr" bitsize="32" regnum="2033" save-restore="no" type="int" grou [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:7000,1000#a3
[remote] Packet received: m type="int" group="csr"/>\n<reg name="mhpmcounter5h" bitsize="32" regnum="3014" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmcounter6h" bitsize="32" regnum="3015" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmcounter7h" bitsize="32" regnum="3016" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmcounter8h" bitsize="32" regnum="3017" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmcounter9h" bitsize="32" regnum="3018" save-restore="no" type="int" group="csr"/>\n<reg [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:8000,1000#a4
[remote] Packet received: mestore="no" type="int" group="csr"/>\n<reg name="hpmcounter16" bitsize="32" regnum="3153" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter17" bitsize="32" regnum="3154" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter18" bitsize="32" regnum="3155" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter19" bitsize="32" regnum="3156" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter20" bitsize="32" regnum="3157" save-restore="no" type="int" group="csr"/> [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:9000,1000#a5
[remote] Packet received: l type="int" group="csr"/>\n<reg name="hpmcounter27h" bitsize="32" regnum="3292" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter28h" bitsize="32" regnum="3293" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter29h" bitsize="32" regnum="3294" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter30h" bitsize="32" regnum="3295" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter31h" bitsize="32" regnum="3296" save-restore="no" type="int" group="csr"/>\n<reg [882 bytes omitted]
[remote] Sending packet: $qTStatus#49
[remote] Packet received:
[remote] packet_ok: Packet qTStatus (trace-status) is NOT supported
[remote] Sending packet: $?#3f
[remote] Packet received: S00
[remote] Sending packet: $qXfer:threads:read::0,1000#92
[remote] Packet received: l<?xml version="1.0"?>\n<threads>\n</threads>\n
[remote] Sending packet: $Hc-1#09
[remote] Packet received: OK
[remote] Sending packet: $qC#b4
[remote] Packet received: QC0
[remote] Sending packet: $qAttached#8f
[remote] Packet received: 1
[remote] packet_ok: Packet qAttached (query-attached) is supported
warning: No executable has been specified and target does not support
determining executable automatically. Try using the "file" command.
[remote] wait: enter
[remote] select_thread_for_ambiguous_stop_reply: enter
[remote] select_thread_for_ambiguous_stop_reply: process_wide_stop = 0
[remote] select_thread_for_ambiguous_stop_reply: first resumed thread is Remote target
[remote] select_thread_for_ambiguous_stop_reply: is this guess ambiguous? = 0
[remote] select_thread_for_ambiguous_stop_reply: exit
[remote] wait: exit
[remote] Sending packet: $g#67
[remote] Packet received: 000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000080
[remote] Sending packet: $qXfer:threads:read::0,1000#92
[remote] Packet received: l<?xml version="1.0"?>\n<threads>\n</threads>\n
[remote] Sending packet: $m80000004,4#59
[remote] Packet received: 93051005
[remote] Sending packet: $m80000000,4#55
[remote] Packet received: 13050005
0x80000000 in ?? ()
[remote] start_remote_1: exit

架构描述文件

riscv描述文件

通信协议日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[remote] Sending packet: $qXfer:features:read:target.xml:0,1000#0c
[remote] Packet received: m<?xml version="1.0"?>\n<!DOCTYPE target SYSTEM "gdb-target.dtd">\n<target version="1.0">\n<architecture>riscv:rv32</architecture>\n<feature name="org.gnu.gdb.riscv.cpu">\n<reg name="zero" bitsize="32" regnum="0" save-restore="yes" type="int" group="general"/>\n<reg name="ra" bitsize="32" regnum="1" save-restore="yes" type="int" group="general"/>\n<reg name="sp" bitsize="32" regnum="2" save-restore="yes" type="int" group="general"/>\n<reg name="gp" bitsize="32" regnum="3" save-restore="yes" type="int" group="genera [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:1000,1000#9d
[remote] Packet received: m="sstateen0" bitsize="32" regnum="333" save-restore="no" type="int" group="csr"/>\n<reg name="sstateen1" bitsize="32" regnum="334" save-restore="no" type="int" group="csr"/>\n<reg name="sstateen2" bitsize="32" regnum="335" save-restore="no" type="int" group="csr"/>\n<reg name="sstateen3" bitsize="32" regnum="336" save-restore="no" type="int" group="csr"/>\n<reg name="snxti" bitsize="32" regnum="390" save-restore="no" type="int" group="csr"/>\n<reg name="sintstatus" bitsize="32" regnum="391" save-restore="no" ty [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:2000,1000#9e
[remote] Packet received: mno" type="int" group="csr"/>\n<reg name="mhpmevent8" bitsize="32" regnum="873" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmevent9" bitsize="32" regnum="874" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmevent10" bitsize="32" regnum="875" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmevent11" bitsize="32" regnum="876" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmevent12" bitsize="32" regnum="877" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmeven [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:3000,1000#9f
[remote] Packet received: m type="int" group="csr"/>\n<reg name="pmpcfg6" bitsize="32" regnum="999" save-restore="no" type="int" group="csr"/>\n<reg name="pmpcfg7" bitsize="32" regnum="1000" save-restore="no" type="int" group="csr"/>\n<reg name="pmpcfg8" bitsize="32" regnum="1001" save-restore="no" type="int" group="csr"/>\n<reg name="pmpcfg9" bitsize="32" regnum="1002" save-restore="no" type="int" group="csr"/>\n<reg name="pmpcfg10" bitsize="32" regnum="1003" save-restore="no" type="int" group="csr"/>\n<reg name="pmpcfg11" bitsize="32" r [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:4000,1000#a0
[remote] Packet received: m" save-restore="no" type="int" group="csr"/>\n<reg name="pmpaddr35" bitsize="32" regnum="1044" save-restore="no" type="int" group="csr"/>\n<reg name="pmpaddr36" bitsize="32" regnum="1045" save-restore="no" type="int" group="csr"/>\n<reg name="pmpaddr37" bitsize="32" regnum="1046" save-restore="no" type="int" group="csr"/>\n<reg name="pmpaddr38" bitsize="32" regnum="1047" save-restore="no" type="int" group="csr"/>\n<reg name="pmpaddr39" bitsize="32" regnum="1048" save-restore="no" type="int" group="csr"/>\n<reg n [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:5000,1000#a1
[remote] Packet received: m0h" bitsize="32" regnum="1629" save-restore="no" type="int" group="csr"/>\n<reg name="hstateen1h" bitsize="32" regnum="1630" save-restore="no" type="int" group="csr"/>\n<reg name="hstateen2h" bitsize="32" regnum="1631" save-restore="no" type="int" group="csr"/>\n<reg name="hstateen3h" bitsize="32" regnum="1632" save-restore="no" type="int" group="csr"/>\n<reg name="htval" bitsize="32" regnum="1668" save-restore="no" type="int" group="csr"/>\n<reg name="hip" bitsize="32" regnum="1669" save-restore="no" type="int [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:6000,1000#a2
[remote] Packet received: m" bitsize="32" regnum="2020" save-restore="no" type="int" group="csr"/>\n<reg name="tinfo" bitsize="32" regnum="2021" save-restore="no" type="int" group="csr"/>\n<reg name="tcontrol" bitsize="32" regnum="2022" save-restore="no" type="int" group="csr"/>\n<reg name="mcontext" bitsize="32" regnum="2025" save-restore="no" type="int" group="csr"/>\n<reg name="mscontext" bitsize="32" regnum="2027" save-restore="no" type="int" group="csr"/>\n<reg name="dcsr" bitsize="32" regnum="2033" save-restore="no" type="int" grou [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:7000,1000#a3
[remote] Packet received: m type="int" group="csr"/>\n<reg name="mhpmcounter5h" bitsize="32" regnum="3014" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmcounter6h" bitsize="32" regnum="3015" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmcounter7h" bitsize="32" regnum="3016" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmcounter8h" bitsize="32" regnum="3017" save-restore="no" type="int" group="csr"/>\n<reg name="mhpmcounter9h" bitsize="32" regnum="3018" save-restore="no" type="int" group="csr"/>\n<reg [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:8000,1000#a4
[remote] Packet received: mestore="no" type="int" group="csr"/>\n<reg name="hpmcounter16" bitsize="32" regnum="3153" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter17" bitsize="32" regnum="3154" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter18" bitsize="32" regnum="3155" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter19" bitsize="32" regnum="3156" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter20" bitsize="32" regnum="3157" save-restore="no" type="int" group="csr"/> [3585 bytes omitted]
[remote] Sending packet: $qXfer:features:read:target.xml:9000,1000#a5
[remote] Packet received: l type="int" group="csr"/>\n<reg name="hpmcounter27h" bitsize="32" regnum="3292" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter28h" bitsize="32" regnum="3293" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter29h" bitsize="32" regnum="3294" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter30h" bitsize="32" regnum="3295" save-restore="no" type="int" group="csr"/>\n<reg name="hpmcounter31h" bitsize="32" regnum="3296" save-restore="no" type="int" group="csr"/>\n<reg [882 bytes omitted]

xml格式描述文件

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
<?xml version="1.0"?>
<!DOCTYPE target SYSTEM "gdb-target.dtd">
<target version="1.0">
<architecture>riscv:rv32</architecture>
<feature name="org.gnu.gdb.riscv.cpu">
<reg name="zero" bitsize="32" regnum="0" save-restore="yes" type="int" group="general"/>
<reg name="ra" bitsize="32" regnum="1" save-restore="yes" type="int" group="general"/>
<reg name="sp" bitsize="32" regnum="2" save-restore="yes" type="int" group="general"/>
<reg name="gp" bitsize="32" regnum="3" save-restore="yes" type="int" group="general"/>
<reg name="tp" bitsize="32" regnum="4" save-restore="yes" type="int" group="general"/>
<reg name="t0" bitsize="32" regnum="5" save-restore="yes" type="int" group="general"/>
<reg name="t1" bitsize="32" regnum="6" save-restore="yes" type="int" group="general"/>
<reg name="t2" bitsize="32" regnum="7" save-restore="yes" type="int" group="general"/>
<reg name="fp" bitsize="32" regnum="8" save-restore="yes" type="int" group="general"/>
<reg name="s1" bitsize="32" regnum="9" save-restore="yes" type="int" group="general"/>
<reg name="a0" bitsize="32" regnum="10" save-restore="yes" type="int" group="general"/>
<reg name="a1" bitsize="32" regnum="11" save-restore="yes" type="int" group="general"/>
<reg name="a2" bitsize="32" regnum="12" save-restore="yes" type="int" group="general"/>
<reg name="a3" bitsize="32" regnum="13" save-restore="yes" type="int" group="general"/>
<reg name="a4" bitsize="32" regnum="14" save-restore="yes" type="int" group="general"/>
<reg name="a5" bitsize="32" regnum="15" save-restore="yes" type="int" group="general"/>
<reg name="a6" bitsize="32" regnum="16" save-restore="yes" type="int" group="general"/>
<reg name="a7" bitsize="32" regnum="17" save-restore="yes" type="int" group="general"/>
<reg name="s2" bitsize="32" regnum="18" save-restore="yes" type="int" group="general"/>
<reg name="s3" bitsize="32" regnum="19" save-restore="yes" type="int" group="general"/>
<reg name="s4" bitsize="32" regnum="20" save-restore="yes" type="int" group="general"/>
<reg name="s5" bitsize="32" regnum="21" save-restore="yes" type="int" group="general"/>
<reg name="s6" bitsize="32" regnum="22" save-restore="yes" type="int" group="general"/>
<reg name="s7" bitsize="32" regnum="23" save-restore="yes" type="int" group="general"/>
<reg name="s8" bitsize="32" regnum="24" save-restore="yes" type="int" group="general"/>
<reg name="s9" bitsize="32" regnum="25" save-restore="yes" type="int" group="general"/>
<reg name="s10" bitsize="32" regnum="26" save-restore="yes" type="int" group="general"/>
<reg name="s11" bitsize="32" regnum="27" save-restore="yes" type="int" group="general"/>
<reg name="t3" bitsize="32" regnum="28" save-restore="yes" type="int" group="general"/>
<reg name="t4" bitsize="32" regnum="29" save-restore="yes" type="int" group="general"/>
<reg name="t5" bitsize="32" regnum="30" save-restore="yes" type="int" group="general"/>
<reg name="t6" bitsize="32" regnum="31" save-restore="yes" type="int" group="general"/>
<reg name="pc" bitsize="32" regnum="32" save-restore="yes" type="int" group="general"/>
</feature>
<feature name="org.gnu.gdb.riscv.csr">
<reg name="utvt" bitsize="32" regnum="72" save-restore="no" type="int" group="csr"/>
<reg name="seed" bitsize="32" regnum="86" save-restore="no" type="int" group="csr"/>
<reg name="jvt" bitsize="32" regnum="88" save-restore="no" type="int" group="csr"/>
<reg name="unxti" bitsize="32" regnum="134" save-restore="no" type="int" group="csr"/>
<reg name="uintstatus" bitsize="32" regnum="135" save-restore="no" type="int" group="csr"/>
<reg name="uscratchcsw" bitsize="32" regnum="137" save-restore="no" type="int" group="csr"/>
<reg name="uscratchcswl" bitsize="32" regnum="138" save-restore="no" type="int" group="csr"/>
<reg name="sedeleg" bitsize="32" regnum="323" save-restore="no" type="int" group="csr"/>
<reg name="sideleg" bitsize="32" regnum="324" save-restore="no" type="int" group="csr"/>
<reg name="stvt" bitsize="32" regnum="328" save-restore="no" type="int" group="csr"/>
<reg name="senvcfg" bitsize="32" regnum="331" save-restore="no" type="int" group="csr"/>
<reg name="sstateen0" bitsize="32" regnum="333" save-restore="no" type="int" group="csr"/>
<reg name="sstateen1" bitsize="32" regnum="334" save-restore="no" type="int" group="csr"/>
<reg name="sstateen2" bitsize="32" regnum="335" save-restore="no" type="int" group="csr"/>
<reg name="sstateen3" bitsize="32" regnum="336" save-restore="no" type="int" group="csr"/>
<reg name="snxti" bitsize="32" regnum="390" save-restore="no" type="int" group="csr"/>
<reg name="sintstatus" bitsize="32" regnum="391" save-restore="no" type="int" group="csr"/>
<reg name="sscratchcsw" bitsize="32" regnum="393" save-restore="no" type="int" group="csr"/>
<reg name="sscratchcswl" bitsize="32" regnum="394" save-restore="no" type="int" group="csr"/>
<reg name="stimecmp" bitsize="32" regnum="398" save-restore="no" type="int" group="csr"/>
<reg name="stimecmph" bitsize="32" regnum="414" save-restore="no" type="int" group="csr"/>
<reg name="vsstatus" bitsize="32" regnum="577" save-restore="no" type="int" group="csr"/>
<reg name="vsie" bitsize="32" regnum="581" save-restore="no" type="int" group="csr"/>
<reg name="vstvec" bitsize="32" regnum="582" save-restore="no" type="int" group="csr"/>
<reg name="vsscratch" bitsize="32" regnum="641" save-restore="no" type="int" group="csr"/>
<reg name="vsepc" bitsize="32" regnum="642" save-restore="no" type="int" group="csr"/>
<reg name="vscause" bitsize="32" regnum="643" save-restore="no" type="int" group="csr"/>
<reg name="vstval" bitsize="32" regnum="644" save-restore="no" type="int" group="csr"/>
<reg name="vsip" bitsize="32" regnum="645" save-restore="no" type="int" group="csr"/>
<reg name="vstimecmp" bitsize="32" regnum="654" save-restore="no" type="int" group="csr"/>
<reg name="vstimecmph" bitsize="32" regnum="670" save-restore="no" type="int" group="csr"/>
<reg name="vsatp" bitsize="32" regnum="705" save-restore="no" type="int" group="csr"/>
<reg name="mstatus" bitsize="32" regnum="833" save-restore="no" type="int" group="csr"/>
<reg name="misa" bitsize="32" regnum="834" save-restore="no" type="int" group="csr"/>
<reg name="mie" bitsize="32" regnum="837" save-restore="no" type="int" group="csr"/>
<reg name="mtvec" bitsize="32" regnum="838" save-restore="no" type="int" group="csr"/>
<reg name="mtvt" bitsize="32" regnum="840" save-restore="no" type="int" group="csr"/>
<reg name="mvien" bitsize="32" regnum="841" save-restore="no" type="int" group="csr"/>
<reg name="mvip" bitsize="32" regnum="842" save-restore="no" type="int" group="csr"/>
<reg name="menvcfg" bitsize="32" regnum="843" save-restore="no" type="int" group="csr"/>
<reg name="mstateen0" bitsize="32" regnum="845" save-restore="no" type="int" group="csr"/>
<reg name="mstateen1" bitsize="32" regnum="846" save-restore="no" type="int" group="csr"/>
<reg name="mstateen2" bitsize="32" regnum="847" save-restore="no" type="int" group="csr"/>
<reg name="mstateen3" bitsize="32" regnum="848" save-restore="no" type="int" group="csr"/>
<reg name="mstatush" bitsize="32" regnum="849" save-restore="no" type="int" group="csr"/>
<reg name="mieh" bitsize="32" regnum="853" save-restore="no" type="int" group="csr"/>
<reg name="menvcfgh" bitsize="32" regnum="859" save-restore="no" type="int" group="csr"/>
<reg name="mstateen0h" bitsize="32" regnum="861" save-restore="no" type="int" group="csr"/>
<reg name="mstateen1h" bitsize="32" regnum="862" save-restore="no" type="int" group="csr"/>
<reg name="mstateen2h" bitsize="32" regnum="863" save-restore="no" type="int" group="csr"/>
<reg name="mstateen3h" bitsize="32" regnum="864" save-restore="no" type="int" group="csr"/>
<reg name="mcountinhibit" bitsize="32" regnum="865" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent3" bitsize="32" regnum="868" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent4" bitsize="32" regnum="869" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent5" bitsize="32" regnum="870" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent6" bitsize="32" regnum="871" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent7" bitsize="32" regnum="872" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent8" bitsize="32" regnum="873" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent9" bitsize="32" regnum="874" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent10" bitsize="32" regnum="875" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent11" bitsize="32" regnum="876" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent12" bitsize="32" regnum="877" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent13" bitsize="32" regnum="878" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent14" bitsize="32" regnum="879" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent15" bitsize="32" regnum="880" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent16" bitsize="32" regnum="881" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent17" bitsize="32" regnum="882" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent18" bitsize="32" regnum="883" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent19" bitsize="32" regnum="884" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent20" bitsize="32" regnum="885" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent21" bitsize="32" regnum="886" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent22" bitsize="32" regnum="887" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent23" bitsize="32" regnum="888" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent24" bitsize="32" regnum="889" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent25" bitsize="32" regnum="890" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent26" bitsize="32" regnum="891" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent27" bitsize="32" regnum="892" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent28" bitsize="32" regnum="893" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent29" bitsize="32" regnum="894" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent30" bitsize="32" regnum="895" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent31" bitsize="32" regnum="896" save-restore="no" type="int" group="csr"/>
<reg name="mscratch" bitsize="32" regnum="897" save-restore="no" type="int" group="csr"/>
<reg name="mepc" bitsize="32" regnum="898" save-restore="no" type="int" group="csr"/>
<reg name="mcause" bitsize="32" regnum="899" save-restore="no" type="int" group="csr"/>
<reg name="mtval" bitsize="32" regnum="900" save-restore="no" type="int" group="csr"/>
<reg name="mip" bitsize="32" regnum="901" save-restore="no" type="int" group="csr"/>
<reg name="mnxti" bitsize="32" regnum="902" save-restore="no" type="int" group="csr"/>
<reg name="mintstatus" bitsize="32" regnum="903" save-restore="no" type="int" group="csr"/>
<reg name="mscratchcsw" bitsize="32" regnum="905" save-restore="no" type="int" group="csr"/>
<reg name="mscratchcswl" bitsize="32" regnum="906" save-restore="no" type="int" group="csr"/>
<reg name="mtinst" bitsize="32" regnum="907" save-restore="no" type="int" group="csr"/>
<reg name="mtval2" bitsize="32" regnum="908" save-restore="no" type="int" group="csr"/>
<reg name="miselect" bitsize="32" regnum="913" save-restore="no" type="int" group="csr"/>
<reg name="mireg" bitsize="32" regnum="914" save-restore="no" type="int" group="csr"/>
<reg name="miph" bitsize="32" regnum="917" save-restore="no" type="int" group="csr"/>
<reg name="mtopei" bitsize="32" regnum="925" save-restore="no" type="int" group="csr"/>
<reg name="pmpcfg0" bitsize="32" regnum="993" save-restore="no" type="int" group="csr"/>
<reg name="pmpcfg1" bitsize="32" regnum="994" save-restore="no" type="int" group="csr"/>
<reg name="pmpcfg2" bitsize="32" regnum="995" save-restore="no" type="int" group="csr"/>
<reg name="pmpcfg3" bitsize="32" regnum="996" save-restore="no" type="int" group="csr"/>
<reg name="pmpcfg4" bitsize="32" regnum="997" save-restore="no" type="int" group="csr"/>
<reg name="pmpcfg5" bitsize="32" regnum="998" save-restore="no" type="int" group="csr"/>
<reg name="pmpcfg6" bitsize="32" regnum="999" save-restore="no" type="int" group="csr"/>
<reg name="pmpcfg7" bitsize="32" regnum="1000" save-restore="no" type="int" group="csr"/>
<reg name="pmpcfg8" bitsize="32" regnum="1001" save-restore="no" type="int" group="csr"/>
<reg name="pmpcfg9" bitsize="32" regnum="1002" save-restore="no" type="int" group="csr"/>
<reg name="pmpcfg10" bitsize="32" regnum="1003" save-restore="no" type="int" group="csr"/>
<reg name="pmpcfg11" bitsize="32" regnum="1004" save-restore="no" type="int" group="csr"/>
<reg name="pmpcfg12" bitsize="32" regnum="1005" save-restore="no" type="int" group="csr"/>
<reg name="pmpcfg13" bitsize="32" regnum="1006" save-restore="no" type="int" group="csr"/>
<reg name="pmpcfg14" bitsize="32" regnum="1007" save-restore="no" type="int" group="csr"/>
<reg name="pmpcfg15" bitsize="32" regnum="1008" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr0" bitsize="32" regnum="1009" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr1" bitsize="32" regnum="1010" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr2" bitsize="32" regnum="1011" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr3" bitsize="32" regnum="1012" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr4" bitsize="32" regnum="1013" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr5" bitsize="32" regnum="1014" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr6" bitsize="32" regnum="1015" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr7" bitsize="32" regnum="1016" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr8" bitsize="32" regnum="1017" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr9" bitsize="32" regnum="1018" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr10" bitsize="32" regnum="1019" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr11" bitsize="32" regnum="1020" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr12" bitsize="32" regnum="1021" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr13" bitsize="32" regnum="1022" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr14" bitsize="32" regnum="1023" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr15" bitsize="32" regnum="1024" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr16" bitsize="32" regnum="1025" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr17" bitsize="32" regnum="1026" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr18" bitsize="32" regnum="1027" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr19" bitsize="32" regnum="1028" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr20" bitsize="32" regnum="1029" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr21" bitsize="32" regnum="1030" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr22" bitsize="32" regnum="1031" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr23" bitsize="32" regnum="1032" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr24" bitsize="32" regnum="1033" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr25" bitsize="32" regnum="1034" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr26" bitsize="32" regnum="1035" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr27" bitsize="32" regnum="1036" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr28" bitsize="32" regnum="1037" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr29" bitsize="32" regnum="1038" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr30" bitsize="32" regnum="1039" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr31" bitsize="32" regnum="1040" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr32" bitsize="32" regnum="1041" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr33" bitsize="32" regnum="1042" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr34" bitsize="32" regnum="1043" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr35" bitsize="32" regnum="1044" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr36" bitsize="32" regnum="1045" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr37" bitsize="32" regnum="1046" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr38" bitsize="32" regnum="1047" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr39" bitsize="32" regnum="1048" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr40" bitsize="32" regnum="1049" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr41" bitsize="32" regnum="1050" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr42" bitsize="32" regnum="1051" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr43" bitsize="32" regnum="1052" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr44" bitsize="32" regnum="1053" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr45" bitsize="32" regnum="1054" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr46" bitsize="32" regnum="1055" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr47" bitsize="32" regnum="1056" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr48" bitsize="32" regnum="1057" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr49" bitsize="32" regnum="1058" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr50" bitsize="32" regnum="1059" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr51" bitsize="32" regnum="1060" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr52" bitsize="32" regnum="1061" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr53" bitsize="32" regnum="1062" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr54" bitsize="32" regnum="1063" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr55" bitsize="32" regnum="1064" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr56" bitsize="32" regnum="1065" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr57" bitsize="32" regnum="1066" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr58" bitsize="32" regnum="1067" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr59" bitsize="32" regnum="1068" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr60" bitsize="32" regnum="1069" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr61" bitsize="32" regnum="1070" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr62" bitsize="32" regnum="1071" save-restore="no" type="int" group="csr"/>
<reg name="pmpaddr63" bitsize="32" regnum="1072" save-restore="no" type="int" group="csr"/>
<reg name="scontext" bitsize="32" regnum="1513" save-restore="no" type="int" group="csr"/>
<reg name="hstatus" bitsize="32" regnum="1601" save-restore="no" type="int" group="csr"/>
<reg name="hedeleg" bitsize="32" regnum="1603" save-restore="no" type="int" group="csr"/>
<reg name="hideleg" bitsize="32" regnum="1604" save-restore="no" type="int" group="csr"/>
<reg name="hie" bitsize="32" regnum="1605" save-restore="no" type="int" group="csr"/>
<reg name="htimedelta" bitsize="32" regnum="1606" save-restore="no" type="int" group="csr"/>
<reg name="hcounteren" bitsize="32" regnum="1607" save-restore="no" type="int" group="csr"/>
<reg name="hgeie" bitsize="32" regnum="1608" save-restore="no" type="int" group="csr"/>
<reg name="henvcfg" bitsize="32" regnum="1611" save-restore="no" type="int" group="csr"/>
<reg name="hstateen0" bitsize="32" regnum="1613" save-restore="no" type="int" group="csr"/>
<reg name="hstateen1" bitsize="32" regnum="1614" save-restore="no" type="int" group="csr"/>
<reg name="hstateen2" bitsize="32" regnum="1615" save-restore="no" type="int" group="csr"/>
<reg name="hstateen3" bitsize="32" regnum="1616" save-restore="no" type="int" group="csr"/>
<reg name="htimedeltah" bitsize="32" regnum="1622" save-restore="no" type="int" group="csr"/>
<reg name="henvcfgh" bitsize="32" regnum="1627" save-restore="no" type="int" group="csr"/>
<reg name="hstateen0h" bitsize="32" regnum="1629" save-restore="no" type="int" group="csr"/>
<reg name="hstateen1h" bitsize="32" regnum="1630" save-restore="no" type="int" group="csr"/>
<reg name="hstateen2h" bitsize="32" regnum="1631" save-restore="no" type="int" group="csr"/>
<reg name="hstateen3h" bitsize="32" regnum="1632" save-restore="no" type="int" group="csr"/>
<reg name="htval" bitsize="32" regnum="1668" save-restore="no" type="int" group="csr"/>
<reg name="hip" bitsize="32" regnum="1669" save-restore="no" type="int" group="csr"/>
<reg name="hvip" bitsize="32" regnum="1670" save-restore="no" type="int" group="csr"/>
<reg name="htinst" bitsize="32" regnum="1675" save-restore="no" type="int" group="csr"/>
<reg name="hgatp" bitsize="32" regnum="1729" save-restore="no" type="int" group="csr"/>
<reg name="hcontext" bitsize="32" regnum="1769" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent3h" bitsize="32" regnum="1892" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent4h" bitsize="32" regnum="1893" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent5h" bitsize="32" regnum="1894" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent6h" bitsize="32" regnum="1895" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent7h" bitsize="32" regnum="1896" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent8h" bitsize="32" regnum="1897" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent9h" bitsize="32" regnum="1898" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent10h" bitsize="32" regnum="1899" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent11h" bitsize="32" regnum="1900" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent12h" bitsize="32" regnum="1901" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent13h" bitsize="32" regnum="1902" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent14h" bitsize="32" regnum="1903" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent15h" bitsize="32" regnum="1904" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent16h" bitsize="32" regnum="1905" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent17h" bitsize="32" regnum="1906" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent18h" bitsize="32" regnum="1907" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent19h" bitsize="32" regnum="1908" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent20h" bitsize="32" regnum="1909" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent21h" bitsize="32" regnum="1910" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent22h" bitsize="32" regnum="1911" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent23h" bitsize="32" regnum="1912" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent24h" bitsize="32" regnum="1913" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent25h" bitsize="32" regnum="1914" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent26h" bitsize="32" regnum="1915" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent27h" bitsize="32" regnum="1916" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent28h" bitsize="32" regnum="1917" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent29h" bitsize="32" regnum="1918" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent30h" bitsize="32" regnum="1919" save-restore="no" type="int" group="csr"/>
<reg name="mhpmevent31h" bitsize="32" regnum="1920" save-restore="no" type="int" group="csr"/>
<reg name="mseccfg" bitsize="32" regnum="1928" save-restore="no" type="int" group="csr"/>
<reg name="mseccfgh" bitsize="32" regnum="1944" save-restore="no" type="int" group="csr"/>
<reg name="tselect" bitsize="32" regnum="2017" save-restore="no" type="int" group="csr"/>
<reg name="tdata1" bitsize="32" regnum="2018" save-restore="no" type="int" group="csr"/>
<reg name="tdata2" bitsize="32" regnum="2019" save-restore="no" type="int" group="csr"/>
<reg name="tdata3" bitsize="32" regnum="2020" save-restore="no" type="int" group="csr"/>
<reg name="tinfo" bitsize="32" regnum="2021" save-restore="no" type="int" group="csr"/>
<reg name="tcontrol" bitsize="32" regnum="2022" save-restore="no" type="int" group="csr"/>
<reg name="mcontext" bitsize="32" regnum="2025" save-restore="no" type="int" group="csr"/>
<reg name="mscontext" bitsize="32" regnum="2027" save-restore="no" type="int" group="csr"/>
<reg name="dcsr" bitsize="32" regnum="2033" save-restore="no" type="int" group="csr"/>
<reg name="dpc" bitsize="32" regnum="2034" save-restore="no" type="int" group="csr"/>
<reg name="dscratch0" bitsize="32" regnum="2035" save-restore="no" type="int" group="csr"/>
<reg name="dscratch1" bitsize="32" regnum="2036" save-restore="no" type="int" group="csr"/>
<reg name="mcycle" bitsize="32" regnum="2881" save-restore="no" type="int" group="csr"/>
<reg name="minstret" bitsize="32" regnum="2883" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter3" bitsize="32" regnum="2884" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter4" bitsize="32" regnum="2885" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter5" bitsize="32" regnum="2886" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter6" bitsize="32" regnum="2887" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter7" bitsize="32" regnum="2888" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter8" bitsize="32" regnum="2889" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter9" bitsize="32" regnum="2890" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter10" bitsize="32" regnum="2891" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter11" bitsize="32" regnum="2892" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter12" bitsize="32" regnum="2893" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter13" bitsize="32" regnum="2894" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter14" bitsize="32" regnum="2895" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter15" bitsize="32" regnum="2896" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter16" bitsize="32" regnum="2897" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter17" bitsize="32" regnum="2898" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter18" bitsize="32" regnum="2899" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter19" bitsize="32" regnum="2900" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter20" bitsize="32" regnum="2901" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter21" bitsize="32" regnum="2902" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter22" bitsize="32" regnum="2903" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter23" bitsize="32" regnum="2904" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter24" bitsize="32" regnum="2905" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter25" bitsize="32" regnum="2906" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter26" bitsize="32" regnum="2907" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter27" bitsize="32" regnum="2908" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter28" bitsize="32" regnum="2909" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter29" bitsize="32" regnum="2910" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter30" bitsize="32" regnum="2911" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter31" bitsize="32" regnum="2912" save-restore="no" type="int" group="csr"/>
<reg name="mcycleh" bitsize="32" regnum="3009" save-restore="no" type="int" group="csr"/>
<reg name="minstreth" bitsize="32" regnum="3011" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter3h" bitsize="32" regnum="3012" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter4h" bitsize="32" regnum="3013" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter5h" bitsize="32" regnum="3014" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter6h" bitsize="32" regnum="3015" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter7h" bitsize="32" regnum="3016" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter8h" bitsize="32" regnum="3017" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter9h" bitsize="32" regnum="3018" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter10h" bitsize="32" regnum="3019" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter11h" bitsize="32" regnum="3020" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter12h" bitsize="32" regnum="3021" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter13h" bitsize="32" regnum="3022" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter14h" bitsize="32" regnum="3023" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter15h" bitsize="32" regnum="3024" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter16h" bitsize="32" regnum="3025" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter17h" bitsize="32" regnum="3026" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter18h" bitsize="32" regnum="3027" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter19h" bitsize="32" regnum="3028" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter20h" bitsize="32" regnum="3029" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter21h" bitsize="32" regnum="3030" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter22h" bitsize="32" regnum="3031" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter23h" bitsize="32" regnum="3032" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter24h" bitsize="32" regnum="3033" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter25h" bitsize="32" regnum="3034" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter26h" bitsize="32" regnum="3035" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter27h" bitsize="32" regnum="3036" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter28h" bitsize="32" regnum="3037" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter29h" bitsize="32" regnum="3038" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter30h" bitsize="32" regnum="3039" save-restore="no" type="int" group="csr"/>
<reg name="mhpmcounter31h" bitsize="32" regnum="3040" save-restore="no" type="int" group="csr"/>
<reg name="cycle" bitsize="32" regnum="3137" save-restore="no" type="int" group="csr"/>
<reg name="time" bitsize="32" regnum="3138" save-restore="no" type="int" group="csr"/>
<reg name="instret" bitsize="32" regnum="3139" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter3" bitsize="32" regnum="3140" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter4" bitsize="32" regnum="3141" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter5" bitsize="32" regnum="3142" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter6" bitsize="32" regnum="3143" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter7" bitsize="32" regnum="3144" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter8" bitsize="32" regnum="3145" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter9" bitsize="32" regnum="3146" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter10" bitsize="32" regnum="3147" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter11" bitsize="32" regnum="3148" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter12" bitsize="32" regnum="3149" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter13" bitsize="32" regnum="3150" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter14" bitsize="32" regnum="3151" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter15" bitsize="32" regnum="3152" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter16" bitsize="32" regnum="3153" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter17" bitsize="32" regnum="3154" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter18" bitsize="32" regnum="3155" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter19" bitsize="32" regnum="3156" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter20" bitsize="32" regnum="3157" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter21" bitsize="32" regnum="3158" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter22" bitsize="32" regnum="3159" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter23" bitsize="32" regnum="3160" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter24" bitsize="32" regnum="3161" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter25" bitsize="32" regnum="3162" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter26" bitsize="32" regnum="3163" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter27" bitsize="32" regnum="3164" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter28" bitsize="32" regnum="3165" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter29" bitsize="32" regnum="3166" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter30" bitsize="32" regnum="3167" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter31" bitsize="32" regnum="3168" save-restore="no" type="int" group="csr"/>
<reg name="cycleh" bitsize="32" regnum="3265" save-restore="no" type="int" group="csr"/>
<reg name="timeh" bitsize="32" regnum="3266" save-restore="no" type="int" group="csr"/>
<reg name="instreth" bitsize="32" regnum="3267" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter3h" bitsize="32" regnum="3268" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter4h" bitsize="32" regnum="3269" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter5h" bitsize="32" regnum="3270" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter6h" bitsize="32" regnum="3271" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter7h" bitsize="32" regnum="3272" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter8h" bitsize="32" regnum="3273" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter9h" bitsize="32" regnum="3274" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter10h" bitsize="32" regnum="3275" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter11h" bitsize="32" regnum="3276" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter12h" bitsize="32" regnum="3277" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter13h" bitsize="32" regnum="3278" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter14h" bitsize="32" regnum="3279" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter15h" bitsize="32" regnum="3280" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter16h" bitsize="32" regnum="3281" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter17h" bitsize="32" regnum="3282" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter18h" bitsize="32" regnum="3283" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter19h" bitsize="32" regnum="3284" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter20h" bitsize="32" regnum="3285" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter21h" bitsize="32" regnum="3286" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter22h" bitsize="32" regnum="3287" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter23h" bitsize="32" regnum="3288" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter24h" bitsize="32" regnum="3289" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter25h" bitsize="32" regnum="3290" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter26h" bitsize="32" regnum="3291" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter27h" bitsize="32" regnum="3292" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter28h" bitsize="32" regnum="3293" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter29h" bitsize="32" regnum="3294" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter30h" bitsize="32" regnum="3295" save-restore="no" type="int" group="csr"/>
<reg name="hpmcounter31h" bitsize="32" regnum="3296" save-restore="no" type="int" group="csr"/>
<reg name="scountovf" bitsize="32" regnum="3553" save-restore="no" type="int" group="csr"/>
<reg name="hgeip" bitsize="32" regnum="3667" save-restore="no" type="int" group="csr"/>
<reg name="mvendorid" bitsize="32" regnum="3922" save-restore="no" type="int" group="csr"/>
<reg name="marchid" bitsize="32" regnum="3923" save-restore="no" type="int" group="csr"/>
<reg name="mimpid" bitsize="32" regnum="3924" save-restore="no" type="int" group="csr"/>
<reg name="mhartid" bitsize="32" regnum="3925" save-restore="no" type="int" group="csr"/>
<reg name="mconfigptr" bitsize="32" regnum="3926" save-restore="no" type="int" group="csr"/>
<reg name="mtopi" bitsize="32" regnum="4081" save-restore="no" type="int" group="csr"/>
</feature>
<feature name="org.gnu.gdb.riscv.virtual">
<reg name="priv" bitsize="8" regnum="4161" save-restore="no" type="int" group="general"/>
</feature>
</target>

注意事项

指令差异

ARM处理器具有ARMThumb两种指令,相互切换一般采用b类指令进行跳转,否则处理器会返回异常。

分布式基础理论

CAP理论基本概念

  • 数据一致性(consistency):若系统更新操作成功,则它之后的读请求都必须读到这个新数据;若失败,则所有读请求都不能读到这个新数据,对调用者而言数据具有强一致性(strong consistency)
  • 服务可用性(availability):所有请求在一定时间内得到响应,可终止,不会一直等待
  • 分区容错性(partition-tolerance):在网络分区的情况下,被分隔的节点仍能正常对外服务

CAP三者不空兼得

若放弃P,即系统称为传统的单服务器模式;而分布式必然不能放弃P;

实现P就允许网络存在异常。若实现CP,向一个分区的更新操作因一致性要求,必然导致网络恢复前,其他分区不能满足可用性。若实现AP,存在不同分区因网络异常,无法保证强一致性。

因此,分布式系统理论上不可能选择CA架构,只能选择CP或AP架构。

Base理论

CAP理论的一种妥协,由于CAP只能二取一,Base理论降低了发生分区容错时对可用性和一致性的要求。

  1. 基本可用:允许可用性降低(可能响应延长、可能服务降级)
  2. 软状态:指允许系统中的数据存在中间状态,并任务该中间状态不会影响系统整体可用性(比如支付完成前的支付中)
  3. 最终一致性:节点数据同步可能存在时延,但在一定的期限后必须达成数据的一致,状态变为最终状态

数据一致性模型

强一致性:当更新操作完成后,任何多个后续进程的访问都会返回更新后的数据。根据CAP理论,这种实现需要牺牲可用性。

弱一致性:当更新操作完成后,不承诺立即可以读到更新后的数据,而这段时间存在“不一致性窗口”。

最终一致性:是弱一致性的特例,强调的是所有的数据副本,在经过一定时间的同步后,最终能够达到一个一致的状态。达到最终一致性的时间,就是不一致窗口时间。在没有故障发生的前提下,不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。

最终一致性模型根据其提供的不同保证可以划分为因果一致性和会话一致性等等模型。

因果一致性:要求有因果关系的操作顺序要得到保证,非因果关系的操作顺序则无所谓。

会话一致性:将对系统数据的访问过程框定在了一个会话当中,约定了系统保证在同一个有效的会话中实现“读己之所写”的一致性。

Quorum、WARO机制

WARO:一种简单的副本控制协议,写操作时,只有当所有副本都成功更新之后,这次写操作才算成功,否则视为失败。优先保证读、任何节点读到的数据都是最新数据,牺牲了更新服务的可用性。但只要有一个节点存活,任然能够提供读服务。

选举算法Quorum机制:如,10个副本,一次成功更新了三个,那么需要读取八个副本的数据,可以保证读到最新数据。无法保证强一致性,也就是无法实现任何时刻或节点都可以读到最近一次成功提交的副本数据。需要配合一个获取最新成功提交的版本号的metadata服务,这样可以确定最新成功提交的版本号,然后从已经读到的数据中就可以确认最新写入的数据。

模拟vexpress-a9

使用QEMU模拟vexpress-a9开发板,网络上教程比较多,可以快速搭建好开发环境。

基本环境

项目信息
操作系统Ubuntu 22.04
内核linux-4.14.334
根文件系统busybox-1.36.1
引导程序u-boot-2022.10-rc5

安装依赖

1
sudo apt-get install -y make gcc bc libncurses5-dev bison flex libssl-dev u-boot-tools gcc-arm-linux-gnueabi g++-arm-linux-gnueabi qemu-system-arm

Linux内核

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 使用vexpress默认配置编译
cd linux-4.14.334
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- vexpress_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig
# 开启NFS4支持。
# 位置:File System -> Network File Systems->NFS client support for NFS version 4 (相关的四项全勾上)

# 编译内核
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- zImage -j$(nproc)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- LOADADDR=0x60003000 uImage -j$(nproc)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- modules -j$(nproc)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- dtbs -j$(nproc)
cd ..

# 启动模拟器
qemu-system-arm -M vexpress-a9 -m 128M -kernel linux-4.14.334/arch/arm/boot/zImage -dtb linux-4.14.334/arch/arm/boot/dts/vexpress-v2p-ca9.dtb -nographic -append "console=ttyAMA0"
# 这里注意文件的路径,是在linux-4.14.334源码目录执行的。
# -M vexpress-a9 模拟vexpress-a9单板,你能够使用-M ?參数来获取该qemu版本号支持的全部单板
# -m 128M 单板执行物理内存128M
# -kernel xx/zImage 告诉qemu单板执行内核镜像路径
# -dtb xx/vexpress-v2p-ca9.dtb 告诉qemu单板的设备树(必须加入)
# -nographic 不使用图形化界面,仅仅使用串口
# -append "console=ttyAMA0" 内核启动參数。这里告诉内核vexpress单板执行。串口设备是哪个tty。

提示没有根文件系统,接下来制作它。

根文件系统

配置编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cd busybox-1.36.1

# 配置
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- clean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig
# 配置页面选择
# Settings->Build Options->Build static binary

# 编译
# 默认安装在_install路径中
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- -j$(nproc)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- install

cd ..

补充根文件系统

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# 拷贝基本文件系统
mkdir rootfs-arm32
cd rootfs-arm32
cp -rfd ../busybox-1.36.1/_install/* .

# 创建设备节点
mkdir dev
sudo mknod -m 666 dev/tty1 c 4 1
sudo mknod -m 666 dev/tty2 c 4 2
sudo mknod -m 666 dev/tty3 c 4 3
sudo mknod -m 666 dev/tty4 c 4 4
sudo mknod -m 666 dev/console c 5 1
sudo mknod -m 666 dev/null c 1 3

# 安装动态链接库
mkdir lib
cp -d /usr/arm-linux-gnueabi/lib/*.so* ./lib

# 配置初始化进程rcS
mkdir -p etc/init.d
touch etc/init.d/rcS
chmod 777 etc/init.d/rcS
cat > etc/init.d/rcS <<EOF
#!/bin/sh
PATH=/bin:/sbin:/usr/bin:/usr/sbin
export LD_LIBRARY_PATH=/lib:/usr/lib
/bin/mount -n -t ramfs ramfs /var
/bin/mount -n -t ramfs ramfs /tmp
/bin/mount -n -t sysfs none /sys
/bin/mount -n -t ramfs none /dev
/bin/mkdir /var/tmp
/bin/mkdir /var/modules
/bin/mkdir /var/run
/bin/mkdir /var/log
/bin/mkdir -p /dev/pts
/bin/mkdir -p /dev/shm
/sbin/mdev -s
/bin/mount -a
echo "-----------------------------------"
echo "*****welcome to vexpress board*****"
echo "-----------------------------------"
EOF

# 配置文件系统fstab
cat > etc/fstab <<EOF
proc /proc proc defaults 0 0
none /dev/pts devpts mode=0622 0 0
mdev /dev ramfs defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /dev/shm tmpfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0
tmpfs /mnt tmpfs defaults 0 0
var /dev tmpfs defaults 0 0
ramfs /dev ramfs defaults 0 0
EOF

# 配置初始化脚本
cat > etc/inittab <<EOF
::sysinit:/etc/init.d/rcS
::askfirst:-/bin/sh
::restart:/sbin/init
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
EOF

# 配置环境变量,'EOF'可以防止内容被解析
cat > etc/profile <<'EOF'
#!/bin/sh
USER="root"
LOGNAME=$USER
# export HOSTNAME=vexpress-a9
export HOSTNAME=`cat /etc/sysconfig/HOSTNAME`
export USER=root
export HOME=root
export PS1="[$USER@$HOSTNAME:\w]\#"
PATH=/bin:/sbin:/usr/bin:/usr/sbin
LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH
export PATH LD_LIBRARY_PATH
EOF

# 增加主机名
mkdir etc/sysconfig
cat > etc/sysconfig/HOSTNAME <<EOF
vexpress-a9
EOF

# 创建其他文件夹
mkdir mnt proc root sys tmp var

cd ..

接下来需要制作根文件系统,有两种方式

方式一:模拟SD卡方式启动

这种方式非常简单,但不适合需要经常修改文件系统中的文件的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建SD卡根文件系统镜像
sudo mkdir /mnt/rootfs
sudo chmod 777 /mnt/rootfs
dd if=/dev/zero of=rootfs-arm32.ext3 bs=1M count=64
mkfs.ext3 rootfs-arm32.ext3
sudo mount -t ext3 rootfs-arm32.ext3 /mnt/rootfs -o loop
sudo cp -rf rootfs-arm32/* /mnt/rootfs/
sudo umount /mnt/rootfs/

# 启动模拟器
qemu-system-arm -M vexpress-a9 \
-m 512M \
-kernel linux-4.14.334/arch/arm/boot/zImage \
-dtb linux-4.14.334/arch/arm/boot/dts/vexpress-v2p-ca9.dtb \
-nographic \
-append "root=/dev/mmcblk0 rw console=ttyAMA0" \
-sd rootfs-arm32.ext3

方式二:通过NFS挂载根文件系统

首先,宿主机需要安装配置nfs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 安装依赖
sudo apt-get install -y nfs-kernel-server

# 配置nfs
sudo mkdir -p /sync/rootfs
sudo chmod 777 -R /home/johnny/project/ldd4/rootfs-arm32
sudo cat >>/etc/exports <<EOF
/home/johnny/project/ldd4/rootfs-arm32 *(rw,sync,no_root_squash,no_subtree_check)
EOF
# 为解决Linux内核与NFS服务器的兼容问题
# 设置Ubuntu20.04的NFS,使之兼容NFS-V2和NFS-V3并增加调试功能。
sudo sed -i 's/\(^RPCSVCGSSDOPTS="\).*/\1--nfs-version 2,3,4 --debug --syslog"/g' /etc/default/nfs-kernel-server

# 重启nfs服务
sudo systemctl restart rpcbind
sudo systemctl restart nfs-kernel-server

配置主机网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 安装依赖
sudo apt-get install -y uml-utilities bridge-utils

# 配置网桥
cat > /etc/network/interfaces <<EOF
auto lo
iface lo inet loopback

auto eth0

auto br0
iface br0 inet dhcp
bridge_ports eth0
EOF

# 创建tap0网卡,用于连接qemu虚拟开发板
sudo tunctl -u root -t tap0
sudo ifconfig tap0 172.16.16.10 promisc up

启动模拟器

1
2
3
4
5
6
7
8
9
sudo qemu-system-arm \
-M vexpress-a9 \
-m 512M \
-kernel linux-4.14.334/arch/arm/boot/zImage \
-dtb linux-4.14.334/arch/arm/boot/dts/vexpress-v2p-ca9.dtb \
-net tap,ifname=tap0,script=no,downscript=no,id=net0 \
-net nic,macaddr=00:16:3e:00:00:01 \
-nographic \
-append "root=/dev/nfs rw nfsroot=172.16.16.10:/home/johnny/project/ldd4/rootfs-arm32,proto=tcp,nfsvers=3,nolock init=/linuxrc console=ttyAMA0 ip=172.16.16.20"

100ask-qemu

目前只支持ubuntu 16.04和18.04,需要拥有桌面环境。

已经支持模拟网卡、LCD显示、LED灯、按键、AT24CXX I2C存储芯片的功能

具体参考百问网嵌入式Linux

Linux驱动开发

与通用主机的差异是使用的内核与交叉编译工具不一样,详见Makefile文件

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
ifeq ($(KERNELRELEASE),)

ifeq ($(ARCH),arm)
KERNELDIR ?= /home/johnny/project/ldd4/linux-4.14.334
OBJECTDIR ?= /home/johnny/project/ldd4/objects/vexpress-v2p-ca9
ROOTFS ?= /home/johnny/project/ldd4/rootfs-arm32
CROSS_COMPILE ?= arm-linux-gnueabi-
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)

modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) CROSS_COMPILE=$(CROSS_COMPILE) O=$(OBJECTDIR) modules

modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) CROSS_COMPILE=$(CROSS_COMPILE) O=$(OBJECTDIR) modules INSTALL_MOD_PATH=$(ROOTFS) modules_install

clean:
rm -rf *.o *.ko .*.cmd *.mod* modules.order Module.symvers .tmp_versions

else
obj-m += globalfifo.o

endif

环境搭建

Linux驱动开发依赖驱动运行内核的版本和对应的编译工具。内核版本查看使用命令uname -r

对于嵌入式来说,一般都是源码编译,编译驱动时需要指定目标系统的内核源码。此外,需要设置交叉编译工具变量CROSS_COMPILE为指定的工具。

对于通用内核来说,可以使用包管理工具直接下载内核,例如在Ubuntu系统中使用命令sudo apt-get install -y linux-headers-$(uname -r)。默认内核安装路径在/lib/modules/$(uname -r)。编译驱动时,需要将内核路径指定到/lib/modules/$(uname -r)/build。对应的头文件在路径/usr/src/linux-headers-$(uname -r)/include中。

对于wsl来说,与通用内核类似,只是内核需要单独从微软wsl2内核仓库下载。然后手动进行编译安装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装编译wsl特供内核依赖
sudo apt-get install -y libelf-dev build-essential pkg-config bison build-essential flex libssl-dev libelf-dev bc dwarves

# 解压进入到目录
tar -xvf WSL2-Linux-Kernel-linux-msft-wsl-5.15.90.1.tar.gz
cd WSL2-Linux-Kernel-linux-msft-wsl-5.15.90.1
cp Microsoft/config-wsl .config

# 编译安装
sudo make scripts
sudo make modules -j$(nproc)
sudo make modules_install

cd ..

驱动简介

Linux驱动可以使用多种方式加载到系统中。

基本框架

hello.c文件的内容如下

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <linux/module.h> // 包含内核编程最常用的函数声明,如printk
#include <linux/kernel.h> // 包含模块编程相关的宏定义,如:MODULE_LICENSE

/*
init初始化函数在模块被插入进内核时调用,主要作用为驱动功能做好预备工作被称为模块的入口函数

__init的作用 :
1. 一个宏,展开后为:__attribute__ ((__section__ (".init.text"))) 实际是gcc的一个特殊链接标记
2. 指示链接器将该函数放置在 .init.text区段
3. 在模块插入时方便内核从ko文件指定位置读取入口函数的指令到特定内存位置
*/
int __init hello_init(void)
{
// 具体初始化逻辑
printk("hello module init.\n");
return 0;
}

/*
module_init 宏
1. 用法:module_init(模块入口函数名)
2. 动态加载模块,对应函数被调用
3. 静态加载模块,内核启动过程中对应函数被调用
4. 对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.initcall段),方便系统初始化统一调用。
5. 对于动态加载的模块,由于内核模块的默认入口函数名是init_module,用该宏可以给对应模块入口函数起别名
*/
module_init(hello_init);

/*
exit退出函数在模块从内核中被移除时调用,主要作用做些init函数的反操作被称为模块的出口函数

__exit的作用:
1. 一个宏,展开后为:__attribute__ ((__section__ (".exit.text"))) 实际也是gcc的一个特殊链接标记
2. 指示链接器将该函数放置在 .exit.text区段
3. 在模块插入时方便内核从ko文件指定位置读取出口函数的指令到另一个特定内存位置
*/
void __exit hello_exit(void)
{
// 具体反初始化逻辑
printk("hello module exit.\n");
}

/*
module_exit宏
1. 用法:module_exit(模块出口函数名)
2. 动态加载的模块在卸载时,对应函数被调用
3. 静态加载的模块可以认为在系统退出时,对应函数被调用,实际上对应函数被忽略
4. 对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.exitcall段),方便系统必要时统一调用,实际上该宏在静态加载时没有意义,因为静态编译的驱动无法卸载。
5. 对于动态加载的模块,由于内核模块的默认出口函数名是cleanup_module,用该宏可以给对应模块出口函数起别名
*/
module_exit(hello_exit);

/*
MODULE_LICENSE(字符串常量);
字符串常量内容为源码的许可证协议 可以是"GPL" "GPL v2" "GPL and additional rights" "Dual BSD/GPL" "Dual MIT/GPL" "Dual MPL/GPL"等, "GPL"最常用
其本质也是一个宏,宏体也是一个特殊链接标记,指示链接器在ko文件指定位置说明本模块源码遵循的许可证
在模块插入到内核时,内核会检查新模块的许可证是不是也遵循GPL协议,如果发现不遵循GPL,则在插入模块时打印抱怨信息:
myhello:module license 'unspecified' taints kernel
Disabling lock debugging due to kernel taint
也会导致新模块没法使用一些内核其它模块提供的高级功能
*/
MODULE_LICENSE("GPL");

Makefile文件的内容如下

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
ifeq ($(KERNELRELEASE),)

ifeq ($(ARCH),arm)
KERNELDIR ?= /root/ldd4/linux-4.14.334
# OBJECTDIR ?= /root/ldd4/objects/vexpress-v2p-ca9
ROOTFS ?= /root/ldd4/rootfs-arm32
CROSS_COMPILE ?= arm-linux-gnueabi-
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)

modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) CROSS_COMPILE=$(CROSS_COMPILE) O=$(OBJECTDIR) modules

modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) CROSS_COMPILE=$(CROSS_COMPILE) O=$(OBJECTDIR) modules INSTALL_MOD_PATH=$(ROOTFS) modules_install

clean:
rm -rf *.o *.ko .*.cmd *.mod* modules.order Module.symvers .tmp_versions

else
obj-m += hello.o

endif

驱动编译成功后,使用附录中的常用命令进行测试。

字符设备驱动

Linux字符设备会涉及到关键数据结构cdevfile_operations结构体的操作方法。

cdev定义在文件include/linux/cdev.h中,主要描述设备驱动基本信息。

file_operations定义在文件include/linux/fs.h中,主要描述设备驱动提供的基本接口函数,比如open、read、write、llseek、unlocked_ioctl等基本操作函数。

关键函数

初始化基本流程为:注册设备号->申请驱动内存->初始化与新增字符设备描述符

注销基本流程为:删除字符设备描述符->释放驱动内存->删除设备号

设备号

1
2
3
4
5
6
7
typedef u32 dev_t;
// 生成设备号
#define MKDEV(ma,minor) (((ma) << 20) | (mi))
// 获取主设备号
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
// 获取次设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))

dev_t为设备编号类型,为32位无符号整数。

  • ma为主设备号major
  • mi为次设备号minor

在创建设备节点时,通过设备号来绑定驱动模块。

1
2
3
4
5
6
// 静态注册设备号
int register_chrdev_region(dev_t dev, unsigned count, const char *name);
// 动态申请设备号
int alloc_chrdev_region(dev_t *dev, unsigned minor, unsigned count, const char *name);
// 注销设备号
void unregister_chrdev_region(dev_t dev, unsigned count);
  • dev为设备编号,动态申请时通过指针传值方式返回
  • minor为需要分配的起始次设备号
  • count为需要分配的设备数量,主设备号一样,次设备号依次累加
  • name为设备驱动名称,可以在/proc/devices中查看

驱动内存

值得注意的是,设备驱动运行在内核空间,因此内存需要使用内核内存管理函数。

1
2
3
4
// 申请内存并置零。
void *kzalloc(size_t size, gfp_t flags);
// 内存释放
void kfree(const void *p)
  • size申请内存大小
  • flags内存标志位,这里使用GFP_KERNEL
  • p内存指针指向待释放的内存

字符设备

1
2
3
4
5
6
// 初始化字符设备描述符
void cdev_init(struct cdev *cdev, const struct file_operations *ops);
// 新增字符设备描述符
int cdev_add(struct cdev *cdev, dev_t dev, unsigned count);
// 删除字符设备描述符
void cdev_del(struct cdev *cdev);
  • cdev字符设备描述符
  • ops文件操作描述符
  • dev设备编号

其他宏

名称作用必选
module_init导出设备驱动的初始化函数
module_exit导出设备驱动的退出函数
module_param导出设备驱动参数
MODULE_LICENSE声明许可信息
MODULE_AUTHOR声明作者信息
MODULE_DESCRIPTION声明描述信息

简单的模拟缓存设备

支持最多创建10个设备节点,每个节点可以读取或写入数据。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

#define GLOBALMEM_SIZE 0x1000
#define GLOBALMEM_MAJOR 230
#define GLOBALMEM_MINOR 0
#define DEVICE_NUM 10

// 为避免多个设备命令污染,Linux推荐使用_IO _IOR _IOW _IOWR来定义ioctl的命令
// 已经定义的设备类型可以见内核文档Documentation/ioctl/ioctl-number.txt
// 内核预定义的控制命令不会被设备驱动处理,这些定义在include/uapi/asm-generic/ioctls.h
#define GLOBALMEM_MAGIC 'g'
#define MEM_CLEAR _IO(GLOBALMEM_MAGIC, 0)

static int globalmem_major = GLOBALMEM_MAJOR;
module_param(globalmem_major, int, S_IRUGO);

typedef struct globalmem_dev
{
// 设备号
dev_t id;
// 字符设备
struct cdev cd;
// 模拟的设备内存
unsigned char mem[GLOBALMEM_SIZE];
} globalmem_dev_t;

globalmem_dev_t *globalmem_devp;

// 设备驱动的打开函数
static int globalmem_open(struct inode *inode, struct file *filp)
{
globalmem_dev_t *dev = container_of(inode->i_cdev, globalmem_dev_t, cd);
filp->private_data = dev;
return 0;
}

// 设备驱动的释放函数
static int globalmem_release(struct inode *, struct file *)
{
return 0;
}

// 设备驱动的I/O控制函数
static long globalmem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
globalmem_dev_t *dev = filp->private_data;

switch (cmd)
{
case MEM_CLEAR:
memset(dev->mem, 0, GLOBALMEM_SIZE);
printk(KERN_INFO "globalmem is set to zero\n");
break;

default:
return -EINVAL;
}

return 0;
}

// 设备驱动的读操作。*ppos是要读的位置相对于内存开头的偏移,如果大于或等于GLOBALMEM_SIZE,则会返回0(EOF)
static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
globalmem_dev_t *dev = filp->private_data;

if (p >= GLOBALMEM_SIZE)
return 0;
if (count > GLOBALMEM_SIZE - p)
count = GLOBALMEM_SIZE - p;

if (copy_to_user(buf, dev->mem + p, count))
{
ret = -EFAULT;
}
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
}

return ret;
}
// 驱动设备的写操作
static ssize_t globalmem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
globalmem_dev_t *dev = filp->private_data;

if (p > GLOBALMEM_SIZE)
return 0;
if (count > GLOBALMEM_SIZE - p)
count = GLOBALMEM_SIZE - p;

if (copy_from_user(dev->mem + p, buf, count))
ret = -EFAULT;
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
}

return ret;
}

// 设备驱动的定位操作。
static loff_t globalmem_llseek_impl(struct file *filp, loff_t offset, int orig)
{
loff_t ret = 0;
loff_t f_pos = 0;

switch (orig)
{
case 1: /* 从内存当前位置开始seek */
f_pos = filp->f_pos;
case 0: /* 从内存开头位置seek */
if ((f_pos + offset) < 0)
{
ret = -EINVAL;
break;
}
if ((f_pos + offset) > GLOBALMEM_SIZE)
{
ret = -EINVAL;
break;
}
filp->f_pos = f_pos;
ret = filp->f_pos;
break;

default:
ret = -EINVAL;
break;
}

return ret;
}

// 设备驱动的文件操作结构体
static const struct file_operations globalmem_fops = {
.owner = THIS_MODULE,
.llseek = globalmem_llseek_impl,
.read = globalmem_read,
.write = globalmem_write,
.unlocked_ioctl = globalmem_ioctl,
.open = globalmem_open,
.release = globalmem_release,
};

// cdev的初始化和添加
static void globalmem_setup_cdev(globalmem_dev_t *dev)
{
int err;
cdev_init(&dev->cd, &globalmem_fops);
dev->cd.owner = THIS_MODULE;
err = cdev_add(&dev->cd, dev->id, 1);
if (err)
printk(KERN_NOTICE "Error %d adding globalmem", err);
}

// 设备驱动的初始化函数
static int __init globalmem_init(void)
{
int ret;
int i;

// 设备号的申请
dev_t id = MKDEV(globalmem_major, GLOBALMEM_MINOR);
if (globalmem_major)
// 1. 静态申请设备号
ret = register_chrdev_region(id, DEVICE_NUM, "globalmemND");
else
{
// 2. 动态申请设备号
ret = alloc_chrdev_region(&id, 0, DEVICE_NUM, "globalmemND");
globalmem_major = MAJOR(id);
}
if (ret < 0)
return ret;

// 从内核中申请一份globalmem_dev的内存并清零
globalmem_devp = kzalloc(sizeof(globalmem_dev_t) * DEVICE_NUM, GFP_KERNEL);
if (!globalmem_devp)
{
ret = -ENOMEM;
goto fail_malloc;
}

for (i = 0; i < DEVICE_NUM; ++i)
{
(globalmem_devp + i)->id = MKDEV(globalmem_major, i);
globalmem_setup_cdev(globalmem_devp + i);
}
return 0;

fail_malloc:
unregister_chrdev_region(id, DEVICE_NUM);
return ret;
}
// 导出设备驱动的初始化函数
module_init(globalmem_init);

// 设备驱动的退出函数
static void __exit globalmem_exit(void)
{
int i;
for (i = 0; i < DEVICE_NUM; ++i)
cdev_del(&(globalmem_devp + i)->cd);
kfree(globalmem_devp);
unregister_chrdev_region(globalmem_devp->id, DEVICE_NUM);
}
// 导出设备驱动的退出函数
module_exit(globalmem_exit);

// 作者版权声明
MODULE_AUTHOR("johnny <johnny@gmail.com>");
MODULE_LICENSE("GPL v2");

阻塞和非阻塞

异步通知和异步I/O

异步通知

异步通知使用Linux信号机制。

设备驱动中使用异步通知,主要用到一个数据结构和两个函数。

1
2
3
4
5
6
7
// 异步通知数据结构
struct fasync_struct;
// 处理标志变更
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
// 释放信号函数
void kill_fasync(struct fasync_struct **fa, int sig, int band);

异步I/O

Linux内核AIO。

AIO无法解决系统调用问题,已经被摒弃,使用io_uring替代AIO机制。一文图解原理|Linux I/O 神器之 io_uring

中断与时钟

Linux将中断处理程序分解为两个半部:顶半部(Top Half)和底半部(Bottom Half)。

ARM Linux默认情况下,中断都是在CPU0上产生的,需要通过接口irq_set_affinity把中断irq设定到CPU i上去。

顶半部用于完成尽量少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态,并在清除中断标志后就进行“登记中断”的工作。

中断处理工作的重心就落在了底半部的头上,需用它来完成中断事件的绝大多数任务。

关键函数

顶半部

1
2
3
4
5
6
7
8
9
10
11
12
typedef irqreturn_t (*irq_handler_t)(int, void *);
typedef int irqreturn_t;
// 申请irq
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
// 申请irq。区别是devm_开头的API申请的是内核“managed”的资源,一般不需要在出错处理和remove()接口里再显式的释放。
int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id);
// 释放irq
void free_irq(unsigned int irq,void *dev_id);
// 屏蔽使能中断源
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);

中断共享需要在申请时,增加IRQF_SHARED标志。

底半部

Linux实现底半部的机制主要有tasklet、工作队列、软中断和线程化irq。

内核定时器

定时器

timer_list

工作队列

内核延时

短延时

1
2
3
4
5
6
7
8
9
10
// 忙等待
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
// 睡眠
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);
// 睡着延时
schedule_timeout

内存与I/O访问

x86处理器中存在I/O空间的概念,而大多数嵌入式微处理器中并不提供I/O空间。

Linux内存管理

在Linux系统中,进程的4GB内存空间被分为两个部分——用户空间与内核空间。用户空间的地址一般分布为03GB(即PAGE_OFFSET,在0x86中它等于0xC0000000),而34GB为内核空间。用户进程通常只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。用户进程只能通过系统调用等方式才可以访问到内核空间。

内核地址空间又被划分为物理内存映射区、虚拟内存分配区、高端页面映射区、专用页面映射区和系统保留映射区这几个区域。

对于x86系统而言,一般情况下,物理内存映射区最大长度为896MB。当系统物理内存大于896MB时,超过物理内存映射区的那部分内存称为高端内存。

内核空间最顶部FIXADDR_TOP~4GB的区域作为保留区。

紧接着最顶端的保留区以下的一段区域为专用页面映射区(FIXADDR_START~FIXADDR_TOP)。

virt_to_phys()和phys_to_virt()方法仅适用于DMA和常规区域,高端内存的虚拟地址与物理地址之间不存在如此简单的换算关系。

内存存取

用户空间动态申请

1
2
malloc
free

内核空间动态申请

1
2
3
4
5
6
7
8
9
// 依赖底层__get_free_pages()来实现,分配标志的前缀GFP正好是这个底层函数的缩写。
// 最常用的分配标志是GFP_KERNEL,其含义是在内核空间的进程中申请内存。
// 使用GFP_KERNEL标志申请内存时,若暂时不能满足,则进程会睡眠等待页,即会引起阻塞,因此不能在中断上下文或持有自旋锁的时候使用GFP_KERNE申请内存。
// 使用GFP_ATOMIC标志申请内存时,若不存在空闲页,则不等待,直接返回。
void *kmalloc(size_t size, int flags);
kfree
// 一般只为存在于软件中(没有对应的硬件意义)的较大的顺序缓冲区分配内存
void *vmalloc(unsigned long size);
void vfree(void * addr);

slab缓存

完全使用页为单元申请和释放内存容易导致浪费(如果要申请少量字节,也需要用1页);另一方面,在操作系统的运作过程中,经常会涉及大量对象的重复生成、使用和释放内存问题。如果我们能够用合适的方法使得对象在前后两次被使用时分配在同一块内存或同一类内存空间且保留了基本的数据结构,就可以大大提高效率。slab算法就是针对上述特点设计的。

1
2
3
4
5
6
7
// 创建slab缓存
struct kmem_cache *kmem_cache_create(const char *name, size_t size,size_t align, unsigned long flags,void (*ctor)(void*, struct kmem_cache *, unsigned long),void (*dtor)(void*, struct kmem_cache *, unsigned long));
// 分配和释放slab缓存
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
void kmem_cache_free(struct kmem_cache *cachep, void *objp);
// 回收slab缓存
int kmem_cache_destroy(struct kmem_cache *cachep);

内存池

内存池技术也是一种非常经典的用于分配大量小对象的后备缓存技术。

1
2
3
4
5
6
7
8
9
// 创建内存池
mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn,mempool_free_t *free_fn, void *pool_data);
typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data); // 标准对象分配的函数指针
typedef void (mempool_free_t)(void *element, void *pool_data); // 标准对象回收的函数指针
// 内存池中分配和回收对象
void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);
// 回收内存池
void mempool_destroy(mempool_t *pool);

I/O端口和I/O内存

当位于I/O空间时,通常被称为I/O端口;当位于内存空间时,对应的内存空间被称为I/O内存。

I/O端口访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 读写字节端口(8位宽)
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
// 读写字端口(16位宽)
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
// 读写长字端口(32位宽)
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
// 读写一串字节
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
// 读写一串字
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
// 读写一串长字
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);

I/O内存访问

在内核中访问I/O内存(通常是芯片内部的各个I2C、SPI、USB等控制器的寄存器或者外部内存总线上的设备)之前,需首先使用ioremap()函数将设备所处的物理地址映射到虚拟地址上。

1
2
3
4
5
6
// 返回一个特殊的虚拟地址,该地址可用来存取特定的物理地址范围,这个虚拟地址位于vmalloc映射区域。
// 通过devm_ioremap进行的映射通常不需要在驱动退出和出错处理的时候进行iounmap
void *ioremap(unsigned long offset, unsigned long size);
void __iomem *devm_ioremap(struct device *dev, resource_size_t offset,unsigned long size);
// 释放ioremap映射的地址
void iounmap(void * addr);

是Linux内核推荐用一组标准的API来完成设备内存映射的虚拟地址的读写。

没有_relaxed后缀的版本与有_relaxed后缀的版本的区别是前者包含一个内存屏障。

以下分别是读写8bit、16bit、32bit的寄存器的版本。

1
2
3
4
5
6
7
8
// 读寄存器
#define readb(c) ({ u8 __v = readb_relaxed(c); __iormb(); __v; })
#define readw(c) ({ u16__v = readw_relaxed(c); __iormb(); __v; })
#define readl(c) ({ u32 __v = readl_relaxed(c); __iormb(); __v; })
// 写寄存器
#define writeb(v,c) ({ __iowmb(); writeb_relaxed(v,c); })
#define writew(v,c) ({ __iowmb(); writew_relaxed(v,c); })
#define writel(v,c) ({ __iowmb(); writel_relaxed(v,c); })

申请释放I/O端口和I/O内存

Linux内核提供了一组函数以申请和释放I/O端口,表明该驱动要访问这片区域。

1
2
3
4
5
// 申请I/O端口
// 变体devm_request_region
struct resource *request_region(unsigned long first, unsigned long n, const char *name);
// 归还I/O端口
void release_region(unsigned long start, unsigned long n);

Linux内核也提供了一组函数以申请和释放I/O内存的范围。此处的“申请”表明该驱动要访问这片区域,它不会做任何内存映射的动作,更多的是类似于“reservation”的概念。

1
2
3
4
5
// 申请I/O内存
// 变体devm_request_mem_region
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
// 归还I/O内存
void release_mem_region(unsigned long start, unsigned long len);

I/O端口和I/O内存访问流程

I/O端口访问的一种途径是直接使用I/O端口操作函数:在设备打开或驱动模块被加载时申请I/O端口区域,之后使用inb()、outb()等进行端口访问,最后,在设备关闭或驱动被卸载时释放I/O端口范围。

I/O内存的访问步骤,首先是调用request_mem_region()申请资源,接着将寄存器地址通过ioremap()映射到内核空间虚拟地址,之后就可以通过Linux设备访问编程接口访问这些设备的寄存器了。访问完成后,应对ioremap()申请的虚拟地址进行释放,并释放release_mem_region()申请的I/O内存资源。

设备地址映射到用户空间

一般情况下,用户空间是不可能也不应该直接访问设备的,但是,设备驱动程序中可实现mmap()函数,这个函数可使得用户空间能直接访问设备的物理地址。

mmap()必须以PAGE_SIZE为单位进行映射。

1
2
// 驱动中mmap原型
int(*mmap)(struct file *, struct vm_area_struct*);

vm_operations_struct结构体的实体会在file_operations的mmap()成员函数里被赋值给相应的vma->vm_ops。一般open()函数也通常在mmap()里调用,close()函数会在用户调用munmap()的时候被调用到。

1
2
// 创建页表项
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,unsigned long pfn, unsigned long size, pgprot_t prot);

I/O内存被映射时需要是nocache的,这时候,我们应该对vma->vm_page_prot设置nocache标志之后再映射。

当访问的页不在内存里,即发生缺页异常时,fault()会被内核自动调用,而fault()的具体行为可以自定义。

I/O内存静态映射

在将Linux移植到目标电路板的过程中,有得会建立外设I/O内存物理地址到虚拟地址的静态映射,这个映射通过在与电路板对应的map_desc结构体数组中添加新的成员来完成。

1
2
3
4
5
6
7
8
struct map_desc
{
unsigned long virtual; /* 虚拟地址 */
unsigned long pfn; /* __phys_to_pfn(phy_addr) */
unsigned long length; /* 大小 */
unsigned int type; /* 类型 */
};
// 然后通过函数iotable_init(struct map_desc&, size_t)建立映射。

驱动工程师可以对非常规内存区域的I/O内存(外设控制器寄存器、MCU内部集成的外设控制器寄存器等)依照电路板的资源使用情况添加到map_desc数组中,但是目前该方法已经不值得推荐。

DMA

DMA是一种无须CPU的参与就可以让外设与系统内存之间进行双向数据传输的硬件机制。

DMA方式的数据传输由DMA控制器(DMAC)控制,在传输期间,CPU可以并发地执行其他任务。当DMA结束后,DMAC通过中断通知CPU数据传输已经结束,然后由CPU执行相应的中断服务程序进行后处理。

DMA与Cache一致性

如果DMA的目的地址与Cache所缓存的内存地址访问有重叠,经过DMA操作,与Cache缓存对应的内存中的数据已经被修改,而CPU本身并不知道,它仍然认为Cache中的数据就是内存中的数据。这样就会发生Cache与内存之间数据“不一致性”的错误。

Cache的不一致性问题并不是只发生在DMA的情况下,实际上,它还存在于Cache使能和关闭的时刻。

Linux下的DMA编程

申请DMA缓冲区时应使用GFP_DMA标志,这样能保证获得的内存位于DMA区域中,并具备DMA能力。

在内核中定义了__get_free_pages()针对DMA的“快捷方式”__get_dma_pages(),它在申请标志中添加了GFP_DMA。

如果不想使用log2size(即order)为参数申请DMA内存,则可以使用另一个函数dma_mem_alloc()。

内核提供了如下函数以进行简单的虚拟地址/总线地址转换

1
2
unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned long address);

在使用IOMMU或反弹缓冲区的情况下,上述函数一般不会正常工作。而且,这两个函数并不建议使用。

设备并不一定能在所有的内存地址上执行DMA操作,在这种情况下应该通过下列函数执行DMA地址掩码

1
int dma_set_mask(struct device *dev, u64 mask);

内核中提供了如下函数以分配一个DMA一致性的内存区域

1
2
3
4
5
6
7
8
9
10
11
12
13
// 申请Cache一致的DMA缓冲区
void * dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle,gfp_t gfp);
// 释放Cache一致的DMA缓冲区
void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr,dma_addr_t handle);

// 申请写合并的DMA缓冲区
void * dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t*handle, gfp_t gfp);
#define dma_free_writecombine(dev,size,cpu_addr,handle) \
dma_free_coherent(dev,size,cpu_addr,handle)

// PCI设备申请DMA缓冲区
void * pci_alloc_consistent(struct pci_dev *pdev, size_t size, dma_addr_t *dma_addrp);
void pci_free_consistent(struct pci_dev *pdev, size_t size, void *cpu_addr,dma_addr_t dma_addr);

缓冲区来自内核的较上层(如网卡驱动中的网络报文、块设备驱动中要写入设备的数据等),上层很可能用普通的kmalloc()、__get_free_pages()等方法申请,这时候就要使用流式DMA映射。

对于单个已经分配的缓冲区而言,使用dma_map_single()可实现流式DMA映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// DMA映射。第4个参数为DMA的方向,可能的值包括DMA_TO_DEVICE、DMA_FROM_DEVICE、DMA_BIDIRECTIONAL和DMA_NONE
dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size,enum dma_data_direction direction);
// DMA反映射
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size,enum dma_data_direction direction);

// 获得DMA缓冲区的拥有权
void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr,size_t size, enum dma_data_direction direction);
// 将其所有权返还给设备
void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr,size_t size, enum dma_data_direction direction);

// 映射SG
int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents,enum dma_data_direction direction);
// 去除映射SG
void dma_unmap_sg(struct device *dev, struct scatterlist *list,int nents, enum dma_data_direction direction);
// 返回scatterlist对应的缓冲区的总线地址和缓冲区的长度
dma_addr_t sg_dma_address(struct scatterlist *sg);
unsigned int sg_dma_len(struct scatterlist *sg);

// 获得DMA缓冲区的拥有权
void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg,int nents, enum dma_data_direction direction);
// 将其所有权返还给设备
void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg,int nents, enum dma_data_direction direction);

Linux内核目前推荐使用dmaengine的驱动架构来编写DMA控制器的驱动,同时外设的驱动使用标准的dmaengine API进行DMA的准备、发起和完成时的回调工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 申请DMA通道
struct dma_chan *dma_request_slave_channel(struct device *dev, const char *name);
struct dma_chan *__dma_request_channel(const dma_cap_mask_t *mask,dma_filter_fn fn, void *fn_param);
// 释放DMA通道
void dma_release_channel(struct dma_chan *chan);

// DMA完成回调函数原型:void (dma_fini_callback)(void*)

// 申请DMA描述符,然后填充callback和callback_param参数
dmaengine_prep_slave_single
// 把描述符插入队列
dmaengine_submit
// 发起DMA动作
dma_async_issue_pending

Linux设备驱动的软件架构思想

让驱动以某种标准方法拿到这些平台信息呢Linux总线、设备和驱动模型实际上可以做到这一点,驱动只管驱动,设备只管设备,总线则负责匹配设备和驱动,而驱动则以标准途径拿到板级信息。

一个现实的Linux设备和驱动通常都需要挂接在一种总线上,对于本身依附于PCI、USB、I2C、SPI等的设备而言,这自然不是问题。在SoC系统中集成的独立外设控制器、挂接在SoC内存空间的外设等却不依附于此类总线。Linux发明了一种虚拟的总线,称为platform总线,相应的设备称为platform_device,而驱动成为platform_driver。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
struct platform_device
{
const char *name;
int id;
bool id_auto;
struct devicedev;
u32 num_resources;
struct resource *resource;

const struct platform_device_id *id_entry;
char *driver_override; /* Driver name to force a match */

/* MFD cell pointer */
struct mfd_cell *mfd_cell;

/* arch specific additions */
struct pdev_archdata archdata;
};

struct platform_driver
{
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device * pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};

struct device_driver
{
const char *name;
struct bus_type *bus;

struct module *owner;
const char *mod_name; /* used for built-in modules */

bool suppress_bind_attrs; /* disables bind/unbind via sysfs */

const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;

int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
const struct attribute_group **groups;

const struct dev_pm_ops *pm;

struct driver_private *p;
};

与platform_driver地位对等的i2c_driver、spi_driver、usb_driver、pci_driver中都包含了device_driver结构体实例成员。它其实描述了各种xxx_driver(xxx是总线名)在驱动意义上的一些共性。

资源本身由resource结构体描述

1
2
3
4
5
6
7
8
struct resource
{
resource__size_t start;
resource_size_t end;
const char *name;
unsigned long flags; // 值可以为IORESOURCE_IO、IORESOURCE_MEM、IORESOURCE_IRQ、IORE-SOURCE_DMA等
struct resource *parent, *sibling, *child;
};

对resource的定义也通常在BSP的板文件中进行,而在具体的设备驱动中通过platform_get_resource()这样的API来获取

1
2
3
4
// 获取资源通用接口
struct resource *platform_get_resource(struct platform_device *, unsigned int,unsigned int);
// 获取IRQ资源封装接口,相当于platform_get_resource(dev, IORESOURCE_IRQ, num);
int platform_get_irq(struct platform_device *dev, unsigned int num);

platform也提供了platform_data的支持,platform_data的形式是由每个驱动自定义的

将globalfifo作为platform设备

globalfifo驱动挂接到platform总线上,这要完成两个工作:

  • 将globalfifo移植为platform驱动
  • 在板文件中添加globalfifo这个platform设备

移植时需要屏蔽module_init和module_exit宏定义的入口。

为了完成在板文件中添加globalfifo这个platform设备的工作,需要在板文件arch/arm/mach-<soc名>/mach-<板名>.c中添加相应的代码

1
2
3
4
static struct platform_device globalfifo_device = {
.name = "globalfifo",
.id = -1,
};

设备驱动分层思想

非常推荐使用misc类型设备驱动框架,编写字符类设备。

可以额外编写驱动触发设备驱动xxx_probe函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int __init globalfifodev_init(void)
{
int ret;

globalfifo_pdev = platform_device_alloc("globalfifo", -1);
if (!globalfifo_pdev)
return -ENOMEM;

ret = platform_device_add(globalfifo_pdev);
if (ret)
{
platform_device_put(globalfifo_pdev);
return ret;
}
return ret;
}
module_init(globalfifodev_init);

Linux块设备驱动

块设备是与字符设备并列的概念,这两类设备在Linux中的驱动结构有较大差异,总体而言,块设备驱动比字符设备驱动要复杂得多。缓冲、I/O调度、请求队列等都是与块设备驱动相关的概念。

Linux块设备驱动结构

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
// 块设备操作描述符
struct block_device_operations
{
// 打开和释放
int (*open)(struct block_device *, fmode_t);
void (*release)(struct gendisk *, fmode_t);

// I/O控制
int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);

// 介质改变,以后会被check_events取代
int (*media_changed)(struct gendisk *);

// 使介质有效
int (*revalidate_disk)(struct gendisk *);

// 获取驱动器信息
int (*getgeo)(struct block_device *, struct hd_geometry *);

// 模块指针,通常指向THIS_MODULE
struct module *owner;

// 其他
int (*rw_page)(struct block_device *, sector_t, struct page *, int rw);
int (*direct_access)(struct block_device *, sector_t, void **, unsigned long *);
unsigned int (*check_events)(struct gendisk *disk, unsigned int clearing);
void (*unlock_native_capacity)(struct gendisk *);
void (*swap_slot_free_notify)(struct block_device *, unsigned long);
};

gendisk

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
// 磁盘描述符
struct gendisk
{
// major、first_minor和minors共同表征了磁盘的主、次设备号,同一个磁盘的各个分区共享一个主设备号,而次设备号则不同
int major;
int first_minor;
int minors;

// 描述的块设备操作集合
const struct block_device_operations *fops;
// 管理这个设备的I/O请求队列的指针
struct request_queue *queue;
// 指向磁盘的任何私有数据,与字符设备驱动的private_data类似
void *private_data;
// 表示一个分区
struct hd_struct part0;
// 容纳分区表。与part0的关系:disk->part_tbl->part[0] = &disk->part0;
struct disk_part_tbl __rcu *part_tbl;

char disk_name[DISK_NAME_LEN]; /* name of major driver */
char *(*devnode)(struct gendisk *gd, umode_t *mode);

unsigned int events; /* supported events */
unsigned int async_events; /* async events, subset of all */

int flags;
struct device *driverfs_dev; // FIXME: remove
struct kobject *slave_dir;

struct timer_rand_state *random;
atomic_t sync_io; /* RAID */
struct disk_events *ev;
#ifdef CONFIG_BLK_DEV_INTEGRITY
struct blk_integrity *integrity;
#endif
int node_id;
};

操作函数

1
2
3
4
5
6
7
8
9
// 分配gendisk
struct gendisk *alloc_disk(int minors);
// 增加gendisk
void add_disk(struct gendisk *disk);
// 释放gendisk
void del_gendisk(struct gendisk *gp);
// gendisk引用计数
struct kobject *get_disk(struct gendisk *disk);
void put_disk(struct gendisk *disk);

bio、request和request_queue

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
struct bvec_iter
{
sector_t bi_sector; /* device address in 512byte sectors */
unsigned int bi_size; /* residual I/O count */
unsigned int bi_idx; /* current index into bvl_vec */
unsigned int bi_bvec_done; /* number of bytes completed in current bvec */
};

struct bio
{
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev;
unsigned long bi_flags; /* status, command, etc */
unsigned long bi_rw; /* bottom bits READ/WRITE,
* top bits priority
*/
struct bvec_iter bi_iter;
/* Number of segments in this BIO after
* physical address coalescing is performed.
*/
unsigned int bi_phys_segments;
...

struct bio_vec *bi_io_vec; /* the actual vec list */
struct bio_set *bi_pool;
/*
* We can inline a number of vecs at the end of the bio, to avoid
* double allocations for a small number of bio_vecs. This member
* MUST obviously be kept at the very end of the bio.
*/
struct bio_vec bi_inline_vecs[0];
};

与bio对应的数据每次存放的内存不一定是连续的,因此需要一个向量。向量中的每个元素实际是一个[page,offset,len],我们一般也称它为一个片段。

1
2
3
4
5
6
struct bio_vec
{
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};

I/O调度算法可将连续的bio合并成一个请求。请求是bio经由I/O调度进行调整后的结果,这是请求和bio的区别。

每个块设备或者块设备的分区都对应有自身的request_queue,从I/O调度器合并和排序出来的请求会被分发(Dispatch)到设备级的request_queue。

主要API

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
// 初始化请求队列
request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
// 清除请求队列
void blk_cleanup_queue(request_queue_t * q);
// 分配请求队列
request_queue_t *blk_alloc_queue(int gfp_mask);
// 对于RAMDISK这种完全随机访问的非机械设备,并不需要进行复杂的I/O调度。
// 使用如下函数来绑定请求队列和“制造请求”函数
void blk_queue_make_request(request_queue_t * q, make_request_fn * mfn);
// 提取请求
struct request * blk_peek_request(struct request_queue *q);
// 启动请求
void blk_start_request(struct request *req);
// 报告完成
void __blk_end_request_all(struct request *rq, int error);
void blk_end_request_all(struct request *rq, int error);
// 用blk_queue_make_request()绕开I/O调度,但是在bio处理完成后应该使用bio_endio
void bio_endio(struct bio *bio, int error);

// 如果是I/O操作故障,可以调用快捷函数bio_io_error()
#define bio_io_error(bio) bio_endio((bio), -EIO)
// 遍历一个请求的所有bio
#define __rq_for_each_bio(_bio, rq) \
if ((rq->bio)) \
for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
// 遍历一个bio的所有bio_vec
#define __bio_for_each_segment(bvl, bio, iter, start) \
for (iter = (start); \
(iter).bi_size && \
((bvl = bio_iter_iovec((bio), (iter))), 1); \
bio_advance_iter((bio), &(iter), (bvl).bv_len))
#define bio_for_each_segment(bvl, bio, iter) \
__bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)
// 迭代遍历一个请求所有bio中的所有segment
#define rq_for_each_segment(bvl, _rq, _iter) \
__rq_for_each_bio(_iter.bio, _rq) \
bio_for_each_segment(bvl, _iter.bio, _iter.iter)

I/O调度器

Linux 2.6以后的内核包含4个I/O调度器,它们分别是Noop I/O调度器(适合Flash)、Anticipatory I/O调度器、Deadline I/O调度器(适合读取多的场景,数据库)与CFQ I/O调度器(适合多媒体应用)。其中,Anticipatory I/O调度器算法已经在2010年从内核中去掉了。

可以通过给内核添加启动参数,选择所使用的I/O调度算法

1
kernel elevator=deadline

通过类似如下的命令,改变一个设备的调度器

1
$ echo SCHEDULER > /sys/block/DEVICE/queue/scheduler

Linux块设备驱动初始化

1
2
3
4
5
6
// 注册设备
// major参数是块设备要使用的主设备号,name为设备名,它会显示在/proc/devices中。
// 如果major为0,内核会自动分配一个新的主设备号,register_blkdev()函数的返回值就是这个主设备号。
int register_blkdev(unsigned int major, const char *name);
// 注销设备
int unregister_blkdev(unsigned int major, const char *name);

块设备的打开释放

块设备驱动的open()函数和其字符设备驱动的对等体不太相似,前者不以相关的inode和file结构体指针作为参数。

1
2
int (*open)(struct block_device *bdev, fmode_t mode);
void (*release)(struct gendisk *disk, fmode_t mode);

块设备ioctl函数

与字符设备驱动一样,块设备可以包含一个ioctl()函数以提供对设备的I/O控制能力。高层的块设备层代码处理了绝大多数I/O控制。例如,drivers/block/floppy.c实现了与软驱相关的命令,drivers/mmc/card/block.c实现了MMC子系统的命令处理。

块设备驱动的I/O请求处理

使用请求队列的源码见drivers/memstick/core/ms_block.c

使用请求队列对于一个机械磁盘设备而言的确有助于提高系统的性能,但是对于RAMDISK、ZRAM(Compressed RAM Block Device)等完全可真正随机访问的设备而言,无法从高级的请求队列逻辑中获益。源码见drivers/block/zram/zram_drv.c

实例:vmem_disk驱动

vmem_disk硬件原理

vmem_disk是一种模拟磁盘,其数据实际上存储在RAM中。它使用通过vmalloc()分配出来的内存空间来模拟出一个磁盘,以块设备的方式来访问这片内存。该驱动是对字符设备驱动章节中globalmem驱动的块方式改造。

加载vmem_disk.ko后,在使用默认模块参数的情况下,系统会增加4个块设备节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ ls -l /dev/vmem_disk*
brw-rw---- 1 root disk 252, 0 2月 25 14:00 /dev/vmem_diska
brw-rw---- 1 root disk 252, 16 2月 25 14:00 /dev/vmem_diskb
brw-rw---- 1 root disk 252, 32 2月 25 14:00 /dev/vmem_diskc
brw-rw---- 1 root disk 252, 48 2月 25 14:00 /dev/vmem_diskd

$ sudo mkfs.ext2 /dev/vmem_diska
mke2fs 1.42.9 (4-Feb-2014)
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
Stride=0 blocks, Stripe width=0blocks
64 inodes, 512 blocks
25 blocks (4.88%) reserved for the super user
First data block=1
Maximum filesystem blocks=524288
1 block group
8192 blocks per group, 8192fragments per group
64 inodes per group
Allocating group tables: done
Writing inode tables: done
Writing superblocks and filesystem accounting information: done

驱动开发常用项

驱动属性项

linux/device.h

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
// 设备属性处理函数
static ssize_t xxx1_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t n)
{
int input;
if (kstrtoint(buf, 10, &input))
return -EINVAL;
...
schedule_work(&work);
return n;
}
// 声明设备属性结构体dev_attr_xxx1,并赋值xxx1_store为对应的处理函数
static DEVICE_ATTR_WO(xxx1);
static struct attribute *xxx_attrs[] = {
&dev_attr_xxx1.attr,
&dev_attr_xxx2.attr,
NULL,
};
static const struct attribute_group xxx_group = {
.attrs = xxx_attrs,
};

// 在进行初始化时进行,例如probe中
// 注册设备属性
sysfs_create_group(&dev->kobj, &xxx_group);
// 移除设备属性
sysfs_remove_group(&dev->kobj, &xxx_group);

工作队列

内核驱动处理时,遇到比较费时类型的任务时,可以将任务放到工作队列,稍后在合适的时候进行处理。

1
2
3
4
5
6
7
8
// 工作队列结构体
struct work_struct{}
// 工作队列处理函数
void xxx_handle(struct work_struct *work);
// 初始化工作队列
INIT_WORK(&xxx_work, xxx_handle);
// 调度工作队列
schedule_work(&work);

附录

常用命令

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
40
41
# 加载驱动
$ sudo insmod globalmem.ko
# 卸载驱动
$ sudo rmmod globalmem

# 只查看最新的内核打印消息
$ dmesg -W

# 查看驱动主设备号
$ cat /proc/devices | grep globalmem
230 globalmem
# 查看驱动设备节点信息
$ ls -l /dev/globalmem
crwxrwxrwx 1 root root 230, 0 Dec 27 17:40 /dev/globalmem
# 查看platform驱动
ls /sys/devices/platform/globalfifo -l
total 0
lrwxrwxrwx 1 root root 0 1月 2 17:00 driver -> ../../../bus/platform/drivers/globalfifo
-rw-r--r-- 1 root root 4096 1月 2 17:03 driver_override
-r--r--r-- 1 root root 4096 1月 2 17:03 modalias
drwxr-xr-x 2 root root 0 1月 2 17:03 power
lrwxrwxrwx 1 root root 0 1月 2 17:03 subsystem -> ../../../bus/platform
-rw-r--r-- 1 root root 4096 1月 2 17:00 uevent

# 创建设备节点
$ sudo mknod /dev/globalmem c 230 0
# 删除设备节点
$ sudo unlink /dev/globalmem
# 创建支持多设备的节点
$ sudo mknod /dev/globalmem0 c 230 0
$ sudo mknod /dev/globalmem1 c 230 1

# 创建的设备普通用户没有写入权限,需要增加写入权限
# 或者给予全权限
$ sudo chmod 777 /dev/globalmem*

# 向设备写入数据
$ echo "hello world 0" >> /dev/globalmem0
# 读取设备中的数据
$ cat /dev/globalmem0
hello world 0

参考

Linux设备驱动开发详解:基于最新的Linux4.0内核.pdf

嵌入式王道长-Linux内核驱动开发

参考原文

Chapter 1 系统接口

Unix utilities实验

实验说明

启动系统

sleep

重点:使用user/user.h的sleep接口实现,单位为jiffies(1/10)。

pingpong

重点:使用pip接口通信。

primes

目的:主进程准备好2-35的数字写入管道。

从管道中读取数字n(此数字为素数),创建一个子进程,并将剩余的非n的倍数的数写入子管道中。然后进程等待子进程的退出。子进程会重复父进程的动作,直到读取的数字到达35,则不再创建子进程。

find

重点:熟悉文件属性读取,和路径拼接。

xargs

重点:使用exec接口实现,并需要构建新的参数数组。

Chapter 2 系统结构

RISC-V has three modes in which the CPU can execute instructions: machine mode, supervisor mode, and user mode.

An application can execute only user-mode instructions and is said to be running in user space, while the software in supervisor mode can also execute privileged instructions and is said to be running in kernel space.

CPUs provide a special instruction ( RISC-V provides the ecall instruction ) that switches the CPU from user mode to supervisor mode and enters the kernel at an entry point specified by the kernel.

the entire operating system resides in the kernel, so that the implementations of all system calls run in supervisor mode. This organization is called a monolithic kernel.

OS designers can minimize the amount of operating system code that runs in supervisor mode, and execute the bulk of the operating system in user mode. This kernel organization is called a microkernel.

源码文件功能描述如下

文件名功能描述
bio.cDisk block cache for the file system.
console.cConnect to the user keyboard and screen.
entry.SVery first boot instructions.
exec.cexec() system call.
file.cFile descriptor support.
fs.cFile system.
kalloc.cPhysical page allocator.
kernelvec.SHandle traps from kernel, and timer interrupts.
log.cFile system logging and crash recovery.
main.cControl initialization of other modules during boot.
pipe.cPipes.
plic.cRISC-V interrupt controller.
printf.cFormatted output to the console.
proc.cProcesses and scheduling.
sleeplock.cLocks that yield the CPU.
spinlock.cLocks that don’t yield the CPU.
start.cEarly machine-mode boot code.
string.cC string and byte-array library.
swtch.SThread switching.
syscall.cDispatch system calls to handling function.
sysfile.cFile-related system calls.
sysproc.cProcess-related system calls.
trampoline.SAssembly code to switch between user and kernel.
trap.cC code to handle and return from traps and interrupts.
uart.cSerial-port console device driver.
virtio_disk.cDisk device driver.
vm.cManage page tables and address spaces.

进程虚拟空间分布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
地址    功能域

MAXVA <--------->
trampoline
<--------->
trapframe
<--------->
heap
<--------->
user stack
<--------->
user text
and data
(followed by global variables)
(Instructions come first)
0 <--------->

risc-v指针宽度为64位,但硬件只使用低39位用于在页表中寻找虚拟地址,而xv6系统中只使用了38位。因此最大地址为2^38 - 1=0x3f,ffff,ffff MAXVA(kernel/riscv.h:363)

进程重要信息存储在struct proc(kernel/proc.h:85)

RISC-V ecall instruction raises the hardware privilege level and changes the program counter to a kernel-defined entry point.

When the system call completes, the kernel switches back to the user stack and returns to user space by calling the sret instruction, which lowers the hardware privilege level and resumes executing user instructions just after the system call instruction.

启动流程:

  • boot loader将kernel加载到内存0x8000 0000
  • 在machine mode下,跳转到_entry(kernel/entry.S:7),设置堆栈,并跳转运行C代码
  • 在C代码入口函数start(kernel/start.c:21)中,切换到supervisor mode,配置时钟中断,并跳转到主函数。
  • 在主函数main(kernel/main.c:11)中,初始化设备和子系统,创建第一个进程
  • 在初始化进程userinit(kernel/proc.c:233)中,寄存器a7装载SYS_EXEC(kernel/syscall.h:8)后再次进入内核
  • 在内核系统调用处理函数syscall(kernel/syscall.c:132)中,启动/init进程
  • 系统调用完成后,返回进程init(user/init.c),创建一个新console设备文件,并打开文件描述符0,1,2。

it sets the previous privilege mode to supervisor in the register mstatus, it sets the return address to main by writing main’s address into the register mepc, disables virtual address translation in supervisor mode by writing 0 into the page-table register satp, and delegates all interrupts and exceptions to supervisor mode.

system call实验

System call tracing

新增一个trace系统调用,它接收一个整数参数,它表明哪些系统调用被标记。当被标记的系统调用返回时,需要打印<pid>: syscall <call name> -> <return value>。此标记对子进程和forks都有效,但对其他进程无效。

关键点

  • user/trace.c中设置调用trace(x)后需要再trace(0)清空进程标记;
  • user/user.h中增加用户调用系统函数int trace(int sys_mask);user/usys.pl增加trace生成相关汇编代码;
  • kernel/syscall.h新增宏编号#define SYS_trace 22kernel/sysproc.c新增标记实现函数uint64 sys_trace(void)
  • syscall增加标记打印逻辑,需要注意allocproc中共用struct proc proc[NPROC];,申请后要清空之前的标记;

Sysinfo

新增一个sysinfo系统调用,它会收集系统空闲内存字节大小freemem和正在使用的进程数量nproc。需要提供用户测试程序sysinfotest调用这个接口,若整个调用没有问题,则打印"sysinfotest: OK"

关键点

  • 需要使用接口copyout将内核空间的数据拷贝到用户空间中

练习

  1. 增加一个系统调用,返回系统剩余可用内存大小

Chapter 3 页表

创建地址空间

核心的数据结构pagetable_t kernel_pagetable(kernel/vm.c:1)中,核心的功能函数是walk,用于查找虚拟地址对应的PTE。

main中调用kvminit(kernel/vm.c:54)创建内核页表,再调用kvmmake,最终通过kvmmapmappageswalk完成物理地址虚拟地址映射。

main中调用kvminithart(kernel/vm.c:62)安装内核页表。主要讲根页表的地址设置到satp寄存器中,设置前后需要刷新TLB缓存。

The RISC-V has an instruction sfence.vma that flushes the current CPU’s TLB. Xv6 executes sfence.vma in kvminithart after reloading the satp register

物理内存分配

分配器定义在kalloc.c(kernel/kalloc.c:1)。每个空闲页的列表元素都是一个struct run

main中调用kinit(kernel/kalloc.c:27)来初始化分配器。它将初始化空闲列表kmem->freelist(kernel/kalloc.c:21)用于保存内核结束位置到PHYSTOP区间的每一页。

进程地址空间

每个进程都有独立的页表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
MAXVA   <--------->
trampoline RX--
<--------->
trapframe R-W-
<--------->
unused
<--------->
heap R-WU
<--------->
stack R-WU
<--------->
guard page
<--------->
data R-WU
<--------->
unused
<--------->
text RX-U
0 <--------->

一个进程的用户内存从虚拟地址零开始,可以增长到MAXVA(kernel/riscv.h:360),允许最大使用256GB内存。

零地址放置的text代码,没有写入权限,当异常的程序试图向零地址写入数据,会出发page fault

sbrk

sbrk是进程为调整内存时的系统调用。它由函数growproc(kernel/proc.c:260)实现。

exec

exec是一个系统调用,它可以用从文件读取的数据替换进程用户空间数据,这样的文件被称为二进制或可行性文件。函数exec(kernel/exec.c:23)会读取并解析ELF格式的文件,它包含struct elfhdrELF文件头部和一系列struct proghdr程序区域头部。每个程序区域头部描述程序必须加载到内存的位置。

/init程序区域头部像下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
$ objdump -p user/_init 

user/_init: file format elf64-little

Program Header:
0x70000003 off 0x0000000000006bac vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**0
filesz 0x0000000000000033 memsz 0x0000000000000000 flags r--
LOAD off 0x0000000000001000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**12
filesz 0x0000000000001000 memsz 0x0000000000001000 flags r-x
LOAD off 0x0000000000002000 vaddr 0x0000000000001000 paddr 0x0000000000001000 align 2**12
filesz 0x0000000000000010 memsz 0x0000000000000030 flags rw-
STACK off 0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4
filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw-

值得注意的是,头部信息中filesz可能会小于memsz,那时因为这些变量值为0,文件中无需存储,但加载时需要申请memsz大小的空间并清零。

然后,函数需要拷贝参数列表,并将堆栈和PC设置好。最后将释放旧页表,使用新页表。

练习

  1. 解析riscv的设备树,找出总共拥有多少物理内存
  2. 写一个用户程序调用sbrk(1),观察调用前后页表的变化。内核申请了多少空间?新内存的PTE包含哪些数据?
  3. 修改源码让内核使用超级页
  4. 在Unix系统中,若exec处理的可执行文件以#!开头,则会使用第一行剩余部分替换程序作为解释文件执行。修改源码让内核支持这个特性。
  5. 实现一个地址空间随机分布的内核。

Chapter 4 traps和系统调用

riscv的trap机制

Each RISC-V CPU has a set of control registers that the kernel writes to tell the CPU how to handle traps, and that the kernel can read to find out about a trap that has occurred.

在文件riscv.h(kernel/riscv.h:1)中包含系统用到的所有描述。这里列举最重要的寄存器

  • stvec: 内核写入trap处理程序的地址
  • sepc: 当trap发生时,处理器会保存pc;当从trap返回时,调用sret会从此寄存器恢复pc
  • scause: 处理器会存放一个数字描述trap的原因
  • sscratch: trap处理程序使用sscratch来避免改写用户寄存器
  • sstatus: SIE位控制设备中断是否使能;SPP位指示trap触发前是user mode还是supervisor mode;

xv6只将它们使用在计时器中断的特殊场景

硬件处理所有类型trap流程:

  • sstatus的SIE位被清零,则后面的步骤略过
  • sstatus的SIE位清零
  • 拷贝pc值到sepc
  • 保存当前模式到sstatus的SPP位
  • 设置scause
  • 模式切换为supervisor mode
  • 拷贝stvecpc
  • 继续从pc处开始执行

Note that the CPU doesn’t switch to the kernel page table, doesn’t switch to a stack in the kernel, and doesn’t save any registers other than the pc.

用户空间的traps流程

用户空间trap路径是:uservec(kernel/trampoline.S:21)->usertrap(kernel/trap.c:37)-return->usertrapret(kernel/trap.c:90)->userret(kernel/trampoline.S:101)

xv6使用trampoline页来存储stvec,它页包含uservec。trampoline页会被每个进程映射到TRAMPOLINE的地址处。

uservec函数会将32个用户寄存器存储到TRAPFRAME地址所在的trapframe结构中,然后将寄存器satp切换为内核页表,再调用usertrap函数。

usertrap(kernel/trap.c:37)函数会检测trap的原因并处理它。首先将设置stveckernelvec,以便处理内核trap。保存sepc寄存器。如果trap是系统调用,则调用syscall处理它;若是设备中断,devintr处理它;否则,是一种异常场景,调用内核终止异常的进程。若是系统调用,则在函数结束时会调用usertrapret

返回用户空间的第一步是调用usertrapret(kernel/trap.c:90)。它会将stvec设置回uservecuservec的映射地址可以通过TRAMPOLINE、trampoline和uservec计算出来(注意内核中这些是物理地址)。然后恢复pc,最后调用userret函数并将a0设置为用户页表。

userret(kernel/trampoline.S:101)函数将切换satp为用户页表,并恢复32个用户寄存器。最后调用sret返回用户空间。

系统调用流程

以initcode.S中第一个系统调用exec为例。

initcode.S在寄存器a0a1存放着exec的参数,并将系统调用编号存放在a7中。根据系统调用编号在syscalls(kernel/syscall.c:107)数组中匹配到处理函数。指令ecall会触发trap切换到内核中并引发uservecusertrapsyscall执行。

系统调用参数列表

内核trap代码将用户寄存器放在当前进程的trap frame上,可以通过内核函数argintargaddrargfd返回第n个参数,它们通过调用argraw实现(kernel/syscall.c:34)。

有些参数通过用户地址传递,fechstr(kernel/syscall.c:25)函数能够拷贝用户传递的字符串。

内核空间的traps流程

内核空间trap路径是:kernelvec(kernel/kernelvec.S:12)->kerneltrap(kernel/trap.c:135)->kernelvec(kernel/kernelvec.S:12)

若trap不是设备中断,则异常会直接导致xv6内核出panic。

当处理器遇到trap进入内核空间时,总会禁用中断,直到设置stvec后。

缺页异常

若发生在用户空间,内核将终止相关进程。若发生在内核空间,则会直接panic。

许多内核利用缺页异常来实现写时复制(copy-on-write, COW)机制。COW fork的基本方法是,为父进程和子进程初始化共享所有物理页,但将他们的全部设置为只读。当进程向某页写入数据时,会触发store page faults。此时内核需要重新申请新的一页将数据拷贝过来,并再次进行映射。一个重要的优化项是,对于缺页发生在仅从该进程中引用的,无需进行拷贝。

另一个广泛使用的特性是惰性分配(lazy allocation)。当一个应用调用sbrk请求更多内存,内核只调整其使用大小,但不会申请物理内存和创建PTEs。当这些新地址被访问时,才会申请对应的内存页并完成映射。

还有一个广泛使用的特性是需求分页(demand paging)。当大型程序启动时,内核无需将所有数据加载到内存中,而仅仅配置足够的用户地址空间,并将其设置为无效。当发生页错误时,内核将页的内容读入并映射到用户空间。

为应对程序运行时需要的空间比硬件RAM大的场景,操作系统可以实现磁盘映射(paging to disk)。

练习

  1. 配置内核页表,让内核可以直接使用用户空间地址;
  2. 实现惰性内存分配机制;
  3. 实现COW fork;
  4. 是否有方法消除TRAPFRAME页映射到每个用户地址空间?比如,修改uservec函数将32个用户寄存器存入内核栈,或将其存入proc结构中?
  5. 是否有方法消除TRAMPOLINE页映射?

中断和设备驱动

在xv6中,内核trap会处理和识别设备中断,最终交于devintr(kernel/trap.c:178)。

Many device drivers execute code in two contexts: a top half that runs in a process’s kernel thread, and a bottom half that executes at interrupt time.

终端输入

终端驱动(kernel/console.c)是一个简单的驱动框架的示例。

The UART hardware that the driver talks to is a 16550 chip [13] emulated by QEMU. On a real computer, a 16550 would manage an RS232 serial link connecting to a terminal or other computer. When running QEMU, it’s connected to your keyboard and display.

UART的基地址是0x10000000(UART0, kernel/memlayout.h:21),UART0各寄存器定义在文件(kernel/uart.c:22)中。

xv6的main调用consoleinit(kernel/console.c:182)函数,然后再调用uartinit(kernel/uart.c:53)函数来初始化UART硬件。

在init.c(user/init.c:19)打开的文件描述符,它能够读取xv6的命令。调用read系统调用,通过内核来调用consoleread(kernel/console.c:80)。它会一直等待中断并缓存数据到cons.buf中,直到整个行输入完成,会将缓存中数据拷贝给用户最终返回到用户空间。

当用户输入一个字符,UART硬件设备会想处理器产生一个中断,它会激活xv6的trap处理程序。设备中断最后会调用devintr(kernel/trap.c:178)进行处理。接着,通过PLIC硬件单元来分辨是哪个设备中断,如果是UART设备devintr会调用uartintr

uartintr(kernel/uart.c:176)会从UART硬件读取任意输入字符,并交于consoleintr(kernel/console.c:136)进行处理;consoleintr的任务就是将输入放到cons.buf,直到整行输入完成,立即唤醒consoleread

终端输出

在一个连接到终端的文件描述符的write系统调用,最终会调用uartputc(kernel/uart.c:87)。对于每个字符,会调用uartstart来开启设备发送。

驱动的并发

These calls acquire a lock, which protects the console driver’s data structures from concurrent access.

计时器中断

Xv6 uses timer interrupts to maintain its clock and to enable it to switch among compute-bound processes; the yield calls in usertrap and kerneltrap cause this switching.

RISC-V requires that timer interrupts be taken in machine mode, not supervisor mode.

代码在start.c配置接收计时器中断(kernel/start.c:63)。部分工作的目的是编写CLINT硬件(core-local interruptor),在特定延时后产生一个中断。最终,start配置mtvectimervec并使能计时器中断。

A timer interrupt can occur at any point when user or kernel code is executing; there’s no way for the kernel to disable timer interrupts during critical operations.

The basic strategy is for the handler to ask the RISC-V to raise a “software interrupt” and immediately return.

在machine mode的中断处理程序是timervec(kernel/kernelvec.S:95),它主要配置CLINT的MTIMECMP寄存器,并设置sip为2后立刻返回。

练习

  1. 修改uart.c完全不使用中断,同时也需要修改console.c
  2. 增加一个以太网卡驱动

Chapter 6 锁

Xv6 uses a number of concurrency control techniques, depending on the situation; many more are possible. This chapter focuses on a widely used technique: the lock.

竞争

On the RISC-V this instruction is amoswap r, a. amoswap reads the value at the memory address a, writes the contents of register r to that address, and puts the value it read into r.

It performs this sequence atomically, using special hardware to prevent any other CPU from using the memory address between the read and the write.

Xv6’s acquire (kernel/spinlock.c:22) uses the portable C library call __sync_lock_test_and_set, which boils down to the amoswap instruction; the return value is the old (swapped) contents of lk->locked.

acquire(kernel/spinlock.c:22)利用riscv处理器的指令amoswap.w.aq a0, a0, (s1)实现。当获取锁成功时lk->locked为1。

release(kernel/spinlock.c:47)利用riscv处理器的指令amoswap.w zero, zero, (s1)实现。当释放锁成功时lk->locked为0。

使用锁

A hard part about using locks is deciding how many locks to use and which data and invariants each lock should protect.

再入锁

It might appear that some deadlocks and lock-ordering challenges could be avoided by using re-entrant locks, which are also called recursive locks. The idea is that if the lock is held by a process and if that process attempts to acquire the lock again, then the kernel could just allow this (since the process already has the lock), instead of calling panic, as the xv6 kernel does.

锁和中断处理程序

Some xv6 spinlocks protect data that is used by both threads and interrupt handlers.

To avoid this situation, if a spinlock is used by an interrupt handler, a CPU must never hold that lock with interrupts enabled.

例如,clockintr计时器中断处理会增加ticks(kernel/trap.c:164),同时内核线程sys_sleep(kernel/sysproc.c:59)会读取ticks的值。锁tickslock会让两次访问串行化。

指令和内存顺序

It is natural to think of programs executing in the order in which source code statements appear. That’s a reasonable mental model for single-threaded code, but is incorrect when multiple threads interact through shared memory.

To tell the hardware and compiler not to re-order, xv6 uses __sync_synchronize() in both acquire (kernel/spinlock.c:22) and release (kernel/spinlock.c:47). __sync_synchronize() is a memory barrier: it tells the compiler and CPU to not reorder loads or stores across the barrier.

睡眠锁

Xv6 provides such locks in the form of sleep-locks. acquiresleep (kernel/sleeplock.c:22) yields the CPU while waiting

练习

  1. 若屏蔽kalloc(kernel/kalloc.c:69)acquirerelease的调用,会出现哪些问题?若没有看到问题,原因是什么?
  2. kfree中屏蔽锁(恢复kalloc中的锁),会出现哪些问题?
  3. 修改kalloc.c源码让内存申请支持并发,CPU不用相互等待。
  4. 使用POSIX线程进行编码。例如,实现一个并行哈希表并测试puts/gets数据量是否随着核数量的增加而增加。
  5. 在xv6中实现pthreads的子集。实现一个用户级的线程库,这样一个用户进程就可以有多个线程,并安排这些线程运行在不同的cpu上并行运行。想出一个设计,正确地处理一个线程进行阻塞系统调用,并改变其共享地址空间。

调度

复用

Xv6 multiplexes by switching each CPU from one process to another in two situations. First, xv6’s sleep and wakeup mechanism switches when a process waits for device or pipe I/O to complete, or waits for a child to exit, or waits in the sleep system call. Second, xv6 periodically forces a switch to cope with processes that compute for long periods without sleeping.

上下文切换

it just saves and restores sets of 32 RISC-V registers, called contexts.

当进程想放弃CPU时,内核线程会调用swtch来保存它的上下文并返回调度器上下文。每个上下文都包含在struct context(kernel/proc.h:2),进程的struct proc和CPU的struct cpu都包含上下文。

swtch(kernel/swtch.S:3)只保存被调用者保存的寄存器。C编译器会生成代码保存调用者保存的寄存器到栈上。

When swtch returns, it returns to the instructions pointed to by the restored ra register, that is, the instruction from which the new thread previously called swtch.

调度

当调用函数swtch后,会切换到调度器的栈。调度器会继续在循环中查找可以切换的进程,并再次调用swtch进行切换。

We just saw that xv6 holds p->lock across calls to swtch: the caller of swtch must already hold the lock, and control of the lock passes to the switched-to code.

mycpu和myproc

Xv6 maintains a struct cpu for each CPU (kernel/proc.h:22), which records the process currently running on that CPU (if any), saved registers for the CPU’s scheduler thread, and the count of nested spinlocks needed to manage interrupt disabling.

RISC-V numbers its CPUs, giving each a hartid. Xv6 ensures that each CPU’s hartid is stored in that CPU’s tp register while in the kernel. This allows mycpu to use tp to index an array of cpu structures to find the right one.

It would be more convenient if xv6 could ask the RISC-V hardware for the current hartid whenever needed, but RISC-V allows that only in machine mode, not in supervisor mode.

The return value of myproc is safe to use even if interrupts are enabled: if a timer interrupt moves the calling process to a different CPU, its struct proc pointer will stay the same.

睡眠和唤醒

Sleep and wakeup are often called sequence coordination or conditional synchronization mechanisms.

管道

A more complex example that uses sleep and wakeup to synchronize producers and consumers is xv6’s implementation of pipes. Each pipe is represented by a struct pipe, which contains a lock and a data buffer.

Let’s suppose that calls to piperead and pipewrite happen simultaneously on two different CPUs.

进程锁

The lock associated with each process (p->lock) is the most complex lock in xv6. A simple way to think about p->lock is that it must be held while reading or writing any of the following struct proc fields: p->state, p->chan, p->killed, p->xstate, and p->pid.

练习

文件系统

概述

The xv6 file system implementation is organized in seven layers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<---------------->
File descriptor
<---------------->
Pathname
<---------------->
Directory
<---------------->
Inode
<---------------->
Logging
<---------------->
Buffer cache
<---------------->
Disk
<---------------->

Disk hardware traditionally presents the data on the disk as a numbered sequence of 512-byte blocks (also called sectors): sector 0 is the first 512 bytes, sector 1 is the next, and so on.

The file system does not use block 0 (it holds the boot sector). Block 1 is called the superblock; it contains metadata about the file system (the file system size in blocks, the number of data blocks, the number of inodes, and the number of blocks in the log). Blocks starting at 2 hold the log. After the log are the inodes, with multiple inodes per block. After those come bitmap blocks tracking which data blocks are in use. The remaining blocks are data blocks

xv6文件系统结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0       <---------------->
boot
1 <---------------->
super
2 <---------------->
log

<---------------->
inodes


<---------------->
bit map
<---------------->
data

....

data
<---------------->

缓存层

代码在bio.c中,缓存层有两个任务

  • 同步访问硬盘块block。确保只有一个块的副本在内存中,且同一时间只有一个内核线程在使用它。
  • 缓存热门数据块block。

The main interface exported by the buffer cache consists of bread and bwrite. A kernel thread must release a buffer by calling brelse when it is done with it.

bread (kernel/bio.c:93) calls bget to get a buffer for the given sector (kernel/bio.c:97).

When the caller is done with a buffer, it must call brelse to release it.

日志层

One of the most interesting problems in file system design is crash recovery.

Xv6 solves the problem of crashes during file-system operations with a simple form of logging.

Once the system call has logged all of its writes, it writes a special commit record to the disk indicating that the log contains a complete operation. At that point the system call copies the writes to the on-disk file system data structures. After those writes have completed, the system call erases the log on disk.

block块分配器

File and directory content is stored in disk blocks, which must be allocated from a free pool. Xv6’s block allocator maintains a free bitmap on disk, with one bit per block.

inode层

It might refer to the on-disk data structure containing a file’s size and list of data block numbers. Or “inode” might refer to an in-memory inode

磁盘上数据展现形式

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
dinode
|----------------|
type
|----------------|
major
|----------------|
minor
|----------------|
nlink
|----------------|
size
|----------------|
address 1 --> data0_1
|----------------|
...
|----------------|
address 12 --> data0_12
|----------------|
indirect --> indirect block
|----------------| |----------------|
address 1 --> data1_1
|----------------|
...
|----------------|
address 256 --> data1_256
|----------------|

文件夹层

A directory is implemented internally much like a file.

目录名称

文件描述符层

All the open files in the system are kept in a global file table, the ftable.

The functions sys_link and sys_unlink edit directories, creating or removing references to inodes. They are another good example of the power of using transactions.

练习

附录

CH.1 Unix utilities实验代码

sleep

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "kernel/types.h"
#include "user/user.h"

int main(int argc, char *argv[])
{
int seconds;

if (argc <= 1)
{
fprintf(2, "sleep: need one arg\n");
exit(0);
}

// ticks = 1/10 seconds
seconds = 10 * atoi(argv[1]);
if (seconds < 0)
{
seconds = 0;
}

sleep(seconds);
exit(0);
}

pingpong

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
#include "kernel/types.h"
#include "user/user.h"

int main(int argc, char *argv[])
{
int p1[2]; // parent->child
int p2[2]; // child->parent
pipe(p1);
pipe(p2);
if (fork() == 0)
{
char buffer[5];
close(p1[1]);
close(p2[0]);
read(p1[0], buffer, sizeof(buffer));
printf("%d: received %s\n", getpid(), buffer);
write(p2[1], "pong", 5);
close(p1[0]);
close(p2[1]);
}
else
{
char buffer[5];
close(p1[0]);
close(p2[1]);
write(p1[1], "ping", 5);
read(p2[0], buffer, sizeof(buffer));
printf("%d: received %s\n", getpid(), buffer);
close(p1[1]);
close(p2[0]);
}
exit(0);
}

primes

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include "kernel/types.h"
#include "user/user.h"

#define MAX_NUMBER 35

int main()
{
int p0[2]; // parent->child
int n, prime;
pipe(p0);

// feeds the numbers 2 through 35
for (int i = 2; i <= MAX_NUMBER; ++i)
write(p0[1], &i, sizeof(i));

while (read(p0[0], &n, sizeof(n)))
{
printf("%d prime %d\n", getpid(), n);
prime = n;
int p1[2]; // child -> grandchild
pipe(p1);

if (fork() == 0)
{
// child
close(p0[0]);
close(p0[1]);
p0[0] = p1[0];
p0[1] = p1[1];
continue;
}
else
{
// parent
while (n < MAX_NUMBER && read(p0[0], &n, sizeof(n)))
if (n % prime != 0)
write(p1[1], &n, sizeof(n));

// close all resources
close(p0[0]);
close(p0[1]);
close(p1[0]);
close(p1[1]);
// wait children
if (n < MAX_NUMBER)
wait(0);
exit(0);
}
}

wait(0);
printf("%d exit\n", getpid());
exit(0);
}

find

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include "kernel/types.h"
#include "user/user.h"
#include "kernel/fs.h"
#include "kernel/stat.h"

void cmp_file(char *path, char *name)
{
char *str1, *str2;
for (str1 = path + strlen(path), str2 = name + strlen(name); str1 >= path && str2 >= name && *str1 == *str2; --str1, --str2)
; // printf("%c %c\n", *str1, *str2);

if ((int)(str2 - name) == -1)
printf("%s\n", path);
}

void find(char *path, char *patern)
{
int fd;
struct dirent de;
struct stat st;
char buf[512], *p;

if ((fd = open(path, 0)) < 0)
{
fprintf(2, "find: connot open %s\n", path);
return;
}

if (fstat(fd, &st) < 0)
{
fprintf(2, "find: connot stat %s\n", path);
close(fd);
return;
}

switch (st.type)
{
case T_DEVICE:
case T_FILE:
cmp_file(path, patern);
break;

case T_DIR:
if (strlen(path) + 1 + DIRSIZ + 1 > sizeof(buf))
{
printf("find: path too long\n");
break;
}

strcpy(buf, path);
p = buf + strlen(buf);
if (*(p - 1) != '/')
*p++ = '/';
while (read(fd, &de, sizeof(de)) == sizeof(de))
{
if (de.inum == 0 || strcmp(".", de.name) == 0 || strcmp("..", de.name) == 0)
continue;
memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0;
find(buf, patern);
}
break;
}

close(fd);
}

int main(int argc, char *argv[])
{
if (argc <= 2)
{
fprintf(2, "find: need more args\n");
exit(-1);
}
find(argv[1], argv[2]);
exit(0);
}

xargs

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
#include "kernel/types.h"
#include "kernel/param.h"
#include "user/user.h"

int main(int argc, char *argv[])
{
char buffer[512];
char *newarg[MAXARG];
int i;
int new_idx;
for (i = 1, new_idx = 0; i < argc; ++i)
if (argv[i][0] == '-')
i += 1;
else
newarg[new_idx++] = argv[i];
while (*gets(buffer, sizeof(buffer)) != '\0')
{
buffer[strlen(buffer) - 1] = '\0';
newarg[new_idx] = buffer;

if (fork() == 0)
{
exec(newarg[0], newarg);
exit(0);
}
wait(0);
}

exit(0);
}

CH.2 system calls实验代码

GDB-常用命令

打开GDB调试开关命令:set debug remote 1

GDB-远程序列化协议

简介

所有GDB命令和响应都是以包(packet)形式进行送。一个包的基本结构:$包数据#2字节校验和

1
$packet-data#checksum

2字节校验和是计算$#之间的所有字符数值和,并取模256后的两位十六进制数。

GDB 5.0之前的协议需要包含两字节序列编号

1
$sequence-id:packet-data#checksum

当主机或目标机接收到第一个包,预期的一个响应是确认:可以是+(表明包接收正确)或-(请求重传)

1
2
-> $packet-data#checksum
<- +

连接一旦建立,可以禁用+/-确认。

主机(GDB)发送命令,而目标机(调试目标)发送响应。

包数据不能包含非法字符#$

若包数据中可以使用, ;:作为分隔符。除特别说明,所有数字均使用十六进制表示。

GDB 5.0之前的协议不能使用:作为分隔符(与sequence-id的分隔符冲突)。

在许多包中二进制数据都以两位十六进制字符表示。

二进制数据7d(ASCII字符})作为转义字符。任何转义字符传输时需要跟一个与0x20异或后的结果。例如,单字节0x7d在传输时会转为两个字节0x7d 0x5d。常见的转义字符有0x23 # 0x24 $ 0x7d } 0x2a *

响应数据可以使用运行长度编号来节省空间。例如:编号后'0* ' 表示编码字符串'0000'*后面的空格表示重复0字符32-29=3次。

对于可打印字符#$或数值超过126的都不可使用。对于7次重复(字符$)可以使用5次(字符")来分开表示。例如,'00000000'编码后为'0*"00'

对于不支持的命令,会返回空响应$#00

在最小场景中,目标机必须支持?命令来告诉GDB停止的原因,gG命令用于寄存器访问,mM命令用于内存访问。对于单线程目标要实现c命令,并支持单步调试命令s。多线程目标需要支持vCont命令。其他所有命令都是可选的。

下表中,命令中的空格表示语义分隔,但实际数据包不需要发送。

命令说明
!使能扩展模式
?当连接第一次建立后,会被询问目标机停止的原因。回复与step和continue一样。
A arglen,argnum,arg,…初始化argv[]参数数组传递给程序。其中arglen是十六进制编码后的字节流参数arg的长度。
b baud(不推荐使用)改变串行通信速率
B addr,mode(不推荐使用,使用Z和z包替代)设置(mode是S)或清除(mode是C)在addr处的断点。
bc向后继续执行。目标系统反向执行。
bs向后单步执行。反向执行一条执行。
c [addr]从addr处继续执行;addr省略时,从当前地址继续。
C sig[;addr]与c命令一样,额外可携带一个信号量sig(十六进制数)
d(不推荐使用)其他调试标志
D
D;pid
通知远端目标机GDB断开连接。第二种包含进程编号,只对特定的进程生效。
F RC,EE,CF;XX文件I/O扩展协议命令
g读取通用寄存器
回复:‘XX…’
寄存器每个字节由两个十六进制表示。当目标寄存器不可用时,会使用字符x进行占位。
G XX…写通用寄存器
z0,addr,kind
Z0,addr,kind
[;con_list…]
[;cmds:persis,cmd_list]
插入(Z0)或移除(z0)在addr处kind类型的一个软件断点
z1,addr,kind
Z1,addr,kind
[;con_list…]
[;cmds:persis,cmd_list]
插入(Z1)或移除(z1)在addr处kind类型的一个硬件断点
z2,addr,kind
Z2,addr,kind
插入(Z2)或移除(z2)在addr处的一个写监控点。监控的字节数由kind指定
z3,addr,kind
Z3,addr,kind
插入(Z3)或移除(z3)在addr处的一个读监控点。监控的字节数由kind指定
z4,addr,kind
Z4,addr,kind
插入(Z4)或移除(z4)在addr处的一个访问监控点。监控的字节数由kind指定

停止回复包

通用包

q开头的是通用询问包;以Q开头的是通用设置包;

参考

详见官方文档 Appendix E gdb Remote Serial Protocol章节

语言简介

Verilog在线学习HDLBits主页

基本语句

常量

1
2
3
3'b101 	// binary 101
4'hf // binary 1111
4'd10 // binary 1010

线wire

物理线路没有方向,但建模上需要指定方向。

创造一个模型,有三个输入和四个输出

1
2
3
4
a -> w
b -> x
b -> y
c -> z

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module top_module( 
input a,b,c,
output w,x,y,z );

assign w=a;
assign x=b;
assign y=b;
assign z=c;

// If we're certain about the width of each signal, using
// the concatenation operator is equivalent and shorter:
// assign {w,x,y,z} = {a,b,b,c};

endmodule

非门Inverter

电路中产生一个非门

1
2
3
4
5
module top_module( input in, output out );
assign out = !in;
// similar
// assign out = ~in;
endmodule

与门AND gate

电路中产生一个与门

1
2
3
4
5
6
7
8
module top_module( 
input a,
input b,
output out );

assign out = a & b;

endmodule

或非门Nor gate

电路中产生一个或非门

1
2
3
4
5
6
7
8
module top_module( 
input a,
input b,
output out );

assign out = ~(a | b);
endmodule

异或非门XNOR gate

电路中产生一个异或非门

1
2
3
4
5
6
7
8
module top_module( 
input a,
input b,
output out );

assign out = ~(a ^ b);
endmodule

声明线Declaring wires

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
`default_nettype none
module top_module(
input a,
input b,
input c,
input d,
output out,
output out_n );

wire w1, w2, w3;
assign out = w3;
assign out_n = ~w3;

assign w3 = w1 | w2;
assign w1 = a & b;
assign w2 = c & d;
endmodule

7458芯片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module top_module ( 
input p1a, p1b, p1c, p1d, p1e, p1f,
output p1y,
input p2a, p2b, p2c, p2d,
output p2y );

wire wp11,wp12;
assign p1y = wp11 | wp12;
assign wp11 = p1a & p1b & p1c;
assign wp12 = p1f & p1e & p1d;

wire wp21,wp22;
assign p2y = wp21 | wp22;
assign wp21 = p2a & p2b;
assign wp22 = p2c & p2d;
endmodule

向量Vector

基本使用

声明语法:

1
type [upper:lower] vector_name;

类型可以是wire, reg等等

1
2
3
4
5
6
wire [7:0] w;         // 8-bit wire
reg [4:1] x; // 4-bit reg
output reg [0:0] y; // 1-bit reg that is also an output port (this is still a vector)
input wire [3:-2] z; // 6-bit wire input (negative ranges are allowed)
output [3:0] a; // 4-bit output wire. Type is 'wire' unless specified otherwise.
wire [0:7] b; // 8-bit wire where b[0] is the most-significant bit.

隐含声明可能导致意想不到的结果

1
2
3
4
5
6
wire [2:0] a, c;   // Two vectors
assign a = 3'b101; // a = 101
assign b = a; // b = 1 implicitly-created wire
assign c = b; // c = 001 <-- bug
my_module i1 (d,e); // d and e are implicitly one-bit wide if not declared.
// This could be a bug if the port was intended to be a

unpacked数组维度跟在名称后,packed数组维度放在名称前

1
2
reg [7:0] mem [255:0];   // 256 unpacked elements, each of which is a 8-bit packed vector of reg.
reg mem2 [28:0]; // 29 unpacked elements, each of which is a 1-bit reg.

描绘如下电路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module top_module ( 
input wire [2:0] vec,
output wire [2:0] outv,
output wire o2,
output wire o1,
output wire o0 ); // Module body starts after module declaration

assign {o2,o1,o0} = vec[2:0];
assign outv = vec;

// This is ok too
//assign o0 = vec[0];
//assign o1 = vec[1];
//assign o2 = vec[2];
endmodule

将字数据(2字节)按高低位分离

1
2
3
4
5
6
7
8
9
10
11
12
`default_nettype none     // Disable implicit nets. Reduces some types of bugs.
module top_module(
input wire [15:0] in,
output wire [7:0] out_hi,
output wire [7:0] out_lo );

assign out_hi = in[15:8];
assign out_lo = in[7:0];

// Concatenation operator also works: assign {out_hi, out_lo} = in;
endmodule

向量门

按位与&和逻辑与&对于N-bit位输入来说,会产生不同结果。前者会产生N-bit位输出,而后者只产生1-bit位输出(作为布尔值,非0值为true,0值为false)。

描绘如下电路,out_not高5-3位为b的非,其余为a的非。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module top_module( 
input [2:0] a,
input [2:0] b,
output [2:0] out_or_bitwise,
output out_or_logical,
output [5:0] out_not
);

assign out_or_bitwise = a | b;
assign out_or_logical = a || b;
// ! is logical inverse
assign out_not = {~b, ~a};
endmodule

颠倒8位

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
40
41
42
43
module top_module (
input [7:0] in,
output [7:0] out
);
assign out = {in[0],in[1],in[2],in[3],in[4],in[5],in[6],in[7]};
//assign {out[0],out[1],out[2],out[3],out[4],out[5],out[6],out[7]} = in;

/*
// I know you're dying to know how to use a loop to do this:

// Create a combinational always block. This creates combinational logic that computes the same result
// as sequential code. for-loops describe circuit *behaviour*, not *structure*, so they can only be used
// inside procedural blocks (e.g., always block).
// The circuit created (wires and gates) does NOT do any iteration: It only produces the same result
// AS IF the iteration occurred. In reality, a logic synthesizer will do the iteration at compile time to
// figure out what circuit to produce. (In contrast, a Verilog simulator will execute the loop sequentially
// during simulation.)
always @(*) begin
for (int i=0; i<8; i++) // int is a SystemVerilog type. Use integer for pure Verilog.
out[i] = in[8-i-1];
end


// It is also possible to do this with a generate-for loop. Generate loops look like procedural for loops,
// but are quite different in concept, and not easy to understand. Generate loops are used to make instantiations
// of "things" (Unlike procedural loops, it doesn't describe actions). These "things" are assign statements,
// module instantiations, net/variable declarations, and procedural blocks (things you can create when NOT inside
// a procedure). Generate loops (and genvars) are evaluated entirely at compile time. You can think of generate
// blocks as a form of preprocessing to generate more code, which is then run though the logic synthesizer.
// In the example below, the generate-for loop first creates 8 assign statements at compile time, which is then
// synthesized.
// Note that because of its intended usage (generating code at compile time), there are some restrictions
// on how you use them. Examples: 1. Quartus requires a generate-for loop to have a named begin-end block
// attached (in this example, named "my_block_name"). 2. Inside the loop body, genvars are read only.
generate
genvar i;
for (i=0; i<8; i = i+1) begin: my_block_name
assign out[i] = in[8-i-1];
end
endgenerate
*/

endmodule

重复操作

声明语法:

1
{num{vector}}

示例

1
2
3
4
{5{1'b1}}           // 5'b11111 (or 5'd31 or 5'h1f)
{2{a,b,c}} // The same as {a,b,c,a,b,c}
{3'd5, {2{3'd6}}} // 9'b101_110_110. It's a concatenation of 101 with
// the second vector, which is two copies of 3'b110.

将8位数据扩展为32位,高24位为符号位(bit[7]),余下8位为输入

1
2
3
4
5
6
7
8
9
10
11
module top_module (
input [7:0] in,
output [31:0] out
);

// Concatenate two things together:
// 1: {in[7]} repeated 24 times (24 bits)
// 2: in[7:0] (8 bits)
assign out = { {24{in[7]}}, in };

endmodule

描述电路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module top_module (
input a, b, c, d, e,
output [24:0] out
);

wire [24:0] top, bottom;
assign top = { {5{a}}, {5{b}}, {5{c}}, {5{d}}, {5{e}} };
assign bottom = {5{a,b,c,d,e}};
assign out = ~top ^ bottom; // Bitwise XNOR

// This could be done on one line:
// assign out = ~{ {5{a}}, {5{b}}, {5{c}}, {5{d}}, {5{e}} } ^ {5{a,b,c,d,e}};

endmodule

模块Module

基本使用

基本语法

1
2
3
module mod_a ( input in1, input in2, output out );
// Module body
endmodule

线与模块端口连接有两种方式:通过位置和通过名称

通过位置连接,模块实例instance1三个端口分别连接线wa, wb, wc

1
mod_a instance1 ( wa, wb, wc );

通过名称连接,模块实例instance2三个端口名称与对应线连接:

1
mod_a instance2 ( .out(wc), .in1(wa), .in2(wb) );

描述电路,其中模块mod_a在其他地方描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module top_module (
input a,
input b,
output out
);

// Create an instance of "mod_a" named "inst1", and connect ports by name:
mod_a inst1 (
.in1(a), // Port"in1"connects to wire "a"
.in2(b), // Port "in2" connects to wire "b"
.out(out) // Port "out" connects to wire "out"
// (Note: mod_a's port "out" is not related to top_module's wire "out".
// It is simply coincidence that they have the same name)
);

/*
// Create an instance of "mod_a" named "inst2", and connect ports by position:
mod_a inst2 ( a, b, out ); // The three wires are connected to ports in1, in2, and out, respectively.
*/

endmodule

移位模块

给已经实现的D触发器(D flip-flop)模块module my_dff ( input clk, input d, output q );,按下图描述电路

1
2
3
4
5
6
7
8
module top_module ( input clk, input d, output q );
wire q1,q2;

my_dff d1 (clk, d, q1);
my_dff d2 (clk, q1, q2);
my_dff d3 (clk, q2, q);
endmodule

8位移位器

描绘电路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module top_module (
input clk,
input [7:0] d,
input [1:0] sel,
output reg [7:0] q
);

wire [7:0] o1, o2, o3; // output of each my_dff8

// Instantiate three my_dff8s
my_dff8 d1 ( clk, d, o1 );
my_dff8 d2 ( clk, o1, o2 );
my_dff8 d3 ( clk, o2, o3 );

// This is one way to make a 4-to-1 multiplexer
always @(*) begin // Combinational always block
case(sel)
2'h0: q = d;
2'h1: q = o1;
2'h2: q = o2;
2'h3: q = o3;
endcase
end
endmodule

全加器

add16全加器已经定义module add16 ( input[15:0] **a**, input[15:0] **b**, input **cin**, output[15:0] **sum**, output **cout** );,它包含16个add1全加器,描述电路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module top_module (
input [31:0] a,
input [31:0] b,
output [31:0] sum
);
wire o1,o2;

add16 inst1(a[15:0],b[15:0],0,sum[15:0],o1);
add16 inst2(a[31:16],b[31:16],o1,sum[31:16],o2);
endmodule

module add1 ( input a, input b, input cin, output sum, output cout );
// Full adder module here
assign sum = cin ? ~(a^b) : a^b;
assign cout = cin ? a|b : a&b;
endmodule

加减器

减法器由全加器变化而来,只需将对第二个操作数取补码即可(反码加一)。等效的电路可以有两种效果:a+b+0a+~b+1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module top_module(
input [31:0] a,
input [31:0] b,
input sub,
output [31:0] sum
);

wire o1,o2;
wire [31:0] b1 = b^{32{sub}};

add16 inst1(a[15:0],b1[15:0],sub,sum[15:0],o1);
add16 inst2(a[31:16],b1[31:16],o1,sum[31:16],o2);
endmodule

过程Procedure

对于综合硬件,有两种相关always块:

  • 组合型:always @(*)
  • 时序型:always @(posedge clk)

块语句需要使用beginend标记出来,若语句只有“单句”时,块标记可以省略。

Always块

对于简单的场景,组合型always块等价于assign语句。

1
2
assign out1 = a & b | c ^ d;
always @(*) out2 = a & b | c ^ d;

assign语句左边符号对应的类型是net(例如:wire),而过程赋值对应的类型是variable(例如:reg)。两者的差异不会对综合的电路产生任何影响,只是一种不成文的约定。

在VHDL中有三种赋值类型:

  • 持续赋值assign x = y;
  • 过程阻塞赋值,在过程中使用x = y;
  • 过程非阻塞赋值,在过程中使用x <= y;

在组合alsways块中,使用阻塞赋值。在时序always块中,使用非阻塞赋值。

使用三种方式,用异或门构建电路。值得注意的是,第三种方式产生的结果会因为有触发器而产生延时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// synthesis verilog_input_version verilog_2001
module top_module(
input clk,
input a,
input b,
output wire out_assign,
output reg out_always_comb,
output reg out_always_ff );

assign out_assign = a^b;
always @(*) out_always_comb = a^b;
always @(posedge clk) out_always_ff <= a^b;
endmodule

if语句

描绘一个2-1选择器

对应的值表

sel_b1sel_b2out_assign out_always
00a
01a
10a
11b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// synthesis verilog_input_version verilog_2001
module top_module(
input a,
input b,
input sel_b1,
input sel_b2,
output wire out_assign,
output reg out_always );

assign out_assign = sel_b1&sel_b2 ? b : a;

always @(*) begin
if (sel_b1&sel_b2) begin
out_always = b;
end
else begin
out_always = a;
end
end
endmodule

if语句锁存

修复下图错误,当cpu_overheated时,产生关闭信号(shut_off_computer设置为1)。

修改前,错误的示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// synthesis verilog_input_version verilog_2001
module top_module (
input cpu_overheated,
output reg shut_off_computer,
input arrived,
input gas_tank_empty,
output reg keep_driving ); //

always @(*) begin
if (cpu_overheated)
shut_off_computer = 1;
end

always @(*) begin
if (~arrived)
keep_driving = ~gas_tank_empty;
end

endmodule

修改后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// synthesis verilog_input_version verilog_2001
module top_module (
input cpu_overheated,
output reg shut_off_computer,
input arrived,
input gas_tank_empty,
output reg keep_driving ); //

always @(*) begin
if (cpu_overheated)
shut_off_computer = 1;
else
shut_off_computer = 0;
end

always @(*) begin
if (~arrived)
keep_driving = ~gas_tank_empty;
else
keep_driving = 0;
end

endmodule

case语句

基本语法

1
2
3
4
5
6
case (cond)
2'h0: statments0;
2'h1: statments1;
...
default: statmentsx;
endcase

建立一个6-1选择器,否则输出0

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
// synthesis verilog_input_version verilog_2001
module top_module (
input [2:0] sel,
input [3:0] data0,
input [3:0] data1,
input [3:0] data2,
input [3:0] data3,
input [3:0] data4,
input [3:0] data5,
output reg [3:0] out );//

always@(*) begin // This is a combinational circuit
case(sel)
4'h0: out = data0;
4'h1: out = data1;
4'h2: out = data2;
4'h3: out = data3;
4'h4: out = data4;
4'h5: out = data5;
default: out = 0;
endcase
end

endmodule

构建一个4bits的优先编码器,输出第一个1所在的比特位。

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
module top_module (
input [3:0] in,
output reg [1:0] pos
);

always @(*) begin // Combinational always block
case (in)
4'h0: pos = 2'h0; // I like hexadecimal because it saves typing.
4'h1: pos = 2'h0;
4'h2: pos = 2'h1;
4'h3: pos = 2'h0;
4'h4: pos = 2'h2;
4'h5: pos = 2'h0;
4'h6: pos = 2'h1;
4'h7: pos = 2'h0;
4'h8: pos = 2'h3;
4'h9: pos = 2'h0;
4'ha: pos = 2'h1;
4'hb: pos = 2'h0;
4'hc: pos = 2'h2;
4'hd: pos = 2'h0;
4'he: pos = 2'h1;
4'hf: pos = 2'h0;
default: pos = 2'b0; // Default case is not strictly necessary because all 16 combinations are covered.
endcase
end

// There is an easier way to code this. See the next problem (always_casez).

endmodule

casez

当部分比特位不关心时,可以使用casez。符号z?在一下场景都是等价的。与casez类似的是casex,后者使用符号x

1
2
3
4
5
6
7
8
9
always @(*) begin
casez (in[3:0])
4'bzzz1: out = 0; // in[3:1] can be anything
4'bzz1z: out = 1;
4'b?1??: out = 2;
4'b1???: out = 3;
default: out = 0;
endcase
end

构建8bits优先编码器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// synthesis verilog_input_version verilog_2001
module top_module (
input [7:0] in,
output reg [2:0] pos );

always @(*) begin
casez (in[7:0])
8'h0: pos = 0;
8'bzzzz_???1: pos = 0;
8'bzzzz_zz10: pos = 1;
8'bzzzz_z100: pos = 2;
8'bzzzz_1000: pos = 3;
8'bzzz1_0000: pos = 4;
8'bzz10_0000: pos = 5;
8'bz100_0000: pos = 6;
8'b1000_0000: pos = 7;
default: pos = 3'bzzz;
endcase
end
endmodule

键盘扫描码

处理如下四个按键

Scancode [15:0]Arrow key
16'he06bleft arrow
16'he072down arrow
16'he074right arrow
16'he075up arrow
Anything elsenone
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// synthesis verilog_input_version verilog_2001
module top_module (
input [15:0] scancode,
output reg left,
output reg down,
output reg right,
output reg up );

always @(*) begin
left = 0; down = 0; right = 0; up = 0;
case (scancode)
16'he06b: left = 1;
16'he072: down = 1;
16'he074: right = 1;
16'he075: up = 1;
default: ;
endcase
end
endmodule

更多特性

条件

三元条件操作符?:

1
(condition ? if_true : if_false)

逻辑操作简化

若需要对向量所有位进行门操作时,正常书写比较冗余,可以对与、或和异或操作简化

1
2
3
& a[3:0]     // AND: a[3]&a[2]&a[1]&a[0]. Equivalent to (a[3:0] == 4'hf)
| b[3:0] // OR: b[3]|b[2]|b[1]|b[0]. Equivalent to (b[3:0] != 4'h0)
^ c[2:0] // XOR: c[2]^c[1]^c[0]

实现奇偶校验位算法

1
2
3
4
5
6
7
module top_module (
input [7:0] in,
output parity);

assign parity = ^in[7:0];
endmodule

100bits翻转

1
2
3
4
5
6
7
8
9
10
11
module top_module (
input [99:0] in,
output reg [99:0] out
);

always @(*) begin
for (int i=0;i<$bits(out);i++) // $bits() is a system function that returns the width of a signal.
out[i] = in[$bits(out)-i-1]; // $bits(out) is 100 because out is 100 bits wide.
end

endmodule

比特1计数

1
2
3
4
5
6
7
8
9
10
11
module top_module( 
input [254:0] in,
output [7:0] out );

always @(*) begin // Combinational always block
out = 0;
for (int i=0; i<$bits(in); i++)
out += in[i];
end
endmodule

100bits全加器

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
module top_module( 
input [99:0] a, b,
input cin,
output [99:0] cout,
output [99:0] sum );

genvar i;
generate
for (i=0; i<$bits(a); ++i) begin: adder_gen
adder_1bit u_adder (
.a(a[i]),
.b(b[i]),
.cin(i==0?cin:cout[i-1]),
.sum(sum[i]),
.cout(cout[i])
);
end
endgenerate

endmodule

module adder_1bit (
input a,b,
input cin,
output cout,
output sum
);
assign {cout,sum} = a+b+cin;
// assign sum = cin ? ~(a^b) : a^b;
// assign cout = cin ? a|b : a&b;
endmodule

100位BCD码全加器

已经定义1位BCD码全加器

1
2
3
4
5
6
module bcd_fadd (
input [3:0] a,
input [3:0] b,
input cin,
output cout,
output [3:0] sum );
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
module top_module( 
input [399:0] a, b,
input cin,
output cout,
output [399:0] sum );

wire [100:0] c;
assign cout = c[100];
assign c[0] = cin;

genvar i;
generate
for(i=0; i<100; ++i) begin: bcd_gen
bcd_fadd u_bcd (
.a( a[(4*i+3):(4*i)] ),
.b( b[(4*i+3):(4*i)] ),
.cin( c[i] ),
.cout( c[i+1] ),
.sum( sum[(4*i+3):(4*i)] )
);
end
endgenerate

endmodule

仿真

timescale

定义仿真中时间单位的比例。它的作用是指定时序仿真中时间单位的大小,以便仿真器可以正确地模拟设计中的时序行为。

基本语法

1
`timescale unit / precision

其中,unit是时间单位,可以是1ns1ps1us等,表示一个时钟周期的时间长度;precision是时间精度,表示仿真的最小时间单位;

#num

等待num个时间单位后,执行后面的语句。

基本语法

1
#num statement

task

Verilog语言中具有类似C语言函数的结构有task和function,他们可以增加代码可读性和重复使用性。Function用来描述组合逻辑,只能有一个返回值,function的内部不能包含时序控制。Task类似procedure,执行一段verilog代码,task中可以有任意数量的输入和输出,task也可以包含时序控制。

基本语法

1
2
task TASK_NAME;
endtask

PS/2键盘设备仿真

当用户按键或松开时,键盘以每帧11位的格式串行传送数据给主机,同时在PS2_CLK时钟信号上传输对应的时钟(一般为10.0–16.7kHz)。第一位是开始位(逻辑0),后面跟8位数据位(低位在前),一个奇偶校验位(奇校验)和一位停止位(逻辑1)。每位都在时钟的 下降沿 有效,下图显示了键盘传送一字节数据的时序。在下降沿有效的主要原因是下降沿正好在数据位的中间,因此可以让数据位从开始变化到接收采样时能有一段信号建立时间。

键盘输出数据时序图

键盘通过PS2_DAT引脚发送的信息称为扫描码,每个扫描码可以由单个数据帧或连续多个数据帧构成。当按键被按下时送出的扫描码被称为 通码(Make Code) ,当按键被释放时送出的扫描码称为 断码(Break Code) 。以 W 键为例, W 键的通码是1Dh,如果 W 键被按下,则PS2_DAT引脚将输出一帧数据,其中的8位数据位为1Dh,如果 W 键一直没有释放,则不断输出扫描码1Dh 1Dh … 1Dh,直到有其他键按下或者 W 键被放开。某按键的断码是F0h加此按键的通码,如释放 W 键时输出的断码为F0h 1Dh,分两帧传输。

多个键被同时按下时,将逐个输出扫描码,如:先按左 Shift 键(扫描码为12h)、再按 W 键、放开 W 键、再放开左 Shift 键,则此过程送出的全部扫描码为:12h 1Dh F0h 1Dh F0h 12h。

键盘扫描码

每个键都有唯一的通码和断码。键盘所有键的扫描码组成的集合称为扫描码集。共有三套标准的扫描码集,所有现代的键盘默认使用第二套扫描码。下图显示了键盘各键的扫描码(以十六进制表示),如Caps键的扫描码是58h。 下图可以看出,键盘上各按键的扫描码是随机排列的,如果想迅速的将键盘扫描码转换为ASCII码,一个最简单的方法就是利用查找表 LookUp Table, LUT ,扫描码到ASCII码的转换表格请读者自己生成。

键盘扫描码
扩展键盘和数字键盘的扫描码

键盘控制器

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
module ps2_keyboard(clk,clrn,ps2_clk,ps2_data,data,
ready,nextdata_n,overflow);
input clk,clrn,ps2_clk,ps2_data;
input nextdata_n;
output [7:0] data;
output reg ready;
output reg overflow; // fifo overflow
// internal signal, for test
reg [9:0] buffer; // ps2_data bits
reg [7:0] fifo[7:0]; // data fifo
reg [2:0] w_ptr,r_ptr; // fifo write and read pointers
reg [3:0] count; // count ps2_data bits
// detect falling edge of ps2_clk
reg [2:0] ps2_clk_sync;

always @(posedge clk) begin
ps2_clk_sync <= {ps2_clk_sync[1:0],ps2_clk};
end

wire sampling = ps2_clk_sync[2] & ~ps2_clk_sync[1];

always @(posedge clk) begin
if (clrn == 0) begin // reset
count <= 0; w_ptr <= 0; r_ptr <= 0; overflow <= 0; ready<= 0;
end
else begin
if ( ready ) begin // read to output next data
if(nextdata_n == 1'b0) //read next data
begin
r_ptr <= r_ptr + 3'b1;
if(w_ptr==(r_ptr+1'b1)) //empty
ready <= 1'b0;
end
end
if (sampling) begin
if (count == 4'd10) begin
if ((buffer[0] == 0) && // start bit
(ps2_data) && // stop bit
(^buffer[9:1])) begin // odd parity
fifo[w_ptr] <= buffer[8:1]; // kbd scan code
w_ptr <= w_ptr+3'b1;
ready <= 1'b1;
overflow <= overflow | (r_ptr == (w_ptr + 3'b1));
end
count <= 0; // for next
end else begin
buffer[count] <= ps2_data; // store ps2_data
count <= count + 3'b1;
end
end
end
end
assign data = fifo[r_ptr]; //always set output data

endmodule

键盘仿真模型

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
`timescale 1ns / 1ps
module ps2_keyboard_model(
output reg ps2_clk,
output reg ps2_data
);
parameter [31:0] kbd_clk_period = 60;
initial ps2_clk = 1'b1;

task kbd_sendcode;
input [7:0] code; // key to be sent
integer i;

reg[10:0] send_buffer;
begin
send_buffer[0] = 1'b0; // start bit
send_buffer[8:1] = code; // code
send_buffer[9] = ~(^code); // odd parity bit
send_buffer[10] = 1'b1; // stop bit
i = 0;
while( i < 11) begin
// set kbd_data
ps2_data = send_buffer[i];
#(kbd_clk_period/2) ps2_clk = 1'b0;
#(kbd_clk_period/2) ps2_clk = 1'b1;
i = i + 1;
end
end
endtask

endmodule

键盘测试代码

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
`timescale 1ns / 1ps
module keyboard_sim;

/* parameter */
parameter [31:0] clock_period = 10;

/* ps2_keyboard interface signals */
reg clk,clrn;
wire [7:0] data;
wire ready,overflow;
wire kbd_clk, kbd_data;
reg nextdata_n;

ps2_keyboard_model model(
.ps2_clk(kbd_clk),
.ps2_data(kbd_data)
);

ps2_keyboard inst(
.clk(clk),
.clrn(clrn),
.ps2_clk(kbd_clk),
.ps2_data(kbd_data),
.data(data),
.ready(ready),
.nextdata_n(nextdata_n),
.overflow(overflow)
);

initial begin /* clock driver */
clk = 0;
forever
#(clock_period/2) clk = ~clk;
end

initial begin
clrn = 1'b0; #20;
clrn = 1'b1; #20;
model.kbd_sendcode(8'h1C); // press 'A'
#20 nextdata_n =1'b0; #20 nextdata_n =1'b1;//read data
model.kbd_sendcode(8'hF0); // break code
#20 nextdata_n =1'b0; #20 nextdata_n =1'b1; //read data
model.kbd_sendcode(8'h1C); // release 'A'
#20 nextdata_n =1'b0; #20 nextdata_n =1'b1; //read data
model.kbd_sendcode(8'h1B); // press 'S'
#20 model.kbd_sendcode(8'h1B); // keep pressing 'S'
#20 model.kbd_sendcode(8'h1B); // keep pressing 'S'
model.kbd_sendcode(8'hF0); // break code
model.kbd_sendcode(8'h1B); // release 'S'
#20;
$stop;
end

endmodule

电路

组合逻辑

基本门

GND

1
assign out = 1'b0;

NOR

1
assign out = ~(in1|in2);

多路选择器

9路选择器

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
module top_module( 
input [15:0] a, b, c, d, e, f, g, h, i,
input [3:0] sel,
output [15:0] out );

// Case statements can only be used inside procedural blocks (always block)
// This is a combinational circuit, so use a combinational always @(*) block.
always @(*) begin
out = '1; // '1 is a special literal syntax for a number with all bits set to 1.
// '0, 'x, and 'z are also valid.
// I prefer to assign a default value to 'out' instead of using a
// default case.
case (sel)
4'h0: out = a;
4'h1: out = b;
4'h2: out = c;
4'h3: out = d;
4'h4: out = e;
4'h5: out = f;
4'h6: out = g;
4'h7: out = h;
4'h8: out = i;
endcase
end
endmodule

4位256路选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module top_module (
input [1023:0] in,
input [7:0] sel,
output [3:0] out
);

// We can't part-select multiple bits without an error, but we can select one bit at a time,
// four times, then concatenate them together.
assign out = {in[sel*4+3], in[sel*4+2], in[sel*4+1], in[sel*4+0]};

// Alternatively, "indexed vector part select" works better, but has an unfamiliar syntax:
// assign out = in[sel*4 +: 4]; // Select starting at index "sel*4", then select a total width of 4 bits with increasing (+:) index number.
// assign out = in[sel*4+3 -: 4]; // Select starting at index "sel*4+3", then select a total width of 4 bits with decreasing (-:) index number.
// Note: The width (4 in this case) must be constant.

endmodule

0%