简介:本资源是一套面向嵌入式初学者与8051单片机开发者的1-Wire总线通信实战代码包,聚焦于单总线协议在温度传感等低功耗场景中的底层实现。资源以C51语言为核心,完整呈现主控端(如8051)对DS18B20等典型1-Wire器件的初始化、ROM搜索、命令发送、时序控制及CRC校验等关键环节,适用于Keil uVision开发环境下的硬件调试与协议理解。压缩包共17个文件,含1个核心C源文件(ledtest.c)、1个汇编实现模块(wireasm.asm)、1个1-Wire专用头文件(ledtest.h)、1个可烧录hex文件及多个编译中间产物(obj/lst/m51)和工程配置备份(Uv2.Bak、Opt.Bak),整体仅49KB,轻量易导入。已有317人学习下载,配套www.pudn.com.txt提供来源说明,目录结构体现典型Keil工程组织逻辑,便于读者逐层剖析时序细节、比对C与ASM混合编程策略,并快速复现单总线多节点通信功能。
1. 项目概述:从“one.zip”到C51的1-Wire总线实战
最近在整理老项目的资料库,翻出了一个名为“one.zip”的压缩包。这个文件名简单到让人会心一笑,里面装的正是我多年前用C51单片机折腾1-Wire总线时攒下的一堆代码、原理图和调试笔记。1-Wire总线,这个由达拉斯半导体(现属美信)发明的单线通信协议,以其极简的硬件连接(一根数据线加地线)和独特的寄生供电能力,在温湿度传感、电子标签、设备认证等场景中经久不衰。尽管如今ARM Cortex-M内核大行其道,但在一些对成本极度敏感、功能专一的小型设备上,经典的8051内核单片机(我们常说的C51)依然有其用武之地。这个“one.zip”项目,就是如何在资源有限的C51平台上,稳定、可靠地实现1-Wire总线通信的完整实践记录。它不仅仅是一份代码,更是一套包含了时序精准控制、抗干扰设计、以及针对DS18B20这类经典器件驱动的心法。如果你正在或即将在51系列单片机上对接1-Wire器件,那么这份从老硬盘里挖出来的经验,或许能帮你避开我当年踩过的那些坑。
2. 1-Wire总线协议核心精要与C51适配挑战
1-Wire协议的精髓,在于它仅用单根数据线就实现了双向数据通信和(对从设备的)供电。这根线上挂载的所有设备都通过一个唯一的64位ROM ID进行寻址,支持多设备挂载。通信完全由主机(Master,即我们的C51单片机)发起和控制,通过严格的时间槽来区分数位“0”、“1”以及复位、存在脉冲等命令。
2.1 协议基础:时序就是一切
1-Wire通信的基石是精确的微秒级时序。所有操作都始于一个由主机发出的复位脉冲(至少480µs的低电平),随后主机释放总线并切换到接收模式。在线路上的1-Wire从设备会在等待15-60µs后,回应一个存在脉冲(60-240µs的低电平)。主机检测到这个脉冲,便确认总线上有设备就绪。
随后的数据读写操作,均以时间槽为单位。每个时间槽通常持续60-120µs,用于传输1比特数据:
- 写“1”时间槽:主机拉低总线1-15µs,然后释放,剩余时间保持高电平。
- 写“0”时间槽:主机拉低总线至少60µs,最多120µs,然后释放。
- 读时间槽:主机拉低总线1-15µs后释放,然后在短暂的采样窗口(通常在起始下降沿后15µs内)读取总线电平。高电平为“1”,低电平为“0”。
注意:上述时间参数是DS18B20等经典器件的典型值,不同厂商、型号的1-Wire器件可能存在细微差异,务必以具体器件的数据手册为准。
2.2 C51平台的特殊挑战与应对思路
在STM32等现代MCU上,我们可以轻松使用硬件定时器产生精准延时,甚至利用外部中断或IO口翻转功能来捕获信号。但在传统的C51(如AT89C51、STC89C52)上,我们面临几个现实挑战:
- 主频较低且可变:常见C51工作在11.0592MHz或12MHz,每个机器周期1µs或约1µs。延时需要靠软件循环实现,精度受编译器优化和中断影响。
- 无高精度硬件定时器:虽然有两个16位定时器,但用于µs级延时开销过大,且可能影响其他功能。
- 中断响应延迟:如果系统中开启了中断,在读写1-Wire的关键时序窗口内若发生中断,可能导致时序超差,通信失败。
因此,在C51上实现1-Wire驱动的核心思路是:编写高度优化的、可重入的微秒级延时函数,并在执行关键1-Wire时序操作时,短暂关闭全局中断(EA=0)。同时,IO口应配置为准双向模式(这是51单片机IO的默认模式,具有弱上拉能力,与1-Wire总线要求相符)。
3. C51驱动1-Wire总线的核心代码实现与解析
下面,我将结合“one.zip”中的核心代码模块,拆解如何在C51上构建一个稳健的1-Wire驱动层。我们以驱动最常见的DS18B20数字温度传感器为例。
3.1 硬件连接与IO口定义
假设使用P3.7口作为1-Wire总线。
// 1-Wire 总线引脚定义 sbit DQ = P3^7; // 数据线硬件上,DQ引脚需要接一个4.7kΩ的上拉电阻至VCC,这是1-Wire总线标准所要求的,用于确保总线在空闲时处于高电平状态,并为寄生供电的从设备提供能量。
3.2 微秒级延时函数的实现
这是整个驱动的精度基础。由于C51指令周期固定,我们可以用_nop_()(空操作,1个机器周期)和循环来构建延时。
/** * @brief 微秒级延时 (适用于12MHz晶振,1机器周期=1us) * @param t: 需要延时的微秒数,对于12MHz,t即us数。 * 注意:此函数本身有调用开销,需校准。 */ void Delay_us(unsigned char t) { while (t--) { _nop_(); _nop_(); _nop_(); _nop_(); // 根据实际测试调整_nop_数量以校准 } }实操心得:这个延时函数非常不精确!因为它受编译器优化和循环开销影响。更可靠的方法是使用定时器。但对于简单应用,可以通过示波器或逻辑分析仪,调整循环体内的
_nop_()数量进行粗略校准。在“one.zip”的后期版本中,我改用定时器0工作在模式2(8位自动重装)来产生一个基准的10µs或50µs中断,在此基础上构建更精确的延时。这是从“能用”到“稳定”的关键一步。
3.3 1-Wire复位与存在检测
这是每次通信会话的开始。
/** * @brief 1-Wire 复位脉冲,并检测存在脉冲 * @retval 1: 有设备响应,0: 无设备响应或总线错误 */ bit OW_Reset(void) { bit presence_pulse; EA = 0; // 关闭全局中断,防止时序被干扰 DQ = 0; // 主机拉低总线,产生复位脉冲 Delay_us(480); // 保持低电平至少480us DQ = 1; // 释放总线,主机切换为接收 Delay_us(70); // 等待15-60us后采样,这里取70us保证安全 presence_pulse = DQ; // 采样总线电平,低电平表示有存在脉冲 Delay_us(410); // 等待存在脉冲周期结束 (最少480us - 70us) EA = 1; // 重新开启中断 return !presence_pulse; // 如果采样到低电平(presence_pulse=0),则返回1(有设备) }关键点解析:
EA=0/EA=1:包裹关键时序操作,这是C51上提高1-Wire通信可靠性的黄金法则。否则,一个意外的定时器或串口中断就可能导致拉低或采样时间偏差几十微秒,导致失败。- 延时参数:
480、70、410这些数值是基于12MHz晶振和粗略软件延时校准后的结果。你需要根据自己系统的实际主频和延时函数精度进行调整。 - 返回值逻辑:存在脉冲期间从机会拉低总线,所以主机采样到低电平(
DQ=0)表示有设备,函数返回1。
3.4 单比特读写操作
所有字节的读写都建立在单比特操作上。
/** * @brief 向1-Wire总线写入1比特数据 * @param bit_value: 要写入的比特值,0或1 */ void OW_WriteBit(bit bit_value) { EA = 0; DQ = 0; // 主机拉低总线,启动时间槽 _nop_(); _nop_(); // 极短延时,约2us (12MHz) DQ = bit_value; // 如果是写1,则释放总线;写0则保持低电平 Delay_us(60); // 保持低电平时间,对于写0是60us,写1则很快释放 DQ = 1; // 释放总线,结束时间槽 // 两个时间槽之间需要至少1us的恢复时间,此处用短延时 _nop_(); _nop_(); EA = 1; } /** * @brief 从1-Wire总线读取1比特数据 * @retval 读取到的比特值,0或1 */ bit OW_ReadBit(void) { bit read_value; EA = 0; DQ = 0; // 主机拉低总线,启动读时间槽 _nop_(); _nop_(); // 极短延时,约2us DQ = 1; // 主机释放总线,准备采样 _nop_(); _nop_(); _nop_(); _nop_(); // 等待约4us,建立采样窗口 read_value = DQ; // 在下降沿后约15us内采样 Delay_us(55); // 等待读时间槽剩余时间结束 EA = 1; return read_value; }为什么读和写函数看起来这么像?因为1-Wire的读操作也是由主机发起的。主机先拉低总线1-15µs(发出“读命令”),然后释放并采样。从设备会在主机拉低总线后,决定是否将总线拉低来回应“0”。所以,OW_ReadBit函数开头拉低又释放的动作,本质上是在发起一个“读时间槽”的请求。
3.5 字节读写函数构建
有了单比特读写,字节操作就是循环。
/** * @brief 向1-Wire总线写入1字节数据 * @param byte_value: 要写入的字节 */ void OW_WriteByte(unsigned char byte_value) { unsigned char i; for (i = 0; i < 8; i++) { OW_WriteBit(byte_value & 0x01); // 先写最低位 (LSB first) byte_value >>= 1; } } /** * @brief 从1-Wire总线读取1字节数据 * @retval 读取到的字节 */ unsigned char OW_ReadByte(void) { unsigned char i, byte_value = 0; for (i = 0; i < 8; i++) { byte_value >>= 1; // 先右移 if (OW_ReadBit()) { byte_value |= 0x80; // 如果读到1,则设置最高位 } } return byte_value; }注意事项:1-Wire协议规定先传输字节的最低位(LSB First)。
OW_WriteByte中byte_value & 0x01和OW_ReadByte中先右移再置位最高位的写法,正是遵循了这一规则。这是很多初学者容易出错的地方。
4. 驱动DS18B20温度传感器的完整应用流程
有了底层读写函数,我们就可以按照DS18B20的命令序列来操作了。一次完整的温度转换和读取流程如下:
4.1 初始化与ROM命令
- 复位与存在检测:调用
OW_Reset(),确认传感器在线。 - 发送ROM命令:如果总线上只有一个DS18B20,可以使用跳过ROM命令
0xCC,直接对总线上所有设备进行操作。这是单设备场景下最常用的命令,简化了流程。OW_WriteByte(0xCC); // 跳过ROM命令
4.2 发送功能命令与读取数据
发送功能命令:紧接着发送启动温度转换命令
0x44。OW_WriteByte(0x44); // 启动温度转换此时,DS18B20开始进行温度转换。对于12位分辨率(默认),转换时间最长为750ms。在此期间,主机可以释放总线去做其他事情,或者通过
OW_ReadBit()读取DQ线,如果为低则表示转换未完成,为高则表示完成(也可用延时等待)。再次初始化:转换完成后,需要再次发起复位和存在检测序列。
发送读取命令:再次发送跳过ROM命令
0xCC,然后发送读暂存器命令0xBE。OW_Reset(); OW_WriteByte(0xCC); // 跳过ROM OW_WriteByte(0xBE); // 读暂存器命令读取温度数据:连续读取9个字节(DS18B20暂存器共9字节)。温度值存储在前两个字节(LSB和MSB)。
unsigned char temp_data[9]; for(i = 0; i < 9; i++) { temp_data[i] = OW_ReadByte(); }数据解析:将两个字节组合成一个16位有符号整数。DS18B20的温度数据以1/16°C为LSB。
int raw_temp = (temp_data[1] << 8) | temp_data[0]; // 合成16位数据 float temperature; if (raw_temp & 0x8000) { // 判断是否为负数 raw_temp = ~raw_temp + 1; // 取补码 temperature = -((float)raw_temp * 0.0625); } else { temperature = (float)raw_temp * 0.0625; // 0.0625 = 1/16 }temp_data[1]是高字节,temp_data[0]是低字节。0.0625是分辨率。
4.3 寄生供电模式下的强上拉操作
如果DS18B20采用寄生供电(即VDD引脚接地,完全依靠数据线在空闲时通过上拉电阻供电),在执行温度转换(0x44)或拷贝暂存器到EEPROM(0x48)等耗电较大的操作时,必须在命令发出后的10µs内,将总线通过一个MOSFET强拉到VCC(提供更大电流),并持续到操作完成。这在C51上通常通过一个额外的IO口控制MOSFET实现。
sbit POWER_CTRL = P1^0; // 控制强上拉MOSFET的引脚 // 发送启动转换命令后 OW_WriteByte(0x44); POWER_CTRL = 1; // 开启强上拉 Delay_ms(750); // 等待转换完成,时间根据分辨率而定 POWER_CTRL = 0; // 关闭强上拉重要提示:如果使用外部电源为DS18B20供电(VDD接3.3V或5V),则不需要强上拉操作,通信可靠性会更高。这是提升系统稳定性的一个关键设计选择。
5. 多设备挂载与ROM ID搜索算法
当一根1-Wire总线上挂有多个设备(如多个DS18B20)时,就需要通过唯一的64位ROM ID来区分它们。这涉及到更复杂的搜索ROM算法(命令0xF0)。
5.1 搜索算法原理简述
搜索算法是一个基于“冲突”的递归过程。主机通过“读”操作,让所有设备在同一时间槽内回复自己ROM ID的某一位。如果所有设备在该位都回复相同的值(0或1),主机就得到该位的确定值。如果有的设备回复0,有的回复1,就发生了“冲突”。主机此时必须做出“选择”,先向0方向搜索,还是向1方向搜索。通过记录这些选择点,主机可以遍历总线上所有设备的ROM ID。
5.2 C51上的简化实现考量
完整的搜索算法实现起来代码量较大,且对时序和状态记录要求高。在资源紧张的C51上,有更实用的替代方案:
- 单设备跳过ROM:如果应用确定只有单一设备,直接使用
0xCC跳过ROM命令,这是最简单可靠的。 - 已知ROM ID直接寻址:如果设备ROM ID已知(可通过编程器或一次性搜索后存入EEPROM),则可以使用匹配ROM命令
0x55,后跟64位ROM ID,直接与特定设备对话。unsigned char rom_code[8] = {0x28, 0xFF, 0x...}; // 已知的ROM ID OW_Reset(); OW_WriteByte(0x55); // 匹配ROM命令 for(i=0; i<8; i++) { OW_WriteByte(rom_code[i]); } // 接下来发送功能命令... - 仅在上电时搜索一次:在系统初始化时,执行一次完整的搜索算法,将找到的所有ROM ID存入数组。之后的正常操作中,使用“匹配ROM”命令进行寻址。这样可以避免在每次测温时都进行耗时的搜索。
对于大多数C51应用,方案2或3更为可行。完整的搜索算法代码较长,在“one.zip”中有一个实现版本,其核心是维护一个“上次分歧位”的变量,并递归地尝试两条路径。由于篇幅所限,这里不展开全部代码,但其关键在于正确处理读回数据中的冲突位,并管理好搜索路径的回溯。
6. 调试技巧、常见问题与稳定性优化
在实际焊接电路和编写代码时,问题总会不期而至。以下是我从“one.zip”项目的调试笔记中总结出的精华。
6.1 硬件问题排查清单
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 始终无存在脉冲 | 1. 接线错误(DQ、GND) 2. 上拉电阻未接或损坏 3. 传感器损坏 4. 电源问题(寄生供电时电流不足) | 1. 用万用表检查通断。 2. 确认4.7kΩ上拉电阻正确连接至VCC。 3. 更换传感器测试。 4. 尝试改为外部供电(VDD接3.3V/5V)。 |
| 偶尔通信失败,数据乱码 | 1. 时序不精确 2. 总线受到干扰(长线、靠近电机等) 3. 中断干扰关键时序 4. 寄生供电模式下未加强上拉 | 1. 用逻辑分析仪抓取波形,对比DS18B20时序图。 2. 缩短总线长度,使用双绞线,远离干扰源。 3. 确认在 OW_ReadBit、OW_WriteBit等函数中关闭了中断(EA=0)。4. 在转换命令后增加强上拉电路并控制。 |
| 读回的温度值固定为85°C或0°C | 85°C是上电默认值,0°C可能是读取错误。通常是因为复位/读写时序不符合要求,导致DS18B20没有正确执行命令。 | 重点检查OW_Reset和OW_WriteBit/OW_ReadBit中的延时参数。使用逻辑分析仪是最高效的手段。 |
6.2 软件层面的稳定性优化
- 增加重试机制:任何一次
OW_Reset()或读写操作失败,都应加入重试逻辑。例如,连续进行3次复位检测,只有两次成功才认为初始化成功。bit OW_Reset_With_Retry(unsigned char retries) { while(retries--) { if(OW_Reset()) { return 1; // 成功 } Delay_ms(2); // 失败后稍作延迟再试 } return 0; // 全部重试失败 } - CRC校验:DS18B20暂存器的第9字节是前面8字节的CRC校验码。在读取温度数据后,应计算前8字节的CRC,并与读回的第9字节对比。如果不匹配,则丢弃本次数据并重试。这能有效发现因干扰导致的传输错误。“one.zip”里包含了一个针对DS18B20的CRC8查表法计算函数。
- 精准延时校准:放弃不可靠的纯软件循环延时。使用定时器中断来产生一个基准时间单元(如10µs)。所有1-Wire延时都基于这个基准单元进行计数,精度和可移植性大幅提升。
- 总线恢复:在连续多次通信失败后,可以尝试让主机连续输出多个480µs以上的低电平脉冲(类似长复位),这有助于将可能“卡住”的从设备状态机拉回初始状态。
6.3 逻辑分析仪:你的最佳搭档
没有逻辑分析仪调试1-Wire就像蒙着眼睛走路。一个廉价的USB逻辑分析仪(如Saleae Logic 8克隆版)配合DSView等软件,可以直观地看到复位脉冲、存在脉冲、每一个读写时间槽的电平变化。将抓取的波形与数据手册中的时序图严格对比,是定位时序问题最快、最直接的方法。你可以清晰地看到你的“15µs低电平”在实际波形中是10µs还是20µs,采样点是否在有效窗口内。
7. 从C51到现代MCU的思考与项目扩展
虽然本项目聚焦于C51,但1-Wire协议的思想是通用的。在现代的STM32、GD32等ARM Cortex-M MCU上,实现方式有了更多选择:
- GPIO模拟:思路与C51完全相同,但得益于更高的主频和更精准的定时器(如SysTick),延时可以做得更精确。同时,利用GPIO的“位带”操作,可以像操作C51的
sbit一样直接操作单个IO,非常方便。 - 外部中断检测:可以利用MCU的外部中断功能,在主机拉低总线启动读时间槽后,配置中断在下降沿或上升沿触发,从而更精准地捕获从设备的响应,减少软件延时的依赖。
- 定时器PWM与输入捕获:更高级的用法是利用定时器的PWM模式产生精确的复位脉冲和写时间槽,利用输入捕获模式来测量存在脉冲的宽度和读时间槽的响应。这几乎将CPU完全解放出来。
“one.zip”项目虽然基于古老的C51平台,但它所涉及的精确时序控制、抗干扰设计、协议层状态机实现、以及调试方法论,是嵌入式开发中普适的技能。理解了这个在资源受限环境下的实现,当你转移到功能更强大的平台时,不仅能轻松移植,更能理解那些高级外设(如UART in smart-card mode, I2C等)为何要如此设计。最后,如果你打算深化这个项目,可以考虑添加更多的1-Wire器件支持(如DS2431 EEPROM、DS2413 IO扩展器),甚至尝试用C51做一个简单的1-Wire网络嗅探器,那将会是对协议理解的又一次升华。
本文还有配套的精品资源,点击获取