4. 风向数据处理:滑动平均滤波、中值滤波、卡尔曼滤波在风向处理中的应用
风向数据,说实话,是偏航控制里最让人头疼的信号之一。
为什么?因为风向本身就是一个随机性很强的物理量。你想想看,风从山坳里吹过来,被塔筒挡一下,被叶片搅一下,传到机舱顶部的风速仪时,早就不是那个"干净"的风向了。我见过太多现场,原始风向数据跳变超过30度,直接拿去用的话,偏航系统会像个没头苍蝇一样来回转。
所以,数据处理这一步,必须做。而且要做好。
今天我就把三种最常用的滤波方法掰开揉碎了讲。每种方法我都踩过坑,咱们直接说干货。
4.1 滑动平均滤波:简单但够用
滑动平均滤波,说白了就是取最近N个点的平均值。算法简单,计算量小,在嵌入式系统里跑起来毫无压力。
公式长这样:
θ_filtered(k) = (1/N) * Σ θ(k-i), i=0 to N-1
其中N是窗口长度。N越大,曲线越平滑,但延迟也越大。
我个人习惯,在陆上风电场,N取5到10就够了。海上风电,因为湍流强度大,我建议N取15到20。
代码实现也很直接:
// 滑动平均滤波
#define WINDOW_SIZE 10
float wind_buffer[WINDOW_SIZE];
int buffer_index = 0;
int buffer_count = 0;
float sliding_average_filter(float new_sample) {
float sum = 0.0;
wind_buffer[buffer_index] = new_sample;
buffer_index = (buffer_index + 1) % WINDOW_SIZE;
if (buffer_count < WINDOW_SIZE) buffer_count++;
for (int i = 0; i < buffer_count; i++) {
sum += wind_buffer[i];
}
return sum / buffer_count;
}
嗯,这里要注意:风向是0到360度的循环量。如果你直接平均,0度和359度平均出来是179.5度,但实际应该是359.5度。这个问题我当年在项目里栽过跟头。
解决办法是先把角度拆成sin和cos分量,分别平均,再用atan2算回来。
4.2 中值滤波:对付野值的利器
滑动平均滤波怕什么?怕野值。一个传感器瞬间跳变到180度,滑动平均会被带偏好一阵子。
中值滤波就不一样了。它取窗口内所有数据的中间值,对野值有天然的免疫力。
我举个例子:
原始数据:[352, 355, 358, 10, 356, 354, 357]
排序后:[10, 352, 354, 355, 356, 357, 358]
中值:355度
你看,那个10度的野值被完美过滤掉了。
代码实现:
// 中值滤波
#define MEDIAN_WINDOW 5
float median_filter(float new_sample) {
static float buffer[MEDIAN_WINDOW];
static int index = 0;
float temp[MEDIAN_WINDOW];
buffer[index] = new_sample;
index = (index + 1) % MEDIAN_WINDOW;
// 拷贝并排序
memcpy(temp, buffer, sizeof(temp));
// 简单的冒泡排序,N小的时候够用
for (int i = 0; i < MEDIAN_WINDOW - 1; i++) {
for (int j = 0; j < MEDIAN_WINDOW - 1 - i; j++) {
if (temp[j] > temp[j+1]) {
float t = temp[j];
temp[j] = temp[j+1];
temp[j+1] = t;
}
}
}
return temp[MEDIAN_WINDOW / 2];
}
中值滤波和滑动平均滤波,我一般搭配着用。先用中值滤波干掉野值,再用滑动平均做平滑。双剑合璧,效果很好。
4.3 卡尔曼滤波:动态系统的首选
前面两种方法都是静态的,它们假设风向在窗口内基本不变。但实际风向是动态变化的,有趋势,有噪声。
卡尔曼滤波就是为这种场景设计的。它能根据系统的动态模型,结合测量值,给出最优估计。
说白了,卡尔曼滤波会"猜"下一时刻的风向大概是多少,然后用实际测量值来修正这个猜测。猜得准,就少修正;猜不准,就多修正。
风向的卡尔曼滤波模型:
状态向量: x = [θ, ω]^T (风向角,角速度)
状态方程: x(k) = A * x(k-1) + w(k-1)
测量方程: z(k) = H * x(k) + v(k)
其中:
A = [1, dt; 0, 1]
H = [1, 0]
Q = 过程噪声协方差
R = 测量噪声协方差
我个人建议,Q和R的调参是关键。Q设大了,滤波结果会跟着测量值剧烈波动;Q设小了,响应太慢。我一般先用一段离线数据跑一遍,找到合适的Q/R比值。
代码实现(简化版):
// 卡尔曼滤波 - 风向应用
typedef struct {
float theta; // 估计风向
float omega; // 估计角速度
float P[2][2]; // 误差协方差矩阵
} KalmanState;
void kalman_init(KalmanState *ks, float init_theta) {
ks->theta = init_theta;
ks->omega = 0.0;
ks->P[0][0] = 1.0; ks->P[0][1] = 0.0;
ks->P[1][0] = 0.0; ks->P[1][1] = 1.0;
}
float kalman_update(KalmanState *ks, float z, float dt) {
// 预测
float theta_pred = ks->theta + ks->omega * dt;
float omega_pred = ks->omega;
// 预测协方差
float P_pred[2][2];
P_pred[0][0] = ks->P[0][0] + dt*ks->P[1][0] + dt*ks->P[0][1] + dt*dt*ks->P[1][1] + Q_theta;
P_pred[0][1] = ks->P[0][1] + dt*ks->P[1][1];
P_pred[1][0] = ks->P[1][0] + dt*ks->P[1][1];
P_pred[1][1] = ks->P[1][1] + Q_omega;
// 卡尔曼增益
float K[2];
float S = P_pred[0][0] + R;
K[0] = P_pred[0][0] / S;
K[1] = P_pred[1][0] / S;
// 更新
float innovation = z - theta_pred;
// 处理0/360度跳变
if (innovation > 180) innovation -= 360;
if (innovation < -180) innovation += 360;
ks->theta = theta_pred + K[0] * innovation;
ks->omega = omega_pred + K[1] * innovation;
// 更新协方差
ks->P[0][0] = (1 - K[0]) * P_pred[0][0];
ks->P[0][1] = (1 - K[0]) * P_pred[0][1];
ks->P[1][0] = -K[1] * P_pred[0][0] + P_pred[1][0];
ks->P[1][1] = -K[1] * P_pred[0][1] + P_pred[1][1];
return ks->theta;
}
| 方法 | 计算量 | 延迟 | 抗野值 | 动态跟踪 | 适用场景 |
|---|---|---|---|---|---|
| 滑动平均 | 低 | 中 | 差 | 差 | 稳态风场 |
| 中值滤波 | 中 | 低 | 好 | 中 | 传感器异常多 |
| 卡尔曼滤波 | 高 | 低 | 好 | 好 | 湍流强、动态变化快 |
4.4 我的推荐方案
在实际项目中,我很少只用一种方法。我的惯用套路是:
- 第一步:中值滤波,窗口N=5,干掉传感器野值
- 第二步:卡尔曼滤波,做动态估计,输出平滑且响应快的风向
- 第三步:限幅,防止卡尔曼发散时输出异常值
这个组合拳,我在三个风场验证过,效果都很稳定。
下面这张图,是我总结的风向数据处理流程,你可以参考:
最后说一句,滤波参数不是一成不变的。不同风场,不同季节,甚至不同风速段,最优参数都不一样。我建议在风机调试阶段,留一个参数配置接口,方便现场微调。
好了,风向数据处理这块,核心就是这些。记住一个原则:滤波的目的是保留有用信息,去掉噪声,而不是把信号抹平。把握好这个度,你的偏航控制就成功了一半。