我们已经学习了总线协议, 知道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_0004
araddr
解析出MROM的地址4
4
译码成选择信号
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) = ch
sb a0, 0(a1)
awaddr = 0x1000_0000
, wdata = 0x48
,
wstrb = 0x1
awaddr
解析出寄存器地址0
0
, 译码出需要访问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
和长度LEN
MA
复制到SA
SA
访问数据段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
0
1
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_cmd
flash_cmd
中解析出命令03h
和地址0x1000
0x1000
译码成选择信号
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管存储单元 | 浮栅晶体管的充/放电 |