10

极术小能手 · 2022年03月13日

【课程实验 LAB2】实现你的首个SoC:搭建Keil工程和Modelsim 仿真

实验前请点击获取配套资料

关于LAB2

  • 在本实验中, 你将动手实现一个基于 CortexM0 的 SoC 基础软硬件系统, 然后通过 Modelsim 对系统功能进行仿真, 仿真通过后, 使用 Vivado 搭建系统并将其下载配置到 FPGA 板卡中, 然后使用 Keil 进行实际上板调试.
  • 本实验旨在加深大家对基于 CortexM0 的 SoC 的理解, 熟悉各个软件的基本使用方法, 所以本实验的软件部分和硬件部分都非常简单.
  • 在进行本实验之前, 你应该已经理解了硬件设计的涵义, 掌握了四种 verilog 关键字语法: always, assign, if, case, 而且掌握了模块例化的基本方式. 另外, 你还应当对 AHB 总线协议有一定的了解.

搭建Keil工程

注意, 本实验没有实现数据存储器, 所以我们暂时不能使用 Load/Store 指令. 在本实验中, 我们仍然使用汇编语言实现一个简单的循环计数器.

首先, 按照在 LAB1 中的方式, 在 "/Task2/keil" 文件夹中创建名为 code 的 Keil 工程, 由于涉及到实际上板调试, 这里的 Target 设置方式与之前有些不同:

image.png
仿真/调试设置
其中 "code.ini" 文件位于 "Task2/keil/code.ini", 直接将其添加到图中位置即可.

接着, 我们点击进入 Debugger 的 settings 窗口, 选择上方的 Flash Download. 设置方式如下:

image.png
Flash 下载设置
点击 "OK" 后, 进入 Utilities 设置界面, 取消勾选 Update Target before Debugging.

image.png
Utilities 设置

注意

  • 上述几步设置与上板调试有关, 与接下来的 modelsim 仿真无关, 我们之后再回来讨论这几步的作用.
  • 除上述对 Debug 和 Utilities 的设置, 不要忘了其他部分 (Device, Target, Output 等) 的设置哦, 这些部分的设置方法与 LAB1 "搭建 Keil 环境" 一节中相同.
  • 将 "/Task2/keil/startup_CMSDK_CM0.s" 文件添加至工程中, 进行编译. 这里的启动文件中的程序与 LAB1 相同, 仍然是一个循环计数器.

注意得到的 "/Task2/keil/code.hex" 文件, 它代表了程序的可执行文件, 是系统的软件组成部分, 为了将它与我们的硬件结合起来, 实现整个系统的 modelsim 仿真, 我们需要修改 "/Task2/rtl/Block_RAM.v" 文件, 将 RAM 初始化文件路径设为上述 "code.hex" 的路径. 注意这里必须使用绝对路径:

initial begin
    $readmemh("Your/absolute/path/to/code.hex",mem);
end

这样, 我们就在仿真的层面上, 将程序的软件代码 "下载" 到了硬件的 "内存" 中.

Modelsim 仿真

打开 Modelsim 软件, 点击左上角 File 菜单, 再点击 New -> Project 创建一个 Modelsim 工程, 命名为 "code", 工程地址选为 "/Task2/modelsim/" 文件夹.

image.png
新建 Modelsim 工程
点击 OK 后, 选择 Add Existing File, 添加源文件.

image.png
添加源文件
由于我们的软件程序已经被 "下载" 到了 RAM 中, 这里只需要把 "/Task2/rtl/" 下的所有的 RTL 文件添加进来, 另外, 还需要把 Testbench 文件 "/Task2/modelsim/CortexM0_SoC_vlg_tst.v" 中的添加进来.

什么是 Testbench ?
Testbench 是数字系统仿真的测试用例, 以 verilog module 的形式存在, Testbench 用于测试激励的产生和响应信号的收集处理, 虽然不属于设计的组成部分, 但必须是仿真时的顶层文件.

关于本实验中的 Testbench
打开 "/Task2/modelsim/CortexM0_SoC_vlg_tst.v" 可以看到:

timescale 1 ps/ 1 ps
module CortexM0_SoC_vlg_tst();

reg clk;
reg RSTn;
reg TXD;

CortexM0_SoC i1 (
    .clk(clk),
    .RSTn(RSTn)
);

initial begin                                                  
    clk = 0; RSTn=0;
    #100 RSTn=1;
end  

always begin #10 clk = ~clk; end       

endmodule

可见, Testbench 中只是对整个 SoC 做了简单的例化, 并提供了时钟信号和初始复位.

文件添加完毕后, 点击编译按钮, 对所有源文件进行编译查错. 通过编译的文件后的 "?" 都变成了 "√":

image.png
编译之前
image.png
编译通过
正如之前所说, Testbench 是仿真时的顶层文件, 所以仿真的对象应该是 Testbench 文件. 我们选择左侧导航栏下方的 Library, 展开 work, 右键单击 CortexM0_SoC_vlg_tst, 选择 Simulate without Optimization, 开始仿真.

image.png
选择 Library -> work -> CortexM0_SoC_vlg_tst
image.png
右键 CortexM0_SoC_vlg_tst 选择 Simulate without Optimization

Instance, Object 和 Wave
仿真时三个非常重要的窗口:

  • Instance: 以 Testbench 为顶层模块, 层次化地展示所有被例化的模块. 注意, 展示的是实例名而不是模块名.
  • Object: 可以理解为展示了某个模块中的所有信号.
  • Wave: 波形窗口, 展示被添加的信号及其波形.

在 Modelsim 中, 如果不小心关闭了这些窗口, 可以在 View 菜单栏中重新打开.

下一步, 我们要把待观察的信号添加到 Wave 窗口. 那么我们应该添加哪些信号呢? 为了观察程序的运行情况, 我们需要观察处理器中寄存器的值, 而 Cortex-M0 中的调试恰好有捕捉寄存器值的功能, 所以我们只需要把 Cortex-M0 中的对应信号添加进来即可.

注意, 在 CortexM0_SoC_vlg_tst.v 中, SoC 的实例名为 i1, 而在 SoC 中, Cortex-M0 核的实例名为 u_logic. 我们在 Instance 窗口中展开 CortexM0_SoC_vlg_tst, 依次选择 i1 -> u_logic.

双击 u_logic, 该模块中的所有信号就呈现在了 Object 窗口中. 在 Object 窗口中选中调试信号 vis_r0_o, vis_r1_o, vis_pc_o; 另外, 为了观察 Cortex-M0 数据接口的工作过程, 我们继续选中 HADDR, HSIZE, HTRANS, HRDATA 这几个 AHB 信号, 右键点击 Add Wave, 这些信号就被添加到了 Wave 窗口中.

image.png
选择待观察的信号
在 Wave 窗口中选中所有信号, 右键点击 Radix -> Hexadecimal, 将其改为 16 进制显示, 以便观察.

image.png
Wave 窗口中的信号
保存波形格式
在 Modelsim 中, 所有对波形格式 (如显示格式, 颜色, 信号分组) 的修改可以被保存为一个 do 脚本文件, 以便下次进入仿真后加载执行.

保存方式为: 点击 Wave 窗口左上角 File 菜单, 选择 Save Format:

image.png
选择波形格式文件保存路径
加载方式为: 点击 Wave 窗口左上角 File 菜单, 选择 Load -> Macro File.
在运行仿真前, 首先要设置仿真时间, 这里设置仿真时间为 100ns, 然后点击旁边按钮正式运行仿真.

image.png
设置仿真时间并运行仿真
仿真运行停止后, 点击 Wave 窗口菜单栏中的17将波形缩放到满屏, 然后通过滚动条和 Ctrl + 鼠标滚轮将波形缩放到合适比例.

image.png
仿真波形
下面根据程序功能, 分析仿真波形:

  • 寄存器

vis_r1_o 在 0-4 范围内循环计数, 与程序功能相符.

vis_pc_o 在 0x25-0x29 之间循环, 为什么? 这时我们就需要进入之前生成的 "/Task2/keil/code.txt" 文件一探究竟了, 在 "code.txt" 的 .text 中有这样一段:

    start
        0x00000048:    2104        .!      MOVS     r1,#4
        0x0000004a:    2000        .       MOVS     r0,#0
        0x0000004c:    1c40        @.      ADDS     r0,r0,#1
        0x0000004e:    4288        .B      CMP      r0,r1
        0x00000050:    d0fb        ..      BEQ      0x4a ; start + 2
        0x00000052:    d1fb        ..      BNE      0x4c ; start + 4

这表明, 程序运行时, 取指地址在 0x0000004a-0x00000052 之间循环, 不难发现, vis_pc_o 恰好是取指地址去除 LSB 后的结果.

  • AHB 信号

Cortex-M0 属于冯诺依曼架构, 指令和数据都要通过同一个 AHB 接口访问. 而本实验中未涉及对数据的读写, 所以这里的 AHB 接口仅用于指令的读取. 另外, 由于 Cortex-M0 内部未设有 Cache 来缓存指令和数据, 所以对于循环程序而言, 也需要不断去主存中读取同一段指令. 所以我们会看到 HADDR 不断在 0x0000004a-0x00000052 之间循环, 对应的 HTRANS 在传输时变为 1, HSIZE 在传输时变为 2, 表示 32 位数据传输, 相应的指令数据在下一拍表现在 HRDATA 总线上.

若要退出仿真, 点击主窗口菜单中的 Simulate -> End Simulation.

image.png
退出仿真

命令行操作 - 从小白到工程师
在工程中, 使用命令行或脚本操作软件可以为我们节省大量的操作时间, 提升我们的工作效率. 熟练的命令行技能是一个优秀工程师必备的专业素质.

如果你厌倦了繁琐的图形界面操作, 请跟我一起学习使用命令行操作 Modelsim 吧.

Modelsim 采用 TCL 命令, 你可以在命令行窗口 (Transcript) 中执行单条命令, 也可以使用 do 脚本来执行多条命令. 对于本仿真实验, 这里给出了一种等效的 do 脚本的编写方法, 有兴趣的同学可以自行尝试(注意你的文件路径):

编写 "run.do" 脚本文件:

vlib work 
vlog "./CortexM0_SoC_vlg_tst.v" 
vlog "./rtl/*"
vsim -voptargs=+acc -c work.CortexM0_SoC_vlg_tst
add wave "CortexM0_SoC_vlg_tst/i1/u_logic/*"
run 100ns

在 Modelsim Transcript 窗口中执行命令:

do run.do

END

文章来源:

推荐内容

更多内容请关注微处理器系统结构与嵌入式系统设计专栏
推荐阅读
关注数
113
内容数
20
电子科技大学示范性微电子学院开设的「微处理器系统结构与嵌入式系统设计」课程配套实验,原链接:[链接]
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息