最近接手了一个智能体脂秤的项目,用户反馈称体重的时候响应太慢,要等好几秒才出结果。我花了两周时间,对体脂秤的固件和APP做了性能调优,把响应时间从5秒降到了1秒以内,降了80%。本文分享这次性能调优的过程和经验,包括性能瓶颈分析、固件优化、蓝牙通信优化、APP优化、以及嵌入式设备性能调优的通用方法,希望对做IoT和嵌入式开发的同学有参考价值。

一、项目背景

这个智能体脂秤,是公司的一款智能家居产品。它的功能很简单:用户站上去,测量体重和体脂率,然后通过蓝牙把数据传到手机APP上,APP显示结果并记录历史数据。

产品上线之后,用户反馈最多的问题就是:响应太慢。用户站上去之后,要等好几秒,APP上才显示体重和体脂率。有些用户甚至以为秤坏了,站了几秒没反应就下来了。

这个问题,影响了用户体验,也影响了产品的口碑。领导让我负责优化这个问题,目标是把响应时间降到2秒以内。

我先测了一下,从用户站上去,到APP显示结果,平均需要5秒左右,最慢的时候甚至要7-8秒。这个速度,确实太慢了。

于是,我开始了这次性能调优。

二、性能瓶颈分析

调优的第一步,不是上来就改代码,而是先分析性能瓶颈,找到慢的原因。

我把整个测量流程,拆分成了几个阶段,然后分别测量每个阶段的耗时:

  1. 传感器采样阶段:用户站上去之后,体重传感器和体脂传感器采集数据,需要多长时间?
  2. 数据计算阶段:传感器采集到原始数据之后,固件计算体重和体脂率,需要多长时间?
  3. 蓝牙连接阶段:秤和手机APP建立蓝牙连接,需要多长时间?
  4. 数据传输阶段:秤把计算结果通过蓝牙传到APP,需要多长时间?
  5. APP处理阶段:APP收到数据之后,处理和显示,需要多长时间?

我在固件和APP里都加了时间戳,把每个阶段的开始和结束时间都记录下来,然后测了几十次,取平均值。

结果发现:

阶段平均耗时占比
传感器采样1.5秒30%
数据计算0.2秒4%
蓝牙连接2.5秒50%
数据传输0.3秒6%
APP处理0.5秒10%
总计5.0秒100%

一目了然,最大的瓶颈是蓝牙连接,占了50%的时间,平均2.5秒。其次是传感器采样,占了30%,平均1.5秒。这两个阶段加起来,占了80%的时间。

数据计算、数据传输、APP处理,这三个阶段加起来才1秒,不是主要瓶颈。

找到了瓶颈,接下来就是针对性地优化。

三、蓝牙连接优化

蓝牙连接是最大的瓶颈,平均2.5秒。我先分析了一下,为什么蓝牙连接这么慢。

问题分析:

原来的蓝牙连接流程是这样的:

  1. 用户站上去,秤检测到重量,开始广播蓝牙信号
  2. APP扫描到秤的广播信号,发起连接
  3. 连接建立之后,进行服务发现(Service Discovery),查找秤提供的服务和特征值
  4. 然后进行特征值的读取和通知订阅
  5. 最后传输数据

这个流程,每一步都需要时间。尤其是服务发现,要遍历所有的服务和特征值,比较慢。

而且,原来的代码里,APP是在用户站上去之后,才开始扫描和连接的。也就是说,用户站上去之后,才开始整个蓝牙连接流程,这2.5秒的连接时间,用户都在等。

优化方案:

我做了几个优化:

优化1:预连接(Pre-connect)

最大的优化,是把蓝牙连接提前。不要等用户站上去之后才开始连接,而是在APP打开的时候,就尝试和秤建立连接。

具体做法:

  • APP打开的时候,自动扫描并连接附近的秤
  • 连接成功之后,保持连接(或者保持一个低功耗的连接状态)
  • 用户站上去的时候,秤直接通过已经建立的连接传输数据,不需要再重新连接

这样,蓝牙连接的时间,就从用户等待的时间里,移到了APP打开的时候。用户站上去的时候,连接已经建立好了,直接传数据就行。

当然,这个优化有一些细节要处理:

  • 连接保持会增加功耗,需要在功耗和响应速度之间权衡。可以用低功耗的连接参数(更长的连接间隔),保持连接但功耗不高
  • 如果APP打开的时候,秤不在附近(比如用户不在家),连接会失败,这时候要处理连接失败的情况,用户站上去的时候再重新连接
  • 如果有多个秤,要处理连接哪个的问题,可以让用户选择默认的秤

即使有这些细节问题,预连接还是带来了巨大的提升。大部分情况下,用户打开APP之后,秤已经连接好了,站上去直接出结果。

优化2:优化服务发现

服务发现(Service Discovery)是蓝牙连接中比较慢的一步。原来的代码里,连接建立之后,会遍历所有的服务和特征值,这比较慢。

优化方法:

  • 我们的秤,只提供一个自定义服务,服务里只有几个特征值(体重、体脂率、电量等)
  • 不需要遍历所有服务,直接用我们知道的服务UUID和特征值UUID,直接访问
  • 跳过通用的服务发现流程,直接用已知的UUID操作

这样,服务发现的时间,从原来的1秒左右,降到了几乎可以忽略。

当然,这种优化只适用于我们自己的设备,因为我们知道服务和特征值的UUID。如果是通用的蓝牙工具,还是需要服务发现。

优化3:优化连接参数

蓝牙连接的参数(连接间隔、从设备延迟、超时时间),会影响连接的响应速度和功耗。

原来的连接参数,是比较保守的,连接间隔比较长(100ms),响应比较慢,但功耗低。

我调整了连接参数:

  • 连接间隔从100ms降到了30ms,响应更快
  • 从设备延迟设为0,确保数据能及时传输
  • 超时时间保持合理的值,避免连接断了之后长时间不重连

当然,连接间隔缩短会增加功耗。我做了一个动态调整:在传输数据的时候,用短的连接间隔(30ms),响应快;传输完成之后,切换到长的连接间隔(100ms),降低功耗。

这样,既保证了响应速度,又控制了功耗。

优化4:优化重连机制

如果蓝牙连接断了(比如用户离开了一会儿,或者蓝牙干扰),重连的速度也很重要。

原来的重连机制,是连接断了之后,先扫描,再连接,比较慢。

优化之后:

  • 连接断了之后,直接用之前的设备地址发起连接,不需要重新扫描
  • 用自动重连参数(autoConnect=true),让系统自动重连
  • 重连的时候,直接用已知的服务和特征值,跳过服务发现

这样,重连的时间,从原来的3-4秒,降到了1秒以内。

蓝牙优化效果:

经过这几个优化,蓝牙连接的时间,从原来的2.5秒,降到了:

  • 预连接成功的情况下:几乎为0(连接已经建立好了)
  • 需要重新连接的情况下:0.5-1秒

平均下来,蓝牙连接的时间,从2.5秒降到了0.5秒左右,降了80%。

四、传感器采样优化

传感器采样是第二大瓶颈,平均1.5秒。我分析了一下,为什么采样需要这么长时间。

问题分析:

体脂秤的传感器采样,包括两部分:

  1. 体重采样:体重传感器(压力传感器)采集体重数据,需要一段时间稳定
  2. 体脂采样:体脂传感器(生物电阻抗)采集体脂数据,需要通入微弱电流,测量电阻抗

原来的采样流程:

  1. 用户站上去,检测到重量
  2. 等待体重传感器稳定(0.5秒)
  3. 采集体重数据,采样10次,取平均值(0.5秒)
  4. 启动体脂测量,通入电流,等待稳定(0.3秒)
  5. 采集体脂数据,采样5次,取平均值(0.2秒)
  6. 总共1.5秒

这个流程,是串行的,体重采样完成之后,才开始体脂采样。而且,采样次数比较多,等待稳定的时间也比较长。

优化方案:

我做了几个优化:

优化1:体重和体脂并行采样

最大的优化,是把体重采样和体脂采样并行化。

原来的流程是串行的:先采体重,再采体脂。但实际上,体重传感器和体脂传感器是独立的,可以同时工作。

优化之后:

  1. 用户站上去,检测到重量
  2. 同时启动体重采样和体脂采样
  3. 体重传感器稳定后,开始采集体重数据
  4. 体脂传感器稳定后,开始采集体脂数据
  5. 两个都完成之后,进行计算

这样,体重采样和体脂采样并行进行,总时间就是两者中较长的那个,而不是两者之和。

体重采样需要1.0秒,体脂采样需要0.5秒,并行之后,总时间就是1.0秒,而不是1.5秒。

优化2:减少采样次数,用算法优化

原来的体重采样,采样10次取平均值;体脂采样,采样5次取平均值。采样次数多,是为了让数据更稳定、更准确。

但实际上,传感器的数据,在稳定之后,波动是很小的。不需要采样那么多次,用更少的采样次数,加上合适的滤波算法,就能达到同样的精度。

我做了优化:

  • 体重采样:从10次降到5次,用滑动平均滤波
  • 体脂采样:从5次降到3次,用中值滤波

采样次数减少了,采样时间自然就短了。而且,用了滤波算法之后,数据的稳定性和精度,和原来差不多,甚至更好。

当然,减少采样次数,要验证精度。我做了对比测试,用标准砝码测试体重精度,用标准体脂模体测试体脂精度,确认优化之后的精度,仍然在产品要求的范围内。

优化3:优化稳定检测算法

原来的稳定检测,是固定等待0.5秒,不管传感器有没有稳定,都等0.5秒。但实际上,有时候传感器0.2秒就稳定了,有时候0.8秒才稳定。固定等待0.5秒,快的时候浪费时间,慢的时候可能还没稳定。

我优化了稳定检测算法:

  • 实时监测传感器数据的波动
  • 当连续N个数据点的波动小于阈值时,认为已经稳定
  • 稳定了就立刻开始采样,不需要固定等待

这样,稳定快的时候,立刻开始采样,节省时间;稳定慢的时候,多等一会儿,确保数据准确。

平均下来,稳定检测的时间,从固定的0.5秒,降到了平均0.3秒。

优化4:提前启动体脂测量

体脂测量,需要通入微弱电流,等待电路稳定。原来的流程,是体重采样完成之后,才启动体脂测量。

优化之后,在检测到用户站上去的时候,就立刻启动体脂测量的电路,让电路提前稳定。等体重采样完成(或者并行采样到一定阶段),体脂电路已经稳定了,可以直接采集数据。

这样,体脂测量的启动时间,就和体重采样的时间重叠了,不需要额外等待。

传感器优化效果:

经过这几个优化,传感器采样的时间,从原来的1.5秒,降到了0.7秒左右,降了50%以上。

而且,我做了精度验证,优化之后的体重和体脂精度,和原来差不多,都在产品要求的范围内。也就是说,我们在不牺牲精度的前提下,把采样时间降了一半。

五、其他优化

除了蓝牙连接和传感器采样这两个主要瓶颈,我还对其他几个阶段做了一些小优化。

数据计算优化:

原来的体脂率计算,用的是一个比较复杂的公式,涉及到很多浮点运算。在8位单片机上,浮点运算比较慢。

我做了优化:

  • 把浮点运算改成定点运算,用整数代替浮点数,速度快很多
  • 把一些常数提前计算好,存在Flash里,不需要每次都算
  • 优化计算公式,去掉了一些不必要的计算步骤

数据计算的时间,从原来的0.2秒,降到了0.05秒。虽然这个阶段本来就不是瓶颈,但优化之后,总时间又少了一点。

数据传输优化:

原来的数据传输,是分多个包传输的,体重一个包,体脂率一个包,电量一个包,每个包都要等确认。

优化之后,把所有数据打包成一个包,一次传输完成。而且,用了蓝牙的Write Without Response(不需要确认的写入),传输更快。

数据传输的时间,从原来的0.3秒,降到了0.1秒。

APP处理优化:

APP收到数据之后,原来的处理流程比较复杂:

  1. 解析蓝牙数据
  2. 转换数据格式
  3. 写入本地数据库
  4. 上传到服务器
  5. 更新UI显示

其中,写入数据库和上传服务器,是同步执行的,会阻塞UI线程,导致显示慢。

优化之后:

  • 解析和转换数据,在主线程做(很快)
  • 写入数据库和上传服务器,放到后台线程异步执行
  • UI显示,在数据解析完成之后立刻更新,不需要等数据库和服务器

这样,APP显示结果的时间,从原来的0.5秒,降到了0.1秒。用户感觉就是,数据一到,立刻就显示出来了。

六、优化效果

经过两周的调优,各个阶段的耗时,变成了这样:

阶段优化前优化后提升
传感器采样1.5秒0.7秒53%
数据计算0.2秒0.05秒75%
蓝牙连接2.5秒0.5秒80%
数据传输0.3秒0.1秒67%
APP处理0.5秒0.1秒80%
总计5.0秒1.45秒71%

平均响应时间,从5秒降到了1.45秒,降了71%。在预连接成功的情况下(大部分情况),响应时间甚至能降到1秒以内,降了80%。

这个结果,达到了领导要求的"2秒以内"的目标,甚至超出了预期。

用户体验的提升也很明显。用户站上去,几乎立刻就能看到体重和体脂率,不需要再等好几秒了。用户反馈里,关于"响应慢"的投诉,几乎消失了。

而且,这些优化,都是在不牺牲精度和功耗的前提下完成的。体重和体脂的精度,和原来一样;秤的电池续航,也没有明显下降。

七、性能调优的通用方法

这次体脂秤的性能调优,让我总结了一些嵌入式设备和IoT产品性能调优的通用方法。

1. 先测量,再优化

这是最重要的一条。不要上来就改代码,先测量,找到性能瓶颈,再有针对性地优化。

测量的方法:

  • 把整个流程拆分成多个阶段
  • 在每个阶段的开始和结束,加时间戳
  • 多次测量,取平均值
  • 找到耗时最长的阶段,就是主要瓶颈

找到了瓶颈,再优化,效率最高。如果盲目优化,可能花了很多时间,优化了一个不是瓶颈的地方,整体效果不大。

2. 并行化,不要串行

很多嵌入式设备的性能问题,是因为流程是串行的。能并行的地方,尽量并行。

比如,这次体脂秤的优化,体重采样和体脂采样并行,就节省了很多时间。

在嵌入式开发中,常见的并行化方法:

  • 用DMA(直接内存访问),让数据传输和CPU计算并行
  • 用中断,让外设工作和CPU处理并行
  • 用多任务/多线程,让不同的任务并行
  • 提前启动后续步骤,让后续步骤的准备工作和当前步骤并行

当然,并行化要注意同步和竞态条件,确保数据的正确性。

3. 减少不必要的工作

很多性能问题,是因为做了不必要的工作。把不必要的工作去掉,性能自然就提升了。

比如,这次优化中:

  • 减少了采样次数,因为不需要那么多次
  • 跳过了通用的服务发现,因为我们知道服务和特征值的UUID
  • 去掉了计算公式中不必要的步骤

减少不必要的工作,是最简单、最有效的优化方法。在优化之前,先问问自己:这一步是必须的吗?能不能去掉?能不能简化?

4. 用空间换时间,用时间换空间

性能优化中,经常需要在时间和空间之间权衡。

用空间换时间的例子:

  • 把计算结果缓存起来,下次直接用,不需要重新计算
  • 把常数提前计算好,存在Flash里,不需要每次都算
  • 用查表法代替计算,用空间换时间

用时间换空间的例子:

  • 数据压缩,用计算时间换存储空间
  • 延迟计算,需要的时候才算,用时间换内存

在嵌入式设备中,内存和Flash空间通常比较紧张,所以要特别注意权衡。但如果空间够用,用空间换时间,通常是很有效的优化方法。

5. 优化算法,而不只是优化代码

很多性能问题,根本原因是算法不好。这时候,再怎么优化代码,效果也有限。要从算法层面优化。

比如,这次体脂秤的优化中,稳定检测算法的优化,就是算法层面的优化。从固定等待,变成自适应检测,既提升了速度,又保证了精度。

在嵌入式开发中,常见的算法优化:

  • 用更高效的滤波算法(如卡尔曼滤波),减少采样次数
  • 用更高效的数值计算方法(如快速傅里叶变换),减少计算量
  • 用更高效的数据结构(如哈希表),减少查找时间
  • 用近似算法,在可接受的精度范围内,大幅提升速度

算法优化,通常能带来数量级的性能提升,比单纯优化代码有效得多。

6. 关注用户感知的性能,而不只是客观指标

性能优化,最终目的是提升用户体验。所以,要关注用户感知的性能,而不只是客观的技术指标。

比如,这次优化中,预连接把蓝牙连接时间,从用户等待的时间里,移到了APP打开的时候。客观上,蓝牙连接的时间没有减少(还是需要那么多时间),但用户感知的响应时间,大大减少了。因为用户不需要等了。

类似的方法:

  • 提前加载,让用户感觉更快
  • 先显示主要内容,再加载次要内容
  • 加loading动画,让等待不那么难熬
  • 乐观更新,先显示结果,后台再同步

用户感知的性能,有时候比客观的性能更重要。优化的时候,要多站在用户的角度想:用户感觉快了吗?用户体验好了吗?

八、调优过程中的坑

在这次调优过程中,我也踩了一些坑,分享给大家。

坑1:优化了精度,结果精度下降了

最开始,我减少了采样次数,结果发现体重和体脂的精度下降了,尤其是在用户站得不稳的时候,数据波动比较大。

后来,我加了滤波算法(滑动平均和中值滤波),才把精度拉回来。

教训:性能优化,不能以牺牲功能和精度为代价。优化之后,一定要做充分的测试,验证功能和精度没有下降。

坑2:预连接导致功耗增加

预连接确实提升了响应速度,但也导致了功耗增加。APP一直保持蓝牙连接,手机的蓝牙一直工作,耗电增加了。

后来,我做了动态连接参数管理,传输数据的时候用短连接间隔,空闲的时候用长连接间隔,才把功耗降下来。

教训:性能优化,要权衡各个方面。响应速度、功耗、精度、稳定性,这些都要兼顾,不能顾此失彼。

坑3:并行采样导致数据冲突

体重采样和体脂采样并行的时候,最开始出现了数据冲突的问题。两个传感器同时工作,互相干扰,导致数据不准确。

后来,我仔细看了硬件设计,发现体重传感器和体脂传感器用的是不同的ADC(模数转换器),理论上不会互相干扰。问题出在软件上,两个采样任务共用了一个缓冲区,导致数据覆盖。

我给两个采样任务分配了独立的缓冲区,问题就解决了。

教训:并行化的时候,要特别注意数据同步和竞态条件。确保并行的任务之间,不会互相干扰,数据不会冲突。

坑4:Write Without Response导致数据丢失

优化蓝牙数据传输的时候,我用了Write Without Response(不需要确认的写入),速度确实快了。但测试发现,偶尔会出现数据丢失的情况,APP收不到数据。

原因是,Write Without Response不需要确认,如果蓝牙连接不稳定,数据可能会丢。

后来,我做了一个折中:关键数据(体重和体脂率)用需要确认的写入,确保不丢;非关键数据(比如电量)用不需要确认的写入,提升速度。

教训:性能优化,不能牺牲可靠性。速度和可靠性之间,要找到平衡。关键数据,一定要确保可靠;非关键数据,可以追求速度。

九、写在最后

这次体脂秤的性能调优,是一次很有意义的经历。它让我从一个纯软件工程师,深入到了嵌入式和IoT领域,了解了硬件、固件、蓝牙、APP的整个链路,也学到了很多性能调优的方法和技巧。

性能调优,是一件很有挑战性,也很有成就感的事情。当你把一个5秒的响应,优化到1秒以内,当你看到用户体验的提升,那种成就感,是写普通代码体会不到的。

但性能调优,也是一件需要耐心和细心的事情。不能盲目优化,要先测量,找到瓶颈;不能急于求成,要一步一步来,每一步都验证;不能顾此失彼,要在速度、精度、功耗、可靠性之间找到平衡。

这次调优,我最大的感悟是:性能优化的本质,是理解系统。只有深入理解了整个系统的工作原理,理解了每个环节的耗时和瓶颈,才能找到最有效的优化方法。如果只是表面地改改代码,效果是有限的。

嵌入式设备和IoT产品的性能调优,和纯软件的性能调优,有很多相似之处,也有很多不同。相似的是,都要先测量、找瓶颈、再优化;不同的是,嵌入式设备受限于硬件资源(CPU、内存、功耗),需要更多地在资源和性能之间权衡。

最后,用一句话结束本文:"性能调优没有银弹,只有深入理解系统,找到瓶颈,针对性地优化,才能取得最好的效果。"希望这次体脂秤性能调优的经验,能对做嵌入式和IoT开发的同学,有一些参考和启发。