4、软件抗干扰策略:数据包超时重传机制、CRC校验失败处理、滑动窗口滤波、异常帧丢弃逻辑
各位同行,咱们接着聊。硬件上的抗干扰手段做得再好,也架不住现场环境太恶劣。我见过不少风场,屏蔽层接地做得一丝不苟,终端电阻也加了,可通信还是时不时抽风。这时候,软件就得站出来兜底了。
说白了,软件抗干扰就是一套“容错机制”。它不指望信道永远干净,而是假设信道一定会出问题,然后想办法把问题消化掉。今天我把四个最常用的策略掰开揉碎了讲,都是我在现场摸爬滚打总结出来的。
核心思想: 不要试图让通信永远不出错,而是让系统在出错时依然能稳定运行。
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:维护一个CRC错误计数器,用于统计信道质量
- 阈值告警:如果连续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通信的稳定性至少能提升一个数量级。
最后说一句: 软件抗干扰是“防守”,不是“进攻”。它不能解决所有问题,但能让你在恶劣环境下多撑一会儿。真正要根治干扰,还得从硬件和布线入手。软件只是最后的防线。