当前位置:网站首页>吃透Chisel语言.22.Chisel时序电路(二)——Chisel计数器(Counter)详解:计数器、定时器和脉宽调制
吃透Chisel语言.22.Chisel时序电路(二)——Chisel计数器(Counter)详解:计数器、定时器和脉宽调制
2022-07-27 09:36:00 【github-3rr0r】
Chisel时序电路(二)——Chisel计数器(Counter)详解
上一篇文章我们学习了时序电路中最基础的寄存器,在Chisel中是如何实现并使用的。而时序电路中还有一种十分常见的结构,它就是计数器(Counter)。计数器在数字设计中很常见,可以用于循环计数、性能计数等场合,几乎是必不可少的组件。从本质上来说,计数器也是一个寄存器,这一篇文章我们就共同学习一下Chisel中的计数器及其波形特性以及包括定时器、脉冲宽度调制在内的应用。
Chisel中的计数器和事件计数器
从本质上来说,计数器也是一个寄存器。只不过计数器中寄存器的输出会连接到一个加法器,加法器的另一个输入为计数器的步进值,一般为1,而加法器的输出就是寄存器的输入,下图就展示了一个自由运行(free-running)的计数器:

一个自由运行的4位计数器会从0变化到15,然后又重新从0开始计数,计数器复位时的初始值应该是已知的。在Chisel中实现如下:
val cntReg = RegInit(0.U(4.W))
cntReg := cntReg + 1.U
如果我们希望通过计数器来统计事件发生的次数,那我们用一个条件更新就行了,如果事件发生就自增,否则计数器值不变。事件计数器的示意图如下:

在Chisel中可以这么实现一个4位的事件计数器:
val cntEventsReg = RegInit(0.U(4.W))
when(event) {
cntEventsReg := cntEventsReg + 1.U
}
向上计数/向下计数
向上计数直到某个特定值,然后再重新从零开始,如果用上面的实现方式,那么计数器只会在达到 2 n − 1 2^n-1 2n−1后回到0开始计数,而不能够指定特定值。所以我们需要把寄存器的值和限定的最大常量进行比较,用一个when条件语句就行了:
val cntReg = RegInit(0.U(8.W))
cntReg := cntReg + 1.U
when(cntReg === N) {
cntReg := 0.U
}
或者这么写更好懂一点:
val cntReg = RegInit(0.U(8.W))
when(cntReg === N) {
cntReg := 0.U
}.otherwise {
cntReg := cntReg + 1.U
}
当然了,我们也可以用一个Mux来代替这种when/otherwise结构:
val cntReg = RegInit(0.U(8.W))
cntReg := Mux(cntReg === N, 0.U, cntReg + 1.U)
上面的是向上计数的计数器,我们我们想从某个最大值值开始向下计数,直到0,再复位到最大值,也就是个循环倒数的计数器,用Chisel实现如下:
val cntReg = RegInit(N)
cntReg := Mux(cntReg === 0.U, N, cntReg - 1.U)
如果在项目中,计数器需要在很多地方用到,但它们的最大值可能不一样,那我们可以写一个带参数的函数来生成计数器,这种方法在Chisel开发中很常:
// 返回计数器的函数
def genCounter(n: Int) = {
val cntReg = RegInit(0.U(8.W))
cntReg := Mux(cntReg === n.U, 0.U, cntReg + 1.U)
cntReg
}
// 可以直接用这个的函数创建各种不同上限的计数器
val counter10 = genCounter(10)
val counter99 = genCounter(99)
genCounter函数的最后一行是函数的返回值,这里返回的就是cntReg。
需要注意的是,上面所有计数器的例子中,计数器的值都是在0到N间的,包括0和N。如果们需要计数10个时钟周期,那我们就应该用一个0到9的计数器,把N设置为10的话就出问题了。
用计数器生成时序
计数器除了用于统计事件,还常用于生成新的时钟概念(作为墙上时钟时间的时间)。对于同步电路而言,时钟频率是固定的,电路会在每个时钟周期前进。所以说在数字电路中是没有时间的概念的,我们只能数时钟周期数。如果我们知道时钟频率,我们就可以生成定时发生的事件,比如以某个频率闪烁LED灯的电路,又比如某位读者在前面博文中提到的LED流水灯的电路。
通常的方法是用频率 f t i c k f_{tick} ftick来生成我们的电路中所需要的单周期的tick,这个tick会每n个时钟周期发生一次,其中 n = f c l o c k / f t i c k n=f_{clock}/f_{tick} n=fclock/ftick,tick就是得到的一个时钟周期的长度。这个tick并不会作为派生的时钟,而会作为逻辑上以 f t i c k f_{tick} ftick为频率工作的电路中寄存器的使能信号。下面的图就是每三个时钟周期一个tick的例子:

在下面的代码里面,我们描述了一个从0计数到N-1的计数器,达到最大值的时候tick的值会维持一个时钟周期的true,然后计数器复位为0,而在这个从0计数到N-1的过程中我们就生成了一个每N个时钟周期发生的逻辑tick:
val tickCounterReg = RegInit(0.U(32.W))
val tick = tickCounterReg === (N-1).U
tickCounterReg := Mux(tick, 0.U, tickCounterReg + 1.U)
这里每n个时钟周期发生的tick的逻辑时序可以用于驱动我们电路中其他以这个更慢的逻辑时钟工作的部分。比如下面的代码中,我们基于上面的tick构造了一个每n个时钟周期自增一次的慢计数器:
val lowFrequCntReg = RegInit(0.U(4.W))
when (tick) {
lowFrequCntReg := lowFrequCntReg + 1.U
}
下图是这个慢计数器和tick的波形图:

那这种更慢的逻辑时钟用处挺大的,比如用于LED灯的闪烁和流水灯、生成串口总线的波特率、生成用于七段数码管显示器多路复用的信号、用于按钮和开关去抖动的输入下采样等。
虽然宽度推理会给出寄存器的宽度,但是最好还是显式指定寄存器的宽度和类型,这个已经提过很多次了。显式的宽度定义可以避免复位为0.U的时候,产生一个宽度为1的计数器的意外情况。
高手是怎么写计数器的
有时候我们会特别执着于优化,比如我们向设计给我们的计数器生成或tick生成设计一个高度优化的版本。标准的计数器会需要这些资源:一个寄存器、一个加法器(或者减法器)以及一个比较器。对于寄存器和加法器我们没什么可以优化的,但是比较器可以。如果我们向上计数,那每次都会和一个上限值进行比较,那会比较一个很长的串。比较器可以由位串中的0的反相器和一个大的与门构成。如果是向下比较到0,那这个零比较器直接就是个大的或非门,在ASIC里面那就比常数比较器更便宜一点了。而在FPGA里面,逻辑是通过查找表构造的,所以跟0比较还是跟1比较并没有区别,所以向上计数和向下计数也没有区别。
不过我们还是可以稍微优化一下的,在硬件设计里面有借鉴之处。目前不管向上计数还是向下计数,都是需要比较所有的位的。那我们如果从N-2计数到-1呢?负数的最高有效位是1,而正数的最高有效位是0,那么我们只需要检查这一位就可以检测到计数器是否到达-1了。那么优化的计数器来了,这是高手才能写出来的:
val MAX = (N - 2).S(8.W)
val cntReg = RegInit(MAX)
io.tick := false.B
cntReg := cntReg - 1.S
when(cntReg(7)) {
cntReg := MAX
io.tick := true.B
}
定时器
除了上面的循环计数器,我们还能创建一个只响一次的定时器。这种定时器就类似大家在厨房用的那种,比如煮个鸡蛋,设定倒计时十分钟,然后按一下开始,十分钟后响铃。数字电路中的定时器加载的时间是时钟周期数,会向下计数直到0,而到0的时候定时器就会设置一个完成信号了。下图就是定时器的示意图:

寄存器可以通过设置信号load从din加载一个值。当load信号置零的时候,会选择向下计数的值作为next(即cntReg - 1)。当计数器达到0的时候,信号done会被设置,计数器会通过选择0作为输入来停止计数。
下面的代码就是定时器的Chisel实现:
val cntReg = RegInit(0.U(8.W))
val done = cntReg === 0.U
next = WireDefault(0.U)
when(load) {
next := din
} .elsewhen (!done) {
next := cntReg - 1.U
} .otherwise {
next := 0.U
}
cntReg := next
这里我们用到了一个8位的寄存器reg,复位值为0.U。布尔值done是reg的值与0.U比较的结果。为了寄存器输入的Mux,我们引入了一个线网next,其默认值为0.U,然后when/elsewhen语句块引入了Mux对应的输入和选择功能。信号load的优先级高于自减的选择。最后一行代码把多选器的输入next和寄存器reg的输入连接了起来。
如果我们希望代码更简洁的话,我们可以直接把多选的结果赋值给reg,不需要使用中间的线网变量next。
用计数器实现脉冲宽度调制
脉冲宽度调制(PWM,Pulse-Width Modulation)是一个信号处理的术语,用于将信号调制为常量周期且占空比在一定范围内的信号。
下图是一个PWM信号:

图中箭头所指之处都是脉冲周期的起始位置。信号为high的时间在周期中所占的比例,也叫做占空比。在前两个脉冲周期,占空比为25%,接着两个是50%的,最后两个是75%的,脉冲宽度被调制在25%到75%之间。
给PWM信号加上一个低通滤波器可以得到一个简单的DA转换器(数模转换器),这个低通滤波器可以与电阻和电容一样简单。
下面的代码会每十个时钟周期生成三个时钟周期的high信号:
def pwm(nrCycles: Int, din: UInt) = {
val cntReg = RegInit(0.U(unsignedBitLength(nrCycles-1).W))
cntReg := Mux(cntReg === (nrCycles-1).U, 0.U, cntReg + 1.U)
din > cntReg
}
val din = 3.U
val dout = pwm(10, din)
上面代码中,pwm函数是PWM生成器,可复用且轻量级。这个函数有两个参数,一个是Scala整数用于配置PWM的时钟周期数(nrCycles),另一个是Chisel线网变量din用于给定占空周期数,即PWM输出信号的脉冲宽度。我们用一个Mux和一个寄存器来表达计数器。函数的最后一行比较计数器的值和输入值din进行比较,依次来返回PWM信号。还是老样子,Chisel函数的最后一行表达式就是返回值,本例就是将线网连接到了一个比较器上。
再看看一些细节,初始化寄存器的时候我们用到了unsignedBitLength(n)函数,来指定寄存器cntReg需要表示n及n以下的无符号整数所需的位数(即 ⌊ l o g 2 ( n ) ⌋ + 1 \lfloor log_2(n)\rfloor+1 ⌊log2(n)⌋+1)。Chisel里面还有signedBitLength函数,用于有符号数的情况。
另一个PWM的应用场景是LED呼吸灯,这种情况下眼睛就充当了低通滤波器的角色。下面的例子是使用三角形函数(不是三角函数!)驱动上面的pwm函数扩展来的,可以得到一个可持续变化强度的LED灯:
val FREQ = 100000000 // 一个100MHz的时钟输入
val MAX = FREQ/1000 // 1kHz
val modulationReg = RegInit(0.U(32.W))
val upReg = RegInit(true.B)
when (modulationReg < FREQ.U && upReg) {
modulationReg := modulationReg + 1.U
} .elsewhen (modulationReg === FREQ.U && upReg) {
upReg := false.B
} .elsewhen (modulationReg > 0.U && !upReg) {
modulationReg := modulationReg -1.U
} .otherwise {
upReg := true.B
}
// modReg除以1024,约等于1kHz
val sig = pwm(MAX, modulationReg >> 10)
看起来有点复杂了,下面解释一下。我们用到了两个寄存器来调制,一是modulationReg,用于向上、向下计数,二是upReg作为向上或向下计数的标志位。我们向上计数到我们的时钟输入(本例中为100MHz),再向下计数到0,这就得到了一个0.5Hz的信号,即一秒上,一秒下,两秒一个周期。代码里面很长的when/elsewhen/otherwise代码块用于向上向下计数和方向变换。
由于我们的PWM要生成1kHz的信号,所以我们需要把调制信号除以1000。又因为除法的实现非常昂贵,所以我们这里用了一个移位来代替,因为 2 10 = 1024 2^{10} = 1024 210=1024和1000非常接近。前面我们已经定义了PWM生成器函数,这里就可以直接调用函数来实例化一个了。线网sig就表示调制好了的PWM信号。
如果现在把sig信号作为控制LED通断的信号,那么这个LED灯会每 1 / 1000 1/1000 1/1000秒亮一次,亮的时间(脉冲宽度)在01秒内递增,在12秒内递减,这个高频率的点亮时间的变化会在人眼形成亮度的变化,也就是在01秒内亮度递增,在12秒内亮度递减。要注意这里的脉冲宽度的变化是线性的,即周期三角波一样的,有兴趣的可以把信号的示意图画出来,我这里简单地画了一个,有点抽象,但今天已经写了很多了,这张图就不做标记了,大家可以自己去尝试理解。蓝色部分就是LED灯亮的部分,可以看到每个 1 / 1000 1/1000 1/1000秒亮的时间是越来越长的。

结语
这一篇主要学习了计数器以及基于计数器延伸出来的高级用法,比如生成时序、定时器、PWM等等。内容有点丰富,理解上难度也逐渐有了难度,还是需要好好理解消化的。不过对于处理器设计而言,最难理解的PWM部分其实一般是用不到的,但实现DSP的话可能就用得到了。如果这一篇文章的内容都理解了,那肯定是没有任何坏处的,对Chisel和数字设计的理解也会更进一步。相比较而言,下一节说的移位寄存器就简单多了,但在数字设计中也是很重要的存在,一点要认真学。
边栏推荐
- 【CTF】ciscn_ 2019_ es_ two
- 七月集训(第13天) —— 双向链表
- 通俗易懂!图解Go协程原理及实战
- Google Earth engine app - maximum image synthesis analysis using S2 image
- Day 7 of learning C language
- C language exercises
- The command prompt cannot start mysql, prompting system error 5. Access denied. terms of settlement
- 【微信小程序】农历公历互相转换
- 命令提示符启动不了mysql,提示发生系统错误 5。拒绝访问。解决办法
- July training (day 16) - queue
猜你喜欢

一骑入秦川——浅聊Beego AutoRouter是如何工作

给自己写一个年终总结,新年快乐!

Eureka delayed registration of a pit

flash闪存使用和STM32CUBEMX安装教程【第三天】

NCCL (NVIDIA Collective Communications Library)

曝光一位出身寒门的技术大佬

Quick apply custom progress bar

如何在树莓派上安装cpolar内网穿透

Read the paper learning to measure changes: full revolutionary Siamese metric networks for scene change detect

How to install cpolar intranet penetration on raspberry pie
随机推荐
Google Earth engine app - print the coordinates of points to the console and map, set the style and update it
The whole process of principle, simulation and verification of breath lamp controlled by FPGA keys
七月集训(第15天) —— 深度优先搜索
July training (day 18) - tree
A ride into Qinchuan -- a brief talk on how beego Autorouter works
内存墙简介
工程材料知识点总结(全)
七月集训(第08天) —— 前缀和
通俗易懂!图解Go协程原理及实战
Nacos配置中心动态刷新数据源
【CTF】ciscn_ 2019_ es_ two
NCCL (NVIDIA Collective Communications Library)
[cloud native] how can I compete with this database?
九种方式,教你读取 resources 目录下的文件路径
NCCL 集合通信--Collective Operations
2068. 检查两个字符串是否几乎相等
July training (day 21) - heap (priority queue)
吃透Chisel语言.25.Chisel进阶之输入信号处理(一)——异步输入与去抖动
[raspberry pie] box related manual-4 web agent
6S parameters