通知

  • SoC部分的讲义有望年底发布 🏳️
    • 过去大家只要在流片前接入SoC, 跑几个测试就结束了
    • SoC的活都是项目组干的: 接入SoC != 学习SoC
      • SoC是我们设计的, 测试的binary也是我们预先编好的
    • 这一期要求大家实现SoC中的一些关键模块: 实现SoC = 学习SoC
      • 深入理解程序如何在SoC上运行
  • 我们给大家准备了约40+个必做任务
    • 经过愉(tong)快(ku)的编(tiao)码(shi)之后, 大家终于可以说自己懂(点)SoC了

引言

我们已经学习了总线协议, 知道CPU如何与设备通信

 

本次课内容: 以 “一生一芯”流片用的SoC为例, 理解程序, CPU和外设之间如何交互

  • MROM
  • UART
  • flash

SoC上的设备

SoC = System On Chip

  • CPU != System
  • SoC = CPU + 总线 + 设备(含内存)

 

一个现实 - 设备的属性五花八门

  • 地址空间, 访问位宽, 接口协议, 可写, 可执行, 非对齐访问, 原子操作, 突发访问, 中断, DMA, 可缓存, 幂等性…
  • 实际中还要考虑电气特性
    • 有的设备无法在高频工作, 需要实现多个时钟域之间的异步通信
    • 有的设备需要固定频率的时钟作为参考时钟
    • 有的设备对复位顺序的先后有要求

 

如何连接这些设备, 让CPU能正确访问, 就成了SoC需要关注的问题

  • 真实的SoC还需要考虑访问的性能: 并发访问(outstanding), 乱序…

例子 - 访问位宽

`define UART_BASE   32'h1000_0000
`define UART_REG_RB `UART_ADDR_WIDTH'd0  // receiver buffer
`define UART_REG_IE `UART_ADDR_WIDTH'd1  // Interrupt enable
`define UART_REG_II `UART_ADDR_WIDTH'd2  // Interrupt identification
`define UART_REG_LC `UART_ADDR_WIDTH'd3  // Line Control
araddr  --->
arvalid --->
<--- arready

如何通过AXI-Lite总线指定读出设备寄存器UART_REG_RB?

  • araddr0x1000_0000? 读多长?

 

“读多长”在memory中没那么重要, 可以读4字节/8字节给CPU选

但在设备中, 读1字节和读4字节的行为不一样

  • 回顾设备模型: 访问设备寄存器会改变设备的状态!

总线需要细心地处理这个问题

AXI的窄传输

AXI-Lite无法解决上述问题

  • AR通道中没有足够的信号编码 “读多长”的信息
  • 设备只能认为实际访问的数据位宽 = AXI-Lite总线的数据位宽
    • 所以并非所有设备都适合通过AXI-Lite接入
      • 若单次AXI-Lite访问会覆盖多个设备寄存器, 则设备状态会出错

 

完整的AXI总线协议通过arsize/awsize信号处理上述问题

  • AXI总线的数据位宽 > 实际访问的数据位宽(即axsize), 称窄传输
    • 前者在硬件设计时静态决定
      • 表示一次总线传输的最大数据位宽, 也用于计算总线的理论带宽
    • 后者在根据软件访存指令中的位宽信息动态决定
      • 表示一次总线传输的实际数据位宽, 如lb指令只访问1字节
  • 设备可以根据arsize得知软件需要读取的范围

如果总线的数据位宽不一致, 怎么办?

  • 4-byte master -> 8-byte slave
    • master的所有请求都属于窄传输
    • XXXX -> AAAABBBB, 4-byte的wdata应该放在A部分还是B部分?
    • XXXX <- AAAABBBB, 8-byte的rdata应该取A部分还是B部分?
      • RTFM
  • 8-byte master -> 4-byte slave
    • 除了上述问题, 还需要考虑, 如果master发起8-byte传输, 怎么办?
      • panic - 仿真可以, 但总不能让真机卡死 😂
      • 抛异常 - 通过resp信号返回slverr
      • 如果slave的一次8字节访问和两次4字节访问的行为等价, 就可以拆
        • 需要slave满足上述属性, 例如内存

 

可以通过AXI Data Width Converter连接总线数据位宽不一致的上下游

AXI适配器 - 属性转换模块

Xilinx的AXI Interconnect文档介绍了更多的AXI适配器:

  • AXI Crossbar - 地址空间
  • AXI Data Width Converter - 数据位宽
  • AXI Clock Converter - 时钟域
    • 可连接时钟频率不同的下游
  • AXI Protocol Converter - 突发访问, 并发访问
    • 可连接接口协议不同的下游(AXI3, AXI4, AXI4-Lite)
  • AXI Data FIFO - 访问带宽
  • AXI Register Slice - 关键路径
  • AXI MMU - 地址空间约束, 读写权限控制

AXI适配器 - 属性转换模块(2)

RocketChip项目也提供一些AXI适配器(文档):

  • AXI AsyncCrossing - 与Xilinx AXI Clock Converter类似
  • AXI Buffer - 与Xilinx AXI Register Slice类似
  • AXI Deinterleaver - 多个并发访问交错时, 恢复其顺序
    • 可连接不支持并发访问的下游
  • AXI Delayer - 插入随机延迟, 测试用
  • AXI Filter - Xilinx AXI MMU的升级版, 拒绝不满足过滤条件的请求
  • AXI Fragmenter - 将一个突发访问(多次传输)拆成多个单次访问
    • 可连接不支持突发访问的下游
  • AXI IdIndexer - 约束请求的ID位宽
    • 可连接ID位宽较短的下游
  • AXI UserYanker - 去除USER信号并暂存USER信息
    • 可连接无USER信号的下游

最简单的SoC

回顾: 自制freestanding运行时环境

一个面向RISC-V程序的简单freestanding运行时环境

  • 程序从地址0开始执行
  • 只支持两条指令
    • addi指令
    • ebreak指令
      • 寄存器a0=0时, 输出寄存器a1低8位的字符
      • 寄存器a0=1时, 结束运行

 

  • 在之前的NPC仿真过程中, 我们借助仿真环境实现了不少功能
    • 将程序的镜像文件放到存储器中
    • 通过host的putchar()函数输出
  • 但在真实的SoC中, 上电后没有仿真环境帮助我们了
    • 需要真实硬件实现这些功能

存放程序的存储器

RAM是易失存储器(volatile memory)

  • 上电时无有效数据, 此时读出的内容是未定义的
  • 如果上电时CPU无法执行指定的程序, 整个系统的行为是未定义的
  • 需要一种非易失存储器(non-volatile memory), 内容可在断电时保持

 

一个简单的方案: ROM(Read-Only Memory)

  • 通过某种方式将信息(第一个程序)存储在ROM中, 每次从相同位置读出相同内容
  • 其实也不完全只读, 只是不能(很容易地)写
    • 在第一次读之前, 总得有个办法 “写”进去: 编程

 

ROM的实现方式有很多

  • 目前比较适合NPC访问的是掩膜ROM(mask ROM, MROM)

通过MROM存储1 bit

MROM的本质 - 将信息 “硬编码”在门电路中

  • 硬连线(hardwire)到逻辑0(地)和1(电源), 很暴力的做法 😂
  • MROM的编程就是RTL编程 😂

 

MROM的编程是一次性的

  • MROM的内容在RTL设计时决定
  • 经过后端物理设计后, 晶圆厂将根据版图制作用于光刻的掩膜(mask), 然后用这个掩膜来制造芯片
    • mask ROM中mask的含义
  • 芯片生产后, MROM的存储内容无法更改

 

一个只对学习有意义的好处 - 制造工艺和电气特性都和CPU一样

  • 访问逻辑简单, 无需复杂的控制器

访问MROM存储阵列

地址译码器 + 存储阵列: 本质上是一些数据输入端为常数的多路选择器

  • 与门的一端输入是常数, 因此实际上这些与门也可以优化掉
// 实现类似 initial $readmemh(...) 的功能
import java.nio.file.{Files, Paths}
val binpath = "/home/ysyx/am-kernels/kernels/hello/build/hello-riscv64-npc.bin"
val wordbits = 32
val bin = Files.readAllBytes(Paths.get(binpath))
val upSize = 1 << log2Ceil(bin.size)
val bingp = (bin ++ Seq.fill(upSize - bin.size)(0.toByte)).grouped(wordbits / 8)
def byteShift(x: Byte, y: BigInt) = (x.toInt & 0xff) | (y << 8)
val wordArray = bingp.map(_.foldRight(BigInt(0))(byteShift)).toSeq
val mrom = VecInit(wordArray.map(x => x.U(wordbits.W)))
io.data := RegNext(mrom(io.addr)) // 根据地址选择MROM中的数据

提供总线接口

class AXILiteMROM(binpath: String, wordbits: Int = 32) extends Module {
  val io = IO(new AXILite)

  import java.nio.file.{Files, Paths}
  val bin = Files.readAllBytes(Paths.get(binpath))
  val upSize = 1 << log2Ceil(bin.size)
  val bingp = (bin ++ Seq.fill(upSize - bin.size)(0.toByte)).grouped(wordbits / 8)
  def byteShift(x: Byte, y: BigInt) = (x.toInt & 0xff) | (y << 8)
  val wordArray = bingp.map(_.foldRight(BigInt(0))(byteShift)).toSeq
  val mrom = VecInit(wordArray.map(x => x.U(wordbits.W)))

  val s_idle :: s_wait_rready :: Nil = Enum(2)
  val state = RegInit(s_idle)
  state := MuxLookup(state, s_idle, List(
    s_idle        -> Mux(io.ar.fire, s_wait_rready, s_idle),
    s_wait_rready -> Mux(io.r.fire, s_idle, s_wait_rready)
  ))

  def getAddr(x: UInt) = x(x.getWidth-1, log2Ceil(wordbits / 8))
  val addr = Mux(io.ar.fire, getAddr(io.ar.bits.addr), 0.U)
  io.r.bits.data := RegEnable(mrom(addr)), io.ar.fire)
  io.r.bits.resp := 0.U
  io.ar.ready := (state === s_idle)
  io.r.valid  := (state === s_wait_rready)
}

将AXILiteMROM连到Xbar上, 即可被CPU访问

软件访问

例子 - 访问MROM中存储的数据

  • C代码 - a = *(int *)(MROM_BASE + 4)
  • RISC-V指令 - lw a0, 4(a1)
  • LSU - 发起AXI-Lite总线请求: araddr = 0x2000_0004
  • Xbar - 将总线请求转发到MROM控制器, 如AXILiteMROM模块
  • MROM控制器 - 状态机接收总线请求后, 从araddr解析出MROM的地址4
  • 地址译码器 - 将地址4译码成选择信号
  • 存储阵列 - 根据选择信号选出存储的数据

 

  • 存储的数据 -> rdata信号 -> a0寄存器 -> a变量

 

取指的过程是类似的, 只不过取指请求由IFU发出

基本的输出设备 - 串口

能存放程序之后, 下一步就要考虑如何输出

 

串口 = 按照一定配置将字符的编码传送到线缆上的设备, 配置包括:

  • 波特率 - 每秒传送的字符数, 反应了信息传输的频率
    • 受电气特性的限制, 波特率越大, 误码率越高, 字符传送成功率越低
  • 字符长度 - 5-8 bit
  • 校验位 - 是否存在, 若是, 是奇校验还是偶校验
  • 停止位 - 1 bit或2 bit

 

  • 一种常见配置是115200 8N1
    • 波特率是115200, 字符长度8 bit, 不含校验位, 停止位1 bit
  • 接收方可在相同配置下从线缆上接收字符的编码
    • 若配置不同, 将解码出不同的信息

串口控制器 - 提供串口功能的抽象

上层软件不希望了解传输细节, 需要串口控制器提供串口的功能抽象

  • 一个真实的串口控制器UART16550
  • ysyxSoC/perip/uart16550/rtl/uart_regs.v
    • 设备寄存器的读写译码
    • 部分寄存器地址相同, 通过读写方式和其他状态区分
    `define UART_REG_RB  `UART_ADDR_WIDTH'd0  // receiver buffer
    `define UART_REG_TR  `UART_ADDR_WIDTH'd0  // transmitter
    `define UART_REG_DL1 `UART_ADDR_WIDTH'd0  // Divisor latch bytes (1-2)
  • ysyxSoC/perip/uart16550/rtl/uart_tfifo.v
    • 发送队列, 让CPU不必等待字符完成发送
  • ysyxSoC/perip/uart16550/rtl/uart_transmitter.v
    • 状态机从发送队列中取出字符, 按照设置的波特率等配置发送
  • ysyxSoC/perip/uart16550/doc/
    • RTFM - 寄存器的地址空间, 功能说明

总线连接 - 接入CPU的地址空间

  • ysyxSoC/perip/uart16550/rtl/uart_top_apb.v
    • 里面是wishbone协议 - RTFM
    • 我们将它封装成APB协议 - RTFM
    • 这些协议比AXI-Lite还简单

 

ysyxSoC中包含APB Xbar和AXI-APB的转接桥, 可以将UART16550控制器连到CPU

软件驱动和应用程序

  • 应用程序 - printf("Hello RISC-V")
  • TRM API - putch('H')
  • API实现 - *(uint8_t *)(UART_BASE + 0) = ch
  • RISC-V指令 - sb a0, 0(a1)
  • LSU - 发起AXI-Lite总线请求
    • awaddr = 0x1000_0000, wdata = 0x48, wstrb = 0x1
  • 总线 - 将请求转发到UART16550控制器
    • AXI-Xbar -> AXI-APB -> APB-Xbar -> APB-wishbone -> 控制器
  • UART16550控制器 - 接收总线请求后, 从awaddr解析出寄存器地址0
  • 地址译码器 - 根据地址0, 译码出需要访问TX寄存器
  • 设备寄存器功能 - 将字符H写入发送队列
  • transmitter - 状态机从发送队列中取出字符H, 按照设置的波特率等配置, 将0100_1000发送到线缆上

更实用的SoC

可写的存储器

大部分程序都需要向存储器写入数据

  • 回顾: 函数调用要在栈上创建栈桢, 必要时通过栈桢存取数据

MROM的一个问题: 不可写入

  • 存储器仅包含MROM的SoC无法支持需要函数调用的程序, 不实用
  • 需要添加RAM作为存储器

 

SRAM是对ysyxSoC最简单的SRAM

  • 和MROM类似, 制造工艺和电气特性都和CPU一样
    • 访问逻辑简单, 无需复杂的控制器
  • 读写延迟只有1周期
  • 但存储密度不高, 价格昂贵, 一般不会在SoC中集成大容量的SRAM

回顾: 通过SRAM存储1 bit

  • 空闲: 字线加低电压时, N1和N2截止, 无读写操作
  • 读出: 向两根位线加高电压(接近逻辑1), 再向字线加高电压, 此时N1和N2导通, 根据存储的信息, 其中一根位线的电压轻微下降, 可通过放大电路检测并确定哪一根位线, 从而得知存储的信息是逻辑0还是逻辑1
  • 写入: 将两根字线分别设置成逻辑0和逻辑1, 再向字线加高电压, 效果类似SR锁存器的复位和置位

可将SRAM单元的行为抽象成一个锁存器(假设高电平有效)

  • SEL有效时, Q端读出单元中数据
  • SEL有效且WE有效时, 将D写入单元

回顾: 访问SRAM存储阵列

同步SRAM: 提前用D触发器存放输入端

  • 引入1周期的读写延迟

 

总线接口和软件访问过程与MROM类似

为ysyxSoC添加AM运行时环境

在ysyxSoC上实现TRM的API:

  • 可以用来自由计算的内存区间 - 堆区
    • 堆区需要分配在可写的内存区间, 因此可以分配在SRAM中
  • 程序 “入口” - main(const char *args)
    • main()函数由AM上的程序提供
    • 但考虑整个运行时环境的入口, 需要将程序链接到MROM的地址空间, 并保证TRM的第一条指令与NPC复位后的PC值一致
  • “退出”程序的方式 - halt()
    • ysyxSoC不支持 “关机”, 可借助ebreak让仿真环境结束仿真
  • 打印字符 - putch()
    • 可通过ysyxSoC中的UART16550输出

MROM带来的其他限制: 程序中不能包含对全局变量的写入操作; 栈区需要分配在可写的SRAM中

支持全局变量的写入操作

无法写入全局变量也是硬伤

  • 一个想法 - 把数据段(全局变量的集合)从MROM搬到SRAM

 

谁来搬?

  • 真实芯片上没有仿真环境, 还是要交给程序自己来搬
  • 程序的初始化部分将数据段从MROM搬到SRAM, 再执行主体部分
  • 换一个视角: CPU复位时先执行bootloader, 将程序的数据段从MROM中搬到SRAM, 再跳转到程序本身

 

目标:

  1. 得到数据段在MROM中的地址MA, 在SRAM中的地址SA和长度LEN
  2. 将数据段从MA复制到SA
  3. 让程序代码通过SA访问数据段

获得两种地址

  • 将数据段从MA复制到SA - memcpy(SA, MA, LEN)就行
  • 得到数据段在MROM中的地址MA, 在SRAM中的地址SA和长度LEN
    • MA - 在链接脚本的数据段开始前定义一个符号
    • LEN - 在链接脚本的数据段结束后定义另一个符号, 相减即可
    • SA - ?
  • 让程序代码通过SA访问数据段 - ???

 

SA的获得需要满足

  • 作为地址 -> 最早要在链接的重定位阶段才能确定
  • 程序代码通过SA访问数据段 -> bootloader很难在运行时修改指令 -> 最晚要在bootloader运行前确定

 

结论: SA只能在链接时确定

链接过程中的VMA和LMA

链接过程中存在两种地址相关的概念:

  • VMA(virtual memory address) - 程序运行时对象所在的地址
  • LMA(load memory address) - 程序运行前对象所在的地址

 

通常VMA = LMA, 但为了实现bootloader的功能, 需要利用这两种地址

  • VMA = SA, LMA = MA
MEMORY {
  mrom : ORIGIN = 0x20000000, LENGTH = 4K
  sram : ORIGIN = 0x0f000000, LENGTH = 8K
}
SECTIONS {
  . = ORIGIN(mrom);
  .text : { /* ... */ } > mrom AT> mrom
  .data : { /* ... */ } > sram AT> mrom
  /* ... */
}

问题变成如何获得数据段的LMA - RTFM

  • 里面有宝藏!

可重复编程的非易失存储器

可重复编程的非易失存储器

MROM的另一个问题: 只能编程一次

val rom = VecInit(wordArray.map(x => x.U(wordbits.W)))
io.data := RegNext(rom(io.addr))

想让CPU换个程序运行, 需要重新流片, 谁受得了 😂

 

随着存储技术的发展, 人们发明了可重复编程的flash存储器

  • 更换flash中程序的代价变得可接受
    • 只需要购买一个数十元的烧录器

 

现在人手好几个的U盘 = USB接口的flash存储器(还带一个MCU)

  • 操作系统内置了USB协议的flash驱动程序, 只要插入U盘, 就能写数据
  • 1999年问世的第一款U盘只有8MB, 但已经秒杀了软盘(1.44MB), 也开始逐渐替代光盘(光驱, 刻录机)

通过flash存储1 bit

存储单元采用浮栅晶体管, 在栅极下还有一个浮栅极, 默认为状态1

  • 编程 - 在栅极加大电压, 对浮栅极充电, 变为状态0
  • 读取 - 在栅极加中电压, 检测源极和漏极是否导通
    • 浮栅极充电后, 会抵消部分电场效应, 导通的电压阈值更高
  • 擦除 - 在衬底加大电压, 使浮栅极放电,变为状态1
    • 单位: 多个存储单元(存储块)共用衬底, 存储块是最小的擦除单位
    • 寿命: 放电无法100%放干净, 擦除次数太多, 读结果和充电状态0一样
  • 写入 = 擦除整个存储块 + 重新编程(写放大), 开销比读取操作大很多

存储阵列: NOR flash vs. NAND flash

存储单元有两种组织方式 - 并联(NOR flash)和串联(NAND flash)

  • NOR flash
    • 字线多, 字长短 - 随机读延迟低, 存储密度低
    • 存储块较大 - 擦除时间长
    • 擦除前要先编程 - 写性能低
      • 防止过擦, 损坏存储单元
    • 适合小规模频繁读的应用场景: 代码执行(BIOS ROM)

 

  • NAND flash
    • 字线少, 字长长 - 读延迟高, 吞吐高, 存储密度高
    • 存储块较小 - 擦除时间短
    • 直接擦除 - 写性能较高
    • 适合大规模反复读写的应用场景: 数据存储(U盘, SD卡, SSD)

flash产品对比

来源: Flash 101: NAND Flash vs NOR Flash

访问flash存储阵列

型号为W25Q128JV的NOR flash颗粒

  • 存储阵列
    • 24根地址线, 16MB存储单元
    • 分成256个64KB块, 16个4KB扇区/块, 16个256B页/扇区
    • 扇区是最小的擦除单位
    • 字节是最小的读出单位, 支持随机读取
  • 寄存器
    • 地址寄存器, 控制寄存器(写保护等), 状态寄存器
  • 颗粒内部就是一个设备控制器!
    • 给flash颗粒发送命令(RTFM)

提供总线接口

flash的制造工艺和CPU不同, 两种芯片通常独立生产, 然后焊接到板卡上, 需要考虑引脚数量

  • 引脚太多, 会增大芯片面积, 也增加封装成本
    • 从四边出引脚(如QFP封装): 芯片边长\(=O(n)\)
    • 从底部出引脚(如BGA封装): 芯片边长\(=O(\sqrt{n})\)

 

NOR flash通常提供两种接口, 可按需选择

  • 并行NOR flash - 地址线和数据线全都通过引脚连出芯片
    • 引脚多, 但传输带宽高
    • 型号为MT28EW01GABA的flash颗粒有56个引脚

  • 串行NOR flash - 通过串行总线协议使用少数几根信号线进行通信
    • 型号为W25Q128JV的flash颗粒只有8个引脚

SPI(Serial Peripheral Interface)总线协议

一种串行总线协议, 采用主从设备架构

  • SCK - master发出的时钟信号, 1位
  • SS - slave select, master发出的slave选择信号, 用于指定通信对象, 每个slave对应1位
  • MOSI - master output slave input, master向slave通信的数据线, 1位
  • MISO - master input slave output, slave向master通信的数据线, 1位

 

master通信过程(slave等待master的通信并回复):

  1. 通过SS选择目标slave
  2. 通过SCK信号向slave发送时钟脉冲, 同时将需要发送的信息转化成串行信号, 逐个比特通过MOSI信号传输给slave
  3. 监听MISO信号, 并将通过MISO接收到的串行信号转化成并行信息, 从而获得slave的回复

从flash颗粒中读出数据

根据手册给flash颗粒发送正确的指令序列

  • 8位指令03h表示读数据, 后面加24位的存储单元地址, 通过MOSI传输
  • 之后返回该存储单元的数据, 通过MISO传输
  • 若SCK持续, 则依次读出后续存储单元的内容

SPI协议的实现

核心是串行/并行信号之间的转换: 发送方如何发送, 接收方如何采样

  • 本质上通过移位寄存器实现
  • 但还需要考虑:
    • 发送的尾端(endianness) - 从高位到低位, 还是从低位到高位
    • 时钟相位(clock phase) - 发送/采样发生在SCK上升沿, 还是下降沿
    • 时钟极性(clock polarity) - SCK空闲时是高电平, 还是低电平

 

  • SCK起到同步作用, 双方共同约定上述内容, 并在RTL层次实现约定
  • 不同slave可能有不同约定, 因此master需要可配
    • 具体约定RTFM

SPI master作为设备控制器

SPI master可配 = 可让上层软件配置参数

 

在总线架构中, SPI master有两重身份

  • 作为AXI(或APB等其他AMBA协议)的slave, 负责接收来自CPU的命令
  • 作为SPI的master, 负责给SPI的slave发送命令
  • 可以看成是AXI和SPI之间的桥接模块, 将AXI请求转换成SPI请求, 从而与SPI slave通信

 

上层软件不关心SPI的通信过程, 需要控制器提供SPI传输的功能抽象

  • 一个真实的SPI master控制器
    • ysyxSoC/perip/spi/rtl/spi_top.v - 设备寄存器的读写译码
    • ysyxSoC/perip/spi/rtl/spi_shift.v - 信息的发送和采样
    • ysyxSoC/perip/spi/doc/ - RTFM

SPI master的总线连接 - 接入CPU的地址空间

  • ysyxSoC/perip/spi/rtl/spi_top_apb.v
    • 里面是wishbone协议 - RTFM
    • 我们将它封装成APB协议 - RTFM

 

ysyxSoC中包含APB Xbar和AXI-APB的转接桥, 可以将SPI master控制器连到CPU

应用程序从flash中读出数据的过程

  • 应用程序 - data = flash_read(0x1000)
  • flash驱动程序 - SPI master的访问序列
*(uint32_t *)(SPI_BASE + ?) = flash_cmd
// ...
uint32_t ret = *(uint32_t *)(SPI_BASE + ?);
return ret;
  • RISC-V指令 - sb a0, ?(a1)
  • LSU - 发起AXI-Lite总线请求
    • awaddr = 0x1000_100?, …
  • 总线 - 将请求转发到SPI master控制器
    • AXI-Xbar -> AXI-APB -> APB-Xbar -> APB-wishbone -> 控制器
  • SPI master控制器 - 接收总线请求后, 从awaddr解析出寄存器地址?
  • 地址译码器 - 根据地址?, 译码出需要访问???寄存器
  • 设备寄存器功能 - 启动SPI传输

应用程序从flash中读出数据的过程(2)

  • 发送模块 - 状态机从移位寄存器中取出信息, 按照配置通过MOSI发送
  • flash颗粒 - 监听MOSI, 得到master发送的命令flash_cmd
  • 命令解析 - 从flash_cmd中解析出命令03h和地址0x1000
  • 地址译码器 - 将地址0x1000译码成选择信号
  • 存储阵列 - 根据选择信号选出存储单元
  • 存储单元 - 在栅极加电压, 根据导通情况检测存储单元中的值

 

  • 存储的数据 -> flash颗粒的MISO -> SPI master的MISO -> SPI master的移位寄存器 -> SPI master的RX寄存器 -> wishbone.wb_dat_o信号 -> APB.prdata信号 -> AXI-Lite.rdata信号 -> a0寄存器 -> ret变量 -> data变量

运行flash中的程序

可以先通过flash_read()将程序加载到SRAM, 然后跳转到SRAM中的程序执行

 

我们已经把程序烧录到flash中了, 可以直接从flash中取指令执行吗?

  • flash_read()也是程序的一部分, 其指令序列也烧录在flash中
    • 想要从flash中取出指令, 需要先执行flash_read()
    • 想要执行flash_read(), 需要先从flash中取出其指令
    • 变成了一个 “鸡和蛋”的循环依赖问题 😂

 

问题的根源: 取指令是硬件层的行为, 不应该依赖软件函数来实现

  • 应在硬件层实现flash_read()的功能!

XIP方式

在硬件层实现flash_read() = 用状态机向SPI master控制器发送请求序列

  • “就地执行”方式, XIP(eXecute In Place)
    • 读出指令后马上执行, 无需先将指令读到内存(如SRAM)中

 

  • 具体实现
    • 将flash存储空间映射到CPU的地址空间
    • 若往该空间发送读请求, 则状态机进入XIP模式
    • 状态机发出请求序列, 激活SPI master控制器与flash颗粒通信
      • 配置SPI master的寄存器, 发送flash读命令, 等待完成, 返回数据

 

可以在flash中直接执行程序后, 就可以完全替代MROM了

  • XIP方式不支持写请求: 往flash写入1 bit都是非常复杂的

总结

SoC的设备栈/存储栈

一个大致的总结:

MROM UART SRAM flash
程序功能 代码/只读数据 printf() 可写数据/堆/栈 代码/只读数据
函数 - putch() - flash_read()(非XIP)
C代码 指针解引用 指针解引用 指针解引用 指针解引用
RISC-V指令 取指/load store 取指/load/store 取指/load[/store(SPI)]
CPU单元 IFU/LSU LSU IFU/LSU IFU/LSU
SoC总线桥 - AXI-APB-wb - AXI-APB-wb-SPI
总线接口 AXI-Lite wishbone(wb) AXI-Lite SPI(含XIP)
地址译码 选择存储单元 选择寄存器 选择存储单元 选择存储单元
1 bit的访问 - 移位寄存器 字线选通晶体管 字线控制栅极加压(读)
1 bit的表示 电源/地 线缆信号 6管存储单元 浮栅晶体管的充/放电
  • 学习SoC = RTFM + RTFSC + WTFSC
  • 还没解决的问题: 更大的RAM