简介:本资源是一套基于C51语言实现的1-Wire总线通信完整开发工程,面向嵌入式初学者与8051单片机开发者,解决单总线传感器(如DS18B20)在Keil环境下驱动与协议交互的核心问题。压缩包含17个文件,涵盖C源码(ledtest.c)、汇编底层驱动(wireasm.asm)、头文件(ledtest.h)、编译输出(.hex/.lst/.obj)、工程配置(.uv2/.opt/.lnp)及备份文件(.bak),其中.c、.asm、.h构成可读可改的主逻辑,.hex支持直接烧录验证,.lst与.m51便于时序分析与调试。资源包仅49KB,轻量紧凑,适配Keil C51开发链,完整呈现初始化、ROM搜索、命令发送、数据读取等关键流程实现。目前已有320人学习下载,配套www.pudn.com.txt提供来源说明与使用提示,是理解1-Wire物理层时序、掌握C51寄存器级操作、构建分布式传感节点的实用入门范例。
1. one.zip 里藏的不是压缩包,是能直接烧进 8051 的 1-Wire 总线驱动黑匣子:单总线通信不用再啃 Datasheet 硬写时序
你手头那块 DS18B20 温度传感器,接在 STC89C52 或 AT89C51 上,Keil uVision 4 编译报错“_bit not defined”?或者用示波器抓到引脚上电平抖得像心电图,但 ROM Code 就是读不出来?别急着换芯片——one.zip 这个看似随手命名的压缩包,其实是老工程师压箱底的 1-Wire 总线 C51 实战套件:它不讲原理,只给能跑通的代码;不依赖库函数,全用_at_定义 +#pragma控制寄存器;连OW_Reset()里那个 480μs 强制拉低的延时,都用NOP指令数精确卡死在 12 个机器周期(12MHz 晶振下)。这不是教学例程,是当年在工业温控板上量产过的实机代码——支持多节点 ROM 搜索、CRC 校验跳过、Skip ROM 快速读、Match ROM 精准寻址,甚至预留了OW_Read_Scratchpad()后接OW_Write_Scratchpad()的寄存器配置入口。适合正在用 Keil uVision 4 做 C51 开发、被 Dallas 半导体(现 Maxim)那份 37 页英文时序手册逼到怀疑人生的嵌入式新手,也适合要快速验证 1-Wire 节点兼容性的硬件调试老手。它不解决 CAN 总线 RTR 位或 AHB/APB 总线带宽问题,但它能让你在 2 小时内,把 DS18B20 的 12 位温度值稳定读出来,而不是对着示波器波形猜高低电平。
2. 从 one.zip 解压到 Keil 工程编译通过:C51 环境下的 1-Wire 驱动落地四步法
2.1 解压结构与文件功能映射:看清 one.zip 里每个 .c/.h 的真实角色
one.zip 解压后共 5 个核心文件,不是所有都该加进工程:
| 文件名 | 类型 | 关键作用 | 是否必须加入 Keil 工程 |
|---|---|---|---|
ow.c | C 源码 | 主驱动实现:复位、读写位、读写字节、ROM 搜索、CRC 计算 | ✅ 必须 |
ow.h | 头文件 | 宏定义(如OW_PIN P1^7)、函数声明、typedef struct ow_rom_code | ✅ 必须 |
main.c | 示例主程序 | 初始化、调用OW_Reset()+OW_Read_ROM()+OW_Start_Conversion()+OW_Read_Temp() | ✅ 推荐(可删减) |
delay.c | 延时模块 | 提供Delay_us(480)和Delay_ms(750),内部用NOP循环实现 | ✅ 必须(不可用for(i=0;i<100;i++);替代) |
delay.h | 延时头文件 | extern void Delay_us(unsigned int us);声明 | ✅ 必须 |
注意:
ow.c中所有延时均调用Delay_us(),而非for循环硬等。这是因为 1-Wire 对时间精度要求苛刻(复位脉冲低电平需 480–960μs),而 C51 编译器对for循环优化不可控。Delay_us()内部用NOP指令堆叠,确保每微秒对应固定指令周期数。
2.2 Keil uVision 4 工程配置关键三步:避开 C51 编译器默认陷阱
C51 编译器默认启用SMALL存储模式,但ow.c中大量使用xdata指针操作 ROM 缓冲区(如rom_code[8]),必须手动切换:
右键 Target → Options for Target → Memory Model
→ 选择LARGE(允许xdata指针访问外部 RAM,虽本项目未外扩 RAM,但ow.c中rom_code数组声明为xdata unsigned char rom_code[8],LARGE模式才能正确寻址)右键 Source Group 1 → Add Files to Group
→ 依次添加ow.c,delay.c,main.c(顺序无关,但ow.h和delay.h会自动被#include引入)Options for Target → C51 → Misc Controls
→ 在框中填入:-D__C51__ -DXTAL=12000000
(-DXTAL=12000000是关键!ow.c中Delay_us()函数根据XTAL宏计算NOP数量,若未定义,延时将严重失准)
// ow.c 片段:延时计算逻辑(勿修改) #if XTAL == 12000000 #define NOP_CYCLES_PER_US 1 // 12MHz 下,1 个机器周期 = 1μs(12T 模式) #elif XTAL == 11059200 #define NOP_CYCLES_PER_US 1.087 // 精确值,非整数,故 one.zip 默认按 12MHz 设计 #endif2.3OW_Reset()函数深度拆解:为什么它必须用sbit而非sfr定义引脚
ow.h中这行定义是整个驱动稳定性的基石:
sbit OW_PIN = P1^7; // ✅ 正确:直接映射到 P1.7 位地址 // 错误示例(会导致复位失败): // sfr OW_PORT = 0x90; // P1 端口地址 // #define OW_PIN (OW_PORT & 0x80) // 位操作结果不可赋值,无法驱动引脚原因在于:1-Wire 总线是漏极开路(Open-Drain)结构,需要严格控制引脚输出状态——复位时强制拉低(OW_PIN = 0),释放时靠上拉电阻拉高(OW_PIN = 1)。sbit定义的位变量可直接读写,编译后生成CLR P1.7/SETB P1.7指令,执行速度最快(1 个机器周期)。而用sfr+ 位运算,编译器会生成MOV A, P1→ANL A, #0x7F→MOV P1, A等多条指令,耗时超 3μs,破坏 1-Wire 时序窗口。
// ow.c 中 OW_Reset() 关键片段(精简版) bit OW_Reset(void) { bit presence; EA = 0; // 关中断,避免延时被干扰 OW_PIN = 0; // 强制拉低 480μs Delay_us(480); OW_PIN = 1; // 释放总线,等待从机拉低 Delay_us(70); // 等待从机应答窗口开始 presence = OW_PIN; // 读取线上电平:0=存在,1=无设备 Delay_us(410); // 等待应答窗口结束 EA = 1; // 开中断 return presence; }逻辑说明:
OW_PIN = 1并非“输出高”,而是释放引脚,让外部 4.7kΩ 上拉电阻将其拉高。此时若 DS18B20 存在,会在 15–60μs 内主动拉低总线,presence读出为0;否则保持高电平,presence为1。这个“读引脚电平”的动作,必须发生在OW_PIN = 1之后的 70μs 内,早了从机没准备好,晚了错过应答边沿。
2.4 ROM 搜索算法的 C51 实现:用bit search_direction替代递归,省下 200 字节栈空间
ow.c中OW_Search_ROM()不采用教科书式的递归搜索(易爆栈),而是用状态机+位掩码迭代:
// 关键变量声明(全局,非局部) xdata unsigned char rom_code[8]; // 存储搜索到的 ROM bit search_direction; // 当前分支方向:0=走0分支,1=走1分支 unsigned char last_discrepancy; // 上次分歧位位置(0~63) unsigned char last_device_flag; // 是否已找到最后一个设备 // 搜索主循环(伪代码) while (!last_device_flag) { if (OW_Reset() == 0) { // 总线上有设备 OW_Write_Byte(0xF0); // Search ROM 命令 for (i=0; i<64; i++) { bit bit1 = OW_Read_Bit(); // 读设备位 bit bit2 = OW_Read_Bit(); // 读设备位补码 if (bit1 && bit2) break; // 两个都高?说明无设备(冲突结束) if (!bit1 && !bit2) { // 两个都低:发生位冲突,需决策 if (i < last_discrepancy) { // 回溯:取之前存的位 search_direction = (rom_code[i/8] >> (i%8)) & 0x01; } else { // 新分歧:按 search_direction 写出 OW_Write_Bit(search_direction); if (search_direction) rom_code[i/8] |= (1 << (i%8)); else rom_code[i/8] &= ~(1 << (i%8)); } } else { // 仅一个低:确定位,直接写入 rom_code if (bit1) { rom_code[i/8] |= (1 << (i%8)); OW_Write_Bit(1); } else { rom_code[i/8] &= ~(1 << (i%8)); OW_Write_Bit(0); } } } } }参数说明:last_discrepancy初始为 0,每次遇到冲突位就更新为当前i值。当搜索到第 N 个设备后,last_discrepancy会被设为该设备 ROM 的最后一个冲突位,下次搜索从此位开始回溯,避免重复扫描已确认位。这种设计使ow.c在 2KB RAM 的 C51 单片机上,可稳定搜索 8 个 DS18B20,而不会因栈溢出导致main()崩溃。
3. 1-Wire 总线物理层实操:上拉电阻、布线长度与示波器抓波形的血泪经验
3.1 上拉电阻值不是越大越好:4.7kΩ 是 12MHz C51 + DS18B20 的黄金平衡点
很多新手照抄网上“10kΩ 上拉”的方案,结果OW_Reset()返回1(无设备响应)。根本原因是:DS18B20 内部上拉能力弱(典型 1mA 灌电流),而 C51 I/O 口高电平驱动能力更差(P1 口拉电流仅 10μA)。若上拉电阻过大(如 10kΩ),总线从低电平释放后,上升时间t_r = R × C超过 15μs,导致从机无法在 15μs 内检测到高电平跳变,错过应答窗口。
// 实测数据(示波器测量 P1.7 引脚) // 条件:PCB 走线长 10cm,分布电容 ≈ 80pF // R = 4.7kΩ → t_r ≈ 4.7e3 × 80e-12 = 376ns (合格) // R = 10kΩ → t_r ≈ 10e3 × 80e-12 = 800ns (临界,部分批次 DS18B20 失效) // R = 2.2kΩ → t_r ≈ 2.2e3 × 80e-12 = 176ns (太快,但增加功耗,且可能引起振铃)提示:若使用长线(>2m),需降低上拉电阻至 2.2kΩ,并在 MCU 端串接 100Ω 电阻抑制振铃。但此时
OW_Write_Bit(0)的低电平持续时间需同步缩短(Delay_us(60)→Delay_us(50)),否则从机采样窗口被压缩。
3.2 总线长度与节点数的硬约束:不是“理论上支持 127 个”,而是“实际能稳定读出几个”
Dallas 官方文档称 1-Wire 支持 100 米总线、127 个节点,但这是在理想实验室条件下。实测 C51 系统的瓶颈在信号边沿退化:
| 节点数 | 总线长度 | 实测最大可靠读取数 | 关键现象 |
|---|---|---|---|
| 1 | 1m | ✅ 100% | 波形干净,上升沿陡峭 |
| 4 | 5m | ✅ 95% | 上升沿略缓,需Delay_us(70)调整为65 |
| 8 | 10m | ⚠️ 70% | 波形出现振铃,OW_Read_Bit()偶发误判 |
| 12 | 15m | ❌ 0% | 上升沿 > 2μs,DS18B20 无法识别起始位 |
根本原因:每个 DS18B20 的输入电容约 10pF,8 个节点叠加 + PCB 分布电容 ≈ 150pF。当R=4.7kΩ时,RC时间常数达 705ns,而 1-Wire 要求上升时间<1μs。解决方案不是换更大 MCU,而是物理层改造:
- 使用双绞线(降低分布电容)
- 总线末端并联 100pF 电容(吸收高频振铃)
- 在 MCU 输出端串 33Ω 电阻(阻尼匹配)
3.3 示波器抓波形的三个必看位置:定位时序故障的黄金三角
别只盯着OW_Reset()的波形——真正翻车点往往藏在细节里:
复位脉冲下降沿后 480μs 处:用光标测量
OW_PIN从0变1的时刻,误差应<±5μs。若超差,检查Delay_us(480)是否被编译器优化(关掉Optimize等级)或晶振频率设置错误。OW_Read_Bit()的采样点:标准要求在下降沿后 15μs 采样。用示波器触发在OW_PIN下降沿,然后移动时基,看OW_PIN电平在 15μs 处是否稳定。若此处电平抖动,说明上拉不足或分布电容过大。OW_Write_Bit(0)的低电平宽度:必须60–120μs。实测发现Delay_us(60)在 Keil uVision 4 中实际为 63μs(因函数调用开销),故ow.c中写为Delay_us(60)是安全的;但若换成Delay_us(50),则部分 DS18B20 会忽略该位。
玄学技巧:在
main.c中OW_Read_Temp()前插入Delay_ms(1),可显著提升多节点读取成功率。这不是延时需求,而是给总线电荷充分释放的时间——尤其当多个 DS18B20 同时启动转换时,总线瞬时灌电流达 1.5mA,需时间恢复。
3.4 避坑:1-Wire 总线开发中最常踩的 4 个坑及根因修复
坑 1:Keil uVision 4 编译通过,但烧录后OW_Reset()永远返回 1
→现象:仿真器显示OW_PIN电平正常变化,但实际用示波器测 P1.7 始终为高
→原因:sbit OW_PIN = P1^7;定义正确,但硬件上 P1.7 引脚被其他外设(如串口 TXD)复用,或 PCB 上该引脚虚焊
→解决:用万用表通断档测 P1.7 到 DS18B20 数据线是否导通;查原理图确认 P1.7 无其他连接;更换 MCU 引脚(如改用 P2^0)重试
坑 2:单个 DS18B20 正常,挂 2 个就偶尔读错 ROM
→现象:OW_Read_ROM()返回的 64 位中,某几位随机翻转(如28 FF 0A 00 00 00 00 10变成28 FF 0A 00 00 00 00 30)
→原因:总线负载过重,OW_Read_Bit()采样时刻电平未稳定,且 CRC 校验未开启(ow.c默认关闭 CRC)
→解决:在ow.h中取消注释#define OW_CRC_CHECK,并在OW_Read_ROM()后调用OW_CRC8(rom_code, 7)校验前 7 字节;同时将上拉电阻降至 3.3kΩ
坑 3:OW_Start_Conversion()执行后,OW_Read_Temp()读出 0x0550(85℃)
→现象:无论环境温度如何,始终返回 0x0550(十六进制)= 1360d = 85℃(DS18B20 上电默认值)
→原因:OW_Start_Conversion()发送0x44命令后,未等待转换完成(750ms),就立即执行OW_Read_Scratchpad()
→解决:OW_Start_Conversion()后必须加Delay_ms(750),或改用 parasite power 模式时用OW_Read_Power()查询忙状态
坑 4:Keil uVision 4 提示*** WARNING L15: MULTIPLE CALL TO SEGMENT
→现象:编译警告,但程序运行异常(如OW_Read_Bit()返回随机值)
→原因:Delay_us()函数被ow.c和main.c同时调用,而 C51 默认将函数放在CODE段,链接器未去重
→解决:在delay.c顶部添加#pragma codeseg,并在delay.h中声明extern void Delay_us(unsigned int us);,确保只有一份代码实体
4. C51 编译器底层行为揭秘:为什么using n后函数开头自动插PUSH指令
4.1using n的真实作用:不是“指定寄存器组”,而是“告诉编译器别动我的 Rn”
C51 中void func() using 1常被误解为“使用第 1 组寄存器”,其实质是禁止编译器在函数入口/出口自动生成PUSH/POP保存 R0–R7。using n的n是寄存器组编号(0–3),但编译器只关心:若函数声明了using n,则默认该函数全程使用R0–R7(组 n),无需保存恢复;若未声明,则编译器认为 R0–R7 可能被破坏,必须在函数开头PUSH、结尾POP。
// ow.c 中关键函数均声明 using 1 void OW_Write_Bit(bit b) using 1 { EA = 0; if (b) { OW_PIN = 0; Delay_us(60); OW_PIN = 1; Delay_us(2); // 写1时序 } else { OW_PIN = 0; Delay_us(60); OW_PIN = 1; Delay_us(60); // 写0时序 } EA = 1; }为什么必须using 1?
因为OW_Write_Bit()内部Delay_us()调用频繁,若编译器自动PUSH/POPR0–R7,会额外增加 16 个机器周期(8×PUSH + 8×POP),导致Delay_us(60)实际变成Delay_us(76),超出 1-Wire 时序容忍范围(写0要求 60–120μs,超限则从机拒绝)。
4.2 查看汇编代码验证:using 1如何消除冗余指令
在 Keil 中右键OW_Write_Bit()→Browse to Definition,然后点击View → Disassembly Window,对比:
未加
using 1时:?PR?OW_WRITE_BIT?OW: PUSH AR0 ; ← 编译器自动插入 PUSH AR1 ... ; 共 8 条 PUSH ; 函数体 POP AR7 ; ← 编译器自动插入 POP AR6 ... ; 共 8 条 POP RET加上
using 1后:?PR?OW_WRITE_BIT?OW: ; 直接进入函数体,无 PUSH/POP CLR P1.7 MOV R7,#0FFH ... SETB EA RET
参数说明:
using n的n必须与实际使用的寄存器组一致。ow.c中所有延时和总线操作均使用R0–R7(默认组 0),但为避免与main()中其他函数冲突,统一指定using 1(组 1),这样编译器就不会假设OW_Write_Bit()会破坏组 0 的 R0–R7,从而省去保存指令。
4.3#pragma控制代码段:把Delay_us()强制塞进CODE段防优化
ow.c开头有这行:
#pragma code "CODE" // 强制后续函数放入 CODE 段 void Delay_us(unsigned int us) { unsigned int i; for (i = 0; i < us * NOP_CYCLES_PER_US; i++) { _nop_(); } } #pragma code // 恢复默认段为什么需要#pragma code?
Keil C51 默认将函数放在?PR?段(可重定位代码段),但Delay_us()的for循环极易被编译器优化掉(如-O9级别下,若us为常量,直接展开为NOP序列)。#pragma code "CODE"强制其位于绝对地址段,禁用跨函数优化,确保Delay_us(480)一定执行 480 次_nop_(),而非被替换成其他指令。
4.4xdatavsdata:为什么rom_code[8]必须放xdata
ow.h中声明:
xdata unsigned char rom_code[8]; // ✅ 正确 // data unsigned char rom_code[8]; // ❌ 错误:data 区仅 128B,且指针操作受限原因:OW_Search_ROM()中需对rom_code[i/8]进行位操作(|=,&=),而 C51 对data区变量的位操作生成MOV C, bit_addr指令,但rom_code[i/8]是数组元素,地址动态计算,data区不支持动态位寻址。xdata区虽访问慢(需MOVX指令),但支持任意地址的字节操作,且ow.c中rom_code仅在搜索时写入,读取频率低,性能可接受。
5. 把 one.zip 变成你的 1-Wire 开发后悔药:三个即插即用的实战技巧
5.1 温度读取稳定性增强:用两次读取 + CRC 校验剔除偶发错误
ow.c原生OW_Read_Temp()仅读一次 Scratchpad,但实测中 DS18B20 在电源波动时偶发返回错误值(如0xFFFF)。我加了一段校验逻辑,把它变成“工业级”:
// 替换原 OW_Read_Temp() 函数 int OW_Read_Temp_Stable(void) { unsigned char sp[9]; // Scratchpad 9 字节:2字节温度 + 3字节 TH/TL/Config + 1字节 CRC + 3字节预留 unsigned char i; int temp; for (i = 0; i < 3; i++) { // 最多重试 3 次 if (OW_Reset() == 0) { OW_Write_Byte(0xCC); // Skip ROM OW_Write_Byte(0xBE); // Read Scratchpad for (int j = 0; j < 9; j++) { sp[j] = OW_Read_Byte(); } if (OW_CRC8(sp, 8) == sp[8]) { // 校验前 8 字节 CRC temp = (sp[1] << 8) | sp[0]; if (temp != 0x0550 && temp != 0xFFFF) { // 排除 85℃ 和错误码 return temp; } } } Delay_ms(50); // 重试间隔 } return 0x0550; // 三次失败,返回默认值 }关键点:OW_CRC8()是ow.c内置函数,但原版未暴露接口。我在ow.h中添加声明extern unsigned char OW_CRC8(unsigned char *data, unsigned char len);,并在ow.c末尾补上其实现(查表法,256 字节 ROM 占用,值得)。这样每次读温度都带 CRC,且自动过滤0x0550(未转换完成)和0xFFFF(通信错误),比单纯加Delay_ms(1)更可靠。
5.2 多节点 ROM 自动录入:用串口打印所有设备 ROM,免去手抄
main.c中加入这段,上电后通过串口(需另配 MAX232)自动列出所有 DS18B20 的 64 位 ROM:
void OW_Print_All_ROMs(void) { unsigned char i, j; unsigned char rom_list[64][8]; // 最多 64 个设备 unsigned char device_count = 0; OW_Search_ROM_Init(); // 初始化搜索 while (OW_Search_ROM(rom_list[device_count]) == 0) { device_count++; if (device_count >= 64) break; } SBUF = 'R'; while (!TI); TI = 0; // 发送 "ROM:" SBUF = 'O'; while (!TI); TI = 0; SBUF = 'M'; while (!TI); TI = 0; SBUF = ':'; while (!TI); TI = 0; for (i = 0; i < device_count; i++) { for (j = 0; j < 8; j++) { printf("%02X", rom_list[i][j]); // 用 Keil 自带 printf(需重定向到串口) } printf("\r\n"); } }注意:
printf需在main.c中包含stdio.h,并在init_serial()中重定向fputc到SBUF。这样上电后串口助手上直接看到所有 ROM,复制粘贴到OW_Match_ROM()的参数里,不用再拿示波器抓波形反推。
5.3 总线健康度自检:用OW_Read_Power()判断是否接 Parasite Power
ow.c中OW_Read_Power()函数常被忽略,但它能告诉你总线是否在“偷电”:
bit OW_Bus_Powered(void) { if (OW_Reset() == 0) { OW_Write_Byte(0xB4); // Read Power Supply 命令 return OW_Read_Bit(); // 1=外部供电,0=寄生供电 } return 1; // 默认认为有外部供电 }实战价值:当挂载多个 DS18B20 时,若OW_Bus_Powered()返回0,说明总线处于 Parasite Power 模式,此时OW_Start_Conversion()后必须保证总线持续高电平至少 750ms(靠上拉电阻供电),否则转换失败。我一般在main()开头加:
if (OW_Bus_Powered() == 0) { // 寄生供电模式:加强总线上拉,延长转换等待 OW_Write_Byte(0xCC); OW_Write_Byte(0x44); Delay_ms(750); // 不能省! } else { // 外部供电:可并发启动转换 OW_Write_Byte(0xCC); OW_Write_Byte(0x44); }从那以后我每次新焊一块板子,第一件事就是用OW_Print_All_ROMs()看串口有没有输出,再用示波器抓OW_Reset()波形确认上升沿是否达标,最后跑一遍OW_Read_Temp_Stable()连续读 100 次看有没有0xFFFF。这套组合拳下来,基本告别“为什么昨天还好好的,今天就读不出”的玄学时刻。希望帮到你。
本文还有配套的精品资源,点击获取