最近项目需要使用FPGA读取DS18B20传感器的温度数据,通过阅读数据手册(DS18B20数据手册下载)发现该传感器使用的是1-wire bus协议,仅需一根数据线即可完成数据传输。此项目使用的DS18B20为TO-92封装,如下图所示:

1.One-Wire协议时序
单总线(One-Wire bus)是一种串行通信接口技术,由Maxim(Dallas)公司设计,DS18B20作为其典型应用,仅需一根数据线(DQ引脚)即可完成数据传输、设备寻址和指令控制等功能。单总线的核心时序包括三个部分:初始化时序,读时序和写时序。
1.1 初始化时序

数据手册中写明,初始化时序中包括主机发送的复位脉冲和DS18B20发送的应答脉冲。


结合文字和时序图一起看,主设备需要拉低总线至少480us,然后主设备释放总线,并进入接收模式。当总线被主设备释放后,总线会被上拉电阻拉高。当DS18B20检测到这个上升沿时,它会等待15-60us然后拉低60-240us总线来发送出一个应答脉冲。
1.2 写时序
写时序包括写0时隙和写1时隙,有些许不同。

主控制器通过写1时隙将逻辑1写到从设备,通过写0时隙将逻辑0写到从设备。所有的写时隙最少持续20us,各个写周期之间至少间隔1us。当主设备将总线从高拉低时,写时隙开始工作。从设备会在15-60us的窗口期间内对单总线采样。
1.2.1 写0时隙
主设备发出一个写0时隙,需要把数据线拉低并保持至少60us。

总结:
主设备持续拉低至少60us即可
1.2.2 写1时隙
主设备发出一个写1时隙,将单总线拉低后,主设备需要在15us内将总线释放。总线被释放后,上拉电阻会将总线拉高。

总结:
主设备拉低小于15us,然后释放,加起来总时间不少于60us即可
1.3 读时序
读时序包括读0时隙和读1时隙,对于主设备来说并无区别。


所有读时隙的持续时间至少为60us,两个读周期之间至少间隔1us。主设备将总线拉低至少1us,然后释放总线。主设备发出读时隙后,从设备通过拉高或拉低总线传输1或0。从设备输出的数据在主设备发出读时隙后的15us内有效。
总结:
主设备拉低总线1us,释放总线;从机占用总线,主机在15us内读取总线数据,总的读时隙时间大于60us即可。
2. DS18B20操作

使用DS18B20传感器,首先需要初始化,即上述所说的主设备发送复位脉冲,DS18B20发送应答脉冲,让主设备知道DS18B20已准备好运行;然后发送ROM操作指令,当总线上连接有多个设备时,可以通过ROM命令识别各个设备;最后发送功能指令。
由于此项目只搭载一个单总线设备,所以可以发送跳过ROM指令CCh,发送完成后可以直接发送功能指令,如此项目中用的温度转换44h和读取暂存器指令BEh。



DS18B20默认是12位,从表格中可知,tCONV即转换温度的等待时间,12位的时候最少要等待750ms。


BEh命令可以读9个字节,其中
byte0:温度低字节
byte1:温度高字节
byte2:TH报警
byte3:TL报警
byte4:配置寄存器
byte5:保留
byte6:保留
byte7:保留
byte8:CRC校验码
数据会从低字节开始传输,此项目只需要读取byte0和byte1字节,传输完两字节(16位)之后,主机发送复位脉冲即可。
总结:
1. 主设备拉低500us,然后释放总线;在570us时间对数据线进行采样,看DS18B20是否将数据线拉低;总时长达到1000us,初始化时序结束
2. 发送CCh 跳过ROM命令
3.发送44h 温度转换命令,然后等待超过750ms
4.再次执行1操作
5.再次执行2操作
6.发送BEh 读取暂存器
7.读取16位温度数据,低字节先行,将数据拼接
8.对温度数据进行转换,并通过串口输出
3. FPGA verilog代码
3.1 ds18b20_moore.v(三段Moore状态机)
cpp
module ds18b20_moore(
input clk, //50Mhz时钟
input rst_n, //复位信号
inout dq, //dq单总线双向端口
output reg[15:0] data //16位原始温度数据
);
// 状态定义
localparam RESET1 = 4'b0000, //第一次复位
WRITE_CC = 4'b0001, //写指令 0xCC(跳过ROM)
WRITE_44 = 4'b0011, //写指令 0x44(温度转换)
WAIT_CONV = 4'b0010, //等待温度转换完成
RESET2 = 4'b0110, //第二次复位
WRITE_CC2 = 4'b0111, //写指令 0xCC(跳过ROM)
WRITE_BE = 4'b0101, //写指令 0xBE(读暂存器)
READ_T = 4'b0100; //读取16位温度数据
// 时间参数(单位:us)
localparam T_INIT = 1000, //初始化总时长
T_WAIT = 780_000, //温度转换等待时间
T_BIT = 62, //单bit通信时长
T_ACK = 570, //应答信号采样时刻
T_READ = 10; //读数据采样点
localparam WR_CMD_CC = 8'hcc; //跳过ROM命令
localparam WR_CMD_44 = 8'h44; //温度转换命令
localparam WR_CMD_BE = 8'hbe; //读温度命令
reg [3:0] current_state; //现态
reg [3:0] next_state; //次态
reg [3:0] wr_bit_cnt; //写bit计数器
reg [4:0] rd_bit_cnt; //读bit计数器
reg reset_ack_valid; //复位应答标志
reg dq_en; //DQ输出使能
reg dq_out; //DQ输出值
reg [15:0] data_temp; //温度拼接
reg clk_us; //1Mhz时钟
reg [19:0] cnt_us; //微秒计数器
reg [4:0] cnt; //分频计数器
wire dq_in; //DQ输入采样
assign dq_in = dq;
assign dq = dq_en ? dq_out : 1'bz; //三态门
// 1MHz分频(生成1us时钟)
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
cnt <= 5'd0;
else if(cnt == 5'd24)
cnt <= 5'd0;
else
cnt <= cnt + 5'd1;
end
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
clk_us <= 1'b0;
else if(cnt == 5'd24)
clk_us <= ~clk_us;
end
// 现态
always @(posedge clk_us or negedge rst_n) begin
if(!rst_n)
current_state <= RESET1;
else
current_state <= next_state;
end
// 次态
always @(*) begin
next_state = RESET1;
case(current_state)
RESET1: next_state = (cnt_us == T_INIT && reset_ack_valid) ? WRITE_CC : RESET1;
WRITE_CC: next_state = (cnt_us == T_BIT && wr_bit_cnt == 7) ? WRITE_44 : WRITE_CC;
WRITE_44: next_state = (cnt_us == T_BIT && wr_bit_cnt == 7) ? WAIT_CONV : WRITE_44;
WAIT_CONV: next_state = (cnt_us == T_WAIT) ? RESET2 : WAIT_CONV;
RESET2: next_state = (cnt_us == T_INIT && reset_ack_valid) ? WRITE_CC2 : RESET2;
WRITE_CC2: next_state = (cnt_us == T_BIT && wr_bit_cnt == 7) ? WRITE_BE : WRITE_CC2;
WRITE_BE: next_state = (cnt_us == T_BIT && wr_bit_cnt == 7) ? READ_T : WRITE_BE;
READ_T: next_state = (cnt_us == T_BIT && rd_bit_cnt == 15) ? RESET1 : READ_T;
default: next_state = RESET1;
endcase
end
// 输出逻辑
always @(posedge clk_us or negedge rst_n)begin
if(!rst_n)begin
cnt_us <= 20'd0;
reset_ack_valid <= 1'b0;
dq_en <= 1'b0;
dq_out <= 1'b0;
data_temp <= 16'd0;
data <= 16'd0;
wr_bit_cnt <= 4'd0;
rd_bit_cnt <= 5'd0;
end
else begin
case(current_state)
//第一次复位:拉低500us ---> 释放总线 ---> 等待应答脉冲
RESET1: begin
cnt_us <= cnt_us + 1;
if(cnt_us <= 499) begin
dq_en <= 1;
dq_out <= 0;
end
else if(cnt_us == T_INIT) begin
cnt_us <= 0;
reset_ack_valid <= 0;
end
else begin
dq_en <= 0;
if(cnt_us == T_ACK && !dq_in)
reset_ack_valid <= 1;
end
end
//写指令0xCC
WRITE_CC: begin
cnt_us <= cnt_us + 20'd1;
if(cnt_us <= 1) begin
dq_en <= 1'b1;
dq_out <= 1'b0;
end
else if(cnt_us == T_BIT) begin
cnt_us <= 0;
dq_en <= 1'b0;
if(wr_bit_cnt == 7) // 等于7时清零
wr_bit_cnt <= 0;
else
wr_bit_cnt <= wr_bit_cnt + 1;
end
else begin
dq_en <= (WR_CMD_CC[wr_bit_cnt] == 1'b0) ? 1'b1 : 1'b0;
dq_out <= 1'b0;
end
end
//写指令0x44,温度转换
WRITE_44: begin
cnt_us <= cnt_us + 20'd1;
if(cnt_us <= 1) begin
dq_en <= 1'b1;
dq_out <= 1'b0;
end
else if(cnt_us == T_BIT) begin
cnt_us <= 0;
dq_en <= 1'b0;
if(wr_bit_cnt == 7) // 等于7时清零
wr_bit_cnt <= 0;
else
wr_bit_cnt <= wr_bit_cnt + 1;
end
else begin
dq_en <= (WR_CMD_44[wr_bit_cnt] == 1'b0) ? 1'b1 : 1'b0;
dq_out <= 1'b0;
end
end
//等待温度转换
WAIT_CONV: begin
if(cnt_us == T_WAIT)
cnt_us <= 20'd0;
else
cnt_us <= cnt_us + 20'd1;
end
//第二次复位
RESET2: begin
cnt_us <= cnt_us + 1;
if(cnt_us <= 499) begin
dq_en <= 1;
dq_out <= 0;
end
else if(cnt_us == T_INIT) begin
cnt_us <= 0;
reset_ack_valid <= 0;
end
else begin
dq_en <= 0;
if(cnt_us == T_ACK && !dq_in)
reset_ack_valid <= 1;
end
end
//再次写0xCC
WRITE_CC2: begin
cnt_us <= cnt_us + 20'd1;
if(cnt_us <= 1) begin
dq_en <= 1'b1;
dq_out <= 1'b0;
end
else if(cnt_us == T_BIT) begin
cnt_us <= 0;
dq_en <= 1'b0;
if(wr_bit_cnt == 7) // 等于7时清零
wr_bit_cnt <= 0;
else
wr_bit_cnt <= wr_bit_cnt + 1;
end
else begin
dq_en <= (WR_CMD_CC[wr_bit_cnt] == 1'b0) ? 1'b1 : 1'b0;
dq_out <= 1'b0;
end
end
//写指令0xBE,读暂存器
WRITE_BE: begin
cnt_us <= cnt_us + 20'd1;
if(cnt_us <= 1) begin
dq_en <= 1'b1;
dq_out <= 1'b0;
end
else if(cnt_us == T_BIT) begin
cnt_us <= 0;
dq_en <= 1'b0;
if(wr_bit_cnt == 7) // 等于7时清零
wr_bit_cnt <= 0;
else
wr_bit_cnt <= wr_bit_cnt + 1;
end
else begin
dq_en <= (WR_CMD_BE[wr_bit_cnt] == 1'b0) ? 1'b1 : 1'b0;
dq_out <= 1'b0;
end
end
//读16位温度
READ_T: begin
cnt_us <= cnt_us + 20'd1;
if(cnt_us <= 1) begin
dq_en <= 1'b1;
dq_out <= 1'b0;
end
else if(cnt_us == T_BIT) begin
cnt_us <= 20'd0;
dq_en <= 1'b0;
if(rd_bit_cnt == 15) begin
data <= data_temp;
rd_bit_cnt <= 0;
end
else begin
rd_bit_cnt <= rd_bit_cnt + 5'd1;
data <= data;
end
end
else begin
dq_en <= 1'b0;
if(cnt_us == T_READ)
data_temp <= {dq_in, data_temp[15:1]}; //移位读取
end
end
default: ;
endcase
end
end
endmodule
3.2 top_ds18b20_uart.v
cpp
`timescale 1ns / 1ps
// 功能:读取16位温度 → 解析正负、整数、小数 → 1秒发送一次温度字符串
module top_ds18b20_uart
#(
parameter CLK_FREQ = 50_000_000 // 50MHz时钟
)
(
input clk, // 系统时钟
input rst_n, // 复位
inout dq, // DS18B20单总线
output uart_tx // UART发送
);
wire [15:0] data; // DS18B20原始16位温度
//================ 温度解析 ====================
wire sign = data[15]; // 符号位(1=负,0=正)
wire [11:0] abs_temp = sign ? (~data[11:0] + 1'b1) : data[11:0]; // 绝对值
wire [7:0] int_part = abs_temp[11:4];// 整数部分
wire [3:0] frac = abs_temp[3:0];// 小数部分(4bit)
// 小数位转十进制 0.0~0.93
reg [3:0] dec_part1, dec_part2;
always @(*) begin
case(frac)
4'h0: begin dec_part1 = 4'd0; dec_part2 = 4'd0; end
4'h1: begin dec_part1 = 4'd0; dec_part2 = 4'd6; end
4'h2: begin dec_part1 = 4'd1; dec_part2 = 4'd2; end
4'h3: begin dec_part1 = 4'd1; dec_part2 = 4'd8; end
4'h4: begin dec_part1 = 4'd2; dec_part2 = 4'd5; end
4'h5: begin dec_part1 = 4'd3; dec_part2 = 4'd1; end
4'h6: begin dec_part1 = 4'd3; dec_part2 = 4'd7; end
4'h7: begin dec_part1 = 4'd4; dec_part2 = 4'd3; end
4'h8: begin dec_part1 = 4'd5; dec_part2 = 4'd0; end
4'h9: begin dec_part1 = 4'd5; dec_part2 = 4'd6; end
4'hA: begin dec_part1 = 4'd6; dec_part2 = 4'd2; end
4'hB: begin dec_part1 = 4'd6; dec_part2 = 4'd8; end
4'hC: begin dec_part1 = 4'd7; dec_part2 = 4'd5; end
4'hD: begin dec_part1 = 4'd8; dec_part2 = 4'd1; end
4'hE: begin dec_part1 = 4'd8; dec_part2 = 4'd7; end
4'hF: begin dec_part1 = 4'd9; dec_part2 = 4'd3; end
default: begin dec_part1 = 4'd0; dec_part2 = 4'd0; end
endcase
end
// 整数部分分解:百位、十位、个位
reg [3:0] int_hundreds, int_tens, int_ones;
always @(*) begin
int_hundreds = int_part / 100;
int_tens = (int_part % 100) / 10;
int_ones = int_part % 10;
end
//================ 1秒定时发送 ====================
reg [25:0] cnt_1s;
reg send_en;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt_1s <= 0;
send_en <= 0;
end else if(cnt_1s == 50000000 - 1) begin // 1秒到
cnt_1s <= 0;
send_en <= 1; // 启动发送
end else begin
cnt_1s <= cnt_1s + 1;
send_en <= 0;
end
end
//================ 串口发送控制(发送11个字符) ====================
reg [7:0] tx_data;
reg tx_flag;
wire tx_done;
reg [3:0] send_cnt;
reg tx_busy;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
send_cnt <= 0;
tx_data <= 0;
tx_flag <= 0;
tx_busy <= 0;
end else if(send_en && !tx_busy) begin // 1秒到,开始发送
tx_busy <= 1;
send_cnt <= 0;
end else if(tx_busy) begin // 正在发送
if(tx_flag) begin // 等待发送完成
if(tx_done) begin
tx_flag <= 0;
send_cnt <= send_cnt + 1; // 下一个字符
end
end else begin // 发送下一个字符
case(send_cnt)
0: begin tx_data <= sign ? 8'h2D : 8'h2B; tx_flag <= 1; end // +/-
1: begin tx_data <= int_hundreds + 8'h30; tx_flag <= 1; end // 百位
2: begin tx_data <= int_tens + 8'h30; tx_flag <= 1; end // 十位
3: begin tx_data <= int_ones + 8'h30; tx_flag <= 1; end // 个位
4: begin tx_data <= 8'h2E; tx_flag <= 1; end // 小数点 .
5: begin tx_data <= dec_part1 + 8'h30; tx_flag <= 1; end // 小数第一位
6: begin tx_data <= dec_part2 + 8'h30; tx_flag <= 1; end // 小数第二位
7: begin tx_data <= 8'h20; tx_flag <= 1; end // 空格
8: begin tx_data <= 8'h43; tx_flag <= 1; end // C
9: begin tx_data <= 8'h0D; tx_flag <= 1; end // 回车
10: begin tx_data <= 8'h0A; tx_flag <= 1; end // 换行
11: begin tx_busy <= 0; tx_flag <= 0; send_cnt <= 0; end // 结束
endcase
end
end
end
//================ DS18B20驱动 ====================
ds18b20_moore u_ds18b20 (
.clk (clk),
.rst_n (rst_n),
.dq (dq),
.data (data)
);
//================ UART发送模块 ====================
uart_send #(
.UART_BPS(115200),
.CLK_FREQ(50_000_000)
) u_uart_send (
.clk (clk),
.sys_rst_n (rst_n),
.pi_data (tx_data),
.pi_flag (tx_flag),
.tx (uart_tx),
.tx_done (tx_done)
);
endmodule
3.3 uart_send.v
cpp
module uart_send
#(
parameter UART_BPS = 115200, // 波特率
parameter CLK_FREQ = 50000000 // 系统时钟频率
)
(
input clk, // 系统时钟
input sys_rst_n, // 复位
input [7:0] pi_data, // 待发送数据
input pi_flag, // 发送使能
output reg tx, // UART发送引脚
output reg tx_done // 发送完成标志
);
// 波特率计数器最大值
localparam BAUD_CNT_MAX = CLK_FREQ / UART_BPS;
reg [15:0] baud_cnt; // 波特率计数器
reg [3:0] bit_cnt; // 数据位计数器
reg [7:0] data_buf; // 数据缓存
reg tx_en; // 发送使能
// 发送使能与数据锁存
always @(posedge clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
data_buf <= 8'd0;
tx_en <= 1'b0;
end
else if (pi_flag) begin // 收到发送请求
data_buf <= pi_data; // 锁存数据
tx_en <= 1'b1; // 启动发送
end
else if (tx_done) begin // 发送完成
tx_en <= 1'b0; // 关闭发送
end
end
// 波特率计数器
always @(posedge clk or negedge sys_rst_n) begin
if (!sys_rst_n)
baud_cnt <= 16'd0;
else if (tx_en) begin // 发送中
if (baud_cnt == BAUD_CNT_MAX - 1)
baud_cnt <= 16'd0;
else
baud_cnt <= baud_cnt + 1'b1;
end
else
baud_cnt <= 16'd0;
end
// 数据位计数器
always @(posedge clk or negedge sys_rst_n) begin
if (!sys_rst_n)
bit_cnt <= 4'd0;
else if (tx_en) begin // 发送中
if (baud_cnt == BAUD_CNT_MAX - 1) begin
if (bit_cnt == 4'd9)
bit_cnt <= 4'd0;
else
bit_cnt <= bit_cnt + 1'b1;
end
end
else
bit_cnt <= 4'd0;
end
// UART发送时序控制
always @(posedge clk or negedge sys_rst_n) begin
if (!sys_rst_n)
tx <= 1'b1; // 空闲高电平
else if (tx_en) begin
case (bit_cnt)
4'd0: tx <= 1'b0; // 起始位
4'd1: tx <= data_buf[0]; // 位0
4'd2: tx <= data_buf[1]; // 位1
4'd3: tx <= data_buf[2]; // 位2
4'd4: tx <= data_buf[3]; // 位3
4'd5: tx <= data_buf[4]; // 位4
4'd6: tx <= data_buf[5]; // 位5
4'd7: tx <= data_buf[6]; // 位6
4'd8: tx <= data_buf[7]; // 位7
4'd9: tx <= 1'b1; // 停止位
default: tx <= 1'b1;
endcase
end
else
tx <= 1'b1;
end
// 发送完成标志
always @(posedge clk or negedge sys_rst_n) begin
if (!sys_rst_n)
tx_done <= 1'b0;
else if (tx_en && (bit_cnt == 4'd9) && (baud_cnt == BAUD_CNT_MAX - 2))
tx_done <= 1'b1; // 发送完成
else
tx_done <= 1'b0;
end
endmodule
3.4 结果显示
将手指盖到DS18B20传感器上的效果

