这两个芯片一个用单总线(OneWire),一个用三线SPI,接口完全不同,但它们有一些共同点:都需要严格的时序、都需要注意中断保护。这一篇把它们放在一起讲,方便对比理解。
DS18B20温度传感器
基本信息
DS18B20是达拉斯半导体(现Maxim)推出的数字温度传感器,通过单总线通信,只需要一根数据线就能完成双向数据传输。
| 数据线 | P1.4(DQ) |
| 温度范围 | -55°C ~ +125°C |
| 精度 | 9~12位可配置(默认12位,0.0625°C/LSB) |
| 转换时间 | 750ms(12位精度) |
| 上电默认值 | 85°C(这是个坑) |
底层驱动(项目实际代码)
#include <STC15F2K60S2.H>
#include <intrins.h>
sbit DQ = P1^4; // DS18B20数据线
/* 单总线延时函数(12MHz下每个循环约2us)*/
void Delay_OneWire(unsigned int t) {
unsigned char i;
while(t–) {
for(i = 0; i < 12; i++);
}
}
/* 复位+检测存在脉冲 */
bit init_ds18b20(void) {
bit initflag = 0;
DQ = 1;
Delay_OneWire(12); // 释放总线
DQ = 0;
Delay_OneWire(80); // 拉低480us以上(复位脉冲)
DQ = 1;
Delay_OneWire(10); // 等待存在脉冲
initflag = DQ; // 读到0=存在,读到1=无应答
Delay_OneWire(5);
return initflag;
}
/* 写一个字节(LSB优先)*/
void Write_DS18B20(unsigned char dat) {
unsigned char i;
for(i = 0; i < 8; i++) {
DQ = 0; // 拉低,开始写时隙
DQ = dat & 0x01; // 放数据(最低位先发)
Delay_OneWire(5); // 保持60us以上
DQ = 1; // 释放
dat >>= 1; // 右移
}
Delay_OneWire(5);
}
/* 读一个字节 */
unsigned char Read_DS18B20(void) {
unsigned char i, dat;
for(i = 0; i < 8; i++) {
DQ = 0; // 拉低,开始读时隙
dat >>= 1;
DQ = 1; // 释放
if(DQ) dat |= 0x80; // 读到1则最高位置1
Delay_OneWire(5);
}
return dat;
}
有几个关键细节要注意:
1) 写操作是LSB优先:dat & 0x01 先发最低位,然后 dat >>= 1 右移。这跟I2C的MSB优先正好相反。
2) 读操作的"先右移后置位":读的时候先 dat >>= 1(腾出最低位),然后读到的数据放在 dat |= 0x80(放在最高位)。这样第一个读到的位最终会移到最高位——虽然读取顺序是LSB优先,但读完之后byte中的位排列是正确的。
温度读取函数
float rd_Tem() {
unsigned char Low, High;
init_ds18b20();
Write_DS18B20(0xcc); // 跳过ROM(总线上只有一个设备)
Write_DS18B20(0x44); // 启动温度转换
Delay_OneWire(200); // 等待转换(实际12位精度需要750ms,这里不够!)
init_ds18b20();
Write_DS18B20(0xcc);
Write_DS18B20(0xbe); // 读取暂存器
Low = Read_DS18B20(); // 温度低字节
High = Read_DS18B20(); // 温度高字节
return (float)(Low | (High << 8)) / 16.0; // 12位精度:除以16
}
温度数据格式:DS18B20返回16位有符号数,12位精度下:
- Bit15~Bit11:符号位(S)
- Bit10~Bit4:整数部分
- Bit3~Bit0:小数部分
所以 原始值 / 16.0 就得到实际温度。例如 0x0191 = 401,401/16.0 = 25.0625°C。
注意 Delay_OneWire(200) 的时间问题:12位精度转换需要750ms,但这里只等了约400us。实际上这段代码是配合调度器使用的——Read_Tem 在调度器中每100ms调用一次,第一次调用启动转换,后续调用才能读到有效数据。第一次读到的是上一次的转换结果或默认值85°C。
第16届省赛的两个重要改进
改进1:读写时关中断保护
void Write_DS18B20(unsigned char dat) {
unsigned char i;
EA = 0; // 关闭中断!
for(i = 0; i < 8; i++) {
DQ = 0;
DQ = dat & 0x01;
Delay_OneWire(5);
DQ = 1;
dat >>= 1;
}
Delay_OneWire(5);
EA = 1; // 恢复中断
}
第16届省赛代码对Write和Read函数都加了EA保护。因为Timer1中断每1ms触发一次,而单总线的读写时隙只有几十微秒,如果中断正好在时隙中间触发,可能会破坏时序。
改进2:精度可配置
unsigned char accuracy_data[4] = {0x1F, 0x3F, 0x5F, 0x7F};
// 对应9位(93.75ms)、10位(187.5ms)、11位(375ms)、12位(750ms)
void Write_Accuracy(unsigned char accuracy) {
Init_Ds18b20();
Write_DS18B20(0xcc);
Write_DS18B20(0x4e); // 写暂存寄存器命令
Write_DS18B20(0xFE); // TH寄存器(过温报警值)
Write_DS18B20(0x01); // TL寄存器(低温报警值)
Write_DS18B20(accuracy_data[accuracy]); // 配置寄存器
}
上电85°C问题
DS18B20上电后的默认温度读数是85°C(0x0550),这是芯片内部的默认值。如果你上电后第一次读取就直接用,就会在数码管上看到85°C然后突然跳变。
第16届省赛代码的处理方式很直接:
void main(void) {
System_Init();
while((temperature = Read_Temperature()) == 85); // 一直等到不再是85
Timer1_Init();
EA = 1;
// …
}
在启动定时器之前,循环读取温度直到读到有效值。虽然会阻塞几秒钟,但保证进入主循环时温度数据就是对的。
DS1302实时时钟
基本信息
DS1302是Maxim/Dallas的三线接口RTC芯片,跟I2C和OneWire都不一样。
| SCK(时钟线) | P1.7 |
| RST/CE(使能线) | P1.3 |
| SDA(数据线) | P2.3 |
| 时间格式 | BCD码(不是普通十进制!) |
| 备用电源 | 外接32.768kHz晶振+纽扣电池 |
BCD码转换
DS1302内部存储的时间是BCD码格式,不是十进制。BCD码的规则是每个十进制数字占4位:
十进制 59 → BCD码 0x59 → 二进制 0101_1001
读取BCD码:十位 = (0x59 >> 4) = 5, 个位 = (0x59 & 0x0F) = 9
所以 59 = (0x59 / 16) * 10 + (0x59 % 16) = 5*10 + 9 = 59
写入BCD码:59 → (59/10)*16 + (59%10) = 5*16 + 9 = 89 = 0x59
所以 十进制 → BCD码:value / 10 * 16 + value % 10
底层驱动
#include <STC15F2K60S2.H>
#include <intrins.h>
sbit SCK = P1^7; // 时钟线
sbit RST = P1^3; // 使能线
sbit SDA = P2^3; // 数据线
/* 写一个字节(LSB优先,跟I2C相反!)*/
void Write_Ds1302(unsigned char temp) {
unsigned char i;
for(i = 0; i < 8; i++) {
SCK = 0;
SDA = temp & 0x01; // 最低位先发
temp >>= 1;
SCK = 1; // 上升沿锁存
}
}
/* 写寄存器 */
void Write_Ds1302_Byte(unsigned char address, unsigned char dat) {
RST = 0; _nop_();
SCK = 0; _nop_();
RST = 1; _nop_(); // 使能
Write_Ds1302(address); // 写地址
Write_Ds1302(dat); // 写数据
RST = 0; // 关闭
}
/* 读寄存器 */
unsigned char Read_Ds1302_Byte(unsigned char address) {
unsigned char i, temp = 0x00;
RST = 0; _nop_();
SCK = 0; _nop_();
RST = 1; _nop_();
Write_Ds1302(address); // 写地址(读操作)
for(i = 0; i < 8; i++) {
SCK = 0;
temp >>= 1;
if(SDA) temp |= 0x80;
SCK = 1;
}
RST = 0; _nop_();
SCK = 0; _nop_();
SCK = 1; _nop_();
SDA = 0; _nop_(); // 释放数据线
SDA = 1; _nop_();
return temp;
}
寄存器地址表
寄存器 | 写地址 | 读地址 | 说明
——–|——–|——–|——
秒 | 0x80 | 0x81 | bit7是CH位(0=计时,1=停止)
分 | 0x82 | 0x83 |
时 | 0x84 | 0x85 |
日 | 0x86 | 0x87 |
月 | 0x88 | 0x89 |
周 | 0x8A | 0x8B |
年 | 0x8C | 0x8D |
写保护 | 0x8E | 0x8F | 0x00=关闭,0x80=开启
写保护必须注意:上电时写保护是开启的,写时间之前必须先关闭写保护(写0x00到0x8E),写完之后再开启(写0x80到0x8E)。而且写时间之前必须先停止计时(写0x80到秒寄存器),否则在计时过程中修改时间可能导致数据混乱。
设置时间
/* 时间数组:{时, 分, 秒} */
unsigned char ucRtc[3] = {23, 59, 59};
void Set_Rtc(unsigned char *ucRtc) {
unsigned char i;
Write_Ds1302_Byte(0x8e, 0x00); // 关闭写保护
Write_Ds1302_Byte(0x80, 0x80); // 停止计时(秒寄存器CH位置1)
for(i = 0; i < 3; i++)
Write_Ds1302_Byte(0x84 – 2*i, ucRtc[i]/10*16 + ucRtc[i]%10);
// 0x84(时) → 0x82(分) → 0x80(秒)
// 十进制转BCD码
Write_Ds1302_Byte(0x8e, 0x80); // 重新开启写保护
}
注意地址计算:0x84 – 2*i,当i=0时是0x84(时),i=1时是0x82(分),i=2时是0x80(秒)。写完时、分后,最后写秒寄存器会自动清除CH位,时钟开始走。
读取时间
void Read_Rtc(unsigned char *ucRtc) {
unsigned char i, Temp;
EA = 0; // 关中断!
for(i = 0; i < 3; i++) {
Temp = Read_Ds1302_Byte(0x85 – 2*i);
// 0x85(时) → 0x83(分) → 0x81(秒)
ucRtc[i] = Temp/16*10 + Temp%16; // BCD码转十进制
}
EA = 1; // 开中断
}
读取时为什么要关中断?因为DS1302的SCK引脚连接P1.7,而数码管扫描在Timer1中断中会操作P2口。虽然不会直接影响P1.7,但为了安全起见,读RTC时关中断是好习惯。如果你在读RTC的过程中正好到了数码管扫描的代码,虽然理论上不会冲突(不同端口),但有些边界条件下可能会出问题。
数码管时间显示
/* 显示格式:HH-MM-SS */
Seg_Buf[2] = Seg_Buf[5] = 12; // 用'-'分隔
for(i = 0; i < 3; i++) {
Seg_Buf[3*i] = ucRtc[i] / 10; // 十位
Seg_Buf[3*i + 1] = ucRtc[i] % 10; // 个位
}
DS18B20 vs DS1302 对比
| 通信协议 | 单总线(OneWire) | 三线制(类SPI) |
| 数据线数量 | 1根 | 3根(SCK/RST/SDA) |
| 数据方向 | LSB优先 | LSB优先 |
| 中断保护 | 推荐 | 必须 |
| 延时要求 | 转换需750ms | 无特殊延时 |
| 初始值问题 | 85°C假值 | 需手动设置时间 |
| 读取陷阱 | 首次读数是85°C | 时间不走检查CH位 |


