
简介围绕MSP430G2553驱动DHT11温湿度传感器并在12864液晶上显示读数的工程说明面向学习超低功耗单片机、单总线时序与LCD驱动的嵌入式初学者及电子类专业学生可作为课程设计、实验报告与自学参考。压缩包内共1个doc文档约50KB以源码加中文注释的形式集中呈现完整实现主程序以1MHz DCO为时钟源经P2.1数据线完成主机与从机握手receive与receive_init负责起始信号及40位数据的逐位解码delay_us、delay_ms基于__delay_cycles实现微秒与毫秒级精确延时show_tishi、show_Wendu、show_Shidu承担提示信息与温湿度数值的排版Ini_Lcd、Disp_HZ、Disp_SZ封装液晶初始化与汉字数字显示并通过温湿度求和与校验位比对判断数据有效性。目前已有962人学习下载。读者可据此理清单总线通信的电平判定与采样窗口设置理解定时器延时与显示刷新的配合方式并对照注释排查时序偏差、校验失败等常见问题。1. 把 MSP430G2553、DHT11 和 12864 接上屏亮了但数字是 0洞洞板焊好程序烧进去12864 背光亮起温度和湿度这几个提示字也规规矩矩排在屏幕上可后面等着显示数字的位置一直是 0。这种场面在单片机课设里太常见——问题不在接线不在芯片坏了而在 DHT11 单总线那几十微秒的判定窗口没有对准。这套 MSP430G2553 驱动 DHT11 温湿度传感器、再用 12864 串行显示的方案要解决的就是把一根线读回 40 位这件事做稳主控是 MSP430G2553在 1MHz 标定 DCO 下运行传感器是 DHT11走单总线协议显示模块是 12864走三线串行不需要额外的电平转换芯片。它适合正在做传感器采集课设、毕设前期把数据链路先跑通的人也适合手上有一块低功耗节点板、想给它配一块能看的人机界面的从业者。2. 硬件连接与单总线时序DHT11 为什么必须掐着微秒算2.1 引脚分配与电气连接一根数据线要扛四种状态这套系统的引脚分配很紧凑但每一根都有明确的角色。DHT11 的数据线接在 P2.1这一根线上要完成四件事主机拉低发起起始信号、主机释放总线改为输入、传感器拉低拉高做应答、传感器逐位输出 0 和 1所以 P2DIR 的对应位必须在输出和输入之间来回切换不能一上电就固定成输出否则传感器的应答永远传不回来。12864 用的是串行模式只占三根线P1.0 做片选 CSP1.1 做串行数据 SIDP1.2 做同步时钟 CLK。串行模式的好处是把并行口的 8 根数据线省下来代价是每一字节数据要拆成两个半字节分三次发时序要求比并行严格得多。如果你手上是并口屏这套代码的 Send() 要整体重写别指望改几个宏就能跑。供电上DHT11 支持 3.3V 到 5.5V 宽压和 MSP430G2553 的 3.3V 可以直接对接不需要电平转换。数据线要接一只 4.7k 到 10k 的上拉电阻到 VCC这是单总线的命门总线空闲时靠这只电阻维持高电平没有它释放总线后引脚会悬空读回来的值随机跳。很多人用内部上拉凑合内部上拉阻值偏大几十 k线稍长一点边沿就软读出来的数据时对时错。通常的做法是老老实实焊一只 4.7k 的贴片电阻。还有一点容易被忽略DHT11 上电后需要约 1 秒的稳定时间才能给出有效数据这 1 秒内读到的值没有意义。程序里如果没有上电延时第一次循环读到的必然是垃圾运气不好还会带偏后面的判断。2.2 一次完整读取的时序拆解从 18ms 拉低到 40 位收完DHT11 的通信是主机发起、传感器应答的主从结构整个过程可以拆成四段。第一段是起始信号主机把数据线拉低保持至少 18ms。这个 18ms 不是随便定的它要足够长让传感器在任意时刻都能明确识别到主机要找我了。工程上一般给 20ms多一点没关系少一点都不行。第二段是主机释放总线拉高 20 到 40us然后把引脚切成输入。这 20 到 40us 是个窗口太短传感器反应不过来太长会和自己后面的应答撞上。代码里取 30us正好卡在中间。第三段是传感器应答DHT11 检测到起始信号后会把总线拉低约 80us再拉高约 80us这一低一高就是我在准备发数据了。主机在这段时间里做两件事——先等低电平结束再等高电平结束等两段都过去了第一位数据就开始了。第四段是 40 位数据传输每一位的格式都一样先 50us 低电平作为位间隔然后是高电平高电平持续 26 到 28us 表示这一位是 0持续 70us 表示这一位是 1。注意0 和 1 的区别不在有没有高电平而在高电平有多长。这就是为什么判定不能靠读到高就是 1必须给一个固定采样点在低电平结束之后延时一段时间再去读引脚——这个采样点落在 28us 和 70us 之间就能把 0 和 1 分开。代码里取 30us比 28us 长一点比 70us 短很多余量充足。这个 30us 是整个方案里最关键的一个数字改它等于改判据。40 位数据的排列是固定的湿度整数 8 位、湿度小数 8 位、温度整数 8 位、温度小数 8 位、校验和 8 位全部高位在前。校验和等于前四个字节相加取低 8 位收到之后自己算一遍对不上就丢弃这一帧。2.3 1MHz 标定 DCO 下的延时预算__delay_cycles 怎么算才不飘MSP430G2553 出厂时在 Flash 里存了 1MHz、8MHz、12MHz、16MHz 几个校准值程序开头把 DCOCTL 和 BCSCTL1 分别赋成 CALDCO_1MHZ 和 CALBC1_1MHZ主频就锁定在 1MHz。这一步不能省只写 DCOCTL 不写 BCSCTL1实际频率可能差出一大截所有基于周期的延时跟着一起飘。锁定之后一个时钟周期就是 1us延时可以直接用编译器内建的__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))这段宏里用 double 做中间运算看起来是在做单位换算实际上是给编译器看的x 是字面量时比如 delay_us(30)整个表达式在编译期折叠成一个整数__delay_cycles(30)就是 30 个周期干净利落。一旦你写成delay_us(n)而 n 是个变量这段代码立刻翻车——编译器要么直接拒绝要么按最坏情况展开成一大坨浮点运算实际延时远远超出预期。所以这套延时宏只能配常量用需要可变延时的地方得自己写_NOP()循环接受它不精确这个事实。顺带说一句代码里另外那套delay_Nus()、delay_1ms()是用for循环加_NOP()凑出来的1ms 那个循环 150 次是在特定编译器和优化等级下试出来的经验值换个编译器版本就变了。它们只用在 12864 初始化的粗延时上误差几十微秒无所谓DHT11 那边一个都不能用必须走__delay_cycles。3. 程序总体架构与采集实现从 40 位原始数据到可用数值3.1 三层职责划分握手层、位解码层、显示层整个程序切成三层边界划得很清楚。最底下是握手层负责拉低总线、释放总线、等待应答把总线状态从主机控制交到传感器控制中间是位解码层负责在一个已经进入数据传输的阶段里把 40 个高低电平翻译成 5 个字节最上面是显示层只管拿现成的数值往屏幕上摆不关心数值是怎么来的。这样切的好处是排查方便。屏幕上没数字你先看显示层有没有被调用再看位解码层拼出来的字节对不对最后才怀疑握手层的时序——从外往里一层层剥比盯着主函数一行行看快得多。两层之间靠几个全局变量传递数据unsigned char temph, templ; /* 温度整数、温度小数 */ unsigned char humdh, humdl; /* 湿度整数、湿度小数 */ unsigned char check, cal; /* 收到的校验和、本地算出的校验和 */用全局变量而不是返回值是这套代码的一个取舍。receive_init()要一次吐出 5 个字节用返回值只能返回一个要么打包成结构体要么传指针数组在只有 512 字节 RAM 的 G2553 上全局变量反而最省事。代价是这几个变量对所有函数可见谁都能改调试时如果发现数值莫名其妙变了得顺着调用链往回找。可行的做法是给它们加volatile防止编译器在优化时把多次读取合并成一次。3.2 receive() 的逐位判定与拼装位解码的核心是一个循环 8 次的函数每次处理一个字节。它的执行前提是总线已经处在传感器要发数据的状态当前正卡在某一位的 50us 低电平里。char receive(void) /* 接收一个字节高位在前 */ { unsigned char num 0, tem, cnt; for (cnt 0; cnt 8; cnt) /* 一个字节 8 位 */ { tem 0; while (!(P2IN BIT1)); /* 等这一位的 50us 低电平结束 */ delay_us(30); /* 低电平结束后 30us 采样 */ if (P2IN BIT1) /* 还在高电平 - 高电平长 - 判为 1 */ { tem 1; while (P2IN BIT1); /* 等高电平结束为下一位做准备 */ } num 1; /* 先收到的在高位 */ num | tem; } return num; }这段代码有三个地方值得盯住。第一处是while (!(P2IN BIT1))它等的是低电平结束也就是等总线被传感器拉高。这一步不能省省了就等于在低电平期间采样永远读到 0。第二处是delay_us(30)这个采样点前面分析过它必须落在 28us 和 70us 之间30us 是最稳的选择改成 20us 会把一部分 1 误判成 0改成 50us 则余量变小、抗干扰变差。第三处是判为 1 之后那个while (P2IN BIT1)它的作用是等高电平走完让下一次循环从一个干净的低电平起点开始判为 0 时不需要这个等待因为 26us 的高电平早就结束了直接进下一次循环正好落在低电平里。num 1; num | tem;这两行决定了位序。DHT11 是高位在前第一个收到的位要落在 bit7所以每收一位先把已有内容左移一位再把新位塞进最低位。如果写成num | tem cnt位序就整体反了读出来的湿度会是一个完全说不通的数。3.3 receive_init() 的启动握手与校验和闭环握手函数负责把总线状态推过去然后连着调 5 次receive()把 5 个字节全部收回来。void receive_init() { P2DIR | BIT1; /* 数据线切输出 */ LOW; /* 拉低总线发起起始信号 */ delay_ms(20); /* 保持 20ms满足 DHT11 的 18ms 要求 */ 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(); /* 校验和 */ } }这里有个非常隐蔽的坑就是if后面那个分号。原始代码里写的是if (!(P2IN BIT1));然后跟一对大括号——分号把if语句就地终结了大括号里那一整段变成了无条件执行。后果是即使传感器完全没应答程序也会照样往下跑在一条死总线上傻等低电平、等高电平然后收回来 5 个 0 或者 5 个 0xFF。屏幕上显示 0你以为是传感器坏了其实是分支根本没生效。这个分号我在自己的代码里翻过一次车后来每次写if都先看一眼行尾。校验和的处理放在主循环里因为它是这一帧能不能用的判据void main(void) { WDTCTL WDTPW WDTHOLD; /* 关看门狗否则循环里会被复位 */ DCOCTL CALDCO_1MHZ; /* DCO 校准到 1MHz */ BCSCTL1 CALBC1_1MHZ; /* 这一行不能少只写 DCOCTL 频率会偏 */ Ini_Lcd(); /* 初始化 12864 */ show_tishi(); /* 先摆好固定的提示字 */ while (1) { receive_init(); cal humdh humdl temph templ; /* 自动溢出天然取低 8 位 */ if (cal check) /* 校验通过才刷新 */ { show_Wendu(); show_Shidu(); } delay_ms(1000); /* 两次采集之间至少隔 1s */ } }cal声明成unsigned char四个字节相加的结果自动截断成低 8 位正好就是 DHT11 校验和的定义不需要额外写 0xFF。校验不通过时不刷新屏幕屏幕上保留的是上一次的合法值——这比刷成 0 要好至少不会让观察者误以为温湿度突然掉到零度。3.4 主循环的节流与异常兜底主循环末尾那个 1 秒延时是必须的不是可选的优化。DHT11 的采样周期建议不低于 1 秒两次读取间隔太短传感器内部还没准备好新的转换结果返回的可能是上一次的缓存甚至直接不应答。原始代码里while(1)是裸着连读的没有延时表现就是前几帧能出数后面开始飘或者干脆卡死在某个while里出不来。卡死是这套代码的另一个风险点所有等待都是while死等没有超时。传感器拔掉、线断、上拉电阻虚焊程序就永久停在某一行里屏幕定格看起来像死机。工程上一般会给每个等待加一个计数上限超了就跳出去返回错误码unsigned int timeout 0; while (!(P2IN BIT1)) { if (timeout 5000) return; /* 约 5ms 还没等到判定为无应答 */ }5000 这个数按 1MHz 主频、每次循环约 1us 估算够覆盖正常的 80us 应答又不至于让程序在这儿耗太久。加了超时之后传感器掉了程序也活着屏幕保持最后一帧有效数据你从程序不卡这个现象就能推断出是硬件连接问题而不是软件跑飞。4. 12864 串行显示的落地实现三字节帧与两套指令集4.1 Send() 的三字节帧格式与位序12864 在串行模式下每传送一个字节的数据要拆成三帧发出去第一帧是同步头加命令/数据标志第二帧是这个字节的高 4 位第三帧是低 4 位。同步头的固定格式是五个 1 打头后面跟 RW固定 0只写不读、RS0 是命令1 是数据和一个 0拼起来就是 0xF8 或 0xFA。void Send(uchar type, uchar transdata) { uchar firstbyte 0xf8; /* 同步头11111 RW0 RS0 0 */ uchar temp; uchar i, j 3; if (type) firstbyte | 0x02; /* type1 表示数据RS 置 1 - 0xFA */ cyPORT | BIT(cyCS); /* 片选拉高开始一帧传输 */ cyPORT ~BIT(cyCLK); while (j 0) { if (j 3) temp firstbyte; /* 第一帧同步头 */ else if (j 2) temp transdata 0xf0; /* 第二帧高 4 位 */ else temp (transdata 4) 0xf0;/* 第三帧低 4 位 */ for (i 8; i 0; i--) /* 每帧 8 个 bit高位先出 */ { 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); /* 片选拉低一帧结束 */ }三个参数的含义要分清type决定这一字节是命令还是显示数据命令走 0xF8数据走 0xFA写错的话控制器会把命令当字符画到屏上屏幕立刻变成一片乱码。transdata是要发送的原始字节函数内部负责把它劈成两个半字节并且都对齐到高 4 位——注意transdata 0xf0保留了高位(transdata 4) 0xf0是把低位挪到高位两个结果都放在 bit7 到 bit4低 4 位补 0。帧间延时是 600us 和 200us这两个数不是随便写的。12864 的串行接口在接收完一帧之后需要时间把数据搬进内部寄存器间隔太短会丢帧表现就是屏幕上一部分字正常、一部分花掉。不同的屏、不同的排线长度对这个时间敏感程度不一样600/200 是个偏保守的组合宁可慢一点也别贪快。4.2 基本指令集与扩展指令集的切换顺序12864 内部有两套指令集基本指令集管字符显示和清屏扩展指令集管绘图 RAM。上电默认在基本指令集初始化只要发几条固定命令void Ini_Lcd(void) { cyDDR | BIT(cyCLK) BIT(cySID) BIT(cyCS); /* 三根线设为输出 */ delay_Nms(100); /* 等模块内部复位完成不能省 */ Send(0, 0x30); delay_Nus(72); /* 8 位数据总线基本指令集 */ Send(0, 0x02); delay_Nus(72); /* DDRAM 地址归零 */ Send(0, 0x0c); delay_Nus(72); /* 开显示不显示光标不反白 */ Send(0, 0x01); delay_Nus(72); /* 清屏 */ Send(0, 0x06); delay_Nus(72); /* 光标右移地址加 1 */ }开头那个 100ms 是给模块自己的复位电路留时间。有些屏上电后需要几十毫秒才能接受命令这期间发过去的指令全被丢掉结果就是初始化看起来执行了屏幕却还是初始的乱码状态。这个延时只能长不能短省掉它是最常见的为什么我的屏上电就花的原因。需要画图或者写自定义字模时先发 0x34 切到扩展指令集操作完 GDRAM 之后发 0x36 打开绘图显示最后发 0x30 切回基本指令集。顺序不能颠倒切回基本指令集必须在打开绘图显示之后否则绘图内容不显示而在扩展指令集状态下写字符命令是无效的会静默丢弃。Draw_PM()和Draw_TX()这两个函数就是按这个顺序写的中间夹着Send(0,0x34)和Send(0,0x36); Send(0,0x30);。4.3 显示排版地址计算与 Disp_HZ / Disp_SZ12864 的 DDRAM 地址是分两行的第一行从 0x80 起第二行从 0x90 起每个地址对应一个 8 像素宽的位置一个 16×16 汉字占两个地址。显示函数只做两件事把地址设进去然后连着写数据。void Disp_HZ(uchar addr, const uchar *pt, uchar num) { uchar i; Send(0, addr); /* 设定显示起始地址 */ for (i 0; i (num * 2); i) /* 一个汉字 16 像素宽 2 个字节 */ Send(1, *(pt)); /* type1按数据写 */ } void Disp_SZ(uchar addr, uchar shuzi) { uchar tmp0, tmp1; tmp0 shuzi / 10; /* 十位 */ tmp1 shuzi % 10; /* 个位 */ Send(0, addr); Send(1, numtable[tmp0]); /* 用 ASCII 字库直接写字符 */ Send(1, numtable[tmp1]); }Disp_HZ的num是汉字个数循环次数要乘 2因为一个汉字在 DDRAM 里占两个字节。传 3 表示写 3 个汉字、占 6 个地址写超了会自动折到下一行这个行为有时候会被误当成 bug。排版的具体坐标是手工算出来的第一行 0x80 放温度三个汉字占 0x80 到 0x850x86 放°占 0x86、0x870x88 放湿度占 0x88 到 0x8D0x8E 放%占 0x8E、0x8F。第二行从 0x90 起湿度数值放在 0x90 附近温度数值放在 0x83 附近正好落在提示字中间的空位上。这种手算地址的方式很土但直观、可预测改一处位置只要改一个十六进制数。Disp_SZ一次只写两位数字通过numtable[]这个0123456789的字符表做转换好处是不用查 ASCII 码表numtable[7]就是字符7。它只能处理 0 到 99超过 99 会显示成乱字符而 DHT11 的湿度上限是 100刚好卡在边界上——湿度报 100% 的时候会显示成10后面跟一个查表越界的字符。稳妥的做法是先判断if (shuzi 99) shuzi 99;牺牲一点真实性换取显示不出错。小数位那边代码里是把小数点和固定的两位数字预先做成 16×16 点阵塞进tishi_dot[]然后用Disp_HZ整块写出去这样不用单独定位小数点缺点是小数位永远是 00。show_Wendu()和show_Shidu()里被注释掉的Disp_SZ(0x84, templ)原本是想动态显示小数注释掉之后就固定成 .00 了。DHT11 的小数位本身精度有限湿度分辨率 1%RH温度 1℃很多场合直接显示整数反而更诚实。5. 避坑与常见问题排查五个最容易翻车的现场5.1 屏亮了但满屏乱码提示字都不对现象上电后背光正常屏幕上是一片随机的黑块或者半截字连温度这种固定字符串都显示不出来。原因通常有两个一是初始化前那个 100ms 复位等待被省掉了模块还没准备好就开始收命令前面的命令全丢二是三字节帧之间的延时太短控制器来不及搬数据收到的半字节是错位的。解决方法是先把delay_Nms(100)补回来再把Send()里的 600us 和 200us 往上加一倍试试如果加完就正常了说明你的排线偏长或者模块偏慢最终值就按能稳定工作的最小值再留一倍余量。5.2 温湿度恒为 0或者恒为 255现象屏幕正常提示字正常数值位置一直是00或者偶尔跳成255。原因有三层得按顺序排。第一层是if后面那个分号让应答判断彻底失效程序在无应答的情况下照样往下跑收回来一串 0第二层是上拉电阻缺失或者阻值太大总线释放后没有回到高电平while一直等不到边沿第三层是传感器上电还没满 1 秒就被读了第一次读到的是未初始化值。排查顺序建议反过来走先量 P2.1 在空闲时是不是稳定高电平万用表就行再看代码里if行尾有没有多余分号最后确认主循环第一次读取前有没有上电延时。5.3 宏定义带分号if-else 悄悄走偏现象编译能过逻辑却不对明明条件不成立语句照样执行或者加了else之后编译报错。原因在#define HIGH P2OUT | BIT1;这个写法上——宏体末尾的分号会跟着展开if (x) HIGH; else LOW;展开后变成if (x) P2OUT | BIT1;; else ...多出来的那个分号把if和else拆散了。解决方法是宏体一律不带分号写成#define HIGH (P2OUT | BIT1)并整体加括号调用处自己写分号。这条规则对LOW一样适用。5.4 连续读偶尔失败隔一会儿又好了现象程序跑起来大部分帧正常偶尔校验不过屏幕上数值停一下又跳回来。原因是主循环里两次采集之间没有间隔DHT11 上一次转换还没结束就被叫醒返回的数据不完整。原始代码的while(1)是裸循环靠 CPU 跑一圈的耗时当间隔1MHz 下这一圈可能只有几毫秒远远不够。解决方法是在循环末尾加delay_ms(1000)把采样率压到 1Hz这个频率对温湿度这种缓变量完全够用还能顺带降低功耗。5.5 校验和偶尔对不上数据看着还挺合理现象读回来的湿度和温度数值肉眼看没问题但校验和就是不相等程序一直不刷新。原因是位接收过程中被中断打断了——MSP430 的定时器、看门狗或者外部中断一旦在receive()执行期间触发30us 的采样点就被推迟本该判 0 的位可能被判成 1。解决方法是在receive_init()调用前后关中断再开中断用__disable_interrupt()和__enable_interrupt()包起来。40 位数据的接收时间加起来不到 5ms关中断这么久对绝大多数应用没有影响但对时序的改善是立竿见影的。如果关了中断还是偶尔错那就得怀疑延时宏的参数是不是传了变量。6. 验收与答辩怎么证明这套方案真的跑通验收这件事最怕的是看起来能显示数字就交差。屏幕上跳出 26 和 58你不能证明这两个数是从传感器来的还是 RAM 里没清干净的残留。我一般会按三步走每一步都有独立的判据前一步不过不往下走。第一步验时序不看屏幕只看引脚。把 P2.1 接到示波器或者逻辑分析仪上触发方式设成下降沿抓一次完整的读取过程。合格的波形应该能看到一段约 20ms 的低电平起始信号紧跟着约 30us 的高电平释放总线然后是传感器回的两段约 80us 的应答脉冲再往后是一串 50us 低电平打底、高电平宽度在 27us 和 70us 之间交替的位流。如果应答脉冲根本不出现问题在握手如果应答有但位流的高电平宽度全都一样问题在传感器没进入数据模式。没有示波器的话退一步用万用表的频率档看 P2.1 在读取瞬间有没有跳变虽然看不出宽度至少能确认总线上有动静。第二步验数据把 5 个字节原样送到串口打出来。这一步的目的是绕开显示层确认校验和能对上。串口配置成 9600、8 位、无校验每帧打印成H:58.0 T:26.0 SUM:OK这样的格式连续观察十分钟。合格的判据是校验和连续通过、数值随环境变化单调用手捂住传感器湿度应该几十秒内明显上升并且没有出现 0 或 255 这种边界值。这一步能过说明采集链路是通的后面显示出任何问题都是显示层的事。第三步验显示把串口关掉只看屏幕。合格的判据有三条提示字位置固定不漂移、数值刷新时其他位置不闪、断电重启后能自己恢复。第三条特别重要很多人在调试过程中反复按复位掩盖了上电时序的问题冷启动一次就露馅了。答辩环节最常被问到的其实不是代码细节而是你为什么这么选。下面这三个问题几乎必问提前准备好答案可能的问题回答的落点为什么采样点取 30us0 的高电平是 26~28us1 的高电平是 70us30us 落在两者之间且离 0 更近对 0 的容错更大同时留出了 40us 的余量给 1为什么主频定 1MHz 而不是更高延时全部基于周期数换算1MHz 下一个周期正好 1us换算不用心算功耗也最低这套系统没有算力需求数据为什么要校验单总线没有时钟线靠电平宽度传信息抗干扰能力弱校验和是唯一能发现位翻转的手段不校验等于盲目相信总线还有一个加分项答辩时主动说出这套代码的三个已知缺陷——if后面的分号、while等待没有超时、小数位被固定成 00。能指出自己方案的问题比声称它完美无缺可信得多评委也更愿意往下聊。从那以后我每次写完单总线的握手函数都会先把if那一行的行尾盯三秒再把所有while挨个加上超时计数最后才烧板子。这个习惯救过我至少两次通宵。希望帮到你。本文还有配套的精品资源点击获取