AHT10 是奥松(ASAIR)推出的一款数字温湿度传感器,I2C 接口,出厂已经校准好,不需要外部 AD 转换。供电范围 2.2 - 5.5 V,只要 SCL、SDA 两根线就能同时读回温度和湿度。
它的用法和 SHT20 这类 I2C 温湿度传感器很像:发命令 → 等一会儿 → 读结果。区别在于 AHT10 一条触发命令就同时把温度和湿度都测完,数据一次读回来,而且是湿度在前、温度在后。
1、模块来源
规格书(datasheet)请在商品详情页的资料下载区获取,认准奥松 ASAIR 的 AHT10 型号。本文标注「以规格书为准」的地方,都以你手上这份为准。
|AHT10 实物图
VCC:电源,2.2 - 5.5 V(模块一般按 3.3 V )GND:地线,必须和 FPGA 共地SCL:I2C 时钟线,需上拉到模块VDDSDA:I2C 数据线,双向,同样需上拉到模块VDD
2、参数
工作电压:2.2 - 5.5 V(模块一般 3.3 V / 5 V 供电)
通讯接口:I2C
从机地址:7 位地址 0x38(写地址 = 0x38 左移一位 = 0x70,读地址 = 0x71)
上电等待:≥ 40 ms 后才能开始通讯
单次测量等待:≥ 80 ms
测量输出:温度 + 湿度一次读回(数据怎么摆见第 5 节)
可选校验:CRC8,多项式 0x31、初值 0xFF(是否必须校验请以规格书为准)
管脚数量:4 Pin(VCC、GND、SCL、SDA)
量程 / 精度:本文不写死,请以规格书为准
⚠️警告:
AHT10 的供电范围是 2.2 - 5.5 V。 逻辑派要接3.3V引脚
3、实战任务
使用立创·逻辑派 FPGA-G1 开发板编写 AHT10 驱动代码,每隔 1 秒自动测量一次,按一下按键(key)也可以手动立刻补测一次,测完通过串口把温度和湿度一起发给上位机显示:
温度: 25.4℃ 湿度: 45.3%两行并成一行打印,一共 26 个字节(含回车换行)。温度为负数时,冒号后面是一个 -;正数时冒号后面是空格。
4、时序说明
4.1 读写时序
AHT10 走的是标准 I2C 协议。I2C 时序我们这里不再进行赘述,有兴趣可去模块里的IIC屏幕里学习
我们在这里简单的过一下 AHT10 的读写过程时序。
器件地址为 0x38,但是最后一位是读写位,需要空出一位给读写位,因此需要左移一位,即 0x38<<1 得到 0X70
采集步骤:(写=0,读=1)
起始信号-> 器件地址左移 1 位 + 写 -> 等待传感器应答 -> 发送触发测量命令(0XAC)-> 等待传感器应答 -> 发送数据位 0X33 -> 等待传感器应答 -> 发送数据位 0x00 -> 等待传感器应答 -> 停止信号(可不加)-> 起始信号 -> 器件地址左移 1 位 + 读 -> 等待传感器应答 -> 读取 8 位数据(状态字)-> 主机发送应答 -> 读取湿度高位数据 -> 主机发送应答 -> 读取湿度低位数据 -> 主机发送应答-> 读取湿度最后 4 位数据和温度最高的 4 位数据 -> -> 主机发送应答 -> 读取温度 8 数据 -> 主机发送应答-> 读取温度 8 位数据 -> 主机发送应答 -> 停止信号
4.2 数据换算
湿度和温度的原始值都是 20 位无符号数,换成物理量的公式:
湿度:
温度:
其中 H 是湿度原始值(20 位),S_T 是温度原始值(20 位)。直观感受一下:湿度原始值顶到最大 2^20 时正好 100 %RH;温度原始值等于 2^19(一半)时,代进公式就是 50 ℃。
代码里要显示一位小数,就先整体乘 10,再右移 20 位(右移 20 位就等于除以 2^20,不用真的做除法):
湿度:%RH×10 = (H × 1000) >> 20 范围 0~1000,封顶 99.9
温度: °C×10 = (S_T × 2000) >> 20 − 500 负数时单独出一个负号2
4.3 CRC8(可选)
如果读了第 7 个字节,它是 CRC8 校验值:多项式 0x31,初值 0xFF。自己算一遍和收到的对得上,就说明这批数据没传错。本教程的驱动没有做这一步(能跑能懂即可),想做得更完整可以加上,算错了就丢掉这次数据重测。CRC 是否必须、覆盖哪几个字节,请以规格书为准。
5、程序编写
整个工程分三个模块:
- top.v(顶层):定时 / 按键产生测量请求;把湿度、温度原始值换算并拆成十进制位;拼 26 个字节的中文串喂给串口。
- aht10_driver.v(AHT10 驱动):两层状态机。上层「流程表」管整轮测量先干什么后干什么(初始化 → 触发 → 等 80 ms → 读 6 字节),底层管脚级状态机负责 I2C 的起始、收发、应答、停止。
- uart.v(串口发送):和前面串口章节通用,原样沿用。
`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;
//------------------- aht10_driver 输出(先声明再用,免得被当成隐式线网)-------------------
wire [7:0] status_reg; // 状态字(bit[3]=1 已校准),本工程只存不打印
wire [19:0] hum_raw; // 湿度原始值 20 位
wire [19:0] tem_raw; // 温度原始值 20 位
wire read_done; // 读完成脉冲
//------------------- uart_tx 输出(同样先声明再用)-------------------
wire uart_tx_done;
wire uart_tx_busy;
//------------------- 湿度换算 -------------------
// 公式:%RH = H / 2^20 × 100
// 显示一位小数:%RH×10 = H × 1000 / 2^20 = (H × 1000) >> 20
wire [31:0] hum_mul = hum_raw * 20'd1000; // H*1000,最大约 10.5 亿,32 位装得下
wire [11:0] hum_x10 = hum_mul[31:20]; // = %RH×10,0~1000
wire [11:0] hum_disp = (hum_x10 > 12'd999) ? 12'd999 : hum_x10; // 封顶 99.9%
// 拆分每一位数字:h_shi=十位,h_ge=个位,h_xiao=小数点后一位
wire [3:0] h_shi = (hum_disp / 12'd100) % 12'd10;
wire [3:0] h_ge = (hum_disp / 12'd10) % 12'd10;
wire [3:0] h_xiao = hum_disp % 12'd10;
//------------------- 温度换算 -------------------
// 公式:°C = T / 2^20 × 200 − 50
// 显示一位小数:°C×10 = T × 2000 / 2^20 − 500 = (T × 2000) >> 20 − 500
wire [31:0] tem_mul = tem_raw * 20'd2000; // T*2000,最大约 21 亿,32 位装得下
wire [11:0] tem_shift = tem_mul[31:20]; // 0~2000
wire tem_neg = (tem_shift < 12'd500); // 是否负数(负温度)
wire [11:0] tem_abs10 = tem_neg ? (12'd500 - tem_shift) // |°C|×10
: (tem_shift - 12'd500); // 范围 0~1500
// 拆分每一位数字(这里假设温度在 100℃ 以内,AHT10 量程内不会超):
// t_shi=十位,t_ge=个位,t_xiao=小数点后一位
wire [3:0] t_shi = (tem_abs10 / 12'd100) % 12'd10;
wire [3:0] t_ge = (tem_abs10 / 12'd10) % 12'd10;
wire [3:0] t_xiao = tem_abs10 % 12'd10;
//------------------- 串口发送控制 -------------------
// "温度: 25.4℃ 湿度: 45.3%" + 回车换行(GB2312/GBK,共 26 字节)
// 负温度时冒号后是 '-',正温度冒号后是空格(跟 SHT20 一样)
// 中文串用十六进制参数写法,串口助手编码选 GB2312/GBK
// 温=CE C2 度=B6 C8 ℃=A1 E6 湿=CA AA 度=B6 C8
parameter STR = 80'hCE_C2_B6_C8_A1_E6_CA_AA_B6_C8; // "温度℃湿度"的GB2312编码
wire [7:0] sign_byte = tem_neg ? 8'h2D : 8'h20; // 负号 '-' 或 空格 ' '
// 十进制数字 0~9 转 ASCII 码
function [7:0] dec2ascii;
input [3:0] d;
begin
dec2ascii = 8'h30 + d; // 0~9 -> '0'~'9'
end
endfunction
localparam TX_TOTAL = 6'd26; // 一共发 26 个字节
reg [5:0] tx_byte_cnt; // 发到第几个字节
reg [1:0] tx_state; // 发送状态
reg uart_tx_req; // 给 uart_tx 的发送请求
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
//---- "温度: 25.4℃ 湿度: 45.3%" 共 26 字节 ----
case(tx_byte_cnt)
6'd0: tx_data <= STR[79:72]; // "温" 高字节
6'd1: tx_data <= STR[71:64]; // "温" 低字节
6'd2: tx_data <= STR[63:56]; // "度" 高字节
6'd3: tx_data <= STR[55:48]; // "度" 低字节
6'd4: tx_data <= 8'h3A; // ':'
6'd5: tx_data <= sign_byte; // 符号位(正数为空格)
6'd6: tx_data <= dec2ascii(t_shi); // 温度十位
6'd7: tx_data <= dec2ascii(t_ge); // 温度个位
6'd8: tx_data <= 8'h2E; // '.'
6'd9: tx_data <= dec2ascii(t_xiao); // 温度小数位
6'd10: tx_data <= STR[47:40]; // "℃" 高字节
6'd11: tx_data <= STR[39:32]; // "℃" 低字节
6'd12: tx_data <= 8'h20; // 空格
6'd13: tx_data <= STR[31:24]; // "湿" 高字节
6'd14: tx_data <= STR[23:16]; // "湿" 低字节
6'd15: tx_data <= STR[15:8]; // "度" 高字节
6'd16: tx_data <= STR[7:0]; // "度" 低字节
6'd17: tx_data <= 8'h3A; // ':'
6'd18: tx_data <= 8'h20; // 空格
6'd19: tx_data <= dec2ascii(h_shi); // 湿度十位
6'd20: tx_data <= dec2ascii(h_ge); // 湿度个位
6'd21: tx_data <= 8'h2E; // '.'
6'd22: tx_data <= dec2ascii(h_xiao); // 湿度小数位
6'd23: tx_data <= 8'h25; // '%'
6'd24: tx_data <= 8'h0D; // 回车 CR
6'd25: tx_data <= 8'h0A; // 换行 LF
default: tx_data <= 8'd0;
endcase
uart_tx_req <= 1'b1;
tx_byte_cnt <= tx_byte_cnt + 1'b1;
end
end
default: tx_state <= S_IDLE;
endcase
end
end
//------------------- aht10_driver 例化 -------------------
aht10_driver u_aht10 (
.sys_clk (sys_clk),
.rst_n (rst_n),
.scl (scl),
.sda (sda),
.measure_start (measure_start),
.status_reg (status_reg),
.hum_raw (hum_raw),
.tem_raw (tem_raw),
.read_done (read_done)
);
//------------------- uart_tx 例化 -------------------
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
`timescale 1ns/1ps
module aht10_driver(
input sys_clk, // 系统时钟 50MHz
input rst_n, // 复位,低电平有效
output scl, // I2C 时钟线(接从机 SCL)
inout sda, // I2C 数据线(双向,接从机 SDA)
input measure_start, // 测量请求:一个脉冲触发一次测量
output [7:0] status_reg, // 状态字(bit[3]=1 表示已校准)
output [19:0] hum_raw, // 湿度原始值 20 位
output [19:0] tem_raw, // 温度原始值 20 位
output read_done // 读完成脉冲(给顶层去发串口)
);
//------------------------------------------------------------------
// 分频参数
//------------------------------------------------------------------
localparam SCL_DIV = 100; // SCL 半个周期 = 100 个 sys_clk 周期
// 所以 SCL 频率 = 50MHz / 200 = 250kHz
// (参考工程 SHT20/AGS10 都用这个值;
// 想放慢到标准模式 100kHz,把 100 改成 250 即可)
//------------------------------------------------------------------
// AHT10 地址 / 命令
//------------------------------------------------------------------
localparam add_write = 8'h70; // 写地址 = 0x38 << 1
localparam add_read = 8'h71; // 读地址 = (0x38 << 1) | 1
// 备选(若模块无应答再换,以规格书为准):
// localparam add_write = 8'h78;
// localparam add_read = 8'h79;
localparam cmd_init = 8'hBE; // 初始化命令首字节(后面跟 0x08 0x00)
localparam cmd_trig = 8'hAC; // 触发测量命令首字节(后面跟 0x33 0x00)
//------------------------------------------------------------------
// 底层 I2C 时序状态(跟参考工程 i2c_master / ags10_driver 一致)
//------------------------------------------------------------------
localparam IDLE = 4'd0; // 空闲:等上层发起始条件
localparam START = 4'd1; // 起始:发 START 条件
localparam WRITE = 4'd2; // 写数据:发一个字节的 8 位
localparam ACK = 4'd3; // 应答:发第 9 个时钟,等从机 ACK
localparam STOP = 4'd4; // 停止:发 STOP 条件
localparam READ = 4'd5; // 读数据:收一个字节的 8 位
localparam NACK = 4'd7; // 最后一个字节读完,主机回 NACK
localparam RACK = 4'd8; // 中间字节读完,主机回 ACK
//------------------------------------------------------------------
// 上层协议步骤(决定这一步是做什么事务 / 延时)
// 类比:底层状态机是"手",管脚上一拍一拍动;
// 这层是"流程表",决定先干什么后干什么。
//------------------------------------------------------------------
localparam S_POW_WAIT = 4'd0; // 上电延时 40ms
localparam S_RD_STAT = 4'd1; // 事务:读状态字(START+0x71+读 1 字节+STOP)
localparam S_CHK_STAT = 4'd2; // 看 bit[3]:已校准?没校准则发初始化
localparam S_WR_INIT = 4'd3; // 事务:写初始化 0xBE 0x08 0x00
localparam S_INIT_WAIT = 4'd4; // 初始化后延时 10ms
localparam S_IDLE = 4'd5; // 空闲,等 measure_start
localparam S_WR_TRIG = 4'd6; // 事务:写触发测量 0xAC 0x33 0x00
localparam S_MEAS_WAIT = 4'd7; // 测量延时 80ms
localparam S_RD_DATA = 4'd8; // 事务:读 6 字节结果
localparam S_DONE = 4'd9; // 发 read_done 脉冲,回空闲
//------------------------------------------------------------------
// 内部寄存器
//------------------------------------------------------------------
reg [3:0] cur_state; // 底层 I2C 当前状态
reg [3:0] nxt_state; // 底层 I2C 下一状态
reg [3:0] seq_state; // 上层协议当前步骤
reg [3:0] seq_nxt; // 上层协议下一步骤
reg [21:0] div_cnt; // 节拍计数器:0~100(起始态 0~200)
reg [3:0] bit_cnt; // 位计数器:一个字节发/收到第几位(0~7)
reg [2:0] write_byte; // 字节计数器:发到第几个字节
reg [2:0] read_byte; // 读字节计数器:收到第几个字节
reg sda_oe; // 为 1/0 时控制/放开 sda
reg sda_out; // 要发到 SDA 上的数据
reg scl_out; // 内部生成的 SCL 时钟
reg [22:0] wait_cnt; // 延时计数器(上电 40ms / 初始化 10ms / 测量 80ms 共用)
reg [3:0] seq_state_d; // seq_state 打一拍,用来产生 bus_start 脉冲
reg [7:0] rx0; // byte0 状态字
reg [7:0] rx1; // byte1 湿度[19:12]
reg [7:0] rx2; // byte2 湿度[11:4]
reg [7:0] rx3; // byte3 湿度[3:0] | 温度[19:16]
reg [7:0] rx4; // byte4 温度[15:8]
reg [7:0] rx5; // byte5 温度[7:0]
//------------------------------------------------------------------
// 连续赋值
//------------------------------------------------------------------
assign sda = sda_oe ? sda_out : 1'bz;
// SCL 直接输出内部生成的时钟
assign scl = scl_out;
// 收到的 6 个字节按图纸拼出湿度 20 位 / 温度 20 位
// 注意:读完后必须保持住,不能清 0,
// 否则回到空闲态后顶层还没把串口发完,数值就变成 0 了
assign status_reg = rx0;
assign hum_raw = {rx1, rx2, rx3[7:4]}; // 湿度 20 位
assign tem_raw = {rx3[3:0], rx4, rx5}; // 温度 20 位
// 读完成脉冲:走完 S_DONE 那一拍(只在"读结果"流程末尾发一次)
assign read_done = (seq_state == S_DONE);
// 本步是不是要上 I2C 总线干活
wire seq_is_bus = (seq_state == S_RD_STAT) || (seq_state == S_WR_INIT) ||
(seq_state == S_WR_TRIG) || (seq_state == S_RD_DATA);
// 总线事务启动脉冲:刚进入事务步骤的那一拍
wire bus_start = seq_is_bus && (seq_state != seq_state_d);
// 总线事务完成脉冲:STOP 条件发完、回到 IDLE 的那一刻
wire bus_done = (cur_state == STOP && nxt_state == IDLE);
// 这次事务要写几个字节、读几个字节(按上层步骤选)
wire [2:0] wr_num = (seq_state == S_WR_INIT || seq_state == S_WR_TRIG) ? 3'd4 : 3'd1;
wire [2:0] rd_num = (seq_state == S_RD_STAT) ? 3'd1 :
(seq_state == S_RD_DATA) ? 3'd6 : 3'd0;
// 本次写字节内容(按"哪个事务 + 发到第几个字节"选,MSB 先发)
reg [7:0] tx_byte;
always @(*) begin
case (seq_state)
S_RD_STAT: tx_byte = add_read; // 读地址
S_RD_DATA: tx_byte = add_read; // 读地址
S_WR_INIT: case (write_byte) // 写地址 + 初始化 3 字节
3'd0: tx_byte = add_write;
3'd1: tx_byte = cmd_init; // 0xBE
3'd2: tx_byte = 8'h08;
3'd3: tx_byte = 8'h00;
default: tx_byte = 8'h00;
endcase
S_WR_TRIG: case (write_byte) // 写地址 + 触发测量 3 字节
3'd0: tx_byte = add_write;
3'd1: tx_byte = cmd_trig; // 0xAC
3'd2: tx_byte = 8'h33;
3'd3: tx_byte = 8'h00;
default: tx_byte = 8'h00;
endcase
default: tx_byte = 8'hFF;
endcase
end
// SDA 方向:读数据、写后应答时释放 SDA,让从机来驱动
always @(*) begin
if(cur_state == READ || cur_state == ACK) // 读数据、写后应答:释放 SDA
sda_oe = 0;
else
sda_oe = 1; // 其它情况:主机自己驱动 SDA
end
// 延时到点标志:上电 40ms / 初始化后 10ms / 测量等待 80ms
// 50MHz 下:40ms = 2_000_000 拍,10ms = 500_000 拍,80ms = 4_000_000 拍
reg wait_done;
always @(*) begin
case (seq_state)
S_POW_WAIT: wait_done = (wait_cnt == 23'd2_000_000); // 40ms
S_INIT_WAIT: wait_done = (wait_cnt == 23'd500_000); // 10ms(通用值,待核对)
S_MEAS_WAIT: wait_done = (wait_cnt == 23'd4_000_000); // 80ms
default: wait_done = 1'b0;
endcase
end
//==================================================================
// 上层协议流程表
// 初始状态 → 谁拉低 → 谁发 → 谁收 → 怎么结束,一步步走:
//
// S_POW_WAIT 上电,等 40ms
// S_RD_STAT 发 START + 0x71,收 1 字节状态字,发 STOP
// S_CHK_STAT 看状态字 bit[3]:=1 已校准 → 直接去 S_IDLE
// =0 没校准 → 去发初始化
// S_WR_INIT 发 START + 0x70 + 0xBE 0x08 0x00 + STOP
// S_INIT_WAIT 等 10ms
// S_IDLE 空闲,等 measure_start(1 秒定时 / 按键)
// S_WR_TRIG 发 START + 0x70 + 0xAC 0x33 0x00 + STOP
// S_MEAS_WAIT 等 80ms(AHT10 内部转换要这么久)
// S_RD_DATA 发 START + 0x71,收 6 字节结果,发 STOP
// S_DONE 给顶层发一个 read_done,回去等下一次
//==================================================================
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
seq_state <= S_POW_WAIT;
else
seq_state <= seq_nxt;
end
// seq_state 打一拍:跟当前值比,刚变化就是"进入新步骤"
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
seq_state_d <= S_POW_WAIT;
else
seq_state_d <= seq_state;
end
always @(*) begin
case (seq_state)
S_POW_WAIT: seq_nxt = wait_done ? S_RD_STAT : S_POW_WAIT;
S_RD_STAT: seq_nxt = bus_done ? S_CHK_STAT : S_RD_STAT;
S_CHK_STAT: seq_nxt = status_reg[3] ? S_IDLE : S_WR_INIT; // bit3=1 已校准
S_WR_INIT: seq_nxt = bus_done ? S_INIT_WAIT : S_WR_INIT;
S_INIT_WAIT: seq_nxt = wait_done ? S_IDLE : S_INIT_WAIT;
S_IDLE: seq_nxt = measure_start ? S_WR_TRIG : S_IDLE; // 有测量请求才干活
S_WR_TRIG: seq_nxt = bus_done ? S_MEAS_WAIT : S_WR_TRIG;
S_MEAS_WAIT: seq_nxt = wait_done ? S_RD_DATA : S_MEAS_WAIT;
S_RD_DATA: seq_nxt = bus_done ? S_DONE : S_RD_DATA;
S_DONE: seq_nxt = S_IDLE;
default: seq_nxt = S_IDLE;
endcase
end
//------------------------------------------------------------------
// 延时计数器:只在三个等待步骤里 +1,其它时候清零
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
wait_cnt <= 'd0;
else if(seq_state == S_POW_WAIT || seq_state == S_INIT_WAIT || seq_state == S_MEAS_WAIT)
wait_cnt <= wait_cnt + 1'b1;
else
wait_cnt <= 'd0;
end
//------------------------------------------------------------------
// 底层 I2C 状态寄存器:每个时钟沿把 nxt_state 搬进 cur_state
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
cur_state <= IDLE;
else
cur_state <= nxt_state;
end
//------------------------------------------------------------------
// 底层状态机(组合逻辑):决定下一状态
//
// 整条事务(初始状态 → 谁拉低 → 谁发 → 谁收 → 怎么结束):
// IDLE 空闲,等上层 bus_start
// START 主机发起始条件(SCL 高时 SDA 拉低)
// WRITE 主机发 1 个字节的 8 位(MSB 先发)
// ACK 第 9 个时钟,释放 SDA 等从机 ACK
// 发完还有字节→接着 WRITE;发完要收→READ;发完就结束→STOP
// READ 主机收 1 个字节的 8 位(从机驱动 SDA)
// RACK 中间字节收完,主机回 ACK(拉低),接着收下一个
// NACK 最后一个字节收完,主机回 NACK(拉高),准备结束
// STOP 主机发停止条件(SCL 高时 SDA 拉高),整条事务结束
//------------------------------------------------------------------
always @(*) begin
case (cur_state)
IDLE:begin
if (bus_start == 1) // 上层说"开干"
nxt_state = START;
else
nxt_state = IDLE; // 没请求就待在空闲
end
START:begin
if (div_cnt == 2*SCL_DIV) // 起始条件走完(SCL 已拉低)
nxt_state = WRITE;
else
nxt_state = START;
end
WRITE:begin
if (bit_cnt == 8 && scl_out == 0) // 一个字节 8 位发完
nxt_state = ACK;
else
nxt_state = WRITE;
end
ACK:begin
if (div_cnt == SCL_DIV && scl_out == 1) begin // 从机应答位走完
if (write_byte + 1 < wr_num) // 还有字节要写
nxt_state = WRITE;
else if (rd_num != 0) // 写完了,接着读
nxt_state = READ;
else // 写完了,也没的读 → 停止
nxt_state = STOP;
end
else
nxt_state = ACK;
end
READ:begin
if (bit_cnt == 8 && scl_out == 0 && (read_byte + 1 < rd_num))
nxt_state = RACK; // 前面的字节读完,主机回 ACK
else if (bit_cnt == 8 && scl_out == 0)
nxt_state = NACK; // 最后 1 个字节读完,主机回 NACK
else
nxt_state = READ;
end
RACK:begin
if (div_cnt == SCL_DIV && scl_out == 1) // ACK 发完,回去读下一个
nxt_state = READ;
else
nxt_state = RACK;
end
NACK:begin
if (div_cnt == SCL_DIV && scl_out == 1) // NACK 发完,去停止
nxt_state = STOP;
else
nxt_state = NACK;
end
STOP:begin
if (div_cnt == SCL_DIV) // 停止条件发完
nxt_state = IDLE;
else
nxt_state = STOP;
end
default: nxt_state = IDLE;
endcase
end
//------------------------------------------------------------------
// 写字节计数器 write_byte:每发完一个字节(离开 ACK)就 +1
// S_WR_INIT / S_WR_TRIG 时:0=写地址 1=命令 2=参数1 3=参数2
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
write_byte <= 'd0;
else if(cur_state == IDLE)
write_byte <= 'd0;
else if(cur_state == ACK && cur_state != nxt_state) // 离开 ACK 的那一刻
write_byte <= write_byte + 1'b1;
// 其它情况保持
end
//------------------------------------------------------------------
// 读字节计数器 read_byte:每发完一个主机应答(离开 RACK)就 +1
// 读结果时:0=状态 1=湿度[19:12] 2=湿度[11:4] 3=湿度[3:0]|温度[19:16]
// 4=温度[15:8] 5=温度[7:0]
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
read_byte <= 'd0;
else if(cur_state == IDLE)
read_byte <= 'd0;
else if(cur_state == RACK && cur_state != nxt_state) // 离开 RACK 的那一刻
read_byte <= read_byte + 1'b1;
// 其它情况保持
end
//------------------------------------------------------------------
// 节拍计数器 div_cnt:所有时序的"节拍器"
// 写数据/应答/停止态 0~100 循环;起始态 0~200
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
div_cnt <= 'd0;
else if(div_cnt == SCL_DIV && cur_state != START) // 非起始态:数到 100 清零
div_cnt <= 'd0;
else if(cur_state != nxt_state) // 状态要切换:清零
div_cnt <= 'd0;
else if(cur_state == START) // 起始态:一直数(到 200 靠换状态清零)
div_cnt <= div_cnt + 1'b1;
else if(cur_state == WRITE || cur_state == ACK || cur_state == STOP ||
cur_state == READ || cur_state == RACK || cur_state == NACK)
div_cnt <= div_cnt + 1'b1; // 其它工作态:+1
end
//------------------------------------------------------------------
// 位计数器 bit_cnt:数一个字节里发/收到第几位(0~7)
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
bit_cnt <= 'd0;
else if(cur_state == IDLE || cur_state == ACK || cur_state == RACK || cur_state == NACK)
bit_cnt <= 'd0; // 空闲/应答态:归 0
else if((cur_state == WRITE || cur_state == READ) && div_cnt == SCL_DIV/2 && scl_out == 1'b1)
bit_cnt <= bit_cnt + 1'b1; // 每过一个 SCL 高电平:位 +1
// 其它情况保持
end
//------------------------------------------------------------------
// SCL 时钟生成
// 写数据/应答态:div_cnt 每数到 100 取反一次,形成方波(半周期=100个sys_clk)
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
scl_out <= 1'b1; // 复位后 SCL 高(I2C 空闲是高)
else if(cur_state == START && div_cnt == SCL_DIV*2) // 起始末尾:SCL 拉低
scl_out <= 1'b0;
else if(cur_state == STOP && div_cnt == SCL_DIV/2) // 停止:先拉高 SCL
scl_out <= 1'b1;
else if((cur_state == WRITE || cur_state == ACK || cur_state == READ ||
cur_state == RACK || cur_state == NACK) && div_cnt == SCL_DIV)
scl_out <= ~scl_out; // 数据/应答:数到 100 取反
end
//------------------------------------------------------------------
// SDA 发送控制 sda_out:决定每个时刻 SDA 上发什么
//------------------------------------------------------------------
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 == 50) begin
if (scl_out == 0)
sda_out <= tx_byte[7-bit_cnt]; // 发数据(MSB 先发)
end
else if(cur_state == RACK)
sda_out <= 1'b0; // 读数据后主机发 ACK(拉低)
else if(cur_state == NACK)
sda_out <= 1'b1; // 最后一个字节主机发 NACK(拉高)
else if(cur_state == STOP) begin
if(div_cnt == 0)
sda_out <= 1'b0; // 停止准备:SDA 先拉低
else if(div_cnt == SCL_DIV)
sda_out <= 1'b1; // SCL 高时 SDA 拉高 = 停止条件
end
end
//------------------------------------------------------------------
// 读数据逻辑:在 SCL 高电平中点采样 SDA 引脚(读时是 AHT10 在驱动 SDA)
// 先把 6 个字节收进 rx0~rx5 这 6 个"信箱",再由上面的拼线拼出湿度/温度
//
// 本工程不检查从机 ACK 电平,能跑能懂即可;
// 要加可靠性可以再采样 ACK 判断从机在不在。以规格书为准。
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n) begin
rx0 <= 8'd0;
rx1 <= 8'd0;
rx2 <= 8'd0;
rx3 <= 8'd0;
rx4 <= 8'd0;
rx5 <= 8'd0;
end
else if (cur_state == READ && scl_out == 1 && div_cnt == 50) begin
case (read_byte)
3'd0: rx0[7-bit_cnt] <= sda; // byte0 状态字
3'd1: rx1[7-bit_cnt] <= sda; // byte1 湿度[19:12]
3'd2: rx2[7-bit_cnt] <= sda; // byte2 湿度[11:4]
3'd3: rx3[7-bit_cnt] <= sda; // byte3 湿度[3:0]|温度[19:16]
3'd4: rx4[7-bit_cnt] <= sda; // byte4 温度[15:8]
3'd5: rx5[7-bit_cnt] <= sda; // byte5 温度[7:0]
default: ; // 第 7 字节 CRC 本工程不读
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
`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、I/O 绑定
顶层模块 top 的端口与约束文件 IIC_aht10.cst 一一对应:
| 端口 | 方向 | FPGA 引脚 | 接线 |
|---|---|---|---|
sys_clk | input | T7 | 板载 50 MHz 系统时钟 |
rst_n | input | F10 | 复位按键(按下为低,异步复位) |
key | input | D11 | 测量按键(按下为低,20 ms 消抖,补测一次) |
scl | output | R14 | 接 J5 排针空闲脚 → AHT10 模块 SCL,需上拉到模块 VDD |
sda | inout | T15 | 接 J5 排针空闲脚 → AHT10 模块 SDA,需上拉到模块 VDD;双向,靠 sda_oe 切换 |
uart_tx | output | F12 | 接 USB 转串口 RX,115200-8-N-1,串口助手编码选 GB2312 / GBK |
对应的约束文件内容如下:
// 引脚约束(立创·逻辑派 FPGA-G1 开发板)
// 器件: 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;
// AHT10 的 I2C(J5 排针空闲脚;SCL/SDA 都要上拉到模块 VDD)
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 发送引脚(接 USB 转串口 RX)
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
AHT10 模块 4 Pin 这样接:
| AHT10 模块 | 开发板 |
|---|---|
| VCC | 3.3 V |
| GND | GND(和 FPGA 共地) |
| SCL | R14(J5 排针空闲脚) |
| SDA | T15(J5 排针空闲脚) |
7、程序下载
- 用 Gowin_V1.9.10.02 打开工程
IIC_aht10.gprj,综合、布局布线后下载到开发板(器件 GW2A-LV18PG256C8/I7)。 - 按接线表接好模块,给开发板上电。
- 打开串口助手,连上
F12对应的串口,就能看到每秒打印一行:
温度: 25.4℃ 湿度: 45.3%温度为负数时,冒号后面是 -;正数时是空格。按一下测量按键(key)也会立刻补测一行。对着传感器哈一口气(湿度会升)、用手指捏一下(温度会升),数值应该跟着变。
串口助手设置
波特率 115200、数据位 8、停止位 1、无校验,编码选择 GB2312 / GBK(否则中文会显示为乱码)。