1、实验目的
使用逻辑派Z1开发板(基于紫光同创 compa 系列 PGC4KD-6ILPG144 芯片)编写DS18B20温度传感器模块驱动代码,使用按键触发对环境温度的测量并使用串口向上位机发送当前测量到的温度。
2、实验原理
2.1、DS18B20模块介绍
DS18B20是一种数字温度传感器,支持单线通信协议,这意味着它只需要一根数据线与FPGA通讯仅需占用一个I/O端口,无须任何外部元件,直接将环境温度转化成数字信号,以数字码方式串行输出,从而大大简化了传感器与FPGA的接口设计。极大地简化了温度测量系统的连接复杂性。DS18B20具有唯一的64位ROM地址,允许在单根数据线上连接多个传感器,实现多点温度测量。此外,它还支持寄生电源模式,可以在数据线供电的情况下工作,进一步简化了系统设计。由于其可靠的性能、低功耗、高精度以及易于集成的特点,DS18B20被广泛应用于各种温度监控和数据采集系统中,如家庭自动化、工业控制和环境监测等。
本次实验使用的DS18B20模块图如下:
示意图:
DS18B20测温范围从-55°C到+125°C,在-10°C到+85°C的范围内,精度可达±0.5°C。现场(实时)温度直接以“单总线” 的数字方式传输,大大提高了系统的抗干扰性。它能直接读出被测温度,并且可根据实际要求通过简单的编程实现9~l2位的数字值读数方式。其芯片内部框图如下:
其中高速缓存器结构示意图如下:
DS18B20 的高速缓冲器是一个临时存储温度测量数据和配置参数的小型存储器,用于在温度转换和数据通信过程中缓存必要的信息。由上图可知缓冲器由 9 个字节组成,其中包括测量的温度数据(前两个字节)、配置寄存器(第四个字节)、以及内部保留位等。用户可通过配置寄存器设置分辨率,而其他字节中的特定位被保留用于内部用途,不可写入。在配置寄存器的8个比特位中,只有第5位(R0)与第6位(R1)可以由用户配置,其他位都保留给DS18B20内部使用。R0与R1的组合决定了传感器的分辨率,以及温度转换所需的时间。它们的作用可以理解为配置选项的二进制编码开关,通过不同的值组合实现分辨率的选择。详情请看下面的图片:
当用户不进行配置时,R1和R0的默认值为1,即采集到的温度信息使用12位分辨率。 当DS18B20配置为12位分辨率的温度数据时,(高速缓冲器的第0、1字节用于输出温度数据)其中输出的温度数据最高位为符号位,温度值实际占11位,最低4位表示小数部分。FPGA读取温度数据时,将会一次读取16位(2字节),从中提取低11位的二进制值,将其转换为十进制后乘以0.0625得到实际温度值。需要注意,温度的正负由前5位符号位决定,这5位同时变化,只需判断其中任意一位即可:若符号位为1,表示温度为负值,需对提取的二进制值取反加1后再乘以0.0625;若符号位为0,则直接乘以0.0625。
2.2、DS18B20通讯时序
1.DS18B20总线复位时序
控制器与DS18B20所有的通信都是由初始化开始的,初始化由主设备发出的复位脉冲及DS18B20响应的存在脉冲组成。当DS18B20响应复位信号的后,向主设备表明其在该总线上,并且已经做好了执行命令的准备。在这个过程中,总线上的主设备需要拉低总线最少 480us 来表示发送复位脉冲。发送完之后,主设备要释放总线进入接收模式。当总线释放后,上拉电阻将总线拉至高电平。当DS18B20检测到该上升沿信号后,其等待15us至60us后将总线拉低60us至240us来实现发送回复。其具体时序图如下:
2.DS18B20总线写时序
在写过程中写入数据有两种情况:写“1”和写“0”。写两种数据需要遵循不同的时序。当主设备将总线从高电平拉至低电平时,启动写操作,所有的写操作持续时间最少为60us,每个写操作间的恢复时间最少为1us。当总线(DQ)拉低后,DS18B20在15us至60us之间对总线进行采样,如果采的DQ为高电平则发生写1,如果为低电平则发生写0,如下图所示(图中的总线控制器即为主设备)。如果主机要写1,必须先将总线拉至逻辑低电平然后释放总线,允许总线在写操作开始后15us内上拉至高电平。若要写0,必须将总线拉至逻辑低电平并保持不变最少60us。
3.DS18B20总线读时序
同样的在读过程中也有有两种情况:读出“1”和读出“0”。每个读时序最小有 60us 的持续时间以及每个读时序之间有 1us 的恢复时间。当主设备将总线从高电平拉至低电平超过 1us,启动读操作。当启动读操作后,DS18B20 将会向主设备发送“0”或者“1”。DS18B20 通过将总线拉高来发送1,将总线拉低来发送0。当读时隙完成后,DQ 引脚将通过上拉电阻将总线拉高至高电平的闲置状态。从 DS18B20 中输出的数据在启动读时隙后的 15us 内有效,所以,主设备在读时隙开始后的 15us 内必须释放总线,并且对总线进行采样。其具体时序图如下:
2.3、DS18B20通讯过程
操作DS18B20需要三个步骤,分别是初始化、写ROM命令、写功能命令。其具体解释如下:
1.初始化
总线上的所有事件都必须以初始化为开始。初始化信号由总线上的主设备发出的复位脉冲以及紧跟着从设备回应的存在脉冲构成。该存在脉冲是让总线主设备知道 DS18B20 在总线上并准备好运行。具体的时序在19.2.2中DS18B20总线复位时序小节有具体介绍。
2.写ROM命令
在初始化完成后,可以执行ROM命令,这些命令用于操作每个设备的64位 ROM 编码,帮助主设备在总线上识别和管理多个从设备。ROM命令共有5种,每种命令长度均为8位,具体如下:
(1)搜索 ROM [F0h]
在系统上电初始化后,主设备使用该命令识别总线上的所有从设备及其 ROM 编码,从而确定从设备的类型和数量。
(2)读 ROM [33h]
该命令允许主设备读取DS18B20的64位ROM编码,仅在总线上只有一个DS18B20时可用。若总线上存在多个从设备,使用此命令将导致所有设备同时响应,从而引起数据冲突。
(3)匹配 ROM [55h]
此命令后接64位ROM编码,用于在多点总线中定位特定的DS18B20。只有编码与指定ROM完全匹配的设备会响应,其他设备将等待下一个复位脉冲。该命令可在单点或多点总线上使用。
(4)跳过 ROM [CCh]
该命令允许主设备跳过64位ROM编码直接执行下一步操作,适用于单点总线(只有一个DS18B20)的情况(本次实验就是此种情况),可节省时间。但在多点总线中,若发送跳过ROM命令后执行读操作,则所有从设备将同时响应,导致数据冲突。
(5)警报搜索 [ECh]
此命令功能类似跳过ROM,但只有温度超出报警阈值(高于TH或低于TL)的从设备会响应。报警状态会持续保留,直到温度回到正常范围或掉电为止。
3.写功能命令
当主设备通过 ROM 命令确认某个 DS18B20 可以通信后,便可向目标从设备发送功能命令,以执行特定操作。以下是 DS18B20 的功能命令及其作用:
(1)温度转换 [44h]
该命令用于启动单次温度转换,完成后,转换结果会存储在高速缓存器的byte0(温度低8位)和byte1(温度高8位)中,随后DS18B20进入低功耗闲置状态。若总线在命令后发出读时隙,DS18B20会返回:
"0":表示温度转换尚未完成。
"1":表示温度转换已完成。
寄生电源模式注意事项:在发送该命令后,必须立即强制拉高总线,且拉高时间需满足时序要求。
(2)写入暂存器 [4Eh]
此命令允许主设备向高速缓存器写入3个字节数据,按顺序写入以下寄存器:byte2(高温触发值):报警触发高温值。byte3(低温触发值):报警触发低温值。byte4(配置寄存器):分辨率配置等参数。数据写入按低位到高位顺序进行,可通过复位随时中断写入操作。
(3)读取高速缓存器 [BEh]
该命令从高速缓存器读取数据,读取从 byte0(温度低8位)开始,到byte8(CRC校验)结束。数据从低位开始传送,读取过程可通过复位随时终止。
(4)复制高速缓存器 [48h]
将高速缓存器中的高温触发值(byte2)、低温触发值(byte3)和配置寄存器(byte4)的值复制到非易失性存储器(EEPROM)中,若命令后主机发出读请求,DS18B20 会返回:
"0":表示复制操作正在进行。
"1":表示复制完成。
需要注意的是如果使用寄生电源模式,发送该命令后,必须立即强制拉高总线至少10ms。
(5)召回 EEPROM [B8h]
将 EEPROM 中存储的高温触发值(byte2)、低温触发值(byte3)和配置寄存器(byte4)的数据恢复到高速缓存器中。上电后,召回操作会自动执行一次,确保缓存器中有有效数据。若执行该命令后主机发出读请求,DS18B20 会返回:
"0":表示正在召回数据。
"1":表示召回完成。
(6)读取供电模式 [B4h]
该命令用于判断 DS18B20 的供电方式:返回 "0":表示使用寄生电源模式。返回 "1":表示使用外部电源模式。
3、代码设计
根据以上对DS18B20的介绍, 我们使用逻辑派Z1开发板对DS18B20进行初始化, 并写入将模块测量的环境温度解析出来,使用串口发送到上位机。
DS18B20驱动模块端口描述如下表:
具体代码如下:
`timescale 1ns / 1ps
module DS18B20_drive
(
input wire clk ,
input wire rst_n ,
input wire cap_flag /* synthesis PAP_MARK_DEBUG="true" */,//触发信号
inout wire dq /* synthesis PAP_MARK_DEBUG="true" */,//通讯总线
output reg [ 3: 0] s_bf ,//百分位
output reg [ 3: 0] s_sf ,//十分位
output reg [ 3: 0] s_sw ,//十位
output reg [ 3: 0] s_gw ,//个位
output reg [ 5: 0] data_symbol /* synthesis PAP_MARK_DEBUG="true" */,//指示温度符号
output reg data_vld /* synthesis PAP_MARK_DEBUG="true" */,//温度有效标志
output wire [ 19: 0] data_out /* synthesis PAP_MARK_DEBUG="true" */ //温度数据输出
);
parameter WAIT_TRI = 7'b0000_001,//等待触发
INIT = 7'b0000_010,//初始化
WR_CMD = 7'b0000_100,//写温度转换命令
WAIT = 7'b0001_000,//等待转换
INIT_AGAIN = 7'b0010_000,//初始化进入接收模式
RD_CMD = 7'b0100_000,//写温度读取命令
RD_TEMP = 7'b1000_000;//读温度
parameter WAIT_MAX = 20'd750_000; //12位分辨率,转换时间为750ms
//温度转换:初始化→跳过ROM→开始温度转换
//温度读取:初始化→跳过ROM→读暂存器→连续的读操作
//cc:跳过 ROM命令。
//44:温度转换命令,将转换结果会存储在16位高速缓存器
//be:读取高速缓存器命令,从高速缓存器读取数据
parameter WR_CC_44 = 16'h44_cc,//温度转换命令
WR_CC_BE = 16'hbe_cc;//温度读取命令
reg clk_us ;//1us时钟
reg [ 5: 0] cnt ;//1us计数器
reg [ 6: 0] state /* synthesis PAP_MARK_DEBUG="true" */;//状态
reg [ 19: 0] us_cnt /* synthesis PAP_MARK_DEBUG="true" */;//us计数器
reg flag ;//初始化成功信号
reg [ 3: 0] bit_cnt /* synthesis PAP_MARK_DEBUG="true" */;//bit计数器
reg [ 15: 0] data_temp ;//温度数据寄存器
reg [ 19: 0] data ;//无符号温度数据
reg dq_en ;//dq有效信号
reg dq_out ;//dq输出
reg [ 19: 0] temperature ;//实际温度数据
assign dq = (dq_en == 1'b1) ? dq_out : 1'bz;////使能1则输出,0则高阻态
assign data_out = temperature;//实际温度数据输出
// 脉冲扩展
reg [ 6: 0] counter ;
reg cap_start ;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cap_start <= 1'b0;
counter <= 0;
end else begin
if (cap_flag) begin
cap_start <= 1'b1;
counter <= 200;
end else if (counter > 0) begin
cap_start <= 1'b1; // 计数器递减,维持高电平
counter <= counter - 1;
end else begin
cap_start <= 1'b0; // 计数结束,输出恢复低电平
end
end
end
//us时钟生成,因为时序都是以us为单位,所以生成一个1us的时钟会比较方便
//50分频计数
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
cnt <= 5'd0;
else if(cnt == 5'd24) //每25个时钟500ns清零
cnt <= 5'd0;
else
cnt <= cnt + 1'd1;
end
//生成1us时钟
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
clk_us <= 1'b0;
else if(cnt == 5'd24) //每500ns
clk_us <= ~clk_us; //时钟反转
else
clk_us <= clk_us;
end
//us_cnt,1us 时钟计数器,用于状态跳转
always@(posedge clk_us or negedge rst_n)
if(rst_n == 1'b0)
us_cnt <= 20'd0;
else if(((state == INIT || state == INIT_AGAIN) && (us_cnt == 20'd959))
|| ((state == WR_CMD || state == RD_CMD || state == RD_TEMP) && (us_cnt == 20'd64))
|| ((state == WAIT && us_cnt == WAIT_MAX))||(state == WAIT_TRI ))//每当us_cnt计数到最大值,或在等待触发状态时,进行清零
us_cnt <= 20'd0;
else
us_cnt <= us_cnt + 1'b1;
//bit_cnt,bit 计数器,写 1bit 或读 1bit 加 1,一次写完之后清零
always@(posedge clk_us or negedge rst_n)
if(rst_n == 1'b0)
bit_cnt <= 4'd0;
else if((state == WR_CMD || state == RD_CMD || state == RD_TEMP)
&& (bit_cnt == 4'd15) && (us_cnt == 20'd64))//读\写0、1的周期变最少60us,我们定义读写周期为65us,当读\写完16bit数据后清零
bit_cnt <= 4'd0;
else if((state == WR_CMD || state == RD_CMD || state == RD_TEMP)//每读\写完1bit数据时,bit_cnt加1
&& (us_cnt == 20'd64))
bit_cnt <= bit_cnt + 1'b1;
//flag
always@(posedge clk_us or negedge rst_n)
if(rst_n == 1'b0)
flag <= 1'b0;
else if((state == INIT || state == INIT_AGAIN) && (us_cnt == 20'd570) && (dq == 1'b0))//初始化状态时,主机发送完低电平复位脉冲后,
flag <= 1'b1; //间隔最少60us后,接收到来自从机低电平回复,初始化完成flag拉高
else if(us_cnt == 20'd959)//初始化周期960us后,flag拉低。
flag <= 1'b0;
else
flag <= flag;
//state
always@(posedge clk_us or negedge rst_n)
if(rst_n == 1'b0)
state <= WAIT_TRI;
else
case(state)
WAIT_TRI : //等待触发采样
if(cap_start)//检测到触发信号,跳转到INIT
state <= INIT;
else
state <= WAIT_TRI;
INIT ://初始化
if(us_cnt == 20'd959 && flag == 1'b1)//初始化完成,跳转到WR_CMD
state <= WR_CMD;
else
state <= INIT;
WR_CMD ://写温度转换命令
if(bit_cnt == 4'd15 && us_cnt == 20'd64)//写完16位数据,跳转到WAIT
state <= WAIT;
else
state <= WR_CMD;
WAIT ://等待转换完成
if(us_cnt == WAIT_MAX)//等待完转换时间,跳转到RD_CMD
state <= INIT_AGAIN;
else
state <= WAIT;
INIT_AGAIN ://初始化
if(us_cnt == 20'd959 && flag == 1'b1)//初始化完成,跳转到RD_CMD
state <= RD_CMD;
else
state <= INIT_AGAIN;
RD_CMD ://写温度读取命令
if(bit_cnt == 4'd15 && us_cnt == 20'd64)//写完16位数据,跳转到RD_TEMP
state <= RD_TEMP;
else
state <= RD_CMD;
RD_TEMP ://读温度数据
if(bit_cnt == 4'd15 && us_cnt == 20'd64)//接收完16位数据,跳转到WAIT_TRI
state <= WAIT_TRI;
else
state <= RD_TEMP;
default:state <= WAIT_TRI;
endcase
always@(posedge clk_us or negedge rst_n)
if(rst_n == 1'b0)
begin
dq_en <= 1'b0;
dq_out <= 1'b0;
end
else
case(state)
WAIT_TRI :
begin
dq_en <= 1'b1;
dq_out <= 1'b1;
end
INIT :
if(us_cnt < 20'd499)
begin
dq_en <= 1'b1;
dq_out <= 1'b0;
end
else
begin
dq_en <= 1'b0;
dq_out <= 1'b0;
end
WR_CMD :
if(us_cnt > 20'd62)
begin
dq_en <= 1'b0;
dq_out <= 1'b0;
end
else if(us_cnt <= 20'd1)
begin
dq_en <= 1'b1;
dq_out <= 1'b0;
end
else if(WR_CC_44[bit_cnt] == 1'b0)
begin
dq_en <= 1'b1;
dq_out <= 1'b0;
end
else if(WR_CC_44[bit_cnt] == 1'b1)
begin
dq_en <= 1'b0;
dq_out <= 1'b0;
end
WAIT :
begin
dq_en <= 1'b1;
dq_out <= 1'b1;
end
INIT_AGAIN :
if(us_cnt < 20'd499)
begin
dq_en <= 1'b1;
dq_out <= 1'b0;
end
else
begin
dq_en <= 1'b0;
dq_out <= 1'b0;
end
RD_CMD :
if(us_cnt > 20'd62)
begin
dq_en <= 1'b0;
dq_out <= 1'b0;
end
else if(us_cnt <= 20'd1)
begin
dq_en <= 1'b1;
dq_out <= 1'b0;
end
else if(WR_CC_BE[bit_cnt] == 1'b0)
begin
dq_en <= 1'b1;
dq_out <= 1'b0;
end
else if(WR_CC_BE[bit_cnt] == 1'b1)
begin
dq_en <= 1'b0;
dq_out <= 1'b0;
end
RD_TEMP :
if(us_cnt <= 1)
begin
dq_en <= 1'b1;
dq_out <= 1'b0;
end
else
begin
dq_en <= 1'b0;
dq_out <= 1'b0;
end
default:
begin
dq_en <= 1'b0;
dq_out <= 1'b0;
end
endcase
//data_temp
always@(posedge clk_us or negedge rst_n)
if(rst_n == 1'b0)
data_temp <= 16'b0;
else if((state == RD_TEMP) && (us_cnt == 20'd13))//DS18B20输出的数据在启动读时隙后的15us内有效,我们在14us时,读取数据,通过位拼接的方式将16位数据储存
data_temp <= {dq,data_temp[15:1]};
else
data_temp <= data_temp;
//data
always@(posedge clk_us or negedge rst_n)
if(rst_n == 1'b0)begin
data <= 20'd0;
data_symbol <= 6'd10;
end
else if((state == RD_TEMP) && (bit_cnt == 4'd15) && (us_cnt == 20'd60)//当接收完16位数据
&& (data_temp[15] == 1'b0))begin //如果是正数直接输出
data <= data_temp[10:0];
data_symbol <= 6'd10; //ASCII "+"
end
else if((state == RD_TEMP) && (bit_cnt == 4'd15) && (us_cnt == 20'd60)
&& (data_temp[15] == 1'b1))begin //如果是负数
data <= ~data_temp[10:0] + 1;
data_symbol <= 6'd11; //ASCII "-"
end
reg rev_done ;
reg rev_done_dl ;
wire rev_done_fall ;
assign rev_done_fall = rev_done_dl&~rev_done;
always@(posedge clk_us or negedge rst_n)
if(rst_n == 1'b0)
rev_done <= 1'b0;
else if((state == RD_TEMP) && (bit_cnt == 4'd15) && (us_cnt == 20'd60))
rev_done <= 1'b1;
else
rev_done <= 1'b0;
always@(posedge clk or negedge rst_n)
if(rst_n == 1'b0)
rev_done_dl <= 1'b0;
else
rev_done_dl <= rev_done;
/*
温度数据显示保留2位小数
无符号温度数据data低4位表示实际温度的小数部分
data相当于把实际温度数据左移了4位,为了转换成实际的温度数据,需要对数据进行右移4位
实际温度数据 = data >> 4 = data / 16 = data * 0.0625
但为了方便显示,将小数点后两位数据显示出来,将整体数据*100
data * 0.0625 * 100 = data * 6.25 = (data * 625) / 100
所以temperature <= (data * 625) / 100
*/
always@(posedge clk_us or negedge rst_n)
if(rst_n == 1'b0)
temperature <= 20'b0;
else if(rev_done == 1'b1)
temperature <= (data * 625) / 100;
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
s_bf <= 0;
s_sf <= 0;
s_sw <= 0;
data_vld<= 1'b0;
end else begin
s_bf <= temperature % 10;
s_sf <= (temperature / 10) % 10;
s_gw <= (temperature / 100) % 10;
s_sw <= (temperature / 1000) % 10;
data_vld<= rev_done_fall;
end
end
endmodule
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
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
在这个模块我们执行的操作如下:等待顶层触发信号、向DS18B20发送初始化信号、向DS18B20发送跳过ROM命令[CCh]和温度转换命令[44h]、等待DS18B20温度转换完成、再次向DS18B20发送初始化信号、向DS18B20发送跳过ROM命令[CCh]和温度读取命令[BEh]、解析温度数据并重新回到初始状态等待顶层模块下一次触发温度采样过程。
因为我们使用的是默认配置,所以温度采样分辨率为12位,温度转换时间为750ms。模块核心逻辑是一个三段式状态机, 我们通过设计这个状态机来处理顶层触发信号与DS18B20的交互。状态机总共有七个状态:DS18B20驱动模块的状态机共有7个状态,每个状态的功能如下:
- WAIT_TRI: 在此状态下,等待顶层模块的触发信号cap_flag启动信号的到来。如果接收到启动信号,状态将转移到INIT。
- INIT: 初始化状态。在该状态中,按照总线时序发送发送初始化信号。如果初始化信号发送成功,且接收到DS18B20的应答,状态机将转移到WR_CMD状态。
- WR_CMD: 写命令状态。在此状态中,发送发送跳过ROM命令[CCh]和温度转换命令[44h]。发送完成后,状态机将转移到WAIT状态,等待DS18B20采集温度数据完成。
- WAIT: 等待状态。在此状态下,进行750ms的等待。DS18B20温度转换完成,当等待完成后状态机将转移到INIT_AGAIN状态。
- INIT_AGAIN: 重新初始化状态。发送初始化命令,如果初始化信号发送成功,且接收到DS18B20的应答,状态机将转移到RD_CMD状态。
- RD_CMD: 读命令状态。在此状态下,向DS18B20发送跳过ROM命令[CCh]和温度读取命令[BEh]。读取命令发送完成后,状态机将转移到RD_TEMP状态。
- RD_TEMP: 读取温度状态。在此状态中,处理DS18B20返回的温度数据。处理完成后,状态机将重新回到等待启动状态(WAIT_TRI)。
分析这个模块需要清楚的理解DS18B20的操作过程,若读者对DS18B20的操作时序不太清楚,请务必返回理论部分进行学习。
DS18B20实验例程顶层模块端口描述如下表:
模块代码如下:
`timescale 1ns / 1ps
module DS18B20_top(
input clk ,
input rst_n ,
input key_in ,
inout dq ,
output uart_txd
);
parameter CNT_MAX = 20'd999_999; //消抖计数器
parameter STR = 128'hb5_b1_c7_b0_bb_b7_be_b3_ce_c2_b6_c8_ce_aa_a3_ba; // "当前环境温度为:" 的 GB2312 编码
//信号定义
wire data_vld ;
wire [ 31: 0] temperature_data ;
wire uart_tx_busy ;
wire uart_tx_done ;
reg [ 7: 0] uart_tx_data ;
reg uart_tx_req ;
reg work_en ;
reg [ 6: 0] tx_byte_cnt ;
reg [ 19: 0] cnt_20ms ;//消抖计数器
reg key_flag ;
reg [ 7: 0] ascii_table [0:11] ;//储存阿拉伯数字0~9与符号+-的ASCII值
wire [ 3: 0] s_bf ;//百分位
wire [ 3: 0] s_sf ;//十分位
wire [ 3: 0] s_sw ;//十位
wire [ 3: 0] s_gw ;//个位
wire [5:0] data_symbol;
//cnt_20ms:如果时钟的上升沿检测到外部按键输入的值为低电平时,计数器开始计数
always@(posedge clk or negedge rst_n)
if(rst_n == 1'b0)
cnt_20ms <= 20'b0;
else if(key_in == 1'b1)
cnt_20ms <= 20'b0;
else if(cnt_20ms == CNT_MAX && key_in == 1'b0)
cnt_20ms <= cnt_20ms;
else
cnt_20ms <= cnt_20ms + 1'b1;
always@(posedge clk or negedge rst_n)
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;
always@(posedge clk or negedge rst_n)
if(rst_n == 1'b0)
work_en <= 1'b0;
else if(data_vld == 1'b1)
work_en <= 1'b1;
else if(tx_byte_cnt== 7'd23&&uart_tx_done)
work_en <= 1'b0;
else
work_en <= work_en;
//串口发送逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx_byte_cnt <= 7'b0;
end else if (work_en) begin
if (tx_byte_cnt == 7'd23&&uart_tx_done) // 总共发送 22 字节
tx_byte_cnt <= 7'b0;
else if(uart_tx_done)
tx_byte_cnt <= tx_byte_cnt + 1'b1;
end
end
// 串口发送数据控制
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
uart_tx_req <= 1'b0;
uart_tx_data <= 8'b0;
ascii_table[0] = 8'd48; //'0'
ascii_table[1] = 8'd49; //'1'
ascii_table[2] = 8'd50; //'2'
ascii_table[3] = 8'd51; //'3'
ascii_table[4] = 8'd52; //'4'
ascii_table[5] = 8'd53; //'5'
ascii_table[6] = 8'd54; //'6'
ascii_table[7] = 8'd55; //'7'
ascii_table[8] = 8'd56; //'8'
ascii_table[9] = 8'd57; //'9'
ascii_table[10] = 8'd43; //'+'
ascii_table[11] = 8'd45; //'-'
end else if (work_en && !uart_tx_busy) begin
case (tx_byte_cnt) // 根据当前字节计数器发送数据
7'd1: uart_tx_data <= STR[127:120]; // "当"
7'd2: uart_tx_data <= STR[119:112]; // "当"
7'd3: uart_tx_data <= STR[111:104]; // "前"
7'd4: uart_tx_data <= STR[103:96]; // "前"
7'd5: uart_tx_data <= STR[95:88]; // "环"
7'd6: uart_tx_data <= STR[87:80]; // "环"
7'd7: uart_tx_data <= STR[79:72]; // "境"
7'd8: uart_tx_data <= STR[71:64]; // "境"
7'd9: uart_tx_data <= STR[63:56]; // "温"
7'd10: uart_tx_data <= STR[55:48]; // "温"
7'd11: uart_tx_data <= STR[47:40]; // "度"
7'd12: uart_tx_data <= STR[39:32]; // "度"
7'd13: uart_tx_data <= STR[31:24]; // "为"
7'd14: uart_tx_data <= STR[23:16]; // "为"
7'd15: uart_tx_data <= STR[15:8]; // ":"
7'd16: uart_tx_data <= STR[7:0]; // ":"
7'd17: uart_tx_data <= ascii_table[data_symbol]; // 温度符号
7'd18: uart_tx_data <= ascii_table[s_sw]; // 温度十位
7'd19: uart_tx_data <= ascii_table[s_gw]; // 温度个位
7'd20: uart_tx_data <= 8'd46;//小数点
7'd21: uart_tx_data <= ascii_table[s_sf]; // 温度十分位
7'd22: uart_tx_data <= ascii_table[s_bf]; // 温度百分位
7'd23: uart_tx_data <= 8'h0A; // 换行符
default: uart_tx_data <= 8'b0;
endcase
uart_tx_req <= 1'b1; // 拉高请求信号
end else begin
uart_tx_req <= 1'b0; // 请求信号拉高仅一个时钟周期
end
end
DS18B20_drive u_DS18B20_drive(
.clk (clk ),
.rst_n (rst_n ),
.cap_flag (key_flag ),
.dq (dq ),
.s_bf (s_bf ), //百分位
.s_sf (s_sf ), //十分位
.s_sw (s_sw ), //十位
.s_gw (s_gw ), //个位
.data_symbol (data_symbol ), // 指示温度符号
.data_vld (data_vld ), // 温度有效标志
.data_out (data_out )
);
uart_tx u_uart_tx(
.clk (clk ),
.rst_n (rst_n ),
.uart_tx_req (uart_tx_req&work_en ),// 发送使能请求
.uart_tx_data (uart_tx_data ),// UART要发送的数据
.uart_txd (uart_txd ),// UART发送端口
.uart_tx_done (uart_tx_done ),//8bit数据发送完成
.uart_tx_busy (uart_tx_busy ) // 发送忙
);
endmodule
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
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
我们在顶层模块接收用户输入的按键信号key_in使用一个20ms计数器对其进行消抖产生DS18B20驱动模块的采样触发信号cap_flag。当data_vld有效时,将DS18B20模块解析得到温度数据十位、个位、十分位、百分位通过串口发送到上位机进行查看,汉字使用GB2312编码,模块中初始化了一个ascii_table用来储存阿拉伯数字0~9和符号“+ -”的ASCII值,将DS18B20采样得到的温湿度数据在ascii_table索引到对应的ASCII值进行发送。其中串口发送部分的知识在之前的章节已有介绍,读者若对这部分不熟悉,可到串口回环章节进行查看。
本实验约束代码如下:
define_attribute {p:dq} {PAP_IO_DIRECTION} {INOUT}
define_attribute {p:dq} {PAP_IO_LOC} {92}
define_attribute {p:dq} {PAP_IO_VCCIO} {1.2}
define_attribute {p:dq} {PAP_IO_STANDARD} {LVCMOS12}
define_attribute {p:dq} {PAP_IO_DRIVE} {2}
define_attribute {p:dq} {PAP_IO_NONE} {TRUE}
define_attribute {p:dq} {PAP_IO_SLEW} {SLOW}
define_attribute {p:uart_txd} {PAP_IO_DIRECTION} {OUTPUT}
define_attribute {p:uart_txd} {PAP_IO_LOC} {33}
define_attribute {p:uart_txd} {PAP_IO_VCCIO} {1.2}
define_attribute {p:uart_txd} {PAP_IO_STANDARD} {LVCMOS12}
define_attribute {p:uart_txd} {PAP_IO_DRIVE} {2}
define_attribute {p:uart_txd} {PAP_IO_NONE} {TRUE}
define_attribute {p:uart_txd} {PAP_IO_SLEW} {SLOW}
define_attribute {p:clk} {PAP_IO_DIRECTION} {INPUT}
define_attribute {p:clk} {PAP_IO_LOC} {5}
define_attribute {p:clk} {PAP_IO_VCCIO} {1.2}
define_attribute {p:clk} {PAP_IO_STANDARD} {LVCMOS12}
define_attribute {p:clk} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:key_in} {PAP_IO_DIRECTION} {INPUT}
define_attribute {p:key_in} {PAP_IO_LOC} {20}
define_attribute {p:key_in} {PAP_IO_VCCIO} {1.2}
define_attribute {p:key_in} {PAP_IO_STANDARD} {LVCMOS12}
define_attribute {p:key_in} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:rst_n} {PAP_IO_DIRECTION} {INPUT}
define_attribute {p:rst_n} {PAP_IO_LOC} {19}
define_attribute {p:rst_n} {PAP_IO_VCCIO} {1.2}
define_attribute {p:rst_n} {PAP_IO_STANDARD} {LVCMOS12}
define_attribute {p:rst_n} {PAP_IO_PULLUP} {TRUE}
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
27
28
29
4、代码仿真
由于本实验需要与DS18B20通过总线交互信息,在测试文件中编写DS18B20的应答逻辑比较麻烦,我们可以将DS18B20驱动模块中的关键信号打上debug标记,分析DS18B20驱动模块是否正确工作,同时检查DS18B20是否正确应答我们通过总线发送的信息。
我们将顶层模块发送拉低采样触发信号cap_flag、总线信号dq、温度符号指示data_symbol、温度有效标志data_vld、温度数据data_out、状态机信号state、微秒计数器us_cnt、接收数据比特计数器bit_cnt添加上debug标记进行分析。
首先我们设置触发模式为cap_flag = 1,按下按键捕获到波形,观察状态机是否正确响应触发信号。波形如下图所示,状态机正确跳转到初始化状态,对DS18B20进行初始化。
然后设置触发模式为(state = 7’b0000_010&&us_cnt = 959)(状态机处于DS18B20初始化状态,接收响应完成),DS18B20总线复位时序中我们有说明,一整个复位操作包括主机发送复位与从机应答总共960us,我们以此作为条件观察复位是否成功,按下按键观察捕获到的波形如下:
由上图可知当us_cnt = 959时,状态机跳转到下一状态,同时总线拉低,符合我们的复位时序。
接下来将触发模式设置为(state = 7’b0000_100&&bit_cnt = 15&&us_cnt =64)(状态机发送16bit命令状态,并且发送完成),捕获到的波形图如下图:
由上图可知在发送完成16bit命令[CCh][44h]之后状态机跳转到等待状态,等待DS18B20进行温度转换,共等待750ms,此时总线拉高。由此可见状态跳转逻辑正确。接下来就是再次进行初始化,发送命令[CCh][BEh],状态机跳转到接收温度数据状态,开始读数据。我们将触发模式设置为(state = 7’b1000_000&&bit_cnt = 15&&us_cnt =64)(状态机接收温度数据,并且接收完成),捕获到的波形图如下:
在这张图中,状态机接收数据完成,并计算出当前温度为17.18度,同时data_vld信号拉高,表示当前温度数据解析完成,数据有效。之后状态机跳转到等待触发状态,等待顶层触发信号再次到来。由此可见,模块功能正确。
5、实验现象
将DS18B20模块按照约束文件与板卡进行连接后,将程序下载到开发板,使用USB转TTL的转换器连接开发板与电脑,打开串口助手,按下开发板按键k0,等待温度采样完成之后,开发板将会返回当前环境温度的测量值。具体现象如下: