普通视图

发现新文章,点击刷新页面。
昨天以前strongwong's Blog

浅谈 RISC-V 软件开发生态之 IDE

作者 strongwong
2021年2月12日 21:56

0x00 前言

今天简单谈一些关于 RISC-V 开发的软件生态相关,主要是关于 RISC-V 的开发 IDE,就是集成开发环境。集成开发环境(IDE,Integrated Development Environment )是用于提供程序开发环境的应用程序,一般包括代码编辑器、编译器、调试器和图形用户界面等工具。集成了代码编写功能、分析功能、编译功能、调试功能等一体化的软件开发服务套(组)。

目前来看,RISC-V 的硬件生态已经在蓬勃发展,而 RISC-V 相关的软件生态还在日趋完善的过程中,这里就来浅谈一点我的个人认识,如有不对,请批评指正。

目前,RISC-V 的软件开发 IDE ,主要是有以下几种解决方案。

0x01 eclipse —— 著名开源 IDE

全开源,需要自行集成 RISC-V Toolchains + eclipse + OpenOCD 来搭建开发环境。

优点:

  1. 全开源,自由,免费 的 IDE
  2. 有众多公司厂商,组织,支持维护
  3. 可扩展的插件功能

缺点:

  1. 庞大臃肿,而且需要 java 运行环境。现在的版本在安装过程中会自动安装一个 jre 的运行环境。但是目前国内用户如果自行安装会下载很慢,使用代理相对快很多。如果不使用安装版本,也可以使用 zip 包解压的版本
  2. 集成众多插件,插件数量多了之后会拖慢系统
  3. 需要自行集成编译器及仿真器,打包发布给用户使用

0x02 IAR for RISC-V—— 老牌商业 IDE

IAR for RISC-V 版本目前已经正在和部分芯片厂商进行合作适配、授权支持。主要包括:SiFive、Andes Technology、Nuclei(芯来科技)、CloudBEAR、Syntacore、C-SKY(平头哥)、GigaDevice(兆易创新)、MicroChip。

优点:

  1. IAR 有非常优秀的编译器,针对代码的大小和速度有很好的优化;从 coremark 跑分排行榜上的 ARM 芯片来看,相对来说 IAR 的代码性能更高一些,当然各版本之间也会存在差异。但目前 IAR for RISC-V 的版本还未开放下载,暂时还没有相关测评。
  2. 有很优秀的 Trace 工具进行调试

coremark

IAR for RISC-V

缺点:

  1. 纯商业 IDE,使用需要授权,相对来说用户不易获得授权使用,尤其是新版本更新了 License 授权方案
  2. RISC-V 芯片需要通过其 i-jet 来调试

0x03 Embedded Studio for RISC-V —— SEGGER 老牌嵌入式开发工具供应商

Embedded Studio 目前正在适配支持 Andes Technology、Nuclei(芯来科技)、GigaDevice(兆易创新) 、SiFive、Syntacore、Western Digital 的 RISC-V 芯片开发。

优点:

  1. 个人用户免费,支持全功能, 跨平台支持含 Windows, macOS 和 Linux 版本。
  2. 优于 IAR 的编辑器
  3. 启动速度快,大工程启动速度相对较快
  4. 调试工具丰富,因为是传统嵌入式工具链厂商,有 J-link 系列工具支持
  5. 支持 RISC-V 内核

缺点:

  1. 目前只支持 J-link 进行 debug
  2. 设置选项较为烦锁

0x04 VS code —— 开发新秀

VS code 在我看来就是一款优秀的开源跨平台代码编辑器,但由于其内置了标准 Debugger Adaptor Protocol,经过各路大神,一些组织,部分企业公司结合各自的需求,开发了各具特色的 Debug 插件。于是乎 VS Code 俨然变成了一款极具竞争力的 IDE,成功跨界抢各类 IDE 的市场,因为其灵活小巧,迅速成为了各路开发者的新宠。但也由于其开发调试功能不是本身内置,也给不同的开发需求带来了一定的门槛,需要开发者自行进行一些开发环境配置,对于新手小白不是特别友好。但还是有很多愿意折腾的大佬。

优点:

  1. 软件全开源免费,体积小,启动快,界面新颖,更新快,新兴的优秀编辑器代表
  2. 开源众多的插件
  3. 可跨平台使用

缺点:

  1. 其本身就是一个,优秀的开源编辑器,如果要进行 MCU 开发,需要开发插件,或者使用相应的工具链来自行配置,对用户不是很友好
  2. 目前没有支持 RISC-V 的通用插件
  3. 通过体验 cortex debug 、esp-idf、platformIO 等开发插件,体验也并不是很友好;但也是跟插件开发者的能力、需求和习惯相关
  4. Cortex Debug 插件,代码在 bootrom 里运行时,反汇编窗口无法显示当前的 bootrom 代码,除非手动反汇编。它执行显示当前 elf 范围内的文件,regs 窗口没法设置显示格式;这些可能是 Cortex Debug 插件的局限。

0x05 KendryteIDE —— 基于 VS code 包装的 IDE 方案

KendryteIDE 是嘉楠勘智,基于开源的 VS code 编辑器,自己定制的 RISC-V 芯片 IDE 解决方案,整体风格继承 VS code。

优点:

  1. 基于开源 VS code,二次开发,完全自主可控,轻量级
  2. 继承了 VS code 的优秀编辑器,及其优点

缺点:

  1. 目前不可以直接使用其 IDE,来进行其他的 RISC-V 芯片调试
  2. 需要重新适配自己的 MCU 来做开发,开发工作量和时间周期是不确定的
  3. 定制程度取决于开发人员的能力

0x05 总结

可能正是由于 RISC-V 硬件的自由更改的属性,各家厂商都可以有自己独特的 RISC-V 架构,所以这也导致了 RISC-V 的编译工具链会有各厂商自己定制,不能像 ARM 那样各个厂商都使用 MDK,只需要做一个 SDK or Pack 包集成到 MDK 中即可。于是乎,我们就看到了市面上的各大 RISC-V 芯片或 IP 公司,都在做自己的 IDE 用自己的工具链。
那么为什么各家都在做各自的工具链呢,我认为还是没有形成类似于 ARM 的 CMSIS 这样的嵌入式软件接口标准,来统一管理底层软件接口,于是乎就变成了各自玩各自的,没有统一。这也是导致 RISC-V 软件生态薄弱,碎片化的一个重要因素。

但我相信,未来应该也会出现类似于 CMSIS 的标准,来完成一统大业的工作

于是乎现在就是八仙过海,各显神通的局面。但大部分的 RISC-V 厂商的开发 IDE,还是基于开源的 eclipse + gcc toolchains + openocd 的方案来开发和调试芯片产品,相对来说这可能是比较快和相对成熟的方案。

当然,我个人猜测,像 IAR、SEGGER 这种纯商业的第三方 IDE 、嵌入式工具供应商,也希望能够适配市面上的各型号 MCU 开发,稳固自己的工具链生态,所以 SEGGER 率先推出了支持 RISC-V 开发的 IDE,但可能也正是由于 RISC-V 的灵活性,致使全面支持 RISC-V 架构的 IAR 版本还迟迟没有正式推出。

个人认为,如果一个 RISC-V 芯片厂商需要尽快推出自己的 IDE ,那么可能使用 eclipse + gcc toolchains + openocd 的方案会比较快;如果时间不急的话,在目前商业 IDE 还不成熟的情况下,可能自行研发是比较好的选择,对其自己的芯片开发的适配程度也会更高;那么自行研发也有两个方向,一个就是基于 VS code 编辑器做二次开发,参考嘉楠勘智方案;另一个就是从头开发一款自己的 IDE,但由于自己定制调试器也是一个时间周期比较长的过程,所以大概率底层还是 gcc + openocd。当然了,这里也还有未列出的阿里平头哥发布的 剑池 CDK 开发环境,他们有中天微时期的基础,所以他们就是自己完全定制的 IDE,并且有自己的调试器 ck-link,是解决方案比较成熟的厂商了。

eclipse VS code IAR Embedded Studio
版权 开源,免费 开源,免费 商业授权 个人用户免费,合作厂商用户免费
是否支持 RISC-V 编辑器本身不支持,可定制 编辑器本身不支持,可定制 支持,但需要厂商和 IAR 合作开发 支持,需要厂商合作开发
是否插件拓展 有插件 有插件,没有 RISC-V 通用插件,需要厂商 or 第三方 or 开源社区自定义 不支持 不支持
是否可调试 使用开源 openocd 调试 使用开源 openocd 调试 支持 RISC-V 的版本需要使用 IAR 官方 I-jet 仿真器 支持 RISC-V,但仅支持 SEGGER 的 J-link 仿真器
快捷键调试 支持 各种调试插件使用方式不一致 支持 支持
汇编 Debug 支持 需要自定义插件支持 支持 支持
Mem 访问、读写 支持 需要自定义插件支持 支持 支持
寄存器访问、读写 支持 需要自定义插件支持 支持 支持
窗口中变量、数据的格式是否可更改 支持 不确定,可能需要自定义插件支持 支持 支持
用户界面 新版本有所改进,有颜色主题更改 新潮,有较多颜色主题,代码配色友好 新版本有有限主题更改 有限更改
…… …… …… …… ……

简单了解一下小米 vela

作者 strongwong
2021年1月12日 15:12

0x00 小米 vela

前段时间小米推出了 vela 物联网平台,vela 就是基于 NuttX 打造的物联网开发平台。

小米对 NuttX 的评价:

  1. NuttX 对 POSIX 标准有原生兼容:NuttX 是可商用化 RTOS 中唯一一个对 POSIX API 有原生支持的实时操作系统,所以很多 Linux 社区的开源软件可以很方便的移植到 NuttX 上,这样可以极大的简化开源软件移植,方便代码复用,降低学习曲线,其它 RTOS 需要适配层把 POSIX API 转成内部 API,而且通常只兼容一小部分的 POSIX 接口。
  2. 完成度高:NuttX 集成了文件系统、网络协议栈、图形库和驱动框架,减少开发成本。
  3. 模块化设计:所有组件甚至组件内部特性,都可以通过配置 Kconfig 来调整或关闭,可按需对系统进行裁剪,适用于不同产品形态。
  4. 代码精简:所有组件都是从头编码,专门对代码和数据做了优化设计。
  5. 轻量级:虽然 NuttX 实现了传统操作系统的所有功能,但是最终生成的代码尺寸还是可以很小(最小配置不到 32KB,最大配置不超过 256KB)。
  6. 和 Linux 系统的兼容性:因为 NuttX 整体设计、代码组织,编译过程和 Linux 非常接近,将会极大地降低 Android/Linux 开发者的迁移成本。
  7. 活跃开放的社区:很多厂商(比如小米、Sony,乐鑫、NXP 等)和开源爱好者都在积极回馈社区。

不难看出小米对 NuttX 的评价很不错。所以我也来赶紧学习一波,做了一个简单的了解,作为自己的技术储备。

0x01 安装配置

环境: Ubuntu20.04 系统、ARM GUN 2019q4 版本 gcc、openocd 0.10 版本

安装 ARM gcc 工具链,如果开发板是用其他的架构 cpu 请去安装其他架构下相应的版本工具链,我这里是 ARM 的。

1
2
3
4
5
sudo mkdir /opt/gcc
sudo chgrp -R users /opt/gcc
cd /opt/gcc
wget https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2019q4/gcc-arm-none-eabi-9-2019-q4-major-x86_64-linux.tar.bz2
tar xf gcc-arm-none-eabi-9-2019-q4-major-x86_64-linux.tar.bz2

配置工具链到系统环境变量

1
echo "export PATH=/opt/gcc/gcc-arm-none-eabi-9-2019-q4-major/bin:$PATH" >> ~/.bashrc

下载 nuttx 、apps、tools 三个 nuttx 系统源码及构建工具

1
2
3
4
5
mkdir nuttxWS
cd nuttxWS
git clone https://github.com/apache/incubator-nuttx.git nuttx
git clone https://github.com/apache/incubator-nuttx-apps apps
git clone https://bitbucket.org/nuttx/tools.git tools

安装 kconfig ,构建工具。注意:低于 ubuntu20 版本的安装要少许麻烦一些

1
$ apt install kconfig-frontends

查看 nuttx 支持的板卡

1
2
$ cd nuttxWS
$ ./tools/configure.sh -L | less

选择相应的板卡及其支持的应用程序进行配置

1
2
3
4
$ cd nuttxWS
$ ./tools/configure.sh -l <board-name>:<config-dir>
# for instance:
$ ./tools/configure.sh -l stm32f103-minimum:nsh

运行 menuconfig 进行自定义配置

1
$ make menuconfig

0x02 编译运行

配置完成后编译 NuttX 系统

1
$ make -j$(nproc)

编译完成后会在 nuttx 目录下生成一个 nuttx.bin 文件,接下来把他下载到板子是运行
首先安装 openocd

1
$ sudo apt install openocd

如果使用 apt 安装的 openocd 版本过低,就自己从源码安装一下 openocd 即可

Openocd 安装好之后,正确连接板卡和电脑,下载程序。
烧写程序之前可以先打开串口,当系统正常运行起来之后可以在串口中观察到 nsh> 命令行。注意,我这边是 stlink 的虚拟串口所以是 ACM0,你的如果不是虚拟串口,可能是 USB0 之类的。

1
$  picocom -b 115200 /dev/ttyACM0

下载程序需要注意正确选择你使用的下载器和板卡对应的 .cfg 文件

1
2
$ cd nuttxWS
$ openocd -f interface/stlink-v2-1.cfg -f target/stm32f1x.cfg -c 'init' -c 'program nuttx.bin 0x08000000 verify reset' -c 'shutdown'

如果程序正常运行了就可以在中观察到信息,输入 help 命令就可以查看支持的命令列表。如果没有看到信息,reset 一下板子。

修改配置编译一个 blinking 控制 led 的程序

回到 nuttx 目录下清除原来的配置,重新生成,在 menuconfig 中检查 led 相关配置是否设置成功。在 NSH library 中使能 printf 功能。

1
2
3
4
5
$ cd nuttxWS/nuttx
$ make distclean
$ ./tools/configure.sh -l stm32f103-minimum:userled
$ make menuconfig
$ make -j$(nproc)

编译完成后下载到板卡上,打开串口,按一下 reset 连接上板子就可以在串口看到输出的信息了。直接执行 leds ,可以看到板卡上的 led 已经正常运行起来了。

今天就先点个灯了解一下,其他的内容,等我翻翻 NuttX 源码在学习一下。

RISC-V 指令集特权架构

作者 strongwong
2021年1月11日 20:09

RISC-V 指令集手册

第二卷:特权架构

文档版本 20190608

前言

本文档介绍了 RISC-V 特权架构。此版本为 20190608-Priv-MSU-Ratified,表示已批准的 machine 和 supervisor 模块的指令集。

本文档包含以下版本的RISC-V ISA模块:

Module Version Status
Machine ISA 1.11 Ratified
Supervisor ISA 1.11 Ratified

本版本文档的变更包括:

  • 将 Machine 和 Supervisor 规范更改到 Ratified 状态。
  • 改进说明和注释。
  • 增加了有关虚拟机监控程序扩展的草案。
  • 规定哪些中断源保留供标准使用。
  • 分配了一些同步异常源供自定义使用。
  • 规定了同步异常的优先级顺序。
  • 添加了规范,即如果支持 A 扩展,则 xRET 指令可以清除 LR 保留,但不是必须的。
  • 无论 SUM 如何设置,虚拟内存系统不再允许超级用户(Supervisor)模式执行用户页面中的指令。
  • 强烈建议软件在全局范围内分配 ASID(地址空间 ID),以便将来的扩展可以全局化ASID,以提高性能和硬件灵活性。
  • SFENCE.VMA 语义已阐明。
  • 将 mstatus.MPP 字段设置为 WARL,而不是 WLRL
  • 将未使用的 xip 字段设置为 WPRI,而不是 WIRI
  • 将未使用的 misa 字段设置为 WLRL,而不是 WIRI
  • 将未使用的 pmpaddr 和 pmpcfg 字段设置为 WARL,而不是 WIRI
  • 要求系统中的所有 harts 都采用相同的 PTE-update 格式。
  • 纠正了编辑错误,该错误错误描述了在发生异常时写入 mstatus.xIE 的机制。
  • 描述了用于仿真未对齐 AMOs 的格式。
  • 在可变 IALIGN 的系统中规定了 misa 和 xepc 寄存器的行为。
  • 规定了向 misa 寄存器中写入自相矛盾的值的行为。
  • 定义了 mcountinhibit CSR 寄存器,该寄存器可停止性能计数器的递增以减少功耗。
  • 规定 PMP 区域的语义大于四个字节。
  • 规定跨 XLEN 修改 CSRs 的内容。
  • 将 PLIC 章节移至其自己的文档中。

译者注:

这里不是完整版指令集的翻译,只是我个人在学习 RISC-V 实现过程中目前了解到的部分内容。主要关注机器模式(M-mode)的实现部分内容。不完整的地方可查阅 RISC-V 基金会官方文档。

在试译过程中为了便于理解将 RISC-V 架构中有关于异常 (exception)、陷阱 (trap) 和中断 (interrupt)的描述做了一些简化。在 RISC-V 架构中,异常指在运行时发生的不寻常情况(包括地址异常、非法指令异常等),且同步处理;陷阱指正常运行的 hart 由另一个hart 条件引起的异常(包括系统 timer、ecall 和 ebreak 等),且同步的提升到特权模式;中断指由外部事件引发的异常(包括但不限于 TIM、GPIO 等外设引起的异常),且异步地提升到特权模式。但相较于 cpu 在正常运行时的状态来说,这三类情况从广义上来说都属于异常。所以本文中一般情况下所指的异常是比较广义上的异常,必要的地方会有区分。

第一章 介绍

本文档介绍了 RISC-V 特权体系结构,它涵盖了非特权 ISA 以外的 RISCV 系统的所有方面,包括特权指令以及运行操作系统和连接外部设备所需的其他功能。

1.1 RISC-V 特权软件堆栈术语

本节描述了我们用来描述 RISC-V 的各种可能的特权软件栈组件的术语。

下图显示了 RISC-V 架构可以支持的一些可能的软件栈。左侧显示了一个简单的系统,该系统仅支持在应用程序执行环境(AEE)上运行的单个应用程序。该应用程序以一个特定的应用程序二进制接口(ABI)来编码来运行。ABI 包括受支持的用户级 ISA+ 和一组与 AEE 进行交互的 ABI 调用。ABI 在应用程序中隐藏了 AEE 的详细信息,以便在实现 AEE 时具有更大的灵活性。相同的 ABI 可以在多个不同的主机 OS 上原生实现,也可以支持在具有不同本机 ISA 的机器上通过用户模式(U-mode)模拟环境来运行。

图片1.1

图1.1 支持各种特权执行形式的不同实现栈

中间配置显示了可以支持多个应用程序的多程序执行的常规操作系统(OS)。每个应用程序通过 ABI 与提供 AEE 的 OS 进行通信。就像应用程序接口通过 ABI 与 AEE 进行通信一样,RISC-V 操作系统也通过管理员二进制接口(SBI)与管理员执行环境(SEE)进行通信。一个 SBI 包括用户级和管理员级 ISA 以及一组 SBI 函数调用。在所有 SEE 实现中使用单个 SBI 可使单个 OS 二进制镜像在任何 SEE 上运行。SEE 可以是低端硬件平台中的简单引导加载程序和 BIOS 风格的 IO 系统,也可以是高端服务器中由管理程序提供的虚拟机,或者在体系结构模拟中可以是主机操作系统上的单薄转换层环境。


: 大多数管理员级别的 ISA 定义并未将 SBI 与执行环境和 / 或硬件平台分开,从而使虚拟化变得复杂,并带来了新的硬件平台。

最右边的配置显示了一个虚拟机监视器配置,其中单个虚拟机管理程序支持多个多任务 OS。每个 OS 都通过 SBI 与提供 SEE 的虚拟机管理程序进行通信。虚拟机管理程序使用虚拟层二进制接口(HBI)与虚拟层执行环境(HEE)通信,以将虚拟机管理程序与硬件平台的详细信息隔离开。


: ABI,SBI 和 HBI 仍在开发中,但是我们现在优先考虑对 Type-2 虚拟机管理程序的支持,其中 SBI 由 S 模式 OS 递归地提供。

RISC-V ISA 的硬件实现通常还需要特权 ISA 以外的其他特性来支持各种执行环境(AEE,SEE或HEE)。

1.2 特权等级

任何时候,RISC-V 硬件线程(hart)都以某种特权级别运行,该特权级别被编码为一个或多个 CSR(控制和状态寄存器)中的一种模式。 当前定义了三个 RISC-V 特权级别,如表1.1所示。

Level Encoding Name Abbreviation
0 00 User/Application U
1 01 Supervisor S
2 10 Reserved(Hypervisor) (H)
3 11 Machine M

表1.1 RISC-V 特权级别

特权级别用于在软件堆栈的不同组件之间提供保护,并且尝试执行当前特权模式不允许的操作将导致引发异常。 这些异常通常会导致进入底层执行环境的陷阱。


: 在描述中,我们尝试将编写代码的特权级别与运行代码的特权模式区分开,尽管两者通常是捆绑在一起的。例如,管理员级别的操作系统可以在具有三种特权模式的系统上以管理员模式运行,但是也可以在具有两种或多种特权模式的系统上的经典虚拟机监视器下以运行用户模式。在这两种情况下,都可以使用相同的管理员级别的操作系统二进制代码,将其编码为管理员级别的 SBI,因此期望能够使用管理员级别的特权指令和 CSR。在用户模式下运行访客 OS 时,所有具有管理员权限的操作都将被以较高特权级别运行的 SEE 捕获并模拟。

机器级别具有最高特权,并且是 RISC-V 硬件平台的唯一强制特权级别。在机器模式(M-mode)下运行的代码通常天生就受信任,因为它具有对机器实现的低级访问权限。M-mode 可以用来管理 RISC-V 上的安全执行环境。用户模式(U-mode)和超级用户模式(S-mode)分别用于常规应用程序和操作系统。

每个特权级别都有一组核心的特权 ISA 扩展,以及可选的扩展和变体。 例如,机器模式支持用于内存保护的可选标准扩展。

实现可以提供 1 到 3 种特权模式,以权衡减少隔离度以降低实现成本,如表1.2所示。

Number of levels Supported Modes Intended Usage
1 M Simple embedded systems
2 M,U Secure embedded systems
3 M,S,U Systems running Unix-like operating systems

表1.2 支持的特权模式组合。

所有硬件实现都必须提供 M-mode,因为这是唯一可以不受限制地访问整个计算机的模式。 最简单的 RISC-V 实现可能仅提供 M-mode,尽管这将无法防止错误或恶意的应用程序代码。

: 可选的 PMP功能的锁定特性,即使只实现 M-mode,可以提供一些有限的保护。

许多 RISC-V 实现也将至少支持用户模式(U-mode),以保护系统的其余部分免受应用程序代码的侵害。 可以添加管理员模式(S-mode),以在管理员级别的操作系统和 SEE 之间提供隔离。

Hart 通常以 U-mode 运行应用程序代码,直到某些异常(例如,管理员调用或计时器中断)强制将开关切换到异常处理程序,该异常处理程序通常以更高的特权模式运行。然后,hart 将执行异常处理程序,应用程序最终将在 U-mode 中原异常指令处或之后恢复执行。提高特权级别的 Traps 称为 vertical traps,而保持相同特权级别的 Traps 称为 horizontal traps。RISC-V 特权体系结构可将异常灵活路由到不同的特权层。


: horizontal traps 可以实现为 vertical traps,在较低特权模式下将控制权返回给 horizontal traps 处理程序。

1.3 调试模式

一个实现还可以包括一个调试模式,以支持片外调试和/或生产测试。调试模式(D-mode)可以被认为是一种额外的特权模式,其访问权限比 M-mode 更大。单独的调试规范建议描述了RISC-V hart 在调试模式下的操作。调试模式保留一些只能在 D-mode 下访问的 CSR 地址,并且还可能保留平台上物理地址空间的某些部分。

第二章 控制和状态寄存器(CSRs)

SYSTEM 主操作码用于编码 RISC-V ISA 中的所有特权指令。这些可分为两大类:控制和状态寄存器(CSRs)的原子操作(read-modify-write),以及所有其他特权指令。除了本手册第一卷中描述的用户级别状态外,一个实现还可以包含其他 CSRs,这些特权可以通过使用用户级别手册中描述的 CSR 指令的某些特权级别子集来访问。在本章中,我们规划出 CSR 地址空间。以下各章节根据特权级别描述了每个 CSRs 的功能,以及通常与特定特权级别紧密相关的其他特权指令。请注意,尽管 CSRs 和指令与一个特权级别相关联,但是它们也可以在所有更高的特权级别下访问。

2.1 CSR 地址映射规则

标准的 RISC-V ISA 留出 12 位编码空间(csr [11:0]),最多可存储 4096 个 CSRs。

按照惯例,CSR 地址的高 4 位(csr [11:8])用于根据权限级别对 CSRs 的读写可访问性进行编码,如表 2.1 所示。前两位(csr [11:10])指示寄存器是读/写(00、01 or 10)还是只读(11)。接下来的两位(csr [9:8])编码可以访问 CSR 的最低特权级别。


: CSR 地址约定使用 CSR 地址的高位来编码默认访问权限。这简化了硬件中的错误检查,并提供了更大的 CSR 空间,但确实限制了 CSR 到地址空间的映射。实现可能允许更高级别的特权捕获较低级别特权允许的 CSR 访问,以允许拦截这些访问。这个变化应该对特权较少的软件是明晰的。

尝试访问不存在的 CSR 会引发一个非法指令异常。尝试在没有适当特权级别的情况下访问 CSR 或写入只读寄存器也会引发非法指令异常。读/写寄存器可能还包含一些只读位,在这种情况下,对只读位的写操作将被忽略。

表 2.1 还列出了在标准用途和自定义用途之间分配CSR地址的约定。保留用于自定义用途的 CSR 地址不会在以后的标准扩展中重新定义。

机器模式标准读写 CSRs 0x7A0–0x7BF 保留供调试系统使用。在这些 CSRs 中,机器模式可访问 0x7A0-0x7AF,而调试模式仅可见 0x7B0-0x7BF。实现应在机器模式访问后一组寄存器时引发非法指令异常。


: 有效的虚拟化要求在虚拟化环境中尽可能多地在本机上运行指令,而任何特权访问都会 trap到虚拟机监视器[1]。如果将具有较低特权级别的只读 CSR 设为具有较高特权级别的读写,则它们将被映射到单独的 CSR 地址中。这样可以避免在允许低特权访问的同时引发 trap,同时仍然导致对非法访问的 trap。 当前,计数器是仅有的影子 CSRs。

2.2 CSR 寄存器清单

表 2.2–2.5 列出的 CSRs 寄存器是当前已分配了 CSR 地址的。timers,counters 和浮点 CSRs 是标准的用户级 CSRs,以及由 N 扩展添加的其他用户 trap 寄存器。其他寄存器由特权代码使用,如以下各章节所述。 请注意,并非所有实现都需要所有寄存器。

CSR Address Hex Use and Accessibility
[11:10] [9:8] [7:4]
User CSRs
00 00 XXXX 0x000-0x0FF Standard read/write
01 00 XXXX 0x400-0x4FF Standard read/write
10 00 XXXX 0x800-0x8FF Custom read/write
11 00 0XXX 0xC00-0xC7F Standard read-only
11 00 10XX 0xC80-0xCBF Standard read-only
11 00 11XX 0xCC0-0xCFF Custom read-only
Supervisor CSRs
00 01 XXXX 0x100-0x1FF Standard read/write
01 01 0XXX 0x500-0x57F Standard read/write
01 01 10XX 0x580-0x5BF Standard read/write
01 01 11XX 0x5C0-0x5FF Custom read/write
10 01 0XXX 0x900-0x97F Standard read/write
10 01 10XX 0x980-0x9BF Standard read/write
10 01 11XX 0x9C0-0x9FF Custom read/write
11 01 0XXX 0xD00-0xD7F Standard read-only
11 01 10XX 0xD80-0xDBF Standard read-only
11 01 11XX 0xDC0-0xDFF Custom read-only
Hypervisor CSRs
00 10 XXXX 0x200-0x2FF Standard read/write
01 10 0XXX 0x600-0x67F Standard read/write
01 10 10XX 0x680-0x6BF Standard read/write
01 10 11XX 0x6C0-0x6FF Custom read/write
10 10 0XXX 0xA00-0xA7F Standard read/write
10 10 10XX 0xA80-0xABF Standard read/write
10 10 11XX 0xAC0-0xAFF Custom read/write
11 10 0XXX 0xE00-0xE7F Standard read-only
11 10 10XX 0xE80-0xEBF Standard read-only
11 10 11XX 0xEC0-0xEFF Custom read-only
Machine CSRs
00 11 XXXX 0x300-0x3FF Standard read/write
01 11 0XXX 0x700-0x77F Standard read/write
01 11 100X 0x780-0x79F Standard read/write
01 11 1010 0x7A0-0x7AF Standard read/write debug CSRs
01 11 1011 0x7B0-0x7BF Debug-mode-only CSRs
01 11 11XX 0x7C0-0x7FF Custom read/write
10 11 0XXX 0xB00-0xB7F Standard read/write
10 11 10XX 0xB80-0xBBF Standard read/write
10 11 11XX 0xBC0-0xBFF Custom read/write
11 11 0XXX 0xF00-0xF7F Standard read-only
11 11 10XX 0xF80-0xFBF Standard read-only
11 11 11XX 0xFC0-0xFFF Custom read-only

表 2.1 RISC-V CSR地址范围的分配

sM7OiD.png

表 2.2 目前分配的 RISC-V 用户级 CSR 地址

sMHtyR.png

表 2.3 目前分配的 RISC-V 管理员级 CSR 地址

sMH7lj.png

表 2.4 目前分配的 RISC-V 机器级 CSR 地址。

sMbyNT.png

表 2.5 目前分配的 RISC-V 机器级 CSR 地址。

2.3 CSR 字段规范

以下定义和缩写用于指定 CSR 中字段的行为。

Reserved Writes Preserve Values, Reads Ignore Values (WPRI)(写保留原值,读忽略值)

一些完整的读/写字段保留供将来使用。 当将值写入同一寄存器的其他字段时,软件应忽略从这些字段读取的值,并应保留这些字段中保存的值。 为了向前兼容,不提供这些字段的实现时必须将其硬接线为零。 这些字段在寄存器描述中标记为 WPRI。

Write/Read Only Legal Values (WLRL)(只写或者只写合法值)

一些读/写 CSR 字段仅为可能的位编码的子集指定行为,而保留其他位编码。软件不应该向这样的字段写入任何合法值以外的内容,并且不应该假定读取将返回合法值,除非最后一次写入是合法值,或者由于另一次操作将寄存器设置为合法值,寄存器还没有被写入。这些字段在寄存器描述中标记为 WLRL。

如果指令试图将不支持的值写入 WLRL 字段,则允许实现,但不要求引发非法指令异常。 当最后一次写入具有非法值时,实现可以在读取WLRL字段时返回任意位模式,但是返回的值应确定性地取决于非法写入值和写入前的字段值。

Write Any Values, Reads Legal Values (WARL)(写任意值,读合法值)

一些读/写 CSR 字段仅针对位编码的子集定义,但允许写入任何值,同时保证每次读取时均返回合法值。 假设编写 CSR 没有其他副作用,则可以通过尝试写入所需的设置然后读取以查看是否保留该值来确定支持值的范围。 这些字段在寄存器说明中标记为 WARL。

在将不支持的值写入 WARL 字段时,实现不会引发异常。 当最后一次写入具有非法值时,实现可以在读取 WARL 字段时返回任何合法值,但是返回的合法值应确定性地取决于非法写入值和写入之前的字段值。

2.4 CSR 宽度调整

如果更改了 CSR 的宽度(例如,如第 3.1.6.2 节中所述,通过更改 MXLEN 或 UXLEN),则除非另有说明,否则新宽度CSR的可写字段和位是由先前宽度 CSR 确定的,就像通过以下算法确定一样:

  1. 将先前宽度的 CSR 的值复制到相同宽度的临时寄存器中。

  2. 对于先前宽度的CSR的只读位,临时寄存器中相同位置的位设置为零。

  3. 临时寄存器的宽度更改为新的宽度。 如果新宽度 W 窄于先前宽度,则保留临时寄存器的最低有效 W 位,并丢弃较高有效位。 如果新宽度比以前的宽度宽,则将临时寄存器零扩展到较宽的宽度。

  4. 新宽度 CSR 的每个可写字段都采用临时寄存器中相同位置的位值。

更改 CSR 的宽度不是对 CSR 的读取或写入,因此不会触发任何副作用。

第三章 机器级 ISA(Machine-Level ISA), 版本 1.1

本章介绍了机器模式(M-mode)下提供的机器级操作,该模式是 RISC-V 系统中的最高特权模式。M-mode 用于对硬件平台的低级别访问,并且是复位时进入的第一个模式。M-mode 也可以用来完成那些在硬件上直接实现太困难或太昂贵的功能。RISC-V 机器级 ISA 包含一个公共核心,该核心可以根据所支持的其他特权级别以及硬件实现的其他详细信息进行扩展。

3.1 Machine-Level CSRs (机器级 CSRs)

除了本节中描述的机器级 CSRs 外,M-mode 代码还可以访问较低特权级别的所有 CSRs。

3.1.1 机器指令集(ISA)寄存器: misa

misa CSR 是 WARL 读写寄存器,报告硬件(hart)支持的 ISA。该寄存器在任何实现中都必须是可读的,但是可以返回零值以指示未实现 misa 寄存器,这就需要通过一个单独的非标准机制确定 CPU 功能。

sMOvr9.png

图 3.1 机器 ISA 寄存器(misa)

MXL(机器 XLEN)字段编码本机基本整数 ISA 宽度,如表 3.1 所示。MXL 字段在支持多个基本 ISA 宽度的实现中可能是可写的。M-mode 下的有效 XLEN, MXLEN,由 MXL 的设置给出,如果 misa 为零,则有一个固定的值。重置时,MXL 字段始终设置为最广泛支持的 ISA 变种。

MXL XLEN
1 32
2 64
3 128

misa CSR 为 MXLEN 位宽。如果从 misa 读取的值不为零,该值的 MXL 字段总是表示当前的 MXLEN。如果对 misa 的写操作导致 MXLEN 发生更改,则 MXL 的位置将以新的宽度移动到 misa 的最高有效两位。


: 可以使用返回的 misa 值的符号上的分支,以及可能在符号上左移一个分支和第二个分支,来快速确定基本宽度。这些检查可以用汇编代码编写,而无需知道机器的寄存器宽度(XLEN)。 基本宽度由 XLEN = 2^(MXL + 4) 给出。 如果 misa 为零,则可以通过将立即数 4 放置在一个寄存器中,然后一次将寄存器左移 31位来找到基本宽度。如果在一次移位后为零,则该机器为 RV32。 如果两次移位后为零,则机器为 RV64,否则为 RV128。

Extensions 字段对标准扩展的存在进行编码,每个字母都有一个比特(bit 0 编码扩展名“ A” 存在,bit 1 编码扩展名“ B” 存在,直到 bit 25 编码“ Z”)。RV32I,RV64I,RV128I 基本 ISA 的“ I”位置 1,而 RV32E 的“ E”位置 1。Extensions 字段是 WARL 字段,可以包含可写位,其中实现允许修改支持的 ISA。重置时,“Extensions ”字段应包含支持的扩展名的最大集合,并且如果两者均可用,则 “I” 应该被选择而不是 E 。

RV128I 基本 ISA 尚未冻结,尽管本规范的其余大部分有望应用于 RV128,但这个版本的文档仅关注 RV32 和 RV64。

“ G” 位用作转义符,以允许扩展到更大的标准扩展名空间。


: G 用于指示 IMAFD 组合,因此在 misa CSR 中是多余的,因此我们保留该位以指示存在其他标准扩展。

如果分别支持用户和管理员模式,则将设置“ U”和“ S”位。

如果存在任何非标准扩展名,则将设置“ X”位。


: misa CSR 将 CPU 功能的基本目录暴露给机器模式代码。可以在机器模式中通过探测其他机器寄存器,并检查系统中的其他 ROM 存储器,作为引导过程的一部分来获得更多的信息。 我们要求较低特权级别执行环境调用,而不是读取 CPU 寄存器来确定每个特权级别可用的功能。这使虚拟化层可以更改在任何级别观察到的 ISA,并支持更丰富的命令界面,而不会增加硬件设计的负担。

“ E”位是只读的。 除非将 misa 硬连线为零,否则“ E”位始终读取为“ I”位的补码(补集?)。同时支持 RV32E 和 RV32I 的实现可以通过清除“ I”位来选择 RV32E。

sQmQJO.png

表 3.2 misa 中 Extensions 字段的编码。 保留供将来使用的所有位在读取时必须返回零。

如果 ISA 功能 x 取决于 ISA 功能 y,则尝试启用功能 x 但禁用功能 y 会导致两个功能都被禁用。 例如,设置 “ F” = 0 和 “ D” = 1 会导致同时清除“ F”和“ D”。

一个实现可能会对两个或多个 misa 字段的集合设置施加其他约束,在这种情况下,它们将共同充当单个 WARL 字段。 尝试写入不受支持的组合会导致将这些位设置为某些受支持的组合。

写 misa 可能会增加 IALIGN,例如,通过禁用 C 扩展。如果要写入 misa 的指令增加了 IALIGN,而后一条指令的地址未按 IALIGN 位对齐,则将抑制对 misa 的写入,从而使 misa 保持不变。

3.1.2 机器厂商 ID 寄存器: mvendorid

mvendorid CSR 是一个 32 位只读寄存器,提供核心提供者的 JEDEC 制造商 ID。该寄存器在任何实现中都必须是可读的,但是可以返回 0 值指示该字段未实现或这是非商业实现。

sQn1A0.png

图3.2 Vendor ID 寄存器(mvendorid)

JEDEC 制造商 ID 通常编码为单字节连续的 0x7f 代码的序列,以不等于 0x7f 的单字节 ID 终止,并且在每个字节的最高有效位中带有奇校验位。mvendorid 在 Bank 字段中编码单字节的连续代码,并在 Offset 字段中编码最后一个字节,丢弃奇偶校验位。例如,JEDEC 制造商ID 0x7f 0x7f 0x7f 0x7f 0x7f 0x7f 0x7f 0x7f 0x7f 0x7f 0x7f 0x7f 0x8a(十二个连续代码,后跟 0x8a)将在 mvendorid 字段中编码为 0x60a。


: 以前,供应商 ID 是 RISC-V 基金会分配的编号,但这与 JEDEC 在维护制造商 ID 标准方面的工作重复。 在撰写本文时,向 JEDEC 注册制造商 ID 的一次性费用为 500 美元。

3.1.3 机器架构 ID 寄存器: marchid

marchid CSR 是 MXLEN 位的只读寄存器,用于编码 hart 的基本微体系结构。该寄存器在任何实现中都必须是可读的,但是可以返回 0 值以指示未实现该字段。 mvendorid 和 marchid 的组合应该唯一地标识所实现的 hart 微体系结构的类型。

sQMCnJ.png

图 3.3 机器架构 ID 寄存器(marchid)

开源项目体系结构 IDs 由 RISC-V 基金会全局分配,并且具有非零体系结构 IDs,其中最高有效位(MSB)为零。商业体系结构 IDs 由每个商业供应商独立分配,但是必须设置 MSB,并且在其余 MXLEN-1 位中不能包含零。


: *

3.1.4 机器实现 ID 寄存器: mimpid

3.1.5 Hart ID 寄存器: mhartid

3.1.6 机器状态寄存器:mstatus

mstatus 寄存器是一个 MXLEN 位读/写寄存器,其格式在 RV32 上的如图 3.6 和在 RV64 如图 3.7 所示。mstatus 寄存器跟踪并控制 hart 的当前操作状态。 mstatus 寄存器的受限视图分别在 S 级和 U 级 ISA 中显示为 sstatus 和 ustatus 寄存器。

sQINkD.png

图 3.6 RV32下机器模式状态寄存器 (mstatus)

sQIUte.png

图 3.7 RV64 下机器模式状态寄存器 (mstatus)

3.1.6.1 mstatus 寄存器中的特权和全局中断使能栈

为每种特权模式提供了全局中断使能位 MIE,SIE 和 UIE。这些位主要用于保证当前特权模式下中断处理程序的原子性。


: 全局 xIE 位位于 mstatus 的低位,因此可以通过一条 CSR 指令自动设置或清除它们。

当 hart 在 x 特权模式中执行时,当 xIE = 1 时中断被全局启用,当 xIE = 0 时中断被全局禁用。无论低特权模式的全局 wIE 位如何设置,w < x,较低特权模式的中断总是会全局禁用。无论高特权模式的全局 yIE 位如何设置,y > x,始终会全局启用高特权模式的中断。特权级别较高的代码可以使用单独的每个中断启用位来禁用选定的较高级别的特权模式中断,然后再将控制权移交给较低级别的特权模式。


: 较高特权模式 y 可以在将控制权移交给较低特权模式之前禁用其所有中断,但这将是不寻常的,因为它将仅留下同步异常 (trap),不可屏蔽的中断或重置以重新获得对 hart 的控制。

为了支持异常 (traps) 嵌套,每个 x 特权模式都有一个两级的堆栈,其中包括中断使能位和特权模式。xPIE 保留异常之前活动的中断使能位的值,而 xPP 保留先前的特权模式。xPP 字段最多只能保留 x 的特权模式,因此 MPP 为 2 位宽,SPP 为 1 位宽,UPP 隐式为 0。当异常从特权模式 y 进入特权模式 x 时,xPIE 设置为 xIE 的值; xIE 设置为 0;并且 xPP 设置为 y。


: 对于低特权模式,任何异常(同步或者异步)通常都以更高的特权模式进行,进入时禁用中断。更高级别的 trap 处理程序要么为 trap 提供服务,然后使用堆栈的信息返回,要么(如果没有立即返回到被中断的上下文)在重新启用中断之前保存特权堆栈,因此每个堆栈只需要一个入口。

MRET,SRET 或 URET 指令分别用于从 M-mode,S-mode 或 U-mode 的异常返回。当执行 xRET 指令时,假设 xPP 保持值 y,则 xIE 设置为 xPIE; 特权模式更改为 y; xPIE 设置为 1; 并且 xPP 设置为 U(如果不支持用户模式,则为 M)。

xPP 字段是 WARL 字段,只能包含 x 特权模式 和任何低于 x 的已实现特权模式。 如果未实现 x 特权模式,则必须将 xPP 硬接线为 0。


: M-mode 软件可以通过将特权模式写入 MPP 然后将其读回来确定是否实现了特权模式。 如果机器仅提供 U 和 M 模式,则仅需要单个硬件存储位即可表示 MPP 中的 00 或 11。

用户级中断是可选的扩展,并已分配 ISA 扩展字母 N。 如果省略了用户级中断,则 UIE 和 UPIE 位被硬接线为零。对于所有其他受支持的 x 特权模式,不得对 xIE 和 xPIE 进行硬接线。


: 用户级中断的主要目的是支持仅包含 M-mode 和 U-mode 的安全嵌入式系统,但也可以在运行类 Unix 操作系统的系统中支持的用户级中断,以支持用户级异常处理。

3.1.6.2 mstatus 寄存器中基本 ISA 控制

对于 RV64 系统,SXL 和 UXL 字段是 WARL 字段,分别控制 S-mode 和 U-mode 的 XLEN 值。这些字段的编码与 misa 的 MXL 字段相同,如表 3.1 所示。 S-mode 和 U-mode 下的有效 XLEN 分别称为 SXLEN 和 UXLEN。

对于 RV32 系统,SXL 和 UXL 字段不存在,并且 SXLEN = 32 和 UXLEN = 32。

对于 RV64 系统,如果不支持 S-mode,则 SXL 硬接线为零。 否则,它是一个 WARL 字段,对 SXLEN 的当前值进行编码。 特别地,该实现可以硬连线 SXL,使得 SXLEN = MXLEN。

对于 RV64 系统,如果不支持 U-mode,则 UXL 硬接线为零。 否则,它是一个 WARL 字段,对 UXLEN 的当前值进行编码。 特别地,该实现可以硬编码 UXL,使得 UXLEN = MXLEN 或 UXLEN = SXLEN。

只要在任何模式下将 XLEN 的值设置为小于支持的最大 XLEN 的值,所有操作都必须忽略配置的 XLEN 上方的源操作数寄存器位,并且必须对结果进行符号扩展以填充目标寄存器中整个支持的最大 XLEN。

如果 MXLEN 从 32 更改为更宽的宽度,则 mstatus 字段 SXL 和 UXL 中的每个(如果未硬接线为强制值)将获得与最大支持宽度对应的值,该宽度不比新 MXLEN 宽。

3.1.6.3 mstatus 寄存器中内存特权

3.1.6.4 mstatus 寄存器中虚拟化支持

3.1.6.5 mstatus 寄存器中扩展上下文状态(Extension Context Status)

支持大量扩展是 RISC-V 的主要目标之一,因此,我们定义了一个标准接口,以允许不变的特权模式代码(尤其是管理员级别的 OS)支持任意的用户模式状态扩展。

FS [1:0] WARL字段和XS [1:0]只读字段分别用于通过设置和跟踪浮点单元和任何其他用户模式扩展的当前状态来减少上下文保存和还原的成本。

3.1.7 机器异常向量(Trap-Vector)基地址寄存器: mtvec

mtvec 寄存器是 MXLEN 位的读/写寄存器,用于保存异常向量配置, 由向量基址(BASE)和向量模式(MODE)组成。

image-20210111141718084

图 3.8 机器异常向量基地址寄存器 (mtvec)

mtvec 寄存器必须始终实现,但可以包含硬连线的只读值。 如果 mtvec 是可写的,则寄存器可能保存的一组值可能因实现而异。 BASE 字段中的值必须始终在 4 字节边界上对齐,并且 MODE 设置可能会对 BASE 字段中的值施加其他对齐约束。


: 我们在实现 trap 向量基址时具有相当大的灵活性。 一方面,我们不希望负担大量状态位给底层实现带来负担,但另一方面,我们希望为更大的系统提供灵活性。

s37vLj.png

表 3.5 mtvec MODE 字段编码

表 3.5 中显示了 MODE 字段的编码。 当 MODE = Direct 时,所有进入机器模式的 trap 都会导致将 pc 设置为 BASE 字段中的地址。当 MODE = Vectored 时,进入机器模式的所有同步异常都将 pc 设置为 BASE 字段中的地址,而中断将 pc 设置为 BASE 字段中的地址加上四倍的中断 cause 编号。例如,机器模式计时器中断导致将 PC 设置为 BASE + 0x1c(请参阅表 3.6)。


: 启用向量中断后,与用户模式软件中断相对应的中断 cause 0 被向量到与同步异常相同的位置。 由于用户模式软件中断被禁用或委托给特权较少的模式,因此在实践中不会出现这种歧义。

对于不同的模式,实现可能具有不同的对齐约束。 特别的是,MODE = Vectored 可能比 MODE = Direct 更严格的对齐约束。

3.1.8 机器异常(Trap)委托寄存器:(medeleg and mideleg)

3.1.9 机器中断寄存器:(mip 和 mie)

mip 寄存器是 MXLEN 位读/写寄存器,包含有关挂起的中断的信息,而 mie 是对应的 MXLEN 位读/写寄存器,包含中断使能位。通过该 CSR 地址只能写入 mip 中的低特权软件中断(USIP,SSIP),定时器中断(UTIP,STIP)和外部中断(UEIP,SEIP)所对应的位; 其余位是只读的。


: 机器级中断寄存器处理一些根中断源,为简单起见,这些中断源被分配了固定的服务优先级,而单独的外部中断控制器可以对更大的一组中断实施更复杂的优先级排序方案,然后将这些中断混和到机器级 中断源。

mip 和 mie 寄存器的受限视图分别以 sip / sie 和 uip / uie 寄存器的形式出现在 S-mode 和 U-mode 下。 如果通过在 mideleg 寄存器中设置一个位将中断委托给 x 特权模式,则该中断在 x ip 寄存器中可见,并且可以使用 x ie 寄存器屏蔽。否则,x ip 和 x ie 中的相应位呈现硬连接为零。

sGp4Dx.png

图 3.11 机器中断挂起寄存器

MTIP,STIP,UTIP 位对应于机器级,管理员级和用户级的定时器中断。MTIP 位是只读的,可通过写入内存映射的机器模式定时器比较寄存器来清除。UTIP 和 STIP 位可以由 M-mode 软件写入,以将计时器中断传递到较低的特权级别。 用户和管理员层软件可以分别通过调用 AEE 和 SEE 来清除 UTIP 和 STIP 位。

sG9jW4.png

图 3.12 机器中断使能寄存器

有一个单独的定时器中断使能位,分别称为 MTIE,STIE 和 UTIE,分别用于 M 模式,S 模式和 U 模式定时器中断使能。

每个较低的权限级别都有一个单独的软件挂起中断位( SSIP、USIP ), CSR 访问本地 hart 上运行的代码可以在相关的权限级别或任何更高的权限级别上读写该位。机器级别的 MSIP 位是通过访问内存映射的控制寄存器来写入的,远程 harts 使用这些寄存器来提供机器模式的处理器间中断。较低特权级别的处理器间中断是通过特定于实现的机制来实现的,例如,通过对 AEE 或 SEE 的调用,这可能最终导致对接收方 hart 的机器模式写操作是 MSIP 位。 hart 可以使用相同的内存映射控制寄存器写入自己的 MSIP 位。

mie CSR 中的 MSIE,SSIE 和 USIE 字段分别启用M模式软件中断,S 模式软件中断和 U 模式软件中断。


: 当以适当的模式运行时,我们仅允许 hart 直接写入自己的 SSIP 或 USIP 位,因为其他 hart 可能会被虚拟化,并可能被更高的特权级别调度。因此,我们依靠对 AEE 和 SEE 的调用来提供处理器间中断。机器模式的 hart 不是虚拟化的,可以通过设置它们的 MSIP 位直接中断其他 hart,通常根据平台规范使用未缓存的 I/O 写入内存映射控制寄存器。

mip 中的 MEIP 字段是一个只读位,指示机器模式外部中断正在处理中。 MEIP 由特定于平台的中断控制器设置和清除。 设置时,mie 中的 MEIE 字段启用机器外部中断。

mip 中的 SEIP 字段包含单个读写位。 SEIP 可由 M 模式软件编写,以指示 S 模式外部中断正在处理。 此外,平台级别的中断控制器可能会生成主管级别的外部中断。 软件可写位的逻辑或与来自外部中断控制器的信号用于生成对管理程序的外部中断。 当使用 CSRRW,CSRRS 或 CSRRC 指令读取 SEIP 位时,在第一个目标寄存器中返回的值包含软件可写位的逻辑或,以及来自中断控制器的中断信号。 但是,在 CSRRS 或 CSRRC 指令的读-修改-写序列中使用的值只是软件可写 SEIP 位,而忽略了来自外部中断控制器的中断值。

3.1.10 机器时间寄存器:(mtime 和 mtimecmp)

3.1.11 硬件性能监控器

3.1.12 计数器使能寄存器:([m|s]counteren)

3.1.13 机器计数器禁止 CSR 寄存器:(mcountinhibit)

3.1.14 机器暂存寄存器:(mscratch)

3.1.15 机器异常程序计数器:(mepc) ——指向发生异常PC地址

3.1.16 机器异常源寄存器:(mcause)——(异常源 or 中断号 or 异常种类?)

3.1.17 机器异常值寄存器:(mtval)

3.2 机器模式(M-mode)特权指令

3.2.1 环境调用和断点:(ECALL 和 EBREAK)

3.2.2 异常返回指令(Trap-return)

3.2.3 等待中断指令:(WFI)

3.3 复位(Reset)

3.4 不可屏蔽中断(NMI)

3.5 物理内存特性(PMA)

3.5.1 主内存对 I/O 设备和空白区域

3.5.2 PMAs 支持的访问类型

3.5.3 PMAs 的原子操作

3.5.4 PMAs 内存排序操作

3.5.5 PMAs 的一致性和可缓存性

3.5.6 PMAs 幂等性

3.6 物理内存保护(PMP)

为了支持安全处理和遏制故障,最好是限制在 hart 上运行的软件所能访问的物理地址。一个可选的物理内存保护(PMP)单元提供每个 hart 的机器模式控制寄存器,允许为每个物理内存区域指定物理内存访问权限(读、写、执行)。PMP 值与第 3.5 节中描述的 PMA 检查并行检查。

PMP 访问控制设置的粒度是特定于平台的,平台内的粒度可能因物理内存区域而异,但标准 PMP 编码支持小到四个字节的区域。某些区域的权限可以是硬连接的。例如,某些区域可能仅在机器模式下可见,但在较低的权限层中不可见。


: 平台对物理内存保护的需求差异很大,一些平台可能提供其他 PMP 结构,以补充或替代本节中描述的方案。

当 hart 在 S 或 U 模式下运行时,PMP 检查适用于所有访问;当 mstatus 寄存器中设置 MPRV 位且 mstatus 寄存器中的 MPP 字段包含 S 或 U 时,PMP 检查适用于加载和存储。PMP 检查也适用于页表访问,以进行虚拟地址转换,有效特权模式为 S。可选地,PMP 检查可额外应用于 M 模式访问,在这种情况下,PMP 寄存器本身被锁定,因此即使是 M 模式软件也无法在没有系统重置的情况下更改它们。实际上,PMP 可以向 S 模式和 U 模式授予权限,默认情况下 S 模式和 U 模式没有权限,并且可以从 M 模式撤销权限,M 模式默认情况下具有完全权限。

PMP 冲突总是精确地捕获在处理器上。

3.6.1 CSRs 的物理内存保护寄存器(PMP CSR)

PMP 条目由一个 8 位配置寄存器和一个 MXLEN 位地址寄存器描述。某些 PMP 设置还使用与前面的 PMP 条目关联的地址寄存器。最多支持 16 个 PMP 条目。如果实施了任何 PMP 条目,则必须实施所有 PMP CSR,但所有 PMP CSR 字段均为 WARL,并且可能硬连线为零。PMP CSR 仅适用于 M 模式。

PMP 配置寄存器被密集地打包到 CSRs 中,以最小化上下文切换时间。对于 RV32,四个 CSRs pmpcfg0–pmpcfg3 为 16 个 PMP 条目保留配置 pmp0cfg–pmp15cfg,如图 3.24 所示。对于 RV64,pmpcfg0 和 pmpcfg2 保留 16 个 PMP 条目的配置,如图 3.25 所示;pmpcfg1 和 pmpcfg3 是非法的。


: RV64 系统使用 pmpcfg2 而不是 pmpcfg1 来保存 PMP 条目 8–15 的配置。这种设计降低了支持多个 MXLEN 值的成本,因为 RV32 和 RV64 的 PMP 条目 8–11 的配置都出现在 pmpcfg2[31:0] 中。

Ia85GV.png

IaGheH.png

PMP 地址寄存器是名为 pmpaddr0–pmpaddr15 的 CSR。每个 PMP 地址寄存器对 RV32 的 34 位物理地址的 33–2 位进行编码,如图 3.26 所示。对于 RV64,每个 PMP 地址寄存器对 56 位物理地址的位 55–2 进行编码,如图 3.27 所示。并非所有物理地址位都可以实现,因此 pmpaddr 寄存器是 WARL。


: 第 4.3 节中描述的基于 Sv32 页的虚拟内存方案支持 RV32 的 34 位物理地址,因此 PMP 方案必须支持比 RV32 的 XLEN 更宽的地址。第 4.4 节和第 4.5 节中描述的基于 Sv39 和 Sv48 页的虚拟内存方案支持 56 位物理地址空间,因此 RV64 PMP 地址寄存器施加相同的限制。

图 3.28 显示了 PMP 配置寄存器的布局。设置时,R、W 和 X 位分别表示 PMP 条目允许读取、写入和指令执行。当这些位之一清除时,相应的访问类型被拒绝。R=0 和 W=1 的组合留作将来使用。其余两个字段 A 和 L 将在以下部分中介绍。

IaNlcV.png

IaN0c6.png

试图从没有执行权限的 PMP 区域获取指令会引发获取访问异常。如果在没有读取权限的情况下尝试执行有效地址位于 PMP 区域内的 load 或 load 保留指令,则会引发加载访问异常。如果在没有写入权限的情况下,尝试执行有效地址位于 PMP 区域内的存储、存储条件(无论是否成功)或 AMO 指令,则会引发存储访问异常。

如果更改了 MXLEN,则 pmpxcfg 字段的内容将保留,但会出现在由 MXLEN 的新设置规定的 pmpcfgy CSR 中。例如,当 MXLEN 从 64 更改为 32 时,pmp4cfg 从 pmpcfg0[39:32] 移动到 pmpcfg1[7:0]。pmpaddr CSR 遵循第 2.4 节中描述的通常 CSR 宽度调制规则。

地址匹配

PMP 条目的配置寄存器中的 A 字段对关联 PMP 地址寄存器的地址匹配模式进行编码。该字段的编码如表 3.9 所示。当 A=0 时,此 PMP 条目被禁用,并且不匹配任何地址。支持另外两种地址匹配模式:自然对齐的二次幂区域(NAPOT),包括自然对齐的四字节区域(NA4)的特例;和任意范围的上边界(TOR)。这些模式支持四字节粒度。

IaNzuT.png

NAPOT 范围利用相关地址寄存器的低位对范围大小进行编码,如表 3.10 所示。

IaUIR1.png

如果选择 TOR,则相关地址寄存器构成地址范围的顶部,前面的 PMP 地址寄存器构成地址范围的底部。如果 PMP 条目 i 的 A 字段设置为 TOR,则该条目与任何地址 y 匹配,从而 pmpaddr i−1 ≤ y < pmpaddr i。如果 PMP 条目 0 的 A 字段设置为 TOR,则下限使用 0,因此它匹配任何地址 y < pmpaddr 0。

尽管 PMP 机制支持小到四个字节的区域,但平台可能会指定更粗糙的 PMP 区域。通常,PMP 粒度为 2G+2 字节,并且在所有 PMP 区域中必须相同。当 G≥1、NA4 模式不可选择。当 G≥2 且设置了pmpcfgi.A[1],即模式为 NAPOT,然后位 pmpaddr i[G-2:0] 读取为所有。当 G≥1 和 pmpcfg i.A[1] 清除,即模式为 OFF 或 TOR,则位pmpaddr i[G-1:0] 读取为全零。位 pmpaddr i[G-1:0] 不影响 TOR 地址匹配逻辑。虽然更改 pmpcfg i.A[1] 会影响从 pmpaddr i 读取的值,但不会影响存储在该寄存器中的基础值,特别是,当 pmpcfg i.A 从 NAPOT 更改为 TOR/OFF,然后再更改回 NAPOT 时,pmpaddr i[G-1] 保留其原始值。


: 软件可以通过将 0 写入 pmp0cfg ,然后将所有的写入 pmpaddr0,然后读回 pmpaddr0 来确定 PMP 粒度。如果 G 是最低有效位集的索引,则 PMP 粒度为 2G+2 字节。

如果当前 XLEN 大于 MXLEN,为了地址匹配,PMP 地址寄存器从 MXLEN 零扩展到 XLEN 位。

锁定和特权模式

L 位表示 PMP 条目已锁定,即忽略对配置寄存器和相关地址寄存器的写入。锁定的 PMP 条目只能在系统重置时解锁。如果 PMP 条目 i 被锁定,则将忽略对 pmpicfg 和 pmpaddri 的写入。此外,如果将 pmpicfg.A 设置为 TOR,则将忽略对 pmpaddri-1 的写入。

除了锁定 PMP 条目外,L 位还指示在 M 模式访问上是否强制执行 R/W/X 权限。当设置 L 位时,所有特权模式都强制执行这些权限。当 L 位清除时,任何与 PMP 条目匹配的 M 模式访问都将成功;R/W/X 权限仅适用于 S 和 U 模式。

优先级和匹配逻辑

PMP 条目按静态优先级排列。与访问的任何字节相匹配的编号最低的 PMP 条目确定访问是成功还是失败。匹配的 PMP 条目必须匹配访问的所有字节,否则访问失败,与 L、R、W 和 X 位无关。例如,如果将 PMP 条目配置为匹配四字节范围 0xC–0xF,则对范围 0x8–0xF 的 8 字节访问将失败,前提是 PMP 条目是匹配这些地址的最高优先级条目。

如果 PMP 条目与访问的所有字节匹配,则 L、R、W 和 X 位确定访问是否成功。如果L位清除且访问权限模式为 M,则访问成功。否则,如果设置了 L 位或访问的特权模式为 S 或 U,则仅当设置了与访问类型对应的 R、W 或 X 位时,访问才会成功。

如果没有与 M 模式访问匹配的 PMP 条目,则访问成功。如果没有 PMP 条目匹配 S 模式或 U 模式访问,但至少实现了一个 PMP 条目,则访问失败。

失败的访问将生成加载、存储或指令访问异常。请注意,一条指令可能会生成多个访问,这些访问可能不是相互原子的。如果一条指令生成的至少一次访问失败,则会生成访问异常,尽管该指令生成的其他访问可能会成功,并产生明显的副作用。值得注意的是,引用虚拟内存的指令被分解为多个访问。

在某些实现中,未对齐的加载、存储和指令获取也可能分解为多个访问,其中一些可能在访问异常发生之前成功。特别是,即使另一部分未通过 PMP 检查,通过 PMP 检查的未对齐存储的一部分也可能变得可见。对于比 XLEN 位宽的浮点存储(例如,RV32D 中的 FSD 指令),即使存储地址自然对齐,也可能表现出相同的行为。

3.6.2 物理内存保护和分页

物理内存保护机制旨在与第 4 章中描述的基于页面的虚拟内存系统组成。启用分页时,访问虚拟内存的指令可能导致多个物理内存访问,包括对页表的隐式引用。PMP 检查适用于所有这些访问。隐式页表访问的有效特权模式是 S。

使用虚拟内存的实现允许推测性地执行地址转换,并且比显式虚拟内存访问所需的时间更早。可以在地址转换和显式虚拟内存访问之间的任意点检查结果物理地址的 PMP 设置。因此,当修改 PMP 设置的方式影响保存页表的物理内存或页表指向的物理内存时,M 模式软件必须将 PMP 设置与虚拟内存系统同步。这是通过在写入 PMP CSR 后执行 rs1=x0 和 rs2=x0 的 SFENCE.VMA 指令来实现的。

如果未实现基于页的虚拟内存,或者禁用了基于页的虚拟内存,则内存访问会同步检查 PMP 设置,因此不需要 Fence。

wujian100 的 PWM 周期问题

作者 strongwong
2020年8月2日 08:56

PWM 波形发生器实验结果

在 wujian100 的样例程序代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int32_t  pwm_signal_test(uint32_t pwm_idx, uint8_t pwm_ch)
{
int32_t ret;
pwm_handle_t pwm_handle;

example_pin_pwm_init();

pwm_handle = csi_pwm_initialize(pwm_idx);

if (pwm_handle == NULL) {
printf("csi_pwm_initialize error\n");
return -1;
}

ret = csi_pwm_config(pwm_handle, pwm_ch, 3000, 1500); //设置pwm周期3ms

if (ret < 0) {
printf("csi_pwm_config error\n");
return -1;
}

csi_pwm_start(pwm_handle, pwm_ch);
mdelay(20);

ret = csi_pwm_config(pwm_handle, pwm_ch, 200, 150); //设置pwm周期200us

if (ret < 0) {
printf("csi_pwm_config error\n");
return -1;
}

mdelay(20);
csi_pwm_stop(pwm_handle, pwm_ch);

csi_pwm_uninitialize(pwm_handle);

return 0;

}

其中有两句配置函数

1
2
ret = csi_pwm_config(pwm_handle, pwm_ch, 3000, 1500); //设置pwm周期3ms,占空比1500/3000
ret = csi_pwm_config(pwm_handle, pwm_ch, 200, 150); //设置pwm周期200us占空比50/200

这两句都是用来配置 pwm 周期的,是实际测试中 pwm 周期和占空比都没有问题。但是如果设置 pwm 周期为 5ms 时,需要修改这句话为:

1
ret = csi_pwm_config(pwm_handle, pwm_ch, 5000, 2500);

在实际测试过程中会发现,他的周期实际是 2.5ms 并不是预期的 5ms,如果设置为 100ms 会发现 pwm 的实际输出周期是 2.5ms。实际输出是有问题的。

查找问题

检查软件问题

跟踪 csi_pwm_config 函数,发现在函数中会根据设置的周期进行时钟配置:
drv_pwm_config_clockdiv(handle, channel, cnt_div[count_div]); 由于 pwm 计数器是 16 位,所以软件会根据设置的周期值计算出要配置的数值,如果该数值超过 0xffff,就会设置分频系数,直到需要计数的值小于 0xffff 为止。

继续跟踪
drv_pwm_config_clockdiv(handle, channel, cnt_div[count_div]);
我们可以看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void drv_pwm_config_clockdiv(pwm_handle_t handle, uint8_t channel, uint32_t div)
{
PWM_NULL_PARAM_CHK_NORETVAL(handle);

wj_pwm_priv_t *pwm_priv = handle;
wj_pwm_reg_t *addr = (wj_pwm_reg_t *)(pwm_priv->base);
addr->PWMCFG &= ~(7 << 24);

switch (div) {
case 1:
addr->PWMCFG &= ~(PWM_CFG_CNTDIV_EN);
break;

case 2:
addr->PWMCFG |= PWM_CFG_CNTDIV_EN | PWM_CFG_CNTDIV_2;
break;

case 5:
addr->PWMCFG |= PWM_CFG_CNTDIV_EN | PWM_CFG_CNTDIV_5;
break;
........

这个函数会对寄存器 PWMCFG 的第 25 位到第 27 位进行了赋值操作,查找数据手册发现

PWMCFG

第 28 位是分频使能,第 26 到 24 位是分频系数设置。

在 csi 库里提供了一个函数可以读取这几位的值来查看分频的设置:

1
2
3
4
5
6
7
/**
\brief get pwm clock division.
\param[in] handle pwm handle to operate.
\param[in] channel channel num.
\return div clock div.
*/
uint33_t drv_pwm_get_clockdiv(pwm_handle_t handle, uint8_t channel)

通过这个函数可以得到 PWMCFG 寄存器里的分频系数 cntdiv 的值。通过实验我们发现分频系数的值配置到寄存器里了,并且读回来的值也是配置的值。

总结:所以软件上对于 pwm 的配置是没有问题的,配置 pwm 周期不是预期值不是软件问题。

检查硬件问题

排除软件问题,那么出现 pwm 周期非预期值他的问题就只可能是 pwm 外设在设计时的硬件问题了。我们查找 wujian100 内部设计的问题,为了方便查看我们使用 verdi 来查看跟踪模块设计,这样效率高。

首先第一步配置 wujian100 工作路径:在 linux 系统中利用 source 将 wujian100 工作目录添加到系统环境变量。

第二步 tb 目录下的 tb_file.list 文件,这个文件里加载的顶层文件是 wujian100_open_top.v,并不是我们生成bit文件时的 wujian100_open_fpga_top.v 文件(该文件在 fpga 目录下),我们首先赋值 fpga 目录下的 wujian100_open_fpga_top.v 到 soc 目录下。修改文件 tb_file.list 里的第 3 行,将 wujian100_open_top.v 替换为 wujian100_open_fpga_top.v

第三步进入 tb 目录。使用 verdi -f tb_file.list 打开软件 verdi 并加载 tb_file.list 里列出的文件。(前提是你的 linux 系统安装了 verdi 软件)。

以上三步正确就会打开 wujian100 设计的模块图。

wujian100 top

查看选中打开该文件,查看文件名是不是wujian100_open_fpga_top.v

wujian100_open_fpga_top.v

然后打开原理图。点击如图按钮:

原理图))

打开 wujian100 的设计如图:

wujian100

接下来可以按文件查找相应模块,也可以双击原理图上的模块一层一层进入。

pwm 模块位于 PDU 下的 ahb1 上。一层一层进入查看下,定位到 pwm:

pwm_sec_top

继续进入 pwm_sec_top ,再进入 pwm 就是 pwm 外设的内部了。

pwm_ctrl & pwm_aphif

在这里有两部分,一个是 aphif 负责总线,ctrl 就是 pwm 的实际实现了。
进入 pwm_ctrl:

pwm_ctrl

我们发现有六个 pwm_gen。和数据手册上介绍的一样。

6 pwm_gen

我们现在定位到左下角,放大看,这部分就是pwm的时钟部分:

pwm_gen
我们在 view 菜单下打开端口名称显示和模块内部端口名称显示。在图上我们可以看到 cntdiv[3:0] 控制了分频系数,分频系数会通过分频器(图上的 f)对 pclk 系统时钟进行分频,然后通过 gated_clk_cell 控制时钟的通过然后通过 clk_mux2 选择器送到后续的 pwm 发生器上作为发生器的时钟。我们双击模块就能够查看各自对应的 verilog 代码。我们在逐个查找时发现 gated_clk_cell(如下图)它的结构有问题:

gated_clk_cell

双击它,查看它内部结构:

gated_clk_cell

如图这样一个很奇怪的结构,直接就是 clk_in 输入,直接送到 clk_out 输出上去了。结合上一张图我们可以看出 clk_in 就是 pclk 系统时钟,这样导致前面的分频没有任何作用,pclk 将会直接送到 pwm 发生器上作为 pwm 的时钟。这样不论你有没有设置分频,pwm 就只有系统时钟 pclk(21M)。这样 pwm 的信号周期无法修改。

反过来推到下,我们之前的实验设置周期 6ms 是得到的结果是 2.5ms,其实我们的分频系数是 2,由于时钟没有分频,导致我们的输出周期是 2.5ms,如果分频成功我们的周期就会是 2.5*2=5ms,同理,10ms 时的分频系数是 4。有兴趣的可以根据 sdk 提供的代码推到下,也可以用

1
uint33_t drv_pwm_get_clockdiv(pwm_handle_t handle, uint8_t channel)

这个函数查看分频系数,然后分析下。

我们直到了问题出在模块 gated_clk_cell 上了,我们定位到模块对应的 verilog 文件,查找到代码位于 pwm.v 文件的 4186 行例化了一个叫做 gated_clk_cell 的模块,我们双击 gated_clk_cell 进入内部,双击模块定位到了 common.v 文件的 66 行,代码如下:

1
2
3
4
5
6
7
8
9
10
`ifdef FPGA
assign clk_out = clk_in;
`else
Standard_Cell_CLK_GATE x_gated_clk_cell(
.CK (clk_in),
.SE (SE),
.EN (clk_en_bf_latch),
.Q (clk_out)
);
`endif

这下一目了然了,由于定义了 FPGA 这个量,导致 assign clk_out=clk_in; 而下面的模块没有实现,所以最主要原因就是定义了 FPGA 这个量,这个量在哪里定义的呢,就是在 wujian100_open_fpga_top.v 这个文件的最开头定义的(第 37 行):

wujian100_open_fpga_top.v

将这行用 // 注释,然后在 verdi 中点击 file 下选择 reload 设计,重新加载文件,我们再查看 geted_clk_cell 模块,得到如下图:

geted_clk_cell 改

这样 clk_in 不会直接送到 clk_out 输出了。

修改后用 vivado 重新分析综合生成 bit,下载到开发板上,我们发现输出的 pwm 的周期输出正确了。6ms,10ms 的周期也能生成了。

这里有一点注意,修改后源码用 vivado 建立工程,用 vivado 去 Synthesis,不要用官方方法,用 Synplify_pro 去 Synthesis。不然生成的 bit 下载开发板,输出的 pwm 周期会小一半。例如 11ms 周期只能输出 5ms。有兴趣的可以试下。

后续问题

查看这张结构图

PWM

仔细查看,不难发现,它的六个 pwm_gen 都是用的一个时钟源,都是 pclk 通过分频之后的时钟直接连接在了每一个 pwn_gen 的时钟上,六个时钟都是一个源,那么就会造成一个问题,他的六组 pwm 发生器只能同时产生一个频率(周期)的信号,例如 ch1 产生了 5ms,那个这时 ch2 也只能产生 5ms,没法产生 10ms 周期的 pwm,所有通道的周期都会被最后那个设置改成同一个频率,就是因为他们的时钟是同一个。这将怎么解决,有一个思路,就是每一个 pwm_gen 有各自的分频模块。怎么解决下一篇介绍。

本文转自 Verimake 论坛:
https://verimake.com/topics/122

用 Kindle 做树莓派的显示屏

作者 strongwong
2020年5月24日 17:28

一、工具

  1. 树莓派3b 一台
  2. Kindle PaperWhite 1 一台
  3. 无线键鼠一套
  4. 无线路由器 or USB 数据线

二、Kindle 越狱

首先,按照网上的教程把 Kindle 越狱。但是我手上这台 kindle 是 5.6.1.1 的最高版本了。不能直接越狱,需要先把其刷机,强行固件降级到 5.4.4 版本才能越狱。越狱前注意事项,建议遵守:

  • 确认 Kindle 已绑定亚马逊账号;
  • 确认 Kindle 电量处于充满状态;
  • 确认 Kindle 的特惠广告已关闭;
  • 确认 Kindle 已停用 设备密码和家长监护设置;
  • 确认 Kindle 已开启飞行模式处于离线状态。

2.1 固件降级

如果你的 KPW1 也和我一样是高于 5.4.4 的版本,需要先把固件降级到 5.4.4。可以前往书伴的 『固件大全』 下载 5.4.4 版本固件,然后把 Kindle 连接到电脑直到出现 Kindle 磁盘。把下载到的固件文件 update_kindle_5.4.4.bin 放到 Kindle 的根目录。不要拔出数据线,直接长按电源键直到开始更新。 没有问题继续下一步。

2.2 开始越狱

下载越狱文件『kindle-jailbreak-1.14.N.zip』。解压得到压缩包 kindle-5.4-jailbreak.zip,将其再次解压,得到一个名为 kindle-5.4-jailbreak 的文件夹。文件夹内有如下所示七个文件:

  • bridge.conf
  • bridge.sh
  • developer.keystore
  • gandalf
  • jb.sh
  • json_simple-1.1.jar
  • Update_jb_$(cd mnt && cd us && sh jb.sh).bin

把这些文件拷贝到 Kindle 根目录,安全弹出磁盘。在 Kindle 中依次点击『 首页 -> 菜单(屏幕右上角)-> 设置 -> 菜单(屏幕右上角)-> 更新您的 Kindle』。点击菜单后系统不会重启也不会有其它任何反应,在这期间不要有任何操作,直到屏幕下方出现 『 ** JAILBREAK ** 』的字样时,表示越狱已成功。

越狱

2.3 升级固件

最后前往书伴的 『固件大全』下载最新的 5.6.1.1 版本固件,将固件文件放到 Kindle 根目录,依次点击菜单『 首页 -> 菜单(屏幕右上角)-> 设置 -> 菜单(屏幕右上角)-> 更新您的 Kindle』。等待更新完毕,便得到一个有着最新版本固件的越狱了的 Kindle 系统,你可以进行安装MRPI、KUAL、kterm 、USBNetwork 等插件来完成接下来的操作。

三、安装插件

3.1 安装 MobileRead Package Installer (MRPI) —— 插件安装器

★ 安装步骤:

  1. 用 USB 数据线将 Kindle 连接到电脑上,直到出现 Kindle 磁盘;
  2. 解压缩下载到的 kual-mrinstaller-1.7.N-xxx.tar.xz 得到一个文件夹;
  3. 把文件夹内的 extensions 和 mrpackages 拷贝到 Kindle 根目录下。

3.2 安装 KUAL —— 插件程序启动器

KUAL (即 Kindle Unified Application Launcher),是一款插件启动器。安装KUAL之后,你可以下载或自己编写插件并通过KUAL启动。

★ 安装步骤:

  1. 用 USB 数据线将 Kindle 连接到电脑上,直到出现 Kindle 磁盘;
  2. 解压缩下载到的 KUAL-v2.x-xxx.tar.xz 得到一个文件夹;
  3. 在文件夹中找到 KUAL-KDK-2.0.azw2 拷贝到 Kindle 的 Documents 文件夹中;
  4. 弹出 Kindle 磁盘,打开 Kindle,可以看到一个名 Kindle LAUNCHER 的文档,正常情况下,点开此图标应显示菜单。

3.3 安装 Kterm

kterm 是 Kindle 的终端控制台,安装之后可以在 Kindle 进行 shell 的交互操作。

★ 安装步骤:

  1. 首先确保安装好了 KUALMRPI;
  2. 下载 kterm-kindle-2.6.zip,解压得到 Kterm 文件夹;
  3. 将 Kterm 整个文件夹,复制到 Kindle 根目录下的 extensions 文件夹中;弹出 kindle,重启即可。

点击 Kindle 中的 Kindle LAUNCHER -> Kterm,即可打开 shell 使用。如果 Kindle 和树莓派连接到了同一个 wifi 下的话,就可以直接通过 Kterm 窗口使用 SSH 登录到树莓派使用了。

1
$ ssh pi@10.0.0.31    # ssh $user@ip

接着输入账户密码就可以登录了。

Kindle Shell

3.4 安装 USBNetwork Hack – 无线管理 Kindle

USBNetwork 是一款 Kindle 插件,它可以让我们通过 WiFi 直接连接到 Kindle 并对其进行传送文件、管理等操作。可以通过 USB 连接 Kindle 和树莓派,通过把 Kindle 当成一块 USB 网卡,这样 Kindle 就可以和树莓派建立物理连接了。

★ 安装步骤:

  1. 首先确保安装了 KUAL 及其插件 MRPI
  2. 用 USB 数据线将 Kindle 连接到电脑上,直到出现 Kindle 磁盘;
  3. 解压缩下载到的 kindle-usbnet-0.22.N-xxx.tar.xz 压缩包,得到一个文件夹;
  4. 把文件夹内的 Update_usbnet_0.22.N_install_touch_pw.bin 拷贝到 Kindle 里 mrpackages 文件夹中;
  5. 弹出 Kindle 磁盘,点击 Kindle 中的 Kindle LAUNCHER,依次点击 Helper -> Install MR Packages
  6. 耐心等待 usbnet 安装,直到安装完成后 Kindle 重启完毕;
  7. 重启完成后,重新用 USB 数据线将 Kindle 连接到电脑上,直到出现 Kindle 磁盘;
  8. 在 Kindle 根目录可以看到『 usbnet 』文件夹,把此文件夹里名为『 DISABLED_auto 』的文件名改为『 auto 』;
  9. 然后在此文件夹里的『 etc 』文件夹中找到『 config 』,并用纯文本编辑器(不建议使用记事本,建议使用 VS Code 等代码编辑器)打开。找到『 USE_WIFI 』改为 true,『 USE_WIFI_SSHD_ONLY 』改为 false ,保存并关闭;
  10. 这样就可以通过 WIFIUSBNet 登录 Kindle 了。完成这些步骤之后,点击弹出/移除设备,断开 Kindle 与电脑的连接,重启 Kindle。

四、 配置树莓派,连接 Kindle

连接 Kindle 和 树莓派有两种方式,一种是把 Kindle 和树莓派连接到同一个 WIFI;另一种就是让 Kindle 作为 USB 网卡,通过 USBNet 的方式用 USB 数据线物理连接树莓派。但是想让 Kindle 作为屏幕使用,其实就是通过 screen、tmux 等软件共享屏幕。我这里安装的是 tmux。

4.1 安装 tmux

1
2
$ sudo apt update
$ sudo apt install tmux

~/.bashrc 文件最后添加一段脚本,自动运行 tmux。

1
2
3
4
5
6
7
# start tmux on console(not ssh)
if command -v tmux>/dev/null; then
if [[ "$(tty)" =~ /dev/tty ]] && [ -z "$TMUX" ]; then
# We're on a TTY and *not* in tmux
exec tmux -u
fi
fi

4.2 通过 WIFI 连接树莓派

点击 Kindle 中的 Kindle LAUNCHER -> Kterm,通过 SSH 连接树莓派;在这之前你需要先查看一下树莓派的 IP 地址是多少。

1
$ ssh pi@10.0.0.31 -t "tmux attach"

接着输入账户密码就可以登录了。

4.3 通过 USBNet 连接树莓派

首先打开 Kindle LAUNCHER -> USBNetwork -> Toggle USBNetwork,将模式切换到 USB Network 模式;当前状态可点击 USBNetwork Status 查看,成功的话提示USBNetwork enabled。
可以打开 kterm 使用 ifconfig 命令查看网卡的地址。注意这里默认是 192.168.15.x 网段,和网上常见的 192.168.2.x 网段不同。这个配置是可以在 usbnetconfig 文件修改的。

接下来将树莓派和 kindle 用 USB 数据线连接,输入 lsusb 命令,,如果看到 Linux-USB Ethernet/RNDIS Gadget 这样的设备说明连接成功。

编辑 /etc/network/interfaces 添加网卡配置:

1
$ sudo vi /etc/network/interfaces
1
2
3
4
5
6
7
8
9
10
# Kindle USBNet
allow-hotplug usb0
mapping hotplug
script grep
map usb0
iface usb0 inet static
address 192.168.15.1
netmask 255.255.255.0
broadcast 192.168.15.255
up iptables -I INPUT 1 -s 192.168.15.1 -j ACCEPT

重启树莓派,输入 ifconfig usb0 检查网卡是否连接成功,连接成功会显示一个 usb0 网卡设备。
这时你可以 ping 192.168.15.244,检查网络是否通畅。一切 ok 的话就就可以启动 Kterm 连接树莓派了。
打开 Kterm

1
$ ssh  pi@192.168.15.1 -t "tmux attach"

接着输入账户密码就可以登录了。

同屏

最后,如果你想要横屏,那么你就在打开 Kterm 之前,随便打开一本书,切换成横屏即可。切换回竖屏同理。

参考资料

https://www.shuyz.com/posts/phicomm_n1_with_kindle_as_screen/
https://www.fabiszewski.net/kindle-terminal/
https://bookfere.com/post/512.html
http://blog.yarm.is/kindleberry-pi-zero-w.html

关于 RISC-V 架构下 RTOS 的一些知识

作者 strongwong
2020年4月7日 16:12

0x00

之前的 blog 有介绍了一些,wujian100 的一些知识,包括综合、测试等。最近就想在 wujian100 上看看能不能移植一下比较常见的一些 RTOS (Real Time Operating System,实时操作系统)上去试试,比如 Free RTOS、RT-Thread等。结果发现这里还是有一些坑的。虽然 FreeRTOS 和 RTT 都支持 RISC-V 的芯片了,但是 wujian100 这个是 RISC-V “E” 基础架构,也就是 RV32E 就是 标准嵌入式扩展 指令集(这个版本降低了核心的开销,CPU 寄存器裁剪了一半,为 16 个)。但是 FreeRTOS 和 RTT 目前支持的版本都是 32 个寄存器的,对于任务或者说线程的上下文切换时对栈帧的操作还是有一些差异。然后呢也想对比一下 ARM 架构和 RISC-V 架构下嵌入式实时操作系统处理的一些区别,这里呢就想做一些的简单记录。

ARM 和 RISC-V 架构的区别

由于我是先学的 ARM 也相对了解一些,所以做什么总是想拿来和 ARM 对比一下,看看能不能套在 ARM 上,这也对自己理解也有一些帮助。缺点就是会产生一些先入为主的观念。

一个最简单的 RTOS 应该至少要实现一个多任务管理的功能,所以 RTOS 也可以叫实时多任务操作系统。那么一个简单的 RTOS 的核心就是怎么处理多任务或者说多线程之间的切换,这里我们也叫做上下文切换,所以上下文切换机制的实现就非常重要,这就要牵扯到不同架构的 CPU 会有不同的处理方式。

ARM 架构下 RTOS 的一般处理过程

这里以 Cortex-M3 为例,在 ARM 架构中有一组 特殊功能寄存器组,很多时候就是专门留给 OS 使用的。其中由 CONTROL[0:1] 寄存器来定义 CPU 的特权等级。这里就要提到在 ARM 架构中的双堆栈机制,在 CM3 内核中支持两个堆栈,一个是 MSP(主堆栈指针)指向的主堆栈和 PSP(线程堆栈指针)指向的线程堆栈。通过配置 CONTROL 寄存器的两个位来选择特权级别和使用不同的堆栈指针(还有一个骚操作就是从异常返回时修改 LR 的 bit1bit2 也可以切换模式和堆栈,我们可以在很多开源的 RTOS 中见到)。这样通过这两个寄存器的配置就可以分开对待用户程序和系统程序,避免因用户级程序的问题对系统造成危害。同时在出入异常处理时这两个堆栈指针是通过硬件自动切换的,对于现场的保存就不需要软件来处理了。而且在 Handler 或者说异常中只能使用 MSP(主堆栈指针)。

CONTROL[0] CONTROL[1] 组合 模式
特权选择 堆栈指针选择
0 0 特权级+MSP Handler 模式和 Kernel(OS)
0 1 特权级+PSP 线程模式
1 0 用户级+MSP 错误用法
1 1 用户级+PSP 线程模式

由于有了这样的机制,在 RTOS 中对于任务切换就带来了很多便利,通常情况下都是通过 SVCall(即 SVC,System service Call,系统服务调用)PendSV(Pendable request for system serivce,可挂起系统调用)这两个异常来完成系统特权和任务上下文的切换。当然也可以先不考虑特权模式和用户模式,那么就可以仅通过 PendSV 异常来完成任务上下文切换即可。这里可以参考一下 FreeRTOS 的处理代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// SVCHandler 进行任务切换
__asm void vPortSVCHandler( void )
{
extern pxCurrentTCB; // 外部参数,当前任务控制块指针

PRESERVE8

ldr r3, = pxCurrentTCB // 加载 pxCurrentTCB 的地址到 r3
ldr r1, [r3] // 加载 pxCurrentTCB 到 r1
ldr r0, [r1] // 加载 pxCurrentTCB 指向的值到 r0, 即当前第一个任务的任务栈栈顶指针
ldmia r0!, {r4-r11} // 以 r0 为基地址,将栈里面的内容加载到 r4-r11 寄存器,同时 r0 会递增
msr psp, r0 // 将 r0 的值,即任务栈指针更新到 psp
isb
mov r0, #0 // 将 r0 的值,设置为 0
msr basepri, r0 // 将 basepri 寄存器设置为0,即所有的中断都没有被屏蔽

//骚操作
orr r14, #0x0d // 当从 SVC 中断服务退出前,通过向 R14 最后4位按位或上0x0d,
// 使得硬件在退出时,使用进程堆栈指针 PSP 完成出栈操作并返回后进入线程模式、返回 Thumb 状态
// r14 的 bit1 : 0 PSP 1 MSP;bit2: 0 特权模式 1 用户模式

bx r14 // 异常返回,这个时候栈中的剩下内容将会自动加载到 CPU 寄存器
// xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务形参) 同时 PSP 的值也将更新,即指向任务栈的栈顶
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
__asm void xPortPendSVHandler(void)
{
extern pxCurrentTCB; // 外部参数,当前任务控制块指针
extern vTaskSwitchContext; // 外部函数,当前任务切换函数

PRESERVE8

// 当进入 PendSVC Handler 时,上一个任务运行环境,即:
// xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务形参),这些将自动保存到任务栈中,剩下的r4-r11需要手动保存
// 获取任务栈指针到 r0
mrs r0, psp
isb

ldr r3, =pxCurrentTCB // 加载 pxCurrentTCB 的地址到 r3
ldr r2, [r3] // 加载 pxCurrentTCB 到 r2

stmdb r0!, {r4-r11} // 将 CPU 寄存器 r4-r11 的值存储到 r0 指向的地址
str r0, [r2] // 将任务栈的新的栈顶指针存储到当前任务TCB的第一个成员,即栈顶指针

stmdb sp!,{r3,r14} // 将 r3 和 r14 临时压入堆栈,因为即将调用函数
// 调用函数时,返回地址自动保存到 r14 中,导致 r14 的值会被覆盖,所以 r14 的值需要入栈保护
// r3 保存的当前激活的任务 TCB 指针( pxCurrentTCB ),函数调用后会用到,因此也需要入栈保护

mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY // 进入临界段
msr basepri, r0 // 屏蔽所有中断
dsb
isb
bl vTaskSwitchContext // 调用函数 vTaskSwitchContext,寻找新的任务运行,通过使变量 pxCurrentTCB 指向新的任务来实现任务切换
mov r0, #0 // 退出临界段
msr basepri, r0
ldmia sp!, {r3,r14} // 恢复 r3, r14

ldr r1, [r3]
ldr r0, [r1] // 当前激活的任务 TCB 第一项保存了任务堆栈的栈顶指针,现在栈顶值存入了 r0
ldmia r0!,{r4-r11} // 出栈
msr psp, r0
isb
bx r14 // 异常发生时,R14 中保存异常返回标志,包括返回后进入线程模式还是处理器模式
// 使用 psp 堆栈指针还是 msp 堆栈指针,当调用 bx r14 指令后,硬件会知道要从硬件返回
// 然后出栈,这个时候堆栈指针 psp 硬件指向了 新任务堆栈的正确位置
// 当新任务的运行地址被出栈到 pc 寄存器后,新的任务也会被执行
nop
}

这两个汇编函数就完成了 ARM 架构下的任务切换机制。其实对于任务上下文切换就是任务现场的保存和恢复,这个现场就是当前的 CPU 运行状态,也就是 CPU 各个寄存器的状态的保存与恢复。这其中也包括很重要的栈帧切换。当然仅仅靠这两个函数也是不完全可靠的,还有一些临界段的处理函数来共同保证任务的安全切换。

RISC-V 架构下的 RTOS 一般处理过程

在 RISC-V 架构中,也有不同的特权级别,目前主要定义了三种特权级别,分别是机器模式(Machine Mode,M-mode)、监管模式(Supervisor Mode,S-mode)和用户模式(User Mode,U-Mode), 通过 CSRs(control and status registers,控制状态寄存器)bit11bit12(即 MPP 位)两个位的不同编码来实现不同特权模式的切换,在不同特权模式下都有单独的 CSRs。这里需要说明的是我这个 MPP 指的是 Machine-Level CSRs 中 mstatus 寄存器(即 M-mode status register)的控制位。

Level MPP[12:11] 模式 简写
0 0 0 User/Application U
1 0 1 Supervisor S
2 1 0 Reserved(Hypervisor) (保留)
3 1 1 Machine M

但是一个 RISC-V 处理器的实现并不要求同时支持这三种特权级,接受以下的一些实现组合,降低实现成本:

Number of levels Supported Modes Intended Usage
1 M Simple embedded systems
2 M,U Secure embedded systems
3 M,S,U Systems running Unix-like operating systems

上图中可以看出,这三种模式只有 M-mode 是必须要实现的,其它两种模式是可选的。M-mode 是 RISC-V 中 hart(hardware thread,硬件线程)可以执行的最高权限模式。在 M 模式下运行的 hart 对内存,I/O 和一些对于启动和配置系统来说必要的底层功能有着完全的使用权。因此它是唯一所有标准 RISC-V 处理器都必须实现的权限模式。实际上简单的 RISC-V 微控制器仅支持 M 模式。

好了,上面说的是特权模式和 ARM 的区别,下面就是堆栈指针的区别。上文已经提到 ARM 中有 MSP 和 PSP 之分,且在 handler 中只能使用 MSP,也就意味着 OS 和线程模式使用不同的栈。并且出入异常的栈帧切换由硬件完成。

而在 RISC-V 架构处理器中,没有区分异常、中断和线程模式使用的栈帧,在进入和退出中断处理模式时没有硬件自动保存和恢复上下文(通用寄存器)的操作,因此需要软件明确地使用(汇编语言编写的)指令进行上下文的保存和恢复。并且还要区分 ecall(environment call for U/S/M-mode,不同特权模式下的环境调用异常)。

所以 RISC-V 这一块的处理要复杂一些,有大量的 RISC-V 汇编,具体的代码我就不贴了,有兴趣的可以去看一下 FreeRTOS 的源码。链接:https://github.com/FreeRTOS/FreeRTOS-Kernel/blob/master/portable/GCC/RISC-V/portASM.s

下表是 RISC-V RV32I 基础指令集寄存器结构,但 RV32E 基础指令集只有 x0-x15

Register ABI Name Description Saver
x0 zero Hard-wired zero -
x1 ra Return address Caller
x2 sp Stack pointer Callee
x3 gp Global pointer -
x4 tp Thread pointer -
x5-7 t0-2 Temporaries Caller
x8 s0/fp Saved register/Frame pointer Callee
x9 s1 Saved register Callee
x10-11 a0-1 Function Arguments/return values Caller
x12-17 a2-7 Function arguments Caller
x18-27 s2-11 Saved registers Callee
x28-31 t3-6 Temporaries Caller

上表中虽然对各个寄存器有了一些描述,在 RISC-V 指令集中并没有指定专用的堆栈指针或子程序返回地址链接寄存器等,事实上指令编码允许将任何 x 寄存器用于这些目的。 但是,标准软件调用约定使用寄存器 x1 来保存呼叫的返回地址,而寄存器 x5 可用作备用链接寄存器。 标准调用约定使用寄存器 x2 作为堆栈指针。硬件可能会选择加速使用 x1 或 x5 的函数调用和返回。(不知道这段 Google 翻译的描述是否准确,大家可以去阅读《riscv-spec-20191213》的 2.1 节原文参考)

在 wujian100 RISC-V 开源平台上实现简单的任务调度系统

在了解了上面的一些区别后,我准备尝试移植 FreeRTOS 或者 RT-Thread 到 wujian100 上试试,但是我发现它们大多是只支持了以 RV32I 为基础指令集的处理器。而 wujian100 是 E902,是 RV32E 基础指令集,在底层汇编的处理上有一些不同,可能还要做一些修改。所以我就想试着把我之前学习 FreeRTOS 时,实现的仅有任务调度功能的极简版 FreeRTOS 放上去试试,因为代码量比较少。

接下来,我就尝试在 wujian100 开源的 SDK 中移花接木,把我自己这个极简的小操作系统移植上去。在仔细翻阅了 wujian 开源的代码后发现他们这里提供了一个 AliOS 的内核,叫 rhino 内核。在他们这个内核的底层是有实现一些上下文切换的代码的,于是我就基于这个底层把我的上层接上去。当然这过程中还要修改很多东西,这里就不一一详述,直接看这段汇编代码是怎么处理的,这里我已经做了一些修改,我的两个小任务也转起来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#define MSTATUS_PRV1 0x1880

.global cpu_intrpt_save
.type cpu_intrpt_save, %function
cpu_intrpt_save:
csrr a0, mstatus // 读控制状态寄存器,写入 a0,并返回到 psr 返回值中,psr 是外部定义的一个变量,恢复时会使用
csrc mstatus, 8 // 将控制状态寄存器清零。清零对应的标志位,该语句即为清除 MIE ,即禁止全局中断使能。就是禁用中断
ret

.global cpu_intrpt_restore
.type cpu_intrpt_restore, %function
cpu_intrpt_restore:
csrw mstatus, a0 // a0 是传进来的参数,即上一次保存的控制状态寄存器的值,对于 a0 中每一个为 1 的位,把 mstatus 中对应的位进行置位
ret

.global cpu_task_switch
.type cpu_task_switch, %function
cpu_task_switch: // 主动任务切换调度
la a0, g_intrpt_level_1 // g_intrpt_level_1 是一个全局变量,用于保存当前中断嵌套的层级;这里是将其地址加载到 a0 中
lb a0, (a0) // 将 a0 地址的数据加载到 a0 中
beqz a0, __task_switch // beqz 是对于零时的分支指令,如果等于零,就执行 __task_switch 函数,也就是意味着当前没有中断嵌套

la a0, pxCurrentTCB // 如果不等于零,即有中断嵌套,就进行下面的操作;加载 pxCurrentTCB 的地址到 a0,即获取当前任务指针
la a1, g_ReadyTasksLists // 加载 g_ReadyTasksLists 的地址到 a1,即获取当前最高优先级的就绪任务指针
lw a2, (a1) // 加载就绪任务指针到 a2 (lw 指令读取一个字,即4个字节的数据 到 a2
sw a2, (a0) // 将 a2 的低4个字节存储到 a0(即将就绪任务指针放到当前任务)

ret

.global cpu_intrpt_switch
.type cpu_intrpt_switch, %function
cpu_intrpt_switch: // 中断中的任务切换 操作和上面类似
la a0, pxCurrentTCB
la a1, g_ReadyTasksLists
lw a2, (a1)
sw a2, (a0)

ret

.global cpu_first_task_start
.type cpu_first_task_start, %function
cpu_first_task_start: // 第一次进入任务时是不用返回的
j __task_switch_nosave

.type __task_switch, %function
__task_switch: // 任务切换函数
addi sp, sp, -60 // 规划保存数据需要的栈帧大小
// 保存现场,将寄存器的数据保存到栈帧中
sw x1, 0(sp)
sw x3, 4(sp)
sw x4, 8(sp)
sw x5, 12(sp)
sw x6, 16(sp)
sw x7, 20(sp)
sw x8, 24(sp)
sw x9, 28(sp)
sw x10, 32(sp)
sw x11, 36(sp)
sw x12, 40(sp)
sw x13, 44(sp)
sw x14, 48(sp)
sw x15, 52(sp)

sw ra, 56(sp)

la a1, pxCurrentTCB // 将当前任务控制块指针地址,加载到 a1
lw a1, (a1) // 将任务控制块指针地址加载到 a1
sw sp, (a1) // 将栈指针加载到当前任务控制块指针地址

__task_switch_nosave: // 第一次进入任务入口,接下来切换任务指针
la a0, g_ReadyTasksLists
la a1, pxCurrentTCB
lw a2, (a0)
sw a2, (a1)

lw sp, (a2)

/* Run in machine mode */
li t0, MSTATUS_PRV1
csrs mstatus, t0 // 将对于 t0 对应为 1 的每一位置位,即 mpp 设置为 11,machine mode 运行;mpie 置位,用于保存发生异常时 mie 的值;即切换到 M-mode

lw t0, 56(sp) // 将 56(sp) 低 4个字节的数据加载到 t0,即返回地址
csrw mepc, t0 // 将 t0 写入 mepc 这里需要注意的是,栈区的数据,在任务初始化的时候就要初始化好,包括第一次启动
// 加载栈帧数据
lw x1, 0(sp)
lw x3, 4(sp)
lw x4, 8(sp)
lw x5, 12(sp)
lw x6, 16(sp)
lw x7, 20(sp)
lw x8, 24(sp)
lw x9, 28(sp)
lw x10, 32(sp)
lw x11, 36(sp)
lw x12, 40(sp)
lw x13, 44(sp)
lw x14, 48(sp)
lw x15, 52(sp)

addi sp, sp, 60
mret // M-mode 特有指令,返回时将 PC 指针设置为 mepc,将 mpie 复制到 mie 恢复之前的中断设置,并将特权模式设置为 mpp 中的值;这里就可以完成特权模式的切换(M-U or U-M)

.global Default_IRQHandler
.type Default_IRQHandler, %function
Default_IRQHandler: // 异常、中断处理,这里也需要保存现场,处理类似
addi sp, sp, -60

sw x1, 0(sp)
sw x3, 4(sp)
sw x4, 8(sp)
sw x5, 12(sp)
sw x6, 16(sp)
sw x7, 20(sp)
sw x8, 24(sp)
sw x9, 28(sp)
sw x10, 32(sp)
sw x11, 36(sp)
sw x12, 40(sp)
sw x13, 44(sp)
sw x14, 48(sp)
sw x15, 52(sp)

csrr t0, mepc
sw t0, 56(sp)

la a0, pxCurrentTCB
lw a0, (a0)
sw sp, (a0)

la sp, g_top_irqstack

csrr a0, mcause // 读取异常类型
andi a0, a0, 0x3FF
slli a0, a0, 2
// 处理异常
la a1, g_irqvector
add a1, a1, a0
lw a2, (a1)
jalr a2
// 退出异常,恢复
la a0, pxCurrentTCB
lw a0, (a0)
lw sp, (a0)

csrr a0, mcause
andi a0, a0, 0x3FF

/* clear pending *///清除挂起的异常
li a2, 0xE000E100
add a2, a2, a0
lb a3, 0(a2)
li a4, 1
not a4, a4
and a5, a4, a3
sb a5, 0(a2)

/* Run in machine mode */
li t0, MSTATUS_PRV1
csrs mstatus, t0

lw t0, 56(sp)
csrw mepc, t0

lw x1, 0(sp)
lw x3, 4(sp)
lw x4, 8(sp)
lw x5, 12(sp)
lw x6, 16(sp)
lw x7, 20(sp)
lw x8, 24(sp)
lw x9, 28(sp)
lw x10, 32(sp)
lw x11, 36(sp)
lw x12, 40(sp)
lw x13, 44(sp)
lw x14, 48(sp)
lw x15, 52(sp)

addi sp, sp, 60
mret

除了上面的汇编部分,还有几个主要函数如下,代码工程我后面整理好会上传到我的 Github 上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

void TaskSwitching_example(void)
{
prvInitTaskLists();

Task1_Handle = xTaskCreateStatic( Task1_Entry,
"Task1_Entry",
TASK1_STACK_SIZE,
NULL,
1,
Task1Stack,
&Task1TCB );
// 核心就是插入函数 vListInsert, 将任务插入到就绪列表中
vListInsert(&pxReadyTasksLists[1], &Task1TCB.xStateListNode);

Task2_Handle = xTaskCreateStatic( Task2_Entry,
"Task2_Entry",
TASK2_STACK_SIZE,
NULL,
2,
Task2Stack,
&Task2TCB );
vListInsert(&pxReadyTasksLists[2], &Task2TCB.xStateListNode);
vTaskStartScheduler(); //去启动第一个任务
}

void vTaskSwitchContext(void)
{
// 轮流切换两个任务,我这里任务暂时是手动切换的,没使用优先级
if( pxCurrentTCB == &Task1TCB)
{
g_ReadyTasksLists[0] =& Task2TCB;
}
else
{
g_ReadyTasksLists[0] =& Task1TCB;
}
}

void wjYIELD(void)
{
PSRC_ALL();
portDISABLE_INTERRUPTS();
vTaskSwitchContext();
cpu_task_switch();
portENABLE_INTERRUPTS();
}

// 第一个任务函数 Task1 入口函数 ;task2 和 task1 一样
void Task1_Entry(void *p_arg)
{
for(;;)
{
flag1 = 1;
printf("flag1 = %d \n", flag1);
delay( 100 );
// vTaskDelay( 20 );
flag1 = 0;
printf("flag1 = %d \n", flag1);
delay( 100 );
// vTaskDelay( 20 );
wjYIELD(); // 注意,这里是手动切换任务
}
}

int main(void)
{
TaskSwitching_example();
return 0;
}

好了,差不多就这些了。
wujian_CamelOS

踩坑总结

通过这次研究,明白了 ARM 和 RISC-V 架构上的异同,加深了自己对两种架构的理解,相信对以后的学习也更加有帮助。
还有就是 wujian100 的开源资料中并没有提供特权架构的相关文档,异常和中断向量表规划也没有具体的说明文档,目前有限的文档中只介绍了外设 IP 的说明,所以在后续的软件开发增加了很多障碍。只有去扒他们提供的 SDK 中的源代码,通过源码来了解他们的架构,还有一点就是他们提供的代码及资料和阿里体系的东西相对耦合或者说兼容。跟开源社区现有的资料和体系不能很好融合。

参考资料

FPGA 数字图像处理联合仿真平台的搭建及使用举例

作者 strongwong
2019年9月2日 21:25

前言

随着物联网技术的不断发展,对边缘计算的性能要求愈发苛刻,传统的 IoT 级别的 SoC 或者 MCU 难以应对诸如图像识别的场景需求,因此 FPGA + MCU 或 FPGA + SoC 的异构架构愈来愈多地应用于上述场景中,以满足相关需求。(以上这段话毫无意义,只是为了凑字数,另外为了纪念第三次憾负的开场白)

当前 FPGA 发展火热,众多开发者使用 FPGA 来完成部分图像处理的部分功能,但众所周知纯 FPGA 开发(或者数字 IC 设计)与常见的 ARM A 系列(with OS)或者桌面端 PC 在图像处理开发过程中最大的差别在于过程的可视化。开发过程中的可视化多少会影响到开发者对于图像处理的判断与取舍,简而言之,如果开发者都不知道经过了这一步处理后图像变成了什么样,那还如何继续接下来的步骤。因此分享交流一下在本次车牌识别设计中我们所用到的数字图像处理的联合仿真平台(注:本文仅仅介绍平台使用方式,不介绍过程中涉及到的数字图像处理原理),如果有更好的方式,希望大家可以留言给予建议~

首先是我们所需要的软件工具:

本次我们的开发环境为 Win10 所用到的软件工具分别为:Modelsim(10.5B), MATLAB(R2019a)

然后简述一下我们的操作过程(以图片 RGB 色域转换 YCbCr 色域处理为例),整体过程可简述如下:通过 MATLAB 将图片文件转换为 .txt 格式文件,使用 Verilog 读取相应文件后完成相应图像处理操作后输出为 .txt 文件,再次使用 MATLAB 将输出的 .txt 格式文件转换为图片文件并查看。以上步骤的意义在于不仅可以实现数字图像处理的单步可视化,还可以同原始的 MATLAB 对应的处理效果进行对比。(后期我们也会更新,直接使用 Verilog 完成图像读取并输出结果查看的方法)

首先我们通过以下程序(亦可查看附带工程中的 IMG2TXT.m 文件)在 MATLAB 端将图片生成 R,G,B 色域对应的 .txt 格式文件。并同时通过 MATLAB 生成 YCbCr 色域下三个分量的图像予以显示。
其关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
clear all
close all
clc
img = imread('test.jpg');
[a,b,c]= size(img);
R1=img(:,:,1);
G1=img(:,:,2);
B1=img(:,:,3);

fidR1= fopen('testR.txt','w');
fidG1= fopen('testG.txt','w');
fidB1= fopen('testB.txt','w');
for i=1:a
for j= 1:b
fprintf(fidR1,'%d\n',R1(i,j)); %frame1
fprintf(fidG1,'%d\n',G1(i,j));
fprintf(fidB1,'%d\n',B1(i,j));
end
end
fclose(fidR1);
fclose(fidG1);
fclose(fidB1);

RGB 色域文件生成

生成对应 RGB 色域 .txt 格式文件如下图所示:

RGB.txt

生成 YCbCr 色域对应分量图片(详见工程文件 YCbCr.m ),如下图所示:

YCbCr

Verilog 处理

然后在我们的 Modelsim 工程中对 .txt 格式文件进行读取(亦可查看工程中附带的 imread.v 文件),进一步的完成图像处理对应的操作后,输出对应 .txt 格式文件(亦可查看工程中附带 imwrite.v 文件)。
其 txt 读取关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
initial begin
fR = $fopen("testR.txt","r"); // read in testR.txt
fG = $fopen("testG.txt","r");
fB = $fopen("testB.txt","r");
if(fR == `NULL || fG == `NULL || fB == `NULL) begin
$display("data_file handle was NULL");
$finish;
end
else begin
$fscanf(fR,"%d;\n",R);
$fscanf(fG,"%d;\n",G);
$fscanf(fB,"%d;\n",B);
end
end

always @(posedge pixel_clk or negedge reset_n) begin
if(reset_n == 0) begin
R = 8'd0;
G = 8'd0;
B = 8'd0;
end
else if(de) begin
if(fR != 0 && fG != 0 && fB != 0) begin
$fscanf(fR,"%d;\n",R);
$fscanf(fG,"%d;\n",G);
$fscanf(fB,"%d;\n",B);
$display("time=[%d],%d,%d,%d",$realtime,R,G,B);
end
end

end

完成处理后生成 txt 关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
initial begin
fR = $fopen("R.txt","w");
fG = $fopen("G.txt","w");
fB = $fopen("B.txt","w");
if(fR == `NULL || fG == `NULL || fB == `NULL) begin
$display("can not open R.txt or G.txt or B.txt");
$finish;
end
end

always @(posedge pixel_clk or negedge reset_n) begin
if(de) begin
$display("////////////////////////////////////");
$display("time=[%d],%d,%d,%d",$realtime,R,G,B);
$fwrite(fR,"%d\n",R);
$fwrite(fG,"%d\n",G);
$fwrite(fB,"%d\n",B);
end
end

处理结果

最终在 MATLAB 端将仿真生成输出的 .txt 文件转换还原为图片(亦可查看工程中附带的 TXT2IMG.M 文件)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
clear all
close all
clc

imgori = imread('test.jpg');
[a,b,c]= size(imgori);

img1R = uint8(textread('R.txt','%u'));
img1G = uint8(textread('G.txt','%u'));
img1B = uint8(textread('B.txt','%u'));

img1(:,:,1) = reshape(img1R,[b,a]);
img1(:,:,2) = reshape(img1G,[b,a]);
img1(:,:,3) = reshape(img1B,[b,a]);

img=flipdim(img1,1);

figure,
subplot(221),imshow(imrotate(img,-90)),title('YCbCr');
subplot(222),imshow(imrotate(img(:,:,1),-90)),title('Y');
subplot(223),imshow(imrotate(img(:,:,2),-90)),title('Cb');
subplot(224),imshow(imrotate(img(:,:,3),-90)),title('Cr');

由于常见的 FPGA 数字图像处理为了减少数据读取对于缓存资源的消耗,通常其处理步骤在视频流完成,因此本次工程中我们在 VGA 模拟时序中完成相应操作。完成仿真如波形下图所示:

Waveform

生成结果 .txt 文件如下图所示:
Outfile

将生成结果 .txt 文件放回 MATLAB 工程路径下最终实现效果如下图所示:

Outfile of FPGA

MATLAB vs HDL数字图像处理 结果对比

将图像放大后,可以明显发现通过数字思想处理得到的图片结果相较于 MATLAB 直接转换得到的结果存在一定的差距,Cr 分量下表现的最为明显,具体原因在此不具体展开分析。其对比如下图(注:左侧为 MATLAB 处理结果右侧为 verilog 处理结果

Matlab vs HDL

之后我们将逐步介绍本次我们 ARM 杯车牌识别系统的每个实现过程~

并公开全套设计源码!!!

举例部分全部代码详见 GitHub 仓库:https://github.com/strongwong/FPGA-DIP

By Ricky

拆解一个很有意思的开关

作者 strongwong
2019年8月30日 20:54

linptech-sw

最近公司买了一个很有意思的开关,说是叫无源无线开关。无线嘛,就是没有电线连接,是无线的,好像是 433 无线模块通讯的。所谓无源就是没有电池或者其他直接的电源供电了,是一个自发电的开关。就是上图这个东东,我很是好奇,很想知道他这个自发电的开关内部是个什么样子,那我们拆开看看吧~ 嘿嘿~~

趁着下班了,大家都不在,我一个人开始在公司开拆!

外壳

这个无源无线开关和其他普通开关从外观上来看都差不多。它的塑料外壳和普通开关一样,轻轻撬开即可。打开外壳我们可以看到一根铜线,这应该就是用于无线通讯的天线了。卸下按键壳,下面就是整个开关的主体模块了。开关的主体很牢固的卡在底座上,只能硬撬下来了。

开关主体

开关主体拿下来还是可以正常使用的,当我把主板上的壳子拆下来后,开关就不可以自发电了。主板的正面有 5 个电容按键,具体功能不得而知,可能是备用、兼容或者是为调试预留的?

拿下电容按键,看到主板背面线圈的一瞬间,相信中学物理还可以的朋友应该都能反应过来,这个开关的自发电原理应该就是咱们学过的电磁感应原理啊!通过线圈切割磁感线然后产生电能供应给其他电路使用。我迫不及待地卸下主板,想一探究竟,看看是不是我想的这样。但是主板和底座之间有一些机械机构互相钳制,费了好大力气才将主板卸下。

磁铁簧片结构
主体拆解图

卸下主板后,我们看到底座上确实有一个由三个铆钉固定的磁铁。哈哈,果然是这样。在主体的壳子上有一个弹簧,怼着底座上的拨动机构,在开关按下的时候,拨动机构带动簧片拨动磁铁,磁铁的移动在线圈那端是有限位的,磁铁在一瞬间移动就相当于线圈切割了一次磁感线,从而产生电能给到其他负载。

  • 拨杆机构细节图,挺有意思的一个结构。
    拨杆机构

然后来详细看一下主板:

MainBoard

在主板上主要有一颗 TI 的 CC115L 超值射频发射芯片、一颗 ST 的 stm8s003f3p6 8 位单片机、一个线圈以及电能采集储能电路。 stm8s003+CC115L/CC113L 的搭配在无线智能家居等场景好像应用很多。具体的芯片资料大家直接看厂商的 datasheet 吧,这里就不多做介绍了。

我仔细研究一下这个磁感线圈电能采集存储电路,主要有 5 个肖特基二极管,一个稳压二极管,一个 PMOS和一个 NMOS 丝印分别是 1GM 、2GM ,以及一个丝印为 A18F的未知芯片,没有找到直接对应的芯片型号,但根据电感来判断应该是一个 DC-DC 转换器,至于应该是降压还是升压就不太清楚了,因为拆开后就没有办法拨动开关产生电能,来测试对应点的电位了。我个人觉得这个线圈应该生产不了多大的电压,所以这里可能是一个升压的。通过按压,电磁线圈产生电能后,通过前级的电路采集后,到电感这里短暂存储,供控制器和射频芯片短暂地工作几十毫秒把控制信号发射出去,这样就可以控制接收端的各种设备了。简单理解了这部分的电路后,我简单绘制了一下这部分的原理图,不一定完全正确,但大体上应该是对的。有兴趣的朋友可以帮我看看,如果有什么错误的地方欢迎给我留言指正。

能量采集电路

好了!这一次的拆解就到这里啦,我要赶快装回去了,不能让大家发现!(逃

简单总结一下吧!

  • 这个无源无线开关还是很 nice 的,不用布线,不用电池,自发电,节能环保!发电线圈的设计也不失为一种新的低功耗方案,在一些更小体积的应用场景可能不太合适吧。
  • 神奇的事情背后都有基本的科学知识,中学的物理知识就这样实际运用在我们的日常生活中啦!
  • 还有拆东西是一件很爽的事情!哈哈哈!

Have fun~

Windows10 环境下搭建 RISC-V 调试环境

作者 strongwong
2019年3月11日 10:32

环境要求

软件环境

硬件要求

  • 目标 RISC-V 芯片
  • 调试器: J-Link,FT2232 或其他含有标准 JTAG 接口的调试器

配置环境

以下内容来自 ChenRQ 同学!

启动 IDE: GNU MCU Eclipse IDE for C/C++ Developers,Eclipse 基于 Java 开发,运行时需要 Java 的运行环境(JRE),如没有请自行安装。

新建一个工程

新建工程

工程类型选择 Hello World RISC-V C Project,工具链选择 RISC-V Cross GCC 如下所示

项目类型设置

使用默认配置 next 至 GNU 工具链选择, 文件路径应指向为您的 RISC-V Embedded GCC 目录下的 bin 文件夹,如下图所示

工具链路径设置

完成后点击 Finish 由此完成工程项目的创建。创建完成后,我们可以看到还有一个报错, 如下图所示

工程创建完成界面

因此我们还需要继续对项目进行配置。

工程相关配置

对工程右键选择 “properties”,在 MCU 选栏中配置 Build Tools Path,该路径应指向您的 Build Tools 目录下的 bin 文件夹,如下图所示

Build Tools 路径配置

继续配置 OpenOCD Path,路径为 OpenOCD 目录下的 bin 文件夹,如下图所示,并点击apply

OpenOCD 路径配置

再配置 RISC-V Toolchain Path(若新建项目时已配置过工具链路径,可以跳过此步骤),配置路径与工程建立时选择的工具链路径相同。

配置编译和链接选项

继续在 “properties” 窗口中,选择 C/C++ Build 中的 settings,在 Tool Settings 中 Target Processor 进行配置,由于是 RISC-V,因此架构 (architecture) 选择 RV32I 并勾选乘法指令拓展(RVM),原子指令拓展(RVA)及压缩指令拓展(RVC),ABI 调用选择 ILP32(表明为 32 位架构无浮点型,PS: ilp32f 和 ilp32d 则分别表示单精度浮点和双精度浮点),Code Model 选择 Medium Low,勾选整数除法指令(-mdiv),如下图所示,并点击 apply

Target Processor 配置

继续配置 Optimization,Level 选择 -O2,如下图所示,并点击 apply

Optimization 配置

继续配置 Debugging,Level 选择 -g,如下图所示,并点击 apply

Debugging 配置

在 Tool Settings 中选择 GNU RISC-V Cross C Linker 的 General,点击右上角+号,弹窗中选择 Workspace 选择路径您芯片对应的 lds 文件,用于对地址区间进行约束,如下图所示

链接脚本配置

勾选对应选项后,点击 apply,如下图所示

链接选项配置

在 Tool Settings 中选择 GNU RISC-V Cross C Linker 的 Miscellaneous 进行勾选,如下图所示,并点击 apply

链接杂项配置

添加您的工程汇编类型的头文件路径,方法如下图所示

添加工程汇编头文件目录

添加您的工程 C/C++ 类型的头文件路径,方法如下图所示

 添加工程 C/C++ 头文件目录

待续…

后续测试进行中…

使用汇编实现 pc 和 sp 的保存及恢复操作

作者 strongwong
2019年1月13日 19:32

前言

在 ARM Cortex 系列的芯片中本来就有一套保护现场的机制,例如当产生了一个中断时,会自动将当前寄存器的值入栈,并在 lr(r14) 寄存器中保存将要返回的 pc 值,在中断服务程序执行完成后将 pc 恢复到之前的位置。如果在执行中断服务程序的时候又发生了优先级更高的中断,也就是说发生了中断嵌套,这是将再次进行现场保护,同时 lr 值会被压栈(上一次的 pc ),新的 lr 生成。

但是在一些场景下,这样的机制就不太好用了,比如说要进入 sleep 模式 cpu 掉电了,想要恢复到掉电前的状态。这样的话就需要我们自己实现保护现场了,下面就来简单介绍一下我的实现。

硬件及 IDE 环境

  • 硬件: Cortex-M3 FPGA 开发板
  • IDE: IAR 8.22.1

在进行 FPGA 验证之前,还跑了 RTL 的仿真,从仿真波形的结果来看也是正确的。

c 文件

现场保护主要就是保存当前的运行状态,在从 sleep 模式唤醒后将保存的状态恢复,使 cpu 回到到 sleep 之前的状态。在我们这里最主要的是保存 pc 和 sp 的值,cpu 唤醒之后恢复 pc 和 sp 就好,所以我们需要将进入 sleep 之前的 pc 和 sp 保存即可。

在进入 sleep 模式中,虽然 cpu 掉电了,但是 SRAM 还是维持着的,所以我们可以使用一个全局变量(存储在 SRAM 中)来保存 pc 和 sp 的值。

“xxx.c” 文件中部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 函数及变量的声明和引用
extern void Save_PC_SP(void)
extern void Restore_PC_SP(void)
extern u32 pc_save;
extern u32 sp_save;

//······
// 在执行 sleep 指令(WFI/WFE)之前保存 pc、sp
Save_PC_SP(); // 保存 pc 和 sp
__WFI(); // 睡眠
__NOP();
__NOP();
__NOP();

//······

s 汇编文件

“xxx.s”文件中的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
;函数及变量的声明和引用
PUBLIC Save_PC_SP
PUBLIC Restore_PC_SP

IMPORT pc_save
IMPORT sp_save

;唤醒后判断的代码
THUMB

PUBWEAK Reset_Handler
SECTION .text:CODE:REORDER:NOROOT(2)
Reset_Handler
LDR R0, =0x4001f000
LDR R1, [R0]
CMP R1, #1
BEQ __iar_program_start
B Restore_PC_SP


;Save pc sp 的代码

SECTION .text:CODE:NOROOT
Save_PC_SP
LDR R0, =0x4001f000
MOV R1, #1
STR R1, [R0]
LDR R0, =sp_save
LDR R2, =pc_save
MOV R1, R13
STR R1, [R0]
MOV R1, LR
STR R1, [R2]
BX LR


;Restore pc sp 的代码

SECTION .text:CODE:NOROOT
Restore_PC_SP
LDR R0, =sp_save
LDR R1, [R0]
MOV R13, R1
LDR R0, =pc_save
LDR R1, [R0]
ADD R1, R1, #0x8
MOV PC, R1
NOP
NOP
NOP

在汇编文件中主要实现的是 save 和 restore 的操作,以及恢复过程的判断。因为我们的设计是从睡眠唤醒是从 Reset 起来的,这就导致第一次 cpu 的正常启动会和 restore 发生冲突,所以我这里选择了一个不会掉电的寄存器来作为是否进行 restore 的判断。

还有就是加 NOP 指令是因为 Cortex-M3 是三级流水线,为了防止 cpu 因为 pc 的预取而发生错误。

数字 IC 设计流程

作者 strongwong
2019年1月13日 18:41

0x00

最近即将开始要带着学弟们入门数字 IC 的设计,但很多学弟对于接下来要做什么是迷茫的,很多练就了各式各样的基本功却不知道如何施展,因此这里简单介绍一下数字 IC 设计的全过程及相关的设计工具及涉及到的相关职位,如果有写的不合适或者不正确的地方还请各位提出~

详见下图:

0x01

看了上图之后很多学弟就又问了,那平时我们都是 vivado,quartus,FPGA …… 为啥感觉和上面的都不沾边呢,这里说一点个人的看法,如果不是做硬件并行加速或者 FPGA 的嵌入式开发,那么平日 FPGA 的最大作用就是 —— 功能验证性工具。因为流片的价格非常昂贵,很少有实验室或者学校会让你不断地流片来实现你的设计,另外的,一个实验室如果没有同时具备设计,验证,版图 ……(全栈)技能同学的话要想能流片(同时具备以上技能)其实也很难的。那么这时 FPGA 就可以验证你的设计是否在一定程度上是正确的。

0x02

我们最后再来看一下数字前端的设计流程,如下图所示~

之前的 sdram 设计剩余部分,我们将尽快更新~

By Ricky

初窥 SDRAM

作者 strongwong
2018年11月24日 17:21

前言

上次挖的坑现在来填,在我们把 SDRAM 控制器接进 AHB 总线之前,我们先来设计一个 SDRAM 控制器。

引用知乎上看见的一段话:

在做这个 SDRAM 控制器之前,博主有一个疑问,对于学生来说,是否有必要学习用纯 Verilog 写一个 SDRAM 控制器?因为目前广告厂(X)和牙膏厂(A)都有了 DDR IP Core,而 SDRAM 的控制 IP 更是漫天飞舞,对于要实现一个应用可以直接调用 IP Core,只需要对其接口操作即可。对于开发者来说,与其费时费力用 Verilog 去写一个性能差而且老的 SDRAM 控制器,还不如直接调用官方经过打磨的更为先进 IP Core。所以博主特地来号称平均学历 211,平均月薪 7、8 万的知(bi)乎提出了这个问题,得到的解答博主总结大致如下。

对于学生这个身份来说,应该是要以学习为主要目的,虽然说目前企业为了加快项目进度会直接使用 IP Core,但是我们以学为本的初衷不应该为了避过难点而直接不去尝试,就比如我们刚开始学 Verilog 的时候肯定都会写过分频器,那么为什么不直接去学更简单精度更高 PLL IP Core 呢?从一个新手逐渐成长成一个老手都是由简单到复杂,由基础到提升,这是一个必经的过程。这也就是很多高校还是会开设汇编语言编写单片机的课程,学 FPGA 全用 IP Core 和学单片机全用库函数是一个道理。这是其一。

第二,写一个 SDRAM 控制器还是可以锻炼一些典型的技能。

  • 看官方文档
  • 根据时序图设计 SDRAM 逻辑,使用状态机
  • 配合仿真模型写测试仿真
  • 调试,提高频率,让你的 SDRAM 跑的更快
  • 研究时序约束

这一套做下来,你就可以提高一个层次了,经历过和没经历过是有质的区别。其实博主在提问的时候心中早已有了答案,只是还没有足够的信念去完成这个事情,当时看到很多业界前辈都支持去写的时候,博主心里也是比较开心的。之前博主已经学一些 SDRAM 的基础知识,只是当时水平还不够,没有坚持下去,心里一直不甘。趁着最近两个月之内没有什么事情要忙,所以决定要再次死磕 SDRAM。

正文

SDRAM 基本介绍

关于 SDRAM 的基本概念,在这再引用《终极内存指南》这篇文章中的一段话:

SDRAM(Synchronous Dynamic Random Access Memory),同步动态随机存储器。同步是指 Memory 工作需要同步时钟,内部的命令的发送与数据的传输都以它为基准;动态是指存储阵列需要不断刷新来保证存储的数据不丢失,因为 SDRAM 中存储数据是通过电容来工作的,大家知道电容在自然放置状态是会有放电的,如果电放完了,也就意味着 SDRAM 中的数据丢失了,所以 SDRAM 需要在电容的电量放完之前进行刷新;随机是指数据不是线性依次存储,而是自由指定地址进行数据的读写。

下面再简单看一下 SDRAM 的内部结构。
对于 SDRAM 的内容结构,就如同 Excel 的表格(如下图所示),即一个单元格就是一个存储地址。要确定具体的存储位置,只需要知道行地址(row-address )和列地址(column address )即可。
excel

一个常见的 SDRAM 中的一个 BANK 就有如上图所示的 13 行 9 列,通常一个 SDRAM 中有 4 个 BANK,那么 SDRAM(DDR 类似)的计算公式就是:

SDRAM(DDR容量) = 2^(row-address) × 2^(column-address) × 2^(bank-address) × datawidth
= 2^(row-address) × 2^(column-address) × bank数 × datawidth

以 DE10-LITE 开发板板载的 SDRAM-IS42S16320D-7TL 为例,标称为 64MB。根据芯片手册(如下图所示)我们可以看见其行地址宽度为 13,列地址宽度为 9(此时数据位宽为 32),则根据公式我们可以算出其容量确实为 64MB

2^13 × 2^9 × 4 × 32 = 536870912 b
⇒ 536870912 b ÷ 1024 = 524288 kb
⇒ 524288 kb ÷ 1024 = 512 Mb
⇒ 512 Mb ÷ 8 = 64 MB

SDRAM 芯片介绍

既然都打开了芯片手册(IS42S16320D-7TL),那就不要关上了,那我们再来看看芯片手册中的那些重要参数。
首先我们在第一页就可以看到它的刷新周期是 64ms(这个重要参数将在后面进行具体介绍)

在上文中我们已经提到了该芯片的行地址和列地址,我们需要注意的是其行列地址是复用的,其他相关引脚的功能描述都有介绍。

SDRAM 的初始化及寄存器的配置

SDRAM 初始化时序图如图所示,首先上电后,电源 Vcc 及 CLK 稳定时间至少 100us,然后对所有 BANK 进行预充电(precharge),经过 tRP 后给 auto refresh 命令,再经过 tRC 后再次 auto refresh 命令,再进过 tRC 后进行模式寄存器的配置。

那么以上命令是如何实现的呢,当时就是给与相应管脚的高低电平控制,由此实现,那么这就回到了我们数电的功能真值表(在之前我们就有提到过,数字 IC 终归是数字电路,不要把它搞成了编程项目),我们将下图的真值表以使用顺序总结为表格形式,方便接下来的 RTL 表述。

CMD CS RAS CAS WE
Precharge 0 0 1 0
Auto-Refresh 0 0 0 1
Nop 0 1 1 1
Mode 0 0 0 0

在了解到命令描述后我们还需要注意时间的间隔,在时序图中只告知了我们 T = 100us,而其余的 tRP,tRC,tMRD 均未告诉我们,这是因为通常一个芯片手册中有多种型号的芯片,因此我们需要去查看 AC characteristic 表格,根据芯片型号去确定时间。我们的板载芯片型号为 IS42S16320D-7TL,因此我们选择 -7 对应的时间,则 tRP = 15ns,tRC = 60ns,tMRD = 14ns

接下来我们就要进入到模式配置,模式配置的配置说明如下图所示:

A0-A2 为突发长度控制,即表示单次读或者写的时候的数据『长度』,本次突发长度参数我们设为 010。A3 突发模式通常设为 0。A4-A6 为潜伏期控制,专门针对读命令时,当给出读命令后,若有设置 CAS latency 则会延迟相应的周期数后给出数据,本次潜伏期参数我们设为 011,A9 突发模式通常设为 0。则最终我们初始化设置参数为 13’b0_0000_0011_0010

至此,我们便可以开始着手设计我们的初始化模块了,首先时序图上 T = 100us Min,则我们取 200us = 200,000ns 在不经过 PLL 的前提下,DE10LITE 开发板默认提供的时钟频率为 50MHz,则一个周期为 20ns,因此 T 延时可以取 10,000clk。延时后我们执行 precharge 命令。之后执行 tRP = 15ns Min,我们的 tRP 延迟就可以取 1clk(至少满足 15ns 的最低要求),然后执行 auto refresh 命令,tRC = 60ns Min 则延迟可取为 4clk,然后再次执行 auto refresh 命令,在这期间一共 9 个 clk。具体的设计可以首先设计一个 200us 的不自清零的计数器;设计一个对应的 200us 计数器标志位;针对 tRP 和 tRC 设计一个计数器,分别实现监测计数到对应的周期发出对应的命令;命令寄存器用来存放对应的命令;最后完成初始化操作后给一个初始化完成的标志位信号。

下面是具体实现的描述语言:

sdram_init.v
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/*
File name: sdram_init
Function: Power on initialization for IS42S16320D-7TL SDRAM
Module name: sdram_init
Author: Ricky
Time: 20181119
*/

module sdram_init(
//system signals
inputsys_clk,
inputsys_rst_n,

//others
output reg [3:0] cmd_reg,
output wire [12:0] sdram_addr,
output wireinit_flag

);

/*==============================================================================
**********************Define Parameter and inside Signals***********************


Note: syssys_clk=50MHz
T|min=100us >>> 200us=200,000ns >>> 10,000sys_clk >>> [13:0] cnt_200us
tRP|min=15ns >>> 20ns >>> 1sys_clk >>> [4:0] cnt_cmd
tRC|min=60ns >>> 80ns >>> 4sys_clk >>> [4:0] cnt_cmd
===============================================================================*/
reg [13:0] cnt_200us;
wire cnt_200us_flag;
reg [4:0]cnt_cmd;

//define sdram initial cmd
localparamprecharge = 4'b0010;
localparamauto_refresh = 4'b0001;
localparamnop = 4'b0111;
localparam modeset = 4'b0000;

/*==============================================================================
**********************************Main Logic************************************
==============================================================================*/
//T=200us counter
always @(posedge sys_clk or negedge sys_rst_n) begin
if(~sys_rst_n)
cnt_200us <= 13'd0;
else
if(cnt_200us_flag == 1'b0)
cnt_200us <= cnt_200us + 1'b1;
else
cnt_200us <= cnt_200us;
end

//cmd counter
always @(posedge sys_clk or negedge sys_rst_n) begin
if(~sys_rst_n)
cnt_cmd <= 4'd0;
else
if (cnt_200us_flag == 1'b1 && init_flag == 1'b0)
cnt_cmd <= cnt_cmd + 1'b1;
end

//cmd
always @(posedge sys_clk or negedge sys_rst_n) begin
if(~sys_rst_n)
cmd_reg <= nop;
else
if(cnt_200us_flag == 1'b1)
case(cnt_cmd)
0: cmd_reg <= precharge;
1: cmd_reg <= auto_refresh;
5: cmd_reg <= auto_refresh;
9: cmd_reg <= modeset;
default: cmd_reg <= nop;
endcase
end

assign cnt_200us_flag = (cnt_200us >= 10000) ? 1'b1:1'b0;
assign init_flag = (cnt_cmd >= 9) ? 1'b1:1'b0;
assign sdram_addr = (cmd_reg == modeset) ? 13'b0000000110010 : 13'b0010000000000;

endmodule

sdram_top.v
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
 /*
File name: sdram_top
Author: Ricky
Time: 20181121
*/
module sdram(
//system signals
inputsys_clk,
inputsys_rst_n,
//sdram pin
output wiresdram_clk,
output wire [12:0]sdram_addr,
output wire [1:0] sdram_bank,
output wiresdram_cas_n,
output wiresdram_cke,
output wiresdram_cs_n,
output wire[1:0]sdram_dqm,
output wiresdram_ras_n,
output wiresdram_we_n,

inout[15:0]sdram_dq
);

/*==============================================================================
**********************Define Parameter and inside Signals***********************
===============================================================================*/
wire init_flag;
wire [3:0] init_cmd_reg;
wire [12:0] init_addr;

/*==============================================================================
**********************************Main Logic************************************
==============================================================================*/
assign sdram_addr = init_addr;
assign {sdram_cs_n, sdram_ras_n, sdram_cas_n, sdram_we_n} = init_cmd_reg;
assign sdram_clk = ~sys_clk;

assign sdram_dqm = 2'b00;
assign sdram_cke = 1'b1;

//instantiating sdram_init module
sdram_init sdram_init(
//system signals
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
//others
.cmd_reg(init_cmd_reg),
.sdram_addr(init_addr),
.init_flag (init_flag)

);

endmodule

sdram_tb.v
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/*
File name: sdram_tb
Function: Testbench for power on initialization for IS42S16320D-7TL SDRAM
Author: Ricky
Time: 20181123
*/
`timescale 1ns/1ns
module sdram_tb;

regsys_clk;
reg sys_rst_n;
wiresdram_clk;
wire[12:0]sdram_addr;
wire [1:0] sdram_bank;
wiresdram_cas_n;
wiresdram_cke;
wiresdram_cs_n;
wire[1:0]sdram_dqm;
wiresdram_ras_n;
wiresdram_we_n;

wire[15:0]sdram_dq;

initial begin
sys_clk = 1;
sys_rst_n <= 0;
#100
sys_rst_n <= 1;
end

// 20ns/clock
always #10 sys_clk = ~sys_clk;

/* defparamsdram_model_plus.addr_bits =13;
defparamsdram_model_plus.data_bits = 16;
defparamsdram_model_plus.col_bits =9 ;
defparamsdram_model_plus.mem_sizes =2*1024*1024; */

//instantiating sdram_init module
sdram sdraminit(
//system signals
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n),
//sdram pin
.sdram_clk (sdram_clk),
.sdram_addr (sdram_addr),
.sdram_bank (sdram_bank),
.sdram_cas_n (sdram_cas_n),
.sdram_cke (sdram_cke),
.sdram_cs_n (sdram_cs_n),
.sdram_dqm (sdram_dqm),
.sdram_ras_n (sdram_ras_n),
.sdram_we_n (sdram_we_n),

.sdram_dq (sdram_dq)
);

//instantiating sdram_model module
sdram_model_plus sdram(
.Dq(sdram_dq),
.Addr(sdram_addr),
.Ba(sdram_bank),
.Clk(sdram_clk),
.Cke(sdram_cke),
.Cs_n(sdram_cs_n),
.Ras_n(sdram_ras_n),
.Cas_n(sdram_cas_n),
.We_n(sdram_we_n),
.Dqm(sdram_dqm),
.Debug(1'b1)
);

endmodule

仿真模型(见附件)一共有两个分别是镁光官方仿真模型以及国内大神基于镁光模型进行修改后便于调试的版本,使用任意一版均可。这里我采用的是 sdram_model.v

sdram_model.v
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
/***************************************************************************************
作者: 李晟
2003-08-27 V0.1 李晟

添加内存模块倒空功能,在外部需要创建事件:sdram_r ,本SDRAM的内容将会按Bank 顺序damp out 至文件
sdram_data.txt 中
×××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××*/
//2004-03-04 陈乃奎 修改原程序中将BANK的数据转存入TXT文件的格式
//2004-03-16 陈乃奎 修改SDRAM 的初始化数据
//2004/04/06 陈乃奎 将SDRAM的操作命令以字符形式表示,以便用MODELSIM监视
//2004/04/19 陈乃奎 修改参数 parameter tAC = 8;
//2010/09/17 罗瑶 修改sdram的大小,数据位宽,dqm宽度;
/****************************************************************************************
*
* File Name: sdram_model.V
* Version: 0.0f
* Date: July 8th, 1999
* Model: BUS Functional
* Simulator: Model Technology (PC version 5.2e PE)
*
* Dependencies: None
*
* Author: Son P. Huynh
* Email: sphuynh@micron.com
* Phone: (208) 368-3825
* Company: Micron Technology, Inc.
* Model: sdram_model (1Meg x 16 x 4 Banks)
*
* Description: 64Mb SDRAM Verilog model
*
* Limitation: - Doesn't check for 4096 cycle refresh
*
* Note: - Set simulator resolution to "ps" accuracy
* - Set Debug = 0 to disable $display messages
*
* Disclaimer: THESE DESIGNS ARE PROVIDED "AS IS" WITH NO WARRANTY
* WHATSOEVER AND MICRON SPECIFICALLY DISCLAIMS ANY
* IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR
* A PARTICULAR PURPOSE, OR AGAINST INFRINGEMENT.
*
* Copyright ?1998 Micron Semiconductor Products, Inc.
* All rights researved
*
* Rev Author Phone Date Changes
* ---- ---------------------------- ---------- ---------------------------------------
* 0.0f Son Huynh 208-368-3825 07/08/1999 - Fix tWR = 1 Clk + 7.5 ns (Auto)
* Micron Technology Inc. - Fix tWR = 15 ns (Manual)
* - Fix tRP (Autoprecharge to AutoRefresh)
*
* 0.0a Son Huynh 208-368-3825 05/13/1998 - First Release (from 64Mb rev 0.0e)
* Micron Technology Inc.
****************************************************************************************/

`timescale 1ns / 100ps

module sdram_model_plus (Dq, Addr, Ba, Clk, Cke, Cs_n, Ras_n, Cas_n, We_n, Dqm,Debug);

parameter addr_bits = 13;
parameter data_bits = 16;
parameter col_bits = 9;
parameter mem_sizes = 4*1024*1024 -1;//1 Meg

inout [data_bits - 1 : 0] Dq;
input [addr_bits - 1 : 0] Addr;
input [1 : 0] Ba;
input Clk;
input Cke;
input Cs_n;
input Ras_n;
input Cas_n;
input We_n;
input [1 : 0] Dqm; //高低各8bit
//added by xzli
input Debug;

reg [data_bits - 1 : 0] Bank0 [0 : mem_sizes];//存储器类型数据
reg [data_bits - 1 : 0] Bank1 [0 : mem_sizes];
reg [data_bits - 1 : 0] Bank2 [0 : mem_sizes];
reg [data_bits - 1 : 0] Bank3 [0 : mem_sizes];

reg [1 : 0] Bank_addr [0 : 3]; // Bank Address Pipeline
reg [col_bits - 1 : 0] Col_addr [0 : 3]; // Column Address Pipeline
reg [3 : 0] Command [0 : 3]; // Command Operation Pipeline
reg [3 : 0] Dqm_reg0, Dqm_reg1; // DQM Operation Pipeline
reg [addr_bits - 1 : 0] B0_row_addr, B1_row_addr, B2_row_addr, B3_row_addr;

reg [addr_bits - 1 : 0] Mode_reg;
reg [data_bits - 1 : 0] Dq_reg, Dq_dqm;
reg [col_bits - 1 : 0] Col_temp, Burst_counter;

reg Act_b0, Act_b1, Act_b2, Act_b3; // Bank Activate
reg Pc_b0, Pc_b1, Pc_b2, Pc_b3; // Bank Precharge

reg [1 : 0] Bank_precharge [0 : 3]; // Precharge Command
reg A10_precharge [0 : 3]; // Addr[10] = 1 (All banks)
reg Auto_precharge [0 : 3]; // RW AutoPrecharge (Bank)
reg Read_precharge [0 : 3]; // R AutoPrecharge
reg Write_precharge [0 : 3]; // W AutoPrecharge
integer Count_precharge [0 : 3]; // RW AutoPrecharge (Counter)
reg RW_interrupt_read [0 : 3]; // RW Interrupt Read with Auto Precharge
reg RW_interrupt_write [0 : 3]; // RW Interrupt Write with Auto Precharge

reg Data_in_enable;
reg Data_out_enable;

reg [1 : 0] Bank, Previous_bank;
reg [addr_bits - 1 : 0] Row;
reg [col_bits - 1 : 0] Col, Col_brst;

// Internal system clock
reg CkeZ, Sys_clk;

reg [24:0] dd;

// Commands Decode
wire Active_enable = ~Cs_n & ~Ras_n & Cas_n & We_n;
wire Aref_enable = ~Cs_n & ~Ras_n & ~Cas_n & We_n;
wire Burst_term = ~Cs_n & Ras_n & Cas_n & ~We_n;
wire Mode_reg_enable = ~Cs_n & ~Ras_n & ~Cas_n & ~We_n;
wire Prech_enable = ~Cs_n & ~Ras_n & Cas_n & ~We_n;
wire Read_enable = ~Cs_n & Ras_n & ~Cas_n & We_n;
wire Write_enable = ~Cs_n & Ras_n & ~Cas_n & ~We_n;

// Burst Length Decode
wire Burst_length_1 = ~Mode_reg[2] & ~Mode_reg[1] & ~Mode_reg[0];
wire Burst_length_2 = ~Mode_reg[2] & ~Mode_reg[1] & Mode_reg[0];
wire Burst_length_4 = ~Mode_reg[2] & Mode_reg[1] & ~Mode_reg[0];
wire Burst_length_8 = ~Mode_reg[2] & Mode_reg[1] & Mode_reg[0];

// CAS Latency Decode
wire Cas_latency_2 = ~Mode_reg[6] & Mode_reg[5] & ~Mode_reg[4];
wire Cas_latency_3 = ~Mode_reg[6] & Mode_reg[5] & Mode_reg[4];

// Write Burst Mode
wire Write_burst_mode = Mode_reg[9];

wire Debug; // Debug messages : 1 = On; 0 = Off
wire Dq_chk = Sys_clk & Data_in_enable; // Check setup/hold time for DQ

reg [31:0] mem_d;

event sdram_r,sdram_w,compare;




assign Dq = Dq_reg; // DQ buffer

// Commands Operation
`define ACT 0
`define NOP 1
`define READ 2
`define READ_A 3
`define WRITE 4
`define WRITE_A 5
`define PRECH 6
`define A_REF 7
`define BST 8
`define LMR 9

// // Timing Parameters for -75 (PC133) and CAS Latency = 2
// parameter tAC = 8; //test 6.5
// parameter tHZ = 7.0;
// parameter tOH = 2.7;
// parameter tMRD = 2.0; // 2 Clk Cycles
// parameter tRAS = 44.0;
// parameter tRC = 66.0;
// parameter tRCD = 20.0;
// parameter tRP = 20.0;
// parameter tRRD = 15.0;
// parameter tWRa = 7.5; // A2 Version - Auto precharge mode only (1 Clk + 7.5 ns)
// parameter tWRp = 0.0; // A2 Version - Precharge mode only (15 ns)

// Timing Parameters for -7 (PC143) and CAS Latency = 3
parameter tAC = 6.5; //test 6.5
parameter tHZ = 5.5;
parameter tOH = 2;
parameter tMRD = 2.0; // 2 Clk Cycles
parameter tRAS = 48.0;
parameter tRC = 70.0;
parameter tRCD = 20.0;
parameter tRP = 20.0;
parameter tRRD = 14.0;
parameter tWRa = 7.5; // A2 Version - Auto precharge mode only (1 Clk + 7.5 ns)
parameter tWRp = 0.0; // A2 Version - Precharge mode only (15 ns)

// Timing Check variable
integer MRD_chk;
integer WR_counter [0 : 3];
time WR_chk [0 : 3];
time RC_chk, RRD_chk;
time RAS_chk0, RAS_chk1, RAS_chk2, RAS_chk3;
time RCD_chk0, RCD_chk1, RCD_chk2, RCD_chk3;
time RP_chk0, RP_chk1, RP_chk2, RP_chk3;

integer test_file;

//*****display the command of the sdram**************************************

parameter Mode_Reg_Set =4'b0000;
parameter Auto_Refresh =4'b0001;
parameter Row_Active =4'b0011;
parameter Pre_Charge =4'b0010;
parameter PreCharge_All =4'b0010;
parameter Write =4'b0100;
parameter Write_Pre =4'b0100;
parameter Read =4'b0101;
parameter Read_Pre =4'b0101;
parameter Burst_Stop =4'b0110;
parameter Nop =4'b0111;
parameter Dsel =4'b1111;

wire [3:0] sdram_control;
reg cke_temp;
reg [8*13:1] sdram_command;

always@(posedge Clk)
cke_temp<=Cke;

assign sdram_control={Cs_n,Ras_n,Cas_n,We_n};

always@(sdram_control or cke_temp)
begin
case(sdram_control)
Mode_Reg_Set: sdram_command<="Mode_Reg_Set";
Auto_Refresh: sdram_command<="Auto_Refresh";
Row_Active: sdram_command<="Row_Active";
Pre_Charge: sdram_command<="Pre_Charge";
Burst_Stop: sdram_command<="Burst_Stop";
Dsel: sdram_command<="Dsel";

Write: if(cke_temp==1)
sdram_command<="Write";
else
sdram_command<="Write_suspend";

Read: if(cke_temp==1)
sdram_command<="Read";
else
sdram_command<="Read_suspend";

Nop: if(cke_temp==1)
sdram_command<="Nop";
else
sdram_command<="Self_refresh";

default: sdram_command<="Power_down";
endcase
end

//*****************************************************

initial
begin
//test_file=$fopen("test_file.txt");
end

initial
begin
Dq_reg = {data_bits{1'bz}};
{Data_in_enable, Data_out_enable} = 0;
{Act_b0, Act_b1, Act_b2, Act_b3} = 4'b0000;
{Pc_b0, Pc_b1, Pc_b2, Pc_b3} = 4'b0000;
{WR_chk[0], WR_chk[1], WR_chk[2], WR_chk[3]} = 0;
{WR_counter[0], WR_counter[1], WR_counter[2], WR_counter[3]} = 0;
{RW_interrupt_read[0], RW_interrupt_read[1], RW_interrupt_read[2], RW_interrupt_read[3]} = 0;
{RW_interrupt_write[0], RW_interrupt_write[1], RW_interrupt_write[2], RW_interrupt_write[3]} = 0;
{MRD_chk, RC_chk, RRD_chk} = 0;
{RAS_chk0, RAS_chk1, RAS_chk2, RAS_chk3} = 0;
{RCD_chk0, RCD_chk1, RCD_chk2, RCD_chk3} = 0;
{RP_chk0, RP_chk1, RP_chk2, RP_chk3} = 0;
$timeformat (-9, 0, " ns", 12);
//$readmemh("bank0.txt", Bank0);
//$readmemh("bank1.txt", Bank1);
//$readmemh("bank2.txt", Bank2);
//$readmemh("bank3.txt", Bank3);
/*
for(dd=0;dd<=mem_sizes;dd=dd+1)
begin
Bank0[dd]=dd[data_bits - 1 : 0];
Bank1[dd]=dd[data_bits - 1 : 0]+1;
Bank2[dd]=dd[data_bits - 1 : 0]+2;
Bank3[dd]=dd[data_bits - 1 : 0]+3;
end
*/
initial_sdram(0);
end

task initial_sdram;

input data_sign;
reg [3:0] data_sign;

for(dd=0;dd<=mem_sizes;dd=dd+1)
begin
mem_d = {data_sign,data_sign,data_sign,data_sign,data_sign,data_sign,data_sign,data_sign};
if(data_bits==16)
begin
Bank0[dd]=mem_d[15:0];
Bank1[dd]=mem_d[15:0];
Bank2[dd]=mem_d[15:0];
Bank3[dd]=mem_d[15:0];
end
else if(data_bits==32)
begin
Bank0[dd]=mem_d[31:0];
Bank1[dd]=mem_d[31:0];
Bank2[dd]=mem_d[31:0];
Bank3[dd]=mem_d[31:0];
end
end

endtask

// System clock generator
always
begin
@(posedge Clk)
begin
Sys_clk = CkeZ;
CkeZ = Cke;
end
@(negedge Clk)
begin
Sys_clk = 1'b0;
end
end

always @ (posedge Sys_clk) begin
// Internal Commamd Pipelined
Command[0] = Command[1];
Command[1] = Command[2];
Command[2] = Command[3];
Command[3] = `NOP;

Col_addr[0] = Col_addr[1];
Col_addr[1] = Col_addr[2];
Col_addr[2] = Col_addr[3];
Col_addr[3] = {col_bits{1'b0}};

Bank_addr[0] = Bank_addr[1];
Bank_addr[1] = Bank_addr[2];
Bank_addr[2] = Bank_addr[3];
Bank_addr[3] = 2'b0;

Bank_precharge[0] = Bank_precharge[1];
Bank_precharge[1] = Bank_precharge[2];
Bank_precharge[2] = Bank_precharge[3];
Bank_precharge[3] = 2'b0;

A10_precharge[0] = A10_precharge[1];
A10_precharge[1] = A10_precharge[2];
A10_precharge[2] = A10_precharge[3];
A10_precharge[3] = 1'b0;

// Dqm pipeline for Read
Dqm_reg0 = Dqm_reg1;
Dqm_reg1 = Dqm;

// Read or Write with Auto Precharge Counter
if (Auto_precharge[0] == 1'b1) begin
Count_precharge[0] = Count_precharge[0] + 1;
end
if (Auto_precharge[1] == 1'b1) begin
Count_precharge[1] = Count_precharge[1] + 1;
end
if (Auto_precharge[2] == 1'b1) begin
Count_precharge[2] = Count_precharge[2] + 1;
end
if (Auto_precharge[3] == 1'b1) begin
Count_precharge[3] = Count_precharge[3] + 1;
end

// tMRD Counter
MRD_chk = MRD_chk + 1;

// tWR Counter for Write
WR_counter[0] = WR_counter[0] + 1;
WR_counter[1] = WR_counter[1] + 1;
WR_counter[2] = WR_counter[2] + 1;
WR_counter[3] = WR_counter[3] + 1;

// Auto Refresh
if (Aref_enable == 1'b1) begin
if (Debug) $display ("at time %t AREF : Auto Refresh", $time);
// Auto Refresh to Auto Refresh
if (($time - RC_chk < tRC)&&Debug) begin
$display ("at time %t ERROR: tRC violation during Auto Refresh", $time);
end
// Precharge to Auto Refresh
if (($time - RP_chk0 < tRP || $time - RP_chk1 < tRP || $time - RP_chk2 < tRP || $time - RP_chk3 < tRP)&&Debug) begin
$display ("at time %t ERROR: tRP violation during Auto Refresh", $time);
end
// Precharge to Refresh
if (Pc_b0 == 1'b0 || Pc_b1 == 1'b0 || Pc_b2 == 1'b0 || Pc_b3 == 1'b0) begin
$display ("at time %t ERROR: All banks must be Precharge before Auto Refresh", $time);
end
// Record Current tRC time
RC_chk = $time;
end

// Load Mode Register
if (Mode_reg_enable == 1'b1) begin
// Decode CAS Latency, Burst Length, Burst Type, and Write Burst Mode
if (Pc_b0 == 1'b1 && Pc_b1 == 1'b1 && Pc_b2 == 1'b1 && Pc_b3 == 1'b1) begin
Mode_reg = Addr;
if (Debug) begin
$display ("at time %t LMR : Load Mode Register", $time);
// CAS Latency
if (Addr[6 : 4] == 3'b010)
$display (" CAS Latency = 2");
else if (Addr[6 : 4] == 3'b011)
$display (" CAS Latency = 3");
else
$display (" CAS Latency = Reserved");
// Burst Length
if (Addr[2 : 0] == 3'b000)
$display (" Burst Length = 1");
else if (Addr[2 : 0] == 3'b001)
$display (" Burst Length = 2");
else if (Addr[2 : 0] == 3'b010)
$display (" Burst Length = 4");
else if (Addr[2 : 0] == 3'b011)
$display (" Burst Length = 8");
else if (Addr[3 : 0] == 4'b0111)
$display (" Burst Length = Full");
else
$display (" Burst Length = Reserved");
// Burst Type
if (Addr[3] == 1'b0)
$display (" Burst Type = Sequential");
else if (Addr[3] == 1'b1)
$display (" Burst Type = Interleaved");
else
$display (" Burst Type = Reserved");
// Write Burst Mode
if (Addr[9] == 1'b0)
$display (" Write Burst Mode = Programmed Burst Length");
else if (Addr[9] == 1'b1)
$display (" Write Burst Mode = Single Location Access");
else
$display (" Write Burst Mode = Reserved");
end
end else begin
$display ("at time %t ERROR: all banks must be Precharge before Load Mode Register", $time);
end
// REF to LMR
if ($time - RC_chk < tRC) begin
$display ("at time %t ERROR: tRC violation during Load Mode Register", $time);
end
// LMR to LMR
if (MRD_chk < tMRD) begin
$display ("at time %t ERROR: tMRD violation during Load Mode Register", $time);
end
MRD_chk = 0;
end

// Active Block (Latch Bank Address and Row Address)
if (Active_enable == 1'b1) begin
if (Ba == 2'b00 && Pc_b0 == 1'b1) begin
{Act_b0, Pc_b0} = 2'b10;
B0_row_addr = Addr [addr_bits - 1 : 0];
RCD_chk0 = $time;
RAS_chk0 = $time;
if (Debug) $display ("at time %t ACT : Bank = 0 Row = %d", $time, Addr);
// Precharge to Activate Bank 0
if ($time - RP_chk0 < tRP) begin
$display ("at time %t ERROR: tRP violation during Activate bank 0", $time);
end
end else if (Ba == 2'b01 && Pc_b1 == 1'b1) begin
{Act_b1, Pc_b1} = 2'b10;
B1_row_addr = Addr [addr_bits - 1 : 0];
RCD_chk1 = $time;
RAS_chk1 = $time;
if (Debug) $display ("at time %t ACT : Bank = 1 Row = %d", $time, Addr);
// Precharge to Activate Bank 1
if ($time - RP_chk1 < tRP) begin
$display ("at time %t ERROR: tRP violation during Activate bank 1", $time);
end
end else if (Ba == 2'b10 && Pc_b2 == 1'b1) begin
{Act_b2, Pc_b2} = 2'b10;
B2_row_addr = Addr [addr_bits - 1 : 0];
RCD_chk2 = $time;
RAS_chk2 = $time;
if (Debug) $display ("at time %t ACT : Bank = 2 Row = %d", $time, Addr);
// Precharge to Activate Bank 2
if ($time - RP_chk2 < tRP) begin
$display ("at time %t ERROR: tRP violation during Activate bank 2", $time);
end
end else if (Ba == 2'b11 && Pc_b3 == 1'b1) begin
{Act_b3, Pc_b3} = 2'b10;
B3_row_addr = Addr [addr_bits - 1 : 0];
RCD_chk3 = $time;
RAS_chk3 = $time;
if (Debug) $display ("at time %t ACT : Bank = 3 Row = %d", $time, Addr);
// Precharge to Activate Bank 3
if ($time - RP_chk3 < tRP) begin
$display ("at time %t ERROR: tRP violation during Activate bank 3", $time);
end
end else if (Ba == 2'b00 && Pc_b0 == 1'b0) begin
$display ("at time %t ERROR: Bank 0 is not Precharged.", $time);
end else if (Ba == 2'b01 && Pc_b1 == 1'b0) begin
$display ("at time %t ERROR: Bank 1 is not Precharged.", $time);
end else if (Ba == 2'b10 && Pc_b2 == 1'b0) begin
$display ("at time %t ERROR: Bank 2 is not Precharged.", $time);
end else if (Ba == 2'b11 && Pc_b3 == 1'b0) begin
$display ("at time %t ERROR: Bank 3 is not Precharged.", $time);
end
// Active Bank A to Active Bank B
if ((Previous_bank != Ba) && ($time - RRD_chk < tRRD)) begin
$display ("at time %t ERROR: tRRD violation during Activate bank = %d", $time, Ba);
end
// Load Mode Register to Active
if (MRD_chk < tMRD ) begin
$display ("at time %t ERROR: tMRD violation during Activate bank = %d", $time, Ba);
end
// Auto Refresh to Activate
if (($time - RC_chk < tRC)&&Debug) begin
$display ("at time %t ERROR: tRC violation during Activate bank = %d", $time, Ba);
end
// Record variables for checking violation
RRD_chk = $time;
Previous_bank = Ba;
end

// Precharge Block
if (Prech_enable == 1'b1) begin
if (Addr[10] == 1'b1) begin
{Pc_b0, Pc_b1, Pc_b2, Pc_b3} = 4'b1111;
{Act_b0, Act_b1, Act_b2, Act_b3} = 4'b0000;
RP_chk0 = $time;
RP_chk1 = $time;
RP_chk2 = $time;
RP_chk3 = $time;
if (Debug) $display ("at time %t PRE : Bank = ALL",$time);
// Activate to Precharge all banks
if (($time - RAS_chk0 < tRAS) || ($time - RAS_chk1 < tRAS) ||
($time - RAS_chk2 < tRAS) || ($time - RAS_chk3 < tRAS)) begin
$display ("at time %t ERROR: tRAS violation during Precharge all bank", $time);
end
// tWR violation check for write
if (($time - WR_chk[0] < tWRp) || ($time - WR_chk[1] < tWRp) ||
($time - WR_chk[2] < tWRp) || ($time - WR_chk[3] < tWRp)) begin
$display ("at time %t ERROR: tWR violation during Precharge all bank", $time);
end
end else if (Addr[10] == 1'b0) begin
if (Ba == 2'b00) begin
{Pc_b0, Act_b0} = 2'b10;
RP_chk0 = $time;
if (Debug) $display ("at time %t PRE : Bank = 0",$time);
// Activate to Precharge Bank 0
if ($time - RAS_chk0 < tRAS) begin
$display ("at time %t ERROR: tRAS violation during Precharge bank 0", $time);
end
end else if (Ba == 2'b01) begin
{Pc_b1, Act_b1} = 2'b10;
RP_chk1 = $time;
if (Debug) $display ("at time %t PRE : Bank = 1",$time);
// Activate to Precharge Bank 1
if ($time - RAS_chk1 < tRAS) begin
$display ("at time %t ERROR: tRAS violation during Precharge bank 1", $time);
end
end else if (Ba == 2'b10) begin
{Pc_b2, Act_b2} = 2'b10;
RP_chk2 = $time;
if (Debug) $display ("at time %t PRE : Bank = 2",$time);
// Activate to Precharge Bank 2
if ($time - RAS_chk2 < tRAS) begin
$display ("at time %t ERROR: tRAS violation during Precharge bank 2", $time);
end
end else if (Ba == 2'b11) begin
{Pc_b3, Act_b3} = 2'b10;
RP_chk3 = $time;
if (Debug) $display ("at time %t PRE : Bank = 3",$time);
// Activate to Precharge Bank 3
if ($time - RAS_chk3 < tRAS) begin
$display ("at time %t ERROR: tRAS violation during Precharge bank 3", $time);
end
end
// tWR violation check for write
if ($time - WR_chk[Ba] < tWRp) begin
$display ("at time %t ERROR: tWR violation during Precharge bank %d", $time, Ba);
end
end
// Terminate a Write Immediately (if same bank or all banks)
if (Data_in_enable == 1'b1 && (Bank == Ba || Addr[10] == 1'b1)) begin
Data_in_enable = 1'b0;
end
// Precharge Command Pipeline for Read
if (Cas_latency_3 == 1'b1) begin
Command[2] = `PRECH;
Bank_precharge[2] = Ba;
A10_precharge[2] = Addr[10];
end else if (Cas_latency_2 == 1'b1) begin
Command[1] = `PRECH;
Bank_precharge[1] = Ba;
A10_precharge[1] = Addr[10];
end
end

// Burst terminate
if (Burst_term == 1'b1) begin
// Terminate a Write Immediately
if (Data_in_enable == 1'b1) begin
Data_in_enable = 1'b0;
end
// Terminate a Read Depend on CAS Latency
if (Cas_latency_3 == 1'b1) begin
Command[2] = `BST;
end else if (Cas_latency_2 == 1'b1) begin
Command[1] = `BST;
end
if (Debug) $display ("at time %t BST : Burst Terminate",$time);
end

// Read, Write, Column Latch
if (Read_enable == 1'b1 || Write_enable == 1'b1) begin
// Check to see if bank is open (ACT)
if ((Ba == 2'b00 && Pc_b0 == 1'b1) || (Ba == 2'b01 && Pc_b1 == 1'b1) ||
(Ba == 2'b10 && Pc_b2 == 1'b1) || (Ba == 2'b11 && Pc_b3 == 1'b1)) begin
$display("at time %t ERROR: Cannot Read or Write - Bank %d is not Activated", $time, Ba);
end
// Activate to Read or Write
if ((Ba == 2'b00) && ($time - RCD_chk0 < tRCD))
$display("at time %t ERROR: tRCD violation during Read or Write to Bank 0", $time);
if ((Ba == 2'b01) && ($time - RCD_chk1 < tRCD))
$display("at time %t ERROR: tRCD violation during Read or Write to Bank 1", $time);
if ((Ba == 2'b10) && ($time - RCD_chk2 < tRCD))
$display("at time %t ERROR: tRCD violation during Read or Write to Bank 2", $time);
if ((Ba == 2'b11) && ($time - RCD_chk3 < tRCD))
$display("at time %t ERROR: tRCD violation during Read or Write to Bank 3", $time);
// Read Command
if (Read_enable == 1'b1) begin
// CAS Latency pipeline
if (Cas_latency_3 == 1'b1) begin
if (Addr[10] == 1'b1) begin
Command[2] = `READ_A;
end else begin
Command[2] = `READ;
end
Col_addr[2] = Addr;
Bank_addr[2] = Ba;
end else if (Cas_latency_2 == 1'b1) begin
if (Addr[10] == 1'b1) begin
Command[1] = `READ_A;
end else begin
Command[1] = `READ;
end
Col_addr[1] = Addr;
Bank_addr[1] = Ba;
end

// Read interrupt Write (terminate Write immediately)
if (Data_in_enable == 1'b1) begin
Data_in_enable = 1'b0;
end

// Write Command
end else if (Write_enable == 1'b1) begin
if (Addr[10] == 1'b1) begin
Command[0] = `WRITE_A;
end else begin
Command[0] = `WRITE;
end
Col_addr[0] = Addr;
Bank_addr[0] = Ba;

// Write interrupt Write (terminate Write immediately)
if (Data_in_enable == 1'b1) begin
Data_in_enable = 1'b0;
end

// Write interrupt Read (terminate Read immediately)
if (Data_out_enable == 1'b1) begin
Data_out_enable = 1'b0;
end
end

// Interrupting a Write with Autoprecharge
if (Auto_precharge[Bank] == 1'b1 && Write_precharge[Bank] == 1'b1) begin
RW_interrupt_write[Bank] = 1'b1;
if (Debug) $display ("at time %t NOTE : Read/Write Bank %d interrupt Write Bank %d with Autoprecharge", $time, Ba, Bank);
end

// Interrupting a Read with Autoprecharge
if (Auto_precharge[Bank] == 1'b1 && Read_precharge[Bank] == 1'b1) begin
RW_interrupt_read[Bank] = 1'b1;
if (Debug) $display ("at time %t NOTE : Read/Write Bank %d interrupt Read Bank %d with Autoprecharge", $time, Ba, Bank);
end

// Read or Write with Auto Precharge
if (Addr[10] == 1'b1) begin
Auto_precharge[Ba] = 1'b1;
Count_precharge[Ba] = 0;
if (Read_enable == 1'b1) begin
Read_precharge[Ba] = 1'b1;
end else if (Write_enable == 1'b1) begin
Write_precharge[Ba] = 1'b1;
end
end
end

// Read with Auto Precharge Calculation
// The device start internal precharge:
// 1. CAS Latency - 1 cycles before last burst
// and 2. Meet minimum tRAS requirement
// or 3. Interrupt by a Read or Write (with or without AutoPrecharge)
if ((Auto_precharge[0] == 1'b1) && (Read_precharge[0] == 1'b1)) begin
if ((($time - RAS_chk0 >= tRAS) && // Case 2
((Burst_length_1 == 1'b1 && Count_precharge[0] >= 1) || // Case 1
(Burst_length_2 == 1'b1 && Count_precharge[0] >= 2) ||
(Burst_length_4 == 1'b1 && Count_precharge[0] >= 4) ||
(Burst_length_8 == 1'b1 && Count_precharge[0] >= 8))) ||
(RW_interrupt_read[0] == 1'b1)) begin // Case 3
Pc_b0 = 1'b1;
Act_b0 = 1'b0;
RP_chk0 = $time;
Auto_precharge[0] = 1'b0;
Read_precharge[0] = 1'b0;
RW_interrupt_read[0] = 1'b0;
if (Debug) $display ("at time %t NOTE : Start Internal Auto Precharge for Bank 0", $time);
end
end
if ((Auto_precharge[1] == 1'b1) && (Read_precharge[1] == 1'b1)) begin
if ((($time - RAS_chk1 >= tRAS) &&
((Burst_length_1 == 1'b1 && Count_precharge[1] >= 1) ||
(Burst_length_2 == 1'b1 && Count_precharge[1] >= 2) ||
(Burst_length_4 == 1'b1 && Count_precharge[1] >= 4) ||
(Burst_length_8 == 1'b1 && Count_precharge[1] >= 8))) ||
(RW_interrupt_read[1] == 1'b1)) begin
Pc_b1 = 1'b1;
Act_b1 = 1'b0;
RP_chk1 = $time;
Auto_precharge[1] = 1'b0;
Read_precharge[1] = 1'b0;
RW_interrupt_read[1] = 1'b0;
if (Debug) $display ("at time %t NOTE : Start Internal Auto Precharge for Bank 1", $time);
end
end
if ((Auto_precharge[2] == 1'b1) && (Read_precharge[2] == 1'b1)) begin
if ((($time - RAS_chk2 >= tRAS) &&
((Burst_length_1 == 1'b1 && Count_precharge[2] >= 1) ||
(Burst_length_2 == 1'b1 && Count_precharge[2] >= 2) ||
(Burst_length_4 == 1'b1 && Count_precharge[2] >= 4) ||
(Burst_length_8 == 1'b1 && Count_precharge[2] >= 8))) ||
(RW_interrupt_read[2] == 1'b1)) begin
Pc_b2 = 1'b1;
Act_b2 = 1'b0;
RP_chk2 = $time;
Auto_precharge[2] = 1'b0;
Read_precharge[2] = 1'b0;
RW_interrupt_read[2] = 1'b0;
if (Debug) $display ("at time %t NOTE : Start Internal Auto Precharge for Bank 2", $time);
end
end
if ((Auto_precharge[3] == 1'b1) && (Read_precharge[3] == 1'b1)) begin
if ((($time - RAS_chk3 >= tRAS) &&
((Burst_length_1 == 1'b1 && Count_precharge[3] >= 1) ||
(Burst_length_2 == 1'b1 && Count_precharge[3] >= 2) ||
(Burst_length_4 == 1'b1 && Count_precharge[3] >= 4) ||
(Burst_length_8 == 1'b1 && Count_precharge[3] >= 8))) ||
(RW_interrupt_read[3] == 1'b1)) begin
Pc_b3 = 1'b1;
Act_b3 = 1'b0;
RP_chk3 = $time;
Auto_precharge[3] = 1'b0;
Read_precharge[3] = 1'b0;
RW_interrupt_read[3] = 1'b0;
if (Debug) $display ("at time %t NOTE : Start Internal Auto Precharge for Bank 3", $time);
end
end

// Internal Precharge or Bst
if (Command[0] == `PRECH) begin // Precharge terminate a read with same bank or all banks
if (Bank_precharge[0] == Bank || A10_precharge[0] == 1'b1) begin
if (Data_out_enable == 1'b1) begin
Data_out_enable = 1'b0;
end
end
end else if (Command[0] == `BST) begin // BST terminate a read to current bank
if (Data_out_enable == 1'b1) begin
Data_out_enable = 1'b0;
end
end

if (Data_out_enable == 1'b0) begin
Dq_reg <= #tOH {data_bits{1'bz}};
end

// Detect Read or Write command
if (Command[0] == `READ || Command[0] == `READ_A) begin
Bank = Bank_addr[0];
Col = Col_addr[0];
Col_brst = Col_addr[0];
if (Bank_addr[0] == 2'b00) begin
Row = B0_row_addr;
end else if (Bank_addr[0] == 2'b01) begin
Row = B1_row_addr;
end else if (Bank_addr[0] == 2'b10) begin
Row = B2_row_addr;
end else if (Bank_addr[0] == 2'b11) begin
Row = B3_row_addr;
end
Burst_counter = 0;
Data_in_enable = 1'b0;
Data_out_enable = 1'b1;
end else if (Command[0] == `WRITE || Command[0] == `WRITE_A) begin
Bank = Bank_addr[0];
Col = Col_addr[0];
Col_brst = Col_addr[0];
if (Bank_addr[0] == 2'b00) begin
Row = B0_row_addr;
end else if (Bank_addr[0] == 2'b01) begin
Row = B1_row_addr;
end else if (Bank_addr[0] == 2'b10) begin
Row = B2_row_addr;
end else if (Bank_addr[0] == 2'b11) begin
Row = B3_row_addr;
end
Burst_counter = 0;
Data_in_enable = 1'b1;
Data_out_enable = 1'b0;
end

// DQ buffer (Driver/Receiver)
if (Data_in_enable == 1'b1) begin // Writing Data to Memory
// Array buffer
if (Bank == 2'b00) Dq_dqm [data_bits - 1 : 0] = Bank0 [{Row, Col}];
if (Bank == 2'b01) Dq_dqm [data_bits - 1 : 0] = Bank1 [{Row, Col}];
if (Bank == 2'b10) Dq_dqm [data_bits - 1 : 0] = Bank2 [{Row, Col}];
if (Bank == 2'b11) Dq_dqm [data_bits - 1 : 0] = Bank3 [{Row, Col}];
// Dqm operation
if (Dqm[0] == 1'b0) Dq_dqm [ 7 : 0] = Dq [ 7 : 0];
if (Dqm[1] == 1'b0) Dq_dqm [15 : 8] = Dq [15 : 8];
//if (Dqm[2] == 1'b0) Dq_dqm [23 : 16] = Dq [23 : 16];
// if (Dqm[3] == 1'b0) Dq_dqm [31 : 24] = Dq [31 : 24];
// Write to memory
if (Bank == 2'b00) Bank0 [{Row, Col}] = Dq_dqm [data_bits - 1 : 0];
if (Bank == 2'b01) Bank1 [{Row, Col}] = Dq_dqm [data_bits - 1 : 0];
if (Bank == 2'b10) Bank2 [{Row, Col}] = Dq_dqm [data_bits - 1 : 0];
if (Bank == 2'b11) Bank3 [{Row, Col}] = Dq_dqm [data_bits - 1 : 0];
if (Bank == 2'b11 && Row==10'h3 && Col[7:4]==4'h4)
$display("at time %t WRITE: Bank = %d Row = %d, Col = %d, Data = Hi-Z due to DQM", $time, Bank, Row, Col);
//$fdisplay(test_file,"bank:%h row:%h col:%h write:%h",Bank,Row,Col,Dq_dqm);
// Output result
if (Dqm == 4'b1111) begin
if (Debug) $display("at time %t WRITE: Bank = %d Row = %d, Col = %d, Data = Hi-Z due to DQM", $time, Bank, Row, Col);
end else begin
if (Debug) $display("at time %t WRITE: Bank = %d Row = %d, Col = %d, Data = %d, Dqm = %b", $time, Bank, Row, Col, Dq_dqm, Dqm);
// Record tWR time and reset counter
WR_chk [Bank] = $time;
WR_counter [Bank] = 0;
end
// Advance burst counter subroutine
#tHZ Burst;
end else if (Data_out_enable == 1'b1) begin // Reading Data from Memory
//$display("%h , %h, %h",Bank0,Row,Col);
// Array buffer
if (Bank == 2'b00) Dq_dqm [data_bits - 1 : 0] = Bank0 [{Row, Col}];
if (Bank == 2'b01) Dq_dqm [data_bits - 1 : 0] = Bank1 [{Row, Col}];
if (Bank == 2'b10) Dq_dqm [data_bits - 1 : 0] = Bank2 [{Row, Col}];
if (Bank == 2'b11) Dq_dqm [data_bits - 1 : 0] = Bank3 [{Row, Col}];

// Dqm operation
if (Dqm_reg0[0] == 1'b1) Dq_dqm [ 7 : 0] = 8'bz;
if (Dqm_reg0[1] == 1'b1) Dq_dqm [15 : 8] = 8'bz;
if (Dqm_reg0[2] == 1'b1) Dq_dqm [23 : 16] = 8'bz;
if (Dqm_reg0[3] == 1'b1) Dq_dqm [31 : 24] = 8'bz;
// Display result
Dq_reg [data_bits - 1 : 0] = #tAC Dq_dqm [data_bits - 1 : 0];
if (Dqm_reg0 == 4'b1111) begin
if (Debug) $display("at time %t READ : Bank = %d Row = %d, Col = %d, Data = Hi-Z due to DQM", $time, Bank, Row, Col);
end else begin
if (Debug) $display("at time %t READ : Bank = %d Row = %d, Col = %d, Data = %d, Dqm = %b", $time, Bank, Row, Col, Dq_reg, Dqm_reg0);
end
// Advance burst counter subroutine
Burst;
end
end

// Write with Auto Precharge Calculation
// The device start internal precharge:
// 1. tWR Clock after last burst
// and 2. Meet minimum tRAS requirement
// or 3. Interrupt by a Read or Write (with or without AutoPrecharge)
always @ (WR_counter[0]) begin
if ((Auto_precharge[0] == 1'b1) && (Write_precharge[0] == 1'b1)) begin
if ((($time - RAS_chk0 >= tRAS) && // Case 2
(((Burst_length_1 == 1'b1 || Write_burst_mode == 1'b1) && Count_precharge [0] >= 1) || // Case 1
(Burst_length_2 == 1'b1 && Count_precharge [0] >= 2) ||
(Burst_length_4 == 1'b1 && Count_precharge [0] >= 4) ||
(Burst_length_8 == 1'b1 && Count_precharge [0] >= 8))) ||
(RW_interrupt_write[0] == 1'b1 && WR_counter[0] >= 2)) begin // Case 3 (stop count when interrupt)
Auto_precharge[0] = 1'b0;
Write_precharge[0] = 1'b0;
RW_interrupt_write[0] = 1'b0;
#tWRa; // Wait for tWR
Pc_b0 = 1'b1;
Act_b0 = 1'b0;
RP_chk0 = $time;
if (Debug) $display ("at time %t NOTE : Start Internal Auto Precharge for Bank 0", $time);
end
end
end
always @ (WR_counter[1]) begin
if ((Auto_precharge[1] == 1'b1) && (Write_precharge[1] == 1'b1)) begin
if ((($time - RAS_chk1 >= tRAS) &&
(((Burst_length_1 == 1'b1 || Write_burst_mode == 1'b1) && Count_precharge [1] >= 1) ||
(Burst_length_2 == 1'b1 && Count_precharge [1] >= 2) ||
(Burst_length_4 == 1'b1 && Count_precharge [1] >= 4) ||
(Burst_length_8 == 1'b1 && Count_precharge [1] >= 8))) ||
(RW_interrupt_write[1] == 1'b1 && WR_counter[1] >= 2)) begin
Auto_precharge[1] = 1'b0;
Write_precharge[1] = 1'b0;
RW_interrupt_write[1] = 1'b0;
#tWRa; // Wait for tWR
Pc_b1 = 1'b1;
Act_b1 = 1'b0;
RP_chk1 = $time;
if (Debug) $display ("at time %t NOTE : Start Internal Auto Precharge for Bank 1", $time);
end
end
end
always @ (WR_counter[2]) begin
if ((Auto_precharge[2] == 1'b1) && (Write_precharge[2] == 1'b1)) begin
if ((($time - RAS_chk2 >= tRAS) &&
(((Burst_length_1 == 1'b1 || Write_burst_mode == 1'b1) && Count_precharge [2] >= 1) ||
(Burst_length_2 == 1'b1 && Count_precharge [2] >= 2) ||
(Burst_length_4 == 1'b1 && Count_precharge [2] >= 4) ||
(Burst_length_8 == 1'b1 && Count_precharge [2] >= 8))) ||
(RW_interrupt_write[2] == 1'b1 && WR_counter[2] >= 2)) begin
Auto_precharge[2] = 1'b0;
Write_precharge[2] = 1'b0;
RW_interrupt_write[2] = 1'b0;
#tWRa; // Wait for tWR
Pc_b2 = 1'b1;
Act_b2 = 1'b0;
RP_chk2 = $time;
if (Debug) $display ("at time %t NOTE : Start Internal Auto Precharge for Bank 2", $time);
end
end
end
always @ (WR_counter[3]) begin
if ((Auto_precharge[3] == 1'b1) && (Write_precharge[3] == 1'b1)) begin
if ((($time - RAS_chk3 >= tRAS) &&
(((Burst_length_1 == 1'b1 || Write_burst_mode == 1'b1) && Count_precharge [3] >= 1) ||
(Burst_length_2 == 1'b1 && Count_precharge [3] >= 2) ||
(Burst_length_4 == 1'b1 && Count_precharge [3] >= 4) ||
(Burst_length_8 == 1'b1 && Count_precharge [3] >= 8))) ||
(RW_interrupt_write[3] == 1'b1 && WR_counter[3] >= 2)) begin
Auto_precharge[3] = 1'b0;
Write_precharge[3] = 1'b0;
RW_interrupt_write[3] = 1'b0;
#tWRa; // Wait for tWR
Pc_b3 = 1'b1;
Act_b3 = 1'b0;
RP_chk3 = $time;
if (Debug) $display ("at time %t NOTE : Start Internal Auto Precharge for Bank 3", $time);
end
end
end

task Burst;
begin
// Advance Burst Counter
Burst_counter = Burst_counter + 1;

// Burst Type
if (Mode_reg[3] == 1'b0) begin // Sequential Burst
Col_temp = Col + 1;
end else if (Mode_reg[3] == 1'b1) begin // Interleaved Burst
Col_temp[2] = Burst_counter[2] ^ Col_brst[2];
Col_temp[1] = Burst_counter[1] ^ Col_brst[1];
Col_temp[0] = Burst_counter[0] ^ Col_brst[0];
end

// Burst Length
if (Burst_length_2) begin // Burst Length = 2
Col [0] = Col_temp [0];
end else if (Burst_length_4) begin // Burst Length = 4
Col [1 : 0] = Col_temp [1 : 0];
end else if (Burst_length_8) begin // Burst Length = 8
Col [2 : 0] = Col_temp [2 : 0];
end else begin // Burst Length = FULL
Col = Col_temp;
end

// Burst Read Single Write
if (Write_burst_mode == 1'b1) begin
Data_in_enable = 1'b0;
end

// Data Counter
if (Burst_length_1 == 1'b1) begin
if (Burst_counter >= 1) begin
Data_in_enable = 1'b0;
Data_out_enable = 1'b0;
end
end else if (Burst_length_2 == 1'b1) begin
if (Burst_counter >= 2) begin
Data_in_enable = 1'b0;
Data_out_enable = 1'b0;
end
end else if (Burst_length_4 == 1'b1) begin
if (Burst_counter >= 4) begin
Data_in_enable = 1'b0;
Data_out_enable = 1'b0;
end
end else if (Burst_length_8 == 1'b1) begin
if (Burst_counter >= 8) begin
Data_in_enable = 1'b0;
Data_out_enable = 1'b0;
end
end
end
endtask

//**********************将SDRAM内的数据直接输出到外部文件*******************************//

/*
integer sdram_data,ind;


always@(sdram_r)
begin
sdram_data=$fopen("sdram_data.txt");
$display("Sdram dampout begin ",sdram_data);
// $fdisplay(sdram_data,"Bank0:");
for(ind=0;ind<=mem_sizes;ind=ind+1)
$fdisplay(sdram_data,"%h %b",ind,Bank0[ind]);
// $fdisplay(sdram_data,"Bank1:");
for(ind=0;ind<=mem_sizes;ind=ind+1)
$fdisplay(sdram_data,"%h %b",ind,Bank1[ind]);
// $fdisplay(sdram_data,"Bank2:");
for(ind=0;ind<=mem_sizes;ind=ind+1)
$fdisplay(sdram_data,"%h %b",ind,Bank2[ind]);
// $fdisplay(sdram_data,"Bank3:");
for(ind=0;ind<=mem_sizes;ind=ind+1)
$fdisplay(sdram_data,"%h %b",ind,Bank3[ind]);

$fclose("sdram_data.txt");
//->compare;
end
*/
integer sdram_data,sdram_mem;
reg [24:0] aa,cc;
reg [24:0] bb,ee;

always@(sdram_r)
begin
$display("Sdram dampout begin ",$realtime);
sdram_data=$fopen("sdram_data.txt");
for(aa=0;aa<4*(mem_sizes+1);aa=aa+1)
begin
bb=aa[18:0];
if(aa<=mem_sizes)
$fdisplay(sdram_data,"%0d %0h",aa,Bank0[bb]);
else if(aa<=2*mem_sizes+1)
$fdisplay(sdram_data,"%0d %0h",aa,Bank1[bb]);
else if(aa<=3*mem_sizes+2)
$fdisplay(sdram_data,"%0d %0h",aa,Bank2[bb]);
else
$fdisplay(sdram_data,"%0d %0h",aa,Bank3[bb]);
end
$fclose("sdram_data.txt");

sdram_mem=$fopen("sdram_mem.txt");
for(cc=0;cc<4*(mem_sizes+1);cc=cc+1)
begin
ee=cc[18:0];
if(cc<=mem_sizes)
$fdisplay(sdram_mem,"%0h",Bank0[ee]);
else if(cc<=2*mem_sizes+1)
$fdisplay(sdram_mem,"%0h",Bank1[ee]);
else if(cc<=3*mem_sizes+2)
$fdisplay(sdram_mem,"%0h",Bank2[ee]);
else
$fdisplay(sdram_mem,"%0h",Bank3[ee]);
end
$fclose("sdram_mem.txt");

end



// // Timing Parameters for -75 (PC133) and CAS Latency = 2
// specify
// specparam
//// tAH = 0.8, // Addr, Ba Hold Time
//// tAS = 1.5, // Addr, Ba Setup Time
//// tCH = 2.5, // Clock High-Level Width
//// tCL = 2.5, // Clock Low-Level Width
////// tCK = 10.0, // Clock Cycle Time 100mhz
////// tCK = 7.5, // Clock Cycle Time 133mhz
//// tCK = 7, // Clock Cycle Time 143mhz
//// tDH = 0.8, // Data-in Hold Time
//// tDS = 1.5, // Data-in Setup Time
//// tCKH = 0.8, // CKE Hold Time
//// tCKS = 1.5, // CKE Setup Time
//// tCMH = 0.8, // CS#, RAS#, CAS#, WE#, DQM# Hold Time
//// tCMS = 1.5; // CS#, RAS#, CAS#, WE#, DQM# Setup Time
// tAH = 1, // Addr, Ba Hold Time
// tAS = 1.5, // Addr, Ba Setup Time
// tCH = 1, // Clock High-Level Width
// tCL = 3, // Clock Low-Level Width
//// tCK = 10.0, // Clock Cycle Time 100mhz
//// tCK = 7.5, // Clock Cycle Time 133mhz
// tCK = 7, // Clock Cycle Time 143mhz
// tDH = 1, // Data-in Hold Time
// tDS = 2, // Data-in Setup Time
// tCKH = 1, // CKE Hold Time
// tCKS = 2, // CKE Setup Time
// tCMH = 0.8, // CS#, RAS#, CAS#, WE#, DQM# Hold Time
// tCMS = 1.5; // CS#, RAS#, CAS#, WE#, DQM# Setup Time
// $width (posedge Clk, tCH);
// $width (negedge Clk, tCL);
// $period (negedge Clk, tCK);
// $period (posedge Clk, tCK);
// $setuphold(posedge Clk, Cke, tCKS, tCKH);
// $setuphold(posedge Clk, Cs_n, tCMS, tCMH);
// $setuphold(posedge Clk, Cas_n, tCMS, tCMH);
// $setuphold(posedge Clk, Ras_n, tCMS, tCMH);
// $setuphold(posedge Clk, We_n, tCMS, tCMH);
// $setuphold(posedge Clk, Addr, tAS, tAH);
// $setuphold(posedge Clk, Ba, tAS, tAH);
// $setuphold(posedge Clk, Dqm, tCMS, tCMH);
// $setuphold(posedge Dq_chk, Dq, tDS, tDH);
// endspecify

endmodule

仿真结果

我们可以看到基于 sdram_model.v 运行了 201us 个周期后,modelsim 上打印信息显示我们初始化的操作是正确的。

仿真波形如图所示:

小结

我们依芯片手册成功实现了 sdram 的上电初始化,接下来我们将继续进行后续的操作,我们将尽快更新~

附件

SDRAM 仿真模型文件:点击下载,提取码:yihx

By Ricky

从计数器开始,看数字 IC 设计

作者 strongwong
2018年11月24日 10:41

计数器设计

之前有实验室的学长去参加海思、中芯国际、瑞芯微、…… 数字 IC 前端方向的面试,几乎都问到了同一系列问题——设计一个计数器及相关问题。这里很多朋友就会觉得很有意思了,为什么一个简单的计数器能有这么多东西,那我们就『简单』的东西简单看。

0x00 请你设计一个 10 进制的异步复位无限循环计数器(0-9)

首先,第一个问题,请你设计一个 10 进制的异步复位无限循环计数器(0-9),你会怎么做?

相信到这很多朋友就开始洋洋洒洒地写道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module counter10(
input clk,
input rst_n,
output wire cnt_flag
);
reg[3:0] cnt_reg;

always @(posedge clk or negedge rst) begin
if( ~rst_n) begin
cnt_reg <= 4'b0;
end
else begin
if(cnt_reg == 4'd9)
cnt_reg <= 4'b0;
else
cnt_reg <= cnt_reg + 4'b1;
end
end

assign cnt_flag = (cnt_reg == 4'd9) ? 1'b1 : 1'b0;

endmodule

0x01 请画出对应的电路图

Bravo! 没有任何问题!接下来开始有分水岭了,请画出对应的电路图 ,有的朋友可能会抓脑袋了:

这里给一点提示,看你能想起什么,确定状态 —— 确定激励方程 —— 逻辑图 —— 自启动检查 —— 状态表。这是什么?数字电路基础,为什么?我们这是数字集成电路设计啊!最终还是要回归到数字电路上来啊!

好,那我们先用 D 触发器来做(不经过编码优化,只是还原最简单的设计步骤),回忆一下最初我们大一大二时怎么弄的。

状态表:

计数顺序 现状态 次状态
- Q3 Q2 Q1 Q0 D3 D2 D1 D0
0 0 0 0 0 0 0 0 1
1 0 0 0 1 0 0 1 0
2 0 0 1 0 0 0 1 1
3 0 0 1 1 0 1 0 0
4 0 1 0 0 0 1 0 1
5 0 1 0 1 0 1 1 0
6 0 1 1 0 0 1 1 1
7 0 1 1 1 1 0 0 0
8 1 0 0 0 1 0 0 1
9 1 0 0 1 0 0 0 0

激励方程:

逻辑图:

…… 剩下的估计大家都能回忆起来了

以上就是我们之前数字电路设计流程,我们回过头来看我们的硬件描述过程,有 if 判断值,那少不了比较器,有 + 运算,自然也有一个加法器 …… 当然我们真正的设计应该是先想好了有比较器和其他逻辑电路才有对应的硬件描述,但我们不妨来看一下我们设计的电路,不正是如下图所示:

0x02 上图中的关键路径是哪一条?

Ok~ 解决了电路图,下一个问题又来了,上图中的关键路径是哪一条?

要知道关键路径就需要时序分析啦,这里为接下来的另一个数字 IC 的小专栏———时序分析与约束挖下第一坑。

关键路径应该是:Q > + > MUX > D (具体分析将在后面填坑,大家也可以先想想为什么不是 Q > CMP > MUX > D)

0x03 上面图中 CMP = 9 即原描述中 cnt_reg == 4’d9 的电路是什么?

接下来的问题,上面图中 CMP = 9 即原描述中 cnt_reg == 4’d9 的电路是什么?
其实答案就在我们的触发器版原理图对应的组合电路中,精简后如下图所示:

0x04 cnt_reg == 4’d9 和 cnt_reg > 8 有什么区别?

功能上确实是一样的,但是如果是一个把 Verilog 当编程玩的朋友对于接下来的东就蛮有意义的了,cnt_reg == 4’d9 实现的电路如上图所示,是一个相对简单的组合逻辑电路。如果是 cnt_reg > 8,对于我们 4 位数据来说可能的范围为 9~15,则综合工具会把所有的情况都列出,cnt_reg == 9,10,11 …… 这样在无形中就浪费了资源。若位宽更大则会被综合为cnt_reg - 8 > 0,由此便会引入一个加法器 ……

0x05 加法器对应的电路是什么?如何验证这个计数器?……

一系列的问题,我们可以发现并非那么简单的。要知道直到现在优化加法器的文章依然不时可以出现在 sci 检索期刊中,这另外说明为什么上一问引入加法器后带来的浪费用省略号来表示,为什么关键路径是到 + 而不是比较器 ……

小结

引用《手把手教你设计 CPU——RISC-V 处理器篇》作者胡振波老师的一段话,当年第一次 Verilog 课时我的授课老师董乾博士也强调过类似的话。

先定义电路微架构而后编写代码。
谨记 Verilog 只是一种硬件描述语言,IC 设计的本质是对于电路的设计,虽然现在Verilog Coding 采用 RTL 级别的抽象描述,但是必须清楚所描述的代码能够映射出的电路结构,其面积和时序的影响都了然于胸,只有如此才能够成为一名优秀的 IC 设计工程师。
不要纠结 Verilog 的语法,而应立足实战。
Verilog 的设计语法子集非常精简简单,很快就可以上手入门。入门之后最好的学习方法是进行设计实战(实战是最好的老师),而不是进一步纠结 Verilog 的语法(不要浪费脑力试图记住大多数高级的 Verilog 语法,而是在需要使用的时候查阅即可)。

By Ricky

❌
❌