SHT20 是 Sensirion(盛思锐)推出的数字温湿度传感器,是一个精度较高的温湿度传感器,内部集成了电容式湿度敏感元件和能隙式温度敏感元件,出厂已完成校准,通过标准 I2C 接口一次性输出温度和湿度的数字量,不需要外部 AD 转换,也不需要用户自己做校准曲线。
相比 DHT11 的单总线时序,SHT20 用的是标准 I2C,读写时序规范、抗干扰更好、精度也更高,是学习 I2C 主机驱动非常合适的一个练手对象。
1、模块来源
2、参数
工作电压:2.1 - 3.6V(推荐 3.0V)
通讯接口:I2C(标准模式,最高 400kHz)
从机地址:0x40(7 位地址)
温度量程:-40 ~ +125 ℃
温度精度:±0.3 ℃(典型值)
湿度量程:0 ~ 100 %RH
湿度精度:±3 %RH(典型值)
默认分辨率:温度 14bit / 湿度 12bit
管脚数量:4 Pin(VCC、GND、SCL、SDA)
3、实战任务
使用逻辑派 FPGA-G1 开发板编写 SHT20 驱动代码,每隔 1 秒读取一次数据,并通过串口发送到上位机显示。同时使用开发板上的按键切换显示内容:
- 默认显示温度,格式
温度: xx℃ - 按一次按键切换到湿度,格式
湿度:xx% - 再按一次切回温度
4、时序说明
SHT20 使用的是标准 I2C 协议。I2C 只有两根线:SCL(时钟线,由主机产生)和 SDA(数据线,双向)。主机是我们的 FPGA,从机是 SHT20。
3.1 起始条件与停止条件
起始条件(START):SCL 保持高电平期间,SDA 由高变低。
停止条件(STOP):SCL 保持高电平期间,SDA 由低变高。
这两个条件是 I2C 里唯一允许"SCL 高电平时 SDA 变化"的场合,所以从机只要看到这种跳变,就知道通讯要开始或者要结束了。
3.2 数据传输
这是 I2C 最核心的一条规则,记住它后面的代码就都好理解了:
- SCL 为低电平时:SDA 允许变化,这个时候主机(或从机)把要发的这一位"摆"到线上。
- SCL 为高电平时:SDA 必须保持稳定,因为接收方会在这段时间去采样 SDA。
一个字节是 8 位,高位先发(MSB First),也就是先发 bit7,最后发 bit0。所以驱动代码里取数据是 add_write[7-bit_cnt],bit_cnt 从 0 数到 7,正好对应从 bit7 取到 bit0。
3.3 应答信号
每发完 8 位数据,主机要再产生第 9 个时钟,这一位专门用来收应答:
- 主机写数据给从机时:主机在第 9 位释放 SDA(让它变成高阻态),从机如果收到了就把 SDA 拉低,这叫 ACK;如果 SDA 还是高的,说明从机没应答,叫 NACK。
- 主机读数据时反过来:从机发 8 位,主机在第 9 位把 SDA 拉低表示"我还要继续读"(ACK),把 SDA 拉高表示"我读完了,别发了"(NACK)。
FPGA 里 SDA 是 inout 类型,用三态门实现这种"有时候驱动、有时候放手"的效果:
assign sda = sda_oe ? sda_out : 1'bz; // sda_oe=1 主机驱动,sda_oe=0 放开给从机5、通讯步骤
SHT20 支持两种测量模式:保持主机模式(Hold Master) 和 非保持主机模式(No Hold Master)。区别在于测量期间从机会不会把 SCL 拉低把总线"占住"。本教程用的是非保持主机模式,它的好处是不需要处理从机拉低 SCL 的情况,逻辑更简单。
非保持主机模式分成两个阶段:先发命令,等测量完成,再回来取数据。
第一阶段:下发测量命令
- 主机发起始条件。
- 主机发从机写地址 0x80(7 位地址 0x40 左移一位,最低位 0 表示写)。
- 等从机 ACK。
- 主机发测量命令:测温度发 0xF3,测湿度发 0xF5。
- 等从机 ACK。
- 主机发停止条件,释放总线。
第二阶段:等待并读取结果
- 等待约 100ms,让 SHT20 把测量做完(14bit 温度最长约 85ms,12bit 湿度约 29ms,统一等 100ms 两种都够)。
- 主机重新发起始条件,发从机读地址 0x81(最低位 1 表示读)。
- 等从机 ACK 后开始读,一共读 3 个字节:数据高字节、数据低字节、CRC 校验字节。
- 读完第 1、2 个字节后,主机回 ACK(还要继续读)。
- 读完第 3 个字节(CRC)后,主机回 NACK(读完了)。
- 主机发停止条件,一次完整测量结束。
说明
本教程的驱动读到了 CRC 字节但没有做校验,只是把它接收下来丢掉。想做得更完整可以加上 CRC-8(多项式 x⁸+x⁵+x⁴+1)校验,读错了就丢弃这次数据重测。
4.1 数据换算
读回来的 2 个字节拼成一个 16 位原始值,datasheet 给的换算公式是:
温度:
湿度:
FPGA 里做浮点很费资源,所以要把公式改成定点整数运算。做法是把结果放大 100 倍(保留两位小数),除以 2¹⁶ 直接用右移 16 位代替:
温度:|T| × 100 = | (ST × 17572) >> 16 − 4685 |
湿度: RH × 100 = (SR × 12500) >> 16 − 6002
推导很直接,以温度为例:175.72 × 100 = 17572,46.85 × 100 = 4685,两边同时乘 100,除法变成移位,全程只有一次乘法和一次减法,综合出来就是一个乘法器加一个减法器,非常省资源。
因为温度可能是负的(低于 -46.85℃ 之前的中间值也可能算出负数),所以代码里用 neg_flag 单独记一下符号,串口发送时再补一个 '-' 或者空格:
wire neg_flag = (mul_shift < 15'd4685); // 是否负数
wire [14:0] temp_abs_100 = neg_flag ? (15'd4685 - mul_shift)
: (mul_shift - 15'd4685);2
3
一个精度上的小细节
datasheet 里提到低字节的最低 2 位是状态位,严格来说换算前应该先把这 2 位清零。本教程的代码没有做这一步,带来的误差大约 0.005℃,肉眼看不出来,但如果要做高精度应用,建议加上 read_data_reg & 16'hFFFC 再参与换算。
6、程序编写
整个工程分成三个模块:
- top.v(顶层):产生 1 秒定时的测量请求,做按键消抖和模式切换,把原始值换算成温度/湿度并拆成一位一位的数字,再按顺序喂给串口。
- i2c_master.v(I2C 驱动):用状态机实现完整的 I2C 主机时序,负责发命令、等测量、读 3 个字节。
- uart.v(串口发送):把一个字节按"起始位 + 8 位数据 + 停止位"一位一位发出去。
`timescale 1ns/1ps
module top(
input sys_clk, // 系统时钟 50MHz
input rst_n, // 复位,低电平有效
output scl, // I2C 时钟线
inout sda, // I2C 数据线
output uart_tx, // 串口发送线
input key // 按键:切换温度/湿度显示
);
reg [25:0] timer_cnt; // 1 秒计数器
reg measure_start; // 每秒发一个测量请求脉冲
reg [22:0] key_cnt;
reg key_flag;
localparam button_cnt = 21'd2000000; // 消抖时间:2000000 个时钟 = 40ms
// 按键消抖 + 模式切换
// key 按下(低电平)稳定 40ms 后,key_flag 翻转一次(0↔1,切换温/湿度)
always @(posedge sys_clk or negedge rst_n) begin
if (!rst_n) begin
key_cnt <= 0;
key_flag <= 0;
end
else if (key == 1) // 松开:计数清零,等下次按下
key_cnt <= 0;
else if (key_cnt == button_cnt) // 已计满:保持不动,防止连续翻转
key_cnt <= key_cnt;
else begin // 按下中:计数 +1
key_cnt <= key_cnt + 1'b1;
if (key_cnt == button_cnt - 1'b1) // 计满的前一拍:翻转一次
key_flag <= ~key_flag;
end
end
// 1 秒定时:每秒产生一个测量请求脉冲
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0) begin
timer_cnt <= 26'd0;
measure_start <= 1'b0;
end
else if(timer_cnt == 26'd50_000_000 - 1'b1) begin
timer_cnt <= 26'd0;
measure_start <= 1'b1; // 计满 1 秒,发出一个测量脉冲
end
else begin
timer_cnt <= timer_cnt + 1'b1;
measure_start <= 1'b0;
end
end
//------------------- 温度换算 -------------------
// 公式:T = -46.85 + 175.72 * ST / 65536
// 换算成 |T|*100 = | (ST*17572) >> 16 - 4685 |
wire [30:0] mul_result = temp_raw * 16'd17572; // 31 位,最大 65535*17572 不会溢出
wire [14:0] mul_shift = mul_result[30:16]; // 0 ~ 17572
wire neg_flag = (mul_shift < 15'd4685); // 是否负数(负温度)
wire [14:0] temp_abs_100 = neg_flag ? (15'd4685 - mul_shift)
: (mul_shift - 15'd4685);
wire [13:0] temp_abs_10 = temp_abs_100 / 14'd10; // |T|*10,范围 0 ~ 1288
// 拆分每一位数字(这里假设温度在 100℃ 以内):
// d_bai=十位,d_shi=个位,d_ge=小数点后一位
wire [3:0] d_bai = (temp_abs_10 / 14'd100) % 14'd10;
wire [3:0] d_shi = (temp_abs_10 / 14'd10) % 14'd10;
wire [3:0] d_ge = temp_abs_10 % 14'd10;
//------------------- 湿度换算 -------------------
// 公式:RH = -6 + 125 * SR / 65536
// 换算成 RH*100 = (SR*12500)>>16 - 600
// 说明:temp_raw 是读到的 16 位原始值,温度模式时它是 ST,湿度模式时它是 SR,
// 两个公式结构一样(都 = 乘以系数 再右移16位 再减偏移),所以共用 temp_raw。
// 温度/湿度两套数字都会算,但串口只发 key_flag 选中的那一套,另一套算错也不影响。
wire [29:0] mul_rh_result = temp_raw * 16'd12500; // 30 位:SR*12500,最大 65535*12500 不会溢出
wire [13:0] mul_rh_shift = mul_rh_result[29:16]; // = (SR*12500)>>16,范围 0~12500
wire [13:0] rh_abs_100 = (mul_rh_shift < 14'd600) ? 14'd0 : (mul_rh_shift - 14'd600); // RH*100,负值按 0
wire [11:0] rh_abs_10 = rh_abs_100 / 14'd10; // RH*10,范围 0~1190
wire [11:0] rh_disp_10 = (rh_abs_10 > 12'd999) ? 12'd999 : rh_abs_10; // 超过 99.9% 就封顶
// 拆分每一位(湿度最多显示 99.9%,只要十位、个位、小数位)
wire [3:0] h_shi = (rh_disp_10 / 14'd100) % 14'd10; // 十位
wire [3:0] h_ge = (rh_disp_10 / 14'd10) % 14'd10; // 个位
wire [3:0] h_xiao = rh_disp_10 % 14'd10; // 小数位
//------------------- 串口发送控制 -------------------
// 温度(key_flag=0):"温度:" + 符号 + 十位 + 个位 + "." + 小数位 + "℃" + 回车换行(GBK,14 字节)
// 湿度(key_flag=1):"湿度:" + 十位 + 个位 + "." + 小数位 + "%" + 回车换行(12 字节)
wire [7:0] sign_byte = neg_flag ? 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_TEMP = 5'd14; // 温度一共发 14 个字节
localparam TX_TOTAL_HUMI = 5'd12; // 湿度一共发 12 个字节
wire [4:0] tx_total = key_flag ? TX_TOTAL_HUMI : TX_TOTAL_TEMP;
reg [4: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 <= 5'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 <= 5'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 == 5'd0) begin
if (key_flag == 1'b1) begin
//---- 湿度:"湿度:45.3%" 共 12 字节 ----
case(tx_byte_cnt)
5'd0: tx_data <= 8'hCA; // "湿" 高字节
5'd1: tx_data <= 8'hAA; // "湿" 低字节
5'd2: tx_data <= 8'hB6; // "度" 高字节
5'd3: tx_data <= 8'hC8; // "度" 低字节
5'd4: tx_data <= 8'h3A; // ':'
5'd5: tx_data <= dec2ascii(h_shi); // 十位
5'd6: tx_data <= dec2ascii(h_ge); // 个位
5'd7: tx_data <= 8'h2E; // '.'
5'd8: tx_data <= dec2ascii(h_xiao); // 小数位
5'd9: tx_data <= 8'h25; // '%'
5'd10: tx_data <= 8'h0D; // 回车 CR
5'd11: tx_data <= 8'h0A; // 换行 LF
default: tx_data <= 8'd0;
endcase
end
else begin
//---- 温度:"温度: 25.4℃" 共 14 字节 ----
case(tx_byte_cnt)
5'd0: tx_data <= 8'hCE; // "温" 高字节
5'd1: tx_data <= 8'hC2; // "温" 低字节
5'd2: tx_data <= 8'hB6; // "度" 高字节
5'd3: tx_data <= 8'hC8; // "度" 低字节
5'd4: tx_data <= 8'h3A; // ':'
5'd5: tx_data <= sign_byte; // 符号位
5'd6: tx_data <= dec2ascii(d_bai); // 十位
5'd7: tx_data <= dec2ascii(d_shi); // 个位
5'd8: tx_data <= 8'h2E; // '.'
5'd9: tx_data <= dec2ascii(d_ge); // 小数位
5'd10: tx_data <= 8'hA1; // "℃" 高字节
5'd11: tx_data <= 8'hE6; // "℃" 低字节
5'd12: tx_data <= 8'h0D; // 回车 CR
5'd13: 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
//------------------- i2c_master 例化 -------------------
wire [15:0] temp_raw; // 读到的 16 位原始值(MSB+LSB)
wire read_done; // 读完成脉冲
i2c_master u_i2c (
.sys_clk (sys_clk),
.rst_n (rst_n),
.scl (scl),
.sda (sda),
.measure_start (measure_start),
.read_data_reg (temp_raw),
.read_done (read_done),
.key_flag (key_flag)
);
//------------------- 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
`timescale 1ns/1ps
module i2c_master(
input sys_clk, // 系统时钟
input rst_n, // 复位,低电平有效
output scl, // I2C 时钟线(接从机 SCL)
inout sda, // I2C 数据线(双向,接从机 SDA)
input measure_start, // 写请求:一个脉冲触发一次测量
input key_flag, // 0=测温度,1=测湿度
output[15:0] read_data_reg, // 读到的 16 位原始值(MSB+LSB)
output read_done // 读完成脉冲(给顶层去发串口)
);
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 WAIT = 4'd6; // 等待:等 SHT20 测量完成
localparam NACK = 4'd7; // 主机发 NACK(读完了)
localparam RACK = 4'd8; // 主机发 ACK(还要继续读)
localparam STOP_for_waiting = 4'd9; // 发完命令后的停止条件
//------------------------------------------------------------------
// 分频参数
//------------------------------------------------------------------
localparam SCL_DIV = 100; // SCL 半个周期 = 100 个 sys_clk 周期
// 所以 SCL 频率 = sys_clk / 200 = 250kHz
localparam add_write = 8'h80; // 从机写地址(0x40 << 1 | 0)
localparam add_read = 8'h81; // 从机读地址(0x40 << 1 | 1)
localparam command_tem = 8'hF3; // 温度测量命令
localparam command_wet = 8'hF5; // 湿度测量命令
//------------------------------------------------------------------
// 内部寄存器
//------------------------------------------------------------------
reg[3:0] cur_state; // 当前状态
reg[3:0] nxt_state; // 下一状态
reg wait_reg;
reg[21:0] div_cnt; // 节拍计数器:0~100(起始态 0~200)
reg[3:0] bit_cnt; // 位计数器:一个字节发到第几位(0~7)
reg[1:0] pulse_cnt; // 脉冲计数器
reg[2:0] write_byte; // 写字节计数器:发到第几个字节(0/1/2)
reg sda_oe; // 为 1/0 时控制/放开 sda
reg sda_out; // 要发到 SDA 上的数据
reg scl_out; // 内部生成的 SCL 时钟
reg [15:0] read_data_reg; // 读数据寄存器
reg [7:0] crc_reg; // CRC 寄存器
reg [2:0] read_byte; // 读字节计数器:0/1/2 = MSB/LSB/CRC
reg [22:0] wait_cnt; // 等待延时计数器:WAIT 态数约 100ms
wire stop_waiting_flag; // 等待结束标志
reg [7:0] tem_or_wet_data; // 当前要发的测量命令
//------------------------------------------------------------------
// 连续赋值
//------------------------------------------------------------------
// SDA 三态:主机要发的时候驱动,要收的时候放开成高阻
assign sda = sda_oe ? sda_out : 1'bz;
// SCL 直接输出内部生成的时钟
assign scl = scl_out;
// 读完成脉冲:停止条件结束、回到空闲的那一刻
assign read_done = (cur_state == STOP && nxt_state == IDLE);
always @(*) begin
if(cur_state == READ || cur_state == ACK) // 读数据、写后应答:释放 SDA,让从机来驱动
sda_oe = 0;
else
sda_oe = 1; // 其它情况:主机自己驱动 SDA
end
//------------------------------------------------------------------
// 温湿度切换:空闲态根据 key_flag 决定这一轮发哪个命令
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
tem_or_wet_data <= 0;
else if(cur_state == IDLE && key_flag == 0)
tem_or_wet_data <= command_tem;
else if(cur_state == IDLE && key_flag == 1)
tem_or_wet_data <= command_wet;
else
tem_or_wet_data <= tem_or_wet_data;
end
//------------------------------------------------------------------
// 状态寄存器:每个时钟沿把 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
// wait 寄存器:进过 WAIT 状态就置 1,表示"已经等过测量"
always @(posedge sys_clk or negedge rst_n) begin
if (!rst_n)
wait_reg <= 0;
else if(cur_state == IDLE)
wait_reg <= 0;
else if (cur_state == WAIT)
wait_reg <= 1;
end
// 等待延时计数器:WAIT 状态数约 100ms(SHT20 测量需要这么久)
always @(posedge sys_clk or negedge rst_n) begin
if (!rst_n)
wait_cnt <= 0;
else if (cur_state == WAIT)
wait_cnt <= wait_cnt + 1'b1;
else
wait_cnt <= 0;
end
// 等待结束标志:数够 100ms(50MHz × 5,000,000 个周期)才结束 WAIT
assign stop_waiting_flag = (wait_cnt == 23'd5_000_000);
//------------------------------------------------------------------
// 状态机(组合逻辑):决定下一状态
//------------------------------------------------------------------
always @(*) begin
case (cur_state)
IDLE:begin
if (measure_start == 1) // 有测量请求才发起始条件
nxt_state = START;
else
nxt_state = IDLE; // 没请求就待在空闲
end
START:begin
if (div_cnt == 2*SCL_DIV)
nxt_state = WRITE;
else
nxt_state = START;
end
WRITE:begin
if (bit_cnt == 8 && scl_out == 0 && write_byte != 3)
nxt_state = ACK;
else if(bit_cnt == 8 && scl_out == 0 && write_byte == 3)
nxt_state = RACK;
else
nxt_state = WRITE;
end
READ:begin
if (bit_cnt == 8 && scl_out == 0 && read_byte != 2) // 前两个字节(MSB/LSB)读完,主机发 ACK
nxt_state = RACK;
else if(bit_cnt == 8 && scl_out == 0 && read_byte == 2) // 最后一个字节(CRC)读完,主机发 NACK
nxt_state = NACK;
else
nxt_state = READ;
end
STOP_for_waiting:begin
if(div_cnt == SCL_DIV)
nxt_state = WAIT;
else
nxt_state = STOP_for_waiting;
end
WAIT:begin
if(stop_waiting_flag)
nxt_state = START;
else
nxt_state = WAIT;
end
RACK:begin
if(div_cnt == 100 && scl_out == 1)
nxt_state = READ;
else
nxt_state = RACK;
end
NACK:begin
if(div_cnt == SCL_DIV && scl_out == 1) // SCL 高电平结束(NACK 已发完),去停止
nxt_state = STOP;
else
nxt_state = NACK;
end
ACK:begin
if(div_cnt == SCL_DIV && scl_out == 1) begin // SCL 高电平结束(从机已回 ACK)
if(write_byte == 0) // 发完地址 0x80,接着发命令
nxt_state = WRITE;
else if(write_byte == 1) // 发完命令 0xF3/0xF5,去停止等待测量
nxt_state = STOP_for_waiting;
else // 发完读地址 0x81,去读数据
nxt_state = READ;
end
else
nxt_state = ACK;
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
//------------------------------------------------------------------
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
//------------------------------------------------------------------
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 == STOP_for_waiting || 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) // 空闲/应答态:归 0
bit_cnt <= 'd0;
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 || cur_state == STOP_for_waiting) && 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) // 数据/应答:数到 100 取反
scl_out <= ~scl_out;
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 || cur_state == WAIT)
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 && write_byte == 0)
sda_out <= add_write[7-bit_cnt]; // 发地址 0x80
else if (scl_out == 0 && write_byte == 1)
sda_out <= tem_or_wet_data[7-bit_cnt]; // 发命令 0xF3/0xF5
else if(scl_out == 0 && write_byte == 2)
sda_out <= add_read[7-bit_cnt]; // 发读地址 0x81
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 || cur_state == STOP_for_waiting) 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 引脚(读时是 SHT20 在驱动 SDA)
// 注意:读完后 read_data_reg 必须保持住,不能清 0,
// 否则回到空闲态后顶层还没把串口发完,温度值就变成 0 了
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n) begin
read_data_reg <= 0;
crc_reg <= 0;
end
else if (cur_state == READ && scl_out == 1) begin
if(div_cnt == 50 && read_byte == 0)
read_data_reg[15-bit_cnt] <= sda; // 高字节 MSB
else if(div_cnt == 50 && read_byte == 1)
read_data_reg[7-bit_cnt] <= sda; // 低字节 LSB
else if(div_cnt == 50 && read_byte == 2)
crc_reg[7-bit_cnt] <= sda; // CRC 字节
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
`timescale 1ns/1ps
//==================================================================
// uart_tx : UART 发送模块
// 115200 波特率,8 位数据位,无校验,1 位停止位
// 每个字节的发送顺序:起始位(0) + bit0..bit7 + 停止位(1)
//==================================================================
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
73
74
75
76
77
78
| 状态 | 作用 |
|---|---|
| IDLE | 空闲,等 measure_start 脉冲 |
| START | 产生起始条件 |
| WRITE | 发送一个字节的 8 位 |
| ACK | 第 9 个时钟,释放 SDA 等从机应答 |
| STOP_for_waiting | 发完命令后的停止条件,释放总线去等测量 |
| WAIT | 等待 100ms,让 SHT20 完成测量 |
| READ | 读一个字节的 8 位 |
| RACK | 主机发 ACK(拉低 SDA),还要继续读 |
| NACK | 主机发 NACK(拉高 SDA),读完了 |
| STOP | 产生停止条件,回到空闲 |
7、I/O 绑定
对应的约束文件内容如下:
// 引脚约束(立创 LJPI-G1 开发板)
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;
// ---- SHT20 的 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;
// ---- 按键
IO_LOC "key" D11;
IO_PORT "key" IO_TYPE=LVCMOS33 PULL_MODE=UP 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
引脚绑定
此处需要杜邦线把 SHT20 模块和拓展排针相连,接线方式:
| SHT20 模块 | 开发板 |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SCL | R14 |
| SDA | T15 |
模块接线
注意
SHT20 的工作电压是 2.1 - 3.6V,不能接 5V,接错会烧掉传感器。
8、程序下载
把程序综合、布局布线后烧录进开发板,打开串口助手,设置好参数:
- 波特率:115200
- 数据位:8
- 停止位:1
- 校验位:无
显示乱码的话
代码里的"温度""湿度""℃"用的是 GBK 编码(0xCE 0xC2 = "温",0xCA 0xAA = "湿",0xA1 0xE6 = "℃")。串口助手如果按 UTF-8 解码就会显示乱码,把接收编码切换成 GBK / ANSI 即可正常显示。
正常情况下每秒会打印一行温度,按一下开发板按键切换到湿度显示,再按一下切回温度。
如图所示