简介:本资源是一套面向嵌入式初学者与8051单片机开发者的1-Wire总线通信实践代码包,聚焦于单总线协议在C51环境下的底层实现与硬件协同控制,适用于温度传感(如DS18B20)、分布式ID识别等低功耗传感器网络开发场景。压缩包共17个文件,49KB,涵盖C语言源码(.c)、汇编实现(.asm)、Keil工程配置(.uv2、.opt、.lnp)、编译输出(.hex、.lst、.obj、.m51)及头文件(.h),其中owire.asm与ledtest.c为核心通信模块,完整呈现初始化、复位应答、ROM搜索、命令发送与时序精准控制等关键流程;www.pudn.com.txt提供原始来源说明。已有317人学习下载,资源结构清晰、工程可直接加载Keil uVision2编译运行,附带汇编级时序注释与C51接口封装,便于理解单线双向通信的物理层约束与软件抗干扰设计逻辑。
1. 项目缘起:为什么还在折腾1-Wire和C51?
最近在整理一个老项目的资料,翻出来一个名为“one.zip”的压缩包,里面是十几年前用C51单片机驱动1-Wire总线的程序。看到这个,估计不少年轻点的工程师会有点懵:这都什么年代了,还在用51单片机?还在玩1-Wire这种“古老”的总线?确实,现在STM32、ESP32满天飞,I2C、SPI、CAN总线才是主流,1-Wire似乎只活在大学实验课和温湿度传感器DS18B20的例程里。
但恰恰是这种“古老”的技术组合,在今天依然有它的价值。很多存量设备、工业现场、低成本传感器节点,核心依然是那颗经典的8051内核单片机。当你需要为一个老设备添加一个DS18B20测温,或者读取一个iButton的ID时,现成的库可能不好找,或者臃肿不堪,这时一个精简、可靠的裸机驱动就显得尤为珍贵。这个“one.zip”里的代码,就是这样一个产物。它不是最先进的,但它是经过实际项目验证、能在最有限的资源(比如只有256字节RAM的STC89C52)上稳定跑起来的“手搓”方案。
所以,这篇文章不是一篇前沿技术科普,而是一次“考古”与“重构”。我会带你重新梳理1-Wire总线的通信原理,然后用最“原始”的C51编程方式,从时序模拟开始,一步步实现一个健壮的1-Wire主机驱动。过程中,你会深刻理解“位操作”的精髓,体会到在资源极度受限环境下编程的乐趣与挑战。无论你是想维护老代码,还是单纯想夯实底层硬件驱动的基础,这篇内容都会很有料。
2. 1-Wire总线协议深度拆解:单线背后的精妙设计
1-Wire,顾名思义,单线通信。一根数据线(DQ),既要传数据,又要给从设备供电(通过寄生供电方式),还要协调多个设备(通过搜索ROM序列号)。它的设计哲学是在极简的硬件成本下,实现一套完整的主从式、半双工、低速串行通信协议。理解其底层时序,是写出稳定驱动的前提。
2.1 基础时序:复位、写1、写0、读
1-Wire通信的最小单位是“时隙”(time slot),通常为60微秒到120微秒。所有操作都由主机发起并严格控制时序。
复位脉冲与存在脉冲:这是每次通信的“握手”阶段。主机拉低DQ线至少480微秒,然后释放(上拉电阻将总线拉高)。之后,主机会切换到输入模式,监测总线。如果总线上有1-Wire从设备,它会在主机释放总线后的15-60微秒内,主动拉低总线60-240微秒,这个低电平脉冲就是“存在脉冲”(Presence Pulse)。主机检测到这个脉冲,就知道总线上有设备准备就绪。
注意:这个“释放”和“监测”的时机非常关键。在C51上,我们通常用软件延时来模拟时序,但必须考虑函数调用、循环本身带来的微小开销。一个常见的坑是,主机释放总线后,切换到输入模式的速度不够快,可能错过从设备发出的早期存在脉冲。
写时隙:写1和写0共用同一种启动方式:主机拉低总线,启动一个时隙。
- 写1时隙:主机拉低总线后,必须在15微秒内释放总线(拉高)。剩下的时隙时间内,总线由上拉电阻维持高电平。
- 写0时隙:主机拉低总线后,需要持续保持低电平60-120微秒,然后释放。
从设备会在主机启动时隙后的15-60微秒这个窗口内对总线进行采样。如果是高电平,就认为是“1”;低电平,就认为是“0”。所以,写1的本质是主机产生一个很短的脉冲(>1微秒但<15微秒),写0则是产生一个长的低电平脉冲。
读时隙:读数据也由主机发起。主机拉低总线至少1微秒,然后释放。在释放后的15微秒内,从设备会开始驱动总线:如果想输出1,它就什么都不做(总线被上拉电阻拉高);如果想输出0,它就主动拉低总线。因此,主机需要在启动读时隙后,延迟大约15微秒(这个延迟很关键,要给从设备足够的响应时间),再去读取DQ线的电平状态,这个状态就是读到的数据位。
2.2 寄生供电与强上拉
这是1-Wire的另一个精妙之处。很多1-Wire器件(如DS18B20)可以不用额外电源线,直接从数据线“偷电”来工作,这就是寄生供电。在数据传输间隙,DQ线被上拉电阻(通常4.7kΩ)拉到高电平,器件内部的电容器被充电,用以维持其逻辑电路工作。
但是,当器件进行温度转换(DS18B20)或写入EEPROM时,需要的电流较大(可达1.5mA),仅靠弱上拉电阻无法提供。这时就需要“强上拉”:主机需要主动将DQ线通过一个MOS管直接拉到电源(VCC),持续一段时间(如DS18B20温度转换需要至少750ms)。在C51驱动中,我们通常用一个GPIO引脚控制这个MOS管。忘记在需要时使能强上拉,是导致DS18B20温度转换失败或读取数据为85℃(上电默认值)的最常见原因。
2.3 ROM命令与功能命令:访问多设备的基石
1-Wire协议通过唯一的64位ROM序列号来寻址总线上的多个设备。ROM序列号包含8位家族代码、48位序列号和8位CRC校验码。
通信流程通常是:复位→存在脉冲→发送ROM命令→发送功能命令。
- ROM命令用于选择设备,例如:
0x33(Read ROM):读取单个设备ROM,仅适用于单设备总线。0x55(Match ROM):匹配指定ROM序列号,用于访问特定设备。0xCC(Skip ROM):跳过ROM寻址,用于总线只有一个设备,或向所有设备广播命令(如启动所有DS18B20转换)。0xF0(Search ROM):搜索ROM,用于发现总线上的所有设备,这是实现多设备识别的核心算法。
- 功能命令是器件特定的,例如DS18B20的
0x44(启动温度转换)、0xBE(读取暂存器)。
3. C51平台下的1-Wire驱动实现:精准的软件模拟
在C51上,我们通常没有硬件1-Wire控制器,所以必须用GPIO口来软件模拟时序。这要求代码对时序的控制必须非常精确。我们以最常见的P1^0引脚为例。
3.1 硬件连接与宏定义
首先,确保硬件上,DQ线通过一个4.7kΩ的上拉电阻接到VCC。如果需要强上拉,可以用另一个GPIO(如P1^1)控制一个PNP三极管或MOS管,将DQ线强拉到VCC。
#include <REG52.H> #include <intrins.h> // 用于_nop_()空操作指令 sbit DQ = P1^0; // 1-Wire数据线 sbit DQ_PWR = P1^1; // 强上拉控制引脚,0有效 // 延时函数声明,需要根据实际晶振频率校准 void DelayUs(unsigned int t); // 微秒级延时 void DelayMs(unsigned int t); // 毫秒级延时微秒级延时DelayUs是时序模拟的灵魂。在12MHz晶振的51单片机中,一个机器周期是1微秒。我们可以用_nop_()(一个空操作,消耗1个机器周期)或循环来构建。
// 粗略的微秒延时,12MHz晶振下近似 void DelayUs(unsigned int t) { while (t--) { _nop_(); _nop_(); _nop_(); // 调整_nop_数量来校准 } }实操心得:这个
DelayUs函数非常不精确,受编译器优化和中断影响大。对于1-Wire这种有时序要求的,最好用定时器中断来产生精确延时,或者用while循环配合反汇编查看指令周期来精细调整。在早期项目中,我们常常通过示波器来校准这个延时函数,调整循环次数或_nop_数量,直到波形符合规范。这是“手搓”驱动的必经之路。
3.2 核心时序函数实现
基于前面的时序分析,我们实现四个最基础的函数:复位、写一位、读一位。
/** * @brief 1-Wire总线复位,检测存在脉冲 * @retval 0: 有设备响应,1: 无设备响应或总线错误 */ unsigned char OneWire_Reset(void) { unsigned char presence = 0; DQ = 0; // 主机拉低总线 DelayUs(480); // 保持低电平480us以上 (典型值) DQ = 1; // 主机释放总线 DelayUs(70); // 等待15-60us后采样,这里取70us保证窗口 presence = DQ; // 采样存在脉冲,低电平表示有设备 DelayUs(410); // 等待存在脉冲结束 (至少480us总周期) return presence; // 返回0表示有设备 } /** * @brief 向1-Wire总线写入一个位 * @param bit: 要写入的位,0或1 */ void OneWire_WriteBit(unsigned char bit) { DQ = 0; // 启动写时隙 _nop_(); _nop_(); // 极短延时,远小于15us if (bit) { DQ = 1; // 如果是写1,很快释放总线 } DelayUs(60); // 保持整个时隙长度约60us DQ = 1; // 释放总线,为下一个时隙准备 // 这里可以加一个短暂的恢复延时,如1-2us } /** * @brief 从1-Wire总线读取一个位 * @retval 读取到的位,0或1 */ unsigned char OneWire_ReadBit(void) { unsigned char bit = 0; DQ = 0; // 启动读时隙 _nop_(); _nop_(); // 极短延时 DQ = 1; // 主机释放总线 _nop_(); _nop_(); // 释放后短暂延时 DelayUs(15); // 关键延时!等待15us让从设备驱动总线 bit = DQ; // 采样总线电平 DelayUs(45); // 补足整个读时隙的时长 (约60us) return bit; }基于读写位函数,我们可以很容易地实现读写字节函数。
void OneWire_WriteByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { OneWire_WriteBit(dat & 0x01); dat >>= 1; // 先传低位 } } unsigned char OneWire_ReadByte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { dat >>= 1; // 先右移 if (OneWire_ReadBit()) { dat |= 0x80; // 如果读到1,放到最高位 } } return dat; }3.3 强上拉控制函数
对于DS18B20,在发送温度转换命令0x44后,必须提供强上拉。
void OneWire_StrongPullUp_Enable(void) { DQ = 1; // 先确保DQ为高 DQ_PWR = 0; // 打开强上拉MOS管,将DQ直接拉到VCC } void OneWire_StrongPullUp_Disable(void) { DQ_PWR = 1; // 关闭强上拉MOS管 // 此时DQ由外部4.7k上拉电阻维持高电平 }4. 驱动DS18B20温度传感器:完整的应用示例
有了底层驱动,我们以DS18B20为例,展示完整的应用层代码。假设总线只有一个DS18B20,我们使用Skip ROM (0xCC)命令。
4.1 初始化与启动温度转换
/** * @brief 启动DS18B20进行温度转换 * @retval 0: 成功,1: 设备无响应 */ unsigned char DS18B20_StartConvert(void) { if (OneWire_Reset() != 0) { return 1; // 复位失败,设备无响应 } OneWire_WriteByte(0xCC); // Skip ROM OneWire_WriteByte(0x44); // Convert T command // 启动转换后,需要强上拉供电 OneWire_StrongPullUp_Enable(); DelayMs(750); // 等待转换完成,12位精度时最多750ms OneWire_StrongPullUp_Disable(); return 0; }避坑指南:
DelayMs(750)是阻塞延时,在这750ms内单片机什么都干不了。这在很多实际应用中是不可接受的。更好的做法是:启动转换后,记录一个时间戳,然后单片机可以去执行其他任务,通过定时器中断或主循环查询的方式,判断750ms是否已过,再去读取结果。这是从“玩具代码”到“工程代码”的关键一步。
4.2 读取温度值
DS18B20的温度数据以16位补码形式存放在暂存器的前两个字节。
/** * @brief 从DS18B20读取温度值 * @param temp: 指向存储温度值的数组,temp[0]为整数部分,temp[1]为小数部分(分辨率0.0625) * @retval 0: 成功,1: 失败 */ unsigned char DS18B20_ReadTemperature(int *temp) { unsigned char TL, TH; int t; if (OneWire_Reset() != 0) { return 1; // 设备无响应 } OneWire_WriteByte(0xCC); // Skip ROM OneWire_WriteByte(0xBE); // Read Scratchpad TL = OneWire_ReadByte(); // 低字节 TH = OneWire_ReadByte(); // 高字节 // 后续字节(CRC等)可以继续读,但这里我们只关心温度 if (OneWire_Reset() != 0) { return 1; // 读取后复位失败(可选,但推荐) } // 将两个字节组合成16位有符号整数 t = TH; t <<= 8; t |= TL; // 处理温度值,转换为实际温度 // DS18B20默认12位分辨率,低4位是小数部分 *temp = t * 0.0625; // 浮点运算,在51上效率低 // 更常用的定点数处理方法: // *temp = t; // 直接返回原始值,在主程序里处理显示 // 或者将整数部分和小数部分分开存储 // temp_integer = t >> 4; // 右移4位得到整数部分 // temp_decimal = (t & 0x0F) * 625 / 1000; // 计算小数部分,用于显示 return 0; }经验技巧:在资源紧张的C51上,应尽量避免浮点数运算
* 0.0625。通常的做法是进行定点数运算。例如,将读取的16位值t右移4位得到整数部分,低4位乘以625再除以10000,得到小数部分的两位十进制表示(如0.0625 * 12 = 0.75,即小数部分显示75)。或者更简单地,只显示整数部分,牺牲一点精度。
4.3 主程序示例
void main() { int temperature_raw = 0; float temperature = 0.0; while (1) { if (DS18B20_StartConvert() == 0) { // 延时等待转换完成,实际应用应使用非阻塞方式 DelayMs(800); if (DS18B20_ReadTemperature(&temperature_raw) == 0) { // 这里使用浮点运算仅为示例 temperature = temperature_raw * 0.0625; // 将temperature通过串口或LCD显示出来 // UART_SendFloat(temperature); } } DelayMs(2000); // 每2秒测量一次 } }5. 进阶话题:多设备搜索与CRC校验
5.1 实现Search ROM算法
当总线上有多个1-Wire设备时,Search ROM (0xF0)命令配合一套巧妙的二叉树搜索算法,可以枚举出所有设备的64位ROM码。这个算法是1-Wire协议中最精妙的部分之一。
算法核心是“冲突检测”。主机发送一位,所有设备同时回复这一位。如果所有设备这一位都相同(全0或全1),主机就收到一个确定值。如果有的设备发0,有的发1,总线会产生“线与”效果,主机读回的是0(低电平优先),这就发生了“冲突”。
发生冲突时,主机可以选择发送0或1。如果发送0,那么ROM码该位为1的设备将暂时退出本次搜索,只有该位为0的设备继续响应。通过记录每次冲突时的选择,主机可以遍历整个ROM地址空间,找出所有设备。实现这个算法需要维护一个全局的“上次分歧位”信息,代码较为复杂,但网上有成熟的“状态机”式实现可以参考。在C51上实现时,需要注意栈深度和变量管理,避免内存溢出。
5.2 CRC校验确保数据可靠
1-Wire器件在ROM码和部分数据中包含了8位CRC校验码(使用Dallas CRC8多项式)。在读取多字节数据(如DS18B20的9字节暂存器)后,进行CRC校验可以极大提高通信可靠性,避免因总线干扰导致的数据错误。
// Dallas CRC8查表法,高效适合C51 unsigned char dscrc_table[256] = { /* 标准CRC8表 */ }; unsigned char OneWire_CRC8(unsigned char *addr, unsigned char len) { unsigned char crc = 0; while (len--) { crc = dscrc_table[crc ^ *addr++]; } return crc; } // 读取DS18B20暂存器并校验 unsigned char DS18B20_ReadScratchpad(unsigned char *buff) { // ... 发送读命令 ... for (i=0; i<9; i++) { buff[i] = OneWire_ReadByte(); } if (OneWire_CRC8(buff, 8) != buff[8]) { return 1; // CRC校验失败 } return 0; }6. 调试技巧与常见问题排查
用C51调试1-Wire,逻辑分析仪或带波形显示的示波器是神器。没有的话,就得靠“灯”和“串口”了。
问题1:永远检测不到存在脉冲(复位失败)。
- 检查硬件:上拉电阻是否接了?值是否合适(4.7k)?DQ线是否接触良好?电源电压是否足够?
- 检查软件:延时函数是否准确?用示波器看复位脉冲的低电平时间是否大于480us?主机释放总线后,是否足够快地切换到了输入模式?在读取
DQ引脚前,确保该IO口被设置为高电平输入状态(对于准双向口的C51,写1即为输入)。 - 排查顺序:先确保单设备、单次复位-检测能成功,再考虑复杂逻辑。
问题2:能检测到设备,但读写数据全是0xFF或0x00。
- 时序问题:读/写时隙的时序不对,特别是读时隙中,主机采样太早(从设备还没驱动总线)或太晚(从设备已释放总线)。调整
OneWire_ReadBit中的DelayUs(15)这个关键延时。 - 电源问题:如果是寄生供电,在长时间通信或转换时,电量可能不足。尝试加强上拉(减小上拉电阻值,如改为2.2k),或在转换时启用强上拉。
- 代码逻辑:检查
OneWire_ReadByte和OneWire_WriteByte的位顺序(LSB first)是否正确。
问题3:DS18B20读出的温度一直是85℃或0℃。
- 85℃:这是DS18B20的上电默认值。说明温度转换命令
0x44可能没有成功执行,或者主机在转换完成前就去读取了。务必确保在发送0x44后,提供了足够的强上拉并等待了足够的转换时间(750ms for 12-bit)。这是最高频的坑。 - 0℃:可能读到的数据就是0。检查读出的高低字节是否正确组合,检查读写函数的字节顺序。
问题4:多设备时搜索不全或不稳定。
- 总线驱动能力:设备太多或布线太长,导致信号边沿变差。可以尝试在总线两端增加上拉电阻,或降低通信速度(拉长时隙)。
- 搜索算法Bug:仔细检查Search ROM算法的实现,特别是冲突处理逻辑和“上次分歧位”的更新。建议先用已知ROM码的少量设备测试。
- 中断干扰:1-Wire通信对时序敏感,如果通信过程中被高优先级中断打断,可能导致时序错乱。在关键的复位、读位、写位函数中,可以考虑临时关闭全局中断。
最后,把驱动模块化、封装好。onewire.c和onewire.h提供底层操作,ds18b20.c和ds18b20.h提供应用层函数。这样,当你下次在另一个C51项目里需要用到DS18B20时,直接移植这两个文件,稍微调整一下延时和引脚定义,就能快速跑起来。这份“one.zip”里的老代码,其价值不在于技术本身有多新,而在于它提供了一种在极端受限环境下,通过精准控制硬件来达成目标的思维方式。在如今这个依赖现成库和强大硬件的时代,偶尔回头看看这些“底层手艺”,对理解计算机系统的本质,依然大有裨益。
本文还有配套的精品资源,点击获取