DSP28335驱动DHT11并UART串口回传:GPIO模拟时序的工程实践 简介基于DSP28335的DHT11温湿度读取与UART回传工程面向使用TI TMS320F28335DSP28335进行嵌入式开发的工程师与学习者解决DHT11单总线时序模拟、传感器数据解析以及UART串口回传等典型问题。压缩包为zip格式共95个文件约309KB内容涵盖DSP2833x标准库源码、CCS工程配置文件含.ccxml、.cproject、.launch、.cmd链接脚本、用户主程序main_dht11.c及头文件。文件类型以c源文件、h头文件、asm汇编、mk编译脚本、obj目标文件、out可执行文件、map映射文件为主并附有readme.txt说明整体目录结构清晰可直接导入CCS环境编译调试。已有880人浏览学习。资料中不仅包含完整可编译的DSP工程还展示了DHT11复位与应答时序、数据位读取、校验处理、UART波特率与帧格式配置等关键代码并涉及GPIO引脚配置、DHT11总线时序控制与UART发送缓冲处理适合作为入门DSP外设驱动、传感器采集及串口通信的参考范例。1. DSP28335 接 DHT11 再走 UART 回传为什么值得折腾一遍如果你调过 DHT11大概率见过串口里疯狂打印 0xFF 或校验失败的场景。DHT11 是单总线器件数据线只有一根时序全靠高低电平的宽度来区分 0 和 1而 DSP28335 的 SCI 串口模块没法直接解析这种协议必须用 GPIO 手动拉电平去模拟时序。整套链路做下来等于把 GPIO 输入输出方向切换、微秒级延时、SCI 收发、校验解析全部过了一遍刚好是嵌入式里最常见的几块硬骨头。这份资源不是那种跑一下点个灯的玩具而是一个能拿去实际采集温湿度并回传给上位机的完整基础工程尤其适合刚入门 DSP28335、想把外设驱动真正跑通的开发者也适合给 SCI 通信做参考模板。2. 硬件对接与初始化GPIO 选型、上拉电阻和方向切换的三个隐藏细节2.1 DHT11 为什么必须用 GPIO 模拟单总线SCI 直连行不行DHT11 的数据口是单线开漏结构空闲时为高电平主机要发起始信号时把总线拉低再释放然后 DHT11 会主动拉低响应信号之后每一位数据都是低电平 50us 高电平 26~28us 或 70us的组合。这种时序不是标准的 UART 帧没有起始位、数据位、停止位的固定格式SCI 模块收到的是连续的跳变沿无法正确识别出 40 位数据。所以只能把 GPIO 配成普通 IO用软件延时一点一点地去量电平宽度。常见的错误想法是接一个比较器把信号整形后当成串口数据来收实测完全行不通。DHT11 的每一位数据之间的低电平间隔并不固定而且整个数据帧前没有同步头SCI 根本找不到帧边界。选 GPIO 引脚时要注意 DSP28335 的引脚复用GPIO0 到 GPIO31 属于 GPIOA 组默认很多引脚是外设功能比如 GPIO28~GPIO31 可能被 SCI-B 占用。我一般选 GPIO0 作为 DHT11 数据脚因为它默认就是通用 IO而且和 SCI-A 的收发引脚不冲突方便后期测试时直接看波形。2.2 硬件连接与电平匹配上拉电阻不是玄学是时序的命根子DHT11 供电范围是 3.3V~5.5VDSP28335 的 GPIO 是 3.3V 电平所以直接统一用 3.3V 供电就行不需要电平转换。数据线必须接一个 4.7kΩ 到 10kΩ 的上拉电阻到 VCC因为 DHT11 的输出是开漏结构没有上拉电阻的话高电平输出会被拉成浮空状态GPIO 读回来的电平就是乱的。我踩过的坑是用了 1kΩ 上拉电阻导致低电平被拉不高DHT11 的响应信号和 0/1 区分都变得模糊。DHT11 的时序规格书上高电平宽度只有 26~70us上拉电阻太小会延长上升沿时间采样点稍微偏一点就会把 0 读成 1。4.7kΩ 是稳妥值。硬件连接也有一点要注意DHT11 的信号线尽量短超过 20cm 就建议加一个 100nF 的滤波电容在 DHT11 的 VCC 和 GND 之间否则长线上引入的干扰会让校验和的出错率明显上升。这个电容不影响时序只滤电源噪声。2.3 GPIO 复用配置与初始化代码方向切换后必须等一拍初始化代码要做的三件事把 GPIO0 配成通用 IO、方向设为输出、默认输出高电平。这里有个细节DSP28335 的寄存器写入到引脚状态真正生效有几个周期的延迟方向切换后立刻去读引脚电平大概率读到的是切换前的状态。所以我一般在 GPIO 方向切换后加 5us 左右的延时。void DHT11_GPIO_Init(void) { EALLOW; // GPIO0 配置为普通 IO 功能关闭复用 GpioCtrlRegs.GPAMUX1.bit.GPIO0 0; // 0GPIO, 1外设 // GPIO0 方向设为输出 GpioCtrlRegs.GPADIR.bit.GPIO0 1; // 1输出, 0输入 // 输出默认高电平保持总线空闲 GpioDataRegs.GPADAT.bit.GPIO0 1; EDIS; DELAY_US(10); // 等待引脚状态稳定 }这段初始化里最关键的是GPAMUX1的配置很多初学者漏掉这一步导致 GPIO0 被复用成别的外设功能引脚上怎么拉电平都不起作用。GPADIR决定了方向DHT11 读取过程中需要频繁在输入和输出之间切换后面每个切换点都要记得加延时。另外如果你的板子上电后默认开启了看门狗初始化阶段要先把看门狗处理好否则整个 DHT11 读取流程跑不完就会触发复位。具体现象和处理方式放到第 5 章避坑部分详细说。3. 时序读取实现起始信号、40 位数据采样和校验解析3.1 DHT11 时序拆解每一个电平的时长边界必须记清楚DHT11 一次完整的数据传输分三个阶段。主机先拉低总线至少 18ms这是起始信号然后释放总线并延时 20~40us等待 DHT11 响应。DHT11 收到起始信号后会把总线拉低 80us再释放拉高 80us表示我准备好了。之后连续发送 40 位数据每一位都是先低电平 50us然后高电平 26~28us 表示逻辑 0高电平 70us 表示逻辑 1。40 位数据的排列是8 位湿度整数 8 位湿度小数 8 位温度整数 8 位温度小数 8 位校验和。这里有个坑DHT11 的小数位实际上固定返回 0所以有效数据只有整数部分别把小数位当成精度去看。校验和的规则是前四个字节相加取低 8 位如果等于校验字节则数据有效。这个校验必须在代码里做否则一旦线路受干扰串口会打印出明显错误的温度值比如 55 度的室内温度而你完全不知道是哪一位跳错了。3.2 读取函数实现起始信号、逐 bit 采样与超时保护读取的核心难点在采样点。我用的方法是检测到低电平结束上升沿后延时 40us 再去读电平。如果此时是高电平说明高电平持续超过 40us接近 70us判定为逻辑 1如果此时已经变低说明高电平只持续了 26~28us判定为逻辑 0。这个采样点位于两种电平宽度的中间位置容错性最好。// 读取一个 bit返回 0 或 1 unsigned char DHT11_ReadBit(void) { unsigned char ret 0; // 等待低电平开始 while (DHT11_DATA_IN() 1); // 等待总线被拉低 // 等待低电平结束上升沿出现 while (DHT11_DATA_IN() 0); // 延时 40us避开跳变沿取电平稳定的中间时刻 DELAY_US(40); if (DHT11_DATA_IN() 1) // 高电平仍持续 - 70us - 逻辑1 { ret 1; } // 等待高电平结束准备下一个 bit while (DHT11_DATA_IN() 1); return ret; } // 读取一个字节由 8 个 bit 拼装 unsigned char DHT11_ReadByte(void) { unsigned char byte 0; for (int i 0; i 8; i) { byte 1; byte | DHT11_ReadBit(); } return byte; }DHT11_DATA_IN()是我封装的一个读取宏内部实现是先把 GPIO0 方向切成输入再读GPADAT这个宏里必须带上方向切换和短暂延时。由于程序中要频繁调这个宏DELAY_US(40) 的精度就直接影响识别成功率。DELAY_US是官方库的软件延时在 150MHz 主频下实际延时比标称值略大但 40us 这个采样点离 0/1 的判决边界还有 20us 余量所以实测是稳定的。起始信号的实现在DHT11_Start函数里完成代码如下。注意拉低时间不能小于 18ms我习惯拉到 20ms留足余量。void DHT11_Start(void) { // 切到输出准备拉低总线 DHT11_DATA_OUT(); GpioDataRegs.GPADAT.bit.GPIO0 0; // 拉低总线 DELAY_US(20000); // 保持低电平 20ms触发起始条件 GpioDataRegs.GPADAT.bit.GPIO0 1; // 释放总线 DELAY_US(30); // 等待 DHT11 响应约 20~40us // 切回输入准备接收响应和数据 DHT11_DATA_IN(); }DHT11_DATA_OUT()和DHT11_DATA_IN()内部除了改GPADIR方向位后面都跟了一个 DELAY_US(5)这一步不能省。方向切换后引脚状态不是立即生效的特别是从输出切到输入时如果没有延时读回来的电平可能是输出状态下残留的值会直接导致第一个 bit 采样错误。3.3 完整数据帧解析与校验拿不到有效数据的兜底逻辑主读取函数把起始、40 位读取、校验串起来并且做了超时保护。DHT11 有时候会不响应比如上电还没稳定或者读取间隔太短此时DHT11_ReadBit里的 while 循环会卡死。解决方案是为每个等待循环加一个计数上限超时直接返回失败。unsigned char DHT11_ReadData(int *temp, int *humi) { unsigned char buf[5] {0, 0, 0, 0, 0}; int i, timeout; DHT11_Start(); // 等待响应信号先低 80us再高 80us timeout 1000; while (DHT11_DATA_IN() 1) { if (--timeout 0) return 1; // 无响应 } timeout 1000; while (DHT11_DATA_IN() 0) { if (--timeout 0) return 2; // 响应信号异常 } for (i 0; i 5; i) { buf[i] DHT11_ReadByte(); } // 校验前 4 字节相加低 8 位等于第 5 字节 if ((unsigned char)(buf[0] buf[1] buf[2] buf[3]) ! buf[4]) { return 3; // 校验失败 } *humi buf[0]; // 湿度整数位 *temp buf[2]; // 温度整数位 return 0; }返回值 1、2、3 分别对应无响应、响应异常、校验失败这个分类在调试时很好用。我在实际调试中见过不少情况是返回值在 1 和 3 之间来回跳这种问题多半不是代码逻辑而是总线时序被干扰优先检查上拉电阻和供电稳定性。DHT11 读取完成后总线回到空闲高电平状态此时 GPIO0 保持在输入方向即可不需要切回输出。下一次读取前DHT11_Start会自动切到输出。4. UART 回传配置SCI 模块初始化、波特率计算和主循环整合4.1 SCI-A 初始化与波特率寄存器计算高位低位别写反DSP28335 的 UART 模块叫 SCISCI-A 的收发引脚默认是 GPIO28SCIRXDA和 GPIO29SCITXDA。这两个引脚同样需要配置成外设功能不能留在 GPIO 模式。初始化分四步配置帧格式、使能收发、设置波特率、软件复位释放。void SCI_Init(void) { EALLOW; // GPIO28 - SCI-RX, GPIO29 - SCI-TX GpioCtrlRegs.GPAMUX1.bit.GPIO28 1; GpioCtrlRegs.GPAMUX1.bit.GPIO29 1; EDIS; // 8 位数据位无校验1 停止位 SciaRegs.SCICCR.all 0x0007; // 使能 TX 和 RX SciaRegs.SCICTL1.all 0x0003; // 波特率 LSPCLK / (8 * BRR) - 1 // LSPCLK 默认 37.5MHz150MHz 主频 / 4 // 目标 9600: BRR 37.5e6 / (8 * 9600) - 1 487.25 - 487 SciaRegs.SCIHBAUD.all 487 8; // 高字节 SciaRegs.SCILBAUD.all 487 0xFF; // 低字节 // 软件复位释放使配置生效 SciaRegs.SCICTL1.all | 0x0002; }波特率寄存器是 16 位的拆成高字节和低字节两个 8 位寄存器写入顺序无所谓但SCIHBAUD和SCILBAUD千万不要写反。487 的高字节是 1低字节是 231如果写反了实际波特率会变成 37.5e6 / (8 * (2318 | 1)) - 1算出来约等于 31bps串口助手根本收不到任何东西。算波特率时注意 LSPCLK 的值。默认情况下 LSPCLK 是 CPU 时钟的四分之一也就是 37.5MHz。如果有人在InitPeripheralClocks里改过分频这个值会变波特率公式算出来的寄存器值也就不同。我用 9600 波特率是为了兼容性和稳定性如果你要用 115200BRR 算出来是 39.7取 40实际波特率 116250误差 0.9%没问题。4.2 数据打包发送与主循环整合2 秒读一次别让 DHT11 罢工发送逻辑比较简单SCI 的发送缓冲寄存器是SCITXBUF写之前要等SCICTL2.bit.TXRDY变 1表示上一字节已发送完成。我封装了一个发送字符串函数。void UART_SendString(char *str) { while (*str) { // 等待发送缓冲为空 while (SciaRegs.SCICTL2.bit.TXRDY 0); SciaRegs.SCITXBUF *str; } } void UART_SendInt(int val) { char buf[8]; // 整数转字符串负号要单独处理 if (val 0) { UART_SendString(-); val -val; } int len 0; if (val 0) buf[len] 0; while (val 0) { buf[len] 0 (val % 10); val / 10; } while (len 0) UART_SendChar(buf[--len]); }主循环里我加了 2 秒的延时这是 DHT11 规格书的要求连续读取间隔必须大于 1~2 秒否则传感器不会响应。如果你串口打印里频繁出现无响应先检查主循环延时是不是被删了。void main(void) { int temp, humi; unsigned char ret; // 初始化系统时钟、看门狗等 InitSysCtrl(); DHT11_GPIO_Init(); SCI_Init(); // 上电后等 1 秒让 DHT11 内部完成初始化 DELAY_US(1000000); for (;;) { ret DHT11_ReadData(temp, humi); if (ret 0) { UART_SendString(TEMP:); UART_SendInt(temp); UART_SendString( HUMI:); UART_SendInt(humi); UART_SendString(\r\n); } else { UART_SendString(ERR:); UART_SendInt((int)ret); UART_SendString(\r\n); } DELAY_US(2000000); // 2 秒后再读下一次 } }这里DELAY_US(2000000)是阻塞延时整个主循环被卡在延时里后续如果还想做按键扫描或者显示刷新就得改成状态机。这个优化思路放在最后一章展开。串口输出格式设计成TEMP:26 HUMI:54这种纯文本直接用串口助手就能看不需要再做上位机解析。5. 避坑复盘DHT11 在 DSP28335 上的五个高频翻车点和处理办法5.1 上电后第一次读取必失败返回 1无响应现象程序烧录后复位运行串口打印的第一个结果一定是 ERR:1之后的数据恢复正常。原因DHT11 上电后有 1 秒左右的内部初始化时间在此期间不响应任何起始信号。如果主循环在初始化后立即调用读取函数DHT11 还没准备好。解决在主循环开始前加一个 DELAY_US(1000000) 的阻塞延时或者做一个前三次读取失败自动重试的逻辑。我两种方法都试过延时最省事重试逻辑更健壮。5.2 串口刷出连续的 0xFF并且 ERR 码在 1 和 3 之间跳现象串口输出的数据要么全 0xFF要么一直报校验失败而且 GPIO0 引脚用示波器看波形是乱的高电平只有 1V 左右。原因数据线上拉电阻没接或者上拉电阻阻值选太大比如 100kΩ。DHT11 开漏输出在高电平时靠上拉电阻提供电平阻值过大导致高电平上升缓慢低电平到高电平的跳变沿变得很钝DHT11 响应阶段的高电平 80us 无法被正确识别所有 bit 都会被读成 0。解决换成 4.7kΩ 上拉电阻重新上电后波形立刻恢复。这个坑的排查思路是用示波器量电平幅值而不是去改代码。5.3 连续读取两次数据第二次必失败现象如果主循环延时被改成 500ms串口里会出现第一次成功、第二次 ERR:1、第三次成功的规律。原因DHT11 规格书要求两次读取间隔不小于 2 秒快于这个频率时传感器会直接不响应。这不是代码问题是器件的物理限制。解决把读取间隔固定在 2 秒以上。如果你需要更高采样率直接换 DHT22 或 SHT3xDHT11 的采样率天花板就是 0.5Hz 左右。5.4 波特率看起来没错串口助手收到的却是乱码现象代码里配的 9600串口助手也选的 9600但收到的字符是乱码比如 TEMP 显示成乱糟糟的符号。原因SCIHBAUD和SCILBAUD的高低字节赋值写反了或者 LSPCLK 被改过导致波特率算错。我调试时遇到的另一种可能是个别 USB 转串口模块的晶振精度太差9600 波特率下误差被放大。解决先用串口助手发一个固定字符回显测试。可以让 DSP28335 上电后无条件发送字符串 OK\r\n如果收发正常说明 SCI 配置没问题再排查 DHT11 部分。如果 OK 都乱码用示波器量 TX 引脚的电平宽度对比 9600 下 1 bit 应该是 104us偏差超过 10% 就说明波特率配置有问题。5.5 程序每隔几秒自动复位串口重新打印初始化信息现象板子运行过程中串口莫名其妙重新输出上电信息现象像是程序重启了。原因DSP28335 的看门狗被意外使能而主循环里全是阻塞延时长时间没喂狗导致看门狗溢出复位。DHT11 读取一次要 20ms 左右主循环再有 2 秒延时喂狗语句放在循环末尾的话间隔最长达到 2 秒以上超过了看门狗默认超时窗口。解决在主循环开头喂一次狗或者直接关闭看门狗。我的习惯是开发阶段直接关掉看门狗产品化之前再统一处理喂狗策略。关看门狗的代码是WdRegs.WDCR.all 0x0068;放在系统初始化之后。如果不想关就在主循环里保证每 500ms 内至少喂一次狗。6. 进阶验证串口数据可靠性的三个检查方法以及把阻塞延时改成读取状态机资源下载下来之后第一件事不是直接用而是验证工程本身的质量。我把串口收到的数据连续采 10 分钟正常现象是温度在小范围波动、湿度不突变且 ERR 输出占比低于 5%。如果 ERR 频繁大概率是硬件问题而不是代码问题。另一个验证方法是抓波形用逻辑分析仪看 GPIO0 在起始信号后的电平跳变DHT11 响应阶段的 80us 低电平 80us 高电平必须清晰可见如果上升沿超过 10us说明上拉电阻偏大。进阶改进方向是去掉主循环里的DELAY_US(2000000)改成状态机驱动。因为阻塞延时期间 CPU 做不了任何事后续要接 LCD 显示或按键扫描就卡死了。我的做法是维护一个 2 秒的计数器DHT11_ReadData只在计数器归零后调用读取完成后立刻切回其他任务。这样 DHT11 的单总线时序仍然由阻塞延时保证但整个系统不再被绑死在等待上。数据可靠性方面还可以加一个简单的滤波。DHT11 的温度分辨率只有 1 度如果连续两次读取的差值超过 2 度大概率是某次传输出错直接丢弃本次结果保留上次值。这个逻辑一度治好了我的数据跳变焦虑比反复调上拉电阻实用得多。从那以后我每次拿到类似单总线传感器都强制走一遍完整流程先看示波器波形再检查校验和最后才碰代码逻辑。这个顺序帮我省掉了大量无效调试。希望帮到你。本文还有配套的精品资源点击获取