4、软件抗干扰策略:数据包超时重传机制、CRC校验失败处理、滑动窗口滤波、异常帧丢弃逻辑

各位同行,咱们接着聊。硬件上的抗干扰手段做得再好,也架不住现场环境太恶劣。我见过不少风场,屏蔽层接地做得一丝不苟,终端电阻也加了,可通信还是时不时抽风。这时候,软件就得站出来兜底了。

说白了,软件抗干扰就是一套“容错机制”。它不指望信道永远干净,而是假设信道一定会出问题,然后想办法把问题消化掉。今天我把四个最常用的策略掰开揉碎了讲,都是我在现场摸爬滚打总结出来的。

核心思想: 不要试图让通信永远不出错,而是让系统在出错时依然能稳定运行。

软件抗干扰策略 超时重传机制 等待超时 → 重新发送 CRC校验失败处理 校验错误 → 丢弃/重传 滑动窗口滤波 N次采样 → 取平均/中值 异常帧丢弃逻辑 非法帧 → 直接丢弃 目标:在恶劣信道下保持通信稳定

4.1 数据包超时重传机制

这个机制其实很简单:主站发一个请求,然后开始计时。在规定时间内没收到从站的回复,就判定这个包丢了,重新发一次。

但这里有个坑——超时时间设多少合适?我见过有人拍脑袋设个100ms,结果现场总线负载一高,正常响应都要150ms,结果一直在重传,把网络搞瘫痪了。

我的经验: 超时时间一般设为正常响应时间的3~5倍。比如正常响应是20ms,超时就设80~100ms。另外,重传次数建议不超过3次。超过3次还收不到,就该报通信故障了,别死磕。

代码实现上,我习惯用状态机来做:

// 伪代码:Modbus主站超时重传逻辑
typedef enum {
    IDLE,       // 空闲
    WAITING,    // 等待响应
    RETRY,      // 重传
    TIMEOUT     // 超时故障
} CommState;

CommState state = IDLE;
int retryCount = 0;
int maxRetries = 3;
unsigned long timeoutMs = 100;  // 超时时间100ms

void SendRequest(uint8_t* req, uint16_t len) {
    state = WAITING;
    retryCount = 0;
    UART_Send(req, len);
    startTimer(timeoutMs);
}

void TimerCallback() {
    if (state == WAITING) {
        retryCount++;
        if (retryCount <= maxRetries) {
            state = RETRY;
            // 重新发送上一帧
            UART_Send(lastReq, lastLen);
            startTimer(timeoutMs);
        } else {
            state = TIMEOUT;
            ReportCommError("从站无响应");
        }
    }
}

void UART_RxCallback(uint8_t* data, uint16_t len) {
    if (state == WAITING) {
        stopTimer();
        state = IDLE;
        ProcessResponse(data, len);
    }
}

你想想看,这个机制其实就是在赌——赌下一次能成功。大多数情况下,现场的干扰是瞬时的,重传一次就能搞定。但如果一直失败,那说明信道已经彻底坏了,别浪费带宽了。

4.2 CRC校验失败处理

Modbus RTU用的是CRC-16校验,这个算法本身很成熟。但现场的问题往往不在算法,而在处理策略上。

我记得有一次在东北的风场,冬天零下30度,通信频繁报CRC错误。排查了半天,发现是温度太低导致晶振频率漂移,波特率对不上了。这种硬件问题软件解决不了,但CRC校验能帮你快速定位问题。

注意: CRC校验失败后,从站绝对不能回复任何数据。这是Modbus协议的规定,也是为了避免总线冲突。从站只需要默默丢弃这帧数据,等主站超时重传就行。

主站这边,收到CRC错误的响应帧,处理逻辑是这样的:

  1. 直接丢弃:不处理、不回复、不记录(除非调试模式)
  2. 计数器+1:维护一个CRC错误计数器,用于统计信道质量
  3. 阈值告警:如果连续N帧都CRC错误,触发告警

我个人的习惯是,在调试阶段把CRC错误的原始数据打印出来,看看是哪个字节被干扰了。这能帮你判断干扰源——如果是固定字节出错,可能是硬件问题;如果是随机字节出错,大概率是环境干扰。

4.3 滑动窗口滤波

这个策略主要用在数据采集侧。你从传感器读回来的数据,可能因为干扰偶尔跳变一下。比如风速传感器,正常是8m/s,突然蹦到20m/s,然后又恢复正常。这种毛刺如果不处理,控制系统会误动作。

滑动窗口滤波的原理很简单:维护一个长度为N的缓冲区,每次新数据进来,就把最老的数据丢掉,然后对缓冲区里的N个数据做处理(取平均、取中值、去极值平均等)。

我给大家看一个常用的去极值平均滤波:

// 滑动窗口去极值平均滤波
#define WINDOW_SIZE 5

float window[WINDOW_SIZE];
int index = 0;
int count = 0;

float SlidingAverageFilter(float newValue) {
    float sum = 0;
    float min = 9999.0, max = -9999.0;
    int i;
    
    // 存入新数据
    window[index] = newValue;
    index = (index + 1) % WINDOW_SIZE;
    if (count < WINDOW_SIZE) count++;
    
    // 窗口未满时直接返回原始值
    if (count < WINDOW_SIZE) return newValue;
    
    // 计算总和、最大值、最小值
    for (i = 0; i < WINDOW_SIZE; i++) {
        sum += window[i];
        if (window[i] < min) min = window[i];
        if (window[i] > max) max = window[i];
    }
    
    // 去掉一个最大值和一个最小值,取平均
    return (sum - min - max) / (WINDOW_SIZE - 2);
}

窗口大小N怎么选?我一般这样定:

场景 推荐窗口大小 说明
快速变化的信号(如电流) 3~5 响应快,滤除毛刺
缓慢变化的信号(如温度) 8~16 平滑效果好,延迟可接受
开关量信号 3 防抖,连续3次一致才确认

说白了,窗口越大越平滑,但响应也越慢。你得在平滑度和实时性之间找个平衡点。

4.4 异常帧丢弃逻辑

这个策略听起来简单——不符合协议的帧就丢掉。但实际做起来,有很多细节要考虑。

什么样的帧算异常帧?我列个清单:

  • 长度异常:Modbus RTU帧有最小长度要求(从站地址+功能码+CRC,至少4字节),太短的直接丢
  • 地址不匹配:帧里的从站地址跟自己的地址对不上,丢
  • 功能码不支持:比如从站只支持03和06,收到01功能码,丢
  • CRC校验错误:这个前面说过了,丢
  • 帧间隔异常:Modbus RTU要求帧之间至少有3.5个字符的静默时间,如果间隔太短,说明上一帧可能还没结束,丢

避坑指南: 我曾经遇到过一个情况,现场有两台设备地址设成了同一个。结果主站发请求时,两台设备同时回复,数据在总线上打架。从那以后,我要求所有设备上电后先做地址冲突检测——发一个广播帧,看看有没有多个设备回复。

异常帧丢弃的逻辑,我建议放在接收中断里做第一层过滤。这样能减轻主循环的负担。伪代码如下:

bool IsValidFrame(uint8_t* buffer, uint16_t len) {
    // 1. 检查最小长度
    if (len < 4) return false;
    
    // 2. 检查地址是否匹配
    if (buffer[0] != MY_ADDR && buffer[0] != BROADCAST_ADDR) return false;
    
    // 3. 检查功能码是否支持
    if (!IsSupportedFunction(buffer[1])) return false;
    
    // 4. 检查CRC
    uint16_t calcCrc = CalculateCRC16(buffer, len - 2);
    uint16_t recvCrc = (buffer[len-1] << 8) | buffer[len-2];
    if (calcCrc != recvCrc) return false;
    
    // 5. 检查帧间隔(需要硬件定时器辅助)
    if (!CheckFrameInterval()) return false;
    
    return true;
}

嗯,这里要注意一点:广播帧(地址0x00)的处理要特殊一些。广播帧不需要从站回复,所以即使地址匹配,也不要发送响应。我见过有人忘了这个细节,结果广播帧引发了一堆从站同时回复,总线直接炸了。

好了,这四个策略讲完了。它们不是孤立的,在实际项目中要组合使用。比如超时重传和CRC校验是“黄金搭档”,滑动窗口滤波和异常帧丢弃是“数据清洗组合”。你把这些都用上,Modbus通信的稳定性至少能提升一个数量级。

最后说一句: 软件抗干扰是“防守”,不是“进攻”。它不能解决所有问题,但能让你在恶劣环境下多撑一会儿。真正要根治干扰,还得从硬件和布线入手。软件只是最后的防线。