目录
- 前言
- 一丶看懂DS18B20数据手册
-
- 1.DS18B20内部结构
- 2.DS18B20的命令
-
- ①ROM功能命令
- ②RAM功能命令
- 3.命令用法
- 4.初始化
- 5.读写时隙
-
- ①写时隙
- ②读时隙
- 二、分析实验任务
-
- 1.状态机
- 2.温度解码
- 3.模块原理图
- 三、代码设计
-
- 1.顶层模块
- 2.DS18B20驱动
- 3.温度转换模块
- 4.数字管驱动
- 四丶仿真
-
- 1.Testbench
- 2.仿真分析
- 五、上板验证
- 六丶源码
前言
提供数字温度传感器
9-Bit到12-Bit
用户可编程的摄氏温度测量精度和非易失性,具有温和低温触发报警功能。 DS18B20采用的(单总线)1通信只使用数据线(和地)与微控制器通信。 该传感器的为-55℃至 125℃,温度范围超过-10℃至85℃还有±0.5℃精度。DS18B20不需要外部电源就可以直接由数据线供电。每片DS18B20都有一个,所以一个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位,,最后8位是前56位CRC循环冗余校准码(CRC=X8 X5 X4 1) 光刻ROM它的作用是让每一个DS18B20都不一样,这样一条总线就可以挂多个DS18B20目的。相应的命令如下五条:
-
33H
: 该命令允许总线控制器(FPGA)读取DS18B8位系列编码,唯一的序列号和8位CRC码。 只有在总线上才存在DS18B该命令只能在20点使用。 -
55H
: 发出这个命令后,然后发出64人ROM编码,让总线控制器(FPGA)在多点总线上定位一个特定的DS18B20。只有和64位ROM完全匹配序列DS18B20会做出反应。所有64位ROM序列不匹配DS18B20将等待复位脉冲。当总线上有单个或多个设备时,可以使用此命令。 -
CCH
: 该命令允许总线控制器(FPGA)不用提供64位ROM编码在单点总线(一个DS18B20)可以节省时间。如果总线上有不止一个从机,跳过ROM命令结束后,发送阅读命令。由于多个从机器同时发送信号,数据冲突发生在总线上(漏极开路上拉效果相当好 于相与)。 -
F0H
: 当系统首次启动时,总线控制器可能不知道总线上有多少器件或64位ROM编码。搜索ROM命令允许总线控制器用排除法识别总线上的所有DS18B20的64位编码。 -
ECH
: 发出此命令后,只有温度超过设定值的上限或下限DS18B20做出反应。只要需要注意, DS18B20不断电,报警状态将保持,直到再次测量的温度值达不到报警条件。
②RAM功能命令
RAM功能命令是操作,我们这里的RAM主要指,让我们来看看它的数据结构
由上图可知DS18B20的高速缓存器共有,其中()对应字节地址0,()对应字节地址1,以此类推,字节地址为4。温度数据存储格式如下图所示:
①DS18B20出厂时默认配置温度数据,其中,即,最低四位为。
②FPGA读取温度数据时,一次读2字节共16位,读完后将低11位的二进制数转化为十进制数,再乘以0.实际温度值为0625。
③此外,还需要判断温度,,这五位同时改变,。当前5位为1时,读取温度为负值时,需要取反加1,乘以0.只有0625才能获得实际温度值。当前五名为0时,读取的温度为正值,只要测量值乘以0.0625可获得实际温度值。
-
44H
: 该命令启动温度转换。然后执行温度转换命令DS18B保持等待状态。如果总线控制器遵循此命令后的读取间隙DS18B如果20忙于时间转换,DS18B20将在总线上输出0,如果温度转换完成,则输出1。如果使用寄生电源,总线控制器必须在发出此命令后立即启动强拉,并保持500ms。低存入转换结果BYTE0、高位存BYTE1。 -
BEH
: 此命令读取临存器的内容。读取将从BYTE从0开始,一直持续到9字节(BYTE8,CRC)读完。如果不想读完所有字节,控制器可以在任何时间发出复位命令来终止读取。另外需要注意的是,字节内容都是最低位先传送。 -
4EH
: 这个命令向DS18B20的高速缓存器中写入数据,开始位置在地址2。接下来写入的两个字节将被存到高速缓存器的字节地址位2和3的高、低温触发器。可以在任何时刻发出复位命令来终止写入。 -
48H
: 这条命令把高速缓存器的内容拷贝到DS18B20的E2PROM存储器中,即把温度报警触发字节存入非易失性存储器里。如果总线控制器在这条命令之后跟着发出读时间隙,而DS18B20又正在忙于把暂存器拷贝到E2PROM存储器,DS18B20就会输出一个“0”,如果拷贝结束的话,DS18B20则输出“1”。如果使用寄生电源,总线控制器必须在这条命令发出后立即起动强上拉并最少保持10ms。 -
B8H
: 这条命令把E2PROM里的值拷回高速缓存器。这种拷回操作在DS18B20上电时自动执行,这样器件一上电高速缓存器里马上就存在有效的数据了。若在这条命令发出之后发出读时间隙,器件会输出温度转换忙的标识:“0”=忙,“1”=完成。 -
B4H
: 读DS18B20的供电方式。若把这条命令发给DS18B20后发出读时间隙,器件会返回它的电源模式:“0”=寄生电源,“1”=外部电源。
3.命令用法
阅读数据手册:
大致意思就是访问这个温度传感器有三个步骤:
1.初始化时序 2.使用ROM命令匹配总线上相应的传感器 3.使用RAM命令读取温度,最后回到初始化时序
下面介绍以上几条指令的用法。当进行操作时:
首先应将主机逐个与DS18B20挂接,
如果主机只对一个DS18B20进行操作,就不需要读取ROM编码以及匹配ROM编码,
初始化➔发跳过ROM命令(CCH)➔发开始转换命令(44H)➔延时➔初始化➔发送跳过ROM命令(CCH)➔发读存储器命令(BEH)➔连续读出两个字节数据(即温度)➔结束或开始下一循环。
4.初始化
一个复位脉冲跟着一个存在脉冲表明DS18B20已经准备好
5.读写时隙
初始化完成之后,主机就可以向从机读写数据。
读写数据涉及到读写时隙的概念。在单总线通信协议中,读写时隙的概念十分重要,
①写时隙
单总线通信协议中写时隙有两种:写1和写0。 主机采用写1时隙向从机写入1,而采用写0时隙向从机写入0。所有写时隙
两种写时隙均起始于主机拉低数据总线。
②读时隙
对于读时隙,单总线器件仅在主机
每个读时隙都
在主机发出读时隙之后,单总线器件才开始在总线上发送0或1。
若从机发送1,则保持总线为高电平;
若发出0,则拉低总线。 当发送0时,从机在
二丶分析实验任务
1.使用状态机编写温度传感器驱动 2.在数码管上实时显示温度
1.状态机
首先我们分析温度传感器驱动需要哪些状态
我们访问此传感器并获取温度 首先需要进行①
④
注意:发送指令以及读取温度数据存在 读写数据操作,这就需要用到读写时隙,所以上面列出的步骤中省略的 产生读写时隙可以作为” 从状态机” ,而总的步骤作为“ 主状态机 ”
- M_IDLE:空闲状态,等待开始通信;
- M_RST:发送复位脉冲;
- M_REL:释放总线;
- M_RACK:接收存在脉冲;
- M_RSKP:发送跳过 ROM 指令;
- M_SCON:发送温度转换命令;
- M_WAIT:等待 750ms;
- M_SRTM:发送温度读取指令;
- M_RTMP:读取温度值;
- 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在出厂时
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