我们已经学习了总线协议, 知道CPU如何与设备通信
本次课内容: 以 “一生一芯”流片用的SoC为例, 通过RTFSC理解大家实现的CPU如何与设备交互
一个面向RISC-V程序的, 只支持两条指令的简单freestanding运行时环境
0
开始执行addi
指令ebreak
指令
a0=0
时, 输出寄存器a1
低8位的字符a0=1
时, 结束运行
在真实的SoC中, 上电后没有运行时环境帮助我们了
RAM是易失存储器(volatile memory), 上电时无有效数据
一个简单的方案: ROM
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 rom = VecInit(wordArray.map(x => x.U(wordbits.W)))
io.data := RegNext(rom(io.addr))
行为模型 - 综合成触发器, 基于触发器的行为搭建出ROM的行为
ASIC原语 - 使用晶体管直接搭建ROM, 有更优的面积和功耗
class AXILiteROM(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 rom = 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 romAddr = Mux(io.ar.fire, getAddr(io.ar.bits.addr), 0.U)
io.r.bits.data := RegEnable(rom(romAddr)), io.ar.fire)
io.r.bits.resp := 0.U
io.ar.ready := (state === s_idle)
io.r.valid := (state === s_wait_rready)
}
ysyxSoC项目提供了一些流片用的真实IP
我们可以来RTFSC
ysyxSoC/ysyx/perip/uart16550/rtl/uart_apb.v
ysyxSoC/ysyx/perip/uart16550/rtl/uart_regs.v
只需要设置正确的除数即可(我们不使用中断功能)
`define UART_REG_DL1 `UART_ADDR_WIDTH'd0 // Divisor latch bytes (1-2)
`define UART_REG_DL2 `UART_ADDR_WIDTH'd1
void uart_init() {
// RTFM
outb(UART_BASE + 0x3, 0x83); // LCR <= 8N1, DLA
// DL = clk / (16 * baud rate) = 25MHz / (16 * 115200) = 13.56 = 14
outb(UART_BASE + 0x1, 0); // DLA.MSB
outb(UART_BASE + 0x0, 14); // DLA.LSB
outb(UART_BASE + 0x3, 0x3); // LCR <= 8N1
}
串口终端也需要进行相应的配置
115200 8N1
写入TX队列:
tf_push
信号tf_push
信号控制数据写入TX队列
TX队列发送字符:
ysyxSoC/ysyx/perip/uart16550/rtl/uart_transmitter.v
中的状态机
enable
信号的控制下工作enable
信号的频率由除数决定,
最后控制字符发送的速率(波特率)有ROM和UART, 怎么知道应该访问哪一个?
`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总线指定读出地址为0的设备寄存器?
araddr
取0x1000_0000
? 读多长?
“读多长”在memory中没那么重要, 可以读4字节/8字节给CPU选
但在设备中, 读1字节和读4字节的行为不一样
总线需要细心地处理这个问题
完整的AXI总线通过arsize/awsize信号处理上述问题
AXI-Lite无法解决上述问题
我们刚才的ROM是硬连线写死在芯片内的
随着材料技术的发展, 人们发明了现场可编程的flash存储器
型号W25Q128JV, RTFM
ysyxSoC/ysyx/perip/spi/rtl/spi_shift.v
根据手册给flash颗粒发送正确的指令序列
03h
表示读数据, 后面加24位的存储单元地址,
通过MOSI传输#define SPI_REG_TX0 0x00
#define SPI_REG_TX1 0x04
#define SPI_REG_RX0 0x00
#define SPI_REG_CTL 0x10
#define SPI_CTL_RD_ENDIAN 0x00004000
#define SPI_CTL_ASS 0x00002000
#define SPI_CTL_TX_NEGEDGE 0x00000400
#define SPI_CTL_GO 0x00000100
uint32_t flash_read(uint32_t addr) {
outl(SPI_BASE + SPI_REG_TX0, 0);
uint32_t cmd = 0x03000000 | (addr & 0xffffff);
outl(SPI_BASE + SPI_REG_TX1, cmd);
// select slave #1 | sck = clk / 2 | 64 bit transfer | auto-SS
uint32_t ctl_word = (0x01 << 24) | (0x00 << 16) | (0x40 << 0) | SPI_CTL_ASS |
// read starting from MSB | MOSI changes on falling edge | start!
SPI_CTL_RD_ENDIAN | SPI_CTL_TX_NEGEDGE | SPI_CTL_GO;
outl(SPI_BASE + SPI_REG_CTL, ctl_word);
while (inl(SPI_BASE + SPI_REG_CTL) & SPI_CTL_GO); // wait until finish
return inl(SPI_BASE + SPI_REG_RX0); // return least 32 bit of slave output
}
问题: 上面的函数编译成指令, 烧写在flash颗粒里面, 如何读出?
想法: 硬件直接将CPU的取指请求翻译成flash颗粒的请求, 读出的内容作为CPU指令直接返回
解决方案: 用状态机实现flash_read()
函数的功能!
ysyxSoC/ysyx/perip/spi/rtl/spi.v
[0x3000_0000, 0x3fff_ffff)
为了支持store指令的执行, 需要有一个支持随机写操作的存储器
ysyxSoC/ysyx/perip/sdram/rtl/sdram_axi_core.v
refresh_timer_q
减到0时发送充电命令(cmd:0010)
将AXI读写请求翻译成上述命令序列
[0xfc00_0000, 0xffff_ffff)
, 后续可能会修改,
以流片SoC的报告为准sp
分配到上述空间memset()
可以直接写入SDRAM, 无需读flashsp
也需要分配到SDRAM空间在M模式下均由真实的硬件触发
M模式 | S模式 | |
---|---|---|
时钟中断 | CLINT的计时器比较 | mip.STIP |
软件中断 | CLINT的寄存器 | mip.SSIP |
外部中断 | PLIC的中断线 | PLIC的中断线或mip.SEIP |
系统支持
RTFM: RISC-V的CLINT(Core Local INTerrupt controller)
一个非常简单的设备, 只有3个寄存器
mtime
- 可读可写, 以恒定速率增加mtimecmp
- 可读可写,
mtime >= mtimecmp
时产生M模式时钟中断msip
- 可读可写, 写入1则触发M模式软件中断
很容易基于总线实现, 我们将它作为一个总线的作业留给大家
一个外部中断的选择器(RTFM)
以M模式时钟中断为例, 其响应需要满足
mtime >= mtimecmp
的比较结果相连
mtimecmp
可以清除M模式时钟中断
medeleg
和mideleg
将相应异常/中断委托给S模式处理
Direct Memory Access
memcpy()
加速器, 帮助一些 “高级”的外设进行数据移动,
无需CPU参与
ChipLink = 用数量较少的引脚将一个请求打包传送到对面后解包
在FPGA端将ChipLink包拼接并还原为TileLink请求, 然后转回AXI请求
为了提高集成度, 我们把多个同学的CPU集成到一个芯片中
SoC代码里面有很多宝藏
现在你已经完全理解仙剑奇侠传如何在真实的SoC上运行!