目录
前言
DS18B20数字温度传感器提供
9-Bit到12-Bit
的摄氏温度测量精度和一个用户可编程的非易失性且具有过温和低温触发报警的报警功能。
DS18B20采用的1-Wire(单总线)1通信即仅采用一个数据线(以及地)与微控制器进行通信。
该传感器的温度检测范围为-55℃至+125℃,并且在温度范围超过-10℃至85℃之外时还具有±0.5℃的精度。此外,DS18B20可以直接由数据线供电而不需要外部电源供电。每片DS18B20都有一个独一无二的64位序列号,所以一个1-Wire总线上可连接多个DS18B20设备。因此,在一个分布式的大环境里用一个微控制器控制多个DS18B20是非常简单的。这些特征使得其在HVAC环境控制,在建筑、设备及机械的温度监控系统,以及温度过程控制系统中有着很大的优势。
一丶看懂DS18B20数据手册
我们将对照原味的“ 英文手册 ”来分析
实在看不懂还可以戳这里中文手册传送门!!!
1.DS18B20内部结构
全英文,算了摆烂了。。。😭
别急,原子哥给出了中文版结构图,这样对照起来一目了然👍
DS18B20的内部结构主要由8部分组成:
- 64位ROM和单线接口
- 存储器和控制逻辑
- 高速缓存器
- 温度传感器
- 配置寄存器
- 高温触发器TH
- 低温触发器TL
- 8位CRC生成器
那么,FPGA需要怎样控制才能让DS18B20工作并将温度数据读取出来呢???
2.DS18B20的命令
温馨提示:命令解释有点繁琐,我们可以把此部分当作资料来查看,现在也可以直接去看命令用法
①ROM功能命令
ROM功能命令就是操作这一块“ 只读 ”的存储器
64位ROM的具体内容如下图:
64位ROM中的序列号是出厂前被光刻好的,它可以看做该DS18B20的地址序列码。
其各位排列顺序是:开始8位为产品类型标号,接下来48位是该DS18B20自身的序列号,最后8位是前面56位的CRC循环冗余校验码(CRC=X8+X5+X4+1)
光刻ROM的作用是使每一个DS18B20都各不相同,这样就可以实现一条总线上挂接多个DS18B20的目的。与此对应的命令有如下5条:
-
33H
:读ROM
这个命令允许总线控制器(FPGA)读取DS18B20的8位系列编码、唯一的序列号和8位CRC码。
只有在总线上存在单只DS18B20的时候才能使用该命令。 -
55H
:匹配ROM
发出此命令之后,接着发出64位ROM编码,让总线控制器(FPGA)在多点总线上定位一只特定的DS18B20。只有和64位ROM序列完全匹配的DS18B20才会做出响应。所有和64位ROM序列不匹配的DS18B20都将等待复位脉冲。这条命令在总线上有单个或多个器件时都可以使用。 -
CCH
:跳过ROM
这条命令允许总线控制器(FPGA)不用提供64位ROM编码就进行下一步操作,在单点总线(一个DS18B20)情况下可以节省时间。如果总线上不止一个从机,在跳过ROM命令之后跟着发一条读命令,由于多个从机同时传送信号,总线上就会发生数据冲突(漏极开路上拉效果相当
于相与)。 -
F0H
:搜索ROM
当一个系统初次启动时,总线控制器可能并不知道总线上有多少器件或它们的64位ROM编码。搜索ROM命令允许总线控制器用排除法识别总线上的所有DS18B20的64位编码。 -
ECH
:报警搜索命令
发出此命令后,只有温度超过设定值上限或下限的DS18B20才做出响应。要注意的是只要
DS18B20不掉电,报警状态将一直保持,直到再一次测得的温度值达不到报警条件为止。
②RAM功能命令
RAM功能命令是操作RAM随机存取存储器,我们这里的RAM主要指高速缓存器,来看一下它的数据结构
由上图可知DS18B20的高速缓存器共有9个8位寄存器,其中温度数据低位(LSB)对应字节地址0,温度数据高位(MSB)对应字节地址1,以此类推,配置寄存器的字节地址为4。温度数据存放的格式如下图:
分析上图:
①DS18B20在出厂时默认配置温度数据为12位,其中最高位为符号位,即温度值共11位,最低四位为小数位。
②FPGA在读取温度数据时,一次会读2字节共16位,读完后将低11位的二进制数转化为十进制数后再乘以0.0625得到所测的实际温度值。
③另外还需要判断温度的正负,前5个数字为符号位,这5位同时变化,我们只需要判断其中任何一位就可以了。前5位为1时,读取的温度为负值,则测到的数值需要取反加1再乘以0.0625才可得到实际温度值。前5位为0时,读取的温度为正值,只要将测得的数值乘以0.0625即可得到实际温度值。
DS18B20具有6条对RAM的命令:
-
44H
:温度转换
这条命令启动一次温度转换。温度转换命令被执行,而后DS18B20保持等待状态。如果总线控制器在这条命令之后跟着发出读时间隙,而DS18B20又忙于做时间转换的话,DS18B20将在总线上输出“0”,若温度转换完成,则输出“1”。如果使用寄生电源,总线控制器必须在发出这条命令后立即启动强上拉,并保持500ms。转换结果的低位存入BYTE0、高位存BYTE1。 -
BEH
:读高速缓存器
这个命令读取暂存器的内容。读取将从BYTE0开始,一直进行下去,直到第9个字节(BYTE8,CRC)读完。如果不想读完所有字节,控制器可以在任何时间发出复位命令来终止读取。另外需要注意的是,字节内容都是最低位先传送。 -
4EH
:写暂存器
这个命令向DS18B20的高速缓存器中写入数据,开始位置在地址2。接下来写入的两个字节将被存到高速缓存器的字节地址位2和3的高、低温触发器。可以在任何时刻发出复位命令来终止写入。 -
48H
:复制高速缓存器
这条命令把高速缓存器的内容拷贝到DS18B20的E2PROM存储器中,即把温度报警触发字节存入非易失性存储器里。如果总线控制器在这条命令之后跟着发出读时间隙,而DS18B20又正在忙于把暂存器拷贝到E2PROM存储器,DS18B20就会输出一个“0”,如果拷贝结束的话,DS18B20则输出“1”。如果使用寄生电源,总线控制器必须在这条命令发出后立即起动强上拉并最少保持10ms。 -
B8H
:重调E2PROM
这条命令把E2PROM里的值拷回高速缓存器。这种拷回操作在DS18B20上电时自动执行,这样器件一上电高速缓存器里马上就存在有效的数据了。若在这条命令发出之后发出读时间隙,器件会输出温度转换忙的标识:“0”=忙,“1”=完成。 -
B4H
:读供电方式
读DS18B20的供电方式。若把这条命令发给DS18B20后发出读时间隙,器件会返回它的电源模式:“0”=寄生电源,“1”=外部电源。
3.命令用法
阅读数据手册:
大致意思就是访问这个温度传感器有三个步骤:
1.初始化时序
2.使用ROM命令匹配总线上相应的传感器
3.使用RAM命令读取温度,最后回到初始化时序
下面介绍以上几条指令的用法。当主机(FPGA)需要对众多在线DS18B20中的某一个进行操作时:
首先应将主机逐个与DS18B20挂接,读出其序列号;然后再将所有的DS18B20挂接到总线上,主机发出匹配ROM命令(55H)之后,主机紧接着提供的64位ROM编码(包括该DS18B20的48位序列号)
之后的操作就是针对该DS18B20的RAM命令。
如果主机只对一个DS18B20进行操作,就不需要读取ROM编码以及匹配ROM编码,只要用跳过ROM(CCH命令),就可进行下一步对高速缓存器的操作。
主机发出对ROM的操作命令之后,就进一步发出对RAM的命令。这里的RAM主要是指高速缓存器
整个DS18B20的温度读取过程为:
初始化➔发跳过ROM命令(CCH)➔发开始转换命令(44H)➔延时➔初始化➔发送跳过ROM命令(CCH)➔发读存储器命令(BEH)➔连续读出两个字节数据(即温度)➔结束或开始下一循环。
4.初始化
初始化分为3个部分:
主机(FPGA)发送复位脉冲→主机释放总线→DS18B20发出存在脉冲(FPGA接收存在脉冲)
初始化序列见下图。:
一个复位脉冲跟着一个存在脉冲表明DS18B20已经准备好发送和接收数据(适当的ROM命令和RAM操作命令)。主机输出低电平,保持低电平时间至少480us,以产生复位脉冲。接着主机释放总线,4.7K的上拉电阻将单总线拉高,延时15~60us,此时DS18B20拉低总线60~240us,以产生低电平存在脉冲以应答主机。
5.读写时隙
初始化完成之后,主机就可以向从机读写数据。
读写数据涉及到读写时隙的概念。在单总线通信协议中,读写时隙的概念十分重要,当主机向从机输出数据时产生写时隙,当主机从从机读取数据时产生读时隙,每一个时隙总线只能传输一位数据。
无论是在读时隙还是写时隙,它们都以主机拉低数据线开始,数据线的下降沿使从设备触发其内部的延时电路,使之与主机同步。在写时隙内,该延迟电路决定从设备采样数据线的时间延迟。
①写时隙
单总线通信协议中写时隙有两种:写1和写0。
主机采用写1时隙向从机写入1,而采用写0时隙向从机写入0。所有写时隙至少要60us,且在两次独立的写时隙之间至少需要1us的恢复时
间。
两种写时隙均起始于主机拉低数据总线。
产生写0时隙的方式:在主机拉低数据线后,只需要在整个时隙间保持低电平即可(至少60us)。
产生1时隙的方式:主机拉低总线后,接着必须在15us之内释放总线,由上拉电阻将总线拉至高电平;在写时隙开始后15us~60us期间,单总线器件采样总电平状态。如果在此期间采样值为高电平,则逻辑1写入器件;如果为0,写入逻辑0,时隙图如下图所示:
②读时隙
对于读时隙,单总线器件仅在主机发出读数据命令后,才向主机传输数据。
主机发出读数据命令后,必须马上产生读时隙,以便从机能够传输数据。所有读时隙至少需要60us,且在两次独立的读时隙之间至少需要1us的恢复时间。
每个读时隙都由主机发起,至少拉低总线1us。
在主机发出读时隙之后,单总线器件才开始在总线上发送0或1。
若从机发送1,则保持总线为高电平;
若发出0,则拉低总线。
当发送0时,从机在读时隙结束后释放总线,由上拉电阻将总线拉回至空闲高电平状态。
从机发出的数据在起始时隙之后,保持有效时间15us,因此主机在
读时隙期间必须释放总线,并且在时隙起始后的15us之内采样总线状态,时隙图如下图所示:
二丶分析实验任务
实验任务:
1.使用状态机编写温度传感器驱动
2.在数码管上实时显示温度
1.状态机
首先我们分析温度传感器驱动需要哪些状态
我们访问此传感器并获取温度
首先需要进行①初始化,而初始化又包括发送复位脉冲,释放总线,发送存在脉冲
然后②发送跳过ROM命令,发送此命令之后就能访RAM问高速缓存器了,而温度值就存储在里面
之后③发送温度转换命令,手册中说明了默认配置温度数据为12位,查表可知,转换12位的温度数据需要最多750ms,所以我们在发送了温度转换命令之后需要延时750ms
④发送读取温度指令,也就是读高速缓存器命令,从低位开始读
⑤读取温度值,温度数据存储在 byte0 和byte1,所以我们只需要读取这两个字节的数据即可
注意:发送指令以及读取温度数据存在 读写数据操作,这就需要用到读写时隙,所以上面列出的步骤中省略的 产生读写时隙可以作为” 从状态机” ,而总的步骤作为“ 主状态机 ”
DS18B20 驱动模块主状态机
- M_IDLE:空闲状态,等待开始通信;
- M_RST:发送复位脉冲;
- M_REL:释放总线;
- M_RACK:接收存在脉冲;
- M_RSKP:发送跳过 ROM 指令;
- M_SCON:发送温度转换命令;
- M_WAIT:等待 750ms;
- M_SRTM:发送温度读取指令;
- M_RTMP:读取温度值;
DS18B20 驱动模块从状态机
- S_IDLE:空闲状态,等待传输请求;
- S_LOW:发数据前先拉低 1us;
- S_SEND:发送 1bit 数据;
- S_SAMP:接收 1bit 数据;
- S_RELE:释放总线;
- S_DONE:发送/接收一次数据完成;
从状态机思路:
我们这里将从状态机分为两类,即写时隙和读时隙
写时隙:写数据时主机向从机发数据
- 首先进入S_LOW状态,拉低总线,保持(>=1us)
- 然后进入S_SEND状态,由主机发送数据,单总线器件采样总电平状态(15us~60us)
- 最后,进入S_RELE状态,主机释放总线(>=1us)
- 释放完成,进入S_DONE状态
读时隙:读数据时从机向主机发送数据
- 首先进入S_LOW状态,拉低总线,保持(>=1us)
- 然后进入S_SAMP状态,由从机发送数据(主机接收数据),主机采样数据(<=15us)
- 最后,进入S_RELE状态,主机释放总线(>=1us)
- 释放完成,进入S_DONE状态
如下图所示我们综合出来的从状态机转移图
2.温度解码
DS18B20在出厂时默认配置温度数据为12位,其中最高位为符号位,即温度值共11位,最低四位为小数位。
FPGA在读取温度数据时,一次会读2字节共16位,读完后将低11位的二进制数转化为十进制数后再乘以0.0625得到所测的实际温度值。
另外还需要判断温度的正负,前5个数字为符号位,这5位同时变化,我们只需要判断其中任何一位就可以了。
前5位为1时,读取的温度为负值,则测到的数值需要取反加1再乘以0.0625才可得到实际温度值。前5位为0时,读取的温度为正值,只要将测得的数值乘以0.0625即可得到实际温度值。
3.模块原理图
三丶代码设计
1.顶层模块
注意:因为我们使用单总线通信,这里dq端口既是输入又是输出
处理方式:使用三态门
assign dq_in = dq;
assign dq = dq_out_en?dq_out:1'bz;
- dq:单总线
- en:主机发送使能
- dq_out:在en使能的时候,将dq_out的数据通过dq总线发送到从机
- dq_in:在en使能关闭的时候,将从机通过dq总线发送的数据输入到主机
module temp_detect (
input clk ,
input rst_n ,
inout dq , //传感器总线---单总线
output [5:0] sel , //数码管位选
output [7:0] seg //数码管段选
);
wire dq_out ;
wire dq_in ;
wire dq_out_en ;
wire temp_sign ; //温度正负
wire [23:0] temp_out ; //温度值
wire temp_out_vld ; //温度值有效
wire [23:0] dout ;
wire dout_vld ;
assign dq = dq_out_en ? dq_out : 1'bz; //如果输出使能,则将dq_out的值赋给dq输出,反之则为高阻态输出
assign dq_in=dq; //接收dq输入信号
//例化模块
ds18b20_driver u_ds18b20_driver(
.clk (clk ),
.rst_n (rst_n ),
.dq_in (dq_in ),
.dq_out (dq_out ),
.dq_out_en (dq_out_en ),
.temp_out (temp_out ),
.temp_sign (temp_sign ),
.temp_out_vld (temp_out_vld)
);
control u_control(
.clk (clk ),
.rst_n (rst_n ),
.temp_out (temp_out ),
.temp_sign (temp_sign ),
.temp_out_vld (temp_out_vld),
.dout (dout ),
.dout_vld (dout_vld )
);
seg_driver u_seg_driver(
.clk (clk ),
.rst_n (rst_n ),
.temp_sign (temp_sign ),
.dout (dout ),
.dout_vld (dout_vld ),
.sel (sel ),
.seg (seg )
);
endmodule //temp_detect
2.DS18B20驱动
代码量有亿点点大😥
我们梳理一下大纲
整个驱动分为三个模块
总的来说
- 设置了一个1微妙的计数器作为基础单位,因为我们通信中的延时都是1微妙的倍数;
- 一个主状态机计数器,控制各个状态的延时时间;
- 一个从状态机计数器,控制读写时隙各个状态的延时时间;
- 一个比特计数器,因为主机与从机之间的通信是用命令来控制的,而这些命令都是8位的2进制数,读取温度的时候又需要读取两个字节(16位)的数据,所以需要一个比特计数器来控制读写的数据位数
①主状态机模块
宏观上来设置主机与从机之间通过单总线的通信过程,忽略收发数据的细节(也就是读写时隙),大致又分为两步:1.首先向从机发送温度转换命令;2.之后发送读取温度命令。
②从状态机模块
从微观上来描述主机发送指令或者主机读取温度产生的读写时隙,其中写时隙:首先从空闲状态进入拉低总线状态(代表开始读写了),然后发送1bit数据,之后释放总线,最后确定写数据是否结束,就看比特计数器是否记满8bit;
读时隙:首先从空闲状态进入拉低总线状态(代表开始读写了),然后读取1bit数据,之后释放总线,最后确定读数据是否结束,就看比特计数器是否记满16bit;
③温度转换模块 这个模块就比较简单了,主要任务就是输出我们与温度传感器通信之后得到的正确温度数据
module ds18b20_driver (
input clk ,
input rst_n ,
input dq_in ,
output reg dq_out ,
output reg dq_out_en ,
output reg [23:0] temp_out ,
output reg temp_sign ,
output reg temp_out_vld
);
//主状态机参数
localparam
M_IDLE = 9'b000_000_001 , //空闲状态,等待开始通信
M_REST = 9'b000_000_010 , //发送复位脉冲
M_RELE = 9'b000_000_100 , //释放总线
M_RACK = 9'b000_001_000 , //接收存在脉冲
M_RSKP = 9'b000_010_000 , //发送跳过 ROM 指令
M_CONT = 9'b000_100_000 , //发送温度转换命令
M_WAIT = 9'b001_000_000 , //等待 750ms
M_RCMD = 9'b010_000_000 , //发送温度读取指令
M_RTMP = 9'b100_000_000 ; //读取温度值
//从状态机参数
localparam
S_IDLE = 6'b000_001 , //空闲状态,等待传输请求
S_LOW = 6'b000_010 , //发数据前先拉低 1us
S_SEND = 6'b000_100 , //发送 1bit 数据
S_SAMP = 6'b001_000 , //接收 1bit 数据
S_RELE = 6'b010_000 , //释放总线
S_DONE = 6'b100_000 ; //发送/接收一次数据完成
//定义需要的命令
localparam
CMD_RSKP = 8'hCC, //跳过ROM指令
CMD_CONT = 8'h44, //温度转换
CMD_RTMP = 8'hBE; //读暂存器
//定义常用的时间
parameter
TIME_1US = 50, //1微秒 --- 基本单位
//主状态机延时
TIME_RST = 500, //复位脉冲 500us (480~690)
TIME_REL = 20, //主机释放总线 20us (15~60)
TIME_PRE = 200, //主机接收存在脉冲 200us (60~240)
TIME_WAIT = 750000, //主机发完温度转换命令 等待750ms
//从状态机的延时
TIME_LOW = 2, //主机拉低总线 2us (>=1)
TIME_RW = 60, //主机读、写1bit 60us (>=60)
TIME_REC = 3; //主机读写完1bit释放总线 3us (>=1)
//定义状态机
reg [8:0] m_state_c; //主现态
reg [8:0] m_state_n; //主次态
reg [5:0] s_state_c; //Slave--从机
reg [5:0] s_state_n;
//定义计数器
reg [19:0] m_cnt; //复位脉冲,释放总线,存在脉冲,温度转换
wire m_add_cnt;
wire m_end_cnt;
reg [19:0] X; //控制主状态机各个状态计数的最大值
reg [5:0] s_cnt; //从状态机各个状态的时间
wire s_add_cnt;
wire s_end_cnt;
reg [5:0] Y; //控制从状态机各个状态计数的最大值
reg [5:0] cnt_1us; //1us计数器
wire add_cnt_1us;
wire end_cnt_1us;
reg [4:0] cnt_bit; //bit计数器 --- 计数发送数据的bit数
wire add_cnt_bit;
wire end_cnt_bit;
reg [7:0] cmd_r; //寄存待发送指令
reg slave_ack; //接收存在脉冲
reg [15:0] temp_out_r; //寄存读取的温度
reg flag; //0:发温度转换命令 1:发温度读取命令
reg [10:0] temp_data; //对补码进行操作生成原码
wire [23:0] temp_data_r;//解码之后的实际温度值
//定义状态转移条件
wire m_idle2m_rest; //发送复位脉冲
wire m_rest2m_rele; //释放总线
wire m_rele2m_rack; //接收存在脉冲 --- 由从机通过dq总线发送
wire m_rack2m_rskp; //发送跳过 ROM 指令
wire m_rskp2m_cont; //发送温度转换命令
wire m_rskp2m_rcmd; //发送温度读取指令
wire m_cont2m_wait; //发送温度转换命令
wire m_wait2m_rest; //发送复位脉冲
wire m_rcmd2m_rtmp; //读取温度值
wire m_rtmp2m_idle; //返回空闲状态
wire s_idle2s_low ; //拉低总线 --- 读写数据前
wire s_low2s_send ; //写数据 --- 主机向从机发数据
wire s_low2s_samp ; //读数据 --- 从机向主机发数据
wire s_send2s_rele; //释放总线
wire s_samp2s_rele; //释放总线
wire s_rele2s_low ; //拉低总线 --- 继续写数据或者读数据
wire s_rele2s_done; //读写一次数据结束
//主状态机
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
m_state_c<=M_IDLE;
end
else
m_state_c<=m_state_n;
end
always @(*) begin
case(m_state_c)
M_IDLE:begin
if(m_idle2m_rest)
m_state_n = M_REST;
else
m_state_n = m_state_c;
end
M_REST:begin
if(m_rest2m_rele)
m_state_n = M_RELE;
else
m_state_n = m_state_c;
end
M_RELE:begin
if(m_rele2m_rack)
m_state_n = M_RACK;
else
m_state_n = m_state_c;
end
M_RACK:begin
if(m_rack2m_rskp)
m_state_n = M_RSKP;
else
m_state_n = m_state_c;
end
M_RSKP:begin
if(m_rskp2m_cont)
m_state_n = M_CONT;
else if(m_rskp2m_rcmd)
m_state_n = M_RCMD;
else
m_state_n = m_state_c;
end
M_CONT:begin
if(m_cont2m_wait)
m_state_n = M_WAIT;
else
m_state_n = m_state_c;
end
M_WAIT:begin
if(m_wait2m_rest)
m_state_n = M_REST;
else
m_state_n = m_state_c;
end
M_RCMD:begin
if(m_rcmd2m_rtmp)
m_state_n = M_RTMP;
else
m_state_n = m_state_c;
end
M_RTMP:begin
if(m_rtmp2m_idle)
m_state_n = M_IDLE;
else
m_state_n = m_state_c;
end
default:m_state_n = M_IDLE;
endcase
end
assign m_idle2m_rest = m_state_c == M_IDLE && (1'b1); //状态机一开始就进入复位状态,等待复位脉冲
assign m_rest2m_rele = m_state_c == M_REST && (m_end_cnt); //主机输出低电平,保持低电平时间至少480us(500us)
assign m_rele2m_rack = m_state_c == M_RELE && (m_end_cnt); //释放总线,由上拉电阻拉高总线,延时15~60us(20us)
assign m_rack2m_rskp = m_state_c == M_RACK && (m_end_cnt && slave_ack == 0); //从机发送存在脉冲60~240us(200us),主机在60us处采样
assign m_rskp2m_cont = m_state_c == M_RSKP && (s_state_c == S_DONE && flag == 0); //首先进行温度转换
assign m_rskp2m_rcmd = m_state_c == M_RSKP && (s_state_c == S_DONE && flag == 1); //然后读取温度
assign m_cont2m_wait = m_state_c == M_CONT && (s_state_c == S_DONE); //温度转换
assign m_wait2m_rest = m_state_c == M_WAIT && (m_end_cnt); //12位的温度数据进行转换需要延时最多750ms
assign m_rcmd2m_rtmp = m_state_c == M_RCMD && (s_state_c == S_DONE); //发送读温度指令
assign m_rtmp2m_idle = m_state_c == M_RTMP && (s_state_c == S_DONE); //读取温度,之后恢复空闲状态 --- 进行下一次温度读取
//从状态机
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
s_state_c <= S_IDLE;
end
else
s_state_c <= s_state_n;
end
always @(*) begin
case (s_state_c)
S_IDLE:begin
if(s_idle2s_low)
s_state_n = S_LOW;
else
s_state_n = s_state_c;
end
S_LOW :begin
if(s_low2s_send)
s_state_n = S_SEND;
else if(s_low2s_samp)
s_state_n = S_SAMP;
else
s_state_n = s_state_c;
end
S_SEND:begin
if(s_send2s_rele)
s_state_n = S_RELE;
else
s_state_n = s_state_c;
end
S_SAMP:begin
if(s_samp2s_rele)
s_state_n = S_RELE;
else
s_state_n = s_state_c;
end
S_RELE:begin
if(s_rele2s_low)
s_state_n = S_LOW;
else if(s_rele2s_done)
s_state_n = S_DONE;
else
s_state_n = s_state_c;
end
S_DONE:begin //进入s_done状态,下一个时钟周期上升沿将回到初始状态
s_state_n = S_IDLE;
end
default:s_state_n = S_IDLE;
endcase
end
assign s_idle2s_low = s_state_c == S_IDLE && (m_state_c == M_RSKP || m_state_c == M_CONT || m_state_c == M_RTMP || m_state_c == M_RCMD ); //需要在总线上传输数据时产生读写时隙
assign s_low2s_send = s_state_c == S_LOW && (s_end_cnt && (m_state_c == M_RSKP ||
m_state_c == M_CONT || m_state_c == M_RCMD)); //发命令
assign s_low2s_samp = s_state_c == S_LOW && (s_end_cnt && (m_state_c == M_RTMP)); //读温度值
assign s_send2s_rele = s_state_c == S_SEND && (s_end_cnt); //发送数据
assign s_samp2s_rele = s_state_c == S_SAMP && (s_end_cnt); //采样 --- 15us内有效
assign s_rele2s_low = s_state_c == S_RELE && (s_end_cnt && ~end_cnt_bit); //还有需要读写的bit,继续回到起始位,拉低总线
assign s_rele2s_done = s_state_c == S_RELE && (s_end_cnt && end_cnt_bit); //已经读写完成
//cnt_1us
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt_1us<=0;
end
else if(add_cnt_1us) begin
if (end_cnt_1us) begin
cnt_1us<=0;
end
else
cnt_1us<=cnt_1us+1;
end
end
assign add_cnt_1us = (m_state_c !=M_IDLE) || (s_state_c != S_IDLE); //非空闲状态计时
assign end_cnt_1us = add_cnt_1us && cnt_1us == TIME_1US -1;
//m_cnt
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
m_cnt<=0;
end
else if(m_add_cnt) begin
if (m_end_cnt) begin
m_cnt<=0;
end
else
m_cnt<=m_cnt+1;
end
end
assign m_add_cnt = (m_state_c == M_REST || m_state_c == M_RELE || m_state_c == M_RACK || m_state_c == M_WAIT) && end_cnt_1us ;
assign m_end_cnt = m_add_cnt && m_cnt ==X -1;
//X
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
X<=0;
end
else begin
case (m_state_c)
M_REST: X <= TIME_RST; //复位脉冲 --- 500us
M_RELE: X <= TIME_REL; //释放总线 --- 20us
M_RACK: X <= TIME_PRE; //接收存在脉冲 --- 200us
M_WAIT: X <= TIME_WAIT; //温度转换 --- 750ms
endcase
end
end
//s_cnt
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
s_cnt <= 0;
end
else if(s_add_cnt) begin
if (s_end_cnt) begin
s_cnt <= 0;
end
else
s_cnt <= s_cnt+1;
end
end
assign s_add_cnt = (s_state_c == S_LOW || s_state_c == S_SEND ||
s_state_c == S_SAMP || s_state_c == S_RELE) && end_cnt_1us;
assign s_end_cnt = s_add_cnt && s_cnt == Y -1;
//Y
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
Y <= 0;
end
else begin
case (s_state_c)
S_LOW : Y <= TIME_LOW; //主机拉低总线2us (>=1us)
S_SEND: Y <= TIME_RW ; //主机写1bit数据60us (>=60us)
S_SAMP: Y <= TIME_RW ; //主机读1bit数据60us (>=60us)
S_RELE: Y <= TIME_REC; //主机读写完1bit数据之后释放总线3us (>=1us)
endcase
end
end
//cnt_bit
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_bit <= 0;
end
else if(add_cnt_bit)begin
if(end_cnt_bit)begin
cnt_bit <= 0;
end
else begin
cnt_bit <= cnt_bit + 1;
end
end
end
assign add_cnt_bit = s_state_c == S_RELE && s_end_cnt; // 从机从收发数据状态进入时隙恢复状态,表明1 bit 数据传输完成
assign end_cnt_bit = add_cnt_bit && cnt_bit == ((m_state_c == M_RTMP)?16-1:8-1); //只有读温度需要接收两个字节的数据,其他时间都是发送一个字节的命令
//slave_ack
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
slave_ack <= 1;
end
else if(end_cnt_1us && m_state_c == M_RACK && m_cnt == 60) begin //延时60us采样
slave_ack <= dq_in;
end
end
//dq_dout_en
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
dq_out_en <= 0;
end
else if(m_idle2m_rest || m_wait2m_rest || s_idle2s_low || s_rele2s_low) begin //拉低总线使能
dq_out_en <= 1;
end
else if (s_low2s_samp || m_rest2m_rele ||s_send2s_rele) begin //读数据,关闭输出使能
dq_out_en<=0; //释放总线,关闭输出使能,由上拉电阻拉高总线
end //发送完数据,释放总线,关闭输出使能
end
//dq_out
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
dq_out <= 0;
end
else if(m_idle2m_rest || m_wait2m_rest || s_idle2s_low || s_rele2s_low) begin
dq_out <= 0;
end
else if (s_low2s_send) begin
dq_out <= cmd_r[cnt_bit]; //发送命令 --- 每次发1bit
end
end
//cmd_r
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cmd_r <= 0;
end
else begin
case (m_state_c)
M_RSKP: cmd_r <= CMD_RSKP;
M_CONT: cmd_r <= CMD_CONT;
M_RCMD: cmd_r <= CMD_RTMP;
endcase
end
end
//temp_out_r
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
temp_out_r <= 0;
end
else if(s_state_c == S_SAMP && s_cnt == 13 && end_cnt_1us) begin //温度采集,每次读1bit,在读时隙产生后15us内有效
temp_out_r[cnt_bit] <= dq_in;
end
end
//temp_data 温度判断
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
temp_data <= 0;
end
else if(s_state_c == S_SAMP && cnt_bit == 15 && s_samp2s_rele)begin
if(temp_out_r[15]) //判断正负温度(0正1负),决定是否需要做补码-原码转化
temp_data <= ~temp_out_r[10:0] + 1'b1; //负温 则取反加1
else
temp_data <= temp_out_r[10:0]; //正温
end
end
/*
实际的温度值为 temp_data * 0.0625;
为了保留4位小数精度,将实际温度值放大了10000倍,
即 temp_data * 625;
*/
assign temp_data_r = temp_data*625;
//temp_out --- 输出温度
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
temp_out <= 0;
end
else if(m_state_c == M_RTMP && s_rele2s_done)begin
temp_out <= temp_data_r;
end
end
//temp_sign --- 输出温度正负
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
temp_sign <= 0;
end
else if(s_state_c == S_SAMP && cnt_bit == 15 && s_samp2s_rele) begin
temp_sign <= temp_out_r[15];
end
end
//temp_out_vld --- 数据有效之后拉高一个周期
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
temp_out_vld <= 0;
end
else if(m_state_c == M_RTMP && s_rele2s_done) begin
temp_out_vld <= 1;
end
else
temp_out_vld <= 0;
end
//flag
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
flag <= 0;
end
else if(m_wait2m_rest) begin
flag <= 1;
end
else if (m_rtmp2m_idle) begin
flag <= 0;
end
end
endmodule //ds18b20_driver
3.温度转换模块
我们这里做的比较简单,只是把温度数据每一位放到了4bit的2进制数中,方便数码管显示
module control (
input clk ,
input rst_n ,
input [23:0] temp_out ,
input temp_out_vld ,
output [23:0] dout
);
//定义数码管每一位的数字
wire [3:0] data_r1;
wire [3:0] data_r2;
wire [3:0] data_r3;
wire [3:0] data_r4;
wire [3:0] data_r5;
wire [3:0] data_r6;
reg [23:0] dout_r; //寄存温度驱动模块传来的温度值
//dout_r
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
dout_r <= 0;
end
else if(temp_out_vld) begin
dout_r <= temp_out;
end
end
assign data_r1 = dout_r%10 ;
assign data_r2 = (dout_r/10 )%10;
assign data_r3 = (dout_r/100 )%10;
assign data_r4 = (dout_r/100_0 )%10;
assign data_r5 = (dout_r/100_00 )%10;
assign data_r6 = (dout_r/100_000)%10;
assign dout = {data_r6,data_r5,data_r4,data_r3,data_r2,data_r1};
endmodule //control
4.数码管驱动
这个模块不太明白的可以参考之前写的文章电子时钟
module seg_driver (
input clk ,
input rst_n ,
input [23:0] dout ,
output reg [5:0 ] sel ,
output reg [7:0 ] seg
);
reg [3:0] seg_flag; //寄存数码管每一位要显示的值
//1ms计时器---用来切换数码管位选,以达到轮流显示时间的各位(肉眼可以看到动态的时间计数)
reg [15:0] cnt;
wire add_cnt;
wire end_cnt;
reg dot; //小数点,分割整数位和小数位
parameter MAX_CNT =50_000 ,
ZERO =7'b100_0000,
ONE =7'b111_1001,
TWO =7'b010_0100,
THREE =7'b011_0000,
FOUR =7'b001_1001,
FIVE =7'b001_0010,
SIX =7'b000_0010,
SEVEN =7'b111_1000,
EIGHT =7'b000_0000,
NINE =7'b001_0000;
//计时器
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt<=0;
end
else if(add_cnt) begin
if (end_cnt) begin
cnt<=0;
end
else
cnt<=cnt+1;
end
end
assign add_cnt=1'b1;
assign end_cnt=add_cnt&&cnt==MAX_CNT-1;
//切换数码管位选
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sel<=6'b111_110;
end
else if(end_cnt) begin
sel<={sel[4:0],sel[5]};
end
end
//切换数码管段选
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
seg_flag<=0;
end
else begin
case (sel)
6'b111_110: begin seg_flag<=dout[23:20]; dot<= 1;end
6'b111_101: begin seg_flag<=dout[19:16]; dot<= 0;end
6'b111_011: begin seg_flag<=dout[15:12]; dot<= 1;end
6'b110_111: begin seg_flag<=dout[11:8]; dot<= 1;end
6'b101_111: begin seg_flag<=dout[7:4]; dot<= 1;end
6'b011_111: begin seg_flag<=dout[3:0]; dot<= 1;end
default :seg_flag<=0;
endcase
end
end
//段选译码
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
seg<=8'b1111_1111;
end
else begin
case (seg_flag)
0: seg <= {dot,ZERO} ;
1: seg <= {dot,ONE} ;
2: seg <= {dot,TWO} ;
3: seg <= {dot,THREE} ;
4: seg <= {dot,FOUR} ;
5: seg <= {dot,FIVE} ;
6: seg <= {dot,SIX} ;
7: seg <= {dot,SEVEN} ;
8: seg <= {dot,EIGHT} ;
9: seg <= {dot,NINE} ;
default: seg<=8'b1111_1111;
endcase
end
end
endmodule //seg_driver
四丶仿真
1.Testbench
`timescale 1ns/1ns
module tb_ds18b20_driver ();
// defparam u_ds18b20_driver.TIME_1US = 2;
defparam u_ds18b20_driver.TIME_RST = 200,
u_ds18b20_driver.TIME_PRE = 100,
u_ds18b20_driver.TIME_WAIT= 750;
//信号定义
reg clk ;
reg rst_n ;
reg dq_in ;
wire temp_sign ;
wire dq_out ;
wire dq_out_en ;
wire [23:0] temp_out ;
wire temp_out_vld;
parameter CYCLE = 20;
//模块例化
ds18b20_driver u_ds18b20_driver(
.clk (clk ),//时钟信号
.rst_n (rst_n ),//复位信号
.dq_in (dq_in ),
.temp_sign (temp_sign ),
.dq_out (dq_out ),//dq总线FPGA输出
.dq_out_en (dq_out_en ),//输出数据有效信号
.temp_out (temp_out ),//温度输出
.temp_out_vld (temp_out_vld) //温度输出有效信号
);
always #(CYCLE/2) clk = ~clk;
integer i=0;
initial begin
clk = 1'b1;
rst_n = 1'b0;
dq_in = 0;
#(CYCLE*20);
rst_n = 1'b1;
repeat(5)begin
for(i=0;i<500000;i=i+1)begin
dq_in = {$random};
#(CYCLE*20);
end
#(CYCLE*20);
end
$stop;
end
endmodule //tb_ds18b20_driver
2.仿真分析
五丶上板验证
DS18B20
六丶源码
链接:https://pan.baidu.com/s/1-N9ONTeFiQioVeaqoNpQYg?pwd=opbi
提取码:opbi
单总线传输的定义:顾名思义,即主机和从机用一根总线进行通信,是一种半双工的通信方式,单线=时钟线+数据线+控制线(+电源线)。
理想状况下一条总线上的从器件数量几乎不受数量限制。
单总线技术具有线路简单,硬件开销少,成本低廉,便于总线扩展和维护等优点。但由于只有一根总线,驱动能力一般较差,不能接过多的从器件,实际使用中,一般最多只能接8个从器件;抗干扰能力较差,一般只能在中短距离的低速传输中使用;软件设计复杂,事物往往有两面性,硬件部分的简单往往需要软件在复杂度上做出牺牲。
单总线传输就如同一根独木桥,行走于独木桥上的各种信号必须严格遵守通信协议才能“安全过桥”。
接下来我们就简单的介绍一下数据是如何传输的。
单总线通信协议为确保数据的完整性,定义了如下几种单线信号类型:复位脉冲、存在脉冲、写0、写1、读0和读1。所有这些信号,除存在脉冲外,都是由总线控制器发出的。 ↩︎