基于ICCAVR和Proteus的DS18B20温度采集仿真

基于ICCAVR和Proteus的DS18B20温度采集仿真 简介基于ICCAVR与ATmega16的DS18B20温度采集显示工程包面向单片机学习者和嵌入式开发工程师。工程以ATmega16为主控通过一线总线驱动DS18B20温度传感器在LCD1602上实现正负温度显示并精确到0.0001℃。Proteus仿真文件DSN可用于电路验证适合温度监测、仪表显示类项目参考。包内共96个文件约270KB以C源码、备份文件LCD.c、ds18b20.c、shi.c、编译中间文件o/lis/hex/cof及Proteus工程文件为主并保留了大量带时间戳的历史版本便于观察开发调试迭代路径。已有124人学习下载。通过这份工程可以深入了解ICCAVR环境下的AVR端口配置、一线总线时序解析、高精度温漂处理思路以及Proteus与AVR的联合仿真流程。对于想掌握DS18B20驱动写法或快速搭建高精度温度显示方案的人是一份较完整的实操参考。1. ICCAVR 下 DS18B20 驱动挂在 ATmega16 上Proteus 仿真的第一步不是写代码一个命名带着ceshi.zip和ICCAVR的工程包里面的核心组合基本固定ICCAVR 写 C 程序、ATmega16M16当主控、DS18B20 测温、Proteus 做电路仿真。这套组合在课程设计和仪表类开发里出现频率很高但绝大多数第一次跑的人会卡在同一个地方代码看起来对编译也通过Proteus 里温度却一直是 85.00 或干脆不更新。反直觉的结论是问题往往不在 DS18B20 的温度换算而在单总线的时序没有落在传感器要求的窗口里或者 DQ 线上少了 4.7k 上拉电阻。本文按“时序原理 → ICCAVR 工程 → Proteus 联调 → 多点测温”的顺序把能直接复现的最小驱动讲清楚。2. DS18B20 单总线时序先搞清楚读写窗口再写 ICCAVR 代码2.1 DS18B20 的“严格时序”到底严在哪DS18B20 用单根 DQ 线完成双向通信。与 I2C 不同它的位时序在微秒级别有明确边界驱动代码的每条指令展开时间都会影响结果。主机的每个通信周期都由主机拉低 DQ 开始传感器在特定窗口释放或拉低总线回答问题。操作低电平时间释放后等待采样点/结果复位脉冲480~960 µs60~240 µs存在脉冲为低写 060~120 µs释放整个写时隙保持低写 11~15 µs释放写时隙余下为高读时隙1~15 µs主机释放总线15 µs 内采样温度读取流程固定为四步复位 → 跳过 ROM(0xCC) → 启动转换(0x44) → 等待转换完成 → 复位 → 跳过 ROM → 读暂存器(0xBE)。超过一个 DS18B20 时才需要 ROM 匹配指令 (0x55)后面第 5 章用独立 IO 的方式绕开它。注意单总线空闲必须为高。DS18B20 数据手册明确要求 DQ 接 VCC 的 4.7k 上拉电阻这不仅决定存在脉冲能不能被主机看到也决定写 1 和读数据时总线能不能回到高电平。Proteus 里不画上拉电阻时“复位成功”的概率很低。2.2 ATmega16 的 IO 翻转与延时函数选择ICCAVR 不像 GCC 自带_delay_us()所以要先有一个可控的微秒延时。ATmega16 内部默认时钟可能是 1MHzProteus 中双击单片机属性可以改成 8MHz下面的延时按 8MHz 估算。// ICCAVR 中的微秒延时8MHz 时钟下按汇编周期估算 void delay_us(unsigned int us) { while (us--) { asm(nop); // 1 个时钟周期 0.125us asm(nop); } }这段代码里循环体除了两条 nop还有 while 的判断与跳转实际周期会比 0.25us 略大。真正的延时精度不需要毫米级因为 DS18B20 的窗口最短也有 15 µs。ICCAVR 的优化等级会影响循环展开建议优化级别设在默认或关闭优化并在需要保持时序的变量前加volatile。上面代码中的asm(nop)是 ICCAVR 支持的嵌入汇编写法如果用_NOP()宏也可以但不同版本头文件引入的宏名略有差异。需要说明8MHz 只是推荐起点。如果读者用的晶振是 4MHzdelay_us(60)实际时间会翻倍到 120 µs接近读时隙的上限所以整板晶振必须和延时函数匹配。2.3 复位脉冲与存在脉冲的读取将 DQ 接到 ATmega16 的 PA0定义一组宏来切换端口方向和电平。#define DQ_DDR DDRA #define DQ_PORT PORTA #define DQ_PIN PINA #define DQ_BIT 0 // 拉低总线方向输出电平拉低 #define DQ_LOW do { DQ_DDR | (1DQ_BIT); DQ_PORT ~(1DQ_BIT); } while(0) // 释放总线方向输入使能内部上拉 #define DQ_RELEASE do { DQ_DDR ~(1DQ_BIT); DQ_PORT | (1DQ_BIT); } while(0) // 读取输入 #define DQ_READ ((PINA DQ_BIT) 0x01)释放总线时同时把 PORT 置 1是为了启用 ATmega16 内部上拉。Proteus 仿真中内部上拉可能掩盖缺少外部电阻的问题但实物仍然建议在 DQ 与 VCC 之间接 4.7k 电阻。unsigned char ds18b20_reset(void) { DQ_LOW; delay_us(500); // 复位脉冲 480us 以上 DQ_RELEASE; delay_us(70); // 等传感器拉低存在脉冲 unsigned char presence DQ_READ; // 0 表示存在 delay_us(400); // 补完整个复位时隙 return presence 0; }存在脉冲的判断窗口是主机释放总线后的 60~240 µs。代码在第 70 µs 采样落在窗口内。若返回 0表示 DS18B20 已把总线拉低如果不为 0基本可以断定复位失败优先查上拉电阻、IO 配置和延时是否过短。2.4 写时隙和读时隙的位操作写位和读位都必须在 60~120 µs 内完成一个时隙。DS18B20 在主机拉低后的 15 µs 内采样数据因此写 1 时可以先拉低约 5 µs再释放总线写 0 则保持低电平约 60 µs。void ds18b20_write_bit(unsigned char bit) { if (bit) { DQ_LOW; delay_us(5); // 小于 15us DQ_RELEASE; delay_us(55); } else { DQ_LOW; delay_us(60); // 保持整个写 0 时隙 DQ_RELEASE; delay_us(5); } } unsigned char ds18b20_read_bit(void) { DQ_LOW; delay_us(2); // 拉低 2us 产生读时隙 DQ_RELEASE; delay_us(2); // 第 15us 前采样 unsigned char val DQ_READ; delay_us(55); return val; }读时序的关键是“主机释放总线后由 DS18B20 拉低或保持高”如果主机端口仍为输出高就永远读到 1。因此每次读前必须把 DDR 切成输入。字节级函数就是 8 次位操作先发最低位这与 DS18B20 的 LSB First 规则一致。void ds18b20_write_byte(unsigned char data) { for (unsigned char i 0; i 8; i) { ds18b20_write_bit(data 0x01); data 1; } } unsigned char ds18b20_read_byte(void) { unsigned char data 0; for (unsigned char i 0; i 8; i) { data 1; if (ds18b20_read_bit()) data | 0x80; } return data; }不要小看data 1与data | 0x80的配合先右移再置最高位得到的是 LSB First 的字节顺序反了会读出完全乱的温度值。到这里底层驱动已经具备下一步要把这些函数组织成 ICCAVR 工程。3. 在 ICCAVR 里搭建 ATmega16 工程并完成温度换算3.1 ICCAVR 工程选项与 HEX 输出ICCAVR 的使用流程是新建工程后选择 AVR 芯片型号为 ATmega16在 Project Options 的 Target 页确认器件型号和时钟频率。如果代码用了延时估算这里的时钟必须填 8MHzICCAVR 本身不会校验时钟靠的是开发者在delay_us里保持一致。编译后默认输出.hex文件Proteus 需要的就是它。遇到编译报错提示找不到iom16v.h时检查头文件包含路径是否指向 ICCAVR 的 include 目录常见安装路径形如C:\iccv7avr\include。工程目录不要放在含中文或空格的路径ICCAVR 对路径解析能力较弱这会影响生成文件定位。3.2 IO 初始化与主流程ATmega16 默认上电后端口为输入高阻直接把宏定义代入使用时 DQ_LOW 就已经完成方向切换。主循环里需要做的只有复位、发指令、等待转换、再读暂存器。void main(void) { unsigned int raw; int temp_x10; char buf[8]; while (1) { ds18b20_reset(); ds18b20_write_byte(0xCC); // 跳过 ROM ds18b20_write_byte(0x44); // 启动温度转换 delay_ms(750); // 12 位分辨率下需要 750ms ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); // 读暂存器 unsigned char tl ds18b20_read_byte(); unsigned char th ds18b20_read_byte(); raw tl | (th 8); // 温度换算放到 3.3 节说明 delay_ms(1000); } }这段代码里的两个0xCC都只针对单传感器。第一次复位后的0x44让传感器开始模数转换之后必须等待足够时间。有人用循环查询 DQ 电平来判断转换完成但 ICCAVR 下查询写法容易在优化后变成死循环老实用delay_ms(750)更省事。3.3 12 位温度数据的符号与分辨率处理DS18B20 的暂存器第 0、1 字节是温度低字节和高字节12 位分辨率下低 4 位为小数位数据格式是二进制补码。直接把两个字节拼起来后需要区分正负值。int calc_temp_x10(unsigned int raw) { int t_x10; if (raw 0x8000) { raw (~raw 1) 0xFFFF; // 补码转原码 t_x10 (int)((raw * 625) / 10); // 0.0625*1006.25? t_x10 -t_x10; } else { t_x10 (int)((raw * 625) / 10); } return t_x10; // 温度*10例如 256 表示 25.6 }这里raw * 625 / 10是把分辨率 0.0625 放大 10 倍的整数算法0.0625 × 10 0.625所以原码数值乘以 625 再除以 1000。为便于显示写成(raw * 625) / 1000也可以但要注意 ICCAVR 的 int 是 16 位raw 最大 2047乘以 625 后约 127 万会超出 16 位有符号范围必须写成(unsigned long)raw * 625 / 1000。这个溢出是 ICCAVR 迁移到其他编译器时最容易踩的坑。ICCAVR 也支持 32 位长整型但使用long类型前应确认编译选项里没有启用紧凑库。换算后的整数可以直接拼字符串避免引入浮点数sprintf(buf, %d.%d, t_x10 / 10, t_x10 % 10);注意t_x10 % 10在负温度时可能为负显示前要先取绝对值。3.4 显示或上传前的数据拼装LCD1602 是这套课程设计最常见的显示设备。LCD 驱动不是本文主题但数据从buf到屏幕之间的拼接是高频出错点。常见做法是先把t_x10换算成字符串再一次性发给 LCD 的写数据函数不要在 LCD 内部做除法。如果题目场景是“温度过高报警”可以在主循环里加一段if (t_x10 420) // 42.0 度 { PORTC | (1 PC0); // 接蜂鸣器输出高 } else { PORTC ~(1 PC0); }Proteus 仿真中蜂鸣器没声音多是蜂鸣器元件电源电压设置问题或驱动为灌电流方式与门控信号极性相反和 DS18B20 本身无关。这个点在第 4 章的排错表里会再次出现。4. 在 Proteus 8 Professional 里完成仿真联调4.1 元件放置与时钟频率设置Proteus 中新建工程后从元件库依次放置 ATmega16、DS18B20、电阻 4.7k、LED 或蜂鸣器。双击 ATmega16 弹出属性面板把 Program File 指向 ICCAVR 编译出的 hex 文件同时将 Clock Frequency 改为 8MHz。晶振可以不画Proteus 直接使用属性里的时钟频率。DS18B20 的 DQ 引脚接 PA0VCC 接 5VGND 接地DQ 与 5V 之间放一只 4.7k 电阻。Proteus 的模型对缺少上拉电阻非常敏感不加电阻的温度值会长期显示 85 或出现随机跳变。4.2 运行与观察总线波形在 Proteus 左侧工具栏选择虚拟仪器放置一个示波器把通道 A 接到 DQ 节点。运行仿真后示波器上应能看到周期约为 750ms 的一簇脉冲。每簇脉冲的第一个低电平宽度约 500µs是复位信号随后的一串 60~120µs 时隙是命令字节。如果看不到 500µs 的低电平说明delay_us的循环体时间计算严重偏离 8MHz或者程序没进主循环。Proteus 的虚拟示波器要使用交流耦合还是直流耦合都无所谓关键是时基要调到 200µs/div 到 1ms/div。锁住波形后再用光标测量低电平宽度能直接验证 DS18B20 时序是否落在规格内。4.3 常见现象与排查优先级现象主要原因处理方式一直显示 85.00上电默认值或复位失败检查复位时序、上拉电阻显示 -55.00读到的全是 1总线悬空检查 DQ 是否接对、MCU 时钟频率温度不刷新没有等待转换完成确认0x44后延时 750ms数值是 25.0 的倍数乱跳字节序 LSB/MSB 反了检查read_byte的移位方向编译通过但 Proteus 无反应HEX 没有加载或路径不对双击 MCU 重新加载 hex蜂鸣器不响驱动极性和电压不匹配改用灌电流方式或加三极管断电重启后第一次读到的 85.00 代表 DS18B20 内部上电默认值只要程序后面有正常转换这个值会被覆盖。如果用户看到 85 并保持不动优先怀疑程序根本没有执行到0x44之后的读取流程。4.4 单步调试时序的替代方案Proteus 对 AVR 支持源码级调试但 ICCAVR 的调试信息与 Proteus 的兼容性不如 AVR Studio 的 COF 文件好。常见做法是不依赖源码调试而是在关键点翻转另一个 IO 口用示波器对比两个 IO 的脉冲宽度。例如在ds18b20_reset()进入和返回时翻转 PB0观察 PB0 高低电平的时间差就能测出复位函数的实际执行时间。这个方法在不连接实物的情况下同样有效而且比一步一步跟代码更直观。5. 多点测温技巧给每只 DS18B20 分配独立 IO避免 ROM 搜索当需求从“测一路温度”变成“测三路温度”时很多人的第一反应是走 ROM 搜索算法在单一总线上识别多个传感器。但 ROM 搜索的代码量至少 150 行Proteus 里还要仔细核对每个传感器的 64 位序列号排错成本很高。更稳的做法是让每个 DS18B20 独占一个 IO共用同一套驱动函数。接线时把三只传感器的 DQ 分别接到 PA0、PA1、PA2VCC 和 GND 并联。每根 DQ 仍然需要各自的 4.7k 上拉电阻不能为省电阻把它们并成一根线。驱动层面把宏改成可参数化的形式#define READ_SENSOR(port, pin) \ do { \ DQ_DDR port; /* 调用前设置方向寄存器 */ \ } while(0)宏在 ICCAVR 里展开后容易出现重复定义问题更常用的做法是复制三份端口定义或者写三个几乎相同的底层函数。复制代码虽然不优雅但在课程设计和原型验证阶段最可靠因为改一个传感器不会影响另外两路。读取顺序是先对第 1 路执行复位、0xCC、0x44等待 750ms 后同时复位三路并逐一读取暂存器。由于每路独立0xCC跳过 ROM 的指令在这里不产生冲突。三路温度数据分别存到数组后再统一做 3.3 节的补码换算。不要在每一路读取后才开始下一路的转换那样总耗时是 750ms 乘以传感器数量。最后一类容易忽略的问题是总线释放。每次DQ_RELEASE之后如果立即进行下一次复位传感器可能还在输出数据或总线电容未放完。建议在一次完整读取周期结束后延时 10ms 再接下一次复位这是 1-Wire 协议里规定的最小复位间隔。验证多点接线是否正确用 Proteus 的虚拟示波器分别观察三根 DQ 线三路都应该出现同样周期的脉冲簇。如果某一根线上完全没有低电平检查该 IO 对应的 DDR 宏是否写成了其他端口。仿真通过后再移到实物注意每只 DS18B20 的供电线要就近并 0.1µF 退耦电容长线传输时把上拉电阻减小到 2.2k 能改善波形边沿。把 PA0 到 PA2 三路都调到 25.0 附近后“多点温度采集”这个功能就基本完成。剩下的工作只剩显示排版和报警阈值整个工程从驱动到仿真已经闭环。本文还有配套的精品资源点击获取