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

📅 发布时间:2026/9/16 20:22:01
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/st 是 Echo 高电平时间。也就是说FPGA 并不直接测量距离它只负责精确测量一段高电平的时间宽度。这里有一个容易被忽略的点340m/s 是 15℃ 空气里的典型声速。0℃ 时声速约 331m/s40℃ 时约 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 3d0; localparam TRIG_PULSE 3d1; localparam WAIT_ECHO 3d2; localparam COUNT_ECHO 3d3; localparam LOCKED 3d4; 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 20d0; trig_cnt 10d0; dist_cnt 20d0; trig_out 1b0; data_valid 1b0; end else begin case (state) IDLE: begin data_valid 1b0; trig_out 1b1; trig_cnt 10d0; state TRIG_PULSE; end TRIG_PULSE: begin // 10us 高电平50MHz 下计 500 个周期 if (trig_cnt 10d499) begin trig_out 1b0; state WAIT_ECHO; end else begin trig_cnt trig_cnt 1b1; end end WAIT_ECHO: begin // 等待 Echo 上升沿带超时退出 if (echo_in) state COUNT_ECHO; end COUNT_ECHO: begin if (echo_in) begin cnt cnt 1b1; // 约 58ms 超时对应约 10 米量程 if (cnt 20d2_900_000) state LOCKED; end else begin dist_cnt cnt; data_valid 1b1; 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,23550MHz20 ns0.0034 mm约 1,176,471100MHz10 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 3d0; localparam START 3d1; localparam SHIFT_BIT 3d2; localparam ACK_BIT 3d3; localparam STOP 3d4; reg [3:0] shift_cnt; reg [8:0] div_cnt; // 时钟分频计数 reg [7:0] shift_reg; reg sda_out; assign sda sda_out ? 1bz : 1b0; always (posedge clk or negedge rst_n) begin if (!rst_n) begin scl 1b1; sda_out 1b1; done 1b0; end else begin case (state) IDLE: begin done 1b0; if (send_en) begin shift_reg data_in; shift_cnt 4d0; sda_out 1b0; // SDA 拉低SCL 高产生起始条件 state START; end end START: begin // 起始条件保持 5us随后开始第一个时钟周期 if (div_cnt 9d249) begin scl 1b0; state SHIFT_BIT; end else div_cnt div_cnt 1b1; end SHIFT_BIT: begin // 在 SCL 低电平期间更新 SDA if (div_cnt 9d499) begin scl ~scl; if (scl) begin shift_reg {shift_reg[6:0], 1b0}; shift_cnt shift_cnt 1b1; if (shift_cnt 4d8) 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 标准模式。如果模块支持快速模式可以把计数阈值改成 124SCL 变为 400kHz但要注意总线上其他设备是否都支持这个频率。真实模块上电后如果 sda 一直保持低电平说明从设备没有释放总线或者上拉电阻缺失这时候测任何寄存器都读不到数据。3.3 初始化序列与连续测量模式配置TOF 激光测距模块上电后通常处于待机状态需要先写入一系列初始化寄存器才能进入测量模式。初始化序列的长度因模块而异链接里的模块数据手册会给出寄存器地址和默认值。典型的配置流程是复位传感器、设置测距模式为连续、设置精度等级、启动测量。// 初始化流程IIC 寄存器写序列 // 寄存器地址和数据来自模块数据手册 reg [7:0] init_seq [0:11]; initial begin init_seq[0] {8h00, 8h00}; // 复位 init_seq[1] {8h01, 8h01}; // 连续测量使能 init_seq[2] {8h02, 8h08}; // 测量精度配置 init_seq[3] {8h03, 8h10}; // 测量时间预算 init_seq[4] {8h04, 8h00}; // 量程上限 init_seq[5] {8h05, 8h00}; // 量程下限 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 20d0; key_sync 1b1; end else begin // 第一级同步防止亚稳态进入计数逻辑 key_prev key_in; key_sync key_prev; // 连续计数稳定电平超过 20ms 才认为按键有效 if (key_sync key_stable) begin cnt 20d0; end else begin cnt cnt 1b1; if (cnt 20d999_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 20d999_999; // 20ms 周期 always (posedge clk) begin period_cnt period_cnt 1b1; if (period_cnt PWM_PERIOD) period_cnt 20d0; // 距离越近占空比越高 if (period_cnt dist_value) pwm_out 1b1; else pwm_out 1b0; end endmodule这个映射方式把 dist_value 直接当作比较阈值距离值越大高电平时间越长。如果你的应用需要反方向映射只需要把比较方向反过来。PWM 频率的选择要结合负载特性蜂鸣器通常在 2kHz 到 4kHz 之间发声最响舵机控制必须用 50Hz 标准频率两者不能共用同一套 PWM 周期参数。提示pwm_control.v 里的 dist_value 默认单位是毫米如果你的量程到了 4m 即 4000mm20ms 周期内最大比较阈值只有 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 扫描逻辑把所有地址读一遍确认模块实际地址和数据手册一致。本文还有配套的精品资源点击获取