MATLAB xcorr 的 C 语言实现:补零规则、归一化与 FFT 优化
2026/9/10 1:49:14 网站建设 项目流程

简介:这套资源以纯C语言给出了Matlab中xcorr函数的完整实现,面向信号处理、嵌入式及跨平台开发者,用于在没有Matlab环境或需要高性能计算的场景下求解两个序列的互相关,进而分析信号间延迟关系,也可作为学习互相关原理的辅助材料。压缩包为rar格式,共2个文件,包含1个c源文件与1个h头文件,总大小仅2KB;其中xcorr.c实现了偏置、无偏与交叉互相关三种计算模式,xcorr.h则声明了对外接口及所需结构体。目前已有2998人学习下载。通过研读源码,可以学会输入序列预读取、输出长度计算、动态内存分配以及嵌套循环时间偏移累加等关键步骤,便于将算法快速移植到嵌入式平台或集成到自有项目,也能为噪声检测、信号同步、滤波器设计等后续开发提供直接参考,有效提升C语言与Matlab之间的代码转换能力。 做信号处理的人大概都有过这种体验:算法原型在 MATLAB 里一个小时就能写完,xcorr 一调,相关峰明明白白。可一旦要把它变成嵌入式设备里的 C 代码,或者给其他语言写扩展模块,就发现 MATLAB 的便捷全部变成了包袱。我最近做一个时延估计的工程,需要在 ARM 板子上实时算互相关,绕了一圈把 xcorr 用 C 从零实现了一遍。这个过程没有多少玄学,但细节是真的多:补零规则、maxlag 的默认值、四种归一化的区别,稍不注意,输出的相关峰就差了几个点。这篇文章就把我的实现思路和踩坑记录完整放出来,包含可以抄走的代码和验证方法,适合正在做跨平台移植、写 DSP 算法库,或者整天被 MATLAB 原型追着要 C 版本的人。

1. 先把 MATLAB xcorr 的行为拆明白,别急着写循环

1.1 公式、输出长度和 lag 的含义

xcorr 的数学定义不复杂:

r(k) = Σn x(n+k) · conj(y(n))

实信号时 conj 可以当作不存在。k 是滞后量,取值范围从 -maxlag 到 maxlag,因此输出长度固定是 2*maxlag+1 个点。下标要特别注意:C 数组里的第 maxlag 个元素(也就是 MATLAB 里的第 maxlag+1 个元素)对应 lag=0。很多移植的人栽在顺序上,其实是把负滞后那段填反了。

这个符号规则还有个容易迷惑的地方:r(1) 表示 x 往右移了一个点后和 y 的乘积和,不是 y 往右移。做时延估计时,如果峰值出现在正滞后,说明 x 相对 y 是滞后的;具体谁先谁后要看你的信号采集顺序。峰值位置本身不受影响,但写文档和注释时最好把符号约定写清楚,不然三个月后的自己会感谢你。

1.2 长度不一致时 MATLAB 偷偷补了零

真正容易让人翻车的是输入长度不一致。MATLAB 在做 xcorr(x,y) 时,会先把短的那条从尾部补零,补到和长的那条等长,然后按 N = max(nx, ny) 参与计算,默认的 maxlag 也是 N-1。也就是说,xcorr(ones(1,2), ones(1,3)) 的默认输出不是常见的 Nx+Ny-1=4 个点,而是 2*3-1=5 个点。

我第一次移植时就栽在这里。当时拿 MATLAB 和 C 的结果对波形,怎么看怎么对不上,检查了半天才发现是输出长度预期错了。这个补零行为反过来帮了 C 实现一个大忙:先把两个序列都拷贝到 N 长的缓冲区,剩下部分全是 0,核心循环里就不用再判断"谁长谁短"了。

1.3 四种归一化选项,一句话记牢

xcorr 的归一项不复杂,但很多人默认它输出的是 [-1,1] 的相关系数,这是个常见误区。实际上默认选项是 'none',就是原始累加和。四种选项可以归纳成一张表:

选项输出含义计算公式
'none'原始累加和r(k)
'biased'有偏估计r(k) / N
'unbiased'无偏估计r(k) / (N -
'coeff'(别名 'normalized')归一化到相关系数量纲r(k) / sqrt(Σx² · Σy²)

'coeff' 在 lag=0 处恰好是归一化相关系数(未去均值的版本),这也是很多人误以为默认输出的原因。'biased' 常用于功率谱估计里的自相关修正;'unbiased' 在小滞后处分母小、方差大,实际用的时候要留意。移植时这四个选项全都要做,因为你不知道上游算法会选哪个。

2. 直接法实现:一份可以抄的 C 代码

2.1 为什么先做直接法

有人一听说"高性能"就直接上 FFT,我劝你先写直接法。直接法的逻辑和公式一一对应,出错好查;后面做优化时再拿它当基准对照,至少能确认优化没有改坏结果。它的复杂度是 O((2*maxlag+1) * N),看起来是三层循环,最内层其实只有一次乘加。maxlag 较小、N 在几千点以下的场景,C 里开 O3 后运行时间完全能接受。很多实际工程要的只是 lag 0 附近一个小窗口,直接法比 FFT 更划算。

2.2 核心循环:索引和边界

下面这段是互相关核心,输入是已经补零对齐到等长的两个序列:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> typedef enum kXcorrScale { XC_NONE = 0, XC_BIASED, XC_UNBIASED, XC_COEFF } kXcorrScale; /* x, y: 等长序列,长度 n;out: 长度 2*maxlag+1 */ void xcorr_direct(const double *restrict x, const double *restrict y, size_t n, size_t maxlag, double *restrict out) { size_t len = 2 * maxlag + 1; for (size_t i = 0; i < len; i++) { long lag = (long)i - (long)maxlag; /* 实际滞后:-maxlag .. maxlag */ double sum = 0.0; if (lag >= 0) { size_t k = (size_t)lag; if (k < n) { /* 防 size_t 下溢 */ for (size_t j = 0; j < n - k; j++) { sum += x[j + k] * y[j]; } } } else { size_t k = (size_t)(-lag); if (k < n) { for (size_t j = 0; j < n - k; j++) { sum += x[j] * y[j + k]; } } } out[i] = sum; } }

这里有两个细节值得反复看。第一,out[i] 对应的 lag 是 i - maxlag,不是 i,负滞后那一段的顺序特别容易搞反。第二,C 语言里n - k在 k > n 时不会变成负数,而是变成一个巨大的 size_t,直接拿去当循环上界就是灾难,所以我在两个分支前都加了if (k < n)保护。这个坑我见过不止一次,每次都是查半天才发现是边界写穿。

2.3 外层的补零、maxlag 和归一化

外层函数负责对齐 MATLAB 的完整行为:不等长补零、默认 maxlag、四种归一化。

int xcorr(const double *x, size_t nx, const double *y, size_t ny, long maxlag, /* 传 -1 表示使用默认 N-1 */ kXcorrScale scale, double *out) /* 输出缓冲区由调用方预分配 */ { if (x == NULL || y == NULL || out == NULL) return -1; if (nx == 0 || ny == 0) return -2; size_t n = (nx > ny) ? nx : ny; /* 补零后的统一长度 */ if (maxlag < 0) maxlag = (long)n - 1; if ((size_t)maxlag > n - 1) maxlag = (long)n - 1; double *px = (double *)calloc(n, sizeof(double)); double *py = (double *)calloc(n, sizeof(double)); if (px == NULL || py == NULL) { free(px); free(py); return -3; } memcpy(px, x, nx * sizeof(double)); memcpy(py, y, ny * sizeof(double)); xcorr_direct(px, py, n, (size_t)maxlag, out); size_t len = 2 * (size_t)maxlag + 1; if (scale == XC_BIASED) { for (size_t i = 0; i < len; i++) out[i] /= (double)n; } else if (scale == XC_UNBIASED) { for (size_t i = 0; i < len; i++) { long lag = (long)i - maxlag; out[i] /= (double)(n - (size_t)labs(lag)); } } else if (scale == XC_COEFF) { double ex = 0.0, ey = 0.0; for (size_t i = 0; i < nx; i++) ex += x[i] * x[i]; for (size_t i = 0; i < ny; i++) ey += y[i] * y[i]; double denom = sqrt(ex * ey); if (denom > 0.0) { for (size_t i = 0; i < len; i++) out[i] /= denom; } else { memset(out, 0, len * sizeof(double)); /* 能量为 0 时的兜底 */ } } free(px); free(py); return 0; }

calloc 先补零再 memcpy 前半段,比在热循环里判断边界要干净得多。等长化之后,核心循环完全不用管谁长谁短。这里我做了个工程取舍:如果传入的 maxlag 大于 N-1,直接夹回 N-1。如果调用方坚持要完整 2*maxlag+1 的输出,那尾部本来就是数学上的 0,自己在外层扩展输出并把尾部清零就行;尤其注意 'unbiased' 在 |lag| >= N 时分母会变成 0 或负数,不要在那种边界条件下硬算。

3. 拿什么证明你的 C 代码写对了

3.1 一个算得出来的小例子

拿一个手算都来得及的例子:x = [1 2 3],y = [4 5 6]。按定义展开,原始输出是 [6, 17, 32, 23, 12],滞后从 -2 到 2。逐项验证:lag=-2 只有 x(0)·y(2)=6 一项;lag=-1 是 x(0)·y(1) + x(1)·y(2) = 5+12=17;lag=0 是 4+10+18=32;lag=1 是 x(1)·y(0) + x(2)·y(1) = 8+15=23;lag=2 是 x(2)·y(0)=12。

四种归一化的结果如下,可以直接用来对照:

lagnonebiasedunbiasedcoeff
-26260.1827
-1175.66678.50.5178
03210.666710.66670.9746
1237.666711.50.7005
2124120.3655

注意 'unbiased' 的分母是 N-|lag|,不是这条 lag 上实际参与叠加的非零项数。对于输入长度不一致的补零情况,这两个数常常不相等,别被直觉带偏。

3.2 MATLAB 和 Python 双保险验证

在 MATLAB 里验证:

x = [1 2 3]; y = [4 5 6]; disp(xcorr(x, y)); disp(xcorr(x, y, 'biased')); disp(xcorr(x, y, 'unbiased')); disp(xcorr(x, y, 'coeff'));

在 Python 里验证:

import numpy as np x = np.array([1., 2., 3.]) y = np.array([4., 5., 6.]) print(np.correlate(x, y, 'full')) # [ 6. 17. 32. 23. 12.]

用相对误差去比,不要用==。直接法和 FFT 法的求和顺序不同,结果差 1e-15 量级完全正常。

3.3 测试用例清单

除了上面这个小例子,我建议至少跑这几类用例:

  • 等长、默认 maxlag
  • 不等长,验证补零行为,比如 x=[1 2],y=[1 2 3],raw 输出应为 [3, 8, 5, 2, 0]
  • maxlag 远小于 N-1,验证截断逻辑
  • 冲激信号做自相关,x=[1 0 0 0] 的结果应该只有中心点是 1
  • x 或 y 全零,验证 'coeff' 的除零保护

这些用例全跑过,实现基本可以放心拿去做工程。

4. N 一上去就得上 FFT:频域相关运算的正确写法

4.1 什么时候别用直接法

直接法在 N=10000、maxlag=9999 时大约要算 2 亿次乘加,C 里也要几百毫秒。放到实时系统里,每个周期都要处理一帧数据,这个时间不可接受。这时就要把时域相关变成频域相乘:两个正变换加一个逆变换,复杂度从 O(N·L) 降到了 O(L·logL)。

不过要注意:如果你的业务只关心若干固定 lag,比如延迟估计只查 lag 在 ±100 范围内的峰,直接法反而更优,因为复杂度变成 O(maxlag·N),跟 FFT 比起来省掉一堆内存搬运和复数运算。

4.2 频域公式和常见符号坑

按 MATLAB 的定义推导,频域公式是:

C(f) = X(f) · conj(Y(f))

注意,不是 conj(X(f)) · Y(f)。网上两种写法都有,区别在于 lag 的正负号定义反了过来。如果你用的是别人封装好的 FFT 相关工具,先拿 3.1 的例子验一次方向,再继续优化。我见过有人写反了,结果整个相关峰在时间轴上左右颠倒,光看峰值位置还不一定能发现。

4.3 FFT 段落代码和索引映射

假设你手头有一个常规的 radix-2 复数 FFT,函数原型是fft(re, im, n, sign),sign=1 为正变换、-1 为逆变换,频域计算的核心逻辑如下:

/* L 取满足 L >= N + maxlag 的最小 2 的幂,避免圆形相关把远端滞后卷回来 */ size_t L = 1; while (L < n + (size_t)maxlag) L <<= 1; double *xr = (double *)calloc(L, sizeof(double)); double *xi = (double *)calloc(L, sizeof(double)); double *yr = (double *)calloc(L, sizeof(double)); double *yi = (double *)calloc(L, sizeof(double)); memcpy(xr, px, n * sizeof(double)); /* px, py 来自上一层补零后的等长缓冲 */ memcpy(yr, py, n * sizeof(double)); fft(xr, xi, L, 1); /* X */ fft(yr, yi, L, 1); /* Y */ for (size_t i = 0; i < L; i++) { /* C = X .* conj(Y) */ double cr = xr[i] * yr[i] + xi[i] * yi[i]; double ci = xi[i] * yr[i] - xr[i] * yi[i]; xr[i] = cr; xi[i] = ci; } fft(xr, xi, L, -1); /* 逆变换;若你的实现不自动除以 L,这里要补除 */ for (size_t k = 0; k <= (size_t)maxlag; k++) { out[(size_t)maxlag + k] = xr[k]; /* 正滞后 0..maxlag */ } for (size_t d = 1; d <= (size_t)maxlag; d++) { out[(size_t)maxlag - d] = xr[L - d]; /* 负滞后 -maxlag..-1 */ }

实信号经过逆变换后,xi 里的虚部基本是数值噪声,取 xr 即可。我第一次写这段时,逆变换之后忘了除以 L,整条曲线放大了 L 倍,排查了半个小时才想起来。你把这段和直接法跑同一个例子,两者误差在 1e-12 量级就说明索引映射完全正确。

5. 移植到真实项目里的几条经验

5.1 OpenMP 并行和编译器选项

直接法里每个 lag 的计算完全独立,天生适合并行。在 xcorr_direct 的外层循环前加一句#pragma omp parallel for schedule(static)就行,N 和 maxlag 都很大时提升明显。但数据量小的时候别开,线程创建和同步开销比计算本身还大,收益是负的。编译器至少开到-O3 -march=native-ffast-math能再快一点,但它会改变除零和 NaN 的行为,我在 'coeff' 分支里已经手动判断了能量为 0 的情况,开不开就看你对自己代码的掌控力了。

5.2 内存和接口设计

输出缓冲区由调用方预分配,不要在热路径里 malloc/free。我在实际工程里把输出放进一块环形缓冲,每次只算需要的 lag 窗口,而不是每次都重算整条相关曲线。如果你的数据源是 float,建议累加时用 double,长序列的 float 累加误差会大到肉眼可见。另外,函数尽量做成纯函数,不依赖全局状态,这样多线程调用和单元测试都省心。

5.3 调试互相关代码的习惯

我的调试套路是三分法:先只用 C 代码和一组固定数据做单测,保证内部逻辑自洽;再加 MATLAB 参考值做差异对比;最后才丢进实时系统。最容易忽略的是把 MATLAB 结果导出成文本时精度被截断,导出时要用fprintf('%17.15e')这类高精度格式,否则你以为是代码的 bug,其实是文件格式的锅。

这套代码和校验流程后来在我的浮点版和定点版项目里都跑通了。如果你只让我留一条建议,那就是:先用直接法把正确性钉死,再谈 FFT、并行和定点化,顺序反了,排查问题的成本会翻好几倍。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询