SHT30 是 Sensirion(盛思锐)推出的新一代数字温湿度传感器,是 SHT20 的升级版,精度更高(温度 ±0.2℃、湿度 ±2%RH)、工作电压更宽(2.15~5.5V)、测量速度更快(15ms vs 85ms)。同样使用标准 I2C 接口通讯,出厂已完成校准,不需要外部 AD 转换。
和 SHT20 最大的区别是:SHT30 一条命令就能同时读回温度和湿度,不需要分两次测。命令从 1 字节变成 2 字节,读回的数据从 3 字节变成 6 字节。
1、模块来源
2、参数
工作电压:2.15 - 5.5V(比 SHT20 宽,可以接 5V)
通讯接口:I2C(最高 1MHz)
从机地址:0x44(ADDR 引脚接低/悬空)或 0x45(ADDR 引脚接高)
温度量程:-40 ~ +125 ℃
温度精度:±0.2 ℃(典型值)
湿度量程:0 ~ 100 %RH
湿度精度:±2 %RH(典型值)
默认分辨率:温度 16bit / 湿度 16bit
管脚数量:4 Pin(VCC、GND、SCL、SDA)
3、实战任务
使用逻辑派 FPGA-G1 开发板编写 SHT30 驱动代码,每隔 1 秒读取一次数据,通过串口同时发送温度和湿度到上位机显示:
温度: 25.4℃
湿度:45.3%2
4、时序说明
SHT30 使用标准 I2C 协议,起始条件、停止条件、数据传输、应答信号这些基础知识和 SHT20 完全一样,可以参考 SHT20 教程 的时序说明部分。这里只讲 SHT30 和 SHT20 不同的地方。
4.2 2 字节命令
本教程使用的命令是 0x2400,含义是:单次测量、高重复性(16bit 精度)、禁用时钟拉伸。
禁用时钟拉伸的好处是流程和 SHT20 的非保持主机模式一样:发命令 → STOP → 等 → 重新 START 读。不需要改 I2C 底层时序。
4.3 读 6 个字节
SHT30 一次把温度和湿度都测完,读回来 6 个字节,顺序固定:
| 字节序号 | 内容 | 读完主机回什么 |
|---|---|---|
| 0 | 温度高字节 MSB | ACK |
| 1 | 温度低字节 LSB | ACK |
| 2 | 温度 CRC | ACK |
| 3 | 湿度高字节 MSB | ACK |
| 4 | 湿度低字节 LSB | ACK |
| 5 | 湿度 CRC | NACK(读完了)→ 然后 STOP |
温度和湿度要存到两个独立的 16 位寄存器,不能共用一个,不然后面读湿度会把前面的温度覆盖掉。
5、通讯步骤
完整一轮测量分三个阶段:
第一阶段:下发测量命令(发 3 个字节)
- 主机发起始条件。
- 主机发写地址 0x88(0x44 左移一位,最低位 0 = 写),等 ACK。
- 主机发命令高字节 0x24,等 ACK。
- 主机发命令低字节 0x00,等 ACK。
- 主机发停止条件,释放总线。
第二阶段:等待测量完成
- 等待约 20ms(高重复性最长 15ms,多留 5ms 余量)。
第三阶段:读取结果(读 6 个字节)
- 主机重新发起始条件,发读地址 0x89(最低位 1 = 读),等 ACK。
- 连续读 6 个字节:温度 MSB、温度 LSB、温度 CRC、湿度 MSB、湿度 LSB、湿度 CRC。
- 前 5 个字节读完,主机回 ACK。
- 第 6 个字节读完,主机回 NACK。
- 主机发停止条件,一次完整测量结束。
说明
本教程的驱动读到了 CRC 字节但没有做校验,只是把它接收下来丢掉。想做得更完整可以加上 CRC-8(多项式 0x31,初值 0xFF)校验,读错了就丢弃这次数据重测。
5.1 数据换算
读回来的数据拼成 16 位原始值,datasheet 给的换算公式是:
温度:
湿度:
和 SHT20 比有两个变化:温度偏移从 -46.85 变成 -45,系数从 175.72 变成 175;湿度没有负偏移了(SHT20 是 -6 + 125×…),直接就是 0~100%。
定点化(分母用 65536 代替 65535,误差 0.003℃ / 0.002%,忽略):
温度:|T| × 100 = | (S_T × 17500) >> 16 − 4500 |
湿度: RH × 100 = (S_RH × 10000) >> 162
湿度这条没有减法,不会出负数,代码比 SHT20 简单:
wire [29:0] mul_rh_result = humi_raw * 16'd10000;
wire [13:0] mul_rh_shift = mul_rh_result[29:16]; // 0~10000,直接就是 RH×1002
6、程序编写
整个工程分成三个模块:
- top.v(顶层):产生 1 秒定时的测量请求,把温度和湿度原始值分别换算、拆位,然后按顺序把 26 个字节(温度 14 字节 + 湿度 12 字节)喂给串口,每秒同时打印温度和湿度。
- i2c_master.v(I2C 驱动):用状态机实现完整的 I2C 主机时序。和 SHT20 相比,写阶段多发一个命令低字节,读阶段从 3 字节变成 6 字节,温度和湿度存到两个独立寄存器。
- uart.v(串口发送):和 SHT20 完全一样,不用改。
`timescale 1ns/1ps
module top(
input sys_clk,
input rst_n,
output scl,
inout sda,
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
// 按键消抖(预留,当前未使用)
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
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
//------------------- 温度换算(SHT30)-------------------
// 公式:T = -45 + 175 * ST / 65536
// 换算成 |T|*100 = | (ST*17500) >> 16 - 4500 |
wire [30:0] mul_result = temp_raw * 16'd17500; // 31 位
wire [14:0] mul_shift = mul_result[30:16]; // 0 ~ 17500
wire neg_flag = (mul_shift < 15'd4500); // 是否负数
wire [14:0] temp_abs_100 = neg_flag ? (15'd4500 - mul_shift)
: (mul_shift - 15'd4500);
wire [13:0] temp_abs_10 = temp_abs_100 / 14'd10; // 范围 0 ~ 1300
// 拆分每一位数字: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;
//------------------- 湿度换算(SHT30)-------------------
// 公式:RH = 100 * SR / 65536(没有偏移量,不会出负数)
// 换算成 RH*100 = (SR*10000) >> 16
wire [29:0] mul_rh_result = humi_raw * 16'd10000; // 30 位
wire [13:0] mul_rh_shift = mul_rh_result[29:16]; // = (SR*10000)>>16,范围 0~10000
wire [11:0] rh_abs_10 = mul_rh_shift / 14'd10; // RH*10,范围 0~1000
wire [11:0] rh_disp_10 = (rh_abs_10 > 12'd999) ? 12'd999 : rh_abs_10;
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; // 小数位
//------------------- 串口发送控制 -------------------
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 = 5'd26; // 温度 14 字节 + 湿度 12 字节 = 26 字节
reg [4:0] tx_byte_cnt;
reg [1:0] tx_state;
reg uart_tx_req;
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) // 26 个字节都发完,回空闲
tx_state <= S_IDLE;
else if(uart_tx_done || tx_byte_cnt == 5'd0) begin
case(tx_byte_cnt)
//---- 温度:"温度: 25.4℃\r\n" 共 14 字节 ----
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
//---- 湿度:"湿度:45.3%\r\n" 共 12 字节 ----
5'd14: tx_data <= 8'hCA; // "湿" 高字节
5'd15: tx_data <= 8'hAA; // "湿" 低字节
5'd16: tx_data <= 8'hB6; // "度" 高字节
5'd17: tx_data <= 8'hC8; // "度" 低字节
5'd18: tx_data <= 8'h3A; // ':'
5'd19: tx_data <= dec2ascii(h_shi); // 十位
5'd20: tx_data <= dec2ascii(h_ge); // 个位
5'd21: tx_data <= 8'h2E; // '.'
5'd22: tx_data <= dec2ascii(h_xiao); // 小数位
5'd23: tx_data <= 8'h25; // '%'
5'd24: tx_data <= 8'h0D; // 回车 CR
5'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
//------------------- i2c_master 例化 -------------------
wire [15:0] temp_raw; // 读到的 16 位温度原始值
wire [15:0] humi_raw; // 读到的 16 位湿度原始值
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),
.humi_data_reg (humi_raw),
.read_done (read_done)
);
//------------------- 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
`timescale 1ns/1ps
module i2c_master(
input sys_clk,
input rst_n,
output scl,
inout sda,
input measure_start, // 写请求:一个脉冲触发一次测量
output[15:0] read_data_reg, // 读到的 16 位温度原始值(MSB+LSB)
output[15:0] humi_data_reg, // 读到的 16 位湿度原始值(MSB+LSB)
output read_done // 读完成脉冲
);
localparam IDLE = 4'd0; // 空闲
localparam START = 4'd1; // 起始
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; // 等待:等 SHT30 测量完成
localparam NACK = 4'd7; // 主机发 NACK
localparam RACK = 4'd8; // 主机发 ACK)
localparam STOP_for_waiting = 4'd9;
//------------------------------------------------------------------
// 分频参数
//------------------------------------------------------------------
localparam SCL_DIV = 100; // SCL 半个周期 = 100 个 sys_clk 周期
localparam add_write = 8'h88; // SHT30 写地址(0x44 << 1 | 0)
localparam add_read = 8'h89; // SHT30 读地址(0x44 << 1 | 1)
localparam command_high = 8'h24; // 单次测量命令高字节(禁用时钟拉伸,高重复性)
localparam command_low = 8'h00; // 单次测量命令低字节
//------------------------------------------------------------------
// 内部寄存器
//------------------------------------------------------------------
reg[3:0] cur_state; // 当前状态
reg[3:0] nxt_state; // 下一状态
reg wait_reg;
reg[21:0] div_cnt; // 节拍计数器
reg[3:0] bit_cnt; // 位计数器(0~7)
reg[1:0] pulse_cnt; // 脉冲计数器
reg[2:0] write_byte; // 写字节计数器(0/1/2/3)
reg sda_oe; // 为 1/0 时控制/放开 sda
reg sda_out;
reg scl_out; // 内部生成的 SCL 时钟
reg [15:0] read_data_reg; // 温度读数据寄存器
reg [15:0] humi_data_reg; // 湿度读数据寄存器
reg [7:0] crc_reg_t; // 温度 CRC 寄存器
reg [7:0] crc_reg_w; // 湿度 CRC 寄存器
reg [2:0] read_byte; // 读字节计数器:0~5
reg [22:0] wait_cnt; // 等待延时计数器
wire stop_waiting_flag; // 等待结束标志
// 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
//------------------------------------------------------------------
// 状态寄存器:每个时钟沿把 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 状态计数约 20ms(SHT30 高重复性最长 15ms,留 5ms 余量)
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
// 等待结束标志:数够 20ms(50MHz × 1,000,000 个周期)
assign stop_waiting_flag = (wait_cnt == 23'd1_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 = ACK;
else
nxt_state = WRITE;
end
READ: begin
if (bit_cnt == 8 && scl_out == 0 && read_byte != 5)
nxt_state = RACK; // 前 5 个字节读完,主机发 ACK
else if (bit_cnt == 8 && scl_out == 0 && read_byte == 5)
nxt_state = NACK; // 第 6 个字节读完,主机发 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)
nxt_state = STOP;
else
nxt_state = NACK;
end
ACK: begin
if (div_cnt == SCL_DIV && scl_out == 1) begin
if (write_byte == 0) // 发完写地址 0x88 → 继续发命令高字节
nxt_state = WRITE;
else if (write_byte == 1) // 发完命令高 0x24 → 继续发命令低字节
nxt_state = WRITE;
else if (write_byte == 2) // 发完命令低 0x00 → 去停止,等测量
nxt_state = STOP_for_waiting;
else // 发完读地址 0x89 → 去读数据
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)
div_cnt <= 'd0;
else if(cur_state != nxt_state)
div_cnt <= 'd0;
else if(cur_state == START)
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;
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;
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 时钟生成
// 写数据/应答态:div_cnt 每数到 100 取反一次,形成方波(半周期=100个sys_clk)
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
scl_out <= 1'b1;
else if(cur_state == START && div_cnt == SCL_DIV*2)
scl_out <= 1'b0;
else if((cur_state == STOP || cur_state == STOP_for_waiting) && div_cnt == SCL_DIV/2)
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;
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]; // 发写地址 0x88
else if (scl_out == 0 && write_byte == 1)
sda_out <= command_high[7-bit_cnt]; // 发命令高字节 0x24
else if (scl_out == 0 && write_byte == 2)
sda_out <= command_low[7-bit_cnt]; // 发命令低字节 0x00
else if (scl_out == 0 && write_byte == 3)
sda_out <= add_read[7-bit_cnt]; // 发读地址 0x89
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 引脚
// SHT30 读回 6 个字节:温度MSB/LSB/CRC、湿度MSB/LSB/CRC
// 温度和湿度分别存到两个独立寄存器,互不覆盖
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if (!rst_n) begin
read_data_reg <= 0;
humi_data_reg <= 0;
crc_reg_t <= 0;
crc_reg_w <= 0;
end
else if (cur_state == READ && scl_out == 1 && div_cnt == 50) begin
case (read_byte)
3'd0: read_data_reg[15-bit_cnt] <= sda; // 温度高字节
3'd1: read_data_reg[7-bit_cnt] <= sda; // 温度低字节
3'd2: crc_reg_t[7-bit_cnt] <= sda; // 温度 CRC
3'd3: humi_data_reg[15-bit_cnt] <= sda; // 湿度高字节
3'd4: humi_data_reg[7-bit_cnt] <= sda; // 湿度低字节
3'd5: crc_reg_w[7-bit_cnt] <= sda; // 湿度 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
`timescale 1ns/1ps
//==================================================================
// uart_tx : UART 发送模块
// 115200 波特率,8 位数据位,无校验,1 位停止位
// 每个字节的发送顺序:起始位(0) + bit0..bit7 + 停止位(1)
//==================================================================
module uart_tx (
input clk,
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 | 等待 20ms,让 SHT30 完成测量 |
| 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;
// ---- SHT30 的 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
引脚绑定
此处需要杜邦线把 SHT30 模块和拓展排针相连,接线方式:
| SHT30 模块 | 开发板 |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SCL | R14 |
| SDA | T15 |
模块接线
关于供电
SHT30 的工作电压是 2.15~5.5V,接 3.3V 或 5V 都可以。但模块上的上拉电阻通常拉到 VCC,接 5V 的话 SDA/SCL 高电平也是 5V,保险起见还是接 3.3V。
8、程序下载
把程序综合、布局布线后烧录进开发板,打开串口助手,设置好参数:
- 波特率:115200
- 数据位:8
- 停止位:1
- 校验位:无
显示乱码的话
代码里的"温度""湿度""℃"用的是 GBK 编码(0xCE 0xC2 = "温",0xCA 0xAA = "湿",0xA1 0xE6 = "℃")。串口助手如果按 UTF-8 解码就会显示乱码,把接收编码切换成 GBK / ANSI 即可正常显示。
正常情况下每秒会同时打印一行温度和一行湿度。