简介这套资源以纯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(nk) · conj(y(n))实信号时 conj 可以当作不存在。k 是滞后量取值范围从 -maxlag 到 maxlag因此输出长度固定是 2*maxlag1 个点。下标要特别注意C 数组里的第 maxlag 个元素也就是 MATLAB 里的第 maxlag1 个元素对应 lag0。很多移植的人栽在顺序上其实是把负滞后那段填反了。这个符号规则还有个容易迷惑的地方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)) 的默认输出不是常见的 NxNy-14 个点而是 2*3-15 个点。我第一次移植时就栽在这里。当时拿 MATLAB 和 C 的结果对波形怎么看怎么对不上检查了半天才发现是输出长度预期错了。这个补零行为反过来帮了 C 实现一个大忙先把两个序列都拷贝到 N 长的缓冲区剩下部分全是 0核心循环里就不用再判断谁长谁短了。1.3 四种归一化选项一句话记牢xcorr 的归一项不复杂但很多人默认它输出的是 [-1,1] 的相关系数这是个常见误区。实际上默认选项是 none就是原始累加和。四种选项可以归纳成一张表选项输出含义计算公式none原始累加和r(k)biased有偏估计r(k) / Nunbiased无偏估计r(k) / (N -coeff别名 normalized归一化到相关系数量纲r(k) / sqrt(Σx² · Σy²)coeff 在 lag0 处恰好是归一化相关系数未去均值的版本这也是很多人误以为默认输出的原因。biased 常用于功率谱估计里的自相关修正unbiased 在小滞后处分母小、方差大实际用的时候要留意。移植时这四个选项全都要做因为你不知道上游算法会选哪个。2. 直接法实现一份可以抄的 C 代码2.1 为什么先做直接法有人一听说高性能就直接上 FFT我劝你先写直接法。直接法的逻辑和公式一一对应出错好查后面做优化时再拿它当基准对照至少能确认优化没有改坏结果。它的复杂度是 O((2*maxlag1) * 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: 等长序列长度 nout: 长度 2*maxlag1 */ 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*maxlag1 的输出那尾部本来就是数学上的 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) 51217lag0 是 4101832lag1 是 x(1)·y(0) x(2)·y(1) 81523lag2 是 x(2)·y(0)12。四种归一化的结果如下可以直接用来对照lagnonebiasedunbiasedcoeff-26260.1827-1175.66678.50.517803210.666710.66670.97461237.666711.50.70052124120.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] 的结果应该只有中心点是 1x 或 y 全零验证 coeff 的除零保护这些用例全跑过实现基本可以放心拿去做工程。4. N 一上去就得上 FFT频域相关运算的正确写法4.1 什么时候别用直接法直接法在 N10000、maxlag9999 时大约要算 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)sign1 为正变换、-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 -marchnative。-ffast-math能再快一点但它会改变除零和 NaN 的行为我在 coeff 分支里已经手动判断了能量为 0 的情况开不开就看你对自己代码的掌控力了。5.2 内存和接口设计输出缓冲区由调用方预分配不要在热路径里 malloc/free。我在实际工程里把输出放进一块环形缓冲每次只算需要的 lag 窗口而不是每次都重算整条相关曲线。如果你的数据源是 float建议累加时用 double长序列的 float 累加误差会大到肉眼可见。另外函数尽量做成纯函数不依赖全局状态这样多线程调用和单元测试都省心。5.3 调试互相关代码的习惯我的调试套路是三分法先只用 C 代码和一组固定数据做单测保证内部逻辑自洽再加 MATLAB 参考值做差异对比最后才丢进实时系统。最容易忽略的是把 MATLAB 结果导出成文本时精度被截断导出时要用fprintf(%17.15e)这类高精度格式否则你以为是代码的 bug其实是文件格式的锅。这套代码和校验流程后来在我的浮点版和定点版项目里都跑通了。如果你只让我留一条建议那就是先用直接法把正确性钉死再谈 FFT、并行和定点化顺序反了排查问题的成本会翻好几倍。本文还有配套的精品资源点击获取