小言_互联网的博客

cpu设计和实现(总结篇)

401人阅读  评论(0)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱: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的接口


  
  1. // tinyriscv soc顶层模块
  2. module tinyriscv_soc_top(
  3. input wire clk,
  4. input wire rst,
  5. output reg over, // 测试是否完成信号
  6. output reg succ, // 测试是否成功信号
  7. output wire halted_ind, // jtag是否已经halt住CPU信号
  8. input wire uart_debug_pin, // 串口下载使能引脚
  9. output wire uart_tx_pin, // UART发送引脚
  10. input wire uart_rx_pin, // UART接收引脚
  11. inout wire[ 1: 0] gpio, // GPIO引脚
  12. input wire jtag_TCK, // JTAG TCK引脚
  13. input wire jtag_TMS, // JTAG TMS引脚
  14. input wire jtag_TDI, // JTAG TDI引脚
  15. output wire jtag_TDO, // JTAG TDO引脚
  16. input wire spi_miso, // SPI MISO引脚
  17. output wire spi_mosi, // SPI MOSI引脚
  18. output wire spi_ss, // SPI SS引脚
  19. output wire spi_clk // SPI CLK引脚
  20. );

        mcu的接口就类似于大家正常看到的那些chip的接口一样。其中over、succ、halted_ind很明显是为了调试用的。clk是时钟,rst是复位,uart_debug_pin是下载,uart_tx_pin&uart_rx_pin是串口,gpio是通用口,jtag是调试口,spi是协议口。之前谈到的rom、ram很明显用片上资源实现了。timer也是内部资源实现,对外扇出的就是以上这些端口。

4、总线


  
  1. /*
  2. Copyright 2020 Blue Liang, liangkangnan@163.com
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. ` include "defines.v"
  14. // RIB总线模块
  15. module rib(
  16. input wire clk,
  17. input wire rst,
  18. // master 0 interface
  19. input wire[`MemAddrBus] m0_addr_i, // 主设备 0读、写地址
  20. input wire[`MemBus] m0_data_i, // 主设备 0写数据
  21. output reg[`MemBus] m0_data_o, // 主设备 0读取到的数据
  22. input wire m0_req_i, // 主设备 0访问请求标志
  23. input wire m0_we_i, // 主设备 0写标志
  24. // master 1 interface
  25. input wire[`MemAddrBus] m1_addr_i, // 主设备 1读、写地址
  26. input wire[`MemBus] m1_data_i, // 主设备 1写数据
  27. output reg[`MemBus] m1_data_o, // 主设备 1读取到的数据
  28. input wire m1_req_i, // 主设备 1访问请求标志
  29. input wire m1_we_i, // 主设备 1写标志
  30. // master 2 interface
  31. input wire[`MemAddrBus] m2_addr_i, // 主设备 2读、写地址
  32. input wire[`MemBus] m2_data_i, // 主设备 2写数据
  33. output reg[`MemBus] m2_data_o, // 主设备 2读取到的数据
  34. input wire m2_req_i, // 主设备 2访问请求标志
  35. input wire m2_we_i, // 主设备 2写标志
  36. // master 3 interface
  37. input wire[`MemAddrBus] m3_addr_i, // 主设备 3读、写地址
  38. input wire[`MemBus] m3_data_i, // 主设备 3写数据
  39. output reg[`MemBus] m3_data_o, // 主设备 3读取到的数据
  40. input wire m3_req_i, // 主设备 3访问请求标志
  41. input wire m3_we_i, // 主设备 3写标志
  42. // slave 0 interface
  43. output reg[`MemAddrBus] s0_addr_o, // 从设备 0读、写地址
  44. output reg[`MemBus] s0_data_o, // 从设备 0写数据
  45. input wire[`MemBus] s0_data_i, // 从设备 0读取到的数据
  46. output reg s0_we_o, // 从设备 0写标志
  47. // slave 1 interface
  48. output reg[`MemAddrBus] s1_addr_o, // 从设备 1读、写地址
  49. output reg[`MemBus] s1_data_o, // 从设备 1写数据
  50. input wire[`MemBus] s1_data_i, // 从设备 1读取到的数据
  51. output reg s1_we_o, // 从设备 1写标志
  52. // slave 2 interface
  53. output reg[`MemAddrBus] s2_addr_o, // 从设备 2读、写地址
  54. output reg[`MemBus] s2_data_o, // 从设备 2写数据
  55. input wire[`MemBus] s2_data_i, // 从设备 2读取到的数据
  56. output reg s2_we_o, // 从设备 2写标志
  57. // slave 3 interface
  58. output reg[`MemAddrBus] s3_addr_o, // 从设备 3读、写地址
  59. output reg[`MemBus] s3_data_o, // 从设备 3写数据
  60. input wire[`MemBus] s3_data_i, // 从设备 3读取到的数据
  61. output reg s3_we_o, // 从设备 3写标志
  62. // slave 4 interface
  63. output reg[`MemAddrBus] s4_addr_o, // 从设备 4读、写地址
  64. output reg[`MemBus] s4_data_o, // 从设备 4写数据
  65. input wire[`MemBus] s4_data_i, // 从设备 4读取到的数据
  66. output reg s4_we_o, // 从设备 4写标志
  67. // slave 5 interface
  68. output reg[`MemAddrBus] s5_addr_o, // 从设备 5读、写地址
  69. output reg[`MemBus] s5_data_o, // 从设备 5写数据
  70. input wire[`MemBus] s5_data_i, // 从设备 5读取到的数据
  71. output reg s5_we_o, // 从设备 5写标志
  72. output reg hold_flag_o // 暂停流水线标志
  73. );
  74. // 访问地址的最高4位决定要访问的是哪一个从设备
  75. // 因此最多支持16个从设备
  76. parameter [ 3: 0]slave_0 = 4 'b0000;
  77. parameter [3:0]slave_1 = 4'b0001;
  78. parameter [ 3: 0]slave_2 = 4 'b0010;
  79. parameter [3:0]slave_3 = 4'b0011;
  80. parameter [ 3: 0]slave_4 = 4 'b0100;
  81. parameter [3:0]slave_5 = 4'b0101;
  82. parameter [ 1: 0]grant0 = 2 'h0;
  83. parameter [1:0]grant1 = 2'h1;
  84. parameter [ 1: 0]grant2 = 2 'h2;
  85. parameter [1:0]grant3 = 2'h3;
  86. wire[ 3: 0] req;
  87. reg[ 1: 0] grant;
  88. // 主设备请求信号
  89. assign req = {m3_req_i, m2_req_i, m1_req_i, m0_req_i};
  90. // 仲裁逻辑
  91. // 固定优先级仲裁机制
  92. // 优先级由高到低:主设备3,主设备0,主设备2,主设备1
  93. always @ (*) begin
  94. if (req[ 3]) begin
  95. grant = grant3;
  96. hold_flag_o = `HoldEnable;
  97. end else if (req[ 0]) begin
  98. grant = grant0;
  99. hold_flag_o = `HoldEnable;
  100. end else if (req[ 2]) begin
  101. grant = grant2;
  102. hold_flag_o = `HoldEnable;
  103. end else begin
  104. grant = grant1;
  105. hold_flag_o = `HoldDisable;
  106. end
  107. end
  108. // 根据仲裁结果,选择(访问)对应的从设备
  109. always @ (*) begin
  110. m0_data_o = `ZeroWord;
  111. m1_data_o = `INST_NOP;
  112. m2_data_o = `ZeroWord;
  113. m3_data_o = `ZeroWord;
  114. s0_addr_o = `ZeroWord;
  115. s1_addr_o = `ZeroWord;
  116. s2_addr_o = `ZeroWord;
  117. s3_addr_o = `ZeroWord;
  118. s4_addr_o = `ZeroWord;
  119. s5_addr_o = `ZeroWord;
  120. s0_data_o = `ZeroWord;
  121. s1_data_o = `ZeroWord;
  122. s2_data_o = `ZeroWord;
  123. s3_data_o = `ZeroWord;
  124. s4_data_o = `ZeroWord;
  125. s5_data_o = `ZeroWord;
  126. s0_we_o = `WriteDisable;
  127. s1_we_o = `WriteDisable;
  128. s2_we_o = `WriteDisable;
  129. s3_we_o = `WriteDisable;
  130. s4_we_o = `WriteDisable;
  131. s5_we_o = `WriteDisable;
  132. case (grant)
  133. grant0: begin
  134. case (m0_addr_i[ 31: 28])
  135. slave_0: begin
  136. s0_we_o = m0_we_i;
  137. s0_addr_o = {{ 4 'h0}, {m0_addr_i[27:0]}};
  138. s0_data_o = m0_data_i;
  139. m0_data_o = s0_data_i;
  140. end
  141. slave_1: begin
  142. s1_we_o = m0_we_i;
  143. s1_addr_o = {{4'h0}, {m0_addr_i[ 27: 0]}};
  144. s1_data_o = m0_data_i;
  145. m0_data_o = s1_data_i;
  146. end
  147. slave_2: begin
  148. s2_we_o = m0_we_i;
  149. s2_addr_o = {{ 4 'h0}, {m0_addr_i[27:0]}};
  150. s2_data_o = m0_data_i;
  151. m0_data_o = s2_data_i;
  152. end
  153. slave_3: begin
  154. s3_we_o = m0_we_i;
  155. s3_addr_o = {{4'h0}, {m0_addr_i[ 27: 0]}};
  156. s3_data_o = m0_data_i;
  157. m0_data_o = s3_data_i;
  158. end
  159. slave_4: begin
  160. s4_we_o = m0_we_i;
  161. s4_addr_o = {{ 4 'h0}, {m0_addr_i[27:0]}};
  162. s4_data_o = m0_data_i;
  163. m0_data_o = s4_data_i;
  164. end
  165. slave_5: begin
  166. s5_we_o = m0_we_i;
  167. s5_addr_o = {{4'h0}, {m0_addr_i[ 27: 0]}};
  168. s5_data_o = m0_data_i;
  169. m0_data_o = s5_data_i;
  170. end
  171. default: begin
  172. end
  173. endcase
  174. end
  175. grant1: begin
  176. case (m1_addr_i[ 31: 28])
  177. slave_0: begin
  178. s0_we_o = m1_we_i;
  179. s0_addr_o = {{ 4 'h0}, {m1_addr_i[27:0]}};
  180. s0_data_o = m1_data_i;
  181. m1_data_o = s0_data_i;
  182. end
  183. slave_1: begin
  184. s1_we_o = m1_we_i;
  185. s1_addr_o = {{4'h0}, {m1_addr_i[ 27: 0]}};
  186. s1_data_o = m1_data_i;
  187. m1_data_o = s1_data_i;
  188. end
  189. slave_2: begin
  190. s2_we_o = m1_we_i;
  191. s2_addr_o = {{ 4 'h0}, {m1_addr_i[27:0]}};
  192. s2_data_o = m1_data_i;
  193. m1_data_o = s2_data_i;
  194. end
  195. slave_3: begin
  196. s3_we_o = m1_we_i;
  197. s3_addr_o = {{4'h0}, {m1_addr_i[ 27: 0]}};
  198. s3_data_o = m1_data_i;
  199. m1_data_o = s3_data_i;
  200. end
  201. slave_4: begin
  202. s4_we_o = m1_we_i;
  203. s4_addr_o = {{ 4 'h0}, {m1_addr_i[27:0]}};
  204. s4_data_o = m1_data_i;
  205. m1_data_o = s4_data_i;
  206. end
  207. slave_5: begin
  208. s5_we_o = m1_we_i;
  209. s5_addr_o = {{4'h0}, {m1_addr_i[ 27: 0]}};
  210. s5_data_o = m1_data_i;
  211. m1_data_o = s5_data_i;
  212. end
  213. default: begin
  214. end
  215. endcase
  216. end
  217. grant2: begin
  218. case (m2_addr_i[ 31: 28])
  219. slave_0: begin
  220. s0_we_o = m2_we_i;
  221. s0_addr_o = {{ 4 'h0}, {m2_addr_i[27:0]}};
  222. s0_data_o = m2_data_i;
  223. m2_data_o = s0_data_i;
  224. end
  225. slave_1: begin
  226. s1_we_o = m2_we_i;
  227. s1_addr_o = {{4'h0}, {m2_addr_i[ 27: 0]}};
  228. s1_data_o = m2_data_i;
  229. m2_data_o = s1_data_i;
  230. end
  231. slave_2: begin
  232. s2_we_o = m2_we_i;
  233. s2_addr_o = {{ 4 'h0}, {m2_addr_i[27:0]}};
  234. s2_data_o = m2_data_i;
  235. m2_data_o = s2_data_i;
  236. end
  237. slave_3: begin
  238. s3_we_o = m2_we_i;
  239. s3_addr_o = {{4'h0}, {m2_addr_i[ 27: 0]}};
  240. s3_data_o = m2_data_i;
  241. m2_data_o = s3_data_i;
  242. end
  243. slave_4: begin
  244. s4_we_o = m2_we_i;
  245. s4_addr_o = {{ 4 'h0}, {m2_addr_i[27:0]}};
  246. s4_data_o = m2_data_i;
  247. m2_data_o = s4_data_i;
  248. end
  249. slave_5: begin
  250. s5_we_o = m2_we_i;
  251. s5_addr_o = {{4'h0}, {m2_addr_i[ 27: 0]}};
  252. s5_data_o = m2_data_i;
  253. m2_data_o = s5_data_i;
  254. end
  255. default: begin
  256. end
  257. endcase
  258. end
  259. grant3: begin
  260. case (m3_addr_i[ 31: 28])
  261. slave_0: begin
  262. s0_we_o = m3_we_i;
  263. s0_addr_o = {{ 4 'h0}, {m3_addr_i[27:0]}};
  264. s0_data_o = m3_data_i;
  265. m3_data_o = s0_data_i;
  266. end
  267. slave_1: begin
  268. s1_we_o = m3_we_i;
  269. s1_addr_o = {{4'h0}, {m3_addr_i[ 27: 0]}};
  270. s1_data_o = m3_data_i;
  271. m3_data_o = s1_data_i;
  272. end
  273. slave_2: begin
  274. s2_we_o = m3_we_i;
  275. s2_addr_o = {{ 4 'h0}, {m3_addr_i[27:0]}};
  276. s2_data_o = m3_data_i;
  277. m3_data_o = s2_data_i;
  278. end
  279. slave_3: begin
  280. s3_we_o = m3_we_i;
  281. s3_addr_o = {{4'h0}, {m3_addr_i[ 27: 0]}};
  282. s3_data_o = m3_data_i;
  283. m3_data_o = s3_data_i;
  284. end
  285. slave_4: begin
  286. s4_we_o = m3_we_i;
  287. s4_addr_o = {{ 4 'h0}, {m3_addr_i[27:0]}};
  288. s4_data_o = m3_data_i;
  289. m3_data_o = s4_data_i;
  290. end
  291. slave_5: begin
  292. s5_we_o = m3_we_i;
  293. s5_addr_o = {{4'h0}, {m3_addr_i[ 27: 0]}};
  294. s5_data_o = m3_data_i;
  295. m3_data_o = s5_data_i;
  296. end
  297. default: begin
  298. end
  299. endcase
  300. end
  301. default: begin
  302. end
  303. endcase
  304. end
  305. endmodule

        这个总线为什么叫rib,不是很清楚。不过从代码上看,内容非常简单,就是将命令和数据从master传递给slave。并且根据grant的逻辑,一次只能有一个master参与操作。等选定了master之后, 再根据设备地址的[31:28]位,决定把这个请求发给哪一个slave设备。

5、jtag代码


  
  1. module jtag_top #(
  2. parameter DMI_ADDR_BITS = 6,
  3. parameter DMI_DATA_BITS = 32,
  4. parameter DMI_OP_BITS = 2)(
  5. input wire clk,
  6. input wire jtag_rst_n,
  7. input wire jtag_pin_TCK,
  8. input wire jtag_pin_TMS,
  9. input wire jtag_pin_TDI,
  10. output wire jtag_pin_TDO,
  11. output wire reg_we_o,
  12. output wire[ 4: 0] reg_addr_o,
  13. output wire[ 31: 0] reg_wdata_o,
  14. input wire[ 31: 0] reg_rdata_i,
  15. output wire mem_we_o,
  16. output wire[ 31: 0] mem_addr_o,
  17. output wire[ 31: 0] mem_wdata_o,
  18. input wire[ 31: 0] mem_rdata_i,
  19. output wire op_req_o,
  20. output wire halt_req_o,
  21. output wire reset_req_o
  22. );

        很多做嵌入式的同学虽然不知道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模块


  
  1. module uart_debug(
  2. input wire clk, // 时钟信号
  3. input wire rst, // 复位信号
  4. input wire debug_en_i, // 模块使能信号
  5. output wire req_o,
  6. output reg mem_we_o,
  7. output reg[ 31: 0] mem_addr_o,
  8. output reg[ 31: 0] mem_wdata_o,
  9. input wire[ 31: 0] mem_rdata_i
  10. );

        这个download模块比较特殊,主要就是为了mcu可以正常的把版本烧入到flash里面去。大家可以想一下,自己用的mcu里面,是不是有的芯片也添加了类似这样的功能。

7、简单的一个ram slave代码


  
  1. module ram(
  2. input wire clk,
  3. input wire rst,
  4. input wire we_i, // write enable
  5. input wire[`MemAddrBus] addr_i, // addr
  6. input wire[`MemBus] data_i,
  7. output reg[`MemBus] data_o // read data
  8. );
  9. reg[`MemBus] _ram[ 0:`MemNum - 1];
  10. always @ (posedge clk) begin
  11. if (we_i == `WriteEnable) begin
  12. _ram[addr_i[ 31: 2]] <= data_i;
  13. end
  14. end
  15. always @ (*) begin
  16. if (rst == `RstEnable) begin
  17. data_o = `ZeroWord;
  18. end else begin
  19. data_o = _ram[addr_i[ 31: 2]];
  20. end
  21. end
  22. endmodule

        这是一份slave代码,主要是负责数据的读取和写入。从代码上看,内容也简单,如果是读取,那么组合逻辑直接给出;如果是写入,那么需要等时钟上升沿的时候才写入。

8、公用的功能模块gen_buff.v


  
  1. module gen_pipe_dff #(
  2. parameter DW = 32)(
  3. input wire clk,
  4. input wire rst,
  5. input wire hold_en,
  6. input wire[DW- 1: 0] def_val,
  7. input wire[DW- 1: 0] din,
  8. output wire[DW- 1: 0] qout
  9. );
  10. reg[DW- 1: 0] qout_r;
  11. always @ (posedge clk) begin
  12. if (!rst | hold_en) begin
  13. qout_r <= def_val;
  14. end else begin
  15. qout_r <= din;
  16. end
  17. end
  18. assign qout = qout_r;
  19. 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
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场