1. 超声波测距原理与Verilog实现思路
超声波测距听起来很高科技,其实原理特别简单。想象一下你在山谷里大喊一声,然后听回声——超声波测距就是这个原理的电子版。模块发射超声波,遇到障碍物反射回来,我们只要计算声波往返时间,就能算出距离。我在做智能小车项目时,这个功能帮了大忙。
具体到Verilog实现,核心就四步:触发发射、捕获回波、计算时间、换算距离。这里有个坑要注意:声速会受温度影响,但一般室内应用可以忽略。我实测过,在20℃环境下,声速约343m/s,换算成us级时间就是每毫米往返需要5.8us。不过实际代码里我们用0.173这个魔术数字,这是把声速和单位换算都考虑进去的简化公式。
2. 模块化设计架构
2.1 时钟分频模块
先说说vlg_en这个模块。FPGA的时钟动不动就50MHz,但超声波测距需要的是us级精度。我常用的是把50MHz(20ns周期)分频成1MHz(1us周期)。代码里这个参数P_CLK_PERIORD就是输入时钟周期,单位是ns。这里有个小技巧:用parameter定义常量,后期修改特别方便。
module vlg_en #( parameter P_CLK_PERIORD = 20 //50MHz时钟 )( input clk, input rst_n, output reg clk_en ); //分频计数器最大值计算 localparam P_DIVCLK_MAX = 1000/P_CLK_PERIORD - 1; reg [7:0] r_divcnt; always @(posedge clk or negedge rst_n) begin if(!rst_n) r_divcnt <= 0; else if(r_divcnt < P_DIVCLK_MAX) r_divcnt <= r_divcnt + 1; else r_divcnt <= 0; end always @(posedge clk) begin clk_en <= (r_divcnt == P_DIVCLK_MAX); end endmodule2.2 触发信号生成模块
vlg_tirg模块负责产生10us的TRIG脉冲,这个脉冲就像扣动扳机,让超声波模块开始工作。我建议把周期设为100ms,这样测距频率就是10Hz,既不会太频繁也不会太慢。调试时发现个有趣现象:如果TRIG脉冲太短(比如小于10us),模块可能不响应;太长又会降低刷新率。
module vlg_tirg ( input clk, input rst_n, input clk_en, //1MHz时钟使能 output reg trig ); //100ms=100_000us localparam P_TRIG_PERIORD_MAX = 100_000 - 1; localparam P_TRIG_HIGH_MAX = 10; //10us高电平 reg [16:0] tricnt; always @(posedge clk or negedge rst_n) begin if(!rst_n) tricnt <= 0; else if(clk_en) begin tricnt <= (tricnt < P_TRIG_PERIORD_MAX) ? tricnt + 1 : 0; end end always @(posedge clk) begin trig <= (tricnt > 0) && (tricnt <= P_TRIG_HIGH_MAX); end endmodule3. 回波时间捕获技术
3.1 边沿检测技巧
vlg_echo模块最核心的技术就是边沿检测。我一般用两级寄存器做同步,既防亚稳态又能准确捕捉跳变。这里有个细节:echo信号来自外部模块,一定要先同步到FPGA时钟域!曾经有个项目因为没做同步,测距结果时不时抽风。
module vlg_echo ( input clk, input rst_n, input clk_en, input echo, output reg [15:0] t_us ); reg [1:0] r_echo; wire pos_echo = ~r_echo[1] & r_echo[0]; //上升沿 wire neg_echo = r_echo[1] & ~r_echo[0]; //下降沿 reg cnt_en; reg [15:0] echo_cnt; always @(posedge clk or negedge rst_n) begin if(!rst_n) r_echo <= 0; else r_echo <= {r_echo[0], echo}; end3.2 高电平计时实现
计时逻辑要注意三点:1)只在echo高电平时计数 2)用clk_en控制计数精度 3)下降沿时锁存计数值。实测发现,用1MHz时钟时最大测距约5.6米(65535us),对小车够用了。如果要测更远,可以把计数器改成32位。
always @(posedge clk or negedge rst_n) begin if(!rst_n) cnt_en <= 0; else if(pos_echo) cnt_en <= 1; else if(neg_echo) cnt_en <= 0; end always @(posedge clk or negedge rst_n) begin if(!rst_n) echo_cnt <= 0; else if(!cnt_en) echo_cnt <= 0; else if(clk_en) echo_cnt <= echo_cnt + 1; end always @(posedge clk or negedge rst_n) begin if(!rst_n) t_us <= 0; else if(neg_echo) t_us <= echo_cnt; end endmodule4. 距离计算优化方案
4.1 定点数运算技巧
cal模块的s=0.173*t涉及浮点运算,但在FPGA里浮点计算太耗资源。我的解决方案是定点数运算:把0.173放大4096倍得到709,最后结果右移12位。这样既保证精度又节省资源。调试时发现,用移位相加代替乘法器可以进一步节省LUT。
module cal ( input clk, input rst_n, input [15:0] t_us, output [14:0] s_mm ); //709=512+128+64+4+1 wire [25:0] sum1 = t_us << 9; //512*t wire [25:0] sum2 = t_us << 7; //128*t wire [25:0] sum3 = t_us << 6; //64*t wire [25:0] sum4 = t_us << 2; //4*t wire [25:0] sum5 = t_us; //1*t wire [25:0] sum_total = sum1 + sum2 + sum3 + sum4 + sum5; assign s_mm = sum_total[25:12]; //右移12位 endmodule4.2 乘法器IP核使用
当需要更高性能时,可以用Xilinx的乘法器IP核。在Vivado里创建IP时,选择Multiplier类型,配置为16bit x 10bit无符号乘法。记得把Pipeline Stages设为2,这样时序更稳定。我在Artix-7上实测,用IP核比纯LUT实现节省30%资源。
5. 系统集成与调试
5.1 顶层模块设计
vlg_top就像乐高底座,把所有模块插在一起。接口设计有个原则:控制信号用寄存器输出,状态信号用wire输入。我习惯把参数集中定义在顶层,这样修改起来一目了然。特别注意跨时钟域信号要加同步器,特别是echo信号。
module vlg_top( input clk, input rst_n, input echo, output trig, output [14:0] distance_mm ); parameter P_CLK_PERIORD = 20; //50MHz wire clk_en; wire [15:0] t_us; vlg_en #(.P_CLK_PERIORD(P_CLK_PERIORD)) u_en( .clk(clk), .rst_n(rst_n), .clk_en(clk_en)); vlg_tirg u_trig( .clk(clk), .rst_n(rst_n), .clk_en(clk_en), .trig(trig)); vlg_echo u_echo( .clk(clk), .rst_n(rst_n), .clk_en(clk_en), .echo(echo), .t_us(t_us)); cal u_cal( .clk(clk), .rst_n(rst_n), .t_us(t_us), .s_mm(distance_mm)); endmodule5.2 测试平台搭建
好的TB文件能事半功倍。我用$random生成随机延时,模拟不同距离。注意给echo信号加500ns延迟,模拟硬件响应时间。波形查看重点:trig脉冲宽度、echo响应延迟、计数器清零时机。
module tb_top(); reg clk; reg rst_n; reg echo; wire trig; wire [14:0] s_mm; vlg_top uut(.*); initial begin clk = 1; forever #10 clk = ~clk; end initial begin rst_n = 0; echo = 0; #200 rst_n = 1; repeat(5) begin @(posedge trig); #5000 echo = 1; #($urandom_range(26000, 11)*1000); echo = 0; end $finish; end endmodule6. 常见问题与解决方案
6.1 信号抖动处理
实际项目中echo信号常有毛刺。我的应对方案:1)硬件上加RC滤波 2)Verilog里用计数器去抖。比如连续3个周期高电平才认为有效。在低速应用(如10Hz刷新)时,这个方案特别有效。
6.2 温度补偿方案
虽然我们忽略了温度影响,但对精度要求高的场合可以加DS18B20温度传感器。实测温度每升高1℃,声速增加0.6m/s。可以在cal模块里动态调整乘法系数,公式变为:s = (331.4 + 0.6*T)*t/2,其中T是摄氏温度。
6.3 多模块协同工作
当系统中有多个超声波模块时,要分时复用触发信号。我的方案是用状态机轮询,每个模块间隔100ms触发。关键是要确保前一个模块的echo信号结束再触发下一个,否则会互相干扰。