SGP30 是盛思锐(Sensirion)推出的一款室内空气质量传感器,I2C 接口,一颗芯片同时输出 eCO2(等效 CO2 浓度,单位 ppm) 和 TVOC(总挥发性有机物,单位 ppb) 两个指标。
它把气敏元件和数字处理电路封装在一起,出厂已经标定好,只要 SCL、SDA 两根线就能读数。
读回的 6 个字节里每 2 字节数据后面就跟 1 字节 CRC8 校验。另外它有条硬规矩:两次测量之间必须严格隔 1 秒,芯片靠这个固定节拍维护自己的基线校准。
1、模块来源
VCC:电源,按模块规格书(GY-SGP30 模块一般 3.3 V / 5 V 供电设计,芯片核心 1.62 - 1.98 V 由板载 LDO 负责)GND:地线,必须和 FPGA 共地SCL:I2C 时钟线,需上拉到模块VDDSDA:I2C 数据线,双向,同样需上拉到模块VDD
2、参数
工作电压:模块一般 3.3 V / 5 V 供电(芯片核心 1.62 - 1.98 V,由板载 LDO 负责,以规格书为准)
通讯接口:I2C(命令式:发 16 位命令码 → 等一会儿 → 读结果)
从机地址:7 位地址 0x58(固定,没有地址引脚可改;写地址 = 0x58 左移一位 = 0xB0,读地址 = 0xB1)
测量输出:eCO2 + TVOC 一次读回(6 个字节 = 2 字节 eCO2 + 1 字节 CRC + 2 字节 TVOC + 1 字节 CRC)
eCO2 量程:400 ~ 60000 ppm(等效 CO2 浓度)
TVOC 量程:0 ~ 60000 ppb(总挥发性有机物)
上电等待:≥ 10 ms 后才能发初始化命令
初始化等待:发完 0x2003 后等 ≥ 10 ms
单次测量等待:发完 0x2008 后等 ≥ 12 ms 再读结果
测量节拍:两次 0x2008 之间必须严格隔 1 s(芯片基线校准靠这个固定节拍)
预热时间:上电头 15 s 芯片固定回基线值(eCO2 = 400 ppm、TVOC = 0 ppb),属正常现象
校验:CRC8,多项式 0x31、初值 0xFF、不反转,每 2 字节数据跟 1 字节 CRC
管脚数量:4 Pin(VCC、GND、SCL、SDA)
3、实战任务
使用立创·逻辑派 FPGA-G1 开发板编写 SGP30 驱动代码,上电后先自动发一次初始化命令建立基线,之后每隔 1 秒自动测量一次,按一下按键(key)也可以手动提前补测一次(并重新计 1 秒),测完通过串口把 eCO2 浓度和 TVOC 浓度一起发给上位机显示:
eCO2: 01234ppm TVOC: 01234ppb一行一共 31 个字节(含回车换行)。eCO2、TVOC 都是 5 位十进制、不足补 0,单位分别是 ppm 和 ppb。输出全是 ASCII 字符,没有中文,不涉及 GB2312 编码问题。
CRC 校验没过时,下面多打一行 CRC:ERR(数值照打,不拦截)。
4、时序说明
4.1 读写时序
SGP30 走的是标准 I2C 协议,起始条件、停止条件、应答这些基础动作和前面模块完全一样,这里不再进行赘述,有兴趣可去模块里的 IIC 屏幕里学习。特别之处在于它是「命令式」的:读写不靠寄存器地址,而是靠命令码。
我们在这里简单的过一下 SGP30 的读写过程时序。
器件地址为 0x58,但是最后一位是读写位,需要空出一位给读写位,因此需要左移一位,即 0x58<<1 得到 0xB0(写)、0xB1(读)
采集步骤:(写=0,读=1)
发命令:起始信号-> 器件地址左移 1 位 + 写(0xB0)-> 等待传感器应答 -> 发送命令高字节(0x20)-> 等待传感器应答 -> 发送命令低字节(init 为 0x03、measure 为 0x08)-> 等待传感器应答 -> 停止信号
读结果:起始信号 -> 器件地址左移 1 位 + 读(0xB1)-> 等待传感器应答 -> 读取 eCO2 高字节 -> 主机发送应答 -> 读取 eCO2 低字节 -> 主机发送应答 -> 读取 eCO2 的 CRC 字节 -> 主机发送应答 -> 读取 TVOC 高字节 -> 主机发送应答 -> 读取 TVOC 低字节 -> 主机发送应答 -> 读取 TVOC 的 CRC 字节 -> 主机发送 NACK(这是最后一个)-> 停止信号
4.2 数据换算
和 AHT10 不同,SGP30 读回的原始值就是物理量本身,不需要再除系数换算:每 2 个字节数据大端拼成 16 位,就是 ppm / ppb 数值。
eCO2(等效 CO2 浓度):
TVOC(总挥发性有机物):
其中 D_H 是数据高字节、D_L 是数据低字节,结果单位分别是 ppm 和 ppb。直观感受一下:读回 01 F4(十六进制)就是 500 ppm;上电头 15 秒固定回 01 90(400 ppm)和 00 00(0 ppb)的基线值。
显示的时候直接拆 5 位十进制、不足补 0,不用管小数:
eCO2:400 ~ 60000 ppm,5 位十进制补 0,如 500 → 00500
TVOC:0 ~ 60000 ppb,5 位十进制补 0,如 12 → 000122
4.3 CRC8
SGP30 的校验是按格式固定带上的:每 2 个字节数据后面就跟 1 个字节 CRC8,多项式 0x31,初值 0xFF,不反转,一共两组(eCO2 一组、TVOC 一组)。自己算一遍和收到的对得上,就说明这批数据没传错(官方参考向量:数据 BE EF 算出 CRC = 92)。
本教程的驱动做了校验但不拦截:校验不过照样把数值打出来,只是多打一行 CRC:ERR 提示你这次数据可疑(想做得更完整可以丢了这次数据重测)。CRC 覆盖哪几个字节、出错是否必须丢弃,请以规格书为准。
5、程序编写
整个工程分四个模块:
- top.v(顶层):1 秒定时 + 按键触发;把 eCO2、TVOC 拆成 5 位十进制;拼 31 个字节的串口行,CRC 出错再补一行
CRC:ERR。 - sgp30_driver.v(SGP30 驱动):单一 always 的 STEP 步骤机(两层架构的上层),管整轮采集的「流程表」:发什么命令、等多久、读哪 6 个字节、CRC 对不对,一次采集 23 步。
- i2c_master.v(I2C 主机):底层搬字节,起始、停止、应答、收发全归它;跟接什么芯片无关,写一次进 IP 库,以后所有 I2C 芯片都复用。
- uart_tx.v(串口发送):和前面串口章节通用,原样沿用。
`timescale 1ns / 1ps
module top (
input sys_clk, // 系统时钟 50MHz
input rst_n, // 复位按键,按下为低
input key, // 手动触发按键,按下为低
output scl, // I2C 时钟线 → SGP30 SCL
inout sda, // I2C 数据线 → SGP30 SDA
output uart_tx // 串口发送 → 上位机
);
// ---------------------------------------------------------------------
// 常量定义
// ---------------------------------------------------------------------
// 1 秒定时:50MHz × 1s = 50000000 个时钟周期
localparam SEC_CNT_MAX = 26'd50_000_000;
// 按键消抖:50MHz × 20ms = 1000000 个时钟周期
localparam KEY_CNT_MAX = 20'd1_000_000;
// ---------------------------------------------------------------------
// 内部连线
// ---------------------------------------------------------------------
// sgp30_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;
// sgp30_driver → top
wire [15: 0] eco2_data;
wire [15: 0] tvoc_data;
wire data_valid;
wire crc_err;
wire drv_busy;
// top → uart_tx
wire uart_tx_done;
wire uart_tx_busy;
reg uart_tx_req;
reg [ 7: 0] uart_tx_data;
// 采集触发
reg measure_start;
reg first_done; // 上电后的第一次采集已经发过了
reg [25: 0] sec_cnt; // 1 秒计数器
// 按键消抖
reg [ 1: 0] key_sync; // 两拍同步,打散亚稳态
reg [19: 0] key_cnt; // 消抖计数器
reg key_stable; // 消抖后的电平(1 = 松手)
reg key_d; // 打一拍,用来找按下边沿
reg key_trig; // 按下瞬间打一拍
// 打印控制
reg work_en; // 1 = 整行发送中
reg [ 5: 0] byte_cnt; // 行内字节计数
reg row_sel; // 0 = 数据行 1 = CRC:ERR 行
reg [15: 0] eco2_r; // 锁存本轮要打印的值
reg [15: 0] tvoc_r;
reg crc_err_r;
// ---------------------------------------------------------------------
// 十进制拆位:eCO2 / TVOC 最大 60000,各拆 5 位
// 先用 16 位把除法结果接住(商最大只有 6,装得下),再取低 4 位当数字,
// 这样写综合器不会报"位宽截断"告警
// {4'h3, 数字} 就是 '0'+数字(ASCII 0x30),拼成字符直接发
// ---------------------------------------------------------------------
wire [15: 0] e4_w = eco2_r / 10000; // 万位
wire [15: 0] e3_w = (eco2_r % 10000) / 1000; // 千位
wire [15: 0] e2_w = (eco2_r % 1000) / 100; // 百位
wire [15: 0] e1_w = (eco2_r % 100) / 10; // 十位
wire [15: 0] e0_w = eco2_r % 10; // 个位
wire [15: 0] t4_w = tvoc_r / 10000; // 万位
wire [15: 0] t3_w = (tvoc_r % 10000) / 1000; // 千位
wire [15: 0] t2_w = (tvoc_r % 1000) / 100; // 百位
wire [15: 0] t1_w = (tvoc_r % 100) / 10; // 十位
wire [15: 0] t0_w = tvoc_r % 10; // 个位
// 十进制位只有 0~9,4 位放得下,取低 4 位
wire [ 3: 0] e4 = e4_w[3:0];
wire [ 3: 0] e3 = e3_w[3:0];
wire [ 3: 0] e2 = e2_w[3:0];
wire [ 3: 0] e1 = e1_w[3:0];
wire [ 3: 0] e0 = e0_w[3:0];
wire [ 3: 0] t4 = t4_w[3:0];
wire [ 3: 0] t3 = t3_w[3:0];
wire [ 3: 0] t2 = t2_w[3:0];
wire [ 3: 0] t1 = t1_w[3:0];
wire [ 3: 0] t0 = t0_w[3:0];
// ---------------------------------------------------------------------
// 按键同步 + 20ms 消抖:电平稳住 20ms 才认,按下(高变低)打一拍 key_trig
// 板上按键按下为低,所以"按下"= 下降沿
// ---------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if (!rst_n) begin
key_sync <= 2'b11; // 复位当松手
key_cnt <= 20'd0;
key_stable <= 1'b1;
key_d <= 1'b1;
key_trig <= 1'b0;
end else begin
key_sync <= {key_sync[0], key}; // 两拍同步
key_d <= key_stable; // 打一拍,下一步做边沿检测
key_trig <= 1'b0; // 默认拉低,只在按下那拍拉高
if (key_sync[1] != key_stable) begin
// 电平跟稳定值不一样:开始数时间,数满 20ms 才认账
if (key_cnt == KEY_CNT_MAX) begin
key_stable <= key_sync[1]; // 稳够久了,更新稳定值
key_cnt <= 20'd0;
end else begin
key_cnt <= key_cnt + 1'b1;
end
end else begin
key_cnt <= 20'd0; // 电平又回去了,白数
end
// 按下 = 稳定值从 1 变 0 的下一拍
if (key_d && !key_stable)
key_trig <= 1'b1;
end
end
// ---------------------------------------------------------------------
// 采集触发:上电第一次 / 1 秒到 / key 按下 → 给 sgp30_driver 打一拍 start
// ★ 1s 计数器每次触发都清零重计,保证两次 measure 之间隔 1 秒
// ★ key 在驱动忙的时候按下就丢了(1 秒后还有下一轮,不碍事)
// ---------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if (!rst_n) begin
measure_start <= 1'b0;
first_done <= 1'b0;
sec_cnt <= 26'd0;
end else begin
measure_start <= 1'b0; // 默认拉低,只在触发那拍拉高
if (!first_done) begin
// 上电第一次:复位后 i2c 空闲就启动
// (第一次采集里带 init_air_quality,见 sgp30_driver)
if (!drv_busy) begin
measure_start <= 1'b1;
first_done <= 1'b1;
sec_cnt <= 26'd0;
end
end
else if (key_trig && !drv_busy) begin
// 手动触发:按一下 key 立刻来一次,并重新计 1 秒
measure_start <= 1'b1;
sec_cnt <= 26'd0;
end
else if (sec_cnt == SEC_CNT_MAX) begin
// 1 秒到:驱动空闲才发(忙就压在最大值上等,忙完下一拍补发)
if (!drv_busy) begin
measure_start <= 1'b1;
sec_cnt <= 26'd0;
end
end
else begin
sec_cnt <= sec_cnt + 1'b1;
end
end
end
// ---------------------------------------------------------------------
// 打印行控制:
// 数据行 "eCO2: XXXXXppm TVOC: XXXXXppb\r\n" 共 31 字节(byte_cnt 0~30)
// CRC 行 "CRC:ERR\r\n" 共 9 字节(byte_cnt 0~8,只有校验没过才打)
// data_valid 来 → 锁存结果, work_en 拉高开始发
// 发完最后一字节 → work_en 落下
// ---------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if (!rst_n) begin
work_en <= 1'b0;
byte_cnt <= 6'd0;
row_sel <= 1'b0;
eco2_r <= 16'd0;
tvoc_r <= 16'd0;
crc_err_r <= 1'b0;
uart_tx_req <= 1'b0;
uart_tx_data <= 8'd0;
end else begin
uart_tx_req <= 1'b0; // 默认拉低,只在发的那一拍拉高
// 采样完成:锁存结果,开始打印
if (data_valid) begin
eco2_r <= eco2_data;
tvoc_r <= tvoc_data;
crc_err_r <= crc_err;
work_en <= 1'b1;
byte_cnt <= 6'd0;
row_sel <= 1'b0;
end
// 打印进行中
else if (work_en) begin
if (uart_tx_done) begin
if (row_sel == 1'b0 && byte_cnt == 6'd30) begin
// 数据行 31 字节发完
byte_cnt <= 6'd0;
if (crc_err_r)
row_sel <= 1'b1; // 校验没过,补一行 CRC:ERR
else
work_en <= 1'b0;
end
else if (row_sel == 1'b1 && byte_cnt == 6'd8) begin
// CRC 行 9 字节发完,收工
byte_cnt <= 6'd0;
row_sel <= 1'b0;
work_en <= 1'b0;
end
else begin
byte_cnt <= byte_cnt + 1'b1;
end
end
else if (!uart_tx_busy) begin
// 装下一个字节并打一拍发送请求
if (row_sel == 1'b0) begin
case (byte_cnt)
// ---- "eCO2: " ----
6'd0: uart_tx_data <= "e";
6'd1: uart_tx_data <= "C";
6'd2: uart_tx_data <= "O";
6'd3: uart_tx_data <= "2";
6'd4: uart_tx_data <= ":";
6'd5: uart_tx_data <= " ";
// ---- eCO2 5 位十进制 ----
6'd6: uart_tx_data <= {4'h3, e4}; // '0'+万位
6'd7: uart_tx_data <= {4'h3, e3}; // '0'+千位
6'd8: uart_tx_data <= {4'h3, e2}; // '0'+百位
6'd9: uart_tx_data <= {4'h3, e1}; // '0'+十位
6'd10: uart_tx_data <= {4'h3, e0}; // '0'+个位
// ---- "ppm " ----
6'd11: uart_tx_data <= "p";
6'd12: uart_tx_data <= "p";
6'd13: uart_tx_data <= "m";
6'd14: uart_tx_data <= " ";
// ---- "TVOC: " ----
6'd15: uart_tx_data <= "T";
6'd16: uart_tx_data <= "V";
6'd17: uart_tx_data <= "O";
6'd18: uart_tx_data <= "C";
6'd19: uart_tx_data <= ":";
6'd20: uart_tx_data <= " ";
// ---- TVOC 5 位十进制 ----
6'd21: uart_tx_data <= {4'h3, t4}; // '0'+万位
6'd22: uart_tx_data <= {4'h3, t3}; // '0'+千位
6'd23: uart_tx_data <= {4'h3, t2}; // '0'+百位
6'd24: uart_tx_data <= {4'h3, t1}; // '0'+十位
6'd25: uart_tx_data <= {4'h3, t0}; // '0'+个位
// ---- "ppb" + 回车换行 ----
6'd26: uart_tx_data <= "p";
6'd27: uart_tx_data <= "p";
6'd28: uart_tx_data <= "b";
6'd29: uart_tx_data <= 8'h0D; // 回车
6'd30: uart_tx_data <= 8'h0A; // 换行
default: uart_tx_data <= 8'h20;
endcase
end else begin
// ---- "CRC:ERR\r\n" ----
case (byte_cnt)
6'd0: uart_tx_data <= "C";
6'd1: uart_tx_data <= "R";
6'd2: uart_tx_data <= "C";
6'd3: uart_tx_data <= ":";
6'd4: uart_tx_data <= "E";
6'd5: uart_tx_data <= "R";
6'd6: uart_tx_data <= "R";
6'd7: uart_tx_data <= 8'h0D; // 回车
6'd8: uart_tx_data <= 8'h0A; // 换行
default: uart_tx_data <= 8'h20;
endcase
end
uart_tx_req <= 1'b1;
end
end
end
end
// ---------------------------------------------------------------------
// 例化:SGP30 驱动(上层步骤机,管采集流程)
// ---------------------------------------------------------------------
sgp30_driver u_sgp30_driver (
.clk (sys_clk ),
.rst_n (rst_n ),
.start (measure_start ),
.eco2_data (eco2_data ),
.tvoc_data (tvoc_data ),
.data_valid (data_valid ),
.crc_err (crc_err ),
.busy (drv_busy ),
.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
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
`timescale 1ns / 1ps
module sgp30_driver (
input clk, // 系统时钟 50MHz
input rst_n, // 复位,低电平有效
// ---------- 对外接口 ----------
input start, // 脉冲:开始一次采集
output reg [15:0] eco2_data, // eCO2 浓度,单位 ppm
output reg [15:0] tvoc_data, // TVOC 浓度,单位 ppb
output reg data_valid, // 单周期脉冲:eco2_data / tvoc_data 有效
output reg crc_err, // 1 = 这次 CRC 校验没过(数值照用)
output reg busy, // 1 = 采集流程进行中
// ---------- 连接 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; // 起始条件(重复起始 Sr 也用它)
localparam CMD_WRITE = 2'b01; // 写 1 字节
localparam CMD_READ = 2'b10; // 读 1 字节
localparam CMD_STOP = 2'b11; // 停止条件
// SGP30 的字节(7 位地址固定 0x58,规格书写死,没有地址引脚可改)
localparam DEV_WRITE = 8'hB0; // 0x58 左移 1 位,最低位 0 = 写
localparam DEV_READ = 8'hB1; // 0x58 左移 1 位,最低位 1 = 读
localparam CMD_INIT_H = 8'h20; // init_air_quality = 0x2003,高字节(大端)
localparam CMD_INIT_L = 8'h03; // init_air_quality 低字节
localparam CMD_MEAS_H = 8'h20; // measure_air_quality = 0x2008,高字节(大端)
localparam CMD_MEAS_L = 8'h08; // measure_air_quality 低字节
// 等待时间(按规格书典型值取整,误差 ±10% 也不缩)
// 上电后等 10ms: 50MHz × 10ms = 500000 个时钟周期
// init 发完等 10ms: 同上
// measure 发完等 12ms: 50MHz × 12ms = 600000 个时钟周期
localparam WAIT_10MS = 20'd500000;
localparam WAIT_12MS = 20'd600000;
// ---------------------------------------------------------------------
// 步骤编码:一次采集拆成 23 步,一步一步走(总线动作各占一步,纯等待占一步)
// ---------------------------------------------------------------------
localparam STEP_IDLE = 5'd0; // 空闲,等 start
localparam STEP_PWR_WAIT = 5'd1; // 上电等 10ms(只有第一次走)
localparam STEP_INIT_START = 5'd2; // START
localparam STEP_INIT_ADDR = 5'd3; // 写 0xB0(地址 + 写)
localparam STEP_INIT_H = 5'd4; // 写 0x20(init 命令高字节)
localparam STEP_INIT_L = 5'd5; // 写 0x03(init 命令低字节),发完芯片开始初始化
localparam STEP_INIT_STOP = 5'd6; // STOP,释放总线
localparam STEP_INIT_WAIT = 5'd7; // init 后等 10ms
localparam STEP_M_START = 5'd8; // START
localparam STEP_M_ADDR = 5'd9; // 写 0xB0(地址 + 写)
localparam STEP_M_H = 5'd10; // 写 0x20(measure 命令高字节)
localparam STEP_M_L = 5'd11; // 写 0x08(measure 命令低字节),发完芯片开始算
localparam STEP_M_STOP = 5'd12; // STOP,释放总线
localparam STEP_M_WAIT = 5'd13; // 等 12ms,等结果算完
localparam STEP_R_START = 5'd14; // START
localparam STEP_R_ADDR = 5'd15; // 写 0xB1(地址 + 读)
localparam STEP_RX_EH = 5'd16; // 读 eCO2 高字节,回 ACK
localparam STEP_RX_EL = 5'd17; // 读 eCO2 低字节,回 ACK
localparam STEP_RX_EC = 5'd18; // 读 eCO2 的 CRC,回 ACK
localparam STEP_RX_TH = 5'd19; // 读 TVOC 高字节,回 ACK
localparam STEP_RX_TL = 5'd20; // 读 TVOC 低字节,回 ACK
localparam STEP_RX_TC = 5'd21; // 读 TVOC 的 CRC,回 NACK(这是最后一个)
localparam STEP_R_STOP = 5'd22; // STOP,一次采集结束,打 data_valid
// ---------------------------------------------------------------------
// 内部寄存器
// ---------------------------------------------------------------------
reg [ 4: 0] step; // 当前步骤
reg [19: 0] wait_cnt; // 等待计数器
reg cmd_req; // 给 i2c_master 的启动脉冲(自己的防重发锁)
reg inited; // init_air_quality 已经发过了
reg [ 7: 0] rx0; // 读回 6 字节,一个一个锁存
reg [ 7: 0] rx1;
reg [ 7: 0] rx2;
reg [ 7: 0] rx3;
reg [ 7: 0] rx4;
reg [ 7: 0] rx5;
// ---------------------------------------------------------------------
// CRC8 计算(纯组合函数,Sensirion 标准算法)
// 多项式 0x31,初值 0xFF,不反转,每 2 字节数据算一个 CRC
// 逐位处理:最高位先算,数据位和 crc 最高位异或决定要不要异或多项式
// ---------------------------------------------------------------------
function [ 7: 0] crc8;
input [15: 0] data; // 2 字节数据,高字节在前
integer i;
reg [ 7: 0] crc;
begin
crc = 8'hFF; // 初值
for (i = 15; i >= 0; i = i - 1) begin
if (crc[7] ^ data[i])
crc = {crc[6:0], 1'b0} ^ 8'h31;
else
crc = {crc[6:0], 1'b0};
end
crc8 = crc;
end
endfunction
// ---------------------------------------------------------------------
// 主流程:一步一步发命令,i2c_done 表示这一步的 I2C 动作做完了
// 上层调用纪律:发命令前判 if (!cmd_req && !i2c_busy),
// i2c_start / cmd_req 每拍默认拉低,只在发命令那拍拉高
// ---------------------------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
step <= STEP_IDLE;
wait_cnt <= 20'd0;
cmd_req <= 1'b0;
inited <= 1'b0;
rx0 <= 8'd0;
rx1 <= 8'd0;
rx2 <= 8'd0;
rx3 <= 8'd0;
rx4 <= 8'd0;
rx5 <= 8'd0;
eco2_data <= 16'd0;
tvoc_data <= 16'd0;
data_valid <= 1'b0;
crc_err <= 1'b0;
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 要先做初始化,以后的 start 直接去测量
// =============================================================
STEP_IDLE: begin
busy <= 1'b0;
if (start) begin
busy <= 1'b1;
if (inited)
step <= STEP_M_START; // 已初始化过,直接测量
else
step <= STEP_PWR_WAIT; // 第一次,先走上电初始化
end
end
// ---- 段零:上电等 10ms,等芯片内部电源稳定下来 ----
STEP_PWR_WAIT: begin
if (wait_cnt == WAIT_10MS) begin
wait_cnt <= 20'd0;
step <= STEP_INIT_START;
end else begin
wait_cnt <= wait_cnt + 1'b1;
end
end
// ---- 阶段一:发 init_air_quality(0x2003),建立基线 ----
STEP_INIT_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_INIT_ADDR;
end
STEP_INIT_ADDR: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= DEV_WRITE; // 0xB0:地址 + 写
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_INIT_H;
end
STEP_INIT_H: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= CMD_INIT_H; // 0x20:命令高字节
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_INIT_L;
end
STEP_INIT_L: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= CMD_INIT_L; // 0x03:命令低字节,凑成 0x2003
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_INIT_STOP;
end
STEP_INIT_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
inited <= 1'b1; // ★ 初始化发过一次就够了,下次不再发
step <= STEP_INIT_WAIT;
wait_cnt <= 20'd0;
end
end
// ---- 中间:init 发完等 10ms,让芯片把基线建起来 ----
STEP_INIT_WAIT: begin
if (wait_cnt == WAIT_10MS) begin
wait_cnt <= 20'd0;
step <= STEP_M_START;
end else begin
wait_cnt <= wait_cnt + 1'b1;
end
end
// ---- 阶段二:发 measure_air_quality(0x2008),芯片开始算 ----
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;
end
STEP_M_ADDR: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= DEV_WRITE; // 0xB0:地址 + 写
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_M_H;
end
STEP_M_H: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= CMD_MEAS_H; // 0x20:命令高字节
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_M_L;
end
STEP_M_L: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= CMD_MEAS_L; // 0x08:命令低字节,凑成 0x2008
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
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_WAIT;
wait_cnt <= 20'd0;
end
end
// ---- 中间:measure 发完等 12ms,等芯片把 eCO2 / TVOC 算出来 ----
STEP_M_WAIT: begin
if (wait_cnt == WAIT_12MS) begin
wait_cnt <= 20'd0;
step <= STEP_R_START;
end else begin
wait_cnt <= wait_cnt + 1'b1;
end
end
// ---- 阶段三:读 6 字节结果 ----
STEP_R_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_R_ADDR;
end
STEP_R_ADDR: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_WRITE;
i2c_tx <= DEV_READ; // 0xB1:地址 + 读
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) step <= STEP_RX_EH;
end
STEP_RX_EH: 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
rx0 <= i2c_rx; // eCO2 高字节,done 那拍锁存
step <= STEP_RX_EL;
end
end
STEP_RX_EL: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b0;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
rx1 <= i2c_rx; // eCO2 低字节
step <= STEP_RX_EC;
end
end
STEP_RX_EC: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b0;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
rx2 <= i2c_rx; // eCO2 的 CRC
step <= STEP_RX_TH;
end
end
STEP_RX_TH: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b0;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
rx3 <= i2c_rx; // TVOC 高字节
step <= STEP_RX_TL;
end
end
STEP_RX_TL: begin
if (!cmd_req && !i2c_busy) begin
i2c_cmd <= CMD_READ;
i2c_nack <= 1'b0;
i2c_start <= 1'b1;
cmd_req <= 1'b1;
end
if (i2c_done) begin
rx4 <= i2c_rx; // TVOC 低字节
step <= STEP_RX_TC;
end
end
STEP_RX_TC: 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
rx5 <= i2c_rx; // TVOC 的 CRC
step <= STEP_R_STOP;
end
end
STEP_R_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
eco2_data <= {rx0, rx1}; // 2 字节拼 16 位,大端
tvoc_data <= {rx3, rx4};
// CRC 校验做但不拦截:不过也照常给数值,只拉 crc_err 告诉顶层
crc_err <= (crc8({rx0, rx1}) != rx2) || (crc8({rx3, rx4}) != rx5);
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
`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
// ---------------------------------------------------------------------
// 参数
// ---------------------------------------------------------------------
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
驱动的状态机一共 23 步,按阶段看是这样:
| 状态 | 作用 |
|---|---|
STEP_IDLE | 空闲,等 start |
STEP_PWR_WAIT | 上电等 10 ms(只有第一次走) |
STEP_INIT_START ~ STEP_INIT_STOP | 发 init_air_quality:START + 0xB0 + 0x20 + 0x03 + STOP |
STEP_INIT_WAIT | init 发完等 10 ms |
STEP_M_START ~ STEP_M_STOP | 发 measure_air_quality:START + 0xB0 + 0x20 + 0x08 + STOP |
STEP_M_WAIT | 等 12 ms,等芯片算完 |
STEP_R_START、STEP_R_ADDR | 发 START + 0xB1(地址 + 读) |
STEP_RX_EH ~ STEP_RX_TC | 连读 6 字节,前 5 字节回 ACK、最后 1 字节回 NACK |
STEP_R_STOP | 发 STOP,锁存数据、做 CRC 校验,打 data_valid |
6、I/O 绑定
顶层模块 top 的端口与约束文件 SGP30_gas.cst 一一对应:
| 端口 | 方向 | FPGA 引脚 | 接线 |
|---|---|---|---|
sys_clk | input | T7 | 板载 50 MHz 系统时钟 |
rst_n | input | F10 | 复位按键(按下为低,异步复位) |
key | input | D11 | 手动触发按键(按下为低,20 ms 消抖,补测一次) |
scl | output | R14 | 接 J5 排针空闲脚 → SGP30 模块 SCL,需上拉到模块 VDD |
sda | inout | T15 | 接 J5 排针空闲脚 → SGP30 模块 SDA,需上拉到模块 VDD;双向,靠 sda_oe 切换 |
uart_tx | output | F12 | 接 USB 转串口 RX,115200-8-N-1 |
对应的约束文件内容如下:
// 引脚约束(立创·逻辑派 FPGA-G1 开发板)
// 模块: SGP30_gas(GY-SGP30 气体传感器例程)
// 器件: GW2A-LV18PG256C8/I7
// 注意: G1 板 IO 为 3.3V(LVCMOS33)
// 系统时钟 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
24
25
26
27
GY-SGP30 模块 4 Pin 这样接:
| GY-SGP30 模块 | 开发板 |
|---|---|
| VCC | 3.3 V |
| GND | GND(和 FPGA 共地) |
| SCL | R14(J5 排针空闲脚) |
| SDA | T15(J5 排针空闲脚) |
⚠️警告:
若模块是 5 V 供电版本,SCL、SDA 可能是 5 V 电平,必须先做电平转换再接 FPGA,不能直连!建议优先用 3.3 V 供电。请以规格书为准。
实际接线请用杜邦线把模块接到 J5 排针:
7、程序下载
eCO2: 01234ppm TVOC: 01234ppb上电头 15 秒芯片在做基线校准,固定回 eCO2 = 400 ppm、TVOC = 0 ppb(也就是一直打 eCO2: 00400ppm TVOC: 00000ppb),属正常现象,不是坏了。15 秒之后数值开始随空气变化:对着传感器哈一口气、拿酒精棉在旁边晃一晃,TVOC 应该往上涨;人多不通风的屋里 eCO2 会变大。按一下手动触发按键(key)会立刻补测一行(并重新计 1 秒)。CRC 校验没过时会多打一行 CRC:ERR,数值照打不拦截。
串口助手设置
波特率 115200、数据位 8、停止位 1、无校验(本例输出全是 ASCII 字符,编码选 UTF-8 或 GBK 都不会乱码)。
常见问题排查
一直打 eCO2: 00400ppm TVOC: 00000ppb:先看是不是还在上电头 15 秒预热里;过了 15 秒还不变,查 SCL、SDA 是不是接反了、模块供没供上电。
一直多打 CRC:ERR:I2C 能读到数说明线是通的,多为波形质量问题,查模块上拉电阻、杜邦线是否过长,或把 i2c_master.v 里的 TICK 改大放慢 SCL 再试。
串口全是乱码:波特率没对上 115200,或串口助手接的不是 F12 对应的串口。
完全不出数:查共地、查 R14 / T15 引脚约束,用 SignalTap 抓 scl、sda 看有没有波形。