1. 项目缘起与整体设计思路1.1 为什么选择CC2530加DHT11这个组合温湿度采集是嵌入式入门和物联网感知层开发里绕不开的一个基础场景。我最早接触这个组合是在一个无线传感网络的课程设计里当时的需求很明确用低成本方案实现多点温湿度数据的采集和短距离无线回传。选型的时候对比过几种方案STM32加DHT11性能过剩且成本偏高51单片机加DHT11虽然便宜但缺少无线能力最后落到CC2530上核心原因就是它自带IEEE 802.15.4射频收发器一颗芯片同时解决了采集和通信两件事外围电路极简。CC2530本质上是增强型8051内核加2.4GHz射频模块的SoC运行主频32MHz内置最高256KB闪存和8KB RAM对于驱动DHT11这种单总线传感器绰绰有余。DHT11则是经典的温湿度复合传感器内部集成了电阻式湿敏元件和NTC测温元件输出的是经过校准的数字信号单根数据线就能完成通信。两者搭配一个负责感知一个负责处理和传输逻辑上非常干净。这个项目的核心目标可以拆成三层第一层是让CC2530正确读取DHT11的温湿度数据第二层是把数据通过串口打印出来方便调试第三层是预留无线发送的接口为后续组网做准备。适合的读者包括正在做课程设计的同学、刚接触ZigBee协议栈的开发者以及想找一个完整练手项目的嵌入式新手。1.2 单总线时序为什么是最大的坑DHT11用的是单总线协议数据线既要输出又要输入方向切换全靠时序控制。它的通信过程是这样的主机先拉低总线至少18毫秒作为起始信号然后释放总线DHT11检测到起始信号后会拉低80微秒再拉高80微秒作为响应接着连续输出40位数据每一位以50微秒低电平开始高电平的持续时间决定这一位是0还是1——高电平持续26到28微秒表示0持续70微秒表示1。问题就出在这里。CC2530的8051内核在32MHz下跑一个机器周期的时间尺度是微秒级而DHT11的时序要求精确到微秒。如果直接用普通的延时函数编译器优化、中断打断、函数调用开销都会让延时变得不准。我一开始用简单的for循环做延时读出来的数据全是255或者0折腾了大半天才意识到是时序偏差太大。后来改用定时器配合空指令做精确延时数据才稳定下来。注意DHT11对时序的容忍度很低尤其是读取40位数据的那段每一位的高电平判断窗口只有几十微秒。任何中断都可能破坏时序所以读取期间最好关掉全局中断。1.3 整体方案的分层设计我把整个项目分成三个层次来组织代码这样调试的时候可以逐层验证出问题也容易定位。最底层是硬件驱动层包含GPIO初始化和微秒级延时函数。延时函数是整个项目的地基它不准后面全白搭。中间层是DHT11协议层负责起始信号、响应检测、40位数据读取和校验。最上层是应用层负责周期性地调用读取函数、格式化数据、通过串口输出以及预留无线发送的钩子。这种分层的好处是如果串口打印出来的数据不对我可以先单独测试延时函数是否准确再测试DHT11是否响应最后检查数据解析逻辑一层一层往上排查而不是面对一坨代码无从下手。2. 硬件连接与底层驱动细节2.1 引脚连接与上拉电阻的选择DHT11有三个引脚需要关注VCC、DATA、GND。VCC接3.3VGND接地DATA接CC2530的某个GPIO。我选的是P0_7这个引脚在CC2530上支持外部中断和普通IO切换比较灵活。这里有一个容易被忽略的细节DHT11的DATA线需要接一个上拉电阻典型值是4.7K到10K。原因是DHT11的数据线是开漏输出结构它只能主动拉低释放总线时靠上拉电阻把电平拉高。如果不接上拉电阻总线释放后会处于浮空状态读到的电平不确定。我一开始用的是开发板上自带的10K上拉实测波形上升沿偏缓后来换成4.7K波形干净了很多。元件规格作用DHT113.3V供电版本温湿度采集上拉电阻4.7K欧姆保证总线空闲时为高电平去耦电容100nF滤除电源纹波提升稳定性连接线杜邦线尽量短减少信号干扰提示如果DHT11和CC2530之间的距离超过20厘米建议在VCC和GND之间加一个100nF的去耦电容否则读数容易出现偶发错误。2.2 微秒级延时函数的实现延时函数是整个项目里最需要打磨的部分。CC2530的一个机器周期等于一个时钟周期32MHz晶振下就是31.25纳秒。8051的多数指令需要1到4个机器周期所以一条空指令NOP大约耗时31.25纳秒。我用的方案是用定时器1做微秒级延时。具体做法是配置定时器1为自由运行模式不分频让它以32MHz的频率计数。延时函数记录进入时的计数值然后循环等待计数值差值达到目标微秒数乘以32。这样做的精度比空循环高得多而且不受编译器优化影响。void delay_us(uint16 us) { uint16 start T1CNTL; uint16 target us * 32; while ((uint16)(T1CNTL - start) target); }这段代码里T1CNTL是定时器1的低16位计数器。每次循环读取当前计数值减去起始值就是经过的计数。因为定时器一直在跑所以这个差值就是实际经过的时间。实测下来延时100微秒的误差在2微秒以内完全满足DHT11的要求。2.3 GPIO方向切换的正确姿势单总线通信需要在输出和输入之间切换。CC2530的GPIO通过P0DIR寄存器控制方向某位为1表示输出为0表示输入。切换的时候要注意先把引脚设置为输出并拉高再切换方向避免切换瞬间产生毛刺。// 设置为输出 P0DIR | 0x80; // 设置为输入 P0DIR ~0x80;读取引脚电平用P0_7这个位写的时候直接赋值。这里有个小技巧CC2530的P0INP寄存器可以配置引脚为上拉、下拉或三态。做单总线的时候输出阶段配置为推挽输出输入阶段配置为三态让外部上拉电阻起作用。3. 完整实操流程与核心代码解析3.1 起始信号与响应检测起始信号的流程是主机把DATA线拉低至少18毫秒然后拉高20到40微秒接着切换为输入模式等待DHT11响应。DHT11检测到起始信号后会先拉低80微秒再拉高80微秒这两个电平就是响应信号。uint8 dht11_start(void) { uint8 retry 0; // 主机拉低至少18ms DHT11_OUT_LOW(); delay_ms(20); // 主机拉高20-40us DHT11_OUT_HIGH(); delay_us(30); // 切换为输入等待DHT11响应 DHT11_IN(); // 等待DHT11拉低 while (DHT11_READ() retry 100) { retry; delay_us(1); } if (retry 100) return 1; // 超时无响应 retry 0; // 等待DHT11拉高 while (!DHT11_READ() retry 100) { retry; delay_us(1); } if (retry 100) return 1; // 等待DHT11再次拉低准备输出数据 retry 0; while (DHT11_READ() retry 100) { retry; delay_us(1); } if (retry 100) return 1; return 0; // 响应正常 }这段代码里的retry计数器是防止死循环的关键。如果DHT11没接好或者坏了没有超时机制的话程序会卡死在while循环里。我一般把超时阈值设在100微秒左右实测DHT11的响应时间远小于这个值。3.2 40位数据的读取与解析响应信号之后DHT11连续输出40位数据顺序是湿度整数8位、湿度小数8位、温度整数8位、温度小数8位、校验和8位。每一位的读取逻辑是先等待50微秒的低电平结束然后延时30微秒后采样如果此时是高电平说明这一位是1否则是0。uint8 dht11_read_byte(void) { uint8 i, byte 0; for (i 0; i 8; i) { // 等待低电平结束 while (DHT11_READ() 0); // 延时30us后采样 delay_us(30); byte 1; if (DHT11_READ()) { byte | 1; // 等待高电平结束 while (DHT11_READ()); } } return byte; }这里延时30微秒是有讲究的。DHT11的每一位以50微秒低电平开始然后高电平持续26到28微秒表示0持续70微秒表示1。如果在低电平结束后延时30微秒再采样此时0的高电平已经结束变成低电平而1的高电平还在持续所以采样结果就能区分0和1。这个30微秒是中间值留了足够的容错空间。3.3 数据校验与串口输出读到的5个字节里前4个是数据第5个是校验和。校验规则是前4个字节相加取低8位如果等于第5个字节就说明数据有效。uint8 dht11_read_data(uint8 *temp, uint8 *humi) { uint8 buf[5]; uint8 i; if (dht11_start() ! 0) return 1; for (i 0; i 5; i) { buf[i] dht11_read_byte(); } // 校验 if (buf[0] buf[1] buf[2] buf[3] buf[4]) { *humi buf[0]; *temp buf[2]; return 0; } return 1; }串口输出部分CC2530的UART0配置比较标准波特率设96008位数据位无校验1位停止位。打印格式我习惯用湿度:XX% 温度:XXC这种方便肉眼观察。如果要接无线发送就在串口输出之后调用射频发送函数把数据打包成自定义格式发出去。3.4 主循环的节奏控制DHT11的采样周期不能太快数据手册要求两次读取间隔至少1秒实际使用中我建议2秒以上。原因是DHT11内部需要时间完成一次测量如果读得太频繁它会返回上一次的缓存数据甚至直接不响应。void main(void) { uint8 temp, humi; init_gpio(); init_timer1(); init_uart(); while (1) { if (dht11_read_data(temp, humi) 0) { printf(湿度:%d%% 温度:%dC\r\n, humi, temp); } else { printf(读取失败\r\n); } delay_ms(2000); } }主循环里加了失败重试的提示这样调试的时候一眼就能看出是通信问题还是数据问题。实测下来在室内环境下读取成功率能到99%以上偶尔失败一次下一次就正常了。4. 常见问题排查与避坑经验4.1 数据全是255或0怎么办这是最常见的问题基本可以锁定在时序上。排查顺序是这样的先用示波器或者逻辑分析仪看DATA线的波形确认起始信号的低电平时间是否达到18毫秒响应信号是否出现。如果没有响应检查接线和上拉电阻。如果有响应但数据不对重点看延时函数的精度。我遇到过一次数据全是255的情况查了半天发现是延时函数里的定时器没启动。CC2530的定时器1默认是停止的需要配置T1CTL寄存器启动它。这个细节很容易漏因为代码编译不报错但延时函数实际上一直在立即返回。注意使用定时器做延时之前务必确认定时器已经启动并且没有分频。T1CTL寄存器的低两位控制分频设为0表示不分频。4.2 读数偶尔跳变或校验失败偶发的校验失败通常和电源质量有关。DHT11对供电电压比较敏感如果VCC有纹波或者电压偏低内部测量电路会不稳定。我在一个项目里用USB供电读数一直有5%左右的概率失败后来在DHT11的VCC和GND之间并了一个100nF电容失败率降到几乎为零。另一个可能的原因是读取间隔太短。DHT11完成一次测量需要一定时间如果连续读取第二次可能读到的是未更新的数据。把间隔拉到2秒以上就能解决。4.3 无线发送时数据错乱如果项目里加了无线发送功能可能会遇到发送出去的数据和串口打印的不一致。这通常是因为射频发送和DHT11读取共用了某些资源比如定时器或者中断。我的做法是把DHT11读取和射频发送在时间上错开读取期间关闭射频中断发送期间不读传感器。问题现象可能原因排查方法解决方案数据全255延时不准检查定时器是否启动配置T1CTL启动定时器数据全0总线未释放检查GPIO方向切换输入模式配置为三态偶发校验失败电源纹波示波器看VCC波形加去耦电容响应超时上拉电阻缺失测量总线空闲电平加4.7K上拉电阻无线数据错乱资源冲突检查中断优先级错开读取和发送时序4.4 几个容易被忽略的实操心得第一个心得是关于编译器的。IAR编译器在不同优化等级下对延时循环的处理不一样我建议延时函数用volatile变量或者直接操作寄存器避免被优化掉。第二个心得是关于DHT11的封装市面上有两种引脚排列一种是小黑块正面朝自己从左到右VCC、DATA、GND另一种顺序不同接反了不会烧但肯定读不到数据。第三个心得是调试的时候先别急着上无线把串口调通再说串口是最直接的观测手段。5. 项目扩展与个人体会5.1 从单点到组网的扩展思路单点采集跑通之后下一步自然是组网。CC2530的射频部分支持IEEE 802.15.4协议可以在此基础上跑ZigBee或者自定义的星型网络。我的做法是定义一个简单的数据帧格式包含节点地址、温度、湿度、校验然后通过射频广播出去协调器收到后转发到串口。这样多个采集节点就能同时工作了。扩展的时候要注意射频发送会占用一定时间如果DHT11的读取周期和发送周期重叠可能会互相干扰。我的经验是把发送放在读取完成之后并且给射频发送留出足够的超时时间。5.2 低功耗场景下的注意事项如果项目是电池供电的低功耗就是必须考虑的问题。DHT11本身功耗不高但CC2530的射频和MCU全速运行比较费电。可以的做法是让CC2530在两次采集之间进入睡眠模式用定时器或者外部中断唤醒。DHT11在不读取的时候可以断电进一步降低功耗。不过低功耗和时序精度是有矛盾的。睡眠模式下定时器可能停止唤醒后需要重新校准延时。我的建议是如果对功耗要求不是特别苛刻保持MCU运行但降低主频这样时序稳定开发难度也低。5.3 我个人的几点真实体会这个项目看起来简单但真正跑通并且稳定运行还是花了我不少时间。最大的体会是嵌入式开发里时序就是生命线任何涉及微秒级操作的场景都不能想当然地用软件延时。定时器是更可靠的选择虽然配置麻烦一点但省去了后面调试的很多麻烦。另外一点是DHT11虽然便宜好用但它的精度和响应速度有限适合对成本敏感、对精度要求不高的场景。如果项目需要更高精度或者更快的响应可以考虑换成SHT30或者AHT20这类I2C接口的传感器驱动逻辑会简单很多因为I2C有时钟线同步不像单总线这样对时序如此敏感。最后分享一个小技巧调试DHT11的时候可以在读取函数里加一个全局变量记录失败次数跑一段时间后通过串口打印出来。这样能直观地看到通信质量比一次次手动观察高效得多。我在一个长期运行的项目里用这个方法发现每天凌晨湿度较高的时候失败率会略微上升后来在传感器周围加了防潮处理就解决了。