VL53L0X 是意法半导体(STMicroelectronics)推出的一款激光测距传感器,I2C 接口,内部集成了 940 nm 红外激光发射器和光子探测器,靠测光子的往返时间(ToF,Time of Flight)算出距离,量程 30 - 2000 mm。供电范围 2.6 - 3.5 V,只要 SCL、SDA 两根线就能读回一个 16 位的距离值。
1、模块来源
规格书(datasheet)请在商品详情页的资料下载区获取,认准意法半导体 ST 的 VL53L0X 型号。本文标注「以规格书为准」的地方,都以你手上这份为准。
模块一共 4 个管脚,长这样:
VCC:电源,芯片工作电压 2.6 - 3.5 V(必须用 3.3 V 供电,不能接 5 V)GND:地线,必须和 FPGA 共地SCL:I2C 时钟线SDA:I2C 数据线
2、参数
工作电压:2.6 - 3.5 V(推荐 3.3 V)
通讯接口:I2C(寄存器式)
从机地址:7 位地址 0x29(ADDR 悬空或接 GND 时;写地址 = 0x29 左移一位 = 0x52,读地址 = 0x53)
测距原理:发 940 nm 红外激光脉冲,测光子往返时间换算距离(ToF)
量程:30 - 2000 mm
精度:±3%(典型值,随目标反射率变化,本文不写死,请以规格书为准)
单次测距时间:约 30 ms(典型值;本例等 30 ms 后再查状态)
器件 ID:读 0xC0 寄存器,期望 0xEE(用来验芯片在不在)
测量输出:16 位距离,单位 mm(高字节在 0x1E,低字节在 0x1F)
上电等待:上电后内部时钟约 1.2 ms 才稳,本例等 2 ms 再通讯
管脚数量:4 Pin(VCC、GND、SCL、SDA)
⚠️警告:
VL53L0X 芯片的供电范围是 2.6 - 3.5 V,严禁用 5 V 直接给芯片供电,会烧毁芯片!
3、实战任务
使用立创·逻辑派 FPGA-G1 开发板编写 VL53L0X 驱动代码,上电自动完成初始化(验 ID + 切 I2C 标准模式),之后每隔 1 秒自动测距一次,按一下按键(key)也可以手动立刻补测一次,测完通过串口把距离发给上位机显示:
距离: 1234mm一行一共 14 个字节(含回车换行):距离: 后面跟 4 位十进制数字,不足 4 位补零,单位 mm,取值范围 30 - 2000。判读方法:拿一本书在传感器正前方前后移动,数值应该跟着远近变化;贴太近(小于 30 mm)或太远(大于 2000 mm)读数不可信。
4、时序说明
4.1 读写时序
VL53L0X 走的是标准 I2C 协议。I2C 时序我们这里不再进行赘述,有兴趣可去模块里的 IIC 屏幕里学习
我们在这里简单的过一下 VL53L0X 的读写过程时序。
器件地址为 0x29,但是最后一位是读写位,需要空出一位给读写位,因此需要左移一位,即 0x29<<1 得到 0X52(读操作最低位填 1,得到 0x53)
VL53L0X 是寄存器式器件,读寄存器时先写寄存器地址,中间不发停止,用重复起始(Sr)接着发读地址,才能把数据读回来。多字节读时寄存器地址自动 +1,比如指向 0x1E 连读两个字节,拿到的是 0x1E、0x1F 的内容。
采集步骤:(写=0,读=1)
写寄存器:起始信号 -> 器件地址左移 1 位 + 写(0x52)-> 等待传感器应答 -> 发送寄存器地址 -> 等待传感器应答 -> 发送数据字节 -> 等待传感器应答 -> 停止信号
读寄存器:起始信号 -> 器件地址左移 1 位 + 写(0x52)-> 等待传感器应答 -> 发送寄存器地址 -> 等待传感器应答 -> 重复起始信号 -> 器件地址左移 1 位 + 读(0x53)-> 等待传感器应答 -> 读第 1 个数据字节(还想要下一个就回 ACK)-> 读第 2 个数据字节(没有下一个了回 NACK)-> 停止信号
本教程用到的寄存器一共 5 个:
| 寄存器 | 地址 | 读写 | 作用 |
|---|---|---|---|
| 器件 ID | 0xC0 | 只读 | 期望读回 0xEE,验芯片在不在 |
SYSRANGE_START | 0x00 | 可读写 | 写 0x01 启动一次单次测距 |
RESULT_RANGE_STATUS | 0x14 | 只读 | bit0=1 表示测量完成 |
| 距离寄存器 | 0x1E / 0x1F | 只读 | 16 位距离,单位 mm,高字节在 0x1E。就是 0x14 往后数 10 个字节 |
| I2C 模式 | 0x88 | 可读写 | 写 0x00 切 I2C 标准模式(默认 0x20 是 400 kHz 增强模式) |
一次完整测距的流程(第一次含初始化):
- 等上电:上电后等 2 ms,芯片内部时钟约 1.2 ms 才稳
- 读 ID 验芯片:读
0xC0,期望0xEE(本教程只锁存不报错) - 切 I2C 标准模式:写
0x88=0x00(芯片默认 0x20 是 400 kHz 增强模式,有些 FPGA 主机跑不稳) - 启动单次测距:写
0x00=0x01,芯片开始发激光 - 等 30 ms:芯片肚子里自己测,总线安静
- 轮询状态:读
0x14,bit0=1 表示完成;没完成就等 5 ms 再问一轮,最多问 10 次 - 读距离:连读
0x1E、0x1F两个字节,高字节在前,拼成 16 位 mm
4.2 数据换算
VL53L0X 比前面的温湿度、颜色传感器省事:读回来的两个字节拼起来就是距离本身,单位 mm,不需要再套换算公式。它是大端顺序:高字节(0x1E)在前、低字节(0x1F)在后(TCS34725 是小端,正好对比一下):
其中 d_H、d_L 是读回的高、低字节(十进制 0 - 255)。代入算例:读回 d_H = 0x04、d_L = 0xD2,则 d = 0x04 × 256 + 0xD2 = 1234,也就是 1234 mm。
串口要打印 4 位十进制(不足补零),把 16 位距离按千、百、十、个拆位:
千位 = d / 1000 范围 0~2(量程 2000 mm 以内)
百位 = (d % 1000) / 100
十位 = (d % 100) / 10
个位 = d % 102
3
4
距离值只反映「激光打到的那个表面离模块多远」,和目标的反射率、颜色都有关:太黑、太斜、太远的目标回波太弱,读数会不可信,量程 30 - 2000 mm 以外的数值请直接忽略。
5、程序编写
整个工程分四个模块:
- top.v(顶层):1 秒定时 / 按键消抖产生测距请求;把距离拆成 4 位十进制;拼 14 个字节的打印行喂给串口。
- vl53l0x_driver.v(VL53L0X 驱动):单个 case 步骤机,步骤 0 ~ 36 共 37 步,按顺序指挥底层 I2C:首次测距先做初始化(等上电 → 验 ID → 切 I2C 标准模式),之后每次「启动测距 → 等 30 ms → 轮询状态 → 读距离」。
- i2c_master.v(I2C 主机):底层搬字节,只管发起始 / 写 1 字节 / 读 1 字节 / 发停止
- uart_tx.v(串口发送)
`timescale 1ns / 1ps
module top (
input sys_clk, // 系统时钟 50MHz
input rst_n, // 复位按键,按下为低,异步复位
input key, // 手动触发按键,按下为低
output scl, // I2C 时钟线 → VL53L0X SCL
inout sda, // I2C 数据线 → VL53L0X SDA
output uart_tx // 串口发送 → 上位机
);
// ---------------------------------------------------------------------
// 常量定义
// ---------------------------------------------------------------------
// 按键消抖 20ms:50MHz × 20ms = 1_000_000 个时钟周期,数 0~999_999
localparam CNT_MAX = 20'd999_999;
// 1 秒定时:50MHz × 1s = 50_000_000 个时钟周期,数 0~49_999_999
localparam SEC_MAX = 26'd49_999_999;
// 打印行"距离: XXXXmm\r\n" 共 14 字节,最后一字节序号 13
localparam LAST_BYTE = 4'd13;
// ---------------------------------------------------------------------
// 内部连线
// ---------------------------------------------------------------------
// vl53l0x_driver ↔ i2c_master
wire i2c_start;
wire [ 1: 0] i2c_cmd;
wire [ 7: 0] i2c_tx;
wire i2c_nack;
wire [ 7: 0] i2c_rx;
wire i2c_done;
wire i2c_busy;
// vl53l0x_driver → top
wire [15: 0] dist_mm; // 距离 mm
wire data_valid; // 采样完成脉冲
// top → uart_tx
wire uart_tx_done;
wire uart_tx_busy;
reg uart_tx_req;
reg [ 7: 0] uart_tx_data;
// 触发:1 秒到 或 按键按下(都是单周期脉冲,直接打给 driver)
// ★ driver 忙的时候(start 没人理)脉冲会被丢掉,按键在采样中按下就等下一次
wire measure_start;
// ---------------------------------------------------------------------
// 按键消抖:key=0 持续满 20ms 才算真按了,打一拍 key_flag
// ---------------------------------------------------------------------
reg [19: 0] cnt_20ms; // 消抖计数器
reg key_flag; // 消抖后的按键脉冲
always @(posedge sys_clk or negedge rst_n) begin
if (!rst_n)
cnt_20ms <= 20'd0;
else if (key == 1'b1)
cnt_20ms <= 20'd0; // 松手(高电平)就清零重数
else if (cnt_20ms == CNT_MAX)
cnt_20ms <= cnt_20ms; // 数满了就停,一次按下只出一拍
else
cnt_20ms <= cnt_20ms + 1'b1;
end
always @(posedge sys_clk or negedge rst_n) begin
if (!rst_n)
key_flag <= 1'b0;
else if (cnt_20ms == CNT_MAX - 1'b1)
key_flag <= 1'b1; // ★ 数到 999_998 那一拍打一个脉冲
else
key_flag <= 1'b0;
end
// ---------------------------------------------------------------------
// 1 秒定时:数到 SEC_MAX 出一拍 sec_flag,到点自动采一次
// ---------------------------------------------------------------------
reg [25: 0] cnt_1s; // 1 秒计数器
reg sec_flag; // 1 秒到的脉冲
always @(posedge sys_clk or negedge rst_n) begin
if (!rst_n)
cnt_1s <= 26'd0;
else if (cnt_1s == SEC_MAX)
cnt_1s <= 26'd0; // 到点清零,重新数
else
cnt_1s <= cnt_1s + 1'b1;
end
always @(posedge sys_clk or negedge rst_n) begin
if (!rst_n)
sec_flag <= 1'b0;
else if (cnt_1s == SEC_MAX - 1'b1)
sec_flag <= 1'b1; // 数到 49_999_998 那一拍打一个脉冲
else
sec_flag <= 1'b0;
end
// 两个触发源合到一起,任一个来了都启动一次采样
assign measure_start = key_flag | sec_flag;
// ---------------------------------------------------------------------
// 距离 → 千/百/十/个位(除法和取余拆位,量程 2000mm 以内 4 位够用)
// 先落到 16 位全宽中间值再取低 4 位,免得综合器报位宽截断警告
// ---------------------------------------------------------------------
reg [15: 0] dist_r; // 锁存本轮要打印的距离
wire [15: 0] q16 = dist_r / 1000; // 千位全宽值 0~2
wire [15: 0] b16 = (dist_r % 1000) / 100; // 百位全宽值 0~9
wire [15: 0] s16 = (dist_r % 100) / 10; // 十位全宽值 0~9
wire [15: 0] g16 = dist_r % 10; // 个位全宽值 0~9
wire [ 3: 0] d_q = q16[3:0]; // 千位
wire [ 3: 0] d_b = b16[3:0]; // 百位
wire [ 3: 0] d_s = s16[3:0]; // 十位
wire [ 3: 0] d_g = g16[3:0]; // 个位
// ---------------------------------------------------------------------
// 打印行控制:"距离: XXXXmm\r\n" 共 14 字节
// data_valid 来 → 锁存 dist_mm,work_en 拉高开始发
// 发完第 14 字节 → work_en 落下
// ★ 采一次至少 30ms,发一行只要 1.2ms,下一次 data_valid 来的时候
// 这行早发完了,不会撞车
// ---------------------------------------------------------------------
reg work_en;
reg [ 3: 0] byte_cnt;
always @(posedge sys_clk or negedge rst_n) begin
if (!rst_n) begin
work_en <= 1'b0;
byte_cnt <= 4'd0;
dist_r <= 16'd0;
uart_tx_req <= 1'b0;
uart_tx_data <= 8'd0;
end else begin
uart_tx_req <= 1'b0; // 默认拉低,只在发的那一拍拉高
// 采样完成:锁存结果,开始打印
if (data_valid) begin
dist_r <= dist_mm;
work_en <= 1'b1;
end
// 打印进行中
else if (work_en) begin
if (uart_tx_done) begin
if (byte_cnt == LAST_BYTE) begin // 14 字节发完收工
work_en <= 1'b0;
byte_cnt <= 4'd0;
end else begin
byte_cnt <= byte_cnt + 1'b1;
end
end
else if (!uart_tx_busy) begin
case (byte_cnt)
// "距离:" 的 GB2312 编码:距=BEE0 离=C0EB,再加冒号
4'd0: uart_tx_data <= 8'hBE; // "距" 高字节
4'd1: uart_tx_data <= 8'hE0; // "距" 低字节
4'd2: uart_tx_data <= 8'hC0; // "离" 高字节
4'd3: uart_tx_data <= 8'hEB; // "离" 低字节
4'd4: uart_tx_data <= 8'h3A; // ":"
4'd5: uart_tx_data <= 8'h20; // 空格
4'd6: uart_tx_data <= {4'h3, d_q}; // '0'+千位
4'd7: uart_tx_data <= {4'h3, d_b}; // '0'+百位
4'd8: uart_tx_data <= {4'h3, d_s}; // '0'+十位
4'd9: uart_tx_data <= {4'h3, d_g}; // '0'+个位
4'd10: uart_tx_data <= 8'h6D; // "m"
4'd11: uart_tx_data <= 8'h6D; // "m"
4'd12: uart_tx_data <= 8'h0D; // 回车
4'd13: uart_tx_data <= 8'h0A; // 换行
default: uart_tx_data <= 8'h20;
endcase
uart_tx_req <= 1'b1;
end
end
end
end
// ---------------------------------------------------------------------
// 例化:VL53L0X 驱动(上层步骤机)
// ---------------------------------------------------------------------
vl53l0x_driver u_vl53l0x_driver (
.clk (sys_clk ),
.rst_n (rst_n ),
.start (measure_start ),
.dist_mm (dist_mm ),
.data_valid (data_valid ),
.chip_id ( ), // 这里先不接,调试时可以用 SignalTap 抓(期望 0xEE)
.i2c_start (i2c_start ),
.i2c_cmd (i2c_cmd ),
.i2c_tx (i2c_tx ),
.i2c_nack (i2c_nack ),
.i2c_rx (i2c_rx ),
.i2c_done (i2c_done ),
.i2c_busy (i2c_busy )
);
// ---------------------------------------------------------------------
// 例化:I2C 主机(底层搬字节)
// ---------------------------------------------------------------------
i2c_master u_i2c_master (
.clk (sys_clk ),
.rst_n (rst_n ),
.start (i2c_start ),
.cmd (i2c_cmd ),
.tx_data (i2c_tx ),
.nack (i2c_nack ),
.rx_data (i2c_rx ),
.ack_err ( ), // 这里先不接,调试时可以用 SignalTap 抓
.busy (i2c_busy ),
.done (i2c_done ),
.scl (scl ),
.sda (sda )
);
// ---------------------------------------------------------------------
// 例化:串口发送
// ---------------------------------------------------------------------
uart_tx u_uart_tx (
.clk (sys_clk ),
.rst_n (rst_n ),
.uart_tx_req (uart_tx_req ),
.tx_in_data (uart_tx_data ),
.uart_txd (uart_tx ),
.uart_tx_done (uart_tx_done ),
.uart_tx_busy (uart_tx_busy )
);
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
`timescale 1ns / 1ps
module vl53l0x_driver (
input clk, // 系统时钟 50MHz
input rst_n, // 复位,低电平有效
// ---------- 对外接口 ----------
input start, // 脉冲:开始一次采样(忙的时候来脉冲会被丢掉)
output reg [15:0] dist_mm, // 距离,单位 mm(量程 30~2000)
output reg data_valid, // 单周期脉冲:dist_mm 有效
output reg [ 7: 0] chip_id, // 读回的器件 ID(期望 0xEE),调试用可接 SignalTap
// ---------- 连接 i2c_master 的命令接口 ----------
output reg i2c_start,
output reg [ 1: 0] i2c_cmd,
output reg [ 7: 0] i2c_tx,
output reg i2c_nack,
input [ 7: 0] i2c_rx,
input i2c_done,
input i2c_busy
);
// ---------------------------------------------------------------------
// 常量定义:跟 i2c_master 的命令表保持一致
// ---------------------------------------------------------------------
localparam CMD_START = 2'b00;
localparam CMD_WRITE = 2'b01;
localparam CMD_READ = 2'b10;
localparam CMD_STOP = 2'b11;
// VL53L0X 器件字节(7 位地址 0x29 左移 1 位,低位 0=写 1=读)
localparam DEV_WRITE = 8'h52; // 0x29 写
localparam DEV_READ = 8'h53; // 0x29 读
// 关键寄存器地址
localparam REG_ID = 8'hC0; // 器件 ID(只读,期望 0xEE)
localparam REG_I2C = 8'h88; // I2C 模式寄存器
localparam REG_START = 8'h00; // SYSRANGE_START
localparam REG_STAT = 8'h14; // RESULT_RANGE_STATUS
localparam REG_MM_H = 8'h1E; // 距离高字节(低字节在 0x1F,读时自动 +1)
// 要写进寄存器的值
localparam I2C_STD = 8'h00; // 0x88 写 0x00:I2C 标准模式
localparam RANGE_ONE = 8'h01; // 0x00 写 0x01:启动一次单次测距
localparam ID_EXPECT = 8'hEE; // 芯片 ID 期望值(比对用,教程版只锁存不报错)
// 等待时间(都留了裕量,芯片内部时钟误差可达 ±10%)
// 上电稳定:芯片上电后约 1.2ms 时钟才稳,等 2ms 保险
// 50MHz × 2ms = 100000 个时钟周期
localparam WAIT_PWR = 22'd100000;
// 一次测距约 30ms(典型值,以规格书为准),先等 30ms 再查状态
// 50MHz × 30ms = 1500000 个时钟周期
localparam WAIT_MEAS = 22'd1500000;
// 轮询间隔 5ms
// 50MHz × 5ms = 250000 个时钟周期
localparam WAIT_POLL = 22'd250000;
// 最多轮询 10 次:30ms + 10×5ms = 80ms 还没完成就当超时,照样去读
localparam POLL_MAX = 4'd10;
// ---------------------------------------------------------------------
// 步骤编码:一次采样拆成 37 步,一步一步走
// 步骤多于 16 个,编码位宽用 6 位
// ---------------------------------------------------------------------
// ---- 阶段〇:初始化(只在上电后第一次 start 走) ----
localparam STEP_IDLE = 6'd0; // 空闲,等 start
localparam STEP_PWR = 6'd1; // 等 2ms,等芯片时钟稳
localparam STEP_I_START = 6'd2; // 起始条件
localparam STEP_I_ADDR_W = 6'd3; // 发 0x52(地址 + 写)
localparam STEP_I_REG_ID = 6'd4; // 发 0xC0(寄存器指向 ID)
localparam STEP_I_RSTART = 6'd5; // 重复起始 Sr
localparam STEP_I_ADDR_R = 6'd6; // 发 0x53(地址 + 读)
localparam STEP_I_RX_ID = 6'd7; // 读 ID 一个字节,回 NACK
localparam STEP_I_STOP = 6'd8; // 停止
localparam STEP_I_START2 = 6'd9; // 起始条件
localparam STEP_I_ADDR_W2 = 6'd10; // 发 0x52
localparam STEP_I_REG_88 = 6'd11; // 发 0x88
localparam STEP_I_DAT_88 = 6'd12; // 发 0x00,切 I2C 标准模式
localparam STEP_I_STOP2 = 6'd13; // 停止,初始化结束
// ---- 阶段一:启动单次测距 ----
localparam STEP_M_START = 6'd14; // 起始条件
localparam STEP_M_ADDR_W = 6'd15; // 发 0x52
localparam STEP_M_REG_00 = 6'd16; // 发 0x00(SYSRANGE_START)
localparam STEP_M_DAT_00 = 6'd17; // 发 0x01,芯片开始测
localparam STEP_M_STOP = 6'd18; // 停止
// ---- 中间等待 ----
localparam STEP_M_WAIT1 = 6'd19; // 等 30ms,让芯片测完
// ---- 阶段二:轮询状态 ----
localparam STEP_M_START2 = 6'd20; // 起始条件
localparam STEP_M_ADDR_W2 = 6'd21; // 发 0x52
localparam STEP_M_REG_14 = 6'd22; // 发 0x14(寄存器指向状态)
localparam STEP_M_RSTART = 6'd23; // 重复起始 Sr
localparam STEP_M_ADDR_R = 6'd24; // 发 0x53
localparam STEP_M_RX_ST = 6'd25; // 读状态字节,回 NACK
localparam STEP_M_STOP2 = 6'd26; // 停止
localparam STEP_M_CHK = 6'd27; // 看 bit0:1=好了,0=再等再问
localparam STEP_M_WAIT2 = 6'd28; // 等 5ms(轮询间隔)
// ---- 阶段三:读距离 ----
localparam STEP_M_START3 = 6'd29; // 起始条件
localparam STEP_M_ADDR_W3 = 6'd30; // 发 0x52
localparam STEP_M_REG_1E = 6'd31; // 发 0x1E(指向距离高字节)
localparam STEP_M_RSTART2 = 6'd32; // 重复起始 Sr
localparam STEP_M_ADDR_R2 = 6'd33; // 发 0x53
localparam STEP_M_RX_H = 6'd34; // 读高字节(0x1E),回 ACK
localparam STEP_M_RX_L = 6'd35; // 读低字节(0x1F),回 NACK
localparam STEP_M_STOP3 = 6'd36; // 停止,一次采样结束
// ---------------------------------------------------------------------
// 内部寄存器
// ---------------------------------------------------------------------
reg [ 5: 0] step; // 当前步骤
reg [21: 0] wait_cnt; // 等待计数器
reg cmd_req; // 给 i2c_master 的启动脉冲(防重发锁)
reg init_done; // 初始化做完了没:0=下一次 start 先走初始化
reg [ 7: 0] data_h; // 读回的高字节(ID / 状态 / 距离高字节复用)
reg [ 7: 0] stat_r; // 读回的状态字节(0x14)
reg [ 3: 0] poll_cnt; // 已经问过几次状态
reg busy; // 整个采样流程忙标志
// ---------------------------------------------------------------------
// 主流程:一步一步发命令,i2c_done 表示这一步的 I2C 动作做完了
// 发命令的纪律:if (!cmd_req && !i2c_busy) 才发,i2c_start 和 cmd_req
// 都只拉高一拍,收到 i2c_done 才走下一步
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
step <= STEP_IDLE;
wait_cnt <= 22'd0;
cmd_req <= 1'b0;
init_done <= 1'b0;
data_h <= 8'd0;
stat_r <= 8'd0;
poll_cnt <= 4'd0;
dist_mm <= 16'd0;
data_valid <= 1'b0;
chip_id <= 8'd0;
busy <= 1'b0;
i2c_start <= 1'b0;
i2c_cmd <= 2'b00;
i2c_tx <= 8'd0;
i2c_nack <= 1'b0;
end else begin
cmd_req <= 1'b0; // 默认拉低,只在发命令那拍拉高
i2c_start <= 1'b0; // 默认拉低,只拉高一拍(i2c_master 靠这一拍启动)
data_valid <= 1'b0; // 默认拉低,只在完成那拍拉高
case (step)
// =============================================================
// 空闲:第一次 start 先做初始化,之后直接测距
STEP_IDLE: begin
busy <= 1'b0;
if (start) begin
busy <= 1'b1;
poll_cnt <= 4'd0;
wait_cnt <= 22'd0; // 进等待步前先清零计数器
// ★ 没初始化过就先去验 ID / 写 0x88,初始化过就直奔测距
step <= init_done ? STEP_M_START : STEP_PWR;
end
end
// =============================================================
// 阶段〇:初始化(只走一次)
// =============================================================
// ---- 等 2ms:VL53L0X 上电后内部时钟约 1.2ms 才稳 ----
STEP_PWR: begin
if (wait_cnt == WAIT_PWR)
step <= STEP_I_START;
else
wait_cnt <= wait_cnt + 1'b1;
end
// ---- 读 ID:START + 0x52 + 0xC0 + Sr + 0x53 + 读(NACK) + STOP ----
STEP_I_START: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_START;
i2c_start <= 1'b1; // 打一拍启动
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_I_ADDR_W;
end
STEP_I_ADDR_W: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= DEV_WRITE;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_I_REG_ID;
end
STEP_I_REG_ID: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= REG_ID; // 寄存器指针指向 0xC0
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_I_RSTART;
end
// ★ 重复起始:中间不发 STOP,再来一个 START(波形和 START 一样)
STEP_I_RSTART: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_START;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_I_ADDR_R;
end
STEP_I_ADDR_R: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= DEV_READ;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_I_RX_ID;
end
STEP_I_RX_ID: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b1; // 只读 1 个字节,回 NACK:"这是最后一个"
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
chip_id <= i2c_rx; // 期望 0xEE。教程版只锁存不报错,
// 要严格可以在这里比对 ID_EXPECT 分支重试
step <= STEP_I_STOP;
end
end
STEP_I_STOP: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_STOP;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_I_START2;
end
// ---- 写 0x88 = 0x00:START + 0x52 + 0x88 + 0x00 + STOP ----
STEP_I_START2: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_START;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_I_ADDR_W2;
end
STEP_I_ADDR_W2: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= DEV_WRITE;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_I_REG_88;
end
STEP_I_REG_88: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= REG_I2C;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_I_DAT_88;
end
STEP_I_DAT_88: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= I2C_STD; // 0x00:I2C 标准模式
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_I_STOP2;
end
STEP_I_STOP2: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_STOP;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
init_done <= 1'b1; // ★ 初始化完事,以后不用再走这一段
step <= STEP_M_START;
end
end
// =============================================================
// 阶段一:启动单次测距(写 0x00 = 0x01)
// =============================================================
STEP_M_START: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_START;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_M_ADDR_W;
end
STEP_M_ADDR_W: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= DEV_WRITE;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_M_REG_00;
end
STEP_M_REG_00: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= REG_START;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_M_DAT_00;
end
STEP_M_DAT_00: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= RANGE_ONE; // 0x01:启动一次单次测距
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
// 发完这个字节,VL53L0X 立刻开始发光测距
if (i2c_done) step <= STEP_M_STOP;
end
STEP_M_STOP: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_STOP;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
step <= STEP_M_WAIT1;
wait_cnt <= 22'd0;
end
end
// =============================================================
// 中间:纯等 30ms,啥也不发,让芯片肚子里自己测
// =============================================================
STEP_M_WAIT1: begin
if (wait_cnt == WAIT_MEAS)
step <= STEP_M_START2;
else
wait_cnt <= wait_cnt + 1'b1;
end
// =============================================================
// 阶段二:轮询 0x14 状态,等 bit0=1
// =============================================================
STEP_M_START2: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_START;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_M_ADDR_W2;
end
STEP_M_ADDR_W2: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= DEV_WRITE;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_M_REG_14;
end
STEP_M_REG_14: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= REG_STAT;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_M_RSTART;
end
STEP_M_RSTART: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_START;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_M_ADDR_R;
end
STEP_M_ADDR_R: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= DEV_READ;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_M_RX_ST;
end
STEP_M_RX_ST: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b1; // 只读 1 个字节,回 NACK
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
stat_r <= i2c_rx; // bit0=1 表示测量完成
step <= STEP_M_STOP2;
end
end
STEP_M_STOP2: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_STOP;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_M_CHK;
end
// ★ 判断:bit0=1 就去读距离;没好就等 5ms 再问一轮
// 问满 10 次还没好当超时,照样去读(读到的可能不是最新值;
// 严格做法应报错重测,这里教程版从简,以规格书为准)
STEP_M_CHK: begin
if (stat_r[0]) begin
step <= STEP_M_START3;
end else if (poll_cnt == POLL_MAX) begin
step <= STEP_M_START3; // 超时兜底,别卡死在这
end else begin
poll_cnt <= poll_cnt + 1'b1;
step <= STEP_M_WAIT2;
wait_cnt <= 22'd0;
end
end
// ---- 等 5ms:轮询间隔 ----
STEP_M_WAIT2: begin
if (wait_cnt == WAIT_POLL)
step <= STEP_M_START2; // 回去再问一轮状态
else
wait_cnt <= wait_cnt + 1'b1;
end
// =============================================================
// 阶段三:读距离 0x1E/0x1F
// =============================================================
STEP_M_START3: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_START;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_M_ADDR_W3;
end
STEP_M_ADDR_W3: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= DEV_WRITE;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_M_REG_1E;
end
STEP_M_REG_1E: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= REG_MM_H; // 指向距离高字节 0x1E
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_M_RSTART2;
end
STEP_M_RSTART2: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_START;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_M_ADDR_R2;
end
STEP_M_ADDR_R2: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= DEV_READ;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_M_RX_H;
end
STEP_M_RX_H: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b0; // 中间字节回 ACK:"还要下一个"
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
data_h <= i2c_rx; // 高字节(0x1E)
step <= STEP_M_RX_L;
end
end
STEP_M_RX_L: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b1; // 最后一个字节回 NACK
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
dist_mm <= {data_h, i2c_rx}; // 高 + 低 = 16 位距离 mm
step <= STEP_M_STOP3;
end
end
STEP_M_STOP3: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_STOP;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
data_valid <= 1'b1; // ★ 通知顶层:距离好了
step <= STEP_IDLE;
end
end
default: step <= STEP_IDLE;
endcase
end
end
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
`timescale 1ns / 1ps
module i2c_master (
input clk, // 系统时钟 50MHz
input rst_n, // 复位,低电平有效
// ---------- 命令接口(上层 ads1115_ctrl 调用) ----------
input start, // 脉冲:开始一次操作
input [ 1: 0] cmd, // 操作类型,见上面命令表
input [ 7: 0] tx_data, // 要发送的字节(cmd=01 时用)
input nack, // 读字节后:0=回 ACK(还要读),1=回 NACK(读完了)
output reg [ 7: 0] rx_data, // 收到的字节(cmd=10 时有效)
output reg ack_err, // 写字节时:1 = 从机没应答
output reg busy, // 1 = 正在干活,别发新命令
output reg done, // 一次操作完成,单周期脉冲
// ---------- I2C 物理引脚 ----------
output reg scl,
inout sda
);
// ---------------------------------------------------------------------
// 常量定义
// ---------------------------------------------------------------------
localparam CMD_START = 2'b00; // 起始条件
localparam CMD_WRITE = 2'b01; // 写 1 字节
localparam CMD_READ = 2'b10; // 读 1 字节
localparam CMD_STOP = 2'b11; // 停止条件
// 节拍宽度:SCL 半个周期 = TICK 个系统时钟
// SCL 频率 = 50MHz / (2*100) = 250kHz,远低于 400kHz,稳
localparam TICK = 100;
// 状态定义
localparam IDLE = 4'd0; // 空闲
localparam START = 4'd1; // 发起始条件
localparam WR_BIT = 4'd2; // 写 8 位数据
localparam WR_ACK = 4'd3; // 写完,收从机 ACK
localparam RD_BIT = 4'd4; // 读 8 位数据
localparam RD_ACK = 4'd5; // 读完,发 ACK / NACK
localparam STOP = 4'd6; // 发停止条件
// ---------------------------------------------------------------------
// 内部寄存器
// ---------------------------------------------------------------------
reg [ 3: 0] state; // 当前状态
reg [ 1: 0] phase; // 一个 bit / 一个动作里分 4 拍
reg [15: 0] tcnt; // 拍内计数器,数到 TICK-1
reg [ 2: 0] bit_cnt; // 位计数器 0~7
reg [ 7: 0] sh_tx; // 发送移位寄存器
reg [ 7: 0] sh_rx; // 接收移位寄存器
reg sda_oe; // 1 = 主机驱动 SDA,0 = 放开(高阻,靠上拉)
reg sda_out; // 主机想发到 SDA 上的电平
// SDA 三态门:sda_oe=1 时输出 sda_out,否则松手
assign sda = sda_oe ? sda_out : 1'bz;
// 拍内最后一个时钟(用来切换 phase)
wire tick_end = (tcnt == TICK - 1);
// ---------------------------------------------------------------------
// 拍计数器:每个 phase 走 TICK 个系统时钟
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tcnt <= 16'd0;
phase <= 2'd0;
end else if (state == IDLE) begin
tcnt <= 16'd0;
phase <= 2'd0;
end else if (tick_end) begin
tcnt <= 16'd0;
phase <= phase + 1'b1;
end else begin
tcnt <= tcnt + 1'b1;
end
end
// ---------------------------------------------------------------------
// 状态机:决定下一步干什么
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
busy <= 1'b0;
done <= 1'b0;
ack_err <= 1'b0;
bit_cnt <= 3'd0;
sh_tx <= 8'd0;
sh_rx <= 8'd0;
rx_data <= 8'd0;
end else begin
done <= 1'b0; // done 永远只拉高一拍
case (state)
// ---------------------------------------------------------
IDLE: begin
busy <= 1'b0;
bit_cnt <= 3'd0;
if (start) begin
busy <= 1'b1;
sh_tx <= tx_data; // 锁存要发的字节
case (cmd)
CMD_START: state <= START;
CMD_WRITE: state <= WR_BIT;
CMD_READ : state <= RD_BIT;
CMD_STOP : state <= STOP;
default : state <= IDLE;
endcase
end
end
// ---------------------------------------------------------
// 起始条件:SCL 高的时候 SDA 由高变低
// phase0: SCL=1 SDA=1 (准备好空闲电平)
// phase1: SCL=1 SDA=0 (★ 这里产生 START)
// phase2: SCL=0 (时钟拉低,准备发数据)
// phase3: 结束
// ---------------------------------------------------------
START: begin
if (tick_end && phase == 2'd3) begin
state <= IDLE;
done <= 1'b1;
end
end
// ---------------------------------------------------------
// 写 8 位(MSB 先发),每 bit 4 拍
// phase0: SCL=0,把这一位摆到 SDA 上
// phase1: SCL=1
// phase2: SCL=1 保持(从机在这中间采样)
// phase3: SCL=0,bit_cnt+1
// ---------------------------------------------------------
WR_BIT: begin
if (tick_end && phase == 2'd3) begin
if (bit_cnt == 3'd7) begin
bit_cnt <= 3'd0;
state <= WR_ACK;
end else begin
bit_cnt <= bit_cnt + 1'b1;
sh_tx <= {sh_tx[6:0], 1'b0}; // 左移,下一位到最高位
end
end
end
// ---------------------------------------------------------
// 写完收 ACK:主机松手,从机拉低 = ACK
// phase0: SCL=0,放开 SDA
// phase1: SCL=1
// phase2: SCL=1 保持,这里采样 ACK
// phase3: SCL=0,结束
// ---------------------------------------------------------
WR_ACK: begin
if (tick_end && phase == 2'd2) begin
if (sda) // 没拉低 = NACK,记错误
ack_err <= 1'b1;
end
if (tick_end && phase == 2'd3) begin
state <= IDLE;
done <= 1'b1;
end
end
// ---------------------------------------------------------
// 读 8 位(MSB 先收),每 bit 4 拍
// phase0: SCL=0,主机放开 SDA,让从机驱动
// phase1: SCL=1
// phase2: SCL=1 保持,这里采样数据
// phase3: SCL=0,bit_cnt+1
// ---------------------------------------------------------
RD_BIT: begin
if (tick_end && phase == 2'd2) begin
sh_rx <= {sh_rx[6:0], sda}; // 收一位,左移
end
if (tick_end && phase == 2'd3) begin
if (bit_cnt == 3'd7) begin
bit_cnt <= 3'd0;
state <= RD_ACK;
end else begin
bit_cnt <= bit_cnt + 1'b1;
end
end
end
// ---------------------------------------------------------
// 读完发 ACK / NACK:第 9 个时钟由主机说话
// nack=0 → 拉低 SDA 回 ACK("还要下一个")
// nack=1 → 放开 SDA 回 NACK("这是最后一个")
// ---------------------------------------------------------
RD_ACK: begin
if (tick_end && phase == 2'd3) begin
rx_data <= sh_rx; // 锁存这一字节
state <= IDLE;
done <= 1'b1;
end
end
// ---------------------------------------------------------
// 停止条件:SCL 高的时候 SDA 由低变高
// phase0: SCL=0 SDA=0 (准备好)
// phase1: SCL=1 SDA=0
// phase2: SCL=1 SDA=1 (★ 这里产生 STOP)
// phase3: 结束
// ---------------------------------------------------------
STOP: begin
if (tick_end && phase == 2'd3) begin
state <= IDLE;
done <= 1'b1;
end
end
default: state <= IDLE;
endcase
end
end
// ---------------------------------------------------------------------
// SCL 生成:按状态和拍序输出高低
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
scl <= 1'b1;
else begin
case (state)
// 起始条件:phase0/1 保持高,phase2 开始拉低
START: scl <= (phase <= 2'd1);
// 写/读数据、ACK:phase0 低,phase1/2 高,phase3 低
WR_BIT, WR_ACK, RD_BIT, RD_ACK:
scl <= (phase == 2'd1) || (phase == 2'd2);
// 停止条件:phase0 低,phase1 开始拉高
STOP: scl <= (phase >= 2'd1);
// 空闲:保持高
default: scl <= 1'b1;
endcase
end
end
// ---------------------------------------------------------------------
// SDA 输出控制
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sda_oe <= 1'b1;
sda_out <= 1'b1;
end else begin
case (state)
// ---- 起始条件 ----
START: begin
sda_oe <= 1'b1;
sda_out <= (phase == 2'd0); // phase0 高,phase1 起变低
end
// ---- 写数据:SCL 低的时候摆数据 ----
WR_BIT: begin
sda_oe <= 1'b1;
sda_out <= sh_tx[7]; // 每次取最高位(MSB 先发)
end
// ---- 写完收 ACK:必须放手,让从机拉 ----
WR_ACK: begin
sda_oe <= 1'b0;
sda_out <= 1'b1;
end
// ---- 读数据:放手,从机驱动 ----
RD_BIT: begin
sda_oe <= 1'b0;
sda_out <= 1'b1;
end
// ---- 读完发 ACK/NACK:主机自己说话 ----
RD_ACK: begin
sda_oe <= 1'b1;
sda_out <= nack; // 0=ACK,1=NACK
end
// ---- 停止条件 ----
STOP: begin
sda_oe <= 1'b1;
sda_out <= (phase >= 2'd2); // phase2 起变高
end
// ---- 空闲:SDA 放高 ----
default: begin
sda_oe <= 1'b1;
sda_out <= 1'b1;
end
endcase
end
end
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
`timescale 1ns / 1ps
// =====================================================================
// 模块:uart_tx
// 功能:串口发送,一次发 1 个字节
// 格式:1 位起始位 + 8 位数据(LSB 先发)+ 1 位停止位
// 特点:写一次进 IP 库,以后所有串口发送都复用
//
// 使用方法:
// 1. 等 uart_tx_busy=0
// 2. 把数据放到 tx_in_data,uart_tx_req 打一拍
// 3. 等 uart_tx_done 脉冲,这一个字节就发完了
// =====================================================================
module uart_tx (
input clk, // 系统时钟 50MHz
input rst_n, // 复位,低电平有效
input uart_tx_req, // 发送请求,单周期脉冲
input [ 7: 0] tx_in_data, // 要发送的字节
output reg uart_txd, // 串口发送引脚
output reg uart_tx_done, // 单周期脉冲:一个字节发完了
output reg uart_tx_busy // 1 = 正在发,别塞新数据
);
// ---------------------------------------------------------------------
// 参数
// ---------------------------------------------------------------------
parameter CLK_FREQ = 26'd50_000_000; // 系统时钟 50MHz
parameter UART_BPS = 17'd115200; // 波特率 115200
// 一位数据持续多少个系统时钟:50M / 115200 ≈ 434
localparam BAUD_CNT_MAX = CLK_FREQ / UART_BPS;
// ---------------------------------------------------------------------
// 内部寄存器
// ---------------------------------------------------------------------
reg [ 8: 0] baud_cnt; // 波特率计数器 0~433
reg [ 3: 0] bit_cnt; // 位计数器 0~9(1 起始 + 8 数据 + 1 停止)
reg [ 7: 0] data_reg; // 锁存要发的数据
// ---------------------------------------------------------------------
// 忙标志:收到请求就忙,发完 10 位才清
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
uart_tx_busy <= 1'b0;
else if (uart_tx_req)
uart_tx_busy <= 1'b1;
else if (bit_cnt == 4'd10 && baud_cnt == BAUD_CNT_MAX - 1)
uart_tx_busy <= 1'b0; // 10 位全部发完
end
// ---------------------------------------------------------------------
// 波特率计数器:每 BAUD_CNT_MAX 个时钟走 1 位
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
baud_cnt <= 9'd0;
else if (!uart_tx_busy)
baud_cnt <= 9'd0;
else if (baud_cnt == BAUD_CNT_MAX - 1)
baud_cnt <= 9'd0;
else
baud_cnt <= baud_cnt + 1'b1;
end
// ---------------------------------------------------------------------
// 位计数器:数到 10 清零
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
bit_cnt <= 4'd0;
else if (!uart_tx_busy)
bit_cnt <= 4'd0;
else if (baud_cnt == BAUD_CNT_MAX - 1)
bit_cnt <= bit_cnt + 1'b1;
end
// ---------------------------------------------------------------------
// 数据锁存:请求来的那一拍把数据存住
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
data_reg <= 8'd0;
else if (uart_tx_req)
data_reg <= tx_in_data;
end
// ---------------------------------------------------------------------
// 发送波形:一位一位往外打
// bit_cnt=0 起始位(低)
// bit_cnt=1~8 数据,从最低位开始
// bit_cnt=9 停止位(高)
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
uart_txd <= 1'b1; // 空闲时为高电平
// ★ 不忙的时候必须把线拉高。原来没有这一句: 空闲时 baud_cnt 和 bit_cnt 都是 0,
// 下面那个条件一直成立、一直走 bit_cnt=0(起始位)分支, 串口线在空闲时一直输出低电平,
// 接收端从第一字节起就错位, 收到的全是乱码。
else if (!uart_tx_busy)
uart_txd <= 1'b1;
else if (baud_cnt == 9'd0) begin // 每一位的开头切换电平
case (bit_cnt)
4'd0: uart_txd <= 1'b0; // 起始位
4'd1: uart_txd <= data_reg[0];
4'd2: uart_txd <= data_reg[1];
4'd3: uart_txd <= data_reg[2];
4'd4: uart_txd <= data_reg[3];
4'd5: uart_txd <= data_reg[4];
4'd6: uart_txd <= data_reg[5];
4'd7: uart_txd <= data_reg[6];
4'd8: uart_txd <= data_reg[7]; // 数据最高位
4'd9: uart_txd <= 1'b1; // 停止位
default: uart_txd <= 1'b1;
endcase
end
end
// ---------------------------------------------------------------------
// 完成脉冲:停止位发完的那一刻(bit_cnt 刚到 9 的最后一个时钟)
// 这一拍就代表"这一个字节发完了"
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
uart_tx_done <= 1'b0;
else if (bit_cnt == 4'd9 && baud_cnt == BAUD_CNT_MAX - 1)
uart_tx_done <= 1'b1; // 停止位结束 → 发送完成
else
uart_tx_done <= 1'b0;
end
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
驱动的 37 步按阶段分组如下(步骤编号见 vl53l0x_driver.v 里的 STEP_ 常量):
| 阶段 | 步骤范围 | 作用 |
|---|---|---|
| 〇 等上电 | 1 | 等 2 ms,芯片内部时钟约 1.2 ms 才稳 |
| 〇 验 ID | 2 ~ 8 | START + 0x52 + 0xC0 + 重复起始 + 0x53 + 读 1 字节(NACK)+ STOP,锁存 chip_id(期望 0xEE) |
| 〇 切 I2C 模式 | 9 ~ 13 | 写 0x88 = 0x00(I2C 标准模式),完成后置 init_done |
| 一 启动测距 | 14 ~ 18 | 写 0x00 = 0x01(SYSRANGE_START 单次测距) |
| 等待 | 19 | 等 30 ms,芯片自己发光测距 |
| 二 轮询状态 | 20 ~ 28 | 读 0x14,bit0=1 就去读距离;没完成等 5 ms 再问,最多 10 次,超时兜底照样去读 |
| 三 读距离 | 29 ~ 36 | 连读 0x1E/0x1F(高字节回 ACK、低字节回 NACK),拼成 16 位 mm,打一拍 data_valid |
首次 start 从阶段〇一路走到阶段三(初始化 + 测距),之后每次 start 直接从阶段一开始测距。底层 i2c_master 靠 i2c_start 打一拍启动、cmd_req 防止同一步命令重发,重复起始用 CMD_START 不发 STOP,多字节读中间回 ACK、末字节回 NACK,一步一步把字节搬完。
6、I/O 绑定
顶层模块 top 的端口与约束文件 VL53L0X_distance.cst 一一对应:
| 端口 | 方向 | FPGA 引脚 | 接线 |
|---|---|---|---|
sys_clk | input | T7 | 板载 50 MHz 系统时钟 |
rst_n | input | F10 | 复位按键(按下为低,异步复位) |
key | input | D11 | 手动补测按键(按下为低,20 ms 消抖) |
scl | output | R14 | 接 J5 排针空闲脚 → VL53L0X 模块 SCL,需上拉 |
sda | inout | T15 | 接 J5 排针空闲脚 → VL53L0X 模块 SDA,需上拉;双向,靠 sda_oe 切换 |
uart_tx | output | F12 | 接 USB 转串口 RX,115200-8-N-1 |
对应的约束文件内容如下:
// 系统时钟 50MHz
IO_LOC "sys_clk" T7;
IO_PORT "sys_clk" IO_TYPE=LVCMOS33 PULL_MODE=UP BANK_VCCIO=3.3;
// 复位按键
IO_LOC "rst_n" F10;
IO_PORT "rst_n" IO_TYPE=LVCMOS33 PULL_MODE=UP BANK_VCCIO=3.3;
// 手动触发按键
IO_LOC "key" D11;
IO_PORT "key" IO_TYPE=LVCMOS33 PULL_MODE=UP BANK_VCCIO=3.3;
// I2C 总线
IO_LOC "scl" R14;
IO_PORT "scl" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8 BANK_VCCIO=3.3;
IO_LOC "sda" T15;
IO_PORT "sda" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8 BANK_VCCIO=3.3;
// UART 发送引脚
IO_LOC "uart_tx" F12;
IO_PORT "uart_tx" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8 BANK_VCCIO=3.3;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
VL53L0X 模块 4 Pin 这样接:
| VL53L0X 模块 | 开发板 |
|---|---|
| VCC | 3.3 V |
| GND | GND(和 FPGA 共地) |
| SCL | R14(J5 排针空闲脚) |
| SDA | T15(J5 排针空闲脚) |
实际接线请用杜邦线把模块接到 J5 排针:
7、程序下载
上电后打印的第一行就是初始化完成后的第一次测距结果。拿一本书在传感器正前方前后移动,数值应该跟着远近变化(书离得越远数值越大)。按一下按键(key)也会立刻补测一行。贴太近(小于 30 mm)或太远(大于 2000 mm)、目标太黑太斜,读数都不可信。
串口助手设置
波特率 115200、数据位 8、停止位 1、无校验,编码选择 GB2312 / GBK(否则中文会显示为乱码)。
常见问题排查
串口显示乱码:串口助手编码没选 GB2312 / GBK,距离 两个字是 GB2312 编码(距=BEE0、离=C0EB),选对编码就正常了。
距离一直不变或乱跳:先确认读到的器件 ID 是不是 0xEE(调试时可以把 chip_id 端口接到 SignalTap 抓一下);再检查目标是不是太近(小于 30 mm)、太远(大于 2000 mm)、太黑或太斜,这些情况回波太弱,读数不可信。
完全没有输出:检查 SCL、SDA 接线和上拉、模块供电是不是 3.3 V,串口线是否接到 F12。
器件 ID 读不到 0xEE:多半是接线、上拉或地址问题(ADDR 接法变了地址就不是 0x29),也可能是上电没等够 2 ms,请以规格书为准。