单片机:DS1302时钟

您所在的位置:网站首页 时钟电路接单片机的哪两个脚 单片机:DS1302时钟

单片机:DS1302时钟

2024-07-07 01:02:09| 来源: 网络整理| 查看: 265

index 参考资料1. BCD码的学习2. SPI时序初步认识2.1 认识spi2.2 CPOL和CPHA 3 实时时钟芯片DS13023.1 DS1302的特点3.2 DS1302的引脚3.3 DS1302寄存器介绍3.4 DS1302通信时序介绍3.4.1 单字节写入操作3.4.2 单字节读操作 3.5 编程实例main.cLCD1602.hLCD1602.cDS1302.hDS1302.c 3.6 DS1302的BURST模式 4. 结构体数据类型

参考资料 主要:https://blog.csdn.net/Qingzhusshuiyun/article/details/78248478 1. BCD码的学习

在我们日常生产生活中用的最多的数字是十进制数字,而单片机系统的所有数据本质上都是二进制的,所以聪明的前辈们就给我们创造了BCD码。 BCD码(Binary-Coded Decimal)亦称二进码十进制数或二-十进制代码。用4位二进制数来表示1位十进制数中的0~9这10个数字。是一种二进制的数字编码形式,用二进制编码的十进制代码。BCD码这种编码形式利用了四个位元来储存一个十进制的数码,使二进制和十进制之间的转换得以快捷的进行。我们前边讲过十六进制和二进制本质上是一回事,十六进制仅仅是二进制的一种缩写形式而已。而十进制的一位数字,从0到9,最大的数字就是9,再加1就要进位,所以用4位二进制表示十进制,就是从0000到1001,不存在1010、1011、1100、1101、1110、1111这6个数字。BCD码如果到了1001,再加1的话,数字就变成了0001 0000这样的数字了,相当于用了8位的二进制数字表示了2位的十进制数字。关于BCD码更详细的介绍请点击www.51hei.com的基础教程栏目里面有很多相关文章.

2. SPI时序初步认识 2.1 认识spi

SPI接口内部硬件

SPI接口内部硬件图 UART、I2C和==SPI==是单片机通信中最常用的三种通信协议。前边我们已经学了UART和I2C通信协议,这节课我们来学习剩下的SPI通信协议。SPI是英语Serial Peripheral Interface的缩写,顾名思义就是串行外围设备接口。SPI是一种高速的、全双工、同步通信总线,标准的SPI也仅仅使用4个引脚,常用于单片机和EEPROM、FLASH、实时时钟、数字信号处理器等器件的通信。SPI通信原理比I2C要简单,它主要是主从方式通信,这种模式通常只有一个主机和一个或者多个从机,标准的SPI是4根线,分别是 * SSEL(片选,也写作SCS) SSEL:==从设备==片选使能信号。如果从设备是低电平使能的话,当拉低这个引脚后,从设备就会被选中,主机和这个被选中的从机进行通信,==由主机控制==。 * SCLK(时钟,也写作SCK) SCLK:时钟信号,==由主机产生==,和I2C通信的SCL有点类似。 * MOSI(主机输出从机输入Master Output/Slave Input) MOSI:主机给从机发送指令或者数据的通道。 * MISO(主机输入从机输出Master Input/Slave Output) MISO:主机读取从机的状态或者数据的通道。

在某些情况下,我们也可以用3根线的SPI或者2根线的SPI进行通信。比如主机只给从机发送命令,从机不需要回复数据的时候,那MISO就可以不要;而在主机只读取从机的数据,不需要给从机发送指令的时候,那MOSI可以不要;当一个主机一个从机的时候,从机的片选有时可以固定为有效电平而一直处于使能状态,那么SSEL可以不要;此时如果再加上主机只给从机发送数据,那么SSEL和MISO都可以不要;如果主机只读取从机送来的数据,SSEL和MOSI都可以不要。 3线和2线的SPI大家要知道怎么回事,实际使用也是有应用的,但是当我们提及SPI的时候,一般都是指标准SPI,都是指4根线的这种形式。

SPI接口的一个缺点:没有指定的流控制,没有应答机制确认是否接收到数据。

SPI通信的主机也是我们的单片机,在读写数据时序的过程中,有四种模式,要了解这四种模式,首先我们得学习一下2个名词。

2.2 CPOL和CPHA

CPOL:Clock Polarity,就是时钟的极性。 时钟的极性是什么概念呢?通信的整个过程分为空闲时刻和通信时刻,SCLK在数据发送之前和之后的空闲状态是高电平那么CPOL=1,如果空闲状态SCLK是低电平,那么CPOL=0。

CPHA:Clock Phase,就是时钟的相位。 主机和从机要交换数据,牵涉到一个问题,即主机在什么时刻输出数据到MOSI上而从机在什么时刻采样这个数据,或者从机在什么时刻输出数据到MISO上而主机什么时刻采样这个数据。同步通信的一个特点就是所有数据的变化和采样都是伴随着时钟沿进行的,也就是说数据总是在时钟的边沿附近变化或被采样。而一个时钟周期必定包含了一个上升沿和一个下降沿,这是周期的定义所决定的,只是这两个沿的先后并无规定。又因为数据从产生的时刻到它的稳定是需要一定时间,那么,如果主机在上升沿输出数据到MOSI上,从机就只能在下降沿去采样这个数据了。反之如果一方在下降沿输出数据,那么另一方就必须在上升沿采样这个数据。

CPHA=1,就表示数据的输出是在一个时钟周期的第一个沿上,至于这个沿是上升沿还是下降沿,这要是CPOL的值而定,CPOL=1那就是下降沿,反之就是上升沿。那么数据的采样自然就是在第二个沿上了。

CPHA=0,就表示数据的采样是在一个时钟周期的第一个沿上,同样它是什么沿由CPOL决定。那么数据的输出自然就在第二个沿上了。仔细想一下,这里会有一个问题:就是当一帧数据开始传输第一bit时,在第一个时钟沿上就采样该数据了,那么它是在什么时候输出来的呢?有两种情况:一是SSEL使能的边沿,二是上一帧数据的最后一个时钟沿,有时两种情况还会同时生效。 我们以CPOL=1/CPHA=1为例,把时序图画出来给大家看一下,如图所示 SPI通信时序图11 当数据未发送时以及发送完毕后,SCK都是高电平,因此CPOL=1。可以看出,在SCK第一个沿的时候,MOSI和MISO会发生变化,同时SCK第二个沿的时候,数据是稳定的,此刻采样数据是合适的,也就是上升沿即一个时钟周期的后沿锁存读取数据,即CPHA=1。注意最后最隐蔽的SSEL片选,一般情况下,这个引脚通常用来决定是哪个从机和主机进行通信。剩余的三种模式,我把图画出来,简化起见把MOSI和MISO合在一起了,大家仔细对照看看研究一下,把所有的理论过程都弄清楚,有利于你对SPI通信的深刻理解,如图所示 SPI通信时序图01 在时序上,SPI是不是比I2C要简单的多?没有了起始、停止和应答,UART和SPI在通信的时候,只负责通信,不管是否通信成功,而I2C却要通过应答信息来获取通信成功失败的信息,所以相对来说,UART和SPI的时序都要比I2C简单一些。

3 实时时钟芯片DS1302

DS1302是个实时时钟芯片,我们可以用单片机写入时间或者读取当前的时间数据,我也会带着大家通过阅读这个芯片的数据手册来学习和掌握这个器件。

由于IT技术国际化比较强,因此数据手册绝大多数都是英文的,导致很多英语基础不好的同学看到英文手册头就大了。这里我要告诉大家的是,只要精神不退缩,方法总比困难多,很多英语水平不高的,看数据手册照样完全没问题,因为我们的专业词汇也就那么几个,多看几次就认识了。我们现在不是考试,因此大家可以充分利用一些英文翻译软件,翻译过来的中文意思有时候可能不是那么准确,那你就把翻译的内容和英文手册里的一些图表比较参考学习。此外数据手册除了介绍性的说明外,一般还会配相关的图形或者表格,结合起来看也有利于理解手册所表达的意思。这节课我会把DS1302的英文资料尽可能的用比较便于理解的方式给大家表达出来,同学们可以把我的表达和英文手册多做一下对比,尽可能快的慢慢开始学会了解英文手册。

3.1 DS1302的特点

DS1302是DALLAS(达拉斯)公司出的一款涓流充电时钟芯片,2001年DALLAS被MAXIM(美信)收购,因此我们看到的DS1302的数据手册既有DALLAS的标志,又有MAXIM的标志。

DS1302实时时钟芯片广泛应用于电话、传真、便携式仪器等产品领域,DS1302的主要性能指标 (1)DS1302实时时钟具有能计算2100年之前的秒、分、时、日、日期、星期、月、年的能力,还有闰年调整的能力。 (2)内部含有31个字节静态RAM,可提供用户访问。 (3)采用串行数据传送方式,使得管脚数量最少,简单SPI 3线接口。 (4)工作电压范围宽:2.0~5.5V。 (5)工作电流:2.0V时,小于300nA。 (6)时钟或RAM数据的读/写有两种传送方式:单字节传送和多字节传送方式。 (7)采用8脚DIP封装或SOIC封装。 (8)与TTL兼容,Vcc=5V。 (9)可选工业级温度范围:-40℃~+85℃。 (10)具有涓流充电能力。 (11)采用主电源和备份电源双电源供应。 (12)备份电源可由电池或大容量电容实现。

3.2 DS1302的引脚

在这里插入图片描述 其中: X1、X2:32.768KHz晶振接入引脚。 GND:地。 CE:复位引脚,低电平有效,操作时高电平。 I/O:数据输入/输出引脚,具有三态功能。 SCLK:串行时钟输入引脚。 Vcc1:工作电源引脚。 Vcc2:备用电源引脚。 接入电池断电时提供1302电源,此脚可以直接悬空,断电后不需要DS1302再运行了,或者是在此脚接一个10uF的电容,经过试验可以运行1分钟左右的时间,如果大家想运行时间再长,可以加大电容的容量,典型电路如图: 在这里插入图片描述 在这里插入图片描述 涓流充电功能,也作为选学即可,我们使用的时候直接用5V电源接一个二极管,在有主电源的情况下给电容充电,在主电源掉电的情况下,这个电容可以给DS1302大约供电1分钟左右,这种电路的最大用处是在电池供电系统中更换主电池的时候保持实时时钟的运行不中断,1分钟的时间对于更换电池足够了。此外,通过我们的使用经验,在DS1302的主电源引脚串联一个1K电阻可以有效的防止电源对DS1302的冲击,R6就是,而R9,R26,R32都是上拉电阻。

DS1302引脚功能表 引脚编号引脚名称引脚功能1Vcc2主电源引脚,当Vcc2比Vcc1高0.2V以上时,DS1302由VCC2供电,当Vcc2低于Vcc1时,由Vcc1供电。2X1这两个引脚需要接一个32.768K的晶振,给DS1302提供一个基准。特别注意,要求这个晶振的引脚负载电容必须是6pF,而不是要加6pF的电容。如果使用有源晶振的话,接到X1上即可,X2悬空。3X24GND接地。5CEDS1302的输入引脚。当读写DS1302的时候,这个引脚必须是高电平,DS1302这个引脚内部有一个40k的下拉电阻。6I/O这个引脚是一个双向通信引脚,读写数据都是通过这个引脚完成。DS1302这个引脚的内部含有一个40k的下拉电阻。7SCLK输入引脚。SCLK是用来作为通信的时钟信号。DS1302这个引脚的内部含有一个40k的下拉电阻。8Vcc1备用电源引脚。

DS1302的电路一个重点就是晶振电路,它所使用的晶振是一个32.768k的晶振,晶振外部也不需要额外添加其他的电容或者电阻电路了。时钟的精度,首先取决于晶振的精度以及晶振的引脚负载电容。如果晶振不准或者负载电容过大过小,都会导致时钟误差过大。在这一切都搞定后,最终一个考虑因素是晶振的温漂。随着温度的变化,晶振往往精度会发生变化,因此,在实际的系统中,其中一种方法就是经常校对。比如我们所用的电脑的时钟,通常我们会设置一个选项“将计算机设置于internet时间同步”。选中这个选项后,一般可以过一段时间,我们的计算机就会和internet时间校准同步一次。

3.3 DS1302寄存器介绍 DS1302控制寄存器 用于存放控制命令字,DS1302的CE(或RST)引脚回到高电平后写入的第一个字就是控制命令一条指令就是一个字节包含8位, 第七位(即最高位)是固定1,这一位如果是0的话,那写进去是无效的。第六位是选择RAM还是CLOCK的,所以如果选择CLOCK功能,第六位是0,如果要用RAM,那第六位就是1。从第五到第一位,决定了寄存器的5位地址,第零位是读写位,如果要写,这一位就是0,如果要读,这一位就是1

如图所示。在这里插入图片描述

图DS1302命令字节 DS1302的时钟寄存器, 其中8个和时钟有关的,5位地址分别是00000一直到00111这8个地址,还有一个寄存器的地址是01000,这是涓流充电所用的寄存器,我们这里不讲。在DS1302的数据手册里的地址,直接把第七位、第六位和第零位值给出来了,所以指令就成了80H、81H那些了,最低位是1,那么表示读,最低位是0表示写,如图15-10所示。

在这里插入图片描述

图DS1302的时钟寄存器 寄存器1:最高位CH是一个时钟停止标志位。 如果我们的时钟电路有备用电源部分,上电后,我们要先检测一下这一位,如果这一位是0,那说明我们的时钟在系统掉电后,由于备用电源的供给,时钟是持续正常运行的;如果这一位是1,那么说明我们的时钟在系统掉电后,时钟部分不工作了。若我们的Vcc1悬空或者是电池没电了,当我们下次重新上电时,读取这一位,那这一位就是1,我们可以通过这一位判断时钟在单片机系统掉电后是否持续运行。 剩下的7位高3位是秒的十位,低4位是秒的个位,这里注意再提一次,DS1302内部是BCD码,而秒的十位最大是5,所以3个二进制位就够了。寄存器2:bit7没意义,剩下的7位高3位是分钟的十位,低4位是分钟的个位。寄存器3:bit7是1的话代表是12小时制,是0的话代表是24小时制,bit6固定是0,bit5在12小时制下0代表的是上午,1代表的是下午,在24小时制下和bit4一起代表了小时的十位,低4位代表的是小时的个位。寄存器4:高2位固定是0,bit5和bit4是日期的十位,低4位是日期的个位。寄存器5:高3位固定是0,bit4是月的十位,低4位是月的个位。寄存器6:高5位固定是0,低3位代表了星期。寄存器7:高4位代表了年的十位,低4位代表了年的个位。这里特别注意,这里的00到99年指的是2000年到2099年。寄存器8:bit7是一个保护位,如果这一位是1,那么是禁止给任何其他的寄存器或者那31个字节的RAM写数据的。因此在写数据之前,这一位必须先写成0。 3.4 DS1302通信时序介绍

DS1302前边也有提起过,是三根线,分别是CE、I/O和SCLK,其中CE是使能线,SCLK是时钟线,I/O是数据线。前边我们学过SPI通信,同学们发现没发现,这个DS1302的通信线定义和SPI怎么这么像呢?

事实上,DS1302的通信是SPI的变异种类,它用了SPI的通信时序,但是通信的时候没有完全按照SPI的规则来,下面我们一点点解剖一下DS1302的变异SPI通信方式。

3.4.1 单字节写入操作

DS1302是通过SPI串行总线跟单片机通信的,当进行一次读写操作时最少得读写两个字节,第一个字节是控制字节,就是一个命令,告诉DS1302是读还是写操作,是对RAM还是对CLOK寄存器操作。第二个字节就是要读或写的数据了。 单字节读写:只有在SCLK为低电平时,才能将CE置为高电平。所以在进行操作之前先将SCLK置低电平,然后将CE置为高电平,接着开始在IO上面放入要传送的电平信号,然后跳变SCLK。数据在SCLK上升沿时,DS1302读写数据,在SCLK下降沿时,DS1302放置数据到IO上在这里插入图片描述

图 DS1302单字节写操作

然后我们在对比一下再对比一下CPOL=0并且CPHA=0的情况下的SPI的操作时序,如图所示。

在这里插入图片描述

图 CPOL=0/CPHA=0通信时序

上面两图的通信时序,其中CE和SSEL的使能控制是反的,对于通信写数据,都是在SCK的上升沿,从机进行采样;下降沿的时候,主机发送数据。DS1302的时序里,单片机要预先写一个字节指令,指明要写入的寄存器的地址以及后续的操作是写操作,然后再写入一个字节的数据。

3.4.2 单字节读操作

对于单字节读操作,我就不做对比了,把DS1302的时序图贴出来给大家看一下,如图15-13所示。 在这里插入图片描述 图 DS1302单字节读操作

读操作有两处特别注意的地方。

DS1302的时序图上的箭头都是针对DS1302来说的,因此读操作的时候,先写第一个字节指令,上升沿的时候DS1302来锁存数据,下降沿我们用单片机发送数据。到了第二个字数据,由于我们这个时序过程相当于CPOL=0/CPHA=0,前沿发送数据,后沿读取数据,第二个字节是DS1302下降沿输出数据,我们的单片机上升沿来读取,因此箭头从DS1302角度来说,出现在了下降沿。我们的单片机没有标准的SPI接口,和I2C一样需要用IO口来模拟通信过程。 在读DS1302的时候,理论上SPI是上升沿读取,但是我们的程序是用IO口模拟的,所以数据的读取和时钟沿的变化不可能同时了,必然就有一个先后顺序。通过实验发现,如果先读取IO线上的数据,再拉高SCLK产生上升沿,那么读到的数据一定是正确的,而颠倒顺序后数据就有可能出错。这个问题产生的原因还是在于DS1302的通信协议与标准SPI协议存在的差异造成的,如果是标准SPI的数据线,数据会一直保持到下一个周期的下降沿才会变化,所以读取数据和上升沿的先后顺序就无所谓了;但DS1302的IO线会在时钟上升沿后被DS1302释放,也就是撤销强推挽输出变为弱下拉状态,而此时在51单片机引脚内部上拉的作用下,IO线上的实际电平会慢慢上升,从而导致在上升沿产生后再读取IO数据的话就可能出错。因此这里的程序我们按照先读取IO数据,再拉高SCLK产生上升沿的顺序。 3.5 编程实例

下面我们就写一个程序,先将2013年10月8号星期二12点30分00秒这个时间写到DS1302内部,让DS1302正常运行,然后在不停的读取DS1302的当前时间,并显示在我们的液晶屏上 电路图: 在这里插入图片描述

main.c #include "reg52.h" #include "ds1302.h" #include "lcd1602.h" #define CLOCKRATE 12000000 uchar t0rh; //T0重载值的高字节 uchar t0rl; //T0重载值的低字节 bit flag200ms = 0; //200ms定时标志 /** *DS1302时钟初始化2020年02月20日星期四12点00分00秒。 * 存储顺序是秒分时日月周年,存储格式是用BCD码 **/ uchar time[]={0, 0, 0x12, 0x20, 0x02, 0x05, 0x20};//存储当前时间 void delay_n10us(uint n); //延时10us的整数倍 void timer0Init(uint ms); void ds1302Init(); void lcdInit(); //lcd初始化 void main() { uchar i; uchar psec = 0xAA; //保存上一次读取的秒值 uchar str[12]; //字符串转换缓冲区 lcdInit(); ds1302Init(); timer0Init(1); EA =1; while(1) { if (flag200ms) //每200ms读取一次时间 { flag200ms = 0; for (i=0; i str[0] = '2'; //添加年份的高2位:20 str[1] = '0'; str[2] = (time[6] >> 4) +'0';//“年”高位数字转换为ASCII码 str[3] = (time[6]&0x0F) +'0';//“年”低位数字转换为ASCII码 str[4] = '-'; //添加日期分隔符 str[5] = (time[4] >> 4) +'0'; //“月” str[6] = (time[4]&0x0F) +'0'; str[7] = '-'; str[8] = (time[3] >> 4) +'0'; //“日” str[9] = (time[3]&0x0F) +'0'; str[10] = '\0'; lcdShowStr(0, 0, str); //显示到液晶的第一行 str[0] = (time[5]&0x0F) +'0'; //“星期” str[1] = '\0'; lcdShowStr(11, 0,"week"); lcdShowStr(15, 0, str); //显示到液晶的第一行 str[0] = (time[2] >> 4) +'0'; //“时” str[1] = (time[2]&0x0F) +'0'; str[2] = ':'; //添加时间分隔符 str[3] = (time[1] >> 4) +'0'; //“分” str[4] = (time[1]&0x0F) +'0'; str[5] = ':'; str[6] = (time[0] >> 4) +'0'; //“秒” str[7] = (time[0]&0x0F) +'0'; str[8] = '\0'; lcdShowStr(4, 1, str); //显示到液晶的第二行 psec = time[0]; //用当前值更新上次秒数 } } } } void DS1302Init(void) //DS1302初始化 { unsigned char i; DS1302_CE = 0; //初始化DS1302通信引脚 DS1302_CK = 0; i= DS1302SingleRead(0); //读取秒寄存器 if ((i & 0x80) != 0) //由秒寄存器最高位CH的值判断DS1302是否已停止 { DS1302SingleWrite(7, 0x00); //撤销写保护以允许写入数据 for (i=0; i unsigned long tmp; tmp = CLOCKRATE / 12; //定时器计数频率 tmp = (tmp * ms) / 1000; //计算所需的计数值 tmp = 65536 - tmp; //计算定时器重载值 tmp = tmp + 12; //修正中断响应延时造成的误差 t0rh = (unsigned char)(tmp >> 8); //定时器重载值拆分为高低字节 t0rl = (unsigned char)tmp; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0为模式1 TH0 = t0rh; //加载T0重载值 TL0 = t0rl; ET0 = 1; //使能T0中断 TR0 = 1; //启动T0 } void InterruptTimer0() interrupt 1 //T0中断服务函数 { static unsigned char tmr200ms = 0; TH0 = t0rh; //定时器重新加载重载值 TL0 = t0rl; tmr200ms++; if (tmr200ms >= 200) //定时200ms { tmr200ms = 0; flag200ms = 1; } } void lcdInit() { lcdWriteCom(0x38); //设置8位数据格式,2行,5*7 lcdWriteCom(0x0c);//设置显示开,关光标,不闪烁。 lcdWriteCom(0x06);//写字符后,地址指针+1; lcdWriteCom(0x01);//显示清0,数据指针清0 delay_n10us(500); //延时5MS } void delay_n10us(uint n) //延时10us的整数倍。 { uchar a; uint b; for(b=n;b>0;b--) for(a=2;a>0;a--); } LCD1602.h #ifndef _LCD1602_H #define _LCD1602_H #include "reg52.h" #ifndef uint #define uint unsigned int #endif #ifndef uchar #define uchar unsigned char #endif #define LCD_DB P0 sbit LCD_RS=P2^6; sbit LCD_RW=P2^5; sbit LCD_E=P2^7; void lcdWriteData(uchar dat); void lcdWriteCom(uchar com); void lcdShowStr(unsigned char x,unsigned char y, const unsigned char *str); //void delay_n10us(uint n); #endif LCD1602.c #include "lcd1602.h" void delay_n10us(uint n); void lcdWriteCom(uchar com) { LCD_DB=com; LCD_RS=0; //指令 LCD_RW=0; //写入 LCD_E=1; //允许 delay_n10us(5); //不知道是什么原因,这里不加延时,第2行不能显示。 LCD_E=0; //产生下降沿 delay_n10us(5); } void lcdWriteData(uchar dat) { LCD_DB=dat; LCD_RS=1; //数据 LCD_RW=0; //写入 LCD_E=1; delay_n10us(5);//这里的延时有没有都可以。 LCD_E=0; delay_n10us(5); } void lcdShowStr(uchar x,uchar y, const uchar *str) { uchar i=0; if (y==0) lcdWriteCom(0x80+x); if (y==1) lcdWriteCom(0x80+0x40+x); while (*(str+i) != '\0') { lcdWriteData(*(str+i)); delay_n10us(5); i++; } } DS1302.h #ifndef _DS1302_H #define _DS1302_H #include #ifndef uchar #define uchar unsigned char #endif #ifndef uint #define uint unsigned int #endif sbit DS1302_IO = P3^4; // sbit DS1302_CE = P3^5; // sbit DS1302_CK = P3^6; // void DS1302SingleWrite(unsigned char reg,unsigned char dat); unsigned char DS1302SingleRead(unsigned char reg); #endif DS1302.c #include "ds1302.h" // ds1302读取的指令值 uchar code rtcReadCom[]={0x81, 0x83, 0x85, 0x87, 0x89,0x8b,0x8d}; //ds1302写入的指令值 uchar code rtcWriteCom[]={0x80, 0x82, 0x84, 0x86, 0x88, 0x8a, 0x8c}; //发送一个字节到DS1302通信总线上 void DS1302ByteWrite(unsigned char dat) { unsigned char mask; for (mask=0x01; mask!=0; mask DS1302_IO = 1; } else { DS1302_IO = 0; } DS1302_CK = 1; //然后拉高时钟 DS1302_CK = 0; //再拉低时钟,完成一个位的操作 } DS1302_IO = 1; //最后确保释放IO引脚????? } //由DS1302通信总线上读取一个字节 unsigned char DS1302ByteRead(void) { unsigned char mask; unsigned char dat = 0; for (mask=0x01; mask!=0; mask dat |= mask; } DS1302_CK = 1; //然后拉高时钟 DS1302_CK = 0; //再拉低时钟,完成一个位的操作 } return dat; //最后返回读到的字节数据 } //用单次模式向DS1302的某一寄存器写入一字节数据,寄存器地址reg,待写入字节dat void DS1302SingleWrite(unsigned char reg,unsigned char dat) { DS1302_CE = 1; //使能片选信号 DS1302ByteWrite((reg uint x; for(;y>0;y--) { for(x=110;x>0;x--); } } void delay10us(uint n) //延时n个10us { unsigned char a,b; for(b=n;b>0;b--) for(a=2;a>0;a--); }

前边学习了EEPROM的读写,因此DS1302的读写底层时序的程序应该没有什么问题,我就不过多解释了,大家自己认真揣摩一下。

3.6 DS1302的BURST模式

进行产品开发的时候,逻辑的严谨性非常重要,如果一个产品或者程序逻辑上不严谨,就有可能出现功能上的错误。比如上面的这个程序,我们再回顾一下。当单片机定时器时间到了200ms后,我们连续把DS1302的时间参数的7个字节读了出来。但是不管怎么读,都会有一个时间差,在极端的情况下就会出现这样一种情况:假如我们当前的时间是00:00:59,我们先读秒,读到的秒是59,然后再去读分钟,而就在读完秒到还未开始读分钟的这段时间内,刚好时间进位了,变成了00:01:00这个时间,我们读到的分钟就是01,显示在液晶上就会出现一个00:01:59,这个时间很明显是错误的。出现这个问题的概率极小,但确实实实在在可能存在的。

为了解决这个问题,芯片厂家肯定要给我们提供一种解决方案,这就是DS1302的突发模式。突发模式也分为RAM突发模式和时钟突发模式,RAM部分我们不讲,我们只看和时钟相关的clock burst mode。

当我们写指令到DS1302的时候,只要我们将要写的5位地址全部写1,即读操作用0xBF,写操作用0xBE,这样的指令送给DS1302之后,它就会自动识别出来是burst模式,马上把所有的8个字节同时锁存到另外的8个字节的寄存器缓冲区内,这样时钟继续走,而我们读数据是从另外一个缓冲区内读取的。同样的道理,如果我们用burst模式写数据,那么我们也是先写到这个缓冲区内,最终DS1302会把这个缓冲区内的数据一次性送到他的时钟寄存器内。 要注意的是,不管读写,只要使用时钟的burst模式,则必须一次性读写8个寄存器,要把时钟的寄存器完全读出来或者完全写进去。

下边就提供一个burst模式的例程给大家学习一下。

/***********************lcd1602.c文件程序源代码*************************/ 略 /***********************main.c文件程序源代码*************************/ #include sbit DS1302_CE = P1^7; //DS1302片选引脚 sbit DS1302_CK = P3^5; //DS1302通信时钟引脚 sbit DS1302_IO = P3^4; //DS1302通信数据引脚 bit flag200ms = 0; //200ms定时标志 unsigned char T0RH = 0; //T0重载值的高字节 unsigned char T0RL = 0; //T0重载值的低字节 void ConfigTimer0(unsigned int ms); void DS1302Init(void); void DS1302BurstRead(unsigned char *dat); extern void LcdInit(); extern void LcdShowStr(unsigned char x,unsigned char y, const unsigned char *str); void main () { unsigned char psec = 0xAA; //保存上一次读取的秒数,初值AA可以保证首次读取时间后必定刷新显示 unsigned char time[8]; //当前时间数组 unsigned char str[12]; //字符串转换缓冲区 LcdInit(); //初始化液晶 DS1302Init(); //初始化实时时钟 ConfigTimer0(1); //T0定时1ms EA = 1; //开总中断 while(1) { if (flag200ms) //每200ms读取依次时间 { flag200ms = 0; DS1302BurstRead(time); //读取DS1302当前时间 if (psec != time[0]) //检测到时间有变化时刷新显示 { str[0] = '2'; //添加年份的高2位:20 str[1] = '0'; str[2] = (time[6] >> 4) +'0';//“年”高位数字转换为ASCII码 str[3] = (time[6]&0x0F) +'0';//“年”低位数字转换为ASCII码 str[4] = '-'; //添加日期分隔符 str[5] = (time[4] >> 4) +'0'; //“月” str[6] = (time[4]&0x0F)+ '0'; str[7] = '-'; str[8] = (time[3] >> 4) +'0'; //“日” str[9] = (time[3]&0x0F) +'0'; str[10] = '\0'; LcdShowStr(0, 0, str); //显示到液晶的第一行 str[0] = (time[5]&0x0F) +'0'; //“星期” str[1] = '\0'; LcdShowStr(11, 0,"week"); LcdShowStr(15, 0, str); //显示到液晶的第一行 str[0] = (time[2] >> 4) +'0'; //“时” str[1] = (time[2]&0x0F) +'0'; str[2] = ':'; //添加时间分隔符 str[3] = (time[1] >> 4) +'0'; //“分” str[4] = (time[1]&0x0F) +'0'; str[5] = ':'; str[6] = (time[0] >> 4) +'0'; //“秒” str[7] = (time[0]&0x0F) +'0'; str[8] = '\0'; LcdShowStr(4, 1, str); //显示到液晶的第二行 psec = time[0]; //用当前值更新上次秒数 } } } } void DS1302ByteWrite(unsigned chardat) //发送一个字节到DS1302通信总线上 { unsigned char mask; for (mask=0x01; mask!=0; mask DS1302_IO = 1; } else { DS1302_IO = 0; } DS1302_CK = 1; //然后拉高时钟 DS1302_CK = 0; //再拉低时钟,完成一个位的操作 } DS1302_IO = 1; //最后确保释放IO引脚 } unsigned char DS1302ByteRead(void) //由DS1302通信总线上读取一个字节 { unsigned char mask; unsigned char dat = 0; for (mask=0x01; mask!=0; mask dat |= mask; } DS1302_CK = 1; //然后拉高时钟 DS1302_CK = 0; //再拉低时钟,完成一个位的操作 } return dat; //最后返回读到的字节数据 } void DS1302SingleWrite(unsigned char reg,unsigned char dat) //用单次模式向DS1302的某一寄存器写入一字节数据,寄存器地址reg,待写入字节dat { DS1302_CE = 1; //使能片选信号 DS1302ByteWrite((reg unsigned char i; DS1302_CE = 1; DS1302ByteWrite(0xBE); //发送突发写寄存器指令 for (i=0; i unsigned char i; DS1302_CE = 1; DS1302ByteWrite(0xBF); //发送突发读寄存器指令 for (i=0; i unsigned char dat; unsigned char code InitTime[] = {0x00,0x30,0x12, 0x08, 0x10, 0x02, 0x13,0x00}; //2013年10月8日星期二 12:30:00 DS1302_CE = 0; //初始化DS1302通信引脚 DS1302_CK = 0; dat = DS1302SingleRead(0); //读取秒寄存器 if ((dat & 0x80) != 0) //由秒寄存器最高位CH的值判断DS1302是否已停止 { DS1302SingleWrite(7, 0x00); //撤销写保护以允许写入数据 DS1302BurstWrite(InitTime); //设置DS1302为默认的初始时间 } } void ConfigTimer0(unsigned int ms) //T0配置函数 { unsigned long tmp; tmp = 11059200 / 12; //定时器计数频率 tmp = (tmp * ms) / 1000; //计算所需的计数值 tmp = 65536 - tmp; //计算定时器重载值 tmp = tmp + 12; //修正中断响应延时造成的误差 T0RH = (unsigned char)(tmp >> 8); //定时器重载值拆分为高低字节 T0RL = (unsigned char)tmp; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0为模式1 TH0 = T0RH; //加载T0重载值 TL0 = T0RL; ET0 = 1; //使能T0中断 TR0 = 1; //启动T0 } void InterruptTimer0() interrupt 1 //T0中断服务函数 { static unsigned char tmr200ms = 0; TH0 = T0RH; //定时器重新加载重载值 TL0 = T0RL; tmr200ms++; if (tmr200ms >= 200) //定时200ms { tmr200ms = 0; flag200ms = 1; } } 4. 结构体数据类型

我们在前边学数据类型的时候,主要是字符型、整型、浮点型等基本类型,而学数组的时候,数组的定义要求数组元素必须是相同的数据类型。在实际应用中,有时候还需要把不同类型的数据组成一个有机的整体来处理,这些组合在一个整体中的数据之间还有一定的联系,比如一个学生的姓名、性别、年龄、考试成绩等,这就引入了复合数据类型。复合数据类型主要包含结构体数据类型、共用体数据类型和枚举体数据类型,我们本节主要要学习一下结构体数据类型。

首先我们回顾一下上面的例程,我们把DS1302的7个字节的时间放到一个缓冲数组中,然后把数组中的值稍作转换显示到液晶上,这里就存在一个小问题,DS1302时间寄存器的定义并不是我们常用的“年月日时分秒”的顺序,而是在中间加了一个字节的“星期几”,而且每当我要用这个时间的时候都要清楚的记得数组的第几个元素表示的是什么,这样一来,一是很容易出错,而是程序的可读性不强。当然你可以把每一个元素都定一个明确的变量名字,这样就不容易出错也易读了,但结构上却显得很零散了。于是,我们就可以用结构体来将这一组彼此相关的数据做一个封装,他们既组成了一个整体,易读不易错。而且可以单独定义其中每一个成员的数据类型,比如说把年份用unsigned int类型,即4个十进制位来表示显然比2位更符合日常习惯,而其它的类型还是可以用2位来表示。结构体本身不是一个基本的数据类型,而是构造的,它每个成员可以是一个基本的数据类型或者是一个构造类型。结构体既然是一种构造而成的数据类型,那么在使用之前必须先定义它。

声明结构体变量的一般格式如下:

struct 结构体名 { 类型1 变量名1; 类型2 变量名2; ... 类型n 变量名n; } 结构体变量名;

这种声明方式仅仅是一个结构体变量的声明方式,但是在实际应用中,可能需要多个具有相同形式的结构体变量,这种定义方式就不是很方便了,因此我们推荐以下方式:

struct 结构体名 { 类型1 变量名1; 类型2 变量名2; ... 类型n 变量名n; } ;

struct 结构体名 结构体变量名1,结构体变量名2,...结构体变量名n;

为了方便大家理解,我来构造一个实际的表示日期时间的结构体。

struct sTime { //日期时间结构体定义 unsigned int year; //年 unsigned char mon; //月 unsigned char day; //日 unsigned char hour; //时 unsigned char min; //分 unsigned char sec; //秒 unsigned char week; //星期 }; struct sTime bufTime;

Struct是结构体类型的关键字,sTime是这个结构体的名字,bufTime就是定义了一个具体的结构体变量。那如果要给结构体变量的成员赋值的话,写法是

bufTime.year = 2013; bufTime.mon = 10;

数组的元素也可以是结构体类型,因此可以构成结构体数组,结构体数组的每一个元素都是具有想同结构类型的结构体变量。例如我们前边构造的这个结构类型,直接定义成struct sTime bufTime[3];就表示定义了一个结构体数组,这个数组的3个元素,每一个都是一个结构体变量。同样的道理,结构体数组中的元素的成员如果需要赋值,就可以写成

bufTime[0].year = 2013; bufTime[0].mon = 10;

一个指针变量如果指向了一个结构体变量的时候,称之为结构指针变量。结构指针变量中的值是指向的结构体变量的首地址,通过结构体指针也可以访问到这个结构变量。

结构指针变量声明的一般形式如下:

struct sTime *pbufTime;

这里要特别注意的是,使用结构体指针对结构体成员的访问,和使用结构体变量名对结构体成员的访问,其表达式有所不同。结构体指针对结构体成员的访问表达式为

pbufTime->year = 2013; 或者是 (*pbufTime).year = 2013;

很明显前者更简洁,所以大都使用前者。

介绍结构体数据类型要干嘛,毫无疑问要应用在我们的程序中。下边这个程序的功能相当于一个万年历了,并且加入了按键调时功能。学有余力的同学看到这里,不妨先不看我提供的代码,自己写写试试。如果能够独立写一个按键可调的万年历程序,单片机可以说基本入门了。如果自己还不能够独立完成这个程序,那么还是老规矩,先抄并且理解,而后自己独立默写出来,并且要边默写边理解。

本例直接忽略了星期这项内容,通过上、下、左、右、回车、ESC这6个按键可以调整时间。简单说一下这个程序的几个要点,方便大家阅读理解程序。

定义一个结构体类型sTime用来封装日期时间的各个元素,又用该结构体定义了一个结构体时间缓冲区变量bufTime来暂存从DS1302读出的时间和设置时间时的设定值。需要注意的是在其它文件中要使用这个结构体变量时,必须首先再声明一次sTime类型;

定义一个变量setIndex来控制当前是否处于设置时间的状态,以及设置时间的哪一位,该值为0就表示正常运行,1-12分别代表可以修改日期时间的12个位;

由于这节课的程序功能要进行时间调整,用到了1602液晶的光标功能,添加了设置光标的函数,我们要改变哪一位的数字,就在1602对应位置上进行光标闪烁,所以Lcd1602.c程序和之前的有所不同;

时间的显示、增减、设置移位等上层功能函数都放在main.c中来实现,当按键需要这些函数时则在按键文件中做外部声明,这样做是为了避免一组功能函数分散在不同的文件内而使程序显得凌乱。

/***********************lcd1602.c文件程序源代码*************************/ #include sbit DS1302_CE = P1^7; //DS1302片选引脚 sbit DS1302_CK = P3^5; //DS1302通信时钟引脚 sbit DS1302_IO = P3^4; //DS1302通信数据引脚 struct sTime { //日期时间结构体定义 unsigned int year; //年 unsigned char mon; //月 unsigned char day; //日 unsigned char hour; //时 unsigned char min; //分 unsigned char sec; //秒 unsigned char week; //星期 }; void DS1302ByteWrite(unsigned chardat) //发送一个字节到DS1302通信总线上 { unsigned char mask; for (mask=0x01; mask!=0; mask DS1302_IO = 1; } else { DS1302_IO = 0; } DS1302_CK = 1; //然后拉高时钟 DS1302_CK = 0; //再拉低时钟,完成一个位的操作 } DS1302_IO = 1; //最后确保释放IO引脚 } unsigned char DS1302ByteRead(void) //由DS1302通信总线上读取一个字节 { unsigned char mask; unsigned char dat = 0; for (mask=0x01; mask!=0; mask dat |= mask; } DS1302_CK = 1; //然后拉高时钟 DS1302_CK = 0; //再拉低时钟,完成一个位的操作 } return dat; //最后返回读到的字节数据 } void DS1302SingleWrite(unsigned char reg,unsigned char dat) //用单次模式向DS1302的某一寄存器写入一字节数据,寄存器地址reg,待写入字节dat { DS1302_CE = 1; //使能片选信号 DS1302ByteWrite((reg unsigned char i; DS1302_CE = 1; DS1302ByteWrite(0xBE); //发送突发写寄存器指令 for (i=0; i unsigned char i; DS1302_CE = 1; DS1302ByteWrite(0xBF); //发送突发读寄存器指令 for (i=0; i unsigned char buf[8]; DS1302BurstRead(buf); time->year = buf[6] + 0x2000; time->mon = buf[4]; time->day = buf[3]; time->hour = buf[2]; time->min = buf[1]; time->sec = buf[0]; time->week = buf[5]; } void SetRealTime(struct sTime *time) //设定实时时间,即将当前时间写入DS1302 { unsigned char buf[8]; buf[7] = 0; buf[6] = time->year; buf[5] = time->week; buf[4] = time->mon; buf[3] = time->day; buf[2] = time->hour; buf[1] = time->min; buf[0] = time->sec; DS1302BurstWrite(buf); } void DS1302Init(void) //DS1302初始化 { unsigned char dat; struct sTime code InitTime[] = {0x2013,0x10,0x08, 0x12,0x30,0x00, 0x02};//2013年10月8日 12:30:00 星期二 DS1302_CE = 0; //初始化DS1302通信引脚 DS1302_CK = 0; dat = DS1302SingleRead(0); //读取秒寄存器 if ((dat & 0x80) != 0) //由秒寄存器最高位CH的值判断DS1302是否已停止 { DS1302SingleWrite(7, 0x00); //撤销写保护以允许写入数据 SetRealTime(&InitTime); //设置DS1302为默认的初始时间 } } /***********************main.c文件程序源代码*************************/ #include struct sTime { //日期时间结构体定义 unsigned int year; unsigned char mon; unsigned char day; unsigned char hour; unsigned char min; unsigned char sec; unsigned char week; }; struct sTime bufTime; //日期时间缓冲区 unsigned char setIndex = 0; //时间设置索引 bit flag200ms = 1; //200ms定时标志 unsigned char T0RH = 0; //T0重载值的高字节 unsigned char T0RL = 0; //T0重载值的低字节 void ConfigTimer0(unsigned int ms); void RefreshTimeShow(); extern void DS1302Init(void); extern void GetRealTime(struct sTime*time); extern void SetRealTime(struct sTime*time); extern void KeyDrive(); extern void KeyScan(); extern void LcdInit(); extern void LcdShowStr(unsigned char x,unsigned char y, const unsigned char *str); extern void LcdSetCursor(unsigned char x,unsigned char y); void main () { unsigned char psec = 0xAA; //保存上一次读取的秒数,初值AA可以保证首次读取时间后必定刷新显示 LcdInit(); //初始化液晶 DS1302Init(); //初始化实时时钟 ConfigTimer0(1); //T0定时1ms EA = 1; //开总中断 //初始化屏幕上固定不变的内容 LcdShowStr(3, 0, "20 - - "); LcdShowStr(4, 1, " : : "); while(1) { KeyDrive(); if (flag200ms && (setIndex == 0)) //每隔200ms且未处于设置状态时, { flag200ms = 0; GetRealTime(&bufTime); //获取当前时间 if (psec != bufTime.sec) //检测到时间有变化时刷新显示 { RefreshTimeShow(); psec = bufTime.sec; //用当前值更新上次秒数 } } } } void ShowBcdByte(unsigned char x, unsignedchar y, unsigned char bcd) //将一个BCD码字节显示到屏幕上 { unsigned char str[4]; str[0] = (bcd >> 4) + '0'; str[1] = (bcd&0x0F) + '0'; str[2] = '\0'; LcdShowStr(x, y, str); } void RefreshTimeShow() //刷新日期时间的显示 { ShowBcdByte(5, 0, bufTime.year); ShowBcdByte(8, 0, bufTime.mon); ShowBcdByte(11, 0, bufTime.day); ShowBcdByte(4, 1, bufTime.hour); ShowBcdByte(7, 1, bufTime.min); ShowBcdByte(10, 1, bufTime.sec); } void RefreshSetShow() //刷新当前设置位的光标指示 { switch (setIndex) { case 1: LcdSetCursor(5, 0); break; case 2: LcdSetCursor(6, 0); break; case 3: LcdSetCursor(8, 0); break; case 4: LcdSetCursor(9, 0); break; case 5: LcdSetCursor(11, 0);break; case 6: LcdSetCursor(12, 0);break; case 7: LcdSetCursor(4, 1); break; case 8: LcdSetCursor(5, 1); break; case 9: LcdSetCursor(7, 1); break; case 10: LcdSetCursor(8, 1);break; case 11: LcdSetCursor(10, 1); break; case 12: LcdSetCursor(11, 1); break; default: break; } } unsigned char IncBcdHigh(unsigned charbcd) //递增一个BCD码的高位 { if ((bcd&0xF0) if ((bcd&0xF0) > 0x00) bcd -= 0x10; else bcd |= 0x90; return bcd; } unsigned char DecBcdLow(unsigned charbcd) //递减一个BCD码的低位 { if ((bcd&0x0F) > 0x00) bcd -= 0x01; else bcd |= 0x09; return bcd; } void IncSetTime() //递增时间当前设置位的值 { switch (setIndex) { case 1: bufTime.year =IncBcdHigh(bufTime.year); break; case 2: bufTime.year =IncBcdLow(bufTime.year); break; case 3: bufTime.mon = IncBcdHigh(bufTime.mon); break; case 4: bufTime.mon = IncBcdLow(bufTime.mon); break; case 5: bufTime.day = IncBcdHigh(bufTime.day); break; case 6: bufTime.day = IncBcdLow(bufTime.day); break; case 7: bufTime.hour =IncBcdHigh(bufTime.hour); break; case 8: bufTime.hour =IncBcdLow(bufTime.hour); break; case 9: bufTime.min = IncBcdHigh(bufTime.min); break; case 10: bufTime.min =IncBcdLow(bufTime.min); break; case 11: bufTime.sec =IncBcdHigh(bufTime.sec); break; case 12: bufTime.sec =IncBcdLow(bufTime.sec); break; default: break; } RefreshTimeShow(); RefreshSetShow(); } void DecSetTime() //递减时间当前设置位的值 { switch (setIndex) { case 1: bufTime.year =DecBcdHigh(bufTime.year); break; case 2: bufTime.year =DecBcdLow(bufTime.year); break; case 3: bufTime.mon = DecBcdHigh(bufTime.mon); break; case 4: bufTime.mon = DecBcdLow(bufTime.mon); break; case 5: bufTime.day = DecBcdHigh(bufTime.day); break; case 6: bufTime.day = DecBcdLow(bufTime.day); break; case 7: bufTime.hour =DecBcdHigh(bufTime.hour); break; case 8: bufTime.hour =DecBcdLow(bufTime.hour); break; case 9: bufTime.min = DecBcdHigh(bufTime.min); break; case 10: bufTime.min =DecBcdLow(bufTime.min); break; case 11: bufTime.sec =DecBcdHigh(bufTime.sec); break; case 12: bufTime.sec =DecBcdLow(bufTime.sec); break; default: break; } RefreshTimeShow(); RefreshSetShow(); } void RightShiftTimeSet() //右移时间设置位 { if (setIndex != 0) { if (setIndex if (setIndex > 1) setIndex--; else setIndex = 12; RefreshSetShow(); } } void ConfigTimer0(unsigned int ms) //T0配置函数 { unsigned long tmp; tmp = 11059200 / 12; //定时器计数频率 tmp = (tmp * ms) / 1000; //计算所需的计数值 tmp = 65536 - tmp; //计算定时器重载值 tmp = tmp + 12; //修正中断响应延时造成的误差 T0RH = (unsigned char)(tmp >> 8); //定时器重载值拆分为高低字节 T0RL = (unsigned char)tmp; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0为模式1 TH0 = T0RH; //加载T0重载值 TL0 = T0RL; ET0 = 1; //使能T0中断 TR0 = 1; //启动T0 } void InterruptTimer0() interrupt 1 //T0中断服务函数 { static unsigned char tmr200ms = 0; TH0 = T0RH; //定时器重新加载重载值 TL0 = T0RL; KeyScan(); //按键扫描 tmr200ms++; if (tmr200ms >= 200) //定时200ms { tmr200ms = 0; flag200ms = 1; } }


【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭