AGS10 是奥松(ASAIR)出的数字式 TVOC 传感器。TVOC 就是「总挥发性有机物」,通俗讲就是空气里那些油漆味、酒精味、香水味、新家具味的总称。
它内部把气敏元件和信号处理电路都封好了,直接用 I2C 把浓度数字吐出来,单位是 ppb(十亿分之一),量程 0~99999 ppb,不需要外部 AD。
INFO
- 它是「寄存器式」I2C:不能上来就裸读,得先告诉它「我要读寄存器
0x00」(写阶段),再用重复起始换个方向读(读阶段)。应答一共 5 个字节:1 字节状态 + 3 字节 TVOC 数值 + 1 字节 CRC 校验。 - 时钟频率:AGS10 要求 I2C 时钟不超过 15 kHz(社区实践普遍用 10 kHz)。 上电后前几分钟读数会偏高,因为传感器基线还没稳定,这是正常现象,不是坏了。
1、模块来源
🎫更多信息
本文协议部分的参考资料(第三方资料,最终以官方规格书为准):
- ESPHome 的 AGS10 组件文档:默认地址、地址可修改(一条总线挂多个 AGS10)
- ESPboards 的 AGS10 引脚与协议说明:「5 字节读:状态字节 + 24 位 TVOC(ppb)+ CRC-8」
- gaveshalabs/AGS10_sensor(MicroPython 驱动):明确写了 I2C 必须跑在 15 kHz 以下
- gina-seth/AGS10_sensor(Arduino 驱动)
- AGS10 的 I2C 读取实操笔记:读寄存器
0x00、CRC-8 校验
2、参数
工作电压:典型 3.3 V(也有 5 V 模块版本,注意电平问题)
通讯接口:I2C,时钟必须 ≤15 kHz(本教程取 10 kHz)
从机地址:0x1A(7 位地址,写 0x34 / 读 0x35;地址可修改,方法以规格书为准)
测量对象:TVOC(总挥发性有机物)
量程:0~99999 ppb
数据输出:I2C 数字量,单位 ppb
数据格式:1 字节状态 + 3 字节 TVOC(24 位大端)+ 1 字节 CRC8
预热时间:上电后前几分钟基线未稳定,读数偏高(具体秒数以规格书为准)
管脚数量:4 Pin(VCC、GND、SCL、SDA),部分模块多一个 INT,本教程不用
⚠️警告:
5 V 版本模块不要直连 FPGA! 逻辑派 FPGA-G1 的 IO 电平是 3.3 V(LVCMOS33),而 5 V 模块的 I2C 上拉到 5 V,SCL / SDA 高电平就是 5 V,直接连接有烧坏 FPGA 引脚的风险。 请使用 3.3 V 供电版本的模块;手上只有 5 V 模块的话,必须加电平转换电路。
3、实战任务
使用逻辑派 FPGA-G1 开发板编写 AGS10 驱动代码,完成三件事:
- 每隔 1 秒自动测量一次 TVOC;按下测量按键(
D11,低电平有效)可以随时补测一次。 - 测量成功时,串口每秒打印两行——第一行是浓度值,第二行是原始字节调试行(纯 ASCII),例如:
TVOC浓度为:00125ppb
RAW:80 00 00 7D 4F CRC=12
调试行里 SS HH MM LL CC 依次是状态字节、TVOC 高/中/低字节、传感器给的 CRC,CRC=1 表示本地校验通过。出问题时这一行能直接看出传感器回了什么。
- 传感器没应答(NACK)时,如实打印
AGS10无应答,不显示假数值——该查接线查接线,该查供电查供电。
浓度数值固定 5 位十进制、不够补 0;AGS10 量程就是 0~99999 ppb,超出封顶显示。
4、时序说明
AGS10 用的是标准 I2C 协议 读取TVOC数据的步骤:
5、通讯步骤
完整一轮读数 = 写阶段 + 重复起始 + 读阶段,一口气完成:
- 主机发起始条件(SCL 高,SDA 拉低)。
- 主机发写地址
0x34(0x1A左移一位 + 写位 0),MSB 先发,等 ACK。 - 主机发寄存器地址
0x00(TVOC 数据寄存器),等 ACK。 - 主机发重复起始(不发 STOP,直接再来一次 START)。
- 主机发读地址
0x35,等 ACK。 - 连续读 5 个字节:byte0 状态、byte1~3 TVOC(大端)、byte4 CRC8。
- 前 4 个字节读完,主机回 ACK(拉低 SDA),继续读。
- 第 5 个字节读完,主机回 NACK(拉高 SDA)。
- 主机发停止条件(SCL 高,SDA 拉高),事务结束。
- 顶层拿到
read_done脉冲:ack_ok为 0 就打印「AGS10无应答」;为 1 就把 TVOC 拆成 5 位十进制打印,并附一行原始字节 + CRC 校验结果。
🪜注意:
本教程的驱动只把 byte0 收下来存着,没有解析它的状态位域(数据版本、是否有效等)。位域怎么划分请以规格书为准,想做得更完整可以按规格书加上判断,读到无效数据就丢弃重测。
5.1 数据换算
读回来的 byte1、byte2、byte3 按大端拼成 24 位:
直接就是 ppb,不需要再套任何换算公式,这点比 SHT20 / SHT30 那种「原始值 + 公式」的爽多了。举个例子:
| byte1 | byte2 | byte3 | 拼出来的 TVOC |
|---|---|---|---|
0x00 | 0x01 | 0xF4 | 500 ppb |
0x00 | 0x00 | 0x64 | 100 ppb |
显示上只拆 5 位十进制(万、千、百、十、个),超 99999 封顶。
5.2 CRC8 校验
byte4 是传感器算好的 CRC8 校验值。驱动里用同样的算法对 byte0~3 本地再算一遍,两边一致说明数据在路上没被干扰。算法是 I2C 传感器圈的老熟人了:多项式 0x31、初值 0xFF、不反转,和奥松 AHT 系列、SHT3x 一致。CRC 覆盖的字节范围以规格书为准;本工程校验不过时只在调试行里把 CRC=0 标出来,不影响浓度显示——先保证能看到数,再慢慢核对细节。
6、程序编写
整个工程分成三个模块:
- top.v(顶层):1 秒定时 + 按键消抖,谁来脉冲都测一次;测量结束后按
ack_ok分两条路——没应答发 13 字节「AGS10无应答」,有应答发 22 字节数值行 + 26 字节RAW:调试行。 - ags10_driver.v(I2C 驱动):状态机实现「写寄存器 + 重复起始 + 读 5 字节」的完整时序,SCL 分频到 10 kHz(
SCL_DIV = 2500),带 ACK 检测(ack_ok)和 CRC8 校验(crc_ok)。和 SHT20 驱动比,多了 RESTART 状态和 5 字节读,少了「触发测量后等 80 ms」的环节——AGS10 是连续测量的,随时去读都有新数据。 - uart.v(串口发送):115200-8-N-1 发送,和之前教程完全一样,原样沿用。
`timescale 1ns/1ps
module top(
input sys_clk, // 系统时钟 50MHz
input rst_n, // 复位,低电平有效
output scl, // I2C 时钟线
inout sda, // I2C 数据线
output uart_tx, // 串口发送线
input key // 按键(低电平有效),按下补测一次
);
//------------------------------------------------------------------
// 测量触发:1 秒定时器 + 按键消抖脉冲,谁来都测一次
//------------------------------------------------------------------
reg [25:0] timer_cnt; // 1 秒计数器
reg timer_pls; // 每秒发一个测量请求脉冲
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0) begin
timer_cnt <= 26'd0;
timer_pls <= 1'b0;
end
else if(timer_cnt == 26'd50_000_000 - 1'b1) begin
timer_cnt <= 26'd0;
timer_pls <= 1'b1; // 计满 1 秒,发出一个测量脉冲
end
else begin
timer_cnt <= timer_cnt + 1'b1;
timer_pls <= 1'b0;
end
end
//---- 按键消抖:按下(低电平)稳定 20ms 后,key_flag 发一个脉冲 ----
localparam CNT_MAX = 20'd999_999; // 消抖计数器:1000000 个时钟 = 20ms
reg [19:0] cnt_20ms;
reg key_flag;
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
cnt_20ms <= 20'b0;
else if(key == 1'b1) // 松开:计数清零,等下次按下
cnt_20ms <= 20'b0;
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 == 1'b0)
key_flag <= 1'b0;
else if(cnt_20ms == CNT_MAX - 1'b1) // 计满的前一拍:发一个脉冲
key_flag <= 1'b1;
else
key_flag <= 1'b0;
end
// 两个触发源或在一起:任一来脉冲就测一次
wire measure_start = timer_pls | key_flag;
//------------------- ags10_driver 例化 -------------------
wire [7:0] tvoc_status; // byte0:状态/数据版本
wire [23:0] tvoc_ppb; // byte1..3:TVOC 值(ppb)
wire [7:0] tvoc_crc; // byte4:传感器回的 CRC8
wire ack_ok; // I2C 应答标志:0 = 传感器没应答
wire crc_ok; // CRC8 校验标志
wire read_done; // 读完成脉冲
ags10_driver u_ags10 (
.sys_clk (sys_clk),
.rst_n (rst_n),
.scl (scl),
.sda (sda),
.measure_start (measure_start),
.status_reg (tvoc_status),
.tvoc_reg (tvoc_ppb),
.crc_reg (tvoc_crc),
.ack_ok (ack_ok),
.crc_ok (crc_ok),
.read_done (read_done)
);
//------------------- TVOC 显示数值 -------------------
// TVOC(ppb) = (byte1<<16)|(byte2<<8)|byte3,直接是 ppb,无需公式换算
// 显示只拆 5 位十进制(0~99999,AGS10 量程就是 0~99999ppb),超出封顶
wire [23:0] tvoc_disp = (tvoc_ppb > 24'd99999) ? 24'd99999 : tvoc_ppb;
// 拆分每一位数字:d_wan=万位,d_qian=千位,d_bai=百位,d_shi=十位,d_ge=个位
wire [3:0] d_wan = (tvoc_disp / 24'd10000) % 24'd10;
wire [3:0] d_qian = (tvoc_disp / 24'd1000) % 24'd10;
wire [3:0] d_bai = (tvoc_disp / 24'd100) % 24'd10;
wire [3:0] d_shi = (tvoc_disp / 24'd10) % 24'd10;
wire [3:0] d_ge = tvoc_disp % 24'd10;
//------------------- 串口发送控制 -------------------
// 字符串常量(GB2312/GBK 十六进制写法,已用 iconv 逐字核对):
parameter STR_HEAD = 96'h54_56_4F_43_C5_A8_B6_C8_CE_AA_A3_BA; // "TVOC浓度为:"(CE AA = 为)
parameter STR_UNIT = 24'h70_70_62; // "ppb"
parameter STR_NOACK = 88'h41_47_53_31_30_CE_DE_D3_A6_B4_F0; // "AGS10无应答"
// 十进制数字 0~9 转 ASCII 码
function [7:0] dec2ascii;
input [3:0] d;
begin
dec2ascii = 8'h30 + d; // 0~9 -> '0'~'9'
end
endfunction
// 十六进制半字节 0~F 转 ASCII 码(调试行用)
function [7:0] hex2ascii;
input [3:0] h;
begin
if(h < 4'd10) hex2ascii = 8'h30 + h; // 0~9 -> '0'~'9'
else hex2ascii = 8'h37 + h; // A~F -> 'A'~'F'(0x37+10=0x41='A')
end
endfunction
// 正常:22 字节数值行 + 26 字节调试行 = 48;无应答:11 字节 + CRLF = 13
localparam TX_OK_TOTAL = 6'd48;
localparam TX_NAK_TOTAL = 6'd13;
wire [5:0] tx_total = ack_ok ? TX_OK_TOTAL : TX_NAK_TOTAL;
reg [5:0] tx_byte_cnt; // 发到第几个字节
reg [1:0] tx_state; // 发送状态
reg uart_tx_req; // 给 uart 的发送请求
reg [7:0] tx_data; // 当前要发的字节
localparam S_IDLE = 2'd0; // 空闲
localparam S_SEND = 2'd1; // 发送中
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0) begin
tx_state <= S_IDLE;
tx_byte_cnt <= 6'd0;
uart_tx_req <= 1'b0;
tx_data <= 8'd0;
end
else begin
uart_tx_req <= 1'b0;
case(tx_state)
S_IDLE: begin
tx_byte_cnt <= 6'd0;
if(read_done) // 一次测量结束(成功/失败都发提示)
tx_state <= S_SEND;
end
S_SEND: begin
if(tx_byte_cnt == tx_total) // 所有字节发完,回空闲
tx_state <= S_IDLE;
else if(uart_tx_done || tx_byte_cnt == 6'd0) begin
if(ack_ok == 1'b0) begin
//---- 无应答:"AGS10无应答\r\n" 共 13 字节 ----
case(tx_byte_cnt)
6'd0: tx_data <= STR_NOACK[87:80]; // 'A'
6'd1: tx_data <= STR_NOACK[79:72]; // 'G'
6'd2: tx_data <= STR_NOACK[71:64]; // 'S'
6'd3: tx_data <= STR_NOACK[63:56]; // '1'
6'd4: tx_data <= STR_NOACK[55:48]; // '0'
6'd5: tx_data <= STR_NOACK[47:40]; // "无" 高字节
6'd6: tx_data <= STR_NOACK[39:32]; // "无" 低字节
6'd7: tx_data <= STR_NOACK[31:24]; // "应" 高字节
6'd8: tx_data <= STR_NOACK[23:16]; // "应" 低字节
6'd9: tx_data <= STR_NOACK[15:8]; // "答" 高字节
6'd10: tx_data <= STR_NOACK[7:0]; // "答" 低字节
6'd11: tx_data <= 8'h0D; // 回车 CR
6'd12: tx_data <= 8'h0A; // 换行 LF
default: tx_data <= 8'd0;
endcase
end
else begin
case(tx_byte_cnt)
//---- 第 1 行 "TVOC浓度为:XXXXXppb\r\n" 共 22 字节 ----
6'd0: tx_data <= STR_HEAD[95:88]; // "T"
6'd1: tx_data <= STR_HEAD[87:80]; // "V"
6'd2: tx_data <= STR_HEAD[79:72]; // "O"
6'd3: tx_data <= STR_HEAD[71:64]; // "C"
6'd4: tx_data <= STR_HEAD[63:56]; // "浓" 高字节
6'd5: tx_data <= STR_HEAD[55:48]; // "浓" 低字节
6'd6: tx_data <= STR_HEAD[47:40]; // "度" 高字节
6'd7: tx_data <= STR_HEAD[39:32]; // "度" 低字节
6'd8: tx_data <= STR_HEAD[31:24]; // "为" 高字节
6'd9: tx_data <= STR_HEAD[23:16]; // "为" 低字节
6'd10: tx_data <= STR_HEAD[15:8]; // ":" 高字节
6'd11: tx_data <= STR_HEAD[7:0]; // ":" 低字节
6'd12: tx_data <= dec2ascii(d_wan); // 万位
6'd13: tx_data <= dec2ascii(d_qian); // 千位
6'd14: tx_data <= dec2ascii(d_bai); // 百位
6'd15: tx_data <= dec2ascii(d_shi); // 十位
6'd16: tx_data <= dec2ascii(d_ge); // 个位
6'd17: tx_data <= STR_UNIT[23:16]; // "p"
6'd18: tx_data <= STR_UNIT[15:8]; // "p"
6'd19: tx_data <= STR_UNIT[7:0]; // "b"
6'd20: tx_data <= 8'h0D; // 回车 CR
6'd21: tx_data <= 8'h0A; // 换行 LF
//---- 第 2 行 "RAW:SS HH MM LL CC CRC=x\r\n" 共 26 字节(调试用)----
6'd22: tx_data <= "R";
6'd23: tx_data <= "A";
6'd24: tx_data <= "W";
6'd25: tx_data <= ":";
6'd26: tx_data <= hex2ascii(tvoc_status[7:4]); // 状态字节高半
6'd27: tx_data <= hex2ascii(tvoc_status[3:0]); // 状态字节低半
6'd28: tx_data <= " ";
6'd29: tx_data <= hex2ascii(tvoc_ppb[23:20]); // TVOC 高字节
6'd30: tx_data <= hex2ascii(tvoc_ppb[19:16]);
6'd31: tx_data <= " ";
6'd32: tx_data <= hex2ascii(tvoc_ppb[15:12]); // TVOC 中字节
6'd33: tx_data <= hex2ascii(tvoc_ppb[11:8]);
6'd34: tx_data <= " ";
6'd35: tx_data <= hex2ascii(tvoc_ppb[7:4]); // TVOC 低字节
6'd36: tx_data <= hex2ascii(tvoc_ppb[3:0]);
6'd37: tx_data <= " ";
6'd38: tx_data <= hex2ascii(tvoc_crc[7:4]); // 传感器 CRC
6'd39: tx_data <= hex2ascii(tvoc_crc[3:0]);
6'd40: tx_data <= " ";
6'd41: tx_data <= "C";
6'd42: tx_data <= "R";
6'd43: tx_data <= "C";
6'd44: tx_data <= "=";
6'd45: tx_data <= crc_ok ? "1" : "0"; // 本地校验结果
6'd46: tx_data <= 8'h0D; // 回车 CR
6'd47: tx_data <= 8'h0A; // 换行 LF
default: tx_data <= 8'd0;
endcase
end
uart_tx_req <= 1'b1;
tx_byte_cnt <= tx_byte_cnt + 1'b1;
end
end
default: tx_state <= S_IDLE;
endcase
end
end
//------------------- uart_tx 例化 -------------------
wire uart_tx_done;
wire uart_tx_busy;
uart_tx u_uart (
.clk (sys_clk),
.rst_n (rst_n),
.uart_tx_req (uart_tx_req),
.tx_in_data (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
`timescale 1ns/1ps
module ags10_driver(
input sys_clk, // 系统时钟 50MHz
input rst_n, // 复位,低电平有效
output scl, // I2C 时钟线(接从机 SCL)
inout sda, // I2C 数据线(双向,接从机 SDA)
input measure_start, // 测量请求:一个脉冲触发一次读取
output reg [7:0] status_reg, // byte0:状态/数据版本
output reg [23:0] tvoc_reg, // byte1..3:TVOC 值,24 位大端,单位 ppb
output reg [7:0] crc_reg, // byte4:传感器回的 CRC8
output reg ack_ok, // 1=全程 ACK;0=有 NACK(查接线/供电/上拉)
output crc_ok, // 1=本地 CRC8 与 byte4 一致
output read_done // 事务结束脉冲(成功/失败都发)
);
//------------------------------------------------------------------
// 状态定义
//------------------------------------------------------------------
localparam IDLE = 4'd0; // 空闲:等测量请求
localparam START = 4'd1; // 起始条件:SCL 高时 SDA 拉低
localparam WRITE = 4'd2; // 主机发一个字节(写地址/寄存器/读地址)
localparam WAIT_ACK = 4'd3; // 第 9 个时钟:采样从机 ACK
localparam RESTART = 4'd4; // 重复起始条件
localparam READ = 4'd5; // 主机收一个字节(共 5 个)
localparam MACK = 4'd6; // 中间字节收完:主机发 ACK(拉低 SDA)
localparam MNACK = 4'd7; // 最后字节收完:主机发 NACK(拉高 SDA)
localparam STOP = 4'd8; // 停止条件:SCL 高时 SDA 拉高
//------------------------------------------------------------------
// 分频参数:AGS10 要求 SCL ≤15kHz,这里取 10kHz
//------------------------------------------------------------------
localparam SCL_DIV = 2500; // SCL 半周期 = 2500 个 sys_clk(50us)
// SCL = 50MHz / (2×2500) = 10kHz
localparam add_write = 8'h34; // 写地址 = 0x1A << 1
localparam add_read = 8'h35; // 读地址 = (0x1A << 1) | 1
localparam reg_gas = 8'h00; // TVOC 数据寄存器地址(其余寄存器以规格书为准)
//------------------------------------------------------------------
// 内部寄存器
//------------------------------------------------------------------
reg [3:0] cur_state; // 当前状态
reg [3:0] nxt_state; // 下一状态
reg [15:0] div_cnt; // 节拍计数器
reg [3:0] bit_cnt; // 位计数器:一个字节发到/收到第几位(0~8)
reg [1:0] wr_cnt; // 已经发出去的写字节数:1=写地址 2=+寄存器 3=+读地址
reg [2:0] rx_cnt; // 读字节计数:0~4(状态/TVOC高中低/CRC)
reg [7:0] rx_shift; // 读移位寄存器:一位一位收,收满 8 位再入库
reg sda_oe; // 1=主机驱动 SDA,0=释放给从机
reg sda_out; // 要发到 SDA 上的电平
reg scl_out; // 内部生成的 SCL
//------------------------------------------------------------------
// 连续赋值
//------------------------------------------------------------------
assign sda = sda_oe ? sda_out : 1'bz; // 三态控制:该放手时放手
assign scl = scl_out;
// 事务结束脉冲:STOP 数满 3 个半周期(正常结束和中止都走这里)
assign read_done = (cur_state == STOP && div_cnt == 3*SCL_DIV);
//------------------------------------------------------------------
// CRC8 校验(多项式 0x31、初值 0xFF、不反转,与奥松 AHT 系列一致)
// 覆盖 byte0..3(状态 + TVOC 三字节),覆盖范围以规格书为准
//------------------------------------------------------------------
function [7:0] crc8_ags10;
input [31:0] data;
reg [7:0] crc;
integer i;
begin
crc = 8'hFF;
for(i = 31; 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_ags10 = crc;
end
endfunction
wire [7:0] crc_calc = crc8_ags10({status_reg, tvoc_reg});
assign crc_ok = (crc_calc == crc_reg);
//------------------------------------------------------------------
// 当前要发的字节:正在发第 wr_cnt+1 个(0x34 → 0x00 → 0x35)
//------------------------------------------------------------------
reg [7:0] tx_byte;
always @(*) begin
case(wr_cnt)
2'd0: tx_byte = add_write; // 第 1 个:写地址
2'd1: tx_byte = reg_gas; // 第 2 个:寄存器地址
default: tx_byte = add_read; // 第 3 个:读地址
endcase
end
//------------------------------------------------------------------
// SDA 方向:读数据、等应答时释放(从机驱动),其余主机驱动
//------------------------------------------------------------------
always @(*) begin
if(cur_state == READ || cur_state == WAIT_ACK)
sda_oe = 1'b0;
else
sda_oe = 1'b1;
end
//------------------------------------------------------------------
// 状态寄存器
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
cur_state <= IDLE;
else
cur_state <= nxt_state;
end
//------------------------------------------------------------------
// 状态机(组合逻辑)
//
// 整条读数流程(初始状态 → 谁拉低 → 谁发 → 谁收 → 怎么结束):
// IDLE 空闲等请求
// START 主机发起始条件
// WRITE 主机发 0x34(写地址)
// WAIT_ACK 第 9 个时钟采样从机 ACK,没应答就中止去 STOP
// WRITE 主机发 0x00(寄存器地址)
// WAIT_ACK 再采一次 ACK
// RESTART 重复起始(不掉 STOP 直接换方向)
// WRITE 主机发 0x35(读地址)
// WAIT_ACK 再采一次 ACK
// READ×5 主机收 5 个字节:状态/TVOC高/中/低/CRC
// 前 4 个字节收完主机回 ACK(MACK),第 5 个回 NACK(MNACK)
// STOP 主机发停止条件,事务结束
//------------------------------------------------------------------
always @(*) begin
case(cur_state)
IDLE: begin
if(measure_start == 1'b1)
nxt_state = START;
else
nxt_state = IDLE;
end
START: begin
if(div_cnt == 2*SCL_DIV) // 起始条件发完(SDA 已低、SCL 已低)
nxt_state = WRITE;
else
nxt_state = START;
end
WRITE: begin
if(bit_cnt == 8 && scl_out == 1'b0) // 8 位发完,进第 9 个时钟
nxt_state = WAIT_ACK;
else
nxt_state = WRITE;
end
WAIT_ACK: begin
if(div_cnt == SCL_DIV && scl_out == 1'b1) begin // 第 9 个时钟高半周结束
if(ack_ok == 1'b0)
nxt_state = STOP; // 有 NACK:中止事务,直接停止
else if(wr_cnt == 2'd1)
nxt_state = WRITE; // 发完 1 个(写地址0x34)→ 接着发寄存器地址
else if(wr_cnt == 2'd2)
nxt_state = RESTART; // 发完 2 个(0x34+0x00)→ 重复起始
else
nxt_state = READ; // 发完 3 个(+读地址0x35)→ 开始读 5 字节
end
else
nxt_state = WAIT_ACK;
end
RESTART: begin
if(div_cnt == 3*SCL_DIV) // 重复起始发完
nxt_state = WRITE;
else
nxt_state = RESTART;
end
READ: begin
if(bit_cnt == 8 && scl_out == 1'b0) begin
if(rx_cnt == 3'd4)
nxt_state = MNACK; // 第 5 个字节收完 → 主机 NACK
else
nxt_state = MACK; // 前 4 个字节收完 → 主机 ACK
end
else
nxt_state = READ;
end
MACK: begin
if(div_cnt == SCL_DIV && scl_out == 1'b1) // ACK 发完,读下一个字节
nxt_state = READ;
else
nxt_state = MACK;
end
MNACK: begin
if(div_cnt == SCL_DIV && scl_out == 1'b1) // NACK 发完,去停止
nxt_state = STOP;
else
nxt_state = MNACK;
end
STOP: begin
if(div_cnt == 3*SCL_DIV) // 停止条件发完
nxt_state = IDLE;
else
nxt_state = STOP;
end
default: nxt_state = IDLE;
endcase
end
//------------------------------------------------------------------
// 写阶段计数 wr_cnt:每发完一个字节(离开 WRITE 进 WAIT_ACK)就 +1
// WAIT_ACK 里看到的 wr_cnt = 已经发出去的字节数(1 / 2 / 3)
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
wr_cnt <= 2'd0;
else if(cur_state == IDLE)
wr_cnt <= 2'd0;
else if(cur_state == WRITE && cur_state != nxt_state)
wr_cnt <= wr_cnt + 1'b1;
end
//------------------------------------------------------------------
// 读字节计数 rx_cnt:每离开一次 MACK 就 +1(0~4)
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
rx_cnt <= 3'd0;
else if(cur_state == IDLE)
rx_cnt <= 3'd0;
else if(cur_state == MACK && cur_state != nxt_state)
rx_cnt <= rx_cnt + 1'b1;
end
//------------------------------------------------------------------
// ACK 标志 ack_ok:事务开始先假定成功,采到一次 NACK 就清 0(粘滞)
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
ack_ok <= 1'b0;
else if(cur_state == IDLE && measure_start == 1'b1)
ack_ok <= 1'b1; // 新事务开始
else if(cur_state == WAIT_ACK && div_cnt == SCL_DIV/2 &&
scl_out == 1'b1 && sda == 1'b1)
ack_ok <= 1'b0; // 第 9 个时钟高半周中点 SDA 还是高 = NACK
end
//------------------------------------------------------------------
// 节拍计数器 div_cnt
// 数据/应答态:0~SCL_DIV 循环(一个 SCL 半周期)
// START/RESTART/STOP:自由计数,靠换状态清零
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
div_cnt <= 16'd0;
else if(cur_state == IDLE)
div_cnt <= 16'd0;
else if(cur_state != nxt_state) // 换状态:清零
div_cnt <= 16'd0;
else if(cur_state == WRITE || cur_state == WAIT_ACK ||
cur_state == READ || cur_state == MACK || cur_state == MNACK) begin
if(div_cnt == SCL_DIV) // 半周期到:清零(SCL 同时翻转)
div_cnt <= 16'd0;
else
div_cnt <= div_cnt + 1'b1;
end
else
div_cnt <= div_cnt + 1'b1; // START/RESTART/STOP 自由计数
end
//------------------------------------------------------------------
// 位计数器 bit_cnt:每过一个 SCL 高电平中点 +1(0~8)
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
bit_cnt <= 4'd0;
else if(cur_state == IDLE || cur_state == START || cur_state == RESTART ||
cur_state == STOP || cur_state == WAIT_ACK ||
cur_state == MACK || cur_state == MNACK)
bit_cnt <= 4'd0;
else if((cur_state == WRITE || cur_state == READ) &&
div_cnt == SCL_DIV/2 && scl_out == 1'b1)
bit_cnt <= bit_cnt + 1'b1;
end
//------------------------------------------------------------------
// SCL 时钟生成
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
scl_out <= 1'b1; // 空闲 SCL 高
else if(cur_state == START && div_cnt == 2*SCL_DIV)
scl_out <= 1'b0; // 起始末尾:SCL 拉低
else if(cur_state == RESTART && div_cnt == SCL_DIV)
scl_out <= 1'b1; // 重复起始②:先抬 SCL
else if(cur_state == RESTART && div_cnt == 3*SCL_DIV)
scl_out <= 1'b0; // 重复起始④:再拉低,准备写
else if(cur_state == STOP && div_cnt == SCL_DIV)
scl_out <= 1'b1; // 停止②:抬 SCL
else if((cur_state == WRITE || cur_state == WAIT_ACK || cur_state == READ ||
cur_state == MACK || cur_state == MNACK) && div_cnt == SCL_DIV)
scl_out <= ~scl_out; // 数据/应答:半周期翻转
end
//------------------------------------------------------------------
// SDA 发送控制 sda_out
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
sda_out <= 1'b1;
else if(cur_state == IDLE)
sda_out <= 1'b1; // 空闲:SDA 高
else if(cur_state == START && div_cnt == SCL_DIV/2)
sda_out <= 1'b0; // 起始:SCL 高时 SDA 拉低
else if(cur_state == WRITE && div_cnt == SCL_DIV/2 && scl_out == 1'b0)
sda_out <= tx_byte[7 - bit_cnt]; // 写:SCL 低半周中点换数据(MSB 先发)
else if(cur_state == RESTART && div_cnt == 0)
sda_out <= 1'b1; // 重复起始①:SDA 抬高
else if(cur_state == RESTART && div_cnt == 2*SCL_DIV)
sda_out <= 1'b0; // 重复起始③:SCL 高时 SDA 拉低
else if(cur_state == MACK)
sda_out <= 1'b0; // 主机 ACK:全程拉低
else if(cur_state == MNACK)
sda_out <= 1'b1; // 主机 NACK:全程拉高
else if(cur_state == STOP && div_cnt == 0)
sda_out <= 1'b0; // 停止①:SDA 先拉低
else if(cur_state == STOP && div_cnt == 2*SCL_DIV)
sda_out <= 1'b1; // 停止③:SCL 高时 SDA 拉高 = STOP
end
//------------------------------------------------------------------
// 读数据:SCL 高半周中点采样 SDA,先移进 rx_shift,
// 收满 8 位后按 rx_cnt 存进对应寄存器(读完保持不清零)
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n) begin
rx_shift <= 8'd0;
status_reg <= 8'd0;
tvoc_reg <= 24'd0;
crc_reg <= 8'd0;
end
else if(cur_state == READ && div_cnt == SCL_DIV/2 && scl_out == 1'b1)
rx_shift <= {rx_shift[6:0], sda}; // 一位一位移入(MSB 先进)
else if(cur_state == READ && bit_cnt == 4'd8 && scl_out == 1'b0) begin
case(rx_cnt) // 一个字节收完,按序号入库
3'd0: status_reg <= rx_shift; // byte0 状态
3'd1: tvoc_reg[23:16] <= rx_shift; // byte1 TVOC 高
3'd2: tvoc_reg[15:8] <= rx_shift; // byte2 TVOC 中
3'd3: tvoc_reg[7:0] <= rx_shift; // byte3 TVOC 低
3'd4: crc_reg <= rx_shift; // byte4 CRC8
default: ;
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
`timescale 1ns/1ps
module uart_tx (
input clk, // 系统时钟 50MHz
input rst_n, // 复位,低电平有效
input uart_tx_req, // 发送请求:1 个时钟周期的脉冲
input [7:0] tx_in_data, // 要发送的字节
output reg uart_txd, // 串口发送线(接上位机 RX)
output reg uart_tx_done, // 发完一个字节:1 个时钟周期的脉冲
output reg uart_tx_busy // 正在发送中:1 表示忙
);
parameter CLK_FREQ = 26'd50_000_000; // 时钟频率 50MHz
parameter UART_BPS = 17'd115200; // 波特率 115200
localparam BAUD_CNT_MAX = CLK_FREQ / UART_BPS; // 每个位持续 434 个时钟周期
reg [12:0] baud_cnt; // 位时间计数器
reg [3:0] bit_cnt; // 位位置:0=起始位,1..8=数据位,9=停止位
reg [7:0] data_reg; // 锁存下来的待发送字节
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
baud_cnt <= 13'd0;
bit_cnt <= 4'd0;
data_reg <= 8'd0;
uart_txd <= 1'b1; // 空闲时发送线保持高
uart_tx_busy <= 1'b0;
uart_tx_done <= 1'b0;
end
else begin
uart_tx_done <= 1'b0; // 默认没有完成脉冲
if (uart_tx_req && !uart_tx_busy) begin
// 收到发送请求:锁存数据,立刻发出起始位
data_reg <= tx_in_data;
uart_tx_busy <= 1'b1;
baud_cnt <= 13'd0;
bit_cnt <= 4'd0;
uart_txd <= 1'b0; // 起始位
end
else if (uart_tx_busy) begin
if (baud_cnt == BAUD_CNT_MAX - 1) begin
baud_cnt <= 13'd0;
case (bit_cnt)
4'd0: uart_txd <= data_reg[0]; // 数据位 bit0
4'd1: uart_txd <= data_reg[1];
4'd2: uart_txd <= data_reg[2];
4'd3: uart_txd <= data_reg[3];
4'd4: uart_txd <= data_reg[4];
4'd5: uart_txd <= data_reg[5];
4'd6: uart_txd <= data_reg[6];
4'd7: uart_txd <= data_reg[7]; // 数据位 bit7
4'd8: uart_txd <= 1'b1; // 停止位(走满一个位时间)
default: uart_txd <= 1'b1; // bit_cnt=9:保持停止位不动
endcase
if (bit_cnt == 4'd9) begin
uart_tx_busy <= 1'b0;
uart_tx_done <= 1'b1; // 停止位也走完了,整个字节发完
end
else
bit_cnt <= bit_cnt + 1'b1;
end
else
baud_cnt <= baud_cnt + 1'b1;
end
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
6.1 代码解释
top.v(顶层)
- 触发合并:
measure_start = timer_pls | key_flag,定时到了测一次,按键按下也测一次,谁来都行。 - 两条打印路线:
read_done一来就进S_SEND,先看ack_ok——为 0 发 13 字节「AGS10无应答\r\n」;为 1 发 22 字节数值行 + 26 字节RAW:调试行。tx_total是个 wire,跟着ack_ok自动切 48 / 13。 - 拆 5 位十进制:
tvoc_disp先封顶 99999,再用除法和取余拆出万、千、百、十、个位,dec2ascii把 0~9 变成 ASCII 码'0'~'9'。 - 中文串写法:
STR_HEAD是「TVOC浓度为:」的 GB2312 编码,写法和 MQ-2 教程里STR_ALARM一样,用十六进制参数拼出来。浓 = 0xC5A8、度 = 0xB6C8、为 = 0xCEAA、:= 0xA3BA。写这种字符串常量一定要逐字核对编码——第一版就把「为」写成了0xCEA9(惟),串口打出来是「TVOC浓度惟:」,查了半天。 - 调试行:
hex2ascii把一个十六进制半字节(0~F)变成 ASCII 字符,5 个原始字节 + CRC 结果全部打出来,传感器回没回数据、回了什么,一眼看穿。
ags10_driver.v(I2C 驱动)
- 三态控制:
sda_oe为 1 时主机驱动 SDA,为 0 时放开成高阻1'bz,靠上拉保持高。读数据、等从机 ACK 的时候必须放开,不然和从机抢线。 - 状态机:
IDLE → START → WRITE(0x34) → WAIT_ACK → WRITE(0x00) → WAIT_ACK → RESTART → WRITE(0x35) → WAIT_ACK → READ×5(中间夹 MACK)→ MNACK → STOP → IDLE,和第 4 节讲的时序一一对应。哪一步采到 NACK,直接跳 STOP 中止。 SCL_DIV = 2500分频:半个 SCL 周期 = 2500 个系统时钟(50 us),SCL 频率 = 50 MHz / 5000 = 10 kHz。这是 AGS10 的硬要求(≤15 kHz),不是可以随便调快的参数。- 写阶段选字节:
wr_cnt记「已经发出去几个写字节」(1 = 写地址0x34,2 = 再加寄存器0x00,3 = 再加读地址0x35),tx_byte用组合逻辑按它选出要发的字节,MSB 先发(tx_byte[7-bit_cnt])。WAIT_ACK 里wr_cnt == 2就是「两个地址都发完、都收到 ACK」,这才进重复起始——和时序图上的顺序一一对应。 - ACK 检测:每个第 9 个时钟的高半周中点采样 SDA,还是高就是 NACK,
ack_ok清 0 并粘住(事务开始时才重新置 1)。 - 读数据:SCL 高半周中点采样 SDA,一位一位移进
rx_shift;收满 8 位,按rx_cnt存进status_reg/tvoc_reg/crc_reg。读完不清零,一直保持到下次测量覆盖,避免串口还没发完数值就变了。 - CRC8:
crc8_ags10函数对 byte0~3 算校验(多项式0x31、初值0xFF),和 byte4 比对,结果从crc_ok输出。校验不过不影响显示,只在调试行里标CRC=0。 - 读完成脉冲:
read_done = (cur_state == STOP && div_cnt == 3*SCL_DIV),停止条件发完那一拍,给顶层去触发串口。
7、I/O 绑定
对应的约束文件内容如下:
//==================== 逻辑派 FPGA-G1 AGS10 TVOC 传感器 引脚约束 ====================
// 器件: 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;
//------------------------------- AGS10 的 I2C -------------------------------
// 接 J5 排针空闲脚 = FPGA 引脚 R14
IO_LOC "scl" R14;
IO_PORT "scl" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8 BANK_VCCIO=3.3;
// 接 J5 排针空闲脚 = FPGA 引脚 T15
// SDA 是双向信号,靠 sda_oe 切换输入/输出
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
28
29
此处需要杜邦线把 AGS10 模块和 J5 排针相连,接线方式:
| AGS10 模块 | 开发板 |
|---|---|
| VCC | 3.3 V |
| GND | GND(共地) |
| SCL | R14(J5 排针) |
| SDA | T15(J5 排针) |
| INT | 悬空不用 |
关于供电
请使用 3.3 V 供电的 AGS10 模块。模块 I2C 上拉到 VDD,接 3.3 V 时 SCL / SDA 高电平就是 3.3 V,和 FPGA 的 LVCMOS33 对得上。模块一般自带上拉电阻,不用外加。
8、程序下载
把程序综合、布局布线后烧录进开发板,打开串口助手,设置好参数:
- 波特率:115200
- 数据位:8
- 停止位:1
- 校验位:无
- 编码:GB2312 / GBK(有的软件叫 ANSI)
显示乱码的话
代码里的「浓度为:」用的是 GB2312 / GBK 编码(浓 = 0xC5A8、度 = 0xB6C8、为 = 0xCEAA、:= 0xA3BA)。串口助手如果按 UTF-8 解码就会显示乱码,把接收编码切换成 GB2312 / GBK 即可正常显示。顺带一提:如果显示出来是「TVOC浓度惟:」,那不是串口助手的问题,是代码里「为」的编码写成了 0xCEA9——差一个 bit 就是另一个字,写中文串常量务必逐字核对。
正常情况下每秒会打印两行:
TVOC浓度为:00125ppb
RAW:80 00 00 7D 4F CRC=12
想立刻补测一次,按一下测量按键(D11)就行。
🪜注意:
上电后前几分钟读数会偏高,这是 TVOC 传感器基线还没稳定,属正常现象,不是程序写错了。等基线稳定后再看数值。具体预热时长以规格书为准。
如果一直打印「AGS10无应答」,按这个顺序排查:
- 接线:SCL / SDA 是不是接反了?模块和开发板共地了吗?
- 供电:模块电源灯亮不亮?用的是 3.3 V 还是 5 V 版本?
- 上拉:模块板上有没有上拉电阻(一般都有)?自己画的板子忘了上拉,总线永远是低的。
- 速率:确认
SCL_DIV = 2500(10 kHz)没被改回去——AGS10 跑不了 100 kHz。 - 以上都对还不行,用逻辑分析仪抓 SCL / SDA,看 START 之后第 9 个时钟 SDA 到底有没有被拉低。
如果打印「一直 99999ppb」但 RAW 行有数据:看 RAW 行的三个 TVOC 字节是不是全 FF——全 FF 说明总线上没人驱动,本质还是没应答;有真实数据但数值离谱,对照规格书核对字节序和状态位。
🪜注意:
可以拿酒精棉片、香水、打火机气体(别点火)靠近传感器,看数值变化;拿开后数值会慢慢回落。做实验时注意通风,别对着传感器猛喷。