
我们已经学习了总线协议, 知道CPU如何与设备通信
本次课内容: 以 “一生一芯”流片用的SoC为例, 理解程序, CPU和外设之间如何交互
一个现实 - 设备的属性五花八门
如何连接这些设备, 让CPU能正确访问, 就成了SoC需要关注的问题
`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如何通过AXI-Lite总线指定读出设备寄存器UART_REG_RB?
araddr取0x1000_0000? 读多长?
“读多长”在memory中没那么重要, 可以读4字节/8字节给CPU选
但在设备中, 读1字节和读4字节的行为不一样
总线需要细心地处理这个问题
AXI-Lite无法解决上述问题
完整的AXI总线协议通过arsize/awsize信号处理上述问题
lb指令只访问1字节XXXX -> AAAABBBB,
4-byte的wdata应该放在A部分还是B部分?XXXX <- AAAABBBB,
8-byte的rdata应该取A部分还是B部分?
slverr
可以通过AXI Data Width Converter连接总线数据位宽不一致的上下游
Xilinx的AXI Interconnect文档介绍了更多的AXI适配器:
RocketChip项目也提供一些AXI适配器(文档):
一个面向RISC-V程序的简单freestanding运行时环境
0开始执行addi指令ebreak指令
a0=0时, 输出寄存器a1低8位的字符a0=1时, 结束运行
putchar()函数输出RAM是易失存储器(volatile memory)
一个简单的方案: ROM(Read-Only Memory)
ROM的实现方式有很多
MROM的本质 - 将信息 “硬编码”在门电路中
0(地)和1(电源),
很暴力的做法 😂
MROM的编程是一次性的
一个只对学习有意义的好处 - 制造工艺和电气特性都和CPU一样

地址译码器 + 存储阵列: 本质上是一些数据输入端为常数的多路选择器
// 实现类似 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中存储的数据
a = *(int *)(MROM_BASE + 4)lw a0, 4(a1)araddr = 0x2000_0004araddr解析出MROM的地址44译码成选择信号
rdata信号 -> a0寄存器
-> a变量
取指的过程是类似的, 只不过取指请求由IFU发出
能存放程序之后, 下一步就要考虑如何输出
串口 = 按照一定配置将字符的编码传送到线缆上的设备, 配置包括:
115200 8N1
上层软件不希望了解传输细节, 需要串口控制器提供串口的功能抽象
ysyxSoC/perip/uart16550/rtl/uart_regs.v
ysyxSoC/perip/uart16550/rtl/uart_tfifo.v
ysyxSoC/perip/uart16550/rtl/uart_transmitter.v
ysyxSoC/perip/uart16550/doc/
ysyxSoC/perip/uart16550/rtl/uart_top_apb.v
ysyxSoC中包含APB Xbar和AXI-APB的转接桥, 可以将UART16550控制器连到CPU
printf("Hello RISC-V")putch('H')*(uint8_t *)(UART_BASE + 0) = chsb a0, 0(a1)awaddr = 0x1000_0000, wdata = 0x48,
wstrb = 0x1awaddr解析出寄存器地址00, 译码出需要访问TX寄存器H写入发送队列H,
按照设置的波特率等配置, 将0100_1000发送到线缆上大部分程序都需要向存储器写入数据
MROM的一个问题: 不可写入
SRAM是对ysyxSoC最简单的SRAM


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

同步SRAM: 提前用D触发器存放输入端
总线接口和软件访问过程与MROM类似

在ysyxSoC上实现TRM的API:
main(const char *args)
main()函数由AM上的程序提供halt()
ebreak让仿真环境结束仿真putch()
MROM带来的其他限制: 程序中不能包含对全局变量的写入操作; 栈区需要分配在可写的SRAM中
无法写入全局变量也是硬伤
谁来搬?
目标:
MA,
在SRAM中的地址SA和长度LENMA复制到SASA访问数据段MA复制到SA -
memcpy(SA, MA, LEN)就行MA,
在SRAM中的地址SA和长度LEN
MA - 在链接脚本的数据段开始前定义一个符号LEN - 在链接脚本的数据段结束后定义另一个符号,
相减即可SA - ?SA访问数据段 - ???
SA的获得需要满足
SA访问数据段 ->
bootloader很难在运行时修改指令 -> 最晚要在bootloader运行前确定
结论: SA只能在链接时确定
链接过程中存在两种地址相关的概念:
通常VMA = LMA, 但为了实现bootloader的功能, 需要利用这两种地址
SA, LMA = MA问题变成如何获得数据段的LMA - RTFM
MROM的另一个问题: 只能编程一次
想让CPU换个程序运行, 需要重新流片, 谁受得了 😂

随着存储技术的发展, 人们发明了可重复编程的flash存储器
现在人手好几个的U盘 = USB接口的flash存储器(还带一个MCU)

存储单元采用浮栅晶体管, 在栅极下还有一个浮栅极,
默认为状态1
01
0一样存储单元有两种组织方式 - 并联(NOR flash)和串联(NAND flash)


型号为W25Q128JV的NOR flash颗粒

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


一种串行总线协议, 采用主从设备架构
master通信过程(slave等待master的通信并回复):

根据手册给flash颗粒发送正确的指令序列
03h表示读数据, 后面加24位的存储单元地址,
通过MOSI传输核心是串行/并行信号之间的转换: 发送方如何发送, 接收方如何采样

SPI master可配 = 可让上层软件配置参数
在总线架构中, SPI master有两重身份
上层软件不关心SPI的通信过程, 需要控制器提供SPI传输的功能抽象
ysyxSoC/perip/spi/rtl/spi_top.v -
设备寄存器的读写译码ysyxSoC/perip/spi/rtl/spi_shift.v -
信息的发送和采样ysyxSoC/perip/spi/doc/ - RTFM
ysyxSoC中包含APB Xbar和AXI-APB的转接桥, 可以将SPI master控制器连到CPU
data = flash_read(0x1000)*(uint32_t *)(SPI_BASE + ?) = flash_cmd
// ...
uint32_t ret = *(uint32_t *)(SPI_BASE + ?);
return ret;sb a0, ?(a1)awaddr = 0x1000_100?, …awaddr解析出寄存器地址??, 译码出需要访问???寄存器flash_cmdflash_cmd中解析出命令03h和地址0x10000x1000译码成选择信号
wishbone.wb_dat_o信号 -> APB.prdata信号
-> AXI-Lite.rdata信号 -> a0寄存器 ->
ret变量 -> data变量可以先通过flash_read()将程序加载到SRAM,
然后跳转到SRAM中的程序执行
我们已经把程序烧录到flash中了, 可以直接从flash中取指令执行吗?
flash_read()也是程序的一部分, 其指令序列也烧录在flash中
flash_read()flash_read(), 需要先从flash中取出其指令
问题的根源: 取指令是硬件层的行为, 不应该依赖软件函数来实现
flash_read()的功能!在硬件层实现flash_read() = 用状态机向SPI
master控制器发送请求序列
可以在flash中直接执行程序后, 就可以完全替代MROM了
一个大致的总结:
| 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管存储单元 | 浮栅晶体管的充/放电 |