资讯动态

FPGA二进制转BCD码:Verilog实现移位加三算法详解

发布时间:2026/8/24 5:17:07 来源:尧图企业网站定制
1. 项目概述从二进制到BCD一个FPGA工程师的“翻译”日常在数字电路和嵌入式系统里我们常常需要把内部处理的二进制数据转换成人类习惯的十进制数字显示出来。比如你用FPGA做了一个频率计测出来一个频率值是10101101二进制等于十进制的173你总不能在七段数码管上直接显示这个二进制数吧这时候就需要一个“翻译官”——二进制转BCD码的模块。这个项目就是用Verilog HDL在FPGA上实现这个“翻译”功能。听起来简单不就是个进制转换吗但当你真正动手去写尤其是要考虑如何用硬件并行的思维去高效、省资源地实现时里面门道就多了。无论是做仪器仪表、工业控制面板还是任何带数字显示的人机交互界面这个模块都是基础中的基础。今天我就结合自己踩过的坑和优化心得把这个过程掰开揉碎了讲清楚。2. 核心原理与方案选型为什么不用除法器2.1 BCD码与二进制数的本质区别首先得明确我们在转什么。二进制Binary大家都很熟逢二进一每一位的权重是2^n。而BCD码Binary-Coded Decimal中文叫“二-十进制编码”它的核心思想是用4位二进制数来表示一位十进制数0-9。比如十进制数23用BCD码表示就是0010 0011这里0010是20011是3。关键问题来了二进制数10111十进制23直接看和BCD码0010 0011的位模式完全不同。转换的难点在于十进制是逢十进一而二进制是逢二进一这两种进位机制在硬件上没有直接的对应关系。你不能简单地对二进制数做“除以10取余”的运算因为除法器在FPGA里是非常消耗资源查找表LUT和触发器FF且时序较差的单元对于高速或资源敏感的应用我们需要更“硬件友好”的算法。2.2 三种主流转换算法深度对比在实际工程中主要有三种算法选择哪一种取决于你的需求速度、面积、代码复杂度。1. 移位加三算法Double Dabble Algorithm这是最经典、最常用的方法特别适合用硬件描述语言实现。它的核心思想是“左移并条件加三”。工作原理把待转换的二进制数位串向左移位依次移入BCD结果寄存器。在每次左移后检查每一个BCD位每4bit为一个BCD位是否大于等于5即二进制0101如果是则给这个BCD位加上3二进制0011然后再进行下一次左移。重复这个过程直到所有二进制位都移完。硬件对应这个过程完美对应了硬件上的移位寄存器和加法器。检查“是否≥5”是一个简单的组合逻辑比较加3也是一个小的加法操作。整个流程可以通过一个状态机或简单的计数器控制循环完成无需使用除法器。优点资源消耗相对固定且较少易于流水线化以提高吞吐率理解直观。缺点转换速度与二进制数的位宽成正比对于很宽的数据如32位以上需要较多的时钟周期。2. 查找表法LUT-Based对于位宽不大的输入例如8位或12位以内可以直接使用查找表。工作原理事先计算出所有可能的二进制输入值所对应的BCD码结果将这个映射关系存储在FPGA的Block RAM或分布式RAM中。转换时直接将二进制数作为地址输入从RAM中读出对应的BCD值。硬件对应直接利用FPGA丰富的存储资源。优点速度极快一个时钟周期即可出结果时序性能最好。缺点资源消耗随输入位宽指数级增长。一个8位输入256种可能的查找表还算可以但一个16位输入65536种可能的查找表将占用巨大内存通常不实用。3. 基于加法器的迭代算法这类算法通过一系列的加法和移位操作来模拟除法运算例如通过加6修正因为二进制每4位逢16进1而BCD是逢10进1差值为6。工作原理从二进制数的高位开始尝试减去10的倍数通过判断差值来决定BCD的每一位。实现上可能包含多个减法器和比较器。硬件对应由多个加法器/减法器和数据选择器构成。优点有些变体算法在特定位宽下可能比移位加三法周期数少。缺点控制逻辑相对复杂代码可读性不如移位加三法且资源优化不一定明显。我的选型建议对于通用的、尤其是位宽可变的场景移位加三算法是平衡性最好的选择。它体现了硬件设计的精髓也是面试和学习的重点。因此本项目将重点详解如何用Verilog实现一个参数化、可重用的移位加三算法模块。3. 模块设计与接口定义打造一个可重用的IP核一个好的模块设计应该像一块乐高积木接口清晰、功能明确、便于集成。我们不写“一次性”代码。3.1 接口信号定义我们先定义模块的输入输出端口。这里设计一个同步、带握手信号的模块便于在系统中集成。module bin2bcd #( parameter BIN_WIDTH 16, // 输入二进制数的位宽默认16位 parameter DEC_DIGITS 5 // 输出BCD码的十进制位数根据BIN_WIDTH计算 ceil(BIN_WIDTH*log10(2)) ) ( input wire clk, // 系统时钟 input wire rst_n, // 低电平异步复位 input wire start, // 转换启动信号高电平有效 input wire [BIN_WIDTH-1:0] bin_data, // 输入的二进制数据 output reg [DEC_DIGITS*4-1:0] bcd_data, // 输出的BCD码每4位代表一个十进制位 output reg done, // 转换完成标志高电平有效一个周期 output reg busy // 模块忙标志转换过程中为高 );参数说明BIN_WIDTH这是核心参数决定了模块能处理的最大数值。例如16位二进制数最大值为65535对应5位十进制数。DEC_DIGITS输出BCD码的位数。它可以通过公式ceil(BIN_WIDTH * log10(2))预先计算好。例如16位二进制最大为655355位数所以DEC_DIGITS5。在实例化模块时我们可以根据BIN_WIDTH自动计算或手动指定这个值。端口说明start和done构成了一个简单的握手协议。上级电路在数据bin_data准备好后拉高start一个周期本模块开始转换。转换完成后拉高done一个周期此时bcd_data有效。这种设计避免了连续数据流的冲突。busy信号在转换过程中为高告知其他模块此时本模块不可接收新的start请求。3.2 内部状态机设计移位加三算法是一个多步骤的过程使用状态机FSM来控制是最清晰、可靠的方式。这里我们设计一个三段式状态机。localparam S_IDLE 2b00; // 空闲状态 localparam S_SHIFT 2b01; // 移位加三操作状态 localparam S_DONE 2b10; // 转换完成状态 reg [1:0] current_state, next_state; reg [BIN_WIDTH-1:0] bin_reg; // 二进制数移位寄存器 reg [DEC_DIGITS*4-1:0] bcd_reg; // BCD码移位寄存器 reg [$clog2(BIN_WIDTH)-1:0] shift_cnt; // 移位计数器计数到BIN_WIDTH次状态转移逻辑S_IDLE等待start信号。收到后将输入bin_data载入bin_regbcd_reg清零shift_cnt清零然后跳转到S_SHIFT。S_SHIFT核心操作状态。每个时钟周期完成一次“左移并条件加三”操作同时shift_cnt加1。当shift_cnt等于BIN_WIDTH时说明所有位都已处理完毕跳转到S_DONE。S_DONE拉高done信号一个周期将bcd_reg的值输出到bcd_data然后跳转回S_IDLE。设计心得使用$clog2(BIN_WIDTH)来定义shift_cnt的位宽是一个好习惯这能确保计数器宽度刚好够用不浪费寄存器资源。这是Verilog-2001标准引入的系统函数综合工具都支持。4. 核心算法Verilog实现与仿真4.1 “移位加三”操作的硬件描述这是整个模块的精华所在。我们需要在一个时钟周期内完成对bin_reg和bcd_reg的联合左移并对bcd_reg中的每一个BCD位每4位进行条件加三修正。// 在S_SHIFT状态中执行的组合逻辑块或时序逻辑的赋值部分 always (posedge clk or negedge rst_n) begin if (!rst_n) begin // ... 复位操作 end else if (current_state S_SHIFT) begin // 第一步将{bin_reg, bcd_reg}整体左移1位 {bin_reg, bcd_reg} {bin_reg, bcd_reg} 1; // 第二步对左移后的bcd_reg的每一个十进制位进行条件加三 // 这是一个典型的循环展开因为循环次数DEC_DIGITS在综合时是确定的。 // 以下以DEC_DIGITS5为例手动展开循环 // 检查并修正个位bcd_reg[3:0] if (bcd_reg[3:0] 4) bcd_reg[3:0] bcd_reg[3:0] 3; // 检查并修正十位bcd_reg[7:4] if (bcd_reg[7:4] 4) bcd_reg[7:4] bcd_reg[7:4] 3; // 检查并修正百位bcd_reg[11:8] if (bcd_reg[11:8] 4) bcd_reg[11:8] bcd_reg[11:8] 3; // 检查并修正千位bcd_reg[15:12] if (bcd_reg[15:12] 4) bcd_reg[15:12] bcd_reg[15:12] 3; // 检查并修正万位bcd_reg[19:16] if (bcd_reg[19:16] 4) bcd_reg[19:16] bcd_reg[19:16] 3; // 移位计数器加1 shift_cnt shift_cnt 1; end end关键点解析联合左移{bin_reg, bcd_reg}是将两个寄存器拼接成一个大的向量左移一位意味着bin_reg的最高位移入bcd_reg的最低位bin_reg自身左移最低位补0。这正是算法要求的“从二进制数中逐位移出”。条件加三if (bcd_reg[x:y] 4)就是判断该BCD位是否≥5。这里必须用“4”而不是“5”。因为这是一个组合逻辑条件判断发生在对bcd_reg的“新值”刚左移后的值进行评估。如果使用5在逻辑上等价但有些综合工具在特定优化下可能产生细微差异使用4是更常见的写法。并行操作所有BCD位的“检查并加三”操作是同时并行进行的这正是硬件并行的优势。注意这些if语句是并发执行的它们修改的是同一个bcd_reg的不同位段互不冲突。4.2 使用generate语句实现参数化上面的代码手动展开了5个BCD位。为了模块可重用我们必须使用generate for循环来根据DEC_DIGITS参数自动生成这些修正逻辑。// 使用generate块来循环生成条件加三逻辑 genvar i; generate for (i DEC_DIGITS-1; i 0; i i - 1) begin: bcd_correction always (posedge clk or negedge rst_n) begin if (!rst_n) begin // 复位在另一个always块处理这里主要描述逻辑 end else if (current_state S_SHIFT) begin // 注意这里的判断和加三操作必须基于左移操作“之后”的bcd_reg值。 // 在实际编写时通常将左移后的值存入一个中间变量再用这个变量进行判断和赋值。 // 更清晰的写法是分成两个步骤或者用组合逻辑描述条件用时序逻辑寄存结果。 end end end endgenerate在实际工程中为了避免时序逻辑中的复杂条件赋值更常见的做法是用组合逻辑计算“加三修正”的结果然后在时钟沿到来时将“左移后的值”与“修正值”进行选择最终寄存。这需要引入中间变量。4.3 一个完整的、可综合的示例代码片段下面给出一个更贴近实际实现的核心部分代码结构// 定义中间变量 reg [BIN_WIDTH-1:0] bin_next; reg [DEC_DIGITS*4-1:0] bcd_next; integer j; always (*) begin // 组合逻辑计算下一周期的值 // 默认赋值 bin_next bin_reg; bcd_next bcd_reg; if (current_state S_SHIFT) begin // 1. 执行联合左移 {bin_next, bcd_next} {bin_reg, bcd_reg} 1; // 2. 对bcd_next的每一位进行条件加三修正 for (j 0; j DEC_DIGITS; j j 1) begin if (bcd_next[j*4 : 4] 4) // “:”是位片选择语法表示从j*4开始宽度为4的向量 bcd_next[j*4 : 4] bcd_next[j*4 : 4] 3; end end end always (posedge clk or negedge rst_n) begin // 时序逻辑寄存下一周期的值 if (!rst_n) begin bin_reg 0; bcd_reg 0; end else begin case (current_state) S_IDLE: begin if (start) begin bin_reg bin_data; bcd_reg 0; end end S_SHIFT: begin bin_reg bin_next; bcd_reg bcd_next; shift_cnt shift_cnt 1; end // ... S_DONE状态 endcase end end重要提示for循环在可综合的Verilog中是可以使用的但前提是循环次数在综合时是确定的比如由参数DEC_DIGITS决定。综合工具会将其完全展开Unroll生成并行的硬件电路。上面的for (j0; jDEC_DIGITS; jj1)循环综合后就等同于我们之前手动写的5条if语句。4.4 使用ModelSim进行功能仿真代码写完了不上仿真验证就是“盲人摸象”。我们写一个简单的测试平台Testbench。timescale 1ns/1ps module tb_bin2bcd(); reg clk, rst_n, start; reg [15:0] bin_data; wire [19:0] bcd_data; // 5 digits * 4 20 bits wire done, busy; // 实例化被测模块 bin2bcd #(.BIN_WIDTH(16), .DEC_DIGITS(5)) uut ( .clk(clk), .rst_n(rst_n), .start(start), .bin_data(bin_data), .bcd_data(bcd_data), .done(done), .busy(busy) ); // 生成时钟周期10ns initial begin clk 0; forever #5 clk ~clk; end // 测试过程 initial begin // 初始化 rst_n 0; start 0; bin_data 0; #100; rst_n 1; #20; // 测试用例1二进制 16d12345 - BCD 20h12345 bin_data 16d12345; start 1; #10; start 0; wait(done 1); // 等待转换完成 $display(Test 1: Bin%d, BCD%h, bin_data, bcd_data); #20; // 测试用例2边界值 16d65535 - BCD 20h65535 bin_data 16d65535; start 1; #10; start 0; wait(done 1); $display(Test 2: Bin%d, BCD%h, bin_data, bcd_data); #20; // 测试用例30值 bin_data 0; start 1; #10; start 0; wait(done 1); $display(Test 3: Bin%d, BCD%h, bin_data, bcd_data); #20; $stop; end endmodule在ModelSim中运行这个测试平台观察波形。你应该能看到拉高start后busy信号立即变高。经过16个时钟周期因为BIN_WIDTH16done信号出现一个周期的高脉冲。在done有效时bcd_data上的值就是转换结果。例如12345二进制0011 0000 0011 1001应该被转换成0001 0010 0011 0100 0101即十六进制12345分别代表万位1、千位2、百位3、十位4、个位5。5. 性能优化与高级应用探讨一个基础的转换器做出来了但在实际项目中我们往往需要它更快、更省资源或者适应更特殊的场景。5.1 流水线化设计用面积换速度上面的状态机实现是顺序的处理一个N位数据需要N个时钟周期。如果系统要求高吞吐率比如每时钟周期都要转换一个新数据流水线Pipeline是唯一的选择。流水线思路将移位加三的N个步骤展开成N级流水线。每一级都是一个独立的组合逻辑块完成一次左移和条件加三后面跟一个寄存器。数据从第一级流入每一拍时钟向后移动一级第N拍后从最后一级流出结果。module bin2bcd_pipeline #( parameter BIN_WIDTH 16, parameter DEC_DIGITS 5 )( input wire clk, input wire [BIN_WIDTH-1:0] bin_data, output wire [DEC_DIGITS*4-1:0] bcd_data ); // 定义流水线寄存器数组 reg [BIN_WIDTH-1:0] bin_pipe [0:BIN_WIDTH]; reg [DEC_DIGITS*4-1:0] bcd_pipe [0:BIN_WIDTH]; // 初始化第一级 assign bin_pipe[0] bin_data; assign bcd_pipe[0] 0; genvar stage; generate for (stage 0; stage BIN_WIDTH; stage stage 1) begin: pipe_stage // 组合逻辑计算第stage级的“移位加三”结果 wire [BIN_WIDTH-1:0] bin_stage_out; wire [DEC_DIGITS*4-1:0] bcd_stage_out; // ... 此处是移位加三组合逻辑操作bin_pipe[stage]和bcd_pipe[stage] // 时序逻辑将结果寄存到下一级 always (posedge clk) begin bin_pipe[stage1] bin_stage_out; bcd_pipe[stage1] bcd_stage_out; end end endgenerate // 输出最后一级的结果 assign bcd_data bcd_pipe[BIN_WIDTH]; endmodule优缺点优点吞吐率极高每个时钟周期都能输出一个转换结果初始延迟后。缺点资源消耗大约为顺序实现的BIN_WIDTH倍。同时bin_data必须在连续的时钟周期内保持稳定或有效适用于数据流处理。5.2 资源优化共享加法器与状态编码对于顺序状态机实现我们可以进一步优化共享加法器条件加三操作需要多个4位加法器。如果时序不紧张可以考虑使用一个时间复用的加法器在一个状态中分时处理各个BCD位的加三操作。但这会增加控制复杂度并延长转换周期。紧凑状态编码如果状态机状态不多使用二进制编码Binary可能比独热码One-hot更省触发器。5.3 处理有符号数补码与小数我们的模块目前只处理无符号整数。实际应用中可能需要有符号数补码先判断输入数的符号位。如果是负数先取其绝对值按位取反加一进行转换最后在输出的BCD码前单独用一个信号表示负号如sign信号或使用BCD码的特殊格式如9的补码。定点小数例如一个16位数假设低8位是小数部分。转换前可以先将这个数乘以10^nn是想要显示的小数位数将其放大为整数然后送给整数转换模块。转换后的BCD码通过定点位置来确定小数点在哪里。乘法操作可以用移位和加法实现例如乘以10 乘以8 乘以2。6. 常见问题、调试技巧与实战心得6.1 仿真与调试中的“坑”输出全是X不定态原因最常见的原因是寄存器未初始化。在Testbench中确保在第一个时钟沿之前释放了复位信号rst_n从0变1。在设计中所有寄存器reg都应在复位条件下赋予明确的初值。检查在ModelSim波形中从时间0开始查看所有reg型信号确认复位后是否都被正确清零或初始化。转换结果错误高位总是0原因联合左移操作{bin_reg, bcd_reg} {bin_reg, bcd_reg} 1;的对象可能错了。你必须确保是对“当前周期”的bin_reg和bcd_reg进行左移。如果误用了bin_next和bcd_next可能导致数据通路混乱。调试在仿真中仔细对比每一个时钟沿前后bin_reg和bcd_reg的值变化是否符合预期bin_reg的最高位是否移到了bcd_reg的最低位bin_reg自身是否左移了条件加三逻辑没生效原因判断条件if (bcd_reg[x:y] 4)写在了{bin_reg, bcd_reg} ... 1;这条赋值语句之前。在Verilog的时序逻辑块always (posedge clk)中所有的非阻塞赋值是同时发生的并且使用的是该块开始执行时的右值。如果先判断再加三再左移逻辑就反了。必须是先左移再对左移后的新值进行判断和加三。解决如前文所述使用“组合逻辑计算下一拍值时序逻辑寄存”的两段式写法可以清晰地分离这个顺序避免混淆。done信号不拉高或拉高多次原因状态机跳转条件有误。例如从S_SHIFT跳转到S_DONE的条件是shift_cnt BIN_WIDTH注意shift_cnt是从0开始计数当它等于BIN_WIDTH时说明已经完成了BIN_WIDTH次移位。如果写成shift_cnt BIN_WIDTH-1就会早一个周期结束导致最后一位没处理。调试在仿真中观察状态机current_state、next_state以及shift_cnt的变化画出理想的状态转移图与之对比。6.2 综合与实现注意事项时序约束这个模块的组合逻辑路径主要在于“条件加三”的加法器和多路选择器。如果DEC_DIGITS很大比如10位十进制数这条路径可能较长。需要在Vivado/Quartus中为其添加合理的时序约束确保在目标时钟频率下能稳定工作。资源评估综合后查看综合报告中的资源使用情况LUT、FF、CARRY4等。顺序实现的主要资源消耗是bin_reg、bcd_reg和状态机。流水线实现的资源消耗会成倍增加。参数化验证用不同的BIN_WIDTH和DEC_DIGITS参数实例化你的模块分别进行综合和仿真确保其通用性。特别是验证边界情况如最大输入值、0值等。6.3 我的实战心得从行为级到RTL级初学Verilog时很容易写出像C语言一样“先除10取余再除10取商”的代码。这通常无法综合或者综合出的电路效率极低。移位加三算法是典型的“硬件思维”算法它用移位和加法模拟了除法和取余是学习硬件描述语言思维转换的绝佳例子。仿真先行在写任何稍微复杂的逻辑时一定要同步编写Testbench。哪怕一开始只测试一两个简单用例也能快速发现方向性错误。我习惯先画一个简单的波形图标明每个信号在关键时间点应该是什么值然后根据这个图来写测试激励。命名规范信号和变量名要有意义。bin_reg,bcd_reg,shift_cnt这样的名字半年后回头看代码你还能立刻明白它们是干什么的。避免使用a,b,tmp这种名字。考虑后端在设计初期就要考虑模块的接口握手信号和性能吞吐率、延迟。这个简单的bin2bcd模块通过添加start/done握手信号就能轻松集成到更大的数据流系统中。如果未来需要提速流水线化的架构思路也清晰明了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价