ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C51单片机驱动1-Wire总线:从时序模拟到DS18B20温度采集实战

C51单片机驱动1-Wire总线:从时序模拟到DS18B20温度采集实战 简介本资源是一套面向嵌入式初学者与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 (i0; i9; i) { buff[i] OneWire_ReadByte(); } if (OneWire_CRC8(buff, 8) ! buff[8]) { return 1; // CRC校验失败 } return 0; }6. 调试技巧与常见问题排查用C51调试1-Wire逻辑分析仪或带波形显示的示波器是神器。没有的话就得靠“灯”和“串口”了。问题1永远检测不到存在脉冲复位失败。检查硬件上拉电阻是否接了值是否合适4.7kDQ线是否接触良好电源电压是否足够检查软件延时函数是否准确用示波器看复位脉冲的低电平时间是否大于480us主机释放总线后是否足够快地切换到了输入模式在读取DQ引脚前确保该IO口被设置为高电平输入状态对于准双向口的C51写1即为输入。排查顺序先确保单设备、单次复位-检测能成功再考虑复杂逻辑。问题2能检测到设备但读写数据全是0xFF或0x00。时序问题读/写时隙的时序不对特别是读时隙中主机采样太早从设备还没驱动总线或太晚从设备已释放总线。调整OneWire_ReadBit中的DelayUs(15)这个关键延时。电源问题如果是寄生供电在长时间通信或转换时电量可能不足。尝试加强上拉减小上拉电阻值如改为2.2k或在转换时启用强上拉。代码逻辑检查OneWire_ReadByte和OneWire_WriteByte的位顺序LSB first是否正确。问题3DS18B20读出的温度一直是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”里的老代码其价值不在于技术本身有多新而在于它提供了一种在极端受限环境下通过精准控制硬件来达成目标的思维方式。在如今这个依赖现成库和强大硬件的时代偶尔回头看看这些“底层手艺”对理解计算机系统的本质依然大有裨益。本文还有配套的精品资源点击获取
返回列表