简介:面向电子信息、自动化及单片机课程设计场景的51单片机坐姿矫正系统毕业设计文档,适合正在准备课程设计、毕业设计或电子小制作的学生参考。内容围绕STC89C51/52主控展开,涵盖自动与手动两种工作模式,利用光敏电阻配合模数转换采集环境光线,结合人体感应判断是否有人,并通过测距传感器监测使用者与台灯的距离;当距离过近或学习时长达到设定值时,蜂鸣器发出提醒,人离开后延时关灯,兼顾坐姿矫正、视力保护与节能。文档还涉及LCD1602显示、单片机最小系统、光线监测电路、测距电路及系统测试等模块,可帮助读者梳理传感器选型、硬件连接、报警逻辑和整体方案论证思路。压缩包内共1个doc文件,大小约295KB,结构完整,适合作为方案撰写与模块联调的参考资料。目前已有793人学习,具备一定的参考热度。
1. 坐姿矫正系统:为什么“几十块的 51 单片机”能撑起一个闭环
一个反直觉的结论先摆出来:坐姿矫正这种听起来该用摄像头加神经网络做的活,用一颗 8 位、12MHz、8KB Flash 的 STC89C52 也能闭环跑通,而且跑得相当稳。原因不在于芯片强,而在于问题的物理量极其朴素——距离、光强、时长,全是标量,采样频率低到十几赫兹就够。真正难的从来不是算力,是“什么时候该响、响多久、误报怎么压下去”。
把这件事想透,你会发现它是一台小型状态机加三条独立判据的仲裁器:谁先越界谁发话,谁最紧急谁优先。搞懂这套仲裁逻辑,比会点灯有意义得多。
1.1 三条判据的物理来源与量化路径
系统的全部输入只有三路,理解每一路怎么从物理世界变成单片机里的一个整数,是后面所有代码的前提。
第一路是距离。超声波模块发一个 40kHz 脉冲串,遇到障碍反射回来,单片机捕获回波高电平持续时间。声速在常温下约 340m/s,来回双程,所以距离d = v × t / 2。用 12MHz 晶振的 51,机器周期 1μs,定时器能数到微秒级,20cm 对应的回波宽度约 1.2ms,算下来分辨率够用。项目里给的量程是 3cm 到 90cm,近距离误差小、远距离误差 1~2cm,这个特性恰好和“坐姿距离”这个场景对上了——我们只关心 30cm 以内是不是太近,远处的粗误差无所谓。这是典型的场景选型优于器件指标。
第二路是光强。光敏电阻串一个分压电阻,光照变化导致阻值变化,分压点电压随之变化,这个 0~5V 的模拟量进 ADC0832 变成 8 位数字量 0~255。注意这里有个容易被忽略的细节:光敏电阻的阻值和照度是近似对数关系,不是线性的,所以 ADC 读数 128 不代表“亮度一半”。实际工程里没人去做精确照度标定,只取一个阈值——低于它就认为暗,这个做法糙但够用。
第三路是时长。这一路不需要传感器,纯粹是定时器计数。项目里定的是 45 分钟,到点提醒休息。
三路信号进单片机后,各走各的比较逻辑,最后汇总到蜂鸣器。
1.2 为什么用“阈值比较”而不是“状态识别”
如果用摄像头做姿态识别,能区分含胸、歪头、趴桌,但代价是算力、功耗、成本和调试复杂度全面上升,51 一颗芯片连一帧灰度图都存不下。而阈值比较只回答一个是非题:距离小于安全距离吗?
这个取舍背后的判断是:坐姿矫正的核心价值在“提醒”,不在“诊断”。用户不需要知道自己是第几度含胸,只需要在趴下去的那一刻被“叮”一声打断。行为干预的生效点在打断,不在测量精度。这也是为什么整套系统敢用 8 位 ADC 和 1~2cm 误差的超声波——误差远小于判据本身的模糊区间。
1.3 系统的整体信号流
从硬件框图的角度看,信号流是单向汇聚的:光敏电阻 → 分压 → ADC0832 → 单片机 P 口;超声波模块的 Trig/Echo 两线接普通 IO;LCD1602 占一个 8 位数据口加 RS/RW/E 三根控制线;蜂鸣器经三极管驱动接一个 IO。单片机是唯一的决策中心,没有第二个处理器,也没有中断嵌套的复杂场景,主循环轮询加定时器中断就能覆盖全部时序需求。
这种“单核 + 轮询”的结构常被诟病落后,但它带来一个隐性好处:整个系统的时序是完全确定的,任何一个时刻在执行哪段代码都可以推演出来。调试时这一点极其宝贵——出问题时你能在脑子里跑一遍流程,而不是面对一堆并发任务猜。
2. 主控与显示选型:STC89C52 与 LCD1602 的搭配逻辑
选型这件事看起来是“有什么用什么”,实际上每一步都在为后面的软件复杂度买单。这一章把两个关键器件的选择逻辑拆开,顺带说清 LCD1602 的驱动时序为什么必须严格遵守。
2.1 为什么是 51 而不是 STM32
项目里对比了三个方案:51、AVR、STM32F103。结论选 51,理由写在纸面上是“资料多、便宜、管脚少、容易学”,但真正的技术理由是资源匹配度。
这个系统需要什么?32 个 IO 里用掉大约 20 个,8KB Flash 装下全部逻辑绰绰有余,256 字节 RAM 存几个显示缓冲和计数值也不紧张,三个定时器用一个就够。所有需求都在 51 的能力边界内,且余量充足。反过来说,STM32 的 ADC、DMA、多路定时器、丰富外设在这里全部闲置,多出来的复杂度只体现在“要配置时钟树、要配引脚复用、要装下载环境”这些与功能无关的环节上。
提示:嵌入式选型的第一原则不是“性能越强越好”,而是“能力刚好覆盖需求,且余量可解释”。余量无法解释的部分,都会在调试期变成负债。
51 的另一个实际优势是时序简单可预测。12MHz 晶振下机器周期固定 1μs,写延时函数时可以精确估算,这对驱动 LCD1602 和超声波这种对时序敏感的器件非常友好。STM32 上主频高、流水线、缓存这些特性反而让“延时 1 微秒”变成一个需要查手册的问题。
最小系统本身的构成也值得说清楚:电源加晶振加复位。晶振电路提供内部时序基准,两个 30pF 左右的负载电容配合晶振起振;复位电路保证上电时寄存器回到确定状态。这两块的纹波和稳定性直接决定系统在干扰环境下能不能跑住——很多“程序莫名其妙跑飞”的问题,根子在电源纹波而不是代码。
2.2 LCD1602 的“忙标志”为什么不能省
LCD1602 内部有一颗专用的控制芯片,字符点阵已经固化在里面,支持 192 个字符。单片机要做的只是“告诉它显示什么、显示在哪”。但这个“告诉”有严格的时序要求。
引脚层面:RS 选寄存器(高为数据、低为指令),RW 选读写方向,E 是使能,下降沿触发执行;7~14 脚是 8 位数据口,第 3 脚 VEE 是对比度调节,一般接可调电阻分压。关键在于:LCD1602 执行一条指令需要时间,清屏指令最长可达 1.64ms,而 51 的一条指令只有 1μs 量级。如果不等它做完就发下一条,数据会丢。
所以每次读写前必须查忙标志。标准做法是把数据口当输入读回最高位:
// 等待 LCD1602 空闲,超时退出避免死等 void LcdWaitReady(void) { unsigned char sta; unsigned int timeout = 0; LCD_DAT_PORT = 0xFF; // 数据口置为输入状态,准备读回 LCD_RS = 0; // 选择指令寄存器 LCD_RW = 1; // 读方向 do { LCD_E = 1; // 使能拉高,数据有效 sta = LCD_DAT_PORT; // 读回状态字节 LCD_E = 0; // 下降沿结束本次读 } while ((sta & 0x80) && (++timeout < 5000)); // 最高位为 1 表示忙 }这段代码里有三个值得注意的点。第一,LCD_DAT_PORT = 0xFF是必须的:51 的 IO 口是准双向口,读之前必须先写 1 才能正确读入外部电平,漏掉这行会读到全 0。第二,判断的是最高位 D7,这是忙标志位,其余七位是地址计数器。第三,加了timeout计数——纯while死等在某些异常情况下会把整个系统卡死,加超时是给自己留退路。
写数据时 RS 置 1、RW 置 0,数据口输出字符码;写指令时 RS 置 0。整套驱动的骨架就是“查忙 → 写”,把这两个动作封装好,上层就只需要关心“在第几行第几列显示什么”。
2.3 显示内容的分层设计
LCD1602 只有两行十六列,信息密度极低,所以显示内容必须分层。项目里的用法是:一行显示当前设置的时长或倒计时,另一行显示状态提示(光线是否过暗、距离是否过近)。这种“数值 + 状态”的分配方式,让用户在瞥一眼的瞬间就能判断“要不要调整”。
实现上,把“定位光标”“写字符串”“写数字”拆成三个独立函数,上层组合调用。数字转字符这段如果用/和%反复取位,代码量小但执行慢;用查表或除十取余循环都行,因为刷新频率低,这点开销可以忽略。这里没有性能压力,可读性优先。
3. 光强与距离采样:ADC0832 与超声波的时间片配合
采样环节是整个系统里唯一涉及“模拟世界转数字世界”的部分,也是最容易出玄学的地方。ADC0832 是串行接口,超声波模块对时序敏感,两者放在一个主循环里轮询,必须把时间账算清楚。
3.1 ADC0832 的通道选择与软件模拟时序
ADC0832 的接口是四根线:CS、CLK、DI、DO。项目里提到 DO 和 DI 不同时工作,所以可以并成一根线用——这个简化在读写分时的前提下是成立的,因为发送配置位和接收数据位在时间上不重叠。
芯片工作在串行模式下,一次转换的流程是:CS 拉低启动 → 发起始位(DI 上给 1)→ 发两位通道选择(SGL/DIF 和 ODD/SIGN)→ 芯片在时钟下降沿输出数据位 → 收满 8 位 → CS 拉高结束。
// 软件模拟 SPI 读取 ADC0832 指定通道(ch=0 或 1) unsigned char Adc0832Read(unsigned char ch) { unsigned char i, dat = 0; ADC_CS = 0; // 片选有效,启动一次转换 ADC_CLK = 0; // 起始位 + 通道选择位,共 3 个时钟 AdcClockOut(1); // 起始位固定为 1 AdcClockOut(1); // SGL=1,单端输入模式 AdcClockOut(ch & 0x01); // 通道号 ADC_CLK = 1; // 空转一个时钟,进入数据输出阶段 ADC_CLK = 0; for (i = 0; i < 8; i++) { // 逐位接收,高位在前 ADC_CLK = 1; ADC_CLK = 0; // 下降沿后数据有效 dat <<= 1; dat |= ADC_DO; // 读入一位 } ADC_CS = 1; // 结束本次转换 return dat; }AdcClockOut内部做的事是“置 DI → 拉高 CLK → 拉低 CLK”,保证数据在时钟上升沿之前稳定。整个转换时间在几十微秒量级,项目里给的 32μs 属于这个范围。
关键取舍在这里:为什么用软件模拟 SPI 而不用硬件 SPI?因为 51 没有硬件 SPI,与其为了一个慢速传感器去纠结,不如用两根普通 IO 口位操作。代价是 CPU 被占用几十微秒,这在主循环里完全可接受。这个思路我一般会总结为:低采样率场景下,软件时序永远比硬件外设省心。
另一个细节是数据校验。ADC0832 的 DO 在时钟的不同阶段会输出两次相反的数据位,前面拿 DO 当 DI 用、这里再用 DO 收数据,本质上是分时复用同一根线。做数据校验时可以两次读同一通道比对,不一致就丢弃——这是压制电源噪声误报的低成本手段。
3.2 光强判据的滞后比较设计
直接拿 ADC 读数和阈值比大小,会产生一个经典问题:光照在阈值附近抖动时,蜂鸣器会疯狂开关,噪音极大且体验崩塌。解决办法是加迟滞:设两个阈值,暗到LOW才算暗,亮到HIGH才算亮,中间区间维持上一状态。
// 带迟滞的光照状态判断,避免阈值抖动导致反复报警 #define LIGHT_DARK_TH 60 // 低于此值判定为暗 #define LIGHT_BRIGHT_TH 90 // 高于此值判定为亮 unsigned char IsLightTooDark(unsigned char adc_val) { static unsigned char dark_flag = 0; if (adc_val < LIGHT_DARK_TH) { dark_flag = 1; // 足够暗,置位 } else if (adc_val > LIGHT_BRIGHT_TH) { dark_flag = 0; // 足够亮,复位 } return dark_flag; // 中间区间保持原状态 }两个阈值之间的 30 个刻度就是缓冲区,宽度要根据实测噪声决定:噪声大就拉宽,但拉太宽会让“真暗”响应迟钝。这个参数没有理论最优,只能实测调。血泪经验是:任何和模拟量打交道的地方,第一版都别用单阈值。
3.3 超声波测距的时序边界
超声波模块的 Trig 需要一个至少 10μs 的高电平脉冲触发,之后模块自动发 8 个 40kHz 方波,Echo 拉高,收到回波后拉低。单片机要做的是在 Echo 为高期间用定时器计数。
// 触发一次测距并返回厘米值,超时返回 0xFF 表示无回波 unsigned int UltrasonicMeasure(void) { unsigned int timer_val; unsigned int timeout = 0; TRIG = 1; Delay10us(2); // 维持约 20us 高电平,满足触发要求 TRIG = 0; while (ECHO == 0) { // 等待回波信号拉高 if (++timeout > 30000) return 0xFF; } TH0 = 0; TL0 = 0; // 定时器清零 TR0 = 1; // 开始计时 while (ECHO == 1) { // 高电平持续期间计时 if (++timeout > 60000) { // 超时保护,约 30ms TR0 = 0; return 0xFF; } } TR0 = 0; timer_val = (TH0 << 8) | TL0; return timer_val / 58; // 12MHz 下每 58 个计数约等于 1cm }/58这个系数是整篇代码里最容易被抄错的数字。推导一下:12MHz 晶振、12 分频、定时器加 1 是 1μs;声速 340m/s 等于 0.034cm/μs;来回双程,所以每厘米对应2 / 0.034 ≈ 58.8微秒。取 58 是工程近似,误差约 1%。如果换了晶振,这个系数必须重算,否则测出来的距离会系统性偏移——这种“看起来能跑但数字不对”的坑最难查。
两个while循环都必须带超时。等待 Echo 拉高的超时是因为模块可能没接好或者坏了;等待 Echo 拉低的超时是因为人可能不在(无遮挡时回波永远不回来)。没有超时保护的代码在现场必然卡死。
3.4 三个采样的时间片编排
主循环的结构大致是:读光强 → 测距离 → 刷新显示 → 判断是否报警 → 处理按键。其中超声波是最慢的(一次最坏几十毫秒),LCD 刷新次之,ADC 最快。所以顺序上把快的放前面,慢的放后面,且超声波测量不宜每轮都做——没必要,人的动作尺度是秒级的。
常见做法是用定时器中断维护一个时基,主循环里每 200~500ms 触发一次测距,中间几轮只做显示和按键。这样既保证了响应速度,也不会因为等回波而影响按键手感。这个“中断计时、主循环干活”的架构几乎是所有小系统的标准解法。
4. 定时与报警仲裁:45 分钟计时和蜂鸣器优先级
三条判据都会触发同一个蜂鸣器,这就在软件层面产生了一个真实问题:同时触发怎么办?响多久?怎么让用户知道是哪一条触发的?
4.1 定时器中断里的 45 分钟计时
45 分钟用 16 位定时器直接数是不现实的,12MHz 下定时器最多计约 65ms 就溢出。所以必然是“中断累加”的模式:定时器设 50ms 一次中断,中断里给一个计数变量加 1,加到 54000 就是 45 分钟。
volatile unsigned int tick_50ms = 0; // 50ms 计数 volatile unsigned char time_up_flag = 0; // 到点标志 void Timer0_ISR(void) interrupt 1 { TH0 = 0x3C; // 重装初值,50ms @ 12MHz TL0 = 0xB0; if (++tick_50ms >= 54000) { // 54000 × 50ms = 2700s = 45min tick_50ms = 0; time_up_flag = 1; // 只置标志,不在中断里做耗时操作 } }这里体现了一个重要原则:中断服务程序里只置标志、不做耗时动作。蜂鸣器控制、显示刷新这些全部放到主循环里根据标志处理。原因是中断执行时间越长,主循环的响应就越不可预测,超声波测距这种靠轮询计时的逻辑会被打断得乱七八糟。
tick_50ms和time_up_flag都加了volatile,因为它们被中断和主循环同时访问,不加的话编译器优化可能把主循环里的读取提到循环外面,导致永远看不到标志变化——这是新手最常踩的坑之一,现象是“程序逻辑明明对,就是没反应”。
重装初值 0x3CB0 对应 50ms 的计算:12MHz 下 1 个计数 1μs,50ms 是 50000 个计数,65536 - 50000 = 15536 = 0x3CB0。这个数换晶振就要重算,和上面超声波系数同理。
4.2 报警优先级与去重
三条判据的紧急程度不同:距离过近是即时危害,光暗是持续性影响,时间到是节奏性提示。所以仲裁规则可以定为:距离 > 光暗 > 时长。
实现上不是“if-else 链”那么简单,还要考虑去重和间歇。如果距离持续过近,蜂鸣器不能一直响(会让人烦躁到直接拔电源),应该做“响 200ms、停 800ms”的间歇节奏。而时间到这种一次性事件,响一段固定时长后自动停止,并把计数值清零重新开始。
// 主循环中的报警仲裁,优先级:距离 > 光暗 > 时长 void AlarmArbitrate(void) { if (abs_distance < NEAR_THRESHOLD) { BeepPattern(1); // 最高优先级:距离过近,快速间歇 } else if (IsLightTooDark(light_val)) { BeepPattern(2); // 次优先:光线过暗,慢速间歇 } else if (time_up_flag) { BeepPattern(3); // 时长到,长响一次后清除标志 time_up_flag = 0; } else { BEEP = 1; // 全部正常,关闭蜂鸣器 } }BeepPattern内部用主循环的时间片做非阻塞节奏控制,不能在里面delay——一旦阻塞,按键和测距全都停摆。正确做法是记录上次动作时刻,每次进来判断是否到了该翻转电平的时间点。这个技巧叫“软件定时器”,是所有裸机程序的基本功。
4.3 蜂鸣器的驱动极性与声音频率
项目用的是无源蜂鸣器加三极管驱动。无源蜂鸣器内部没有振荡源,必须给方波才响,频率决定音调。三极管做电流放大,单片机 IO 的驱动能力不足以直接带动蜂鸣器。
这里有个极性细节:项目描述里写的是“基极低电平时三极管导通、蜂鸣器发声”。这意味着用的是 PNP 三极管(或等效接法),IO 输出低电平导通。如果照抄代码但硬件用了 NPN,就会出现“逻辑全反”的现象——该响的时候不响,不该响的时候一直响。这类问题排查起来很费时间,因为代码看起来毫无问题。
注意:拿到任何一份硬件代码,先确认驱动级极性再复用。PNP 与 NPN 的差别只在一个电平翻转,但对调试周期的杀伤力极大。
声音频率方面,2kHz 到 4kHz 是人耳较敏感且不刺耳的区间。用定时器翻转 IO 产生方波比软件延时更稳,因为软件延时会被中断打断,导致音调抖动。
5. 避坑与常见问题:那些让系统“看起来能跑”的坑
这一章全部来自这类小系统调试中反复出现的现象。每一条都按“现象 → 原因 → 解决”写,因为单说结论记不住,遇到现象能对上号才有用。
5.1 现象:LCD 显示乱码或第一行全黑
原因通常有三个层次。最常见的是初始化序列没走完:LCD1602 上电后内部状态不确定,必须按手册要求延时足够时间再发功能设置指令,如果上电就立刻写,芯片还没准备好。其次是查忙标志被省略或者读回值恒为 0——如果数据口没先写 0xFF 就读取,51 的准双向口读回来的是自己输出的低电平,永远显示“不忙”,于是指令连发导致丢数据。第三是对比度调节脚 VEE 悬空或分压不当,电压不合适时字符会显示成黑块。
解决顺序是:先用固定长延时替代查忙,确认能正常显示,再把查忙加回来;对比度用一个 10k 电位器调到字符清晰为止;数据口初始化前统一置 0xFF。
5.2 现象:超声波测出来永远是 0 或者固定某个值
如果恒为 0xFF,说明一直没等到回波,检查 Trig 脉冲宽度是否够(10μs 以上),以及 Echo 是否接反。如果恒为某个固定值,多半是定时器初值没清零就启动了,或者上一次测量的残余计数没清。还有一种隐蔽情况:主循环里别的地方也在用同一个定时器,互相干扰导致计数值错乱。
排查方法是把 Echo 接到示波器或逻辑分析仪上看波形,正常应该看到宽度随距离变化的脉冲;没有仪器的话,用“手掌靠近再远离,看数值是否连续变化”来粗判。
5.3 现象:蜂鸣器在阈值附近持续抖动
这是单阈值比较的必然结果。光照或者距离在临界值上下微小波动,判据反复翻转,蜂鸣器就变成“哒哒哒”的噪音。根因是把模拟量的连续变化当成了离散的是非判断。
解决就是加迟滞,如 3.2 节的双阈值设计。距离判断同理,测距结果本身有 1~2cm 抖动,也要设两个阈值:小于 25cm 触发,大于 32cm 才解除。
5.4 现象:程序运行一段时间后完全卡死
最可能的位置是那些while等待循环。等 Echo 拉低、等 LCD 空闲、等按键释放,任何一处没有超时保护,只要条件永远不满足,整个系统就停在原地。这类 bug 的恶劣之处在于它不一定每次复现——取决于用户操作或环境。
解决方法是给每一个等待循环配一个计数器上限,超时后返回错误码而不是继续等。写裸机程序时我习惯强制自己:凡是while(条件)形式的等待,旁边必须有一个timeout变量。
5.5 现象:换了晶振或换了同型号不同批次芯片,行为变了
定时器初值、超声波换算系数、软件延时函数的循环次数,全部和晶振频率绑定。12MHz 换成 11.0592MHz 后,50ms 初值、/58系数、延时函数的实际时长全都偏了,表现为“计时不准”“测距偏大”“LCD 时序偶尔失败”。
解决是把这些常量做成宏或者统一的计算函数,换晶振时只改一个地方。更进一步,用定时器中断做时基,让所有延时都基于时基派生,减少对软件循环延时的依赖。
6. 设计取舍:低算力系统里,“够用”是一门需要证明的学问
回头看这套系统,最值得琢磨的不是它实现了什么,而是它主动放弃了什么。放弃摄像头姿态识别,放弃精确照度标定,放弃多任务调度,放弃高精度测距——每一个放弃都对应一次判断:这个精度对“提醒用户”这个目标有没有实际影响?
答案基本都是没有。行为干预类系统的有效性,取决于提醒的时机和频率,不取决于测量的分辨率。距离判据从 25cm 改成 24cm,用户的坐姿不会因此改善一丝;但把误报率从 30% 降到 5%,用户才会真的愿意继续用它。所以资源应该压在去抖、迟滞、间歇节奏这些“体验工程”上,而不是压在传感器精度上。
这个思路可以推广成一个判断框架:先确定系统的“有效精度下限”,也就是再精确也不会改变输出行为的那条线,然后把器件选型和算法复杂度都压在刚好跨过这条线的位置。跨得太多是浪费,跨不过去就是功能缺失。51 加 8 位 ADC 加 1cm 误差超声波,恰好卡在这条线上,这就是为什么它“能撑起来”。
验证方法也很朴素:不要只在桌面上静态测试。让人真的坐着写半小时作业,记录误报次数和漏报次数,再让不同身高的人各试一次。静态测试能过、动态测试翻车的系统太多了,因为真实使用中光照在变、人在动、桌面有杂物反射超声波。我个人习惯是任何带传感器的作品,在交付前至少做一次连续两小时的真实场景运行,看它会不会自己把自己搞崩。
还有一个容易被忽视的取舍是“自动模式”和“手动模式”的关系。自动模式下系统全权判断,手动模式让用户设定。这两者共存时,边界必须清晰:手动设定后自动逻辑是否还生效?如果生效,用户会觉得自己设了没用;如果不生效,又失去了“人离开自动关灯”的节能价值。项目里的处理是分模式运行,切换时清空对应状态,避免两套逻辑互相打架。这种“状态清理”的动作在状态机设计里是必须的,漏掉就会出现“切回自动模式后还带着手动模式的残余计时”这类幽灵 bug。
从那以后我每次做带多判据仲裁的小系统,都会先在纸上把优先级表和状态转换图画出来,再开始写代码,尤其是“谁清除谁的标志”这件事必须写清楚。这套坐姿矫正系统的全部源码、电路和文档就在资源里,硬件不复杂,逻辑也不长,正好适合拿来对照着把状态机和时序这两件事彻底跑一遍。
希望帮到你。
本文还有配套的精品资源,点击获取