简介:一份围绕MSP430G2553驱动DHT11温湿度传感器并完成12864液晶显示的嵌入式项目文档,面向正在学习单片机外设驱动、单总线通信与LCD显示的学生和电子设计初学者,可用于课程设计、实验参考或项目复盘。文档以50KB的单个doc文件交付,内容集中呈现主控初始化、DHT11启动与40位数据接收、定时器脉宽解码、温湿度校验及12864汉字与数字显示等实现思路,并涉及微秒与毫秒延时、显示格式取舍。已有962人学习,说明其在低功耗MCU入门实践中具有一定参考价值。文档结构紧凑,围绕初始化、接收、显示与校验等环节展开,便于快速定位关键实现。读者可据此理解MSP430的I/O操作、单总线时序判断、校验和排错方法,以及将传感器数据落到液晶屏的完整组织方式,适合按知识点查阅并对照自己的实验代码查漏补缺。
1. 从一根数据线开始:为什么这个温湿度采集方案值得做一遍
DHT11 只有一根数据线,看起来是入门级器件,但真正把它接到 MSP430G2553 上、再把数据稳定显示到 12864 液晶上,翻车点比想象中多:时序抖动几十微秒就读回全 0,校验和不过就整屏不刷新,液晶串行时序差一点就花屏。这套方案要解决的就是“低成本温湿度采集终端”的完整链路问题——单总线时序解码、数据校验、液晶显示分层,一条链走通。它适合正在做嵌入式课设、毕设的学生,也适合需要快速搭一个环境监测原型的从业者。整套代码只有两个源文件加一个头文件,但结构上的取舍值得逐层拆开讲。
2. 需求与技术选型:三段链路和三个不能省的约束
先把需求摆清楚,选型才有依据。这个项目的功能目标很朴素:周期性地读取环境温度与湿度,做数据有效性判断,把结果连同单位提示一起显示在液晶上。但看似简单的三件事,对应三条独立的技术链路,任何一条断掉整个系统就是黑匣子。
2.1 三段链路的职责边界
第一段是传感链路:MSP430G2553 作为主机,通过 P2.1 单总线与 DHT11 通信,负责发起起始信号、逐位采样 40 bit 数据、做校验和比对。第二段是显示链路:12864 液晶走串行接口,占用 P1.0 片选、P1.1 数据、P1.2 时钟三根线,负责把汉字提示和两位数值画到指定 DDRAM 地址。第三段是时间基准链路:所有单总线时序都建立在微秒级延时之上,延时准不准直接决定解码对不对。
三段的耦合点很明确——传感链路输出的temph / templ / humdh / humdl / check五个字节,是显示链路的唯一输入;而时间基准是前两段共同的底层依赖。做方案设计时我一般会把这三段画成三条并列的横向泳道,中间只留一个数据交接点,这样后面调 bug 时能立刻定位问题出在哪条泳道上,而不是对着一个不亮的屏幕瞎猜。
器件选型上有几个现实考量。DHT11 的精度是湿度 ±5%RH、温度 ±2℃,分辨率只有 1℃/1%RH,这决定了它不适合做精密测量,但足够做室内环境提示。它出厂已校准、单总线、3.3V 至 5.5V 供电,与 MSP430 的 3.3V 电平天然兼容,不需要电平转换。12864 选串行模式而非并行,是因为 MSP430G2553 的 IO 资源有限,串行只吃三根线,代价是刷新速率和时序容错度都下降,必须靠软件延时兜住。
2.2 时钟与延时的选型账
整个项目最容易被低估的决策是时钟。代码里用DCOCTL = CALDCO_1MHZ把 DCO 校准到 1MHz,然后所有延时都通过__delay_cycles换算:
#define CPU (1000000) #define delay_us(x) (__delay_cycles((double)x*CPU/1000000.0)) #define delay_ms(x) (__delay_cycles((double)x*CPU/1000.0))这里CPU宏必须和实际 DCO 频率严格一致。__delay_cycles是编译器内建函数,参数是 CPU 周期数,如果宏写 1000000 而实际跑在 8MHz,所有延时都会缩到八分之一,DHT11 的 30us 采样窗口直接失效,读回的数据会呈现“湿度正常、温度恒为 0”这类很有迷惑性的现象。反过来,如果为了保险把延时放大,while等待高电平结束的循环会拖长整体帧时间,可能错过下一位的起始沿。
DHT11 的时序窗口实际是很紧的:主机拉低至少 18ms 作为起始信号,然后拉高 20 到 40us 释放总线;传感器响应时先拉低约 80us,再拉高约 80us;此后每一位以 50us 低电平开始,高电平持续 26 到 28us 表示 0,持续 70us 表示 1。代码里用delay_us(30)作为判决阈值,正好卡在 0 和 1 的高电平宽度之间,这个 30us 不是随便取的,它需要大于 28us 且明显小于 70us,留出两边各约 40us 的余量。
提示:DCO 的校准值来自芯片内部 CALDCO_1MHZ 常量,不同批次的 G2553 校准精度有差异,通常能到 ±3% 以内。时序临界场景下,这个偏差足以让 30us 判决阈值偏向某一侧。
选型上还有一个常见误区:想用定时器捕获替代软件延时来解码。理论上更精确,但 DHT11 单总线的位间隔没有标准时钟参考,捕获模式下要频繁进出中断,反而增加抖动风险。这个项目用纯软件延时加轮询,代码短、可控、便于排查,是课设场景下的合理取舍。
3. 总体设计:两个源文件的模块划分与数据流
整套代码只有main.c、12864.c和12864.h,文件少不代表没有架构,恰恰相反,正因为文件少,模块边界必须靠函数职责和头文件约定来守住,否则后期加一个功能就会变成一锅粥。
3.1 分层结构与调用关系
从调用关系看是三层的。最底层是12864.c里的 IO 驱动层:Send负责把一个字节拆成三个字节按位串出,delay_Nus / delay_1ms / delay_Nms提供液晶侧的粗延时。往上一层是显示服务层:Ini_Lcd做初始化序列,Clear_GDRAM清扩展显存,Disp_HZ按地址写汉字点阵,Disp_SZ把两位十进制数拆成字符码写入,Draw_PM和Draw_TX负责整屏图片和 16×16 图形绘制。最上层是应用层main.c:receive_init组织一次完整的采集时序,receive解一个字节,show_tishi / show_Wendu / show_Shidu决定显示什么、显示在哪。
分层的价值在于隔离变化。比如液晶从串行改并行,只需改12864.c的Send和端口宏,应用层一行不动;再比如 DHT11 换成 DHT22,只改receive_init里的字节顺序和校验方式,显示层不受影响。头文件12864.h就是这个边界的契约,它只暴露函数原型,不暴露内部端口宏,调用方看不到cyCS / cySID / cyCLK这些实现细节。
3.2 关键数据与显示地址规划
数据流是这样的:receive_init内依次调用五次receive(),把结果落到全局变量humdh / humdl / temph / templ / check,主循环算cal = humdh + humdl + temph + templ,与check比对,相等才调显示函数。这几个变量是unsigned char,正好容纳 DHT11 的一字节输出。
显示地址的规划需要对着 12864 的 DDRAM 映射来算。这个项目把提示信息固定在首行:0x80起写“温度:”,0x86位置写“°”,0x88起写“湿度:”,0x8e位置写“%”。数值则插在提示中间的空位:温度值写到0x83,小数占位写0x84,湿度值写0x8b,小数占位写0x8c。每个汉字占两个字节地址,所以“温度:”三个字占0x80到0x85,0x86恰好接单位符号,地址计算必须精确到字节,错一位整个版面就串行。
| 显示内容 | 起始地址 | 占用字节 | 数据来源 |
|---|---|---|---|
| 温度提示 | 0x80 | 6 | tishi_W三字点阵 |
| 温度单位 | 0x86 | 2 | tishi_Wd |
| 温度数值 | 0x83 | 2 | temph十位与个位 |
| 湿度提示 | 0x88 | 6 | tishi_S |
| 湿度单位 | 0x8e | 2 | tishi_Sd |
| 湿度数值 | 0x8b | 2 | humdh |
这张表看起来琐碎,但它是排版的唯一依据。我见过不少同类项目把数值地址和提示地址写反,结果屏幕上数字和汉字叠在一起,排查半天以为是液晶坏了。实际做法是先在纸上按 16 字节一行画出地址格,标好每个元素的起止,再回来写Disp_HZ和Disp_SZ的实参,能省掉大量试错。
4. 模块实现:单总线时序、字节解码与液晶写入
实现部分按数据流顺序拆三段:采集时序怎么发起、每一位怎么判 0 和 1、显示怎么落到像素上。每段都给出关键代码和参数依据。
4.1 起始信号与响应握手
receive_init承担一次完整采集的握手和后续五次字节读取:
void receive_init() { P2DIR |= BIT1; // P2.1 切为输出,主机掌握总线 LOW; // 拉低总线,发出起始信号 delay_ms(20); // 保持至少 18ms,DHT11 才会识别 HIGH; // 释放总线 delay_us(30); // 20~40us 高电平后交给传感器 P2DIR &= ~BIT1; // 切为输入,准备读响应 if(!(P2IN & BIT1)) // 传感器拉低回应 { while(!(P2IN & BIT1)); // 等 80us 低电平响应结束 while((P2IN & BIT1)); // 等 80us 高电平准备结束 humdh = receive(); humdl = receive(); temph = receive(); templ = receive(); check = receive(); } }这段代码的顺序不能乱。先切输出再拉低,是为了避免引脚处于输入态时拉低无效;拉低 20ms 而不是刚好 18ms,是留余量,因为delay_ms本身受 DCO 精度影响;释放总线后等 30us 再切输入,是给传感器留出识别起始信号并开始响应的时间。切输入之后用if判断总线是否被拉低,这一步是握手确认,如果没等到低电平说明传感器没接好或者上电未稳定,直接跳过整个读取,避免后面的receive卡死在while等待里。
while等待那两行是流程的分水岭。第一行等响应低电平结束,第二行等响应高电平结束,两个都走完才真正进入 40 位数据的采集窗口。省略任何一行都会导致位对齐错位,读出来的字节整体偏移。
注意:如果 DHT11 未接或接触不良,
while(!(P2IN & BIT1))在极端情况下可能长时间阻塞。稳妥做法是在等待循环里加一个计数上限,超时即返回,主循环下一轮再试。
4.2 逐位采样与 30us 判决阈值
receive负责解出一个字节的 8 位,核心是那个 30us 延时判决:
char receive(void) { unsigned char num = 0, tem, cnt; for(cnt = 0; cnt < 8; cnt++) { tem = 0; while(!(P2IN & BIT1)); // 等 50us 低电平时隙结束 delay_us(30); // 关键判决点 if((P2IN & BIT1)) // 30us 后仍为高,判为 1 { tem = 1; while((P2IN & BIT1)); // 等高电平走完,为下一位对齐 } num <<= 1; // 先移位再加位,保证高位在前 num |= tem; } return num; }逐行解释:while(!(P2IN & BIT1))消化每位开头固定的 50us 低电平;delay_us(30)是关键判决点,此时如果高电平还在,说明这一位的高电平宽度超过 30us,只能是 70us 的“1”,如果已经回低,就是 26 到 28us 的“0”;判决为 1 之后必须再用while((P2IN & BIT1))等高电平结束,否则下一位的低电平时隙会被吞掉,造成位错位。
num <<= 1; num |= tem;的顺序决定了字节的位序。DHT11 是高位先出,所以先左移腾出最低位再或入当前位,八次循环后得到的就是标准的 MSB 在前的字节。这个顺序写反的典型现象是读出的湿度值看起来“有数但不对”,比如实际 60% 读成 3%,校验和还恰好偶尔能过,属于最难查的一类问题。
30us 这个阈值需要单独说。DHT11 手册给出“0”的高电平是 26 到 28us,“1”是 70us,取 30us 左右作为分界能同时避开两者。但软件延时本身有误差,加上__delay_cycles的取整,实际判决点可能落在 30 到 32us 之间,仍在安全区。真正危险的是把延时写成delay_us(50),那样“1”才勉强能判对,而“0”有可能因为 DCO 偏差被误判成 1,表现为湿度虚高。
4.3 校验和与显示刷新
主循环里的校验逻辑很短,但它是数据可信度的唯一闸门:
while(1) { receive_init(); cal = humdh + humdl + temph + templ; if(cal == check) { show_Wendu(); show_Shidu(); } }cal取四个数据字节的低八位之和,与第五字节check比对,相等才刷新显示。注意这里是unsigned char相加,溢出自然丢弃高位,正好符合 DHT11 的校验规则,不需要额外取模运算。校验不过就不刷新,屏幕保留上一次的有效值,这比刷出乱码要好得多。
显示侧的两个函数是纯映射:
void show_Wendu() { Disp_SZ(0x83, temph); // 整数部分写两位数字 Disp_HZ(0x84, tishi_dot, 2); // 占位符 ".00" 补足小数位 } void show_Shidu() { Disp_SZ(0x8b, humdh); Disp_HZ(0x8c, tishi_dot, 2); }Disp_SZ内部把数值拆成十位和个位,查numtable取 ASCII 码写入:
void Disp_SZ(uchar addr, uchar shuzi) { uchar tmp0 = shuzi / 10; uchar tmp1 = shuzi % 10; Send(0, addr); Send(1, numtable[tmp0]); Send(1, numtable[tmp1]); }那个tishi_dot里装的是".00",用Disp_HZ按汉字方式写两字节,实际就是把小数位固定显示成.00。原始代码里Disp_SZ(0x84, templ)被注释掉了,原因是 DHT11 的温度小数位在当前精度下恒为 0,显示出来信息量为零,反而占位置。这个取舍在课设答辩时经常被问,回答口径应该是“传感器分辨率只有 1℃,小数位无有效信息”,而不是“懒得写”。
4.4 液晶写字节与指令/数据分流
Send是整条显示链路的地基,它把一个字节变成三字节的串行帧:
void Send(uchar type, uchar transdata) { uchar firstbyte = 0xf8; uchar temp; uchar i, j = 3; if(type) firstbyte |= 0x02; // 数据帧 vs 指令帧 cyPORT |= BIT(cyCS); cyPORT &= ~BIT(cyCLK); while(j > 0) { if(j == 3) temp = firstbyte; else if(j == 2) temp = transdata & 0xf0; else temp = (transdata << 4) & 0xf0; for(i = 8; i > 0; i--) { if(temp & 0x80) cyPORT |= BIT(cySID); else cyPORT &= ~BIT(cySID); cyPORT |= BIT(cyCLK); temp <<= 1; cyPORT &= ~BIT(cyCLK); } // 三个字节之间必须留足延时,否则易出现时序问题 if(j == 3) delay_Nus(600); else delay_Nus(200); j--; } cyPORT &= ~BIT(cySID); cyPORT &= ~BIT(cyCS); }第一字节是命令头,0xf8为基础值,type为 1 时或上0x02变成数据帧头,这就是指令与数据的分流开关。第二、三字节是把一个 8 位数据拆成高 4 位和低 4 位分两次送,因为 12864 串行协议按半字节对齐。每半字节内部再按位从高到低串出:先置数据线,再拉高时钟,左移,再拉低时钟,形成完整时钟脉冲。
延时那一行是血泪经验。代码作者在注释里特意标了“三个字节之间一定要有足够的延时”,第一字节后 600us、后续 200us,看起来奢侈,但串行模式下液晶内部要解析命令头再准备接收数据,间隔太短会直接丢字节,表现为屏幕局部花屏或者汉字显示成半个。这类问题用逻辑分析仪抓波形能一眼看出字节间隔不足,但纯靠肉眼调试极其折磨,属于典型的“不写后悔药就吃药”的场景。
初始化序列Ini_Lcd里的五条命令也要按序理解:0x30设 8 位接口和基本指令集,0x02地址归位,0x0c开显示但不显示光标,0x01清屏,0x06设光标右移和地址加一。每条命令后跟delay_Nus(72),这是液晶内部执行命令所需时间的下限,省掉会偶发命令未生效。
5. 避坑与排查:五类最容易翻车的问题
这套方案代码量不大,但排查成本很高,因为出错现象往往不具备指向性。下面五条是这个项目里最常见的翻车点,按“现象、原因、解决”拆开写。
5.1 读数恒为 0 或恒为 255
现象:屏幕能正常显示提示文字和单位,但温湿度数值固定不动,或者直接显示 0。原因通常是握手没成功——P2DIR |= BIT1之后忘记切回输入,或者拉低保持时间不足 18ms,导致 DHT11 根本没响应,if(!(P2IN & BIT1))分支整段被跳过,全局变量保留上电默认值。解决:先用示波器或逻辑分析仪看 P2.1 是否有那 80us 的响应低脉冲,没有就查起始信号;再把delay_ms(20)临时改成 25ms 加余量验证,确认后回退。
5.2 湿度正确、温度恒为 0
现象:湿度显示跟着环境变化,温度却一直是 0 或者变化极不灵敏。原因基本锁定在位序。receive里如果写成先或位再移位,字节内部的位序就反了,读出的数值呈现“看着像数但完全不对”的状态;另一种可能是receive_init里的字节顺序写错,把humdh和temph的位置调换了。解决:把五个字节原始值通过串口打印出来,对照cal是否等于check判断位序是否自洽;再看温湿度哪个跟着环境变,反推字节顺序。
5.3 校验和偶尔不过
现象:大部分时间显示正常,但每隔几秒数值闪一下停住不更新。原因是单总线时序抖动导致某一位判错,校验和自然对不上,主循环的if(cal == check)挡住了显示刷新。这其实是设计在正确工作,问题是判错频率过高。常见诱因是 DCO 未校准到 1MHz,或者主循环里加了其他耗时操作影响延时精度。解决:确认DCOCTL = CALDCO_1MHZ执行在位,主循环保持精简,不要把显示刷新之外的逻辑塞进来;读取间隔保持 1 秒以上,DHT11 自身采样率不低于 1Hz,读太快会读到上次的缓存值。
5.4 屏幕局部花屏或汉字缺笔画
现象:提示文字位置对但显示残缺,或者整屏随机出现噪点。原因是Send里三个字节之间的延时不够,或者Disp_HZ写汉字时字节数算错——一个汉字占两个字节地址,num参数传的是汉字个数,内部循环要跑到num * 2。解决:先把delay_Nus(600)和delay_Nus(200)各放大一倍做对照实验,如果花屏消失就是延时问题;如果依旧,检查Disp_HZ的调用实参是不是把字节数当成了汉字数。
5.5 数据地址与提示地址重叠
现象:数值显示出来后和汉字叠在一起,或者数字只显示一半。原因是地址规划没按 16 字节一行对齐,0x83这种奇数地址落在汉字的两个字节之间,写入时把汉字点阵的后半截覆盖了。解决:回到第 3 章的地址规划表,先确认提示和数值的起止地址在每个元素上互不侵占,再检查Disp_SZ写两位数字时是否连续占用两个字节地址——如果数值只放一位,第二位地址会被提示文字吃掉。
6. 验收与答辩:怎么证明这套采集链路真的可靠
验收这一步很多人吃亏在“能显示就算完”,但评审更想看到的是你对数据可信度和边界条件的掌控。我一般会用一套三段式验收流程,从静态到动态逐步加压,每段都留下可展示的证据。
第一段是静态自检,不动传感器只查显示。上电后先确认提示文字、单位符号、占位小数全部就位且无残缺,这一步验证Ini_Lcd的初始化序列和Disp_HZ的地址计算。接着用固定值替换temph / humdh的赋值,比如写死 25 和 60,看Disp_SZ是否稳定输出两位数字,确认显示链路与传感链路解耦。这一段过不了,后面所有数据都是假的。
第二段是动态比对,把采集结果和独立仪表对照。用手持温湿度计放在传感器旁边,记录 5 组环境状态下的读数和显示值,填写下面这种比对表。注意 DHT11 的精度是 ±2℃ 和 ±5%RH,比对时按这个容差判断合格与否,而不是要求完全一致。
| 测试场景 | 参考仪表读数 | 显示读数 | 校验是否通过 | 判定 |
|---|---|---|---|---|
| 室温静置 | 24.0℃ / 55%RH | 24℃ / 55%RH | 是 | 合格 |
| 手捂传感器 | 30.0℃ / 70%RH | 29℃ / 68%RH | 是 | 容差内 |
| 空调出风口 | 18.0℃ / 40%RH | 18℃ / 41%RH | 是 | 合格 |
| 加湿器旁 | 22.0℃ / 85%RH | 22℃ / 82%RH | 是 | 容差内 |
| 断开数据线 | 无 | 保持上次值 | 否 | 符合预期 |
第三段是故障注入,专门验证异常处理。依次做三件事:拔掉数据线,确认屏幕保留上次有效值而非刷出乱码;短接数据线到地一段时间,确认程序不会卡死在while等待里;把供电电压在 3.0V 到 3.6V 之间调,确认 DCO 校准和时序仍然可靠。这三步对应第 5 章那几个坑的反向验证,答辩时把现象和预期一一对上,比讲十遍代码流程有说服力。
答辩环节常被追问的两点,提前想好口径。一是“为什么不用定时器捕获解码”,回答要落在单总线没有参考时钟、中断开销反而增加抖动,纯软件延时在 1MHz 下精度足够且代码可控。二是“为什么小数位不显示”,回答传感器分辨率只有 1℃,小数位无有效信息,显示.00是排版占位而非真实读数,这个取舍是为了版面清晰。回答时把精度、时序、取舍三件事串起来说,比只报参数显得更懂设计。
从那以后我每次做单总线类的东西,都会在验收清单里强制加上一条:先断开设备看程序会不会卡死,再接回来确认能否自恢复。这个习惯是从一次答辩现场被要求“现在把线拔了”开始的,当时程序死在while循环里,屏幕和串口一起沉默。希望帮到你。
本文还有配套的精品资源,点击获取