Serial Studio 中的 KISS FFT:轻量混合基 FFT 库的 API、构建与源码级解析
【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio
KISS FFT("Keep It Simple, Stupid")是一个以"简单"为首要目标的混合基快速傅里叶变换库,核心代码仅约 500 行,支持 float、double、int16_t、int32_t 与 SIMD 多种数据类型,并以宽松的 BSD 许可证可被快速嵌入任意 C 程序。在本仓库中,它作为第三方依赖被集成进 lib/KissFFT,并实际承担 Serial Studio FFT 频谱绘图(FFTPlot)与瀑布图(Waterfall)两个核心可视化组件的实数 FFT 运算。读完本文,你将掌握 KISS FFT 的核心 API 用法、两种构建系统的全部配置项、数据类型切换机制,以及它在本项目中的真实调用链路与底层算法原理。
KISS FFT 是什么:设计哲学与背景
KISS FFT 的诞生源于作者 Mark Borgerding 的一个具体痛点:当时找不到一个不使用汇编代码的定点 FFT 实现。于是他先用浮点数把理论理清,再处理定点问题,最终得到一份可通过简单重编译就支持 short、float、double 等类型的精简代码(lib/KissFFT/kiss_fft.c)。
README 中作者明确表示:"已经有很多优秀的 FFT 库了,KISS FFT 并不试图比它们更好,它只希望做一个足够高效、足够实用的 FFT,支持定点或浮点数据类型,并且能在几分钟内以极宽松的许可证嵌入别人的 C 程序。" 换句话说,它的定位是可移植、易集成、许可证友好,而非追求极致性能。
作者在 BACKGROUND 一节给出了与某个高度优化的商业 FFT 库(文中匿名称为 FFT_BRANDX)的对比,这些数字客观反映了该库的设计取舍:
- FFT_BRANDX 超过 10 万行代码,而 KISS FFT 的复数 1-D 核心仅约 500 行;
- 使用 FFT_BRANDX 的简单程序体积为 522KB,而同等功能的 KISS FFT 程序仅 18KB(未做体积优化);
- FFT_BRANDX 在默认模式下大约比 KISS FFT 快 2 倍。
作者的结论是:"那些高度优化的库为了榨取每一分性能背负了巨大的复杂度负担。有时候更简单反而更好,即使它并不'更好'。" 这句话是理解 KISS FFT 一切设计决策的总纲。
快速上手:1-D 复数 FFT 最小用法
在项目中引入 KISS FFT 只需包含头文件并调用三个函数。README 给出的最小编程范式如下:
#include "kiss_fft.h" kiss_fft_cfg cfg = kiss_fft_alloc( nfft ,is_inverse_fft ,0,0 ); while ... ... // put kth sample in cx_in[k].r and cx_in[k].i kiss_fft( cfg , cx_in , cx_out ); ... // transformed. DC is in cx_out[0].r and cx_out[0].i kiss_fft_free(cfg);要点说明:
kiss_fft_alloc(nfft, is_inverse_fft, mem, lenmem)负责为指定长度nfft的 FFT 分配并初始化配置/状态缓冲区(内部包含旋转因子表与因子分解结果),最后一个参数传 0 表示由库内部用malloc分配;- 每次变换调用
kiss_fft(cfg, cx_in, cx_out)完成;输入输出均为复数,通过.r/.i访问实部与虚部; - 用完后必须
kiss_fft_free(cfg)释放,避免内存泄漏。
频率域布局(极易踩坑):输出频率数据从 DC(0 频)一直排到 2π。因此cx_out[0]是 FFT 的直流分量(DC bin),而cx_out[nfft/2](若存在)是奈奎斯特频率对应的 bin。
完整声明与各函数的行为说明位于 lib/KissFFT/kiss_fft.h(如 kiss_fft.h 的 kiss_fft_alloc 注释),具体实现则在 lib/KissFFT/kiss_fft.c 中。
核心 API 详解
从 kiss_fft.h 可以看到该库对外暴露的全部接口,均以extern "C"包裹以便 C++ 程序直接调用:
| 函数 | 签名 | 作用 |
|---|---|---|
kiss_fft_alloc | kiss_fft_cfg kiss_fft_alloc(int nfft, int inverse_fft, void *mem, size_t *lenmem) | 初始化 FFT/IFFT 的 cfg/state 缓冲区,返回 NULL 表示失败 |
kiss_fft | void kiss_fft(kiss_fft_cfg cfg, const kiss_fft_cpx *fin, kiss_fft_cpx *fout) | 对复数输入执行一次 FFT |
kiss_fft_stride | void kiss_fft_stride(kiss_fft_cfg cfg, const kiss_fft_cpx *fin, kiss_fft_cpx *fout, int fin_stride) | 通用版本:每隔fin_stride个样本读取一次输入 |
kiss_fft_free | 宏(映射到KISS_FFT_FREE) | 释放kiss_fft_alloc分配的缓冲区 |
kiss_fft_cleanup | void kiss_fft_cleanup(void) | 清理内部管理的内存,退出前调用可让编译器输出更干净,非必需 |
kiss_fft_next_fast_size | int kiss_fft_next_fast_size(int n) | 返回不小于 n、且只含"快速因子"(2、3、5)的最小整数 |
关于kiss_fft_alloc的mem/lenmem参数,头文件注释给出了三种用法:
lenmem == NULL:库内部用malloc分配 cfg 缓冲区,返回指针需用kiss_fft_free释放;mem与lenmem均非 NULL 且*lenmem足够大:cfg 状态被放入用户提供的mem中,实际占用大小写回*lenmem,返回mem;mem为 NULL 或*lenmem不够大:返回 NULL,并将所需最小 cfg 缓冲区大小写入*lenmem——这是典型的"先查询大小、再自备内存"两段式用法,适合嵌入式等对动态分配敏感的场景。
kiss_fft_next_fast_size的实现(kiss_fft.c 第 408-420 行)非常直白:不断检查 n 能否被 2、3、5 整除,不能则 n 自增,直到找到完全由 2/3/5 分解的数。这是因为核心实现对因子 2、3、4、5 做了专门的蝶形优化(见下文"内部原理")。
实数优化 FFT(kiss_fftr):频谱分析的实际选择
README 特别提到tools/目录下的实数优化 FFT——它"返回正半频谱:(nfft/2+1) 个复数频率 bin"。对于采集自真实传感器、音频或串口的数据(纯实数序列),实数 FFT 相比复数 FFT 可节省约 45% 的 CPU 时间(此数字来自 kiss_fftr.h 的注释)。
实数 FFT 的接口定义在 lib/KissFFT/kiss_fftr.h:
kiss_fftr_alloc(nfft, inverse_fft, mem, lenmem):要求 nfft 必须为偶数;不关心分配细节时 mem 和 lenmem 直接传 NULL;kiss_fftr(cfg, timedata, freqdata):输入timedata为 nfft 个实标量点,输出freqdata为nfft/2+1个复数点(从 DC 到 Nyquist);kiss_fftri(cfg, freqdata, timedata):逆变换,输入 nfft/2+1 个复数点,输出 nfft 个实标量点;kiss_fftr_free:对应的释放宏。
这也正是 Serial Studio 中 FFT 绘图与瀑布图所使用的接口,下一节详细展开。
扩展工具集:多维 FFT 与快速卷积等
README 列出了tools/目录提供的其他能力:
- 多维 FFT(
kiss_fftnd.c/kiss_fftnd.h):任意维度的 FFT; - 实数优化 FFT:见上一节(
kiss_fftr.c); - 快速卷积 FIR 滤波(
kiss_fastfir.c):基于 FFT 实现快速卷积,注意不适用于定点版本; - 频谱图像生成(
psdpng工具):把频谱渲染成 PNG 图片。
核心 FFT 与大部分tools/代码均可编译为使用 float、double、Q15 short 或 Q31 样本(对应 int16_t / int32_t),默认数据类型为 float。这些工具对应的可执行文件在 lib/KissFFT/tools/CMakeLists.txt 中定义,包括fastconv、fastconvr、fft、psdpng等,均可通过KISSFFT_TOOLS选项控制是否构建。
构建 KISS FFT:Make 与 CMake 双构建系统
KISS FFT 提供两套功能等价的构建系统:传统 Makefile(Unix/Linux)与 CMake(≥ 3.6,由 Kitware 开发)。README 给出的支持环境为:
- GNU 构建环境:GCC、Clang + GNU Make 或 CMake(≥ 3.6);
- Microsoft Visual C++(MSVC)+ CMake(≥ 3.6)。
构建与测试的可选依赖库:
- libpng:用于
psdpng工具; - libfftw3:用于将 KISS FFT 结果与 FFTW 对比验证;
- Python 2/3 + Numpy:用于将结果与 Numpy 对比验证;
- OpenMP(GCC / Clang / MSVC 支持):用于多核 FFT 变换。
Cygwin 与 MinGW 环境大概率可用于面向 Windows 平台构建,但截至文档写作时未做过测试。仓库中 lib/KissFFT/Makefile 的版本号为 131.1.0(KFVER_MAJOR=131即 ABI 版本,KFVER_MINOR=1,KFVER_PATCH=0),CMake 工程会从 Makefile 中正则提取这三个数字作为project(kissfft ...)的版本号(见 lib/KissFFT/CMakeLists.txt)。
全部构建配置项
| 配置项(Make / CMake) | 可选值 | 默认值 | 说明 |
|---|---|---|---|
KISSFFT_DATATYPE/-DKISSFFT_DATATYPE= | float、double、int16_t、int32_t、simd | float | 库使用的主要数据类型;simd要求目标 CPU 支持 SSE 指令集 |
KISSFFT_OPENMP=1/-DKISSFFT_OPENMP=ON | 0 / 1 | 关闭 | 启用 OpenMP 多核支持,需编译器支持 |
KISSFFT_STATIC=1/-DKISSFFT_STATIC=ON | 0 / 1 | Make 关闭;CMake 默认 ON | 生成静态库(Windows.lib,Unix/Linux.a);关闭则生成共享库(Windows.dll,Linux/Unix.so,macOS.dylib) |
-DKISSFFT_TEST=OFF | ON/OFF | 关闭 | 关闭测试构建(仅 CMake);Make 下测试通过make testall/make testsingle单独进行 |
KISSFFT_TOOLS=0/-DKISSFFT_TOOLS=OFF | 0 / 1 | 默认构建工具 | 是否构建fastconv等命令行工具 |
KISSFFT_USE_ALLOCA=1/-DKISSFFT_USE_ALLOCA=ON | 0 / 1 | 关闭 | 使用alloca代替malloc/free |
PREFIX=/-DCMAKE_INSTALL_PREFIX= | 路径 | Make 为/usr/local | 指定安装前缀目录 |
注意 CMake 中KISSFFT_STATIC默认值为 ON(CMakeLists.txt 第 50 行),这与 Make 的默认动态库行为不同;同时如果同时指定-DBUILD_SHARED_LIBS=ON与-DKISSFFT_STATIC=ON,CMake 会直接报错退出(第 78-80 行)。CMake 还会校验KISSFFT_DATATYPE的取值,非法值会触发FATAL_ERROR(第 59-65 行)。
示例:以 int16_t 定点类型构建静态 OpenMP 库
README 给出的 Make 命令(在 kissfft 源码树内执行):
make KISSFFT_DATATYPE=int16_t KISSFFT_STATIC=1 KISSFFT_OPENMP=1 all等价 CMake 流程:
mkdir build && cd build cmake -DKISSFFT_DATATYPE=int16_t -DKISSFFT_STATIC=ON -DKISSFFT_OPENMP=ON .. make all示例:指定安装前缀
make PREFIX=/tmp/1234 KISSFFT_DATATYPE=int16_t KISSFFT_STATIC=1 KISSFFT_OPENMP=1 install或:
mkdir build && cd build cmake -DCMAKE_INSTALL_PREFIX=/tmp/1234 -DKISSFFT_DATATYPE=int16_t -DKISSFFT_STATIC=ON -DKISSFFT_OPENMP=ON .. make all make install从 Makefile 可以看到:库产物名随配置变化(如libkissfft-int16_t-openmp.a),头文件(kiss_fft.h、kissfft.hh、kiss_fftnd.h、kiss_fftndr.h、kiss_fftr.h)统一安装到include/kissfft目录,同时生成对应的.pcpkg-config 文件(Linux/macOS 下)。CMake 构建还额外导出kissfft::kissfft与kissfft::kissfft-<datatype>两个 target 别名,并支持安装 CMake package 配置(CMakeLists.txt 第 294-319 行)。
数据类型切换机制:FIXED_POINT 与 kiss_fft_scalar
KISS FFT 通过编译期宏实现类型可重编译。从 kiss_fft.h 第 73-85 行 可以看到:
- 定义了
FIXED_POINT时:FIXED_POINT == 32则kiss_fft_scalar为int32_t,否则为int16_t; - 未定义时:默认
kiss_fft_scalar为float; - 定义了
USE_SIMD时:kiss_fft_scalar为__m128,且自动使用 16 字节对齐的_mm_malloc/_mm_free(kiss_fft.h 第 50-60 行)。
复数类型kiss_fft_cpx仅包含r与i两个kiss_fft_scalar字段(第 87-90 行)。在 CMake 中,各类型对应的编译宏由 lib/KissFFT/CMakeLists.txt 自动生成:float/double定义kiss_fft_scalar=<type>,int16_t/int32_t分别定义FIXED_POINT=16/FIXED_POINT=32,simd定义USE_SIMD并为非 MSVC 编译器附加-msse(MSVC 下为/arch:SSE)。Make 构建在 Makefile 中的TYPEFLAGS变量里做了完全一致的映射。
重要警告(来自 FAQ):所有参与编译的代码必须使用相同的预处理器定义(FIXED_POINT与kiss_fft_scalar),混合构建环境是"输出不符合预期"的两大常见原因之一。另一个常见原因是缩放因子:浮点版本不做缩放,因此输出可能存在一个与期望值之间的常数倍关系。
测试:从单配置到全矩阵
针对上面的 int16_t 静态 OpenMP 配置,README 给出的验证命令:
make KISSFFT_DATATYPE=int16_t KISSFFT_STATIC=1 KISSFFT_OPENMP=1 testsingle使用 CMake 时则执行:
make test若要覆盖所有可能的构建配置组合,运行扩展测试套件(在 kissfft 源码树内):
sh test/kissfft-testsuite.sh注意该扩展套件耗时约 20-40 分钟(取决于运行设备),主要用于上报 bug 或验证 pull request 前做完整回归。从 Makefile 的 testall 目标 可以看到完整的测试矩阵:shared/static × double/float/int16_t/int32_t/simd × 是否启用 OpenMP,共 15 种组合(其中 simd 与 int32_t 在部分机器上可能不工作,脚本对此有注明)。测试目标定义在 lib/KissFFT/test/CMakeLists.txt,包括bm_kiss(基准测试)、ffr/tr(实数 FFT 测试)、testcpp(C++ 头文件kissfft.hh测试)与tsimd(SIMD 测试)等。
性能:作者给出的参考数据
README 的 PERFORMANCE 一节给出了作者的实测(环境为 Athlon XP 2100+、gcc 2.96、float 类型):
- 执行 10000 次 1024 点复数 FFT 耗时 0.63 秒 CPU 时间;
- 作为对照,md5sum 处理同样数据量的耗时是它的两倍;
- 变换 5 分钟 CD 音质音频不到 1 秒(nfft=1024)。
这份数据来自文档原始记录(硬件与编译器已过时),仅作量级参考,不宜与当代库做直接比较。作者同时给出两条忠告:
- 不要在使用 KISS FFT 的场合追求"世界上最快的傅里叶变换";
- 不要要求作者添加会让代码膨胀的功能。
内部原理:时间抽取混合基 FFT
README 的 UNDER THE HOOD 一节与源码相互印证,可以总结出以下实现事实:
算法类型:时间抽取(decimation-in-time)、混合基(mixed-radix)、out-of-place的 FFT。如果传入的输入输出缓冲区相同,内部会创建一个临时缓冲区来存放数据(
kiss_fft_stride中fin == fout分支即处理此情况,见 kiss_fft.c 第 371-395 行——注意注释明确说明"这并非真正的 in-place 算法")。无静态数据、线程安全:核心例程不使用任何静态数据,因此是线程安全的(
tools/目录下的工具不保证)。kiss_fft_cleanup在现代版本中已是空操作(kiss_fft.c 第 403-406 行)。缩放策略:浮点版本不做任何缩放(为了速度);定点版本则在正反变换两个方向都做缩放(为了防止溢出)。源码中每个蝶形里的
C_FIXDIV宏正是定点版本的除法缩放点(如kf_bfly2中的C_FIXDIV(*Fout,2))。优化的蝶形:对因子 2、3、4、5 提供了手工优化的蝶形函数
kf_bfly2、kf_bfly3、kf_bfly4、kf_bfly5(kiss_fft.c 第 15-189 行),其余因子回退到通用蝶形kf_bfly_generic。调度函数kf_work递归分解,因子分解由kf_factor完成(先提 4、再提 2、3,最后遍历剩余素数,见 kiss_fft.c 第 302-328 行)。OpenMP 并行:启用 OpenMP 后,
kf_work在顶层(fstride==1且p<=5且m!=1)将 p 个子任务用#pragma omp parallel for分发到多线程执行(kiss_fft.c 第 250-272 行)。实数优化原理:实数 FFT 优化只对偶数长度的 FFT 生效。它将一个长度为 nfft 的实数序列打包成两个半长 FFT 并行计算(分别放入实部与虚部),再通过旋转因子(twiddling)合并,最终得到从 DC 到 Nyquist 的 nfft/2+1 个复数频率 bin。
快速卷积:使用 overlap-scrap 方法,且做了小修改——把 scrap 放在尾部(见
kiss_fastfir.c)。旋转因子与对齐:
kiss_fft_alloc一次性分配"状态结构 + (nfft-1) 个复数旋转因子"的连续内存(kiss_fft.c 第 337-368 行),SIMD 模式下会向上对齐到 16 字节边界。
在 Serial Studio 中的实际应用:FFT 绘图与瀑布图
KISS FFT 在本仓库中被作为内置第三方依赖集成。顶层 lib/CMakeLists.txt 中通过add_subdirectory(KissFFT)引入,并以静态库形式配置(见 lib/CMakeLists.txt 第 341-379 行 与 第 611 行 的configure_third_party_lib(kissfft)),同时注明其无额外依赖。UI 层在 core/Ui/CMakeLists.txt 中链接kissfft(注释:"kissfft is the real-FFT plan behind FFTPlot and the waterfall spectrogram")。
实际消费 KISS FFT 的是两个波形可视化组件:
- FFTPlot(FFT 频谱绘图):core/Ui/UI/Widgets/FFTPlot.cpp 与 core/Ui/UI/Widgets/FFTPlot.h;
- Waterfall(瀑布频谱图):core/Ui/UI/Widgets/Waterfall.cpp 与 core/Ui/UI/Widgets/Waterfall.h。
两者的用法完全一致且完全贴合上文介绍的实数 FFT 接口,可视为官方 README 用法在生产代码中的真实范本:
- 分配 FFT plan:
m_plan = kiss_fftr_alloc(m_size, 0, nullptr, nullptr);(FFTPlot.cpp 第 104 行、Waterfall.cpp 第 496 行),第二个参数 0 表示正向 FFT,分配失败时记录警告日志; - 缓冲区布局:时域样本
m_samples大小为m_size个实标量(std::vector<kiss_fft_scalar>),频域输出m_fftOutput大小为m_size / 2 + 1个复数(std::vector<kiss_fft_cpx>)——正是文档所述"正半频谱 nfft/2+1 个 bin"; - 执行变换:
kiss_fftr(m_plan, m_samples.data(), m_fftOutput.data());(FFTPlot.cpp 第 832 行、Waterfall.cpp 第 787 行); - 释放 plan:析构函数或重置路径中调用
kiss_fftr_free(m_plan)(FFTPlot.cpp 第 159 行 与 第 539 行)。
此外,FFTPlot 在变换前会先应用加窗(Widgets::fillFftWindow填充窗函数系数,FFT 点大小在 FFTPlot.cpp 第 96-99 行 附近初始化),这也是工程化使用 FFT 时的标准做法。头文件里还包含两条编译期断言:kiss_fft_cpx必须恰好打包两个 float(static_assert(sizeof(kiss_fft_cpx) == 2 * sizeof(float)),FFTPlot.cpp 第 616 行),以及kiss_fft_scalar大小等于float(FFTPlot.cpp 第 818 行)——这从侧面印证了项目使用的是默认 float 数据类型的 KISS FFT 构建。
测试侧同样有对应的验证:应用测试目录 app/tests/CMakeLists.txt 中有一条"Real-versus-complex KissFFT equivalence"测试,专门验证 FFTPlot 与 Waterfall 所读取的 bin 上,实数 FFT 与复数 FFT 结果等价,并链接kissfft作为被测目标(见 app/tests/CMakeLists.txt 第 381-388 行)。
常见问题(FAQ)要点
README 的 FAQ 部分回答了三个高频问题:
- 能否在具有某类许可证的项目中使用 kissfft?可以,见下节许可证说明。
- 为什么输出不符合预期?两大常见原因:一是缩放——检查输出与期望值之间是否存在常数倍关系;二是混合构建环境——所有代码必须用相同的
FIXED_POINT与kiss_fft_scalar预处理器定义编译。 - 作者会帮忙写代码/调试吗?大概率不会(除非付费);作者乐于回答针对性强、切题的技术问题,但也可能指引读者去看书、论坛或其他资料。
许可证
KISS FFT 采用Revised BSD License(即 BSD 3-Clause),许可证全文见 lib/KissFFT/COPYING(仓库的许可证归档位于 LICENSES/BSD-3-Clause.txt)。按作者表述,其要点是"免费使用与修改,署名致谢,无任何担保"。该许可证一方面与 GPL 兼容,另一方面也允许闭源商业软件使用——这正是它被广泛嵌入各类开源与商业项目(包括 Serial Studio)的原因。源码头文件中的 SPDX 标识为SPDX-License-Identifier: BSD-3-Clause。
结语:什么时候该选 KISS FFT
从 README 的 TODO 列表可以看出项目仍留有已知限制:奇数长度实数 FFT 的优化尚未实现、输入/输出缩放语义尚需进一步文档化、kiss_fastfir.c尚不兼容定点类型等。综合来看,KISS FFT 适合"需要一个简单、可移植、类型灵活、许可证友好且性能够用的 FFT"的场景——正如 Serial Studio 在 FFT 频谱绘图与瀑布图中所做的:用不到百行的集成代码(分配 plan → 填窗 →kiss_fftr→ 取 bin → 释放),就获得了稳定可靠的频域分析能力。如果你需要的是为每一个平台压榨到极限的最快 FFT,那么如作者所说,它并不适合你;但如果你认同"有时候更简单反而更好",KISS FFT 的 500 行核心与几分钟的集成时间,会让你体会到这份克制的价值。
【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考