☰
RISC-V开发实战:wujian100驱动DHT11单总线温湿度传感器
2026/10/13 8:48:40 网站建设 项目流程

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(i=0;i<100;i++);这种写法,在O0优化下延时大约是8微秒,开到O2就变成3微秒了。这种不确定性对DHT11来说是不可接受的。

我的做法是用一个已知频率的定时器来校准。wujian100有内置的定时器外设,我先用定时器产生一个1毫秒的基准,然后在这个基准内跑空循环,记录循环次数,算出每次循环的平均耗时。之后所有微秒级延时都用这个校准后的循环次数来计算。虽然麻烦一点,但校准一次之后,后续所有时序相关的代码都不用再改了。如果你不想用定时器,也可以用GPIO翻转加示波器测量的方式,原理一样,只是需要一台示波器。

注意:校准延时的时候要把编译优化等级固定下来,后续编译都用同一个等级。如果换了优化等级,延时循环的次数需要重新校准。

3. 硬件连接与基础驱动框架搭建

3.1 硬件连接清单与注意事项

DHT11一般有三针或四针封装,三针的只有VCC、GND、DATA,四针的多一个NC空脚。我手上的是三针版本,连接方式如下:

DHT11引脚wujian100引脚备注
VCC3.3V不要接5V,DHT11虽然标称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方向切换、微秒延时、位采样这些基本功会有完全不一样的理解。后面再遇到类似的外设,基本就是换个协议解析的事情,底层的那套东西已经刻在脑子里了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询