☰
DSP28335驱动DHT11温湿度传感器:GPIO时序与UART回传实战
2026/10/14 19:01:19 网站建设 项目流程

简介:基于DSP28335的DHT11读取温湿度与UART回传方案,面向DSP开发者和嵌入式系统学习者,解决在TMS320F28335平台上完成单总线传感器数据采集、协议解析与串口透传的问题。资源共95个文件,压缩包约309KB,包含17个C源程序、5个H头文件、3个汇编文件、16个makefile/mk构建脚本、2个cmd链接命令文件以及out/map等编译产物,属于完整的CCS(Code Composer Studio)工程。从内容预览看,工程内含DSP2833x标准外设库(ADC、SCI、SPI、GPIO、PieCtrl等)、DHT11驱动相关源文件、UART(SCI)初始化与回传逻辑,以及TMS320F28335.ccxml目标配置和28335_RAM_lnk.cmd内存链接脚本,可直接导入CCS后对照学习或烧写调试。目前已有880人学习下载,适合需要快速上手DSP28335外设编程、理解单总线时序和UART通信的开发者参考。

1. dsp28335读DHT11为什么容易翻车:这块小传感器比你想的难伺候

DHT11是单总线温湿度传感器,数据和主机之间只有一根信号线,所有时序都以微秒计;而28335是一颗150MHz主频的定点DSP,GPIO没有硬件单总线控制器,只能靠软件翻转电平加延时去卡每一位的宽度。很多人在这个需求上卡几天的原因,往往不是传感器坏了,而是三件事叠在一起:GPIO复用配置被误清、us级延时被编译器优化掉、UART回传收到一堆乱码。这篇文章会按照我实际调通的路径讲明白:GPIO怎么配置、DHT11的起始信号和数据位怎么采、UART怎么把温湿度发回上位机。适合正在做28335课程设计、小型环境监测板,或者打算用DSP代替单片机去接传感器的一线开发者和学生。你不用先精通单总线协议,按章节往下做,串口助手就能稳定读到温湿度。

2. 搭建28335最小数据通路:GPIO与us级延时是这层的地基

2.1 GPIO输入时钟与外设时钟:28335和单片机思路不一样

28335的GPIO在复位后并不是所有功能都默认可用,外设时钟那一关必须先过。很多人从单片机切过来,习惯直接用寄存器读写GPIO,结果读GPADAT永远看到同一个值,这就是输入时钟没打开的表现。28335上GPIO输入采样的同步时钟由PCLKCR3寄存器的GPIOINENCLK位控制,如果这个位是0,输入引脚的电平变化不会进到GPADAT里。

void GpioPeripheralClockInit(void) { EALLOW; // 使能GPIO输入时钟,否则GPADAT读不到真实引脚电平 SysCtrlRegs.PCLKCR3.bit.GPIOINENCLK = 1; // SCI-A外设时钟使能,后面UART回传依赖这一位 SysCtrlRegs.PCLKCR0.bit.SCIENCLKA = 1; EDIS; }

这段代码里EALLOW和EDIS是28335写系统控制寄存器时必须套的“保护罩”。EDIS之前如果少写了EALLOW,寄存器写操作会被硬件忽略,而且编译器不报错,这是C28x平台非常折磨人的一点。GPIOINENCLK位在PCLKCR3的bit0,复位值默认为关闭状态;SCIENCLKA在PCLKCR0的对应位,同样需要显式打开。建议把这两行放在整个工程的初始化最前面,比什么都先执行。

工程文件层面,一个能跑的原生工程至少要包含DSP2833x_GlobalVariableDefs.c、DSP2833x_DefaultIsr.c、DSP2833x_usDelay.asm这三个文件,否则你后面调用DELAY_US会直接链接报错。很多移植过来的工程砍掉了usDelay文件,这是第一个需要检查的点。

2.2 GPIO方向与复用配置:同一根线在输出与输入之间切换

DHT11只有一根数据线,主机要先输出低电平发起始信号,然后立刻把引脚切回输入态去读DHT11的应答和数据。所以GPIO方向寄存器GPADIR会在一次读取流程里切换两次,这和驱动普通输出设备很不一样。下面是DHT11接到GPIO12时的初始化代码。

#define DHT11_GPIO 12 // DHT11数据线接在GPIO12 #define DHT11_OUT() GpioCtrlRegs.GPADIR.bit.GPIO12 = 1; // 方向切到输出 #define DHT11_IN() GpioCtrlRegs.GPADIR.bit.GPIO12 = 0; // 方向切到输入 void Dht11GpioInit(void) { EALLOW; // 把GPIO12复用功能清成0,变成普通GPIO口 GpioCtrlRegs.GPAMUX1.bit.GPIO12 = 0; // 初始化为输入,依靠外部上拉保持高电平 GpioCtrlRegs.GPADIR.bit.GPIO12 = 0; // 0=使能内部上拉;如果模块自带4.7k上拉,可以置1关闭 GpioCtrlRegs.GPAPUD.bit.GPIO12 = 0; EDIS; } void Dht11WriteLow(void) { DHT11_OUT(); GpioDataRegs.GPADAT.bit.GPIO12 = 0; } void Dht11WriteHigh(void) { DHT11_OUT(); GpioDataRegs.GPADAT.bit.GPIO12 = 1; }

GPAMUX1的每一位对应一组2bit复用选择,GPIO12对应第24和25位,写成0才是普通GPIO。这里最危险的操作是习惯性写“GPAMUX1.all = 0”去整段清空,因为GPIO28和GPIO29的SCI功能也被一并清掉了,后面UART发不出任何数据。我一般用位域操作,只改当前引脚对应的位。GPAPUD寄存器内部上拉的默认状态是关闭,但对单总线这种需要强上拉的场景,28335内部上拉阻值偏大,只能兜底,模块不带上拉电阻的话仍然建议外接一个4.7k到3.3V。

初次调试时,建议把DHT11的所有操作都放在一个.c文件里,方向切换用宏而不是函数,这样生成的指令更少,时序更可控。

2.3 us级延时:用指令周期算,不要靠肉眼调循环

DHT11对时序的要求是微秒级:起始信号低电平18us以下会被拒绝,数据位判断窗口也就在几十微秒内。28335的CPU主频150MHz,一个指令周期约6.67ns,1us大约对应150条指令。但不能直接用C语言的for循环去估算,因为编译优化等级一变,循环体生成指令数就变了。

最稳妥的写法是用官方库自带的DELAY_US,它在DSP2833x_usDelay.asm里实现,参数单位是微秒。

#include "DSP2833x_Examples.h" DELAY_US(20); // 延时20us DELAY_US(30); // 延时30us

这段代码的底层是用汇编代码扣周期实现的,和C编译器优化无关,误差通常在几百ns以内,足够DHT11使用。前提是你的工程里加入了DSP2833x_usDelay.asm并且编译链接成功。如果你确实不想依赖官方库,也可以自己写一个带校准的经验延时,但必须用volatile变量防止被优化掉。

volatile Uint32 loopCount; void DelayUs(Uint32 us) { loopCount = us * 30; // 30是当前编译选项下的包裹系数 while (loopCount--); }

参数说明:系数30是经验值,表示大致每个循环贡献约3条指令、加跳转共4条指令左右。换一个优化等级或换一版编译器,这个系数就要重新测。校准方式很简单:让GPIO翻转输出,先拉高再调用DelayUs(100),用示波器看高电平宽度,把系数调整到脉宽为100us。没有示波器的人往往在这里翻车,建议直接把31进去,因为循环体实际编译结果是三条指令加一条跳转,30这个值是按4条指令周期估算的,基本差不了太多。

void DebugPulse(void) { DHT11_OUT(); GpioDataRegs.GPADAT.bit.GPIO12 = 1; DelayUs(100); // 先按100us调系数 GpioDataRegs.GPADAT.bit.GPIO12 = 0; DelayUs(100); }

这个调试函数可以在main里循环执行,停在示波器上看高电平所占宽度。如果宽度是200us,说明系数偏大一半,直接改系数重来。这个方法让我避免了至少三次“传感器换了个寂寞”的尴尬。

3. 读写DHT11时序:起始信号、40bit采样窗口与校验

3.1 起始信号:18us低电平、30us高电平、80us应答

DHT11的读取过程可以分成四段:主机拉低总线、主机拉高总线、等待传感器低电平应答、接收40bit数据。先看前两段和应答。

Uint16 Dht11Start(void) { Uint16 waitCnt = 100; // 超时计数,防止卡死在while里 Dht11WriteLow(); DELAY_US(20); // 低电平至少18us,给20us余量 Dht11WriteHigh(); DELAY_US(30); // 高电平20-40us,取中间值30us DHT11_IN(); // 切回输入,释放总线等应答 // 等待DHT11把总线拉低,开始应答信号 while (GpioDataRegs.GPADAT.bit.GPIO12 == 1) { if (--waitCnt == 0) return 1; // 超时,传感器未接入或线路断开 } // 应答低电平大约80us,等它结束 waitCnt = 1000; while (GpioDataRegs.GPADAT.bit.GPIO12 == 0) { if (--waitCnt == 0) return 1; // 超时,默认传感器不响应 } return 0; }

代码里的waitCnt不是精确计时器,只是防止程序卡死的保险。第一次调这个函数时最容易犯的错误是把低电平延时写成2ms,DHT11不会响应,然后怀疑传感器坏了。数据手册上清清楚楚写着低电平必须是18us以上、30us以内,结尾别超过这个范围。高电平持续时间20到40us,取30us是因为在中间值上测量误差最小。如果这个函数总是返回1,先用示波器看GPIO12上有没有那串应答电平,没有的话再看是不是总线上少了上拉电阻。

DHT11应答之后会输出一个80us的低电平,紧接着一个80us的高电平,这段高电平结束之后才开始逐位发数据。代码中第二个while就是在等低电平结束并进入高电平阶段。很多实现漏掉第二个while,直接去采样,导致前几个bit都是错的。

3.2 40bit数据采样:为什么采样窗口选在40us

DHT11的每bit数据以50us低电平开始,然后是高电平。如果高电平持续约26到28us,表示数据位0;如果高电平持续约70us,表示数据位1。所以不能在高电平一开始就采样,而要在高电平持续到一定宽度后再判断。

Uint16 dht11_data[5]; // 湿整、湿小、温整、温小、校验 void Dht11Read(void) { Uint16 i, bit; Uint16 byte = 0; Uint16 waitCnt; if (Dht11Start()) return; // 起始失败,不更新数据 for (i = 0; i < 5; i++) { byte = 0; for (bit = 0; bit < 8; bit++) { // 等待50us低电平结束 waitCnt = 5000; while (GpioDataRegs.GPADAT.bit.GPIO12 == 0) { if (--waitCnt == 0) return; } // 延时40us后再采样 DELAY_US(40); // 此时仍为高,说明70us的高电平,数据位为1 byte <<= 1; if (GpioDataRegs.GPADAT.bit.GPIO12 == 1) { byte |= 0x01; } // 等待当前高电平结束,为下一位的50us低电平做准备 waitCnt = 5000; while (GpioDataRegs.GPADAT.bit.GPIO12 == 1) { if (--waitCnt == 0) return; } } dht11_data[i] = byte; } }

关键参数是那个40us的采样窗口。为什么不能是30us?因为0的数据位高电平在26到28us之间,加上线缆电容和传感器个体差异,可能在28us以后再被拉低;30us采样时有概率把0误判成1。为什么不能是60us?1的数据位高电平约70us,60us时还在高电平,采样没问题;但0的数据位已经在30us左右结束了,60us时采到的是低电平。所以40us在中间,留足了噪声容限。第一个while等低电平结束,第三个while等当前bit高电平结束,整体流程看起来啰嗦,但缺了任何一个等待阶段,位与位之间就会串位。

数据位发送顺序是最高位在前,所以代码里byte左移再或上1就是正确位序。这四字节原始数据是:湿度整数8位、湿度小数8位、温度整数8位、温度小数8位。DHT11的湿度小数位通常输出0,温度小数位则是0或0.5这样的值。

3.3 校验和、温湿度换算与数据异常时的处理

40bit数据里第5字节是校验和,规定为前4字节累加后的低8位。接收到完整5字节后,先做校验再更新温度值,校验失败直接丢弃本次结果,保留上一次的有效数据。

Uint16 humidity = 0; Uint16 humidity_dec = 0; Uint16 temperature = 0; Uint16 temp_dec = 0; Uint16 Dht11DataValid = 0; void Dht11Update(void) { Uint16 checksum; Dht11Read(); // 填充dht11_data[5] checksum = (dht11_data[0] + dht11_data[1] + dht11_data[2] + dht11_data[3]) & 0xFF; if (checksum != dht11_data[4]) { Dht11DataValid = 0; return; // 校验失败,旧数据继续有效 } humidity = dht11_data[0]; humidity_dec = dht11_data[1]; temperature = dht11_data[2]; temp_dec = dht11_data[3]; // 温度位7是符号位,置1表示零下 if (temperature & 0x80) { temperature = 0 - (temperature & 0x7F); } Dht11DataValid = 1; }

温湿度换算上,DHT11的整数部分直接就是百分数摄氏度的整数位,小数位是0或0.5,不需要复杂换算公式。温度部分特判一下符号位,27315那样的大改造在这里用不上。校验失败时不要反复连续重读传感器,DHT11的采样周期是2秒,连续读只会加剧应答失败。我在项目里一般做法是:主循环每2秒调用一次Dht11Update,如果Dht11DataValid为0,UART回传错误帧而不回传一个已经失效的假温度。

4. UART回传与排错:SCI-A配置、数据帧与常见踩坑

4.1 SCI-A初始化:GPAMUX复用与BRR计算

28335上有三个SCI模块,最常用的是SCI-A,对应引脚是GPIO28的SCITXDA和GPIO29的SCIRXDA。先确认这两个引脚的复用状态为SCI功能,再算波特率寄存器。

#define LSPCLK_FREQ 37500000L // 默认LSPCLK = SYSCLKOUT/4 = 37.5MHz void SciAInit(Uint32 baud) { Uint16 brr; EALLOW; SysCtrlRegs.PCLKCR0.bit.SCIENCLKA = 1; // SCI-A外设时钟使能 GpioCtrlRegs.GPAMUX1.bit.GPIO28 = 1; // GPIO28 -> SCITXDA GpioCtrlRegs.GPAMUX1.bit.GPIO29 = 1; // GPIO29 -> SCIRXDA EDIS; SciaRegs.SCICCR.all = 0x0007; // 8位数据、无校验、1停止位 SciaRegs.SCICTL1.all = 0x0003; // 使能TX和RX SciaRegs.SCICTL2.all = 0x0003; // 使能TX/RX中断,本例只用轮询可写0 brr = (Uint16)(LSPCLK_FREQ / (baud * 8) - 1); SciaRegs.SCIHBAUD.all = (brr >> 8) & 0xFF; SciaRegs.SCILBAUD.all = brr & 0xFF; SciaRegs.SCICTL1.bit.SWRESET = 1; // 释放SCI复位,最后一步 }

波特率公式是BRR = LSPCLK / (8 × 波特率) - 1,LSPCLK默认取37.5MHz。如果你的工程改了LOSPCP分频,这里就不能用37.5MHz算,否则串口收到的全是乱码。115200波特率代入后BRR约等于39,四舍五入到最近整数后实际误差约0.2%,在RS232的容差范围内。

注意SCICTL1寄存器千万不能最后再整体赋值,SWRESET必须放在其它配置之后单独置1。很多新手把SCICTL1.all = 0x0003放在最后,等于直接把SWRESET清掉了,结果是SCI一直处于复位状态,寄存器怎么配都不工作。这是一个不报错、不提示,纯粹靠串口没数据来暴露的问题。

4.2 数据帧打包:二进制帧头还是ASCII文本

UART回传有两种常见做法。第一种是二进制帧,帧头加上数据加上校验,适合机器解析;第二种是ASCII字符串,适合用串口助手直接看。我一开始用二进制帧,调试时还要在电脑端自己写解析脚本,后面改成字符串,肉眼一眼就能判断温度值是否合理。

void SciSendByte(Uint16 ch) { // 等待发送缓冲为空 while (SciaRegs.SCICTL2.bit.TXRDY == 0) { ; } SciaRegs.SCITXBUF.all = ch & 0xFF; } void SciSendString(char *str) { while (*str) { SciSendByte((Uint16)(*str)); str++; } }

TXRDY是SCICTL2的bit7,表示发送数据缓冲准备好,可以写入下一个字符。这是一个轮询发送方式,对DHT11这种2秒才更新一次的数据量绰绰有余。发送一个整型温湿度数值,不能用printf直接接,因为28335的printf重定向在裸机工程里需要另外处理;我一般写一个简单数字转字符串函数,把湿度整数和温度整数转成三个字符组拼进一个缓冲区,再调用SciSendString发送。

char txBuf[32]; void Dht11SendFrame(void) { if (Dht11DataValid == 0) { SciSendString("ERR: dht11 timeout\n"); return; } txBuf[0] = 'H'; txBuf[1] = ':'; // 湿度值转3位十进制字符,省略转换细节 txBuf[5] = '%'; txBuf[6] = ' '; txBuf[7] = 'T'; txBuf[8] = ':'; // 温度值转3位十进制字符 txBuf[12] = 'C'; txBuf[13] = '\r'; txBuf[14] = '\n'; txBuf[15] = 0; SciSendString(txBuf); }

发送失败时回传英文错误串而不是直接静默,这对现场调试非常重要:你能通过串口助手清楚知道传感器是没接、超时还是校验失败,而不是被一个不变的温度值骗了好几天。

4.3 三个典型坑:现象、原因、解决

先说我踩过最隐蔽的一个坑:GPAMUX整段清零误伤SCI。现象是DHT11温度读得很正常,GPIO翻转也对,但串口助手什么也不显示。原因是在Dht11GpioInit里写了GpioCtrlRegs.GPAMUX1.all = 0,把GPIO28、GPIO29复用的SCI功能一并清掉了。解决方法是只操作DHT11引脚对应的GPAMUX1位域,或者把SCI初始化放在DHT11 GPIO初始化之后执行,让SCI的复用配置覆盖回来。

第二个坑:GPIO输入时钟没开导致GPADAT读数恒定。现象是DHT11引脚有明确的高低电平变化,逻辑分析仪能抓到波形,但代码里读到的高电平次数很少,校验总是不通过。原因就是前面第2.1节提到的PCLKCR3.GPIOINENCLK没有置1,输入同步时钟没工作。解决方案就是加上那行寄存器配置。

第三个坑:DHT11连续读导致应答超时。现象是首帧数据正常,后续偶尔出现全0xFF或校验失败。原因有两个层面:一是DHT11上电后1秒内不允许操作;二是两次采样间隔必须不小于2秒,手册明确规定典型采样周期是2秒。解决方案是main函数流程里上电先DELAY_US再延时1秒,随后主循环用2秒作为采样周期,不给传感器施加额外压力。

5. 不接逻辑分析仪怎么验证时序:把采样轨迹从UART回传出去

调试DHT11这类传感器最痛苦的地方是时序看不见摸不着。没有逻辑分析仪时,我习惯在代码里加一个调试模式,把每次采样的中间结果通过UART回传,直接在串口助手里判断时序是否正常。这个方法没有额外成本,比盲调玄学高效得多。

Uint16 dbg_trace[40]; // 记录每个bit的采样状态 // 0=正常读到0 // 1=在40us窗口读到1 // 2=等待低电平超时 // 3=等待高电平超时 void Dht11ReadDebug(void) { Uint16 i, bit; Uint16 state; for (i = 0; i < 5; i++) { for (bit = 0; bit < 8; bit++) { state = Dht11ReadOneBit(); dbg_trace[i * 8 + bit] = state; } } SciSendByte(0x42); // 调试帧头 for (i = 0; i < 40; i++) { SciSendByte(dbg_trace[i]); } }

收到40字节轨迹后,正常情况应该是前面32位里大部分是0状态,1状态集中出现在温度值比较大的字节上;如果等待低电平超时的状态值特别多,先检查上拉电阻和线长;如果等待高电平超时多,那就是采样窗口太靠前,把延时从40us往小调到30us去匹配传感器实际波形。上位机端用串口助手的HEX显示模式,看两个相邻字节的值就能大致判断时序问题出在哪一段。

这个验证方法帮我定位过至少两次疑难问题,一次是传感器引线过长导致上升沿变缓,一次是模块上电容太大把高电平脉宽拉宽,采样窗口和时序值都不匹配,靠轨迹图才一眼看出问题所在。如果你手里有一台普通示波器,也可以把GPIO12换成另一个空闲引脚做翻转标记,这样波形上和DHT11信号完全对齐,直接量出脉宽再回填参数。

我现在接任何单总线传感器,第一件事就是把调试回传的代码写进去,先看到原始轨迹再调业务逻辑,这比对着协议盲改参数省下几个通宵。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询