GY-906 是一块基于 MLX90614 的非接触式红外测温模块。MLX90614 是 Melexis(迈来芯)出品的数字红外温度传感器,内部由红外热电堆和专用信号处理芯片组成:热电堆接收物体自身辐射的红外能量,芯片把它换算成温度数字量,出厂已完成校准,通过 SMBus(兼容 I2C)接口直接输出,不需要外部电路,也不用自己拟合校准曲线。
它测的是「你瞄准的那个物体的表面温度」,不用贴在物体上就能读数——用手指在传感器前晃一下,就能看到串口数值跟着变,非常适合做人体测温、物体表面测温这类应用。在 I2C 驱动学习路线上,它比 SHT20 多了两个知识点:读回的数据低字节在前,以及 SMBus 协议要求的 PEC 校验字节。
1、模块来源
2、参数
工作电压:3.3V(MLX90614 内核为 3.3V,部分模块带 LDO 标称可接 5V,但接 3.3V 最稳妥)
通讯接口:SMBus(兼容 I2C,SCL 时钟上限 100kHz)
从机地址:0x5A(7 位,出厂固定;8 位写地址 0xB4,读地址 0xB5)
测温量程:-70.01 ~ +382.19 ℃(物体温度 Tobj1,实际精度受环境温度影响)
分辨率:0.02 ℃
精度:±0.5 ℃(0~50℃ 范围内典型值,医用版可达 ±0.2 ℃)
管脚数量:4 Pin(VCC、GND、SCL、SDA)
提示
I2C/SMBus 标准里 SCL、SDA 是开漏输出,总线上必须有上拉电阻。GY-906 模块板上一般已焊好上拉电阻,直接接线即可。本工程 SDA 用三态门模拟开漏(1'bz),SCL 为写起来简单直接用了推挽输出(assign scl = scl_out),配合板上上拉实测可用。
3、实战任务
使用逻辑派 FPGA-G1 开发板编写 GY-906 驱动代码,每隔 1 秒读取一次 MLX90614 的物体温度,并通过串口发送到上位机显示,格式 温度: 25.4℃。用手在传感器前晃动,能看到温度实时变化。
4、时序说明
MLX90614 使用的协议叫 SMBus,可以理解为 I2C 的「兄弟标准」:起始条件、停止条件、MSB 先行、第 9 个时钟收应答,这些规则和 I2C 完全一样(不再重复,可参考 SHT20 教程)。对使用者来说最大的区别是:SCL 时钟上限 100kHz——I2C 快速模式能跑到 400kHz,但 MLX90614 是 SMBus 器件,本教程的驱动把 SCL 设为 100kHz,保证兼容。
区别在于通讯流程。SHT20 是「发命令 → 等 100ms → 回来读」,而 MLX90614 上电即测、内部持续刷新温度数据,随读随有,不需要等待测量时间,也不需要任何初始化操作。读温度就是一次标准的 SMBus Read Word(读一个字):
- 主机发起始条件。
- 主机发从机写地址 0xB4(7 位地址 0x5A 左移一位 | 0),等 ACK。
- 主机发命令字 0x07(RAM 里的物体温度 Tobj1;改成 0x06 可读环境温度 Ta),等 ACK。
- 主机发重复起始条件(REPEATED START)——注意这里不发停止条件,而是直接把 SDA 在 SCL 高电平期间拉低再发一次起始。意义是不释放总线:如果先发 STOP,别的主机可能插进来抢走 MLX90614,重复起始保证这一读一写是原子的。
- 主机发从机读地址 0xB5,等 ACK。
- 从机开始吐数据,一共 3 个字节:先低字节 LSB,主机回 ACK 表示还要;再高字节 MSB,主机回 ACK;最后 PEC 校验字节,主机回 NACK 表示读完。
- 主机发停止条件,一次读取结束。
和 SHT20 驱动比,两个最容易踩的坑
① 数据低字节在前:SHT20 和大部分 I2C 器件都是高字节先出,MLX90614 却是 LSB 先出,拼 16 位数的时候顺序是反的,拼错了数据会大得离谱; ② 多一个 PEC 字节:SMBus 协议要求,本教程读完直接丢弃不做校验。想做得更完整可以加 CRC-8 校验(多项式 x⁸+x⁵+x⁴+1,初值 0,覆盖从写地址到 MSB 的所有字节),校验失败就丢弃这次数据重读。
4.1 不需要初始化
这一点和很多传感器不一样:MLX90614 上电即测,没有任何「睡眠模式」需要唤醒,也没有测量命令需要下发,内部数据一直在刷新。所以驱动里不需要初始化状态机,上电后直接开始读就行。
4.2 数据换算
读回来的 LSB、MSB 拼成一个 16 位无符号原始值 raw(LSB 在低 8 位),datasheet 给的换算公式是:
FPGA 里同样要做成定点整数运算。两边乘 100(保留两位小数),0.02 × 100 = 2 正好是整数,273.15 × 100 = 27315:
T × 100 = raw × 2 − 27315验证一下:36.5℃ 对应 raw = (36.5 + 273.15) ÷ 0.02 ≈ 15482,代入得 15482 × 2 − 27315 = 3649,即 36.49℃。
注意和 SHT20 的区别:这次 raw 本身是无符号的(负温度对应的是小 raw 值,raw 不会为负),但减去 27315 之后 T×100 可能为负(冬天低温),所以代码里乘法和拼接全程用无符号,减法结果用 signed 声明,再用 neg_flag 取符号和绝对值拆位——符号处理从「输入端」挪到了「减法之后」。
5、程序编写
整个工程分成三个模块,结构和 SHT20 工程一致:
- top.v(顶层):产生 1 秒定时的测量请求,把原始值换算成温度并拆成一位一位的数字,再按顺序喂给串口。
- i2c_master.v(SMBus 驱动):用状态机实现完整时序:「写地址 + 命令字 → 重复起始 → 读 3 字节(LSB/MSB/PEC)」。
- uart.v(串口发送):把一个字节按「起始位 + 8 位数据 + 停止位」一位一位发出去,与 SHT20 工程完全相同。
`timescale 1ns/1ps
module top(
input sys_clk, // 系统时钟 50MHz
input rst_n, // 复位,低电平有效
output scl, // SMBus 时钟线
inout sda, // SMBus 数据线
output uart_tx // 串口发送线
);
reg [25:0] timer_cnt; // 1 秒计数器
reg measure_start; // 每秒发一个测量请求脉冲
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; // 发送中
localparam TX_TOTAL = 5'd15; // 一共发 15 个字节(含百位)
//------------------------------------------------------------------
// 1 秒定时:每秒产生一个测量请求脉冲
// (MLX90614 上电即测、不需要初始化,每个脉冲直接触发一次读温度)
//------------------------------------------------------------------
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
//------------------- 温度换算 -------------------
// datasheet 公式:T = raw × 0.02 − 273.15(raw 是 16 位无符号原始值,LSB 在前已拼好)
// 两边乘 100 变成纯整数运算:T × 100 = raw × 2 − 27315
// 例:36.5℃ 时 raw ≈ 15482,15482×2−27315 = 3649,即 36.49℃
wire [15:0] temp_raw; // 读到的 16 位温度原始值(MSB:LSB 已拼好)
wire read_done; // 读完成脉冲
wire [16:0] raw_x2 = {temp_raw, 1'b0}; // raw × 2 = 左移一位,最大 131070
wire signed [17:0] temp_100 = $signed({1'b0, raw_x2}) - 18'sd27315; // T×100,范围 -27315 ~ +103755
wire neg_flag = (temp_100 < 0); // 是否负温度(量程最低 -70℃)
wire [17:0] temp_abs_100 = neg_flag ? (-temp_100) : temp_100; // |T| × 100
wire [13:0] temp_abs_10 = temp_abs_100 / 10; // |T| × 10,范围 0 ~ 10375
// 拆分每一位数字:d_bai=百位,d_shi=十位,d_ge=个位,d_xiao=小数点后一位
// (MLX90614 量程 -70 ~ +382℃,可能出现三位整数,所以百位单独处理)
wire [3:0] d_bai = (temp_abs_10 / 1000) % 10;
wire [3:0] d_shi = (temp_abs_10 / 100) % 10;
wire [3:0] d_ge = (temp_abs_10 / 10) % 10;
wire [3:0] d_xiao = temp_abs_10 % 10;
//------------------- 串口发送控制 -------------------
// "温度:" + 符号 + 百位 + 十位 + 个位 + "." + 小数位 + "℃" + 回车换行(GBK,15 字节)
wire [7:0] sign_byte = neg_flag ? 8'h2D : 8'h20; // 负号 '-' 或 空格 ' '
wire [7:0] bai_byte = (d_bai == 0) ? 8'h20 : dec2ascii(d_bai); // 百位为 0 发空格,避免显示 025.4
// 十进制数字 0~9 转 ASCII 码
function [7:0] dec2ascii;
input [3:0] d;
begin
dec2ascii = 8'h30 + d; // 0~9 -> '0'~'9'
end
endfunction
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
//---- 温度:"温度: 25.4℃" 共 15 字节 ----
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 <= bai_byte; // 百位(为 0 时发空格)
5'd7: tx_data <= dec2ascii(d_shi); // 十位
5'd8: tx_data <= dec2ascii(d_ge); // 个位
5'd9: tx_data <= 8'h2E; // '.'
5'd10: tx_data <= dec2ascii(d_xiao); // 小数位
5'd11: tx_data <= 8'hA1; // "℃" 高字节
5'd12: tx_data <= 8'hE6; // "℃" 低字节
5'd13: tx_data <= 8'h0D; // 回车 CR
5'd14: 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 例化 -------------------
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)
);
//------------------- 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
`timescale 1ns/1ps
module i2c_master(
input sys_clk, // 系统时钟 50MHz
input rst_n, // 复位,低电平有效
output scl, // I2C 时钟线(接从机 SCL)
inout sda, // I2C 数据线(双向,接从机 SDA)
input measure_start, // 测量请求:一个脉冲触发一次读温度
output[15:0] read_data_reg, // 读到的 16 位温度原始值(MSB:LSB 拼好,无符号)
output read_done // 读完成脉冲(给顶层去发串口)
);
localparam IDLE = 4'd0; // 空闲
localparam START = 4'd1; // 起始
localparam WRITE = 4'd2; // 写数据
localparam ACK = 4'd3; // 应答
localparam STOP = 4'd4; // 停止:发 STOP 条件
localparam READ = 4'd5; // 读数据:收一个字节的 8 位
localparam NACK = 4'd7; // 主机发 NACK(读完了)
localparam RACK = 4'd8; // 主机发 ACK(还要继续读)
localparam RESTART = 4'd10; // 重复起始:不发 STOP 直接再来一次 START
//------------------------------------------------------------------
// 分频参数与器件参数
//------------------------------------------------------------------
localparam SCL_DIV = 250; // SCL 半个周期 = 250 个 sys_clk 周期
// 所以 SCL 频率 = 50MHz / 500 = 100kHz
localparam add_write = 8'hB4; // MLX90614 写地址
localparam add_read = 8'hB5; // MLX90614 读地址
localparam cmd_tobj = 8'h07; // 命令字
//------------------------------------------------------------------
// 内部寄存器
//------------------------------------------------------------------
reg [3:0] cur_state; // 当前状态
reg [3:0] nxt_state; // 下一状态
reg [21:0] div_cnt; // 节拍计数器:0~250(START 态 0~500,RESTART 态 0~750)
reg [3:0] bit_cnt; // 位计数器:一个字节发到第几位(0~7)
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; // 读数据寄存器(MSB:LSB)
reg [7:0] pec_reg; // PEC 校验字节(读下来但本教程不校验)
reg [2:0] read_byte; // 读字节计数器:0/1/2 = LSB/MSB/PEC
//------------------------------------------------------------------
// 连续赋值
//------------------------------------------------------------------
// SDA 三态:主机要发的时候驱动,要收的时候放开成高阻
assign sda = sda_oe ? sda_out : 1'bz;
// SCL 直接输出内部生成的时钟
assign scl = scl_out;
// 读完成脉冲:NACK 发完、要进 STOP 的那一刻(此时 3 个字节已全部读齐)
assign read_done = (cur_state == NACK && nxt_state == STOP);
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
//------------------------------------------------------------------
// 状态机(组合逻辑):决定下一状态
//------------------------------------------------------------------
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) // 一个字节 8 位发完,去等 ACK
nxt_state = ACK;
else
nxt_state = WRITE;
end
ACK:begin
if(div_cnt == SCL_DIV && scl_out == 1) begin // SCL 高电平结束(从机已回 ACK)
if(write_byte == 0) // 发完写地址 0xB4,接着发命令字 0x07
nxt_state = WRITE;
else if(write_byte == 1) // 发完命令字,重复起始掉头去读
nxt_state = RESTART;
else // 发完读地址 0xB5,开始读数据
nxt_state = READ;
end
else
nxt_state = 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 == 0 && read_byte != 2) // 前两字节(LSB/MSB)读完,主机发 ACK
nxt_state = RACK;
else if(bit_cnt == 8 && scl_out == 0 && read_byte == 2) // 第三字节(PEC)读完,主机发 NACK
nxt_state = NACK;
else
nxt_state = READ;
end
RACK:begin
if(div_cnt == SCL_DIV && scl_out == 1) // 第 9 个时钟(ACK)走完,继续读下一字节
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
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~250 循环;START 态数到 500;RESTART 态数到 750
//------------------------------------------------------------------
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 && cur_state != RESTART) // 非起始态:数到 250 清零
div_cnt <= 'd0;
else if(cur_state != nxt_state) // 状态要切换:清零
div_cnt <= 'd0;
else if(cur_state == START || cur_state == RESTART) // 起始态:一直数(靠换状态清零)
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 || cur_state == RESTART)
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 时钟生成
//------------------------------------------------------------------
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 == RESTART && div_cnt == SCL_DIV) // 重复起始:先把 SCL 拉高
scl_out <= 1'b1;
else if(cur_state == RESTART && div_cnt == 2*SCL_DIV + SCL_DIV/2) // SDA 拉低后再把 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) // 数据/应答:数到 250 取反
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)
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 == RESTART) begin
if(div_cnt == SCL_DIV/2)
sda_out <= 1'b1; // 先确保 SDA 高(此时 SCL 还是低,允许变化)
else if(div_cnt == 2*SCL_DIV)
sda_out <= 1'b0; // 重复起始条件:SCL 高时 SDA 拉低
end
else if (cur_state == WRITE && div_cnt == SCL_DIV/2) begin
if (scl_out == 0 && write_byte == 0)
sda_out <= add_write[7-bit_cnt]; // 第 1 字节:从机写地址 0xB4
else if (scl_out == 0 && write_byte == 1)
sda_out <= cmd_tobj[7-bit_cnt]; // 第 2 字节:命令字 0x07(物体温度)
else if (scl_out == 0 && write_byte == 2)
sda_out <= add_read[7-bit_cnt]; // 第 3 字节:从机读地址 0xB5
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 引脚(读时是 MLX90614 在驱动 SDA)
// MLX90614 数据低字节先出:第 0 字节=LSB,第 1 字节=MSB,第 2 字节=PEC
// 注意:读完后 read_data_reg 必须保持住,不能清 0,
// 否则回到空闲态后顶层还没把串口发完,温度值就变成 0 了
//------------------------------------------------------------------
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n) begin
read_data_reg <= 0;
pec_reg <= 0;
end
else if (cur_state == READ && scl_out == 1) begin
if(div_cnt == SCL_DIV/2 && read_byte == 0)
read_data_reg[7-bit_cnt] <= sda; // 第 0 字节:低字节 LSB
else if(div_cnt == SCL_DIV/2 && read_byte == 1)
read_data_reg[15-bit_cnt] <= sda; // 第 1 字节:高字节 MSB
else if(div_cnt == SCL_DIV/2 && read_byte == 2)
pec_reg[7-bit_cnt] <= sda; // 第 2 字节:PEC 校验字节
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
`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
73
与 SHT20 驱动相比,状态机多了 RESTART 状态、去掉了 WAIT / STOP_for_waiting / 初始化逻辑(MLX90614 不需要等测量、也不需要初始化),读数据时低字节在前,各状态作用如下:
| 状态 | 作用 |
|---|---|
| IDLE | 空闲,等 measure_start 脉冲 |
| START | 产生起始条件 |
| WRITE | 发送一个字节的 8 位 |
| ACK | 第 9 个时钟,释放 SDA 等从机应答 |
| RESTART | 产生重复起始条件(写转读不释放总线) |
| READ | 读一个字节的 8 位 |
| RACK | 主机发 ACK(拉低 SDA),还要继续读 |
| NACK | 主机发 NACK(拉高 SDA),读完了 |
| STOP | 产生停止条件,回到空闲 |
6、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;
// ---- GY-906 的 I2C
IO_LOC "scl" R14;
IO_PORT "scl" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8 BANK_VCCIO=3.3;
IO_LOC "sda" T15;
IO_PORT "sda" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8 BANK_VCCIO=3.3;
// ---- UART 发送引脚
IO_LOC "uart_tx" F12;
IO_PORT "uart_tx" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8 BANK_VCCIO=3.3;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
此处需要杜邦线把 GY-906 模块和拓展排针相连,接线方式:
| GY-906 模块 | 开发板 |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SCL | R14 |
| SDA | T15 |
注意
接线时 VCC 建议接 3.3V。另外红外测温靠的是模块顶部的「视场角」:读数时尽量让传感器正对目标、距离 1~10cm 内,隔着玻璃或塑料罩读数会明显偏低(红外透不过去)。
7、程序下载
把程序综合、布局布线后烧录进开发板,打开串口助手,设置好参数:
- 波特率:115200
- 数据位:8
- 停止位:1
- 校验位:无
显示乱码的话
代码里的「温度」「℃」用的是 GBK 编码(0xCE 0xC2 = 「温」,0xB6 0xC8 = 「度」,0xA1 0xE6 = 「℃」)。串口助手如果按 UTF-8 解码就会显示乱码,把接收编码切换成 GBK / ANSI 即可正常显示。
正常情况下每秒打印一行温度,用手在传感器前晃动或捂住传感器,数值会跟着变化;对准一杯热水,能看到温度快速上升。
常见问题排查
- 一直显示 37.5℃ 不动:这是 raw 恒为
0xFFFF的典型症状——SDA 被上拉一直拉高,从机根本没吐数据。注意0xFFFF代入公式其实是 1037.55℃,拆位时万位被% 10丢掉、百位为 0 又发成空格,就被伪装成了 37.5℃。检查:SCL/SDA 是否接反、模块是否供电、杜邦线是否接触良好。 - 一直显示 -273.1℃:raw 读回来全 0,SDA 一直为低,检查 SDA 是否对地短路或与 GND 接反。
- 数值乱跳:红外测温对视场很敏感,附近有人走过、反光物体都会影响读数,属于正常现象;如果跳变剧烈则检查杜邦线接触。
- 读数比实际偏低:隔着空气层/外壳测温、距离太远、或目标表面是光亮金属(红外发射率低)都会偏低,尽量正对、近距离测量。
关于显示范围
拆位逻辑只按三位整数 + 一位小数设计,正确显示范围是 -99.9 ~ +999.9℃。超出这个范围(比如读到 0xFFFF 算出来的 1037.55℃)会丢掉高位数字,看起来就像一个正常的两位数温度——所以看到「永远是 37.5℃」要先怀疑总线没通,而不是传感器真测到 37.5℃。