二进制速率乘法器(BRM)原理、Verilog实现与工程实战
做数字逻辑这些年,有一个词我很少在年轻工程师的代码里看到,但每次翻开老手册、维修老仪器的时候总会撞见——Binary Rate Multipliers,二进制速率乘法器。我第一次真正需要它,是做一个电机控制板的时候:主频固定12MHz,但我需要输出一个可以在一定范围内任意设定倍率的脉冲序列,普通计数器分频只有整数档,PLL又太贵,翻到一个1970年代的TTL数据手册,里面SN74167让我一下子开了窍。这东西的能力一句话就能说清:每M个输入时钟里,精确地输出N个脉冲,N是一个可编程的二进制数,硬件成本只有一个计数器加一串与门。
如果你做FPGA、做仪器仪表、做运动控制,或者只是对“非整数分频”这件事有兴趣,这篇文章应该能帮你省下不少摸索时间。我会从原理、Verilog实现、波形实测、常见大坑到选型对比一路讲完,尽量把那些数据手册里不会写的东西也翻出来。
1. 为什么需要BRM:从固定分频到任意比率脉冲速率乘法
1.1 计数器分频的硬边界
普通分频器,不管是用计数器还是用级联触发器,本质上只能做一件事:把输入频率除以一个整数。你搭一个模10计数器,输出就是f_in/10;想变成f_in/3,得重新设计计数状态;想变成f_in/7.5,抱歉,一个计数器做不到。
这还不是最要命的。工程里的真实痛点往往是“比率需要动态调整”。比如步进电机控制里,进给倍率要从1.00调到1.25,再到2.50;又比如老式示波器的时基电路,扫描时间要在一个范围内连续可调;还比如串行数据传输里,波特率要按外部系数微调。这些需求用固定模计数器就完全抓瞎了。
有人会说,那用可编程计数器不就行了?当然可以,预置模计数器能改变N值,但每改变一次分频比,输出脉冲的相位就彻底乱掉一次,对于后续的边沿计数、锁相环路,这种跳变完全是灾难。
1.2 一个更“像素化”的需求场景
换个角度看问题。很多应用其实不要求输出一个漂亮的、占空比固定的时钟。它们要的只是“一段时间内的平均频率成比例”。
举个例子。我给一个运动控制器写脉冲输出逻辑:基础时钟是1MHz,系统需要在一个16位寄存器里设定输出速率,设定值从1到65535,输出频率就是setup/65536 * 1MHz。也就是说,每65536个基础时钟里,要输出setup个脉冲。
这个功能和“分频”很不一样。它更像“按比例抽取”或者“脉冲计数”。脉冲本身不需要严格等间隔,只要在窗口内总数对就行。这个需求如果用计数器硬造均匀时钟,那就要上一大堆DDS/NCO逻辑;但如果你只想在大量时钟里挑出一部分作为输出,那么一个简单的组合译码器就够了。这就是BRM的思想原点。
1.3 老派TTL芯片里藏着的答案
BRM其实上世纪60年代就有了。TTL时代的SN74167是十进制速率乘法器,SN74168是二进制速率乘法器,它们输入一个时钟、一个4位控制字,输出脉冲数等于控制字的值除以最大计数值再乘以输入时钟频率。内部结构极其简单:一个同步计数器 + 几个译码门 + 一个或门。没有锁相环,没有延迟锁定,没有高大上的相位累加器。
这种老东西在今天看反而有种惊艳感,因为它用最少的门做到了“任意二进制比率”的脉冲速率调整。下面我把它搬进FPGA,一步一步拆开看内部到底在做什么。
2. BRM背后的数学:每一位二进制都是独立的子速率
2.1 子速率拆解:权重位的频率分配
假设计数器位宽是k,那么它每2^k个周期循环一次。我们希望的控制字是rate[0]到rate[k-1],总计N = rate,输出每2^k个周期里有N个脉冲。
关键技巧在这里:不要把N当成一个整体去“产生”,而是把N拆成二进制位,每一位对应一个独立的子脉冲序列。
以k=4为例,16个周期里:
- rate[3]这一位(权重8),如果为1,就贡献8个脉冲,也就是每2个时钟出1个,平均频率是f/2;
- rate[2]这一位(权重4),如果为1,就贡献4个脉冲,每4个时钟出1个,平均频率f/4;
- rate[1]这一位(权重2),贡献2个脉冲,平均频率f/8;
- rate[0]这一位(权重1),贡献1个脉冲,平均频率f/16。
当rate=0b0101(十进制5)时,输出总脉冲数 = 4 + 1 = 5,平均频率 = 5/16 * f。当rate=0b1001(十进制9)时,输出总脉冲数 = 8 + 1 = 9。每一位独立贡献,最后加在一起,这个概念和二进制加法完全对应。
2.2 互斥窗口是怎么构造出来的:从MSB找第一个0
现在的问题是:这些子脉冲序列怎么互相不打架?如果设计糟糕,两个子序列的脉冲落在同一个时钟周期里,那或门输出的脉冲数就会丢,总数就不对了。
经典BRM的解法很妙:不是让每一位独立产生脉冲再想办法避让,而是让每一位对应计数器的一个“专属窗口”,这些窗口在计数循环里天然互斥。
看4位计数器cnt从0到15循环,我定义四个匹配信号:
- match[3] = 1 当 cnt[3] == 0,即cnt=0到7,共8个周期;
- match[2] = 1 当 cnt[3] == 1 且 cnt[2] == 0,即cnt=8到11,共4个周期;
- match[1] = 1 当 cnt[3:2] == 2'b11 且 cnt[1] == 0,即cnt=12到13,共2个周期;
- match[0] = 1 当 cnt[3:1] == 3'b111 且 cnt[0] == 0,即cnt=14,共1个周期。
这种匹配方式,通俗说就是“从MSB往LSB方向找第一个为0的位”。比如cnt=0b1001,高位从第3位看是1,第2位是0,于是match[2]=1,其他match都为0。每个计数值(除了全1)恰好只匹配一个位。这样16个状态里,match[3]有8次,match[2]有4次,match[1]有2次,match[0]有1次,四者完全错开。
输出逻辑就一句话: pulse = (match[3] & rate[3]) | (match[2] & rate[2]) | (match[1] & rate[1]) | (match[0] & rate[0]);
因为match互斥,这条或门在任何时刻最多只有一个输入为1,输出脉冲数严格等于rate分解出的子脉冲数之和。
2.3 一个具体例子:4位BRM输出9个脉冲的位置
别急着写代码,我们先用手算一个例子,把输出模式看清楚。
设rate = 0b1001,也就是rate[3]=1,rate[0]=1。在一个16周期循环里:
- match[3] = 1 出现在cnt=0到7,这8个周期全部输出脉冲;
- match[0] = 1 出现在cnt=14,输出1个脉冲;
- 其他match为0,不输出。
所以输出波形是:连续8个脉冲,然后7个时钟的空白,到第14个周期再出一个脉冲,然后跳过全1状态,回到0开始下一轮。
这个模式很有意思,它和“均匀间隔”差得远,但16个时钟里精确地出了9个脉冲,平均值是正确的。这就是BRM的特点:平均频率准确,瞬时相位不保证均匀。做运动控制时,只要执行器本身有机械惯性,这种“先连发后停顿”的模式完全可以接受;做通信时钟时,就要小心了,后面我专门讲这件事。
2.4 平均频率与瞬时相位的两个层面
我把BRM的输出看成两层:
第一层是平均频率。给定k位计数器,输出平均频率 = rate / 2^k * f。这个是精确的,不受脉冲位置影响,只要计数窗口足够长,比例就不会漂移。
第二层是瞬时脉冲位置。脉冲位置的分布由计数器的状态决定,是一个固定模式,模式周期等于2^k个时钟。对于k=4,模式重复周期是16;对于k=8,模式周期是256。
理解这两层的分离很重要。很多工程师第一次看到BRM波形会觉得“坏了,脉冲都不均匀”,其实它根本没承诺均匀。它的设计目标就是“用极少的硬件,获得精确的平均速率”,而不是“产生一个干净的时钟”。想清楚这一点,后面所有选型问题都迎刃而解。
3. 从零写一个可综合的BRM模块(Verilog)
3.1 端口与参数设计
直接看可综合的RTL。我习惯把位宽做成参数,默认8位:
module brm #( parameter K = 8 )( input wire clk, input wire rst_n, input wire [K-1:0] rate, // 控制字,0 ~ 2^K-1 output reg pulse // 输出脉冲 );clk是你自由运行的参考时钟,rst_n是异步复位,rate是二进制速率控制字,pulse是输出脉冲。pulse不是每个clk都有,它在满足条件的老时钟周期里拉高一个clk。
3.2 第一个0检测器的生成
计数器部分没什么好说:
reg [K-1:0] cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) cnt <= {K{1'b0}}; else cnt <= cnt + 1'b1; end组合译码部分,用generate生成“从高到低找第一个0”的匹配信号:
wire [K-1:0] match; genvar i; generate for (i = K-1; i >= 0; i = i - 1) begin : gen_match if (i == K-1) begin // MSB:本身就是最高位,只看它是否为0 assign match[i] = ~cnt[i]; end else begin // 高位全1,且本位为0 assign match[i] = &cnt[K-1:i+1] & ~cnt[i]; end end endgeneratematch[K-1]到match[0]在任何时刻最多只有一个为1,这是前面2.2节分析的那个互斥性质。
输出脉冲的生成,我建议加一级寄存器:
always @(posedge clk or negedge rst_n) begin if (!rst_n) pulse <= 1'b0; else pulse <= |(match & rate); end这里的rate是组合输入。如果rate和clk是同一个时钟域,而且保持稳定,那没问题。如果rate会在运行中变化,我建议先做同步,避免match和rate的与操作在组合逻辑里产生亚稳态。后面第5章有完整说明。
到这里,一个可综合的BRM模块就完成了,大约25行代码。它没有任何抖动、没有任何状态机,只是一个计数器加一组与门,消耗资源可以忽略不计。
3.3 为什么输出要打一拍寄存器,直接组合输出会有什么问题
有些教材上的BRM是组合输出,脉冲直接由match和rate与出来,我当时也这么干过,然后就被毛刺教训了。
问题是:match是计数器当前状态的译码,而计数器在时钟上升沿会多个bit一起翻转。比如cnt从0b0111变成0b1000,4个bit同时变化。在这变化瞬间,组合逻辑的输出可能会闪一下,产生一个很窄的毛刺。这个毛刺宽度可能只有几百皮秒,但就足够让后级触发器的边沿误动作了。
所以,宁可多花一个寄存器把pulse同步输出,让脉冲宽度严格等于一个clk周期。这样后级看到的脉冲,对边沿是干净的。代价只是脉冲在时间轴上延迟了一个时钟周期,这对绝大多数速率控制类应用完全无所谓。
3.4 仿真激励与波形验证
写一个最简单的testbench,验证“每16个时钟输出N个脉冲”这个核心性质。
module tb_brm; parameter K = 4; reg clk, rst_n; reg [K-1:0] rate; wire pulse; brm #(.K(K)) dut ( .clk(clk), .rst_n(rst_n), .rate(rate), .pulse(pulse) ); initial begin clk = 0; forever #5 clk = ~clk; end integer window = 1 << K; integer count_pulse; integer i; initial begin rst_n = 0; rate = 4'd0; #20 rst_n = 1; rate = 4'd5; // 每16个时钟期望5个脉冲 count_pulse = 0; for (i = 0; i < window; i = i + 1) begin @(posedge clk); #1; if (pulse) count_pulse = count_pulse + 1; end $display("rate=5, count=%0d", count_pulse); $finish; end endmodule仿真结果应该打印rate=5, count=5。如果没有,那说明match生成逻辑有误,多半是“高位全1”的位宽算错了。
我把这个验证逻辑提炼成一句话:BRM对不对,不要看单周期脉冲波形,要看一个个固定窗口内的脉冲总数。窗口取2^K个clk,数出来的脉冲数必须严格等于rate。这个验证方法简单可靠,我在后面调动态更新、调级联的时候也一直在用。
4. 波形实测:输出脉冲模式里藏着什么东西
4.1 rate=0x01 和 rate=0x80 的极端模式
光有代码不算完,得把几个典型rate值的实际脉冲模式摆出来看。还是以4位为例。
rate=0x01时,只有match[0]使能,输出脉冲出现在cnt=14那一个周期,所以16个时钟里只有1个脉冲,而且位置固定在循环末尾。这个很好理解。
rate=0x80时,只有match[3]使能,match[3]对应cnt[3]==0,也就是cnt=0到7连续8个周期,输出8个连着的脉冲,然后cnt=8到15完全沉默。平均频率是f/2,但波形是50%占空比的方法,周期16。如果你要的是“每两个时钟出一个脉冲”那样均匀交替,BRM给不了。
这两个极端案例告诉我们:BRM的输出模式是“成块的”。当rate的某一位权重很大时,脉冲会集中在一段窗口里;当rate由多个位组合时,脉冲会分散一些但仍然有规律。
4.2 rate=0x55 的“周期性间隔抖动”现象
再来看一个更实际的例子,k=8,rate=0x55(十进制85)。这个值比较有代表性,因为它的多位同时使能,脉冲呈现一种看似随机、实际严格周期复现的间隔组合。
由于match的互斥结构,rate=0x55的输出是:高权重位的8个连续脉冲、4个连续脉冲、2个连续脉冲、1个脉冲,按照每个子窗口的时序错开拼在一起。整体看起来就是一串脉冲,中间夹着几个不等的空窗口,32个、64个之类的间隔规律性重复。脉冲间隔从1个clk到好几个clk都有,但256个时钟内总数严格等于85。
这种“间隔抖动”是BRM的原理特征,不是bug。如果你的下游电路能接受“一段时间内的平均脉冲数正确”,那它工作得很好;如果下游要求每个脉冲间隔接近恒定,BRM就不合适。
4.3 对比NCO:谁的脉冲间隔更均匀
我顺手在同一块FPGA上跑了一个8位相位累加器NCO做对比。NCO的思路是:每来一个clk,相位累加器加一个频率字freq,累加器溢出时输出一个脉冲。因为相位累加器等效于在0到2^K-1之间递增,溢出点相当均匀,所以输出脉冲间隔最多相差一个clk。
比如同样输出256个clk周期内85个脉冲,NCO的脉冲间隔几乎是1个clk和2个clk交替;BRM则是“连续一段脉冲、停一段、再连续一段”。所以从瞬时均匀性看,NCO明显优于BRM。但NCO需要K位加法器,K一旦到16、24,需要一堆寄存器,资源开销比BRM大一个数量级。
4.4 实际上要不要关心频谱和抖动
很多对时钟纯度敏感的场合,工程师一看到“抖动”两个字就紧张。我得说句实在话:BRM的输出本身就不是一个“时钟”,它只是一个“选通脉冲序列”。它接收方如果有机械惯性、有滤波电容、有平均化处理,那BRM完全够用。但如果接收方是PLL的参考输入、是ADC的采样时钟、是高速串行恢复电路,请直接放弃BRM,换DDS/NCO或者PLL。
这个判断标准我总结成一句话:你的系统是在“数脉冲”,还是在“用时钟沿”?前者BRM合适,后者别碰。
5. 绕过这些坑,BRM才能真正用起来
5.1 坑一:输出不是干净时钟
这是最大的坑。BRM输出的一串脉冲,占空比和间隔都是乱的,如果把它直接接到某个模块的clk端口,等于是在制造异步多时钟域,后续的时序约束、复位设计全都变得很麻烦。
我在工程上的习惯是:永远把BRM输出当作“使能信号”,而不是“时钟”。所谓使能信号,就是接入同步时序逻辑的CE端或者门控条件,让功能模块只在pulse有效的那一个周期去执行一步操作。这样整个设计仍然只有一个主时钟域,BRM只是用一种廉价的比率控制方式给功能模块发“心跳”。
5.2 坑二:全1控制字时输出变成直流
rate=2^K-1时,每个子窗口都会输出脉冲,除了计数器全1的那个状态没有匹配,其他每个周期都有脉冲。对于K=8,输出平均频率是255/256 * f,几乎等于连续时钟,脉冲之间的间隔就是0个clk。占空比接近100%,后级如果是边沿计数,看起来就是一直高电平,根本数不出脉冲。
如果你需要接近1的倍率,建议把最大rate限制在2^(K-1)以内,或者用两级BRM抬精度,或者直接换NCO。至少我踩过的坑里,这个“输出恒高”的问题在调试时很难一下看出来,因为它波形上不是没有脉冲,而是脉冲太密了,频谱仪上看像直流。
5.3 坑三:动态更新控制字导致的脉冲毛刺
运行中修改rate,如果没有任何保护,可能发生一个非常隐蔽的错误:本来这个周期的match对应rate的某一位是0,但rate更新后变成1,于是这个周期突然多出/少了一个脉冲。数量级上差一个脉冲可能无所谓,但如果在精密计数、电机细分或者累加器控制的场合,一个脉冲就是一步进,积累起来就是位置误差。
解决办法是同步更新:把rate的写入放在计数器回到0的那一拍,或者至少让rate的更新沿和计数器状态对齐。常见做法是用一个状态寄存器,在主控写入新rate后先缓存,在cnt==0时再锁存到实际使用的rate_ff。这样BRM的输出模式总是从某个完整窗口边界开始切换,不会在中间被拦腰截断。
5.4 坑四:级联时被忽略的跨时钟问题
想要更大范围或者更高分辨率,自然会想到把两个BRM级联。比如第一个BRM的输出作为第二个BRM的参考时钟,这样一个BRM的rate可以作为另一个BRM的窗口使能。这个思路没错,但直接用第一个BRM的pulse去当第二个BRM的clk,就踩了5.1的坑。
正确做法是:让第一个BRM的pulse作为第二个BRM的使能信号,第二个BRM仍在原始主时钟域计数。也就是说,两个BRM的clk都接同一个主时钟,用上层输出作为下层的工作窗口。这样时序是可综合的,仿真是可预测的,不会出现跨时钟亚稳态。
我早年做级联时直接在两个模块间连了pulse到clk,仿真波形看着也对,上板就随机丢脉冲,查了一天,最后发现是毛刺。从那以后,我所有BRM相关设计都有一条铁律:pulse绝不出模块当时钟用。
6. 实际选型:BRM、PLL、NCO之间怎么挑
6.1 资源与精度角度
把BRM、NCO、PLL放在一起比,核心差异在于“你要的频率合成精度和相位质量”值多少门。
BRM的优势是极低资源:几个触发器加一组译码门,在FPGA里甚至可以忽略不计。缺点是瞬时抖动大,频率分辨率受限于K位,一般来说做到16位就是极限了。
NCO的优势是频率分辨率可以做到24位、32位,相位连续,脉冲间隔相对均匀。缺点是K位加法器加K位累加器,资源随位数线性涨。一个32位NCO轻松消耗上百个触发器,对低端FPGA不算小数。
PLL/MMCM的优势是输出干净、边沿质量高、抖动低到皮秒级。缺点是只能做整数分频/倍频,小数分频要靠SDM调制,锁相环响应慢,也不能快速任意切换频率。
6.2 延迟与相位连续性角度
如果你的系统需要快速跳频,比如雷达、跳扩频通信,PLL的环路锁定时间是个大问题;NCO和BRM都是开环结构,跳频是瞬间的。但BRM跳频瞬间的相位是乱的,NCO因为相位累加器有记忆,跳频后波形相位连续,这在高保真信号合成里非常重要。
所以我的个人选型习惯是:
- 要整数分频且质量高:选PLL/MMCM;
- 要任意小数比率且输出均匀:选NCO;
- 只做脉冲速率比例缩放、硬件预算极低:选BRM;
- 想兼顾:BRM+NCO混着用,BRM做粗选频,NCO做细调。
6.3 一张表搞定选型标准
| 维度 | BRM | NCO | PLL/MMCM |
|---|---|---|---|
| 输出类型 | 选通脉冲,非标准时钟 | 脉冲序列,间隔较均匀 | 连续标准时钟 |
| 频率分辨率 | 1/2^K × f | 1/2^M × f,M可做大 | 受限于反馈分频整数 |
| 瞬时抖动 | 大,脉冲成块 | 小,间隔最多差1个clk | 极小 |
| 相位连续性 | 跳变时可能乱 | 天然连续 | 跳频需锁定时间 |
| 资源消耗 | 很少 | 中等,随M线性涨 | 专用模拟/数字块 |
| 典型场景 | 电机速率、仪器脉冲输出 | 通信信号源、软件无线电 | CPU时钟、高速接口时钟 |
6.4 现实中的混合做法
在实际项目里,我很少单独只用一种。最常用的一套组合是:主机CPU通过SPI写入一个频率字,FPGA内部先用NCO产生一个精细可调的溢出脉冲流,再用BRM把这个溢出脉冲流的速率乘以一个固定倍率,得到最终输出。这样既拿到了NCO的精细分辨率,又不需要把NCO位宽做到极大,因为BRM负责把你关心的窗口范围内的脉冲数精确投影出来。
当然,这种混合方案增加了仿真和验证的复杂度。每加一级脉冲处理,都要重新检查“窗口内脉冲总数是否还等于设定值”。好在BRM的数学性质在这里非常稳定,只要保证每一级的控制字都按窗口边界对齐更新,结果就差不了。
最后再说两句
写这篇文章之前,我特意把仓库里那本70年代的TTL数据手册翻了出来,SN74168的逻辑图还在,74系列的教科书上画的还是分立晶体管。几十年过去,门电路变成了查找表,工具链从原理图变成了Verilog,但“一个计数器加一串译码门就能把二进制比率变成脉冲序列”这个思想,依然在无数设备里悄悄工作着。
如果让我总结一条最有用的经验,那就是:不要拿BRM的输出当时钟,把它当使能信号,它能陪你走很远;非要把脉冲接成时钟,它就会用毛刺和亚稳态教育你。第一次调通BRM时那种“原来这么简单”的感觉,我到现在还记得。希望这篇也能给你一样的体验。
