首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >数据丢包怎么办?库库插值是良方!(高频流数据版)

数据丢包怎么办?库库插值是良方!(高频流数据版)

作者头像
云深无际
发布2026-07-22 12:31:56
发布2026-07-22 12:31:56
90
举报
文章被收录于专栏:云深之无迹云深之无迹

最近写了不少程序,记录一个软件上面的设计,目前是将解析好的数据通过 USB发送给上位机,其中数据打包的思路是把结构化的多通道数据转换为一维字节流,并加入“帧头和帧尾”以确保上位机能够准确断帧和对齐频道。

固定帧长数据包(Packit)

现在的数据属于连续的高频流数据,如果直接发送 ASCII 字符串(如使用 printf),会因为字符转换效率低、数据量翻倍而导致卡顿。因此,必须发送原始二进制字节(Binary Data)

现在设计的数据包(共 38 个字节):

字节偏移

字段名称

数据类型

说明

0 ~ 1

帧头 (Header)

uint8_t

固定为 0xAA, 0x55(用于上位机对齐查找包头)

2

包计数器 (Counter)

uint8_t

0 ~ 255 循环,用于上位机检测是否丢包

3 ~ 34

通道数据 (Payload)

int32_t × 8

8 个通道的 32 位脑电数据(8 × 4 = 32 字节)

35

校验和 (Checksum)

uint8_t

前 35 字节的累加和,用于检测数据传输错误

36 ~ 37

帧尾 (Tail)

uint8_t

固定为 0x0D, 0x0A (\r\n,符合多数串口软件换行习惯)

利用 C 语言结构体的内存连续性,可以直接打包:

代码语言:javascript
复制
// 强制编译器不进行内存对齐优化,确保结构体大小严格等于 38 字节
#pragma pack(1) 
typedef struct {
    uint8_t header[2];       // 帧头 0xAA, 0x55
    uint8_t packet_cnt;      // 包计数器
    int32_t channels[8];     // 8通道数据 (32字节)
    uint8_t checksum;        // 校验和
    uint8_t tail[2];         // 帧尾 0x0D, 0x0A
} EEG_Packet_t;
#pragma pack()

// 全局变量
EEG_Packet_t TxPacket;
uint8_t global_packet_counter = 0;

这段是数据的容器,要装在这里,接下来看数据的装载:

代码语言:javascript
复制
void Pack_And_Send_EEG(int32_t *parsed_channels)
{
    // 装载帧头和帧尾
    TxPacket.header[0] = 0xAA;
    TxPacket.header[1] = 0x55;
    TxPacket.tail[0]   = 0x0D;
    TxPacket.tail[1]   = 0x0A;
    // 装载计数器并自增
    TxPacket.packet_cnt = global_packet_counter++;
    // 复制解析好的 8 通道 int32_t 数据 (共 32 字节)
    memcpy(TxPacket.channels, parsed_channels, sizeof(TxPacket.channels));
    // 计算校验和 (从 header 到 channels 的所有字节累加)
    uint8_t sum = 0;
    uint8_t *ptr = (uint8_t *)&TxPacket;
    for(int i = 0; i < 35; i++) // 前 35 个字节
    {
        sum += ptr[i];
    }
    TxPacket.checksum = sum;
    /* 串口发送 (阻塞或 DMA 模式均可,这里以阻塞演示) */
    // 参数:串口句柄, 结构体指针强转为uint8_t*, 整个包大小, 超时时间
    HAL_UART_Transmit(&huart1, (uint8_t *)&TxPacket, sizeof(TxPacket), 10);
    /*USB CDC 虚拟串口发送 */
    // 需要包含 #include "usbd_cdc_if.h"
    // CDC_Transmit_FS((uint8_t *)&TxPacket, sizeof(TxPacket));
}
看这个设计
看这个设计

看这个设计

一般也不写这个,但是数据实在太多了,有时候数据会卡死:

包计数器(Packet Counter)在底层的运行机制非常简单,可以用“排队报数”来形容;在MCU内部,它只是一个普通的 uint8_t(8位无符号整型)全局变量,每当单片机成功组装并发送完一个数据包,这个变量的值就 +1,因为 uint8_t 的最大值是 255,当它加到 255 之后,如果再加 1,由于硬件溢出,它会自动重新归零(变成 0);因此,它发送出去的序列看起来是这样的:0, 1, 2, 3 ... 254, 255, 0, 1, 2 ... 循环往复。

上位机在收到当前包的计数器(假设是 N)时,会把它存下来,并与下一个收到的包计数器(假设是 M)进行对比;正常情况是M = N + 1(或者当 N=255 时,M=0),这说明数据传输非常完美,中间没有丢失;如果刚才收到的包是 5,现在收到的包变成了 8,上位机通过简单的减法就可以知道:8 - 5 - 1 = 2,这意味着在传输过程中漏掉了 2 个包

有了计数器上位机可以实时统计丢包率(丢包数 / 总包数),如果发现丢包率过高,系统可以提醒用户“请检查线缆连接”或“降低采样率”。

最重要的是保证时间轴的连续性,大多数波是一种随时间连续变化的,上位机画波形图时,是默认点与点之间的时间间隔是相等的;如果中间丢了 1 个包,而上位机不知道,就会直接把第 3 个包和第 1 个包连起来;反映在屏幕上,就是波形瞬间发生了一个非自然的“突变/抖动”,这会直接毁掉后面的滤波算法和频谱分析(如 FFT 傅里叶变换)。

一个好消息

我们上位机需要考虑这个问题,而且有大量的算力可以解决,但要注意:

丢包补偿只能恢复时间位置,不能恢复真实信号,插值数据必须始终带有“无效/估计”标记,不能伪装成真实采样值。

包计数器

跨越255时仍然成立

例如上一包为254,当前包为1:

因此丢失2包,即255和0。

Python中可以写成:

代码语言:javascript
复制
distance = (current_seq - previous_seq) & 0xFF
lost_packets = distance - 1

这里的 & 0xFF 相当于对256取模。

上面写的是:

每当单片机成功组装并发送完一个数据包,计数器加1。

这句话不够严谨,更严谨的说法是:

每次采集系统生成一个应当发送的数据包时,就为它分配唯一的序号。

例如:

代码语言:javascript
复制
packet.seq = packet_counter++;
enqueue_tx_packet(&packet);

如果 MCU 内部发生以下问题:ADC采集完成,但发送队列已经满了;DMA发送任务来不及处理;某个数据包在 MCU 内部被覆盖;BLE发送接口返回忙,程序直接放弃该包;如果计数器只在“实际发送成功”后递增,上位机看到的序号仍然连续,反而无法发现 MCU 内部已经丢失过采样数据。

8位序号存在歧义

8位序号最多只能表示256种状态。如果一次断开时间太长,丢失超过255包,上位机无法知道究竟循环了多少次。

例如上一包为5,当前包仍为5,可能表示:收到重复包;恰好丢失了255包;设备重新启动;丢失了511包,有条件使用最低16位序号;更推荐32位 sample_index

32位计数器即使采样率为1 kSPS,也可以连续运行约:

如果每个数据包包含多个采样点,持续时间还会更长。

丢包为什么会影响时间轴

假设采样率为:

采样周期:

真实时间应该由采样序号决定:

假设采样序列是:

代码语言:javascript
复制
100、101、102、105、106

说明103、104缺失;如果绘图程序只是把收到的数据依次放在相邻位置:

代码语言:javascript
复制
显示位置:0、1、2、3、4
真实位置:100、101、102、105、106

那么时间轴被压缩了8 ms。后续所有数据都会向前错位,因此真正需要恢复的是时间位置:

代码语言:javascript
复制
100、101、102、缺失、缺失、105、106

这里不一定必须填写一个数值,但必须保留两个缺失位置。

对FFT影响的一点讨论

情况一:丢失位置被删除

如果丢失采样点直接从序列里消失,后面的时间轴被压缩。此时数据已经不再是严格均匀采样:

而普通FFT默认:

因此直接进行FFT,频率和相位分析都会产生误差。

情况二:缺失位置补0

补0可以恢复样本数量和时间位置,但在信号中引入了突然下降到0的边沿。这会产生宽带频谱泄漏,并激励IIR滤波器的瞬态响应。

情况三:前值保持

前值保持通常不会产生立刻跳到0的巨大阶跃,但会形成一段水平信号,并在真实数据恢复时产生跳变。

情况四:线性插值

短缺口下通常比较平滑,但会人为生成一段近似直线,相当于局部低通处理,高频成分会被压低。

补偿方法

既然可以找到哪里缺了数据,那就可以补回来,这里讨论三个方案

前值保持

公式为:

适合实时显示;缺口极短;信号变化相对缓慢;不能等待后一个样本到达;缺点是会产生平台;恢复时可能产生阶跃;高频信号误差较大;不能作为真实测量数据。

线性插值

若两端真实值为 和 ,中间缺失 个点,则第 个插值为:

例如缺失两个点:

则插值分别为:

适合:短缺口;平滑显示;低频趋势恢复;离线处理或允许等待下一包的实时显示;但插值值是估算,不是真实测量;这个缺口越长,可信度越低;肌电等高频、随机性较强的信号不适合长区间插值;可能人为减少功率谱中的高频能量;可能制造不存在的斜坡,插值结果最好保留浮点数,避免额外量化,只有在最后输出到整数格式时才转换。

NaN加质量标记

这是保存和科研分析中最可靠的基础方法:

代码语言:javascript
复制
data[missing_position] = np.nan
valid[missing_position] = False

明确:

这里没有测量数据。

有了 NaN,不能简单把NaN直接送进IIR滤波器,如果把 NaN 直接送入普通 IIR 滤波器:

代码语言:javascript
复制
y = filter.process(np.nan)

由于IIR滤波器有反馈状态,NaN可能进入内部状态,之后所有输出都变成NaN。

IIR滤波器一般形式:

一旦历史项中出现NaN,后续计算会持续受到污染。

可以采用分级补偿策略

不应该对所有长度的缺口都使用同一种插值,假设缺失时间为:

可以根据任务设置策略,如:

缺口

实时显示

原始保存

频谱/特征分析

1~2点

线性插值

NaN+无效标记

可酌情使用,但保留标记

3~10点

插值但标色

NaN+无效标记

通常排除该区域

较长缺口

显示断线

NaN+无效标记

分段分析

连接中断

新建数据段

记录事件

不跨段滤波/FFT

详细模拟

我们使用Python 模拟插值,以及研究缺失数据在频域会有什么影响,展示的时候使用分辨率高的 FFT。

仿真设置

代码语言:javascript
复制
采样点数              : 92,160
缺失样本数/比例       : 792 / 0.8594%
记录长度理论频率间隔   : 1/T = 0.005556 Hz
Hann 窗 ENBW          : 1.5/T = 0.008333 Hz
零填充 FFT 栅格间距    : fs/NFFT = 0.000488 Hz
注意:更细的 FFT 栅格不是更强的真实分辨能力;真实分辨率主要靠延长 T。

10 Hz附近主要谱峰:
Reference : [(10.123046875, -0.005393100133749951), (10.1728515625, -5.18754105913096), (10.10986328125, -31.439126135762386), (10.13623046875, -31.494161173770728)]
Zero fill : [(10.123046875, -0.06699910014441023), (10.1728515625, -5.216832638381734), (10.10986328125, -31.412882691863118), (10.13623046875, -32.08808645213892)]
ZOH       : [(10.123046875, -0.03559907459472774), (10.1728515625, -5.205575543568598), (10.10986328125, -31.199437020777808), (10.13623046875, -31.503423386907578)]
Linear    : [(10.123046875, -0.03330143431458907), (10.1728515625, -5.193685957757536), (10.10986328125, -31.188468711646827), (10.13623046875, -31.69564251215458)]
Deleted   : [(10.126953125, -5.586233149229062), (10.18359375, -6.546209263501059), (10.154296875, -14.058907252136022), (10.24755859375, -14.16637288270421)]

采样点数              : 92,160
缺失样本数/比例       : 792 / 0.8594%
记录长度理论频率间隔   : 1/T = 0.005556 Hz
Hann 窗 ENBW          : 1.5/T = 0.008333 Hz
零填充 FFT 栅格间距    : fs/NFFT = 0.000488 Hz
注意:更细的 FFT 栅格不是更强的真实分辨能力;真实分辨率主要靠延长 T。

10 Hz附近主要谱峰:
Reference : [(10.123046875, -0.005393100133749951), (10.1728515625, -5.18754105913096), (10.10986328125, -31.439126135762386), (10.13623046875, -31.494161173770728)]
Zero fill : [(10.123046875, -0.06699910014441023), (10.1728515625, -5.216832638381734), (10.10986328125, -31.412882691863118), (10.13623046875, -32.08808645213892)]
ZOH       : [(10.123046875, -0.03559907459472774), (10.1728515625, -5.205575543568598), (10.10986328125, -31.199437020777808), (10.13623046875, -31.503423386907578)]
Linear    : [(10.123046875, -0.03330143431458907), (10.1728515625, -5.193685957757536), (10.10986328125, -31.188468711646827), (10.13623046875, -31.69564251215458)]
Deleted   : [(10.126953125, -5.586233149229062), (10.18359375, -6.546209263501059), (10.154296875, -14.058907252136022), (10.24755859375, -14.16637288270421)]

采用:

记录时间:

实际采样点数:

信号包含:

其中前两个信号只相差:

丢包按照每包8个采样点产生,包括:随机整包丢失;连续3包丢失;连续10包丢失;连续25包丢失。

最终缺失:

丢失率:

比较五种情况:完整参考数据;缺失位置补0;前值保持;线性插值;直接删除缺失点。

“高分辨率”FFT

程序使用:

因此FFT频率栅格间距为:

这个数很小,但它主要来自零填充,只代表频谱曲线计算得更密。

真正由采集时间决定的基本频率间隔为:

使用Hann窗后,其等效噪声带宽约为:

因此:

0.000488 Hz:频谱显示栅格; 0.00556 Hz:记录长度对应的基本分辨尺度; 0.00833 Hz:Hann窗的ENBW。

不能把大点数零填充误认为真正提高了频率分辨率。真正提高分辨率,主要依靠延长采集时间。

两个近邻单音能否分开?

完整信号检测到:

设置信号

FFT检测谱峰

10.123 Hz

10.12305 Hz

10.173 Hz

10.17285 Hz

频率误差只有FFT栅格量级,两个相隔0.05 Hz的信号能够清楚分开,这说明180秒记录足以分辨它们,因为:

如果要分辨相隔0.001 Hz的两个信号,仅仅把FFT改成几百万点不够。记录时间至少应达到约:

实际考虑窗函数,通常还需要更长。

缺失数据为什么产生频谱泄漏?

把完整信号写成 ,有效性掩码写成 :

数据有效数据缺失

补零后的信号实际是:

时域相乘对应频域卷积:

如果没有丢包, 恒等于1,它的频谱主要集中在直流位置,不会明显扩散原信号频谱。

发生丢包后,掩码中出现很多缺口, 会包含宽带频率成分。原本集中的单音谱线与它卷积后,会出现:主谱峰幅度下降;谱峰附近出现裙边;宽带噪声底升高;产生不存在的伪频率分量;连续突发缺口产生类似 sinc 的频谱结构。

因此,即使丢失比例不到1%,高动态范围频谱也能清楚看到影响。

各种处理方法的时域误差

缺失区域RMSE结果:

方法

缺失区域RMSE

全记录RMSE

补零

0.8172

0.07576

前值保持

0.7470

0.06925

线性插值

0.5280

0.04895

在本次信号条件下,线性插值最好,因为它利用了缺口前后的两个真实值。

但这不意味着线性插值能恢复真实高频信号。它只是在两个端点之间构造直线:

其中 为缺失点数;缺口越长,真实信号越可能在缺口内发生多次振荡,直线插值与真实值的差别就越大。

补零的频域影响

补零相当于直接使用缺失掩码:

主要问题是:信号突然跳到0;缺口结束时又突然跳回真实值;两个边沿产生宽带频谱;单音幅度被略微压低;噪声底升高。

本次仿真中,10.123 Hz幅度从:

下降到:

大约下降:

虽然主谱峰变化不大,但在参考信号较弱、动态范围较高时,新增的宽带泄漏会遮盖微弱分量。

前值保持的频域影响

前值保持为:

它不会像补零一样立即跳到0,但会形成一段水平线;缺口结束时,保持值与当前真实值之间仍可能产生阶跃。对低频或缓慢变化信号比较适合;能用于实时处理,因为不需要等待未来数据;对高频信号产生幅度和相位误差;长缺口会形成明显平台;缺口恢复边沿会带来高频泄漏。

仿真中,前值保持比补零的时域RMSE稍低,但频谱误差仍很明显。

线性插值的频域影响

线性插值比补零和前值保持平滑,因此通常减少突变导致的宽带泄漏,仿真中,10.123 Hz幅度变为:

相对完整参考值的误差约:

10.173 Hz幅度几乎保持不变:

但是线性插值本质上具有低通倾向:缺失区被替换成直线;高频细节被消除;高频功率通常被低估;相位信息不再真实;长缺口可能生成不存在的慢斜坡。线性插值适合:实时波形显示;极短缺口;低频趋势观察;不要求缺失区真实频域特征的场景。

直接删除最危险

原始数据长度为:

删除缺失点后变为:

如果仍按512 Hz采样率解释,程序会认为记录长度为:

但真实过程持续了180秒。

相当于把180秒发生的信号压缩到了178.45秒,平均时间压缩比例为:

所以频率会整体向上偏移。仿真中10.123 Hz附近的主峰移动到约:

同时出现:10.1543 Hz;10.1836 Hz;10.2476 Hz;等多个伪峰或严重变形的峰。

这里没有严格按照平均压缩比例整体平移,是因为缺失点不是均匀删除,而是在不同时间发生随机和突发删除,所以时间轴是分段跳变,而不是均匀缩放。

结论是:

绝对不要删除缺失点后,再假定剩余样本仍然是等间隔采样。

小结

上位机算力大,可以任意插值,另外高点数只能让FFT失真看得更细,但不能把插值数据变回真实测量数据。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 云深之无迹 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 固定帧长数据包(Packit)
  • 一个好消息
  • 包计数器
    • 跨越255时仍然成立
    • 8位序号存在歧义
  • 丢包为什么会影响时间轴
  • 对FFT影响的一点讨论
    • 情况一:丢失位置被删除
    • 情况二:缺失位置补0
    • 情况三:前值保持
    • 情况四:线性插值
  • 补偿方法
    • 前值保持
    • 线性插值
    • NaN加质量标记
  • 可以采用分级补偿策略
  • 详细模拟
    • 仿真设置
    • “高分辨率”FFT
  • 两个近邻单音能否分开?
  • 缺失数据为什么产生频谱泄漏?
    • 各种处理方法的时域误差
    • 补零的频域影响
  • 前值保持的频域影响
    • 线性插值的频域影响
    • 直接删除最危险
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档