news 2026/9/16 20:21:57

FPGA驱动超声波与IIC激光测距:状态机设计与Verilog实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA驱动超声波与IIC激光测距:状态机设计与Verilog实现

简介:这套驱动工程面向FPGA开发者,整合了激光测距模块与HC-SR04超声波测距模块的驱动逻辑,支持通过按键切换传感器,并将测距结果显示在数码管上,适合学习外设接口时序、数据解码与模块化设计。工程基于正点原子开发板搭建验证,覆盖0~20厘米近距离与0~4米远距离两类典型量程,能够直观对比不同传感器的响应差异。资源包共877个文件,压缩包大小13.31MB,以Verilog/AHDL源文件、Quartus工程配置与编译生成文件为主,常见后缀包括v、tdf、qpf、qsf、sof、cdb、hdb等,既可打开工程查看完整代码,也能借助中间编译文件理解Quartus综合布局流程。源码按功能拆分为超声检测、激光信号解码、数码管显示、按键选择、PWM控制、IIC驱动等独立模块,各模块职责清晰,适合逐步学习传感器驱动、状态机设计和FPGA顶层互连。目前已有415人学习下载,适合FPGA入门和进阶者作为动手参考资料。

1. 为什么把超声波和激光测距放进同一个 FPGA 工程

HC-SR04 和 IIC 总线 TOF 激光测距模块,恰好代表了 FPGA 处理物理传感器信号的两种典型形态:一种是直接对引脚上的脉冲宽度做计时,另一种是通过串行总线协议完成寄存器读写。把这两套驱动放进同一个工程并跑通,等于把 FPGA 开发里最常用的输入捕获、状态机设计、总线时序、译码显示、按键消抖一次性练完。以正点原子开发板为载体,这套工程支持按键切换两种传感器,并把测量结果在四位一体的数码管上动态显示。工程里还带 PWM 控制模块,可以用来驱动舵机或蜂鸣器做输出联动。对做课设和竞赛的入门者,模块划分和边界处理可以直接复用;对有经验的开发者,回波超时保护和 IIC 初始化序列的细节也值得核对一遍。

2. HC-SR04 回波计时:从 10μs 触发到计数器的状态机设计

2.1 声波往返时间与距离换算

HC-SR04 的时序足够简单:Trig 引脚拉高 10μs 以上,模块内部发出 8 个 40kHz 周期的超声波脉冲,Echo 引脚随后输出一个高电平,高电平持续时间与声波从发射到返回的往返时间严格相等。距离 d = v × t / 2,常温下声速 v 取 340m/s,t 是 Echo 高电平时间。也就是说,FPGA 并不直接测量距离,它只负责精确测量一段高电平的时间宽度。

这里有一个容易被忽略的点:340m/s 是 15℃ 空气里的典型声速。0℃ 时声速约 331m/s,40℃ 时约 354m/s,跨度超过 20m/s。对于 2m 左右的实测距离,温度偏差会带来约 6cm 的误差。课程设计和原型验证可以把这个偏差归入 HC-SR04 自身的精度范围(±3mm 是理想条件),但如果是室外环境或工业检测场景,建议在驱动中预留一个温度补偿端口,用外接温度传感器修正声速值。

HC-SR04 标称量程通常是 2cm 到 4m,实际项目中受到探头余振和盲区影响,近距离 2cm 以下往往采不到稳定回波。驱动逻辑设计时要考虑两个极端:目标太近导致回波宽度过窄,以及目标太远或空旷导致回波超时。前者靠计数精度兜底,后者必须做超时保护,否则状态机可能长时间停在等待中。

2.2 五态测量状态机实现

工程里的 sonic_detect.v 和 csb_unit.v 是超声波驱动的主体。推荐把驱动拆成五个状态:IDLE、TRIG_PULSE、WAIT_ECHO、COUNT_ECHO、LOCKED。用状态机而不是纯计数器去“等”,是因为后续要扩展连续测量、超时保护、多探头切换时,状态机的可读性和扩展性明显更好。

下面是一段可直接综合的 sonic_detect 逻辑骨架,包含触发、等待回波、计数和锁存闭环:

// sonic_detect.v —— HC-SR04 回波捕获与超时保护 module sonic_detect ( input wire clk, // 系统时钟 50MHz input wire rst_n, // 异步复位,低有效 input wire echo_in, // HC-SR04 Echo 引脚输入 output reg trig_out, // HC-SR04 Trig 引脚输出 output reg [19:0] dist_cnt, // 回波高电平持续时钟数 output reg data_valid // 单次测量有效标志 ); localparam IDLE = 3'd0; localparam TRIG_PULSE = 3'd1; localparam WAIT_ECHO = 3'd2; localparam COUNT_ECHO = 3'd3; localparam LOCKED = 3'd4; reg [2:0] state; reg [19:0] cnt; reg [9:0] trig_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; cnt <= 20'd0; trig_cnt <= 10'd0; dist_cnt <= 20'd0; trig_out <= 1'b0; data_valid <= 1'b0; end else begin case (state) IDLE: begin data_valid <= 1'b0; trig_out <= 1'b1; trig_cnt <= 10'd0; state <= TRIG_PULSE; end TRIG_PULSE: begin // 10us 高电平,50MHz 下计 500 个周期 if (trig_cnt == 10'd499) begin trig_out <= 1'b0; state <= WAIT_ECHO; end else begin trig_cnt <= trig_cnt + 1'b1; end end WAIT_ECHO: begin // 等待 Echo 上升沿,带超时退出 if (echo_in) state <= COUNT_ECHO; end COUNT_ECHO: begin if (echo_in) begin cnt <= cnt + 1'b1; // 约 58ms 超时,对应约 10 米量程 if (cnt == 20'd2_900_000) state <= LOCKED; end else begin dist_cnt <= cnt; data_valid <= 1'b1; state <= LOCKED; end end LOCKED: begin // 锁存一个周期后立即开始下一轮测量 state <= IDLE; end default: state <= IDLE; endcase end end endmodule

状态机逻辑说明:IDLE 状态一进入就把 trig_out 拉高,持续 500 个时钟周期后在 TRIG_PULSE 状态内拉低,这样得到的触发脉冲正好是 10μs。WAIT_ECHO 状态不做任何计数,只等待回波上升沿,这样触发脉冲和回波起始点之间不会被额外延时污染。COUNT_ECHO 是测量核心,在 echo_in 为高时每个时钟周期自增一次,回波拉低后把计数结果锁存到 dist_cnt,同时拉高 data_valid 一个周期,通知下游模块取数。

参数说明:dist_cnt 为 20 位,最大计数 1,048,575,在 50MHz 下对应约 20.9ms,换算成往返距离约 3.55m,无法覆盖 HC-SR04 标称的 4 米满量程。如果你需要测 4 米以上,直接把 cnt 和 dist_cnt 扩成 21 位,超时阈值同步放宽。另一种做法是把计数时钟二分频后再计,牺牲一点分辨率换取位宽不变,典型工程里我推荐直接扩位宽,成本只有几个触发器。

2.3 计数位宽、超时阈值与毫米换算

计数频率直接决定测量分辨率。以 50MHz 为例,每个计数代表 20ns 的时间,对应声波单向传播距离约 0.0034mm。距离分辨率远高于 HC-SR04 模块本身的能力,实际瓶颈在探头的起振特性和回波前沿的陡峭程度,而不是计数器位数。

时钟频率每计数时间单向距离分辨率4m 往返所需计数
25MHz40 ns0.0068 mm约 588,235
50MHz20 ns0.0034 mm约 1,176,471
100MHz10 ns0.0017 mm约 2,352,941

换算公式是 d_mm = dist_cnt × 340 / (2 × f_clk) × 1000。以 50MHz 代入,d_mm = dist_cnt × 3.4,这已经不是 2 的整数幂,直接移位会产生 15% 以上误差。工程里常见的做法是乘以 34 再除以 10,即dist_mm = dist_cnt * 34 / 10。FPGA 里的乘法和除法都会消耗逻辑资源,更激进的做法是用dist_cnt * 35 >> 4近似,代价是引入约 3% 的系统偏差,在显示前加一个固定修正系数补偿。

提示:HC-SR04 的回波信号没有经过任何数字滤波,直接连到 FPGA 引脚上时,如果目标表面存在倾斜或吸音材料,Echo 前沿可能出现毛刺抖动。建议在输入引脚到状态机之间加两级同步器,防止亚稳态进入计数状态。

3. IIC 型激光测距模块驱动:寄存器配置与连续测量模式

3.1 从脉冲计时到总线协议的设计跨越

工程里激光测距部分用的是 IIC 接口 TOF 模块,驱动代码在 iic_driver_1607.v 中实现。这类模块和 HC-SR04 最大的区别是:HC-SR04 把测量结果编码为脉宽,FPGA 只需要定时器;而 TOF 模块把测量结果放在内部寄存器里,FPGA 必须通过 SCL 和 SDA 两根线与模块完成寄存器读写,才能把距离值取回来。

IIC 总线上每个从设备有唯一的 7 位地址,模块的数据手册里会给出。读写流程固定为:起始条件、设备地址 + 读写位、从设备 ACK、寄存器地址、从设备 ACK、数据字节、停止条件。多字节读取时还需要在最后一个字节前把 ACK 改为 NACK,通知从设备停止发送。这一点是新手最容易漏的,漏掉后读取到的总是最后一个寄存器值重复输出。

iic_driver_1607.v 里复用了同一套 IIC 时序完成寄存器的配置和数据读取,状态切换由顶层的读写请求信号触发。常见的做法是把 IIC 驱动拆成两个层面:底层是字节读写状态机,负责处理 SCL 时钟生成、SDA 数据搬移、ACK 采样;上层是寄存器操作函数,通过组合状态调用底层字节状态机完成初始化序列和测量结果读取。

3.2 IIC 字节时序的 Verilog 实现

IIC 标准模式是 100kHz,快速模式是 400kHz。FPGA 时钟 50MHz 下,100kHz 意味着每个 SCL 周期要分频 500 个系统时钟。下面是典型的 IIC 写字节状态机骨架:

// iic_driver_1607.v —— IIC 字节写时序 module iic_bit_send ( input wire clk, // 50MHz input wire rst_n, input wire send_en, // 启动发送请求 input wire [7:0] data_in, // 待发送字节 output reg scl, // IIC 时钟线 inout wire sda, // IIC 数据线 output reg done // 发送完成脉冲 ); localparam IDLE = 3'd0; localparam START = 3'd1; localparam SHIFT_BIT = 3'd2; localparam ACK_BIT = 3'd3; localparam STOP = 3'd4; reg [3:0] shift_cnt; reg [8:0] div_cnt; // 时钟分频计数 reg [7:0] shift_reg; reg sda_out; assign sda = sda_out ? 1'bz : 1'b0; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin scl <= 1'b1; sda_out <= 1'b1; done <= 1'b0; end else begin case (state) IDLE: begin done <= 1'b0; if (send_en) begin shift_reg <= data_in; shift_cnt <= 4'd0; sda_out <= 1'b0; // SDA 拉低,SCL 高,产生起始条件 state <= START; end end START: begin // 起始条件保持 5us,随后开始第一个时钟周期 if (div_cnt == 9'd249) begin scl <= 1'b0; state <= SHIFT_BIT; end else div_cnt <= div_cnt + 1'b1; end SHIFT_BIT: begin // 在 SCL 低电平期间更新 SDA if (div_cnt == 9'd499) begin scl <= ~scl; if (scl) begin shift_reg <= {shift_reg[6:0], 1'b0}; shift_cnt <= shift_cnt + 1'b1; if (shift_cnt == 4'd8) state <= ACK_BIT; end end end // ACK 与 STOP 略,逻辑对称 endcase end end endmodule

逻辑说明:IIC 的时序规则是 SDA 只能在 SCL 低电平时变化,在 SCL 高电平时必须保持稳定。所以数据移位放在 SCL 拉低期间完成,SCL 拉高后总线上的数据才被从设备采样。代码里 sda_out 为低时强制拉低 SDA,为高时输出高阻态,由外部上拉电阻把 SDA 拉高,这就是 IIC 线与逻辑的标准处理方式。

参数说明:div_cnt 计数到 499 表示一个完整的 SCL 高或低电平周期,SCL 频率 = 50MHz / 500 = 100kHz,符合 IIC 标准模式。如果模块支持快速模式,可以把计数阈值改成 124,SCL 变为 400kHz,但要注意总线上其他设备是否都支持这个频率。真实模块上电后如果 sda 一直保持低电平,说明从设备没有释放总线或者上拉电阻缺失,这时候测任何寄存器都读不到数据。

3.3 初始化序列与连续测量模式配置

TOF 激光测距模块上电后通常处于待机状态,需要先写入一系列初始化寄存器才能进入测量模式。初始化序列的长度因模块而异,链接里的模块数据手册会给出寄存器地址和默认值。典型的配置流程是:复位传感器、设置测距模式为连续、设置精度等级、启动测量。

// 初始化流程:IIC 寄存器写序列 // 寄存器地址和数据来自模块数据手册 reg [7:0] init_seq [0:11]; initial begin init_seq[0] = {8'h00, 8'h00}; // 复位 init_seq[1] = {8'h01, 8'h01}; // 连续测量使能 init_seq[2] = {8'h02, 8'h08}; // 测量精度配置 init_seq[3] = {8'h03, 8'h10}; // 测量时间预算 init_seq[4] = {8'h04, 8'h00}; // 量程上限 init_seq[5] = {8'h05, 8'h00}; // 量程下限 end

初始化完成后再读取测量结果时,常见做法是轮询状态寄存器,检查测量完成位是否为 1,然后连续读取高字节和低字节寄存器,拼成一个 16 位距离值。读取时要注意:IIC 多字节读取在最后一个字节前必须回 NACK,否则从设备会一直发送数据直到总线出错。拼出来的原始值和真实距离之间往往有一个比例系数,甚至是线性的偏移量,需要根据数据手册做转换。

提示:TOF 模块的测量结果对电源纹波敏感。如果看到距离值在静止状态下跳变超过量程的 1%,先查模块供电是不是和伺服电机或蜂鸣器共用了同一路电源。FPGA 开发板上的 3.3V 通常能带动这类模块,但不要让蜂鸣器启动瞬间的大电流把模块电压拉低太多。

4. 按键仲裁、数码管动态扫描与 PWM 控制输出

4.1 按键切换数据源的消抖与边沿检测

两个传感器的距离值拿到后,需要一个机制决定当前显示哪一路。工程里用按键切换,sel_encode.v 负责处理按键输入。机械按键在按下和松开瞬间会有约 10ms 的抖动,直接作为选择信号会导致数据源在两者之间来回跳变,所以必须做消抖。

// 按键消抖与单周期脉冲生成 module key_debounce ( input wire clk, // 50MHz input wire key_in, // 按键原始输入 output reg key_ok // 消抖后的按键信号 ); reg [19:0] cnt; reg key_sync, key_prev, key_stable; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 20'd0; key_sync <= 1'b1; end else begin // 第一级同步,防止亚稳态进入计数逻辑 key_prev <= key_in; key_sync <= key_prev; // 连续计数稳定电平,超过 20ms 才认为按键有效 if (key_sync == key_stable) begin cnt <= 20'd0; end else begin cnt <= cnt + 1'b1; if (cnt == 20'd999_999) key_stable <= key_sync; end end end assign key_ok = key_stable; endmodule

消抖逻辑的关键是用计数器做“连续稳定时间判定”:输入电平只要发生翻转就清零计数器,只有在同一个电平上保持 20ms 才会把电平锁存为有效状态。工程里 sel_encode.v 除了消抖,还把按键信号编码成两位选择码,用来向下游显示模块指定当前使用的是超声波结果还是激光测距结果。

按键切换时还有一个需要考虑的场景——切换瞬间的显示毛刺。如果按键刚好在下游模块正在锁存数据的时候切过去,数码管上可能出现一个周期的乱码。常见做法是将按键选择信号和 data_valid 信号做组合约束,只在 data_valid 有效时更新显示寄存器,这个逻辑在 seg_show.v 里已经体现。

4.2 四位一体数码管动态扫描

数码管显示模块 seg_show.v 使用的是动态扫描方案:四位数码管共用段选线,位选线轮流拉低,利用人眼视觉暂留效应实现同时显示。每位数码管点亮时间不能太短,否则亮度不足;也不能太长,否则闪烁明显。典型扫描频率在 1kHz 左右,即每一位点亮 1ms,四个位循环一次 4ms。

扫描频率周期单管点亮时间视觉效果
500Hz2ms500μs轻微闪烁
1kHz1ms250μs正常
2kHz500μs125μs亮度略低

数码管从测量计数到显示需要经过 BCD 转换。dist_cnt 是二进制数,段选编码器按 BCD 码来映射字形,中间必须做一个二进制转 BCD 的处理。工程里用的是连续加 3 算法(Double Dabble),在组合逻辑流水线里展开。如果直接在时钟内循环移位相加,会消耗较多 LUT,但对四位数的量程来说资源占用仍然很小,在正点原子这类主流 FPGA 上没有任何压力。

4.3 PWM 控制输出与量程映射

pwm_control.v 的功能是把当前显示的距离值映射成 PWM 占空比输出。常见用法是驱动蜂鸣器做近距报警,或驱动舵机根据距离转动角度。PWM 波形由周期和占空比两个参数决定,例如 20ms 周期、0.5ms 到 2.5ms 高电平宽度,正好对应舵机 0° 到 180° 的转动范围。

// PWM 输出逻辑:距离值映射为占空比 module pwm_control ( input wire clk, // 50MHz input wire [15:0] dist_value, // 当前距离值,单位 mm output reg pwm_out ); reg [19:0] period_cnt; localparam PWM_PERIOD = 20'd999_999; // 20ms 周期 always @(posedge clk) begin period_cnt <= period_cnt + 1'b1; if (period_cnt >= PWM_PERIOD) period_cnt <= 20'd0; // 距离越近,占空比越高 if (period_cnt < dist_value) pwm_out <= 1'b1; else pwm_out <= 1'b0; end endmodule

这个映射方式把 dist_value 直接当作比较阈值,距离值越大高电平时间越长。如果你的应用需要反方向映射,只需要把比较方向反过来。PWM 频率的选择要结合负载特性,蜂鸣器通常在 2kHz 到 4kHz 之间发声最响,舵机控制必须用 50Hz 标准频率,两者不能共用同一套 PWM 周期参数。

提示:pwm_control.v 里的 dist_value 默认单位是毫米,如果你的量程到了 4m 即 4000mm,20ms 周期内最大比较阈值只有 4000,而周期计数到 999999,占空比最大只有 0.4%。所以实际工程里要先把距离值做缩放,映射到周期规模的合理范围,否则 PWM 输出几乎没有可见变化。

5. 上板验证与边界测量调试

5.1 用卷尺标定零点与比例系数

上电后先不要看数码管的绝对值,而是用卷尺或激光测距仪做标定。把被测物体放在 50cm、100cm、150cm、200cm 四个位置,分别记录数码管显示的原始计数和实际距离,画出对应关系。理想情况下是一条过原点的直线,斜率就是计数到毫米的换算系数。如果斜率与你理论计算的 3.4 有偏差,说明声速取值或时钟频率和实际有出入,直接把换算系数改为实测斜率即可。这个标定方法同样适用于激光模块,只是激光模块的线性度通常更好,偏差主要来自固定偏移而不是比例系数。

四位数码管的显示精度通常只到厘米,也就是毫米位四舍五入。调试阶段可以把 seg_show.v 的显示格式临时改为毫米精度,方便和卷尺读数对齐。确认无误后再改回原来的显示格式,避免把调试状态直接用于演示。

5.2 超声波回波毛刺与 IIC 总线上拉排查

超声波模块最常见的异常是数码管显示距离突然跳到最大值或变成 0,原因有两种:一是回波信号在长线上传输引入了毛刺,使计数器提前结束;二是测量超时判据太短,正常回波还没回来就被状态机放弃。排查方法是用示波器同时观察 Echo 引脚和 FPGA 内部的 data_valid 信号,确认回波高电平时间是否与理论值一致。如果 Echo 上有明显振铃,在引脚对地并联一个 100pF 到 1nF 的电容可以滤掉高频毛刺。

IIC 激光模块的典型故障是初始化后读回的数据全部是 0xFF 或 0x00,优先检查模块 SCL 和 SDA 引脚的上拉电阻。绝大多数模块板自带 4.7kΩ 上拉,但如果你用了延长线或面包板,寄生电容会把总线边沿拉缓,需要在模块端再并联一个 2.2kΩ 上拉电阻。总线上同时挂多个设备时,地址冲突也会导致读写失败,用 IIC 扫描逻辑把所有地址读一遍,确认模块实际地址和数据手册一致。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 20:21:35

STM32裸机物联网终端设计:从状态机到低功耗实战

简介&#xff1a;本资源是一套完整的基于STM32的共享充电宝高分毕业设计实现方案&#xff0c;面向计算机、通信、自动化及人工智能等相关专业的本科生与初阶嵌入式学习者&#xff0c;解决从硬件驱动开发、通信协议对接到系统功能集成的典型工程实践问题&#xff0c;适用于课程设…

作者头像 李华
网站建设 2026/9/16 20:21:19

AI产品安全责任与验证机制:工程护栏实践指南

最近这段时间&#xff0c;AI产品安全责任的话题在圈子里讨论得特别多。监管风向已经非常明确&#xff1a;AI企业必须为那些“危险、未经充分验证”的产品承担后果。虽然具体细则还在博弈中&#xff0c;但这个信号已经足够让所有做AI应用开发、AI大模型落地的团队&#xff0c;重…

作者头像 李华
网站建设 2026/9/16 20:17:22

统信UOS系统盘满了?图形化无损扩容实战指南

要我说&#xff0c;“系统盘满了”这件事&#xff0c;在统信UOS上发生的频率&#xff0c;远比很多人想象中高。尤其是那些从Windows迁移过来的老用户&#xff0c;习惯了C盘动不动就飙红&#xff0c;到了Linux生态里&#xff0c;以为换个文件系统就一劳永逸了。实际上&#xff0…

作者头像 李华
网站建设 2026/9/16 20:16:25

YOLOWorld实战:基于ultralytics的开放词汇检测与自定义训练

最近不少做目标检测的朋友都在折腾YOLOWorld。原因很简单&#xff1a;传统YOLO系列训练完就只能检测固定类别&#xff0c;一旦场景里出现没见过的目标&#xff0c;只能重新标注、重新训练&#xff0c;整个流程又慢又重。YOLOWorld不一样&#xff0c;它可以靠文本描述直接识别出…

作者头像 李华
网站建设 2026/9/16 20:15:58

vmdk转qcow2避坑指南:qemu-img转换中的五大典型问题与解法

干这一行久了&#xff0c;虚拟机迁移就是家常便饭。尤其是从VMware往KVM迁&#xff0c;或者你从别人手里拿到一个打包好的vmdk&#xff0c;想在本地用QEMU环境跑起来&#xff0c;第一步就绕不开vmdk转qcow2这个格式转换。网上教程不少&#xff0c;一眼看去就是一条命令的事&…

作者头像 李华