跳转至

23 fft 快速傅里叶变换

作者:孟伟 | 最后修改:2026-04-09

一、FFT(快速傅里叶变换)概述

FFT(快速傅里叶变换)是数字信号处理中的核心算法,用于将时域信号转换为频域表示。LuatOS 提供 Q15 定点和 F32 浮点两种 FFT 实现方式,满足不同应用场景的性能和精度需求。

1.1 FFT 有什么用?

FFT 模块为物联网设备提供强大的频域分析能力,主要应用包括:

  • 频谱分析:分析信号的频率成分和能量分布
  • 音频处理:音调识别、音频特征提取
  • 振动监测:机械设备故障诊断和状态监测

二、演示功能概述

本教程将使用 Air780EPM 开发板演示 FFT 的核心功能,主要包括:

(1)200Hz 正弦波测试信号生成

(2)Q15 定点 FFT 算法处理

(3)F32 浮点 FFT 算法处理

(4)两种实现方式的性能对比

(5)频谱分析和主峰频率定位

三、准备硬件环境

1、Air780EPM V1.3 版本开发板一块;

2、TYPE-C USB 数据线一根 ,Air780EPM V1.3 版本开发板和数据线的硬件接线方式为:

  • Air780EPM V1.3 版本开发板通过 TYPE-C USB 口供电;(外部供电/USB 供电 拨动开关 拨到 USB 供电一端)
  • TYPE-C USB 数据线直接插到核心板的 TYPE-C USB 座子,另外一端连接电脑 USB 口;

四、软件环境

在开始实践本示例之前,先筹备一下软件环境:

1.烧录工具: Luatools 工具

2.本demo开发测试时使用的固件为LuatOS-SoC_V2016_Air780EPM,本demo对固件版本没有什么特殊要求,所以你如果要测试本demo时,可以直接使用最新版本的内核固件Air780EPM固件Air780EHM固件;如果发现最新版本的内核固件测试有问题,可以使用我们开发本demo时使用的内核固件版本来对比测试;

3.LuatOS 需要的脚本和资源文件

脚本和资源文件:https://gitee.com/openLuat/LuatOS/tree/master/module/Air780EPM/demo/fft

准备好软件环境之后,接下来查看如何烧录项目文件到 Air780EPM 开发板,将本篇文章中演示使用的项目文件烧录到 Air780EPM 开发板中。

五、API 接口说明

详细 FFT API 文档请参考:https://docs.openluat.com/osapi/core/fft/

六、代码示例介绍

6.1 数据格式说明

6.1.1 Q15 定点格式

  • 表示范围:-1.0 到 0.999969482421875
  • 存储格式:16 位有符号整数
  • 优势:在无浮点单元的 MCU 上高效运行

6.1.2 F32 浮点格式

  • 表示范围:标准单精度浮点数
  • 精度:更高的计算精度
  • 适用场景:对精度要求较高的应用

6.2 FFT 核心测试代码

function test_fft_fun()
    if not fft then
        -- 如果不支持FFT库,进入死循环并提示错误
        while true do
            sys.wait(1000)  -- 等待1秒
            log.info("bsp", "此BSP不支持fft库,请检查")  -- 输出错误信息
        end
    end

    -- FFT 参数配置段

    -- 设置FFT基本参数
    local N = 2048      -- FFT 点数:决定频率分辨率和计算复杂度,必须是2的幂次方
    local fs = 2000     -- 采样频率 (Hz):根据奈奎斯特定理,可分析的最高频率为fs/2=1000Hz
    local freq = 200    -- 测试信号频率 (Hz):生成一个200Hz的正弦波作为测试信号

    -- 输出测试开始信息和参数
    log.info("fft", "q15 测试开始", "N=" .. N, "fs=" .. fs, "freq=" .. freq)

    -- 内存分配段:为FFT计算准备缓冲区
    -- 分配 zbuff 缓冲区:
    -- 使用16位整数缓冲区来存储Q15格式的数据
    -- 每个复数点需要2个16位整数(实部和虚部),所以总大小为 N * 2 * 2 字节
    local real_i16 = zbuff.create(N * 2)  -- 实部缓冲区:存储时域信号的实部
    local imag_i16 = zbuff.create(N * 2)  -- 虚部缓冲区:存储时域信号的虚部(初始为0)

    -- 测试信号生成段:生成一个200Hz的正弦波测试信号
    -- 生成 U12 整数正弦波并写入缓冲区(避免浮点预处理干扰)
    -- U12格式:12位无符号整数,范围0-4095,2048对应0电平
    for i = 0, N - 1 do
        -- 计算时间点:i/fs 表示第i个样本的时间(秒)
        local t = i / fs

        -- 生成200Hz正弦波:sin(2π * 频率 * 时间)
        local x = math.sin(2 * math.pi * freq * t)

        -- 将浮点数转换为U12格式:
        -- 2048为直流偏置(中间值),2047为幅度范围
        -- +0.5是为了四舍五入到最接近的整数
        local val = math.floor(2048 + 2047 * x + 0.5)

        -- 数值范围限制:确保在U12的有效范围内(0-4095)
        if val < 0 then val = 0 end
        if val > 4095 then val = 4095 end

        -- 将数据写入缓冲区:
        -- seek定位到正确的位置,每个样本占2字节
        -- writeU16写入16位无符号整数(低12位有效)
        real_i16:seek(i * 2, zbuff.SEEK_SET); real_i16:writeU16(val)  -- 实部写入正弦波数据
        imag_i16:seek(i * 2, zbuff.SEEK_SET); imag_i16:writeU16(0)    -- 虚部写入0(实数信号)
    end

    -- 旋转因子生成段:为Q15 FFT计算准备旋转因子
    -- 生成 Q15 旋转因子到 zbuff(避免任何浮点旋转因子)
    -- 旋转因子用于FFT计算中的复数乘法,长度是FFT点数的一半
    local Wc_q15 = zbuff.create((N // 2) * 2)  -- 余弦旋转因子缓冲区
    local Ws_q15 = zbuff.create((N // 2) * 2)  -- 正弦旋转因子缓冲区(实际存储-sin值)

    -- 生成Q15格式的旋转因子表
    fft.generate_twiddles_q15_to_zbuff(N, Wc_q15, Ws_q15)

    -- 使用定点Q15内核执行FFT
    -- 执行 Q15 FFT(输入为 U12),显式传入 Q15 旋转因子
    local t0 = mcu.ticks()  -- 记录开始时间

    -- 执行FFT计算:
    -- real_i16, imag_i16: 输入输出缓冲区(原地计算)
    -- N: FFT点数
    -- Wc_q15, Ws_q15: Q15格式旋转因子
    -- {core = "q15", input_format = "u12"}: 使用Q15定点内核,输入格式为U12
    fft.run(real_i16, imag_i16, N, Wc_q15, Ws_q15, { core = "q15", input_format = "u12" })

    local t1 = mcu.ticks()  -- 记录结束时间
    -- 输出Q15 FFT计算耗时
    log.info("fft", "q15 FFT 完成", "耗时:" .. (t1 - t0) .. "ms")

    -- 分析FFT结果,找到主要频率成分
    -- 扫描前半部分频谱(0 ~ fs/2),寻找主峰
    -- 由于频谱的对称性,只需要分析前N/2个点
    local peak_k, peak_pow = 1, -1  -- peak_k: 峰值位置, peak_pow: 峰值功率

    -- 遍历所有频率bin(跳过直流分量k=0)
    for k = 1, (N // 2) - 1 do
        -- 定位到第k个频率点的实部和虚部
        real_i16:seek(k * 2, zbuff.SEEK_SET)
        imag_i16:seek(k * 2, zbuff.SEEK_SET)

        -- 读取实部和虚部值(Q15格式)
        local rr = real_i16:readI16()  -- 实部
        local ii = imag_i16:readI16()  -- 虚部

        -- 计算功率谱:|X[k]|² = real² + imag²
        -- 功率谱表示该频率成分的能量大小
        local p = rr * rr + ii * ii

        -- 更新峰值信息
        if p > peak_pow then
            peak_pow = p   -- 更新最大功率值
            peak_k = k     -- 更新峰值位置
        end
    end

    -- 计算峰值对应的实际频率:频率 = bin索引 * 频率分辨率
    -- 频率分辨率 = 采样频率 / FFT点数
    local peak_freq = (peak_k) * fs / N

    -- 输出主峰频率信息
    -- peak_k: 峰值所在的bin索引
    -- peak_freq: 计算出的实际频率(应该接近200Hz)
    log.info("fft", "主峰(Hz/bin)", string.format("%.2f", peak_freq), peak_k)

    -- 使用浮点F32内核进行相同计算,对比性能
    -- 比较:使用 f32 内核处理相同输入(验证 q15 与 f32 结果一致性)
    -- 复制相同的 U12 输入到新的 zbuff(浮点格式)

    -- 为浮点FFT分配缓冲区:每个浮点数占4字节
    local real_f32 = zbuff.create(N * 4)  -- 实部缓冲区(浮点)
    local imag_f32 = zbuff.create(N * 4)  -- 虚部缓冲区(浮点)

    -- 将Q15数据复制到浮点缓冲区
    for i = 0, N - 1 do
        -- 从Q15缓冲区读取U12数据
        real_i16:seek(i * 2, zbuff.SEEK_SET)
        local val = real_i16:readU16()

        -- 写入浮点缓冲区(直接写入U12原始值,不进行格式转换)
        real_f32:seek(i * 4, zbuff.SEEK_SET)
        imag_f32:seek(i * 4, zbuff.SEEK_SET)
        real_f32:writeF32(val)  -- 实部写入原始U12值
        imag_f32:writeF32(0.0)  -- 虚部写入0.0
    end

    -- 生成 f32 旋转因子(浮点格式)
    -- 返回Lua table格式的旋转因子,而不是zbuff
    local Wc, Ws = fft.generate_twiddles(N)

    -- 执行 f32 FFT(输入为 U12)
    local t2 = mcu.ticks()  -- 记录开始时间

    -- 执行浮点FFT计算:
    -- 使用默认的f32内核,输入格式为u12
    fft.run(real_f32, imag_f32, N, Wc, Ws, { input_format = "u12" })

    local t3 = mcu.ticks()  -- 记录结束时间
    local dt_f32 = (t3 - t2)  -- 计算浮点FFT耗时

    -- 输出浮点FFT计算耗时
    log.info("fft", "f32 FFT 完成", "耗时:" .. dt_f32 .. "ms")

    -- 结果总结段:对比两种实现的性能


    -- 总结耗时对比:Q15定点 vs F32浮点
    -- 在没有硬件浮点加速的嵌入式设备上,Q15会比F32快很多,
    -- 而780和8000系列都没有硬件浮点加速,建议使用q15计算提高速度,但如果追求计算精度,仍然可以用浮点计算
    log.info("fft", "对比(q15 vs f32, ms)", string.format("%d / %d", (t1 - t0), dt_f32))
end

sys.taskInit(test_fft_fun)

6.3 功能验证

通过 luatools 工具可以观察到:

  • 频率准确性:检测到的主峰频率应接近 200Hz
  • 性能对比:Q15 FFT 应比 F32 FFT 更快

七、总结

至此,本教程详细介绍了 LuatOS FFT 模块的使用方法,包括 Q15 定点和 F32 浮点两种实现方式。

FFT 模块关键特性:

  • 支持定点 Q15 和浮点 F32 两种计算方式
  • 提供完整的频谱分析功能
  • 优化的性能适合嵌入式设备
  • 灵活的参数配置满足不同应用需求