RISC-V开发实战:wujian100驱动DHT11单总线温湿度传感器

发布时间:2026/10/13 8:48:47
RISC-V开发实战:wujian100驱动DHT11单总线温湿度传感器
1. 从裸机到传感器为什么选DHT11作为入门第一站拿到一块陌生的开发板尤其是像wujian100这种基于RISC-V架构、配套工具链还在逐步完善的平台很多人第一反应是点灯。点灯确实能验证工具链和下载流程是否通畅但它验证不了时序控制能力也验证不了对外设的读写逻辑。DHT11这个温湿度传感器恰好卡在一个非常舒服的位置它足够简单单根数据线、单总线协议、不需要额外的ADC或者I2C控制器但它又足够“刁钻”对微秒级的时序有明确要求能真正检验你对GPIO方向切换、延时精度、中断处理的理解程度。我选择在wujian100上驱动DHT11核心目的不是做一个温湿度计而是借这个传感器把GPIO的输入输出切换、微秒级延时、单总线协议的位判决逻辑这三件事一次性打通。这三件事在后续驱动WS2812灯带、红外接收头、超声波模块时都会反复用到属于嵌入式开发的基本功。DHT11的协议手册写得很清楚但真正在RISC-V核上跑起来你会发现手册里没写的东西才是关键——比如上拉电阻到底要不要加、延时函数怎么校准、读到的数据怎么判断有效性。这篇文章适合两类人一类是刚拿到wujian100开发板已经跑通了GPIO点灯想找一个有实际反馈的外设来练手另一类是在其他平台上用过DHT11但换到RISC-V架构后时序对不上想看看别人是怎么解决时钟频率差异带来的延时问题的。我会把整个调试过程拆开包括我踩过的坑和最后验证有效的方案代码层面给出关键片段但不会贴完整工程因为每个人的工程结构不一样贴全了反而不好移植。2. DHT11单总线协议的核心细节与wujian100的适配考量2.1 单总线协议的时序本质DHT11用的是单根数据线完成双向通信这种设计省引脚但对时序的要求就变得很苛刻。整个通信过程可以拆成五个阶段主机发送起始信号、DHT11响应、主机读取40位数据、数据校验、总线释放。每个阶段的电平持续时间和采样时机都有明确规定偏差超过一定范围就会读失败或者读到全0。起始信号是主机把数据线拉低至少18毫秒然后拉高20到40微秒接着释放总线。DHT11检测到这个起始序列后会先把总线拉低80微秒作为响应再拉高80微秒然后开始逐位输出数据。每一位数据都以50微秒的低电平开始随后的高电平持续时间决定这一位是0还是1高电平持续26到28微秒表示0持续70微秒表示1。这里的关键在于主机需要在每个位的高电平期间完成采样而不是在整个位周期结束时采样。我一开始犯的错误就是采样点选错了。我在高电平结束后才去读引脚状态结果读到的全是0因为那时候总线已经被DHT11拉低准备下一位了。正确的做法是在检测到低电平跳变到高电平之后延时大约40微秒再采样这时候如果还是高电平就是1如果已经变低就是0。这个40微秒不是随便定的它要落在26到28微秒和70微秒之间取中间值最稳妥。2.2 wujian100的GPIO特性与方向切换wujian100的GPIO模块支持输入输出方向独立配置但有一点需要注意从输出模式切换到输入模式时引脚的电平状态会短暂不稳定。我在调试时发现如果在拉高总线后立刻切换为输入偶尔会读到一个错误的低电平。后来我在切换方向后加了一个几微秒的稳定延时问题就消失了。这个延时不需要精确用空循环就能实现但必须有。另一个细节是内部上拉。wujian100的GPIO内部上拉电阻阻值比较大大约在50k到100k欧姆之间。DHT11在释放总线时靠的是外部上拉把电平拉高。如果只用内部上拉上升沿会变得很缓在示波器上看可能超过1微秒才爬到高电平阈值。虽然DHT11的时序要求不算特别快但为了稳定我还是建议在数据线和3.3V之间接一个4.7k到10k欧姆的外部上拉电阻。我实测过不加外部上拉时读取成功率大概在七成左右加了之后基本一次成功。2.3 延时函数的精度校准RISC-V核的指令执行时间和流水线结构跟ARM不太一样直接用for循环做延时在不同优化等级下差异很大。我一开始用for(i0;i100;i);这种写法在O0优化下延时大约是8微秒开到O2就变成3微秒了。这种不确定性对DHT11来说是不可接受的。我的做法是用一个已知频率的定时器来校准。wujian100有内置的定时器外设我先用定时器产生一个1毫秒的基准然后在这个基准内跑空循环记录循环次数算出每次循环的平均耗时。之后所有微秒级延时都用这个校准后的循环次数来计算。虽然麻烦一点但校准一次之后后续所有时序相关的代码都不用再改了。如果你不想用定时器也可以用GPIO翻转加示波器测量的方式原理一样只是需要一台示波器。注意校准延时的时候要把编译优化等级固定下来后续编译都用同一个等级。如果换了优化等级延时循环的次数需要重新校准。3. 硬件连接与基础驱动框架搭建3.1 硬件连接清单与注意事项DHT11一般有三针或四针封装三针的只有VCC、GND、DATA四针的多一个NC空脚。我手上的是三针版本连接方式如下DHT11引脚wujian100引脚备注VCC3.3V不要接5VDHT11虽然标称3.3-5.5V但3.3V更稳妥GNDGND共地DATAGPIO任意引脚建议选一个靠近GND的引脚方便接上拉电阻上拉电阻接在DATA和3.3V之间阻值4.7k到10k都可以。我用的4.7k因为手头正好有。数据线长度不要超过20厘米太长的话分布电容会影响上升沿导致读取失败。如果确实需要延长可以考虑降低上拉电阻阻值但不要低于2.2k否则DHT11拉低总线时的灌电流可能超标。3.2 GPIO驱动层的封装在写DHT11驱动之前我先把GPIO的基本操作封装成几个函数设置方向为输出、设置方向为输入、写高电平、写低电平、读引脚电平。这几个函数用宏或者内联函数实现都可以关键是要保证操作的是同一个引脚不要在中途改变引脚编号。// gpio_utils.h #define DHT11_PIN 7 // 假设用GPIO7 static inline void dht11_set_output(void) { // 设置GPIO7为输出模式 GPIO_DIR | (1 DHT11_PIN); } static inline void dht11_set_input(void) { // 设置GPIO7为输入模式 GPIO_DIR ~(1 DHT11_PIN); } static inline void dht11_write_high(void) { GPIO_DATA | (1 DHT11_PIN); } static inline void dht11_write_low(void) { GPIO_DATA ~(1 DHT11_PIN); } static inline int dht11_read(void) { return (GPIO_DATA DHT11_PIN) 1; }这里的寄存器名是示意性的实际wujian100的GPIO寄存器地址和位定义需要查对应的手册。我建议把这几个操作放在头文件里用内联函数避免函数调用带来的额外开销。DHT11的时序虽然不算特别快但函数调用的压栈出栈在几十微秒的量级上还是有影响的。3.3 微秒延时函数的实现前面提到要用定时器校准这里给出一个校准后的延时函数示例。假设校准结果是每个空循环大约0.5微秒那么延时N微秒就需要循环2N次。// delay_utils.c static uint32_t loops_per_us 2; // 校准值根据实际情况调整 void delay_us(uint32_t us) { volatile uint32_t count us * loops_per_us; while (count--) { __asm__ volatile (nop); } }volatile关键字很重要防止编译器把循环优化掉。nop指令确保每次循环至少执行一条指令避免循环被合并。校准的时候我是在定时器中断里翻转一个GPIO然后用示波器看实际延时调整loops_per_us的值直到误差在5%以内。DHT11对时序的容忍度大概在正负10%左右5%的误差完全够用。提示如果你的工程里已经有毫秒级延时函数可以用它来校准微秒延时。比如调用delay_ms(10)在里面计数微秒循环跑了多少次反推出每微秒的循环次数。4. 完整读取流程与代码实现4.1 起始信号与响应检测起始信号的代码逻辑很直接先把引脚设为输出拉低至少18毫秒然后拉高20到40微秒接着设为输入等待DHT11的响应。int dht11_start(void) { dht11_set_output(); dht11_write_low(); delay_ms(20); // 拉低至少18ms这里给20ms留余量 dht11_write_high(); delay_us(30); // 拉高20-40us取中间值30us dht11_set_input(); delay_us(5); // 方向切换后的稳定延时 // 等待DHT11拉低响应 if (dht11_wait_level(0, 100) ! 0) return -1; // 超时返回错误 // 等待DHT11拉高 if (dht11_wait_level(1, 100) ! 0) return -1; // 等待DHT11再次拉低准备输出数据 if (dht11_wait_level(0, 100) ! 0) return -1; return 0; }dht11_wait_level函数的作用是在指定超时时间内等待引脚变成目标电平返回0表示成功-1表示超时。超时时间我设的是100微秒因为DHT11的响应信号最长也就80微秒左右100微秒足够覆盖。如果超时了说明传感器没响应可能是接线问题或者传感器坏了。4.2 40位数据的逐位读取响应信号结束后DHT11开始输出40位数据顺序是湿度整数、湿度小数、温度整数、温度小数、校验和。每一位的读取逻辑是先等低电平结束然后延时40微秒采样根据采样结果判断是0还是1。int dht11_read_bit(void) { // 等待低电平结束超时返回-1 if (dht11_wait_level(1, 100) ! 0) return -1; // 低电平结束后延时40us再采样 delay_us(40); // 采样如果还是高电平说明是1否则是0 if (dht11_read()) { // 等待高电平结束为下一位做准备 if (dht11_wait_level(0, 100) ! 0) return -1; return 1; } else { return 0; } }这里有个细节如果采样结果是1还需要等高电平结束才能读下一位如果是0高电平已经结束了直接返回即可。我一开始忘了在返回1之前等高电平结束结果下一位的读取时序全乱了读出来的数据完全不对。这个坑很隐蔽因为单独看每一位的逻辑好像没问题但连起来跑就出错。4.3 数据校验与温湿度转换读完40位之后把前32位按字节累加取低8位跟第40位比较相等则数据有效。int dht11_read_data(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] {0}; if (dht11_start() ! 0) return -1; for (int i 0; i 5; i) { for (int j 0; j 8; j) { int bit dht11_read_bit(); if (bit 0) return -1; data[i] (data[i] 1) | bit; } } // 校验 uint8_t checksum data[0] data[1] data[2] data[3]; if (checksum ! data[4]) return -1; *humidity data[0]; // 湿度整数部分 *temperature data[2]; // 温度整数部分 return 0; }DHT11的湿度小数和温度小数部分通常都是0因为它的分辨率就是1%RH和1摄氏度。有些资料说小数部分可以用来做扩展但我实测下来一直是0所以直接忽略。如果你需要更高精度得换DHT22或者SHT30那是另一个话题了。注意DHT11的采样周期不能短于1秒连续读取会导致数据不更新或者读取失败。我在主循环里加了1秒的延时实测下来很稳。5. 调试过程中遇到的典型问题与排查方法5.1 读取失败率高的几种原因我在调试过程中遇到过读取成功率很低的情况有时候十次里只能成功两三次。排查下来主要有三个原因上拉电阻缺失、延时不准、采样点偏移。上拉电阻的问题前面说过了加一个4.7k的电阻就能解决。延时不准需要校准这个也说了。采样点偏移比较隐蔽我一开始把采样延时设成了30微秒结果读到的数据偶尔会错位改成40微秒之后就稳定了。还有一个容易被忽略的点是电源质量。DHT11对电源纹波比较敏感如果开发板的3.3V上有较大的开关噪声读取失败率会明显上升。我在电源引脚旁边并了一个100nF的陶瓷电容效果立竿见影。这个电容不是必须的但加上之后稳定性提升很明显成本也就几分钱。5.2 常见问题速查表现象可能原因排查方法解决措施完全无响应接线错误或传感器损坏用示波器看数据线是否有起始信号检查接线更换传感器响应信号有但数据全0采样点太早或太晚调整采样延时用示波器看实际波形采样延时设为40us左右数据偶尔错位高电平结束后未等待检查read_bit函数逻辑返回1前等待高电平结束读取成功率低上拉电阻缺失或电源纹波大测量上升沿时间检查电源加上拉电阻和滤波电容连续读取失败采样周期太短检查主循环延时两次读取间隔至少1秒5.3 用示波器验证时序的小技巧如果你有示波器调试DHT11会轻松很多。把探头接到数据线上触发方式设为下降沿就能抓到完整的通信波形。重点看三个地方起始信号的拉低时间是否够18毫秒、响应信号的80微秒低电平是否标准、每一位的高电平持续时间是否在26到28微秒和70微秒附近。如果高电平时间明显偏短或偏长说明延时函数需要重新校准。没有示波器的话可以用逻辑分析仪价格便宜很多抓DHT11这种低速信号完全够用。再不行就用GPIO翻转加定时器计数的方式间接测量延时虽然麻烦但也能解决问题。6. 从DHT11延伸出去的几个实用方向DHT11跑通之后这套单总线时序控制的思路可以直接迁移到其他外设上。比如WS2812灯带用的也是单总线协议只是时序要求更严格位判决逻辑类似但时间窗口更窄。红外接收头解NEC协议本质上也是测量高低电平的持续时间来区分0和1跟DHT11的位判决逻辑异曲同工。超声波模块HC-SR04虽然用的是两根线但触发信号和回响信号的测量思路是一样的都是靠微秒级延时和电平检测。如果你想把温湿度数据用起来下一步可以加一个OLED屏幕做本地显示或者通过串口传到上位机画曲线。wujian100的串口外设配置起来不复杂把DHT11读到的数据格式化之后发出去就行。再进一步可以加一个SD卡模块做数据记录或者接一个无线模块做远程监测。这些扩展的方向很多但基础都是先把DHT11的时序吃透。我个人在实际操作中的体会是DHT11这个传感器虽然便宜但它的时序要求恰好卡在“需要认真对待但又不至于太难”的区间。把它调通之后你对GPIO方向切换、微秒延时、位采样这些基本功会有完全不一样的理解。后面再遇到类似的外设基本就是换个协议解析的事情底层的那套东西已经刻在脑子里了。