上个月接手了一个智能空气净化器的项目,用户反馈设备响应慢,从APP下发指令到设备执行,有时候要等好几秒,体验很差。领导让我优化一下,目标是把响应时间降到500毫秒以内。

最开始我觉得这应该不难,不就是优化一下代码嘛。结果真正做起来才发现,嵌入式设备的性能优化和服务器端完全不一样,坑很多,花了整整两周时间,才把响应时间从原来的平均2.3秒降到了400多毫秒,降了80%多。

这篇文章就来分享一下这次性能调优的完整经历,从问题定位、性能分析、瓶颈识别,到具体的优化手段,以及过程中的踩坑和经验总结。希望能给做嵌入式开发或者IoT开发的朋友一些参考。

项目背景

先简单介绍一下项目背景。

这是一款智能空气净化器,主控芯片是一款ARM Cortex-M4的MCU,主频120MHz,内存256KB SRAM,闪存1MB。设备上运行的是一个基于FreeRTOS的嵌入式系统,主要功能包括:空气质量检测(PM2.5、甲醛、温湿度等传感器)、风扇控制(多档调速)、滤网管理、APP远程控制(通过WiFi模块)、语音控制、OLED显示屏、按键操作等。

设备的架构大致是这样的:MCU作为主控,负责所有的控制逻辑和传感器数据采集;WiFi模块(ESP8266)负责和云端通信,接收APP的指令,上报设备状态;传感器通过I2C或ADC接口连接到MCU;风扇通过PWM控制;显示屏通过SPI接口连接。

用户反馈的响应慢,主要是指从APP下发指令(比如开关设备、调节风速、切换模式)到设备实际执行并反馈状态的时间太长。有时候点一下APP,要等两三秒设备才有反应,有时候甚至要等五六秒,体验很差。

领导给的目标是把端到端响应时间(从APP下发指令到设备执行并上报状态完成)降到500毫秒以内,最好能做到300毫秒。

最开始我觉得这个目标应该不难达到,因为设备的功能并不复杂,指令处理的逻辑也不复杂。但真正开始做性能分析的时候,才发现问题比想象的严重得多。

第一步:性能分析和问题定位

优化的第一步是性能分析,搞清楚时间到底花在了哪里,瓶颈在哪里。不能凭感觉优化,必须有数据支撑。

嵌入式设备的性能分析不像服务器端那么方便,没有现成的APM工具,也不能随便打日志(因为串口输出本身就很慢,会影响性能)。我用了几种方法来做性能分析。

第一种方法是GPIO翻转法。这是嵌入式开发中最常用、最可靠的性能分析方法。在代码的关键位置,把一个空闲的GPIO引脚拉高或拉低,然后用示波器或者逻辑分析仪观察引脚的电平变化,就能精确地测量出某段代码的执行时间。这种方法的精度很高,可以达到微秒级,而且对系统性能的影响极小。

我在指令处理流程的各个关键节点都加了GPIO翻转,包括:WiFi模块接收到数据、MCU从WiFi模块读取数据、数据解析、指令处理、硬件控制(风扇等)、状态上报、WiFi模块发送数据。然后用逻辑分析仪抓取整个流程的时间线,看看每个环节花了多长时间。

第二种方法是串口日志法。虽然串口输出慢,但在调试阶段还是可以用的,只是要注意不要在性能测试的关键路径上打太多日志。我在关键节点用串口输出时间戳(用MCU的定时器获取毫秒级时间),然后通过串口助手查看日志,分析每个环节的时间。这种方法比GPIO翻转法方便,不需要额外的硬件,但精度稍差,而且串口输出本身会有一定的性能影响。

第三种方法是模拟器或者仿真器。如果有条件的话,可以用J-Link等仿真器配合IDE的性能分析功能,或者用QEMU等模拟器来做性能分析。但这个项目的硬件比较特殊,有些外设模拟器不支持,所以主要还是用前两种方法。

分析的结果让我很意外。原来我以为瓶颈在WiFi通信或者数据解析,但实际上最大的瓶颈在几个我完全没想到的地方。

整个指令处理流程的时间分布大致是这样的(平均2.3秒的响应时间):

  • WiFi模块接收数据并通知MCU:约200毫秒
  • MCU从WiFi模块读取数据:约50毫秒
  • 数据解析和校验:约30毫秒
  • 指令处理(核心逻辑):约150毫秒
  • 硬件控制(风扇调速等):约800毫秒!这是最大的瓶颈
  • 状态上报数据准备:约100毫秒
  • WiFi模块发送数据:约300毫秒
  • 其他(任务调度、等待等):约670毫秒

看到这个结果我当时就懵了,硬件控制(风扇调速)居然花了800毫秒?这怎么可能?不就是设置一下PWM占空比吗?应该几毫秒就搞定了啊。

还有"其他"那670毫秒,也太多了,到底在等什么?

带着这些疑问,我开始深入分析,这才发现了几个严重的问题。

问题一:风扇调速的阻塞延时

第一个大问题是风扇调速的阻塞延时。

我深入看了风扇控制的代码,发现了一个让人哭笑不得的问题。原来的开发者为了让风扇转速平稳变化,避免转速突变产生噪音,在风扇调速的函数里加了一个渐变的逻辑。这个逻辑本身没问题,但实现方式有大问题。

他的实现是这样的:从当前转速渐变到目标转速,每次调整1%,然后延时10毫秒,直到达到目标转速。也就是说,如果从0档调到最高档(假设100档),就要循环100次,每次延时10毫秒,总共就是1000毫秒!而且这个延时是阻塞式的(用的是vTaskDelay,虽然不是忙等,但会阻塞当前任务)。

更糟糕的是,这个风扇调速函数是在指令处理任务里直接调用的,也就是说,指令处理任务会被阻塞在风扇调速这里,直到渐变完成,才能继续处理后面的逻辑(比如状态上报)。这就是为什么硬件控制花了800毫秒(平均风速变化大概80档左右)。

而且,因为指令处理任务被阻塞了,其他的指令也没法处理,只能排队等着。如果用户连续操作(比如连续调风速),设备就会越来越卡,响应越来越慢,这就是用户有时候要等五六秒的原因。

找到问题之后,解决方案就很明确了:把风扇调速的渐变逻辑从指令处理任务里移出来,放到一个独立的风扇控制任务里,用非阻塞的方式实现渐变。

具体的做法是:

  1. 创建一个独立的风扇控制任务,优先级适中。
  2. 指令处理任务收到调速指令后,只设置目标转速,然后立即返回,不等待渐变完成。
  3. 风扇控制任务循环检测当前转速和目标转速的差异,如果有差异,就每次调整1%,然后延时10毫秒,直到达到目标转速。
  4. 用一个互斥锁保护目标转速变量,避免并发访问的问题。

这样改了之后,指令处理任务不再被风扇调速阻塞,收到指令后设置目标转速就立即返回,然后继续处理状态上报等后续逻辑。风扇的渐变在后台任务里慢慢执行,不影响指令的响应速度。

这一个优化,就把响应时间降了将近800毫秒,效果非常明显。

这个问题给我的教训是:在嵌入式系统中,绝对不能在关键任务里做阻塞式的耗时操作,特别是那种需要渐变、过渡的操作,一定要放到后台任务里异步执行。否则不仅会影响当前指令的响应,还会阻塞后续指令的处理,导致系统越来越卡。

问题二:任务优先级和调度问题

第二个大问题是任务优先级和调度问题,这也是"其他"那670毫秒的主要来源。

我分析了系统的任务设计,发现原来的任务划分和优先级设置很不合理。系统里总共有大概10个任务,包括:指令处理任务、传感器采集任务、风扇控制任务、显示任务、按键任务、WiFi通信任务、状态上报任务、日志任务等。

问题主要有几个:

第一,指令处理任务的优先级太低了。原来的开发者把指令处理任务的优先级设得比较低,比传感器采集任务、显示任务都低。这就导致当有其他高优先级任务在运行的时候,指令处理任务要等很久才能被调度执行。特别是传感器采集任务,每秒钟都要采集好几次数据,每次采集都要花几十毫秒,而且优先级比指令处理任务高,经常把指令处理任务抢占了。

第二,任务太多太碎,上下文切换开销大。原来的设计把很多功能都拆成了独立的任务,比如显示一个任务、按键一个任务、日志一个任务、状态上报一个任务等。每个任务都有自己的栈空间,都要参与调度,上下文切换的开销很大。而且很多任务大部分时间都在等待,浪费了调度资源。

第三,没有合理使用事件驱动。很多任务是用轮询的方式实现的,比如显示任务每100毫秒刷新一次屏幕,按键任务每20毫秒扫描一次按键,状态上报任务每5秒上报一次状态。轮询的方式效率很低,很多时候是在空转。应该用事件驱动的方式,有事件的时候才处理,没事件的时候就阻塞等待。

针对这些问题,我做了以下优化:

第一,调整任务优先级。把指令处理任务和WiFi通信任务的优先级提高,确保用户指令能够得到及时响应。传感器采集任务的优先级适当降低,因为传感器数据晚几十毫秒采集根本不影响用户体验。显示任务和按键任务的优先级设为中等。

第二,合并任务。把一些相关性强、耗时短的任务合并到一起,减少任务数量,降低上下文切换的开销。比如把显示任务和按键任务合并成一个UI任务,把状态上报任务合并到指令处理任务里,把日志任务改成异步队列的方式。任务数量从10个减少到了6个,调度开销明显降低。

第三,改用事件驱动。把轮询方式改成事件驱动,用FreeRTOS的队列、信号量、事件组等机制来实现任务间的通信和同步。比如按键任务改成外部中断+事件通知的方式,有按键按下的时候才处理,平时阻塞等待。显示任务改成收到显示更新事件的时候才刷新屏幕,平时不轮询。这样大部分任务大部分时间都在阻塞等待,只有有事件的时候才被唤醒执行,系统的响应性和效率都大大提高。

这些优化做完之后,任务调度的等待时间从670毫秒降到了不到100毫秒,效果也很明显。

这个问题给我的教训是:嵌入式系统的任务设计非常重要,任务数量不是越多越好,优先级不是随便设的。要根据业务的特点,合理划分任务,设置优先级,尽量使用事件驱动而不是轮询。任务设计得好,系统的响应性和效率都会大大提高;任务设计得不好,再怎么优化代码也没用。

问题三:WiFi模块通信效率低

第三个问题是WiFi模块的通信效率低,这部分占了大约500毫秒(接收200毫秒+发送300毫秒)。

原来的WiFi通信方式是这样的:ESP8266WiFi模块通过UART串口和MCU通信,用的是AT指令的方式。MCU要发送数据的时候,先发送AT指令设置发送长度,等待WiFi模块返回"OK",然后再发送数据,等待WiFi模块返回"SEND OK"。接收数据的时候,WiFi模块收到数据后通过串口发送"+IPD"提示,MCU再发送AT指令读取数据。

这种AT指令的方式效率很低,主要有几个问题:

第一,串口波特率太低。原来的串口波特率是9600,传输速度很慢,传输一个几百字节的数据包就要花几百毫秒。后来虽然改成了115200,但AT指令的交互方式本身就有很多等待和开销。

第二,AT指令交互的往返次数太多。发送一次数据,要经过"发送AT指令→等待OK→发送数据→等待SEND OK"四次交互,每次交互都有等待时间。接收数据也要经过"收到+IPD→发送读取指令→等待数据返回"多次交互。这些等待时间加起来就很多了。

第三,没有使用透传模式。ESP8266支持透传模式(Passthrough Mode),在透传模式下,MCU和WiFi模块之间就像串口直连一样,MCU发送什么数据,WiFi模块就直接转发到网络上,网络上收到什么数据,WiFi模块就直接转发给MCU,不需要AT指令的交互。透传模式的效率比AT指令模式高很多。

针对这些问题,我做了以下优化:

第一,提高串口波特率。把串口波特率从9600提高到921600(ESP8266支持的最高波特率),传输速度大大提高。传输一个几百字节的数据包从原来的几百毫秒降到了几毫秒。

第二,改用透传模式。把ESP8266配置成透传模式,MCU和WiFi模块之间直接传输数据,不需要AT指令的交互。这样发送数据的时候,MCU直接把数据写到串口就行,不需要等待OK和SEND OK。接收数据的时候,WiFi模块直接把数据转发到串口,MCU通过串口中断接收,不需要发送读取指令。通信的效率大大提高。

第三,优化数据格式。原来的数据包格式比较冗余,有很多不必要的字段和校验。我重新设计了数据格式,精简了字段,用更紧凑的二进制格式代替了原来的文本格式,数据包的大小减少了将近一半。传输的数据量小了,传输时间自然就短了。

第四,增加接收缓冲区。在MCU端增加了一个环形缓冲区,串口收到数据后先放到缓冲区里,指令处理任务从缓冲区里读取数据处理。这样即使数据来得比较快,也不会丢失,而且指令处理任务可以批量处理数据,提高效率。

这些优化做完之后,WiFi通信的时间从500毫秒降到了不到100毫秒,效果非常明显。

这个问题给我的教训是:在IoT设备中,WiFi通信的效率对响应时间影响很大,不能想当然地用默认的AT指令模式。要根据设备的特点,选择合适的通信模式(透传模式通常效率更高),合理设置串口波特率,优化数据格式,才能提高通信效率,降低响应时间。

问题四:传感器采集的阻塞和耗时

第四个问题是传感器采集的阻塞和耗时,这部分虽然不是响应时间的主要瓶颈,但也影响了系统的整体性能和稳定性。

原来的传感器采集方式是这样的:在传感器采集任务里,依次读取各个传感器的数据,每个传感器的读取都是阻塞式的,读完一个再读下一个。比如读PM2.5传感器,发送读取指令后要等500毫秒才能读到数据;读甲醛传感器,要等300毫秒;读温湿度传感器,要等100毫秒。所有传感器都读一遍,要花将近1秒的时间。

而且,传感器采集任务的优先级原来设得比较高(前面提到过),在这1秒的采集时间里,它会一直占用CPU,其他低优先级的任务(包括指令处理任务)都没法执行。这也是导致指令响应慢的一个原因。

针对这个问题,我做了以下优化:

第一,把阻塞式读取改成非阻塞式。每个传感器的读取都改成状态机的方式,发送读取指令后不等待,而是记录当前状态,然后任务延时或者切换到其他传感器,等时间到了再回来读取数据。这样在等待传感器响应的时间里,CPU可以去做其他事情,不会被阻塞。

第二,合理安排传感器采集的时序。不同传感器的响应时间不一样,不需要都同步采集。可以把响应时间长的传感器(比如PM2.5)放在前面启动,然后在等待它响应的时间里去采集响应时间短的传感器(比如温湿度),等所有传感器都采集完了,再统一处理数据。这样可以用流水线的方式,大大缩短总的采集时间。

第三,降低传感器采集频率。原来的传感器采集频率是1秒一次,实际上空气净化器的空气质量变化没有那么快,2秒甚至5秒采集一次完全够用。降低采集频率后,传感器采集任务占用CPU的时间大大减少,系统的整体负载降低了,其他任务的响应性也提高了。

第四,降低传感器采集任务的优先级。如前所述,把传感器采集任务的优先级降到比指令处理任务低,确保用户指令能够得到及时响应。传感器数据晚几百毫秒采集,用户根本感觉不到,但指令响应慢几百毫秒,用户就能明显感觉到。

这些优化做完之后,传感器采集对系统性能的影响大大降低,虽然对指令响应时间的直接提升不如前面几个优化那么明显,但提高了系统的整体稳定性和响应性。

这个问题给我的教训是:在嵌入式系统中,传感器采集往往是耗时操作,而且很多传感器的读取都是阻塞式的。一定要用非阻塞的方式实现,合理安排采集时序,不要让传感器采集阻塞了其他更重要的任务。同时要根据业务的实际需求,合理设置采集频率,不要为了"实时"而浪费系统资源。

问题五:内存和数据结构的优化

除了上面几个主要问题,我还做了一些内存和数据结构的优化,虽然对响应时间的直接提升不大,但提高了系统的稳定性和可维护性。

第一个优化是减少内存拷贝。原来的代码里有很多不必要的内存拷贝,比如收到WiFi数据后,先拷贝到一个缓冲区,再从缓冲区拷贝到另一个缓冲区解析,解析完再拷贝到指令结构体里。多次内存拷贝不仅浪费CPU时间,还增加了内存占用,而且容易出bug。

我重新设计了数据处理的流程,尽量使用指针操作,减少不必要的内存拷贝。比如收到WiFi数据后,直接在接收缓冲区里解析,解析结果用指针指向缓冲区里的对应位置,不需要再拷贝一份。这样既提高了效率,又减少了内存占用。

第二个优化是使用静态分配而不是动态分配。原来的代码里有些地方用了malloc/free动态分配内存,比如收到指令后动态分配一个指令结构体,处理完再free。在嵌入式系统中,动态分配内存有很多问题:一是容易产生内存碎片,时间长了可能导致内存分配失败;二是malloc/free的执行时间不确定,可能影响系统的实时性;三是容易出现内存泄漏,忘记free就会导致内存越来越少。

我把所有的动态分配都改成了静态分配,用全局变量或者静态变量预分配内存,用内存池的方式管理。这样内存的使用是确定的,不会产生碎片,不会泄漏,执行时间也确定,系统的稳定性大大提高。

第三个优化是优化数据结构。原来的代码里有些数据结构设计得不合理,比如用数组加线性查找的方式管理设备状态,状态多了之后查找效率很低。我根据数据的特点,改用了更合适的数据结构,比如用哈希表管理键值对,用链表管理动态列表,用位图管理状态标志等。合适的数据结构可以大大提高数据处理的效率,减少CPU时间。

第四个优化是减少字符串操作。原来的代码里有很多字符串操作,比如用sprintf格式化数据,用strcpy/strcat拼接字符串,用strcmp比较字符串等。字符串操作通常比较耗时,特别是在MCU这种算力有限的平台上。我尽量用二进制格式代替字符串格式,用整数操作代替字符串操作,减少了不必要的字符串处理,提高了效率。

这些优化虽然对响应时间的直接提升不如前面几个大,但提高了系统的整体效率和稳定性,为后续的功能扩展打下了好的基础。

优化效果和总结

经过两周的优化,最终的效果是:

  • 平均响应时间从2.3秒降到了420毫秒,降了80%多
  • 最大响应时间从6秒降到了600毫秒以内
  • 系统CPU占用率从原来的70%多降到了30%多
  • 内存占用减少了约20%
  • 系统稳定性大大提高,没有再出现死机或者卡顿的情况

用户测试之后反馈很好,说设备响应快多了,体验好了很多。领导也很满意,说优化达到了预期目标。

回顾这次性能调优的经历,我总结了几点经验:

第一,性能优化一定要先做分析,不能凭感觉。最开始我以为瓶颈在WiFi通信或者数据解析,结果分析发现最大的瓶颈居然是风扇调速的阻塞延时。如果没有做性能分析,直接去优化WiFi或者解析,可能花了很多时间效果也不好。一定要用数据说话,找到真正的瓶颈,然后针对性地优化,才能事半功倍。

第二,嵌入式系统的优化重点和服务器端不一样。服务器端的优化重点可能在算法、数据库、缓存、并发等方面,而嵌入式系统的优化重点往往在任务调度、阻塞操作、外设通信、内存管理等方面。特别是阻塞操作,在嵌入式系统中危害很大,一个地方的阻塞可能会导致整个系统的响应都变慢。一定要尽量避免在关键任务里做阻塞操作,用异步、事件驱动的方式来设计。

第三,任务设计是嵌入式系统的关键。这次优化里,任务优先级的调整和任务的合并对性能的提升非常大。嵌入式系统的任务不是越多越好,优先级也不是随便设的。要根据业务的特点,合理划分任务,设置优先级,尽量使用事件驱动。任务设计得好,系统的响应性和效率都会大大提高;任务设计得不好,再怎么优化代码也没用。

第四,外设通信的效率很容易被忽视。这次WiFi通信的优化对响应时间的提升也很大。很多嵌入式开发者可能更关注MCU端的代码优化,而忽视了外设通信的效率。但实际上,外设通信(特别是WiFi、蓝牙等无线通信)往往是响应时间的重要组成部分。要合理选择通信模式,设置通信参数,优化数据格式,才能提高通信效率。

第五,优化要循序渐进,每一步都要测试。性能优化是一个迭代的过程,不能指望一次就把所有问题都解决。我是先优化最严重的瓶颈(风扇阻塞),测试效果,然后再优化下一个瓶颈(任务调度),再测试,这样一步步来的。每优化一步都要测试效果,确认优化有效而且没有引入新的问题,再继续下一步。这样风险可控,也能清楚地看到每个优化的效果。

第六,优化要适度,不要过度优化。性能优化是有成本的,优化的代码往往更复杂,更难维护。不能为了追求极致的性能而把代码搞得非常复杂,难以理解和维护。要在性能和可维护性之间找到平衡。这次优化我也没有追求极致,达到了领导要求的500毫秒以内的目标就可以了,没有继续花时间去抠那几十毫秒。毕竟,代码的可维护性和稳定性也很重要。

写在最后

这次空气净化器的性能调优,虽然花了两周时间,踩了不少坑,但收获也很大。

我对嵌入式系统的性能优化有了更深入的理解,对FreeRTOS的任务调度、外设通信、内存管理等有了更实际的经验。也更加认识到,嵌入式开发不仅仅是写代码实现功能,更重要的是系统设计和性能优化。一个功能实现了但性能很差、响应很慢的产品,用户体验是很差的,也不算真正的成功。

在IoT设备越来越普及的今天,用户对设备的响应速度和体验要求越来越高。作为嵌入式开发者,我们不能只关注功能的实现,还要关注性能和体验。要把性能优化作为开发过程中的重要环节,从系统设计阶段就考虑性能问题,而不是等用户反馈慢了才去优化。

希望这篇文章能给做嵌入式开发或者IoT开发的朋友一些参考和启发。如果你也有类似的性能优化经历,或者有更好的优化方法,欢迎在评论区交流,一起学习,一起进步。

性能优化是一条没有终点的路,我们一直在路上。