【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】
学习cpu,主要还是因为自己对它的原理和实现还有很多不明白、不清楚的地方,本着追根溯源的精神,正好借助于verilog开源代码一窥究竟。和十年、二十年前相比较,现在数字电路学习、verilog学习、ip学习、开发板的购买方面要便捷很多。记得,最早的时候,市面上只有一本关于cpu设计的书,那就是《CPU源代码分析与芯片设计及Linux移植》。这本书上面不光谈了cpu设计,还谈到了怎么让gcc适配新的cpu、怎么把linux移植到新的cpu上面。坦白说,这些内容对于刚入门的新手来说,其实是非常艰困的,学习的曲线未免太陡峭了。
后面随着网络的普及,特别是github这样的网站出现,大家已经可以接触到很多的开源cpu代码了。你可以说,这些代码良莠不齐,但是至少说大家发现,原来一个人也是可以做cpu、写os、完成一个小编译器的。曾经很高大上的东西,自己也是可以掌握的,而不再是纸上谈兵的内容。
最近这一段时间,在网上忽然看到一个tinyriscv的代码,是一位cpu爱好者写的一个完整的mcu。整个代码非常简洁,还移植了freertos,支持jtag烧入,个人觉得非常建议拿来学习。
1、开源代码的地址
https://gitee.com/liangkangnan/tinyriscv
2、开源代码的架构
整个mcu是有四个master,六个slave组成的。图中,master的部分都是蓝色。slave的部分都是绿色。其中riscv作为cpu,有两个master口,一个是指令,一个是数据。download是带有下载功能的uart口。jtag是调试口。slave的部分,这个比较正常,就是一般的rom、ram、gpio、uart、timer和spi,都是常用的一些外设。
3、mcu的接口
-
// tinyriscv soc顶层模块
-
module
tinyriscv_soc_top(
-
-
input wire clk,
-
input wire rst,
-
-
output reg over, // 测试是否完成信号
-
output reg succ, // 测试是否成功信号
-
-
output wire halted_ind, // jtag是否已经halt住CPU信号
-
-
input wire uart_debug_pin, // 串口下载使能引脚
-
-
output wire uart_tx_pin, // UART发送引脚
-
input wire uart_rx_pin, // UART接收引脚
-
inout wire[
1:
0] gpio, // GPIO引脚
-
-
input wire jtag_TCK, // JTAG TCK引脚
-
input wire jtag_TMS, // JTAG TMS引脚
-
input wire jtag_TDI, // JTAG TDI引脚
-
output wire jtag_TDO, // JTAG TDO引脚
-
-
input wire spi_miso, // SPI MISO引脚
-
output wire spi_mosi, // SPI MOSI引脚
-
output wire spi_ss, // SPI SS引脚
-
output wire spi_clk // SPI CLK引脚
-
-
);
mcu的接口就类似于大家正常看到的那些chip的接口一样。其中over、succ、halted_ind很明显是为了调试用的。clk是时钟,rst是复位,uart_debug_pin是下载,uart_tx_pin&uart_rx_pin是串口,gpio是通用口,jtag是调试口,spi是协议口。之前谈到的rom、ram很明显用片上资源实现了。timer也是内部资源实现,对外扇出的就是以上这些端口。
4、总线
-
/*
-
Copyright 2020 Blue Liang, liangkangnan@163.com
-
-
Licensed under the Apache License, Version 2.0 (the "License");
-
you may not use this file except in compliance with the License.
-
You may obtain a copy of the License at
-
-
http://www.apache.org/licenses/LICENSE-2.0
-
-
Unless required by applicable law or agreed to in writing, software
-
distributed under the License is distributed on an "AS IS" BASIS,
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-
See the License for the specific language governing permissions and
-
limitations under the License.
-
*/
-
-
`
include
"defines.v"
-
-
-
// RIB总线模块
-
module
rib(
-
-
input wire clk,
-
input wire rst,
-
-
// master
0
interface
-
input wire[`MemAddrBus] m0_addr_i, // 主设备
0读、写地址
-
input wire[`MemBus] m0_data_i, // 主设备
0写数据
-
output reg[`MemBus] m0_data_o, // 主设备
0读取到的数据
-
input wire m0_req_i, // 主设备
0访问请求标志
-
input wire m0_we_i, // 主设备
0写标志
-
-
// master
1
interface
-
input wire[`MemAddrBus] m1_addr_i, // 主设备
1读、写地址
-
input wire[`MemBus] m1_data_i, // 主设备
1写数据
-
output reg[`MemBus] m1_data_o, // 主设备
1读取到的数据
-
input wire m1_req_i, // 主设备
1访问请求标志
-
input wire m1_we_i, // 主设备
1写标志
-
-
// master
2
interface
-
input wire[`MemAddrBus] m2_addr_i, // 主设备
2读、写地址
-
input wire[`MemBus] m2_data_i, // 主设备
2写数据
-
output reg[`MemBus] m2_data_o, // 主设备
2读取到的数据
-
input wire m2_req_i, // 主设备
2访问请求标志
-
input wire m2_we_i, // 主设备
2写标志
-
-
// master
3
interface
-
input wire[`MemAddrBus] m3_addr_i, // 主设备
3读、写地址
-
input wire[`MemBus] m3_data_i, // 主设备
3写数据
-
output reg[`MemBus] m3_data_o, // 主设备
3读取到的数据
-
input wire m3_req_i, // 主设备
3访问请求标志
-
input wire m3_we_i, // 主设备
3写标志
-
-
// slave
0
interface
-
output reg[`MemAddrBus] s0_addr_o, // 从设备
0读、写地址
-
output reg[`MemBus] s0_data_o, // 从设备
0写数据
-
input wire[`MemBus] s0_data_i, // 从设备
0读取到的数据
-
output reg s0_we_o, // 从设备
0写标志
-
-
// slave
1
interface
-
output reg[`MemAddrBus] s1_addr_o, // 从设备
1读、写地址
-
output reg[`MemBus] s1_data_o, // 从设备
1写数据
-
input wire[`MemBus] s1_data_i, // 从设备
1读取到的数据
-
output reg s1_we_o, // 从设备
1写标志
-
-
// slave
2
interface
-
output reg[`MemAddrBus] s2_addr_o, // 从设备
2读、写地址
-
output reg[`MemBus] s2_data_o, // 从设备
2写数据
-
input wire[`MemBus] s2_data_i, // 从设备
2读取到的数据
-
output reg s2_we_o, // 从设备
2写标志
-
-
// slave
3
interface
-
output reg[`MemAddrBus] s3_addr_o, // 从设备
3读、写地址
-
output reg[`MemBus] s3_data_o, // 从设备
3写数据
-
input wire[`MemBus] s3_data_i, // 从设备
3读取到的数据
-
output reg s3_we_o, // 从设备
3写标志
-
-
// slave
4
interface
-
output reg[`MemAddrBus] s4_addr_o, // 从设备
4读、写地址
-
output reg[`MemBus] s4_data_o, // 从设备
4写数据
-
input wire[`MemBus] s4_data_i, // 从设备
4读取到的数据
-
output reg s4_we_o, // 从设备
4写标志
-
-
// slave
5
interface
-
output reg[`MemAddrBus] s5_addr_o, // 从设备
5读、写地址
-
output reg[`MemBus] s5_data_o, // 从设备
5写数据
-
input wire[`MemBus] s5_data_i, // 从设备
5读取到的数据
-
output reg s5_we_o, // 从设备
5写标志
-
-
output reg hold_flag_o // 暂停流水线标志
-
-
);
-
-
-
// 访问地址的最高4位决定要访问的是哪一个从设备
-
// 因此最多支持16个从设备
-
parameter [
3:
0]slave_0 =
4
'b0000;
-
parameter [3:0]slave_1 = 4'b0001;
-
parameter [
3:
0]slave_2 =
4
'b0010;
-
parameter [3:0]slave_3 = 4'b0011;
-
parameter [
3:
0]slave_4 =
4
'b0100;
-
parameter [3:0]slave_5 = 4'b0101;
-
-
parameter [
1:
0]grant0 =
2
'h0;
-
parameter [1:0]grant1 = 2'h1;
-
parameter [
1:
0]grant2 =
2
'h2;
-
parameter [1:0]grant3 = 2'h3;
-
-
wire[
3:
0] req;
-
reg[
1:
0] grant;
-
-
-
// 主设备请求信号
-
assign req = {m3_req_i, m2_req_i, m1_req_i, m0_req_i};
-
-
// 仲裁逻辑
-
// 固定优先级仲裁机制
-
// 优先级由高到低:主设备3,主设备0,主设备2,主设备1
-
always @ (*) begin
-
if (req[
3]) begin
-
grant = grant3;
-
hold_flag_o = `HoldEnable;
-
end
else
if (req[
0]) begin
-
grant = grant0;
-
hold_flag_o = `HoldEnable;
-
end
else
if (req[
2]) begin
-
grant = grant2;
-
hold_flag_o = `HoldEnable;
-
end
else begin
-
grant = grant1;
-
hold_flag_o = `HoldDisable;
-
end
-
end
-
-
// 根据仲裁结果,选择(访问)对应的从设备
-
always @ (*) begin
-
m0_data_o = `ZeroWord;
-
m1_data_o = `INST_NOP;
-
m2_data_o = `ZeroWord;
-
m3_data_o = `ZeroWord;
-
-
s0_addr_o = `ZeroWord;
-
s1_addr_o = `ZeroWord;
-
s2_addr_o = `ZeroWord;
-
s3_addr_o = `ZeroWord;
-
s4_addr_o = `ZeroWord;
-
s5_addr_o = `ZeroWord;
-
s0_data_o = `ZeroWord;
-
s1_data_o = `ZeroWord;
-
s2_data_o = `ZeroWord;
-
s3_data_o = `ZeroWord;
-
s4_data_o = `ZeroWord;
-
s5_data_o = `ZeroWord;
-
s0_we_o = `WriteDisable;
-
s1_we_o = `WriteDisable;
-
s2_we_o = `WriteDisable;
-
s3_we_o = `WriteDisable;
-
s4_we_o = `WriteDisable;
-
s5_we_o = `WriteDisable;
-
-
case (grant)
-
grant0: begin
-
case (m0_addr_i[
31:
28])
-
slave_0: begin
-
s0_we_o = m0_we_i;
-
s0_addr_o = {{
4
'h0}, {m0_addr_i[27:0]}};
-
s0_data_o = m0_data_i;
-
m0_data_o = s0_data_i;
-
end
-
slave_1: begin
-
s1_we_o = m0_we_i;
-
s1_addr_o = {{4'h0}, {m0_addr_i[
27:
0]}};
-
s1_data_o = m0_data_i;
-
m0_data_o = s1_data_i;
-
end
-
slave_2: begin
-
s2_we_o = m0_we_i;
-
s2_addr_o = {{
4
'h0}, {m0_addr_i[27:0]}};
-
s2_data_o = m0_data_i;
-
m0_data_o = s2_data_i;
-
end
-
slave_3: begin
-
s3_we_o = m0_we_i;
-
s3_addr_o = {{4'h0}, {m0_addr_i[
27:
0]}};
-
s3_data_o = m0_data_i;
-
m0_data_o = s3_data_i;
-
end
-
slave_4: begin
-
s4_we_o = m0_we_i;
-
s4_addr_o = {{
4
'h0}, {m0_addr_i[27:0]}};
-
s4_data_o = m0_data_i;
-
m0_data_o = s4_data_i;
-
end
-
slave_5: begin
-
s5_we_o = m0_we_i;
-
s5_addr_o = {{4'h0}, {m0_addr_i[
27:
0]}};
-
s5_data_o = m0_data_i;
-
m0_data_o = s5_data_i;
-
end
-
default: begin
-
-
end
-
endcase
-
end
-
grant1: begin
-
case (m1_addr_i[
31:
28])
-
slave_0: begin
-
s0_we_o = m1_we_i;
-
s0_addr_o = {{
4
'h0}, {m1_addr_i[27:0]}};
-
s0_data_o = m1_data_i;
-
m1_data_o = s0_data_i;
-
end
-
slave_1: begin
-
s1_we_o = m1_we_i;
-
s1_addr_o = {{4'h0}, {m1_addr_i[
27:
0]}};
-
s1_data_o = m1_data_i;
-
m1_data_o = s1_data_i;
-
end
-
slave_2: begin
-
s2_we_o = m1_we_i;
-
s2_addr_o = {{
4
'h0}, {m1_addr_i[27:0]}};
-
s2_data_o = m1_data_i;
-
m1_data_o = s2_data_i;
-
end
-
slave_3: begin
-
s3_we_o = m1_we_i;
-
s3_addr_o = {{4'h0}, {m1_addr_i[
27:
0]}};
-
s3_data_o = m1_data_i;
-
m1_data_o = s3_data_i;
-
end
-
slave_4: begin
-
s4_we_o = m1_we_i;
-
s4_addr_o = {{
4
'h0}, {m1_addr_i[27:0]}};
-
s4_data_o = m1_data_i;
-
m1_data_o = s4_data_i;
-
end
-
slave_5: begin
-
s5_we_o = m1_we_i;
-
s5_addr_o = {{4'h0}, {m1_addr_i[
27:
0]}};
-
s5_data_o = m1_data_i;
-
m1_data_o = s5_data_i;
-
end
-
default: begin
-
-
end
-
endcase
-
end
-
grant2: begin
-
case (m2_addr_i[
31:
28])
-
slave_0: begin
-
s0_we_o = m2_we_i;
-
s0_addr_o = {{
4
'h0}, {m2_addr_i[27:0]}};
-
s0_data_o = m2_data_i;
-
m2_data_o = s0_data_i;
-
end
-
slave_1: begin
-
s1_we_o = m2_we_i;
-
s1_addr_o = {{4'h0}, {m2_addr_i[
27:
0]}};
-
s1_data_o = m2_data_i;
-
m2_data_o = s1_data_i;
-
end
-
slave_2: begin
-
s2_we_o = m2_we_i;
-
s2_addr_o = {{
4
'h0}, {m2_addr_i[27:0]}};
-
s2_data_o = m2_data_i;
-
m2_data_o = s2_data_i;
-
end
-
slave_3: begin
-
s3_we_o = m2_we_i;
-
s3_addr_o = {{4'h0}, {m2_addr_i[
27:
0]}};
-
s3_data_o = m2_data_i;
-
m2_data_o = s3_data_i;
-
end
-
slave_4: begin
-
s4_we_o = m2_we_i;
-
s4_addr_o = {{
4
'h0}, {m2_addr_i[27:0]}};
-
s4_data_o = m2_data_i;
-
m2_data_o = s4_data_i;
-
end
-
slave_5: begin
-
s5_we_o = m2_we_i;
-
s5_addr_o = {{4'h0}, {m2_addr_i[
27:
0]}};
-
s5_data_o = m2_data_i;
-
m2_data_o = s5_data_i;
-
end
-
default: begin
-
-
end
-
endcase
-
end
-
grant3: begin
-
case (m3_addr_i[
31:
28])
-
slave_0: begin
-
s0_we_o = m3_we_i;
-
s0_addr_o = {{
4
'h0}, {m3_addr_i[27:0]}};
-
s0_data_o = m3_data_i;
-
m3_data_o = s0_data_i;
-
end
-
slave_1: begin
-
s1_we_o = m3_we_i;
-
s1_addr_o = {{4'h0}, {m3_addr_i[
27:
0]}};
-
s1_data_o = m3_data_i;
-
m3_data_o = s1_data_i;
-
end
-
slave_2: begin
-
s2_we_o = m3_we_i;
-
s2_addr_o = {{
4
'h0}, {m3_addr_i[27:0]}};
-
s2_data_o = m3_data_i;
-
m3_data_o = s2_data_i;
-
end
-
slave_3: begin
-
s3_we_o = m3_we_i;
-
s3_addr_o = {{4'h0}, {m3_addr_i[
27:
0]}};
-
s3_data_o = m3_data_i;
-
m3_data_o = s3_data_i;
-
end
-
slave_4: begin
-
s4_we_o = m3_we_i;
-
s4_addr_o = {{
4
'h0}, {m3_addr_i[27:0]}};
-
s4_data_o = m3_data_i;
-
m3_data_o = s4_data_i;
-
end
-
slave_5: begin
-
s5_we_o = m3_we_i;
-
s5_addr_o = {{4'h0}, {m3_addr_i[
27:
0]}};
-
s5_data_o = m3_data_i;
-
m3_data_o = s5_data_i;
-
end
-
default: begin
-
-
end
-
endcase
-
end
-
default: begin
-
-
end
-
endcase
-
end
-
-
endmodule
这个总线为什么叫rib,不是很清楚。不过从代码上看,内容非常简单,就是将命令和数据从master传递给slave。并且根据grant的逻辑,一次只能有一个master参与操作。等选定了master之后, 再根据设备地址的[31:28]位,决定把这个请求发给哪一个slave设备。
5、jtag代码
-
module jtag_top
#(
-
parameter DMI_ADDR_BITS =
6,
-
parameter DMI_DATA_BITS =
32,
-
parameter DMI_OP_BITS =
2)(
-
-
input wire clk,
-
input wire jtag_rst_n,
-
-
input wire jtag_pin_TCK,
-
input wire jtag_pin_TMS,
-
input wire jtag_pin_TDI,
-
output wire jtag_pin_TDO,
-
-
output wire reg_we_o,
-
output wire[
4:
0] reg_addr_o,
-
output wire[
31:
0] reg_wdata_o,
-
input wire[
31:
0] reg_rdata_i,
-
output wire mem_we_o,
-
output wire[
31:
0] mem_addr_o,
-
output wire[
31:
0] mem_wdata_o,
-
input wire[
31:
0] mem_rdata_i,
-
output wire op_req_o,
-
-
output wire halt_req_o,
-
output wire reset_req_o
-
-
);
很多做嵌入式的同学虽然不知道jtag是怎么实现,不过大多数应该用过jtag。如果程序代码跑在ram里面,用软件断点就可以了。但是如果调试的代码保存在rom、flash当中,那么这个时候就只能用jtag来设置硬件断点了。上面这个,就描述了jtag有哪些接口需要处理。
tck、tms、tdi、tdo,这些都是芯片外部接口数据,主要连接jlink这些工具。reg_we_o、reg_addr_o、reg_wdata_o、reg_rdata_i这些都是对cpu的寄存器进行读写。mem_we_o、mem_addr_o、mem_wdata_o、mem_rdata_i、op_req_o则是和rib总线的对接,这样一来就可以借助于bus访问所有的外设设备了。
6、uart download模块
-
module
uart_debug(
-
-
input wire clk, // 时钟信号
-
input wire rst, // 复位信号
-
-
input wire debug_en_i, // 模块使能信号
-
-
output wire req_o,
-
output reg mem_we_o,
-
output reg[
31:
0] mem_addr_o,
-
output reg[
31:
0] mem_wdata_o,
-
input wire[
31:
0] mem_rdata_i
-
-
);
这个download模块比较特殊,主要就是为了mcu可以正常的把版本烧入到flash里面去。大家可以想一下,自己用的mcu里面,是不是有的芯片也添加了类似这样的功能。
7、简单的一个ram slave代码
-
module
ram(
-
-
input wire clk,
-
input wire rst,
-
-
input wire we_i, // write enable
-
input wire[`MemAddrBus] addr_i, // addr
-
input wire[`MemBus] data_i,
-
-
output reg[`MemBus] data_o // read data
-
-
);
-
-
reg[`MemBus] _ram[
0:`MemNum -
1];
-
-
-
always @ (posedge clk) begin
-
if (we_i == `WriteEnable) begin
-
_ram[addr_i[
31:
2]] <= data_i;
-
end
-
end
-
-
always @ (*) begin
-
if (rst == `RstEnable) begin
-
data_o = `ZeroWord;
-
end
else begin
-
data_o = _ram[addr_i[
31:
2]];
-
end
-
end
-
-
endmodule
这是一份slave代码,主要是负责数据的读取和写入。从代码上看,内容也简单,如果是读取,那么组合逻辑直接给出;如果是写入,那么需要等时钟上升沿的时候才写入。
8、公用的功能模块gen_buff.v
-
module gen_pipe_dff
#(
-
parameter DW =
32)(
-
-
input wire clk,
-
input wire rst,
-
input wire hold_en,
-
-
input wire[DW-
1:
0] def_val,
-
input wire[DW-
1:
0] din,
-
output wire[DW-
1:
0] qout
-
-
);
-
-
reg[DW-
1:
0] qout_r;
-
-
always @ (posedge clk) begin
-
if (!rst | hold_en) begin
-
qout_r <= def_val;
-
end
else begin
-
qout_r <= din;
-
end
-
end
-
-
assign qout = qout_r;
-
-
endmodule
部分代码比较琐碎,作者把它提取成了公共模块。这样,在各个模块使用的时候,直接例化就可以了。类似的模块还有full_handshake_rx.v、full_handshake_tx.v。
9、riscv cpu
riscv的内容和我们正常的cpu设计差不多,也要处理逻辑运损、移位运算、数学运算、跳转、异常、中断这些内容。只不过,这里的riscv是三级流水线,省去了访存和写回这两级。整体上虽然效率略有降低,不过代码上更加简单和整齐。有兴趣的同学可以利用iverilog+gtkwave来仿真测试下。
10、其他的话
至此,关于cpu和mcu设计的部分就结束了,有兴趣的同学可以继续拓展。总之,还是要多练习、多实践,才能加深印象。
转载:https://blog.csdn.net/feixiaoxing/article/details/128161268