2026 ADI CodeFusion 竞赛 - 基于MAX32690制作简易频谱仪
该项目使用了MAX32690,实现了简易频谱仪的设计,它的主要功能为:使用ADC 通过 DMA 进行高速数据采样,做FFT实现频谱计算,在 TFT LCD 进行实时频谱图的展示。
标签
ADC
FFT
频谱
DMA
姜小豆
更新2026-09-29
18

基于MAX32690的简易频谱仪


1. 简介

参加了一个由 ADI 赞助硬禾举办的 2026ADI CodeFusion Studio 活动。开发板选择的是 ADI 的 MAX32690EVKIT。

1)所选任务

打算自命题

任务题目:简易频谱仪

描述:做一个频谱仪,实现低频频率的识别。流程为:使用ADC 通过 DMA 进行高速数据采样,做FFT实现频谱计算,在 TFT LCD 进行实时频谱图的展示。

2)硬件介绍

资源汇总:


板卡介绍

MAX32690EVKit板卡美照:

image-20260919231210593.png

MAX32690 芯片介绍

MAX32690 有两个核心,主处理器是带 FPU 的 ARM-CM4,还有 RISC-V 协处理器。

主要参数

  • Ultra-Efficient Microcontroller for Battery-Powered Applications
    • 120MHz Arm Cortex-M4 Processor with FPU
    • Ultra-Low-Power, 32-Bit RISC-V (RV32) Coprocessor Available to Offload Data Processing
    • Multiple Internal Low-Power Oscillators
    • 32.768kHz RTC Clock (Requires External Crystal)
    • 3.25MB Internal Flash, 1MB Internal SRAM
    • 85μW/MHz ACTIVE Mode at 1.1V
    • 1.8V and 3.3V I/O with No Level Translators
    • External Flash and SRAM Expansion Interfaces

3.25MB flash,1MB 内存,真不错。


题外话:ADI 帆布包很好看!


系统框图

image-20260919234150585.png


软件流程:

image-20260919234718915.png

最终的指标:

采样率 Fs        ≈ 180 kSPS
FFT点数 N         = 256
频点间隔 Δf       = Fs / N
                ≈ 703.125 Hz
​
频谱范围         ≈ 0~90 kHz
有效显示bin       = 1~127
单帧采样时间     ≈ 256 / 180000
                ≈ 1.42 ms


2. IDE使用

本次指定使用 CodeFusion Studio,需要在VSCODE上安装插件并安装SDK。安装步骤跳过。

编译后使用OpenOCD进行擦除和烧录:OpenOCD是什么?J-Link、J-Trace、OpenJTAG的区别

image-20260916061728389.png

调试器是一块基于MAX32650做的SWD调试器,小小的模块承载大大的梦想。

image-20260919235709058.png


3. 频谱仪功能开发流程

3.1 通过DMA采集ADC

通过DMA采集ADC,输出稳定数据。


​
无输入 0x65d 左右
​
0~1.2V基本上是采样值可以从0x0~0xF90
但是我的直流稳压电源在低压时好像不太稳定
0V     0x000~0x021
0.1V   0x140~0x19b(甚至有0x0F5在前几秒)
使用信号源选择方波,占空比99%进行测试:
​
1.2 F0E
​
1.1 DCC
​
1.0 C8B
​
0.9 B4E
​
0.8 A0F
​
0.7 8D5
​
0.6 791
​
0.5 658
​
数据还是比较线性
​
​


测量测样率,通过led判断采样,开启DMA前开LED,在中断中关闭LED:

关闭输出,每次采集256点后, 间隔是1.92ms, 高电平1.42ms。


下图为1k数据下的连续采样和单包256个数据的图像:

image-20260919195821436.png

  • 采集占空比:1.42 / 1.92 ≈ 74%
  • 块内实际采样率:Fs=256 / 1.42 ms = 180 kSPS,ADC每秒产生约18万个采样点。根据内奎斯特定律,最高采样率为 Fs/2=90KHz
  • 每个采样点间隔约:Ts=256/1.42 ms=5.55 μs

当前180kSPS采样率基本可用,后面可以提高速率,现在ADC时钟是16分频,可以减小分频。(但不能太快,ADC规定最多25MHz, 当前的时钟为120MHz,起码8分频才行)

     // 查看当前频率  120MHz
  SystemCoreClockUpdate();
  printf("HCLK = %lu Hz\n", (unsigned long)SystemCoreClock);


当前256点FFT大致为:

​image.png

​


3.2 进行FFT进行频率判断

1)ARM CMSIS-DSP库引入

为了使用ARM FFT,需要重新引入CMSIS-DSP库。

MAX32690是Cortex-M4F,先用CMSIS-DSP的单精度浮点实数FFT:arm_rfft_fast_f32() ,这是为实数输入优化的FFT;官方说明支持32、64、128、256一直到4096点,并使用压缩格式存放实数FFT结果。CMSIS-DSP Real FFT官方说明

先在工程里搜索:

arm_math.h
arm_rfft_fast_f32

如果能够找到,说明SDK中已经包含CMSIS-DSP;如果只找到头文件、链接时报未定义,再处理CMSIS-DSP库的编译链接。

看到example下有arm-dsp,其中有fft, 打开看看。其中,mk下有

​
# Include the CMSIS-DSP library
LIB_CMSIS_DSP = 1

增加到当前目录下,可以正确识别 arm_math.h 头文件,并编译成功。


2)在MAX32690上实现FFT


第一步:定义缓冲区

#include <math.h>
#include "arm_math.h"
​
#define FFT_SIZE       256U
#define ADC_FS_HZ     182400.0f
#define PI_F           3.14159265358979323846f
​
static arm_rfft_fast_instance_f32 fft_instance;
​
static float32_t fft_input[FFT_SIZE];
static float32_t fft_output[FFT_SIZE];
static float32_t fft_magnitude[FFT_SIZE / 2 + 1];
static float32_t hann_window[FFT_SIZE];

这里需要三个不同的数组:

  • adc_val[]:DMA写入的原始ADC数据;
  • fft_input[]:去直流、加窗后的浮点数据;
  • fft_output[]:FFT计算出的复数结果;
  • fft_magnitude[]:最终各频率的幅度。

第二步:初始化FFT和窗函数

只在程序启动时执行一次:

int fft_init(void)
{
  arm_status status;
​
  status = arm_rfft_fast_init_f32(&fft_instance, FFT_SIZE);
​
  if (status != ARM_MATH_SUCCESS) {
      return -1;
  }
​
  for (uint32_t i = 0; i < FFT_SIZE; i++) {
      hann_window[i] =
          0.5f -
          0.5f * cosf((2.0f * PI_F * i) /
                      (FFT_SIZE - 1));
  }
​
  return 0;
}

在 main() 初始化阶段调用:

if (fft_init() != 0) {
  printf("FFT init failed\n");
  while (1) {}
}

第三步:ADC数据预处理

DMA完成后调用:

static void fft_prepare_input(const int *adc_data)
{
  float32_t mean = 0.0f;
​
  /* 1. 计算256点平均值 */
  for (uint32_t i = 0; i < FFT_SIZE; i++) {
      uint32_t sample = ((uint32_t)adc_data[i]) & 0x0FFF;
      mean += (float32_t)sample;
  }
​
  mean /= (float32_t)FFT_SIZE;
​
  /* 2. 去直流并乘Hann窗 */
  for (uint32_t i = 0; i < FFT_SIZE; i++) {
      uint32_t sample = ((uint32_t)adc_data[i]) & 0x0FFF;
​
      fft_input[i] =
          ((float32_t)sample - mean) *
          hann_window[i];
  }
}

如果你使用的是 DATA_STATUS 格式,还应该先检查 INVALID 位;无效数据不能送进FFT。如果确实是纯 DATA 格式,则取低12位即可。

第四步:执行FFT

arm_rfft_fast_f32(&fft_instance,
                fft_input,
                fft_output,
                0);

最后一个参数:

0:正向FFT
1:逆FFT

我们需要的是正向FFT,所以填0。

第五步:理解FFT输出格式

对于256点实数FFT:

  • fft_output[0]:0 Hz,也就是直流;
  • fft_output[1]:奈奎斯特频率;
  • fft_output[2*k]:第 kk 格的实部;
  • fft_output[2*k+1]:第 kk 格的虚部。

第 k 格的幅度为:

​image.png

代码:

static void fft_calculate_magnitude(void)
{
  fft_magnitude[0] = fabsf(fft_output[0]);
​
  for (uint32_t k = 1; k < FFT_SIZE / 2; k++) {
      float32_t real = fft_output[2 * k];
      float32_t imag = fft_output[2 * k + 1];
​
      fft_magnitude[k] =
          sqrtf(real * real + imag * imag);
  }
​
  /* Nyquist频点是特殊的纯实数 */
  fft_magnitude[FFT_SIZE / 2] =
      fabsf(fft_output[1]);
}

初期只是找频率峰值,不必急着做精确幅度归一化。

第六步:寻找最大频率峰值

不要把 bin 0 算进去,因为它是直流:

static void fft_find_peak(void)
{
  uint32_t peak_bin = 1;
  float32_t peak_value = fft_magnitude[1];
​
  for (uint32_t k = 2; k < FFT_SIZE / 2; k++) {
      if (fft_magnitude[k] > peak_value) {
          peak_value = fft_magnitude[k];
          peak_bin = k;
      }
  }
​
  uint32_t frequency_hz =
      (uint32_t)(((float32_t)peak_bin *
                  ADC_FS_HZ /
                  FFT_SIZE) + 0.5f);
​
  printf("peak bin=%lu, frequency=%lu Hz\n",
          (unsigned long)peak_bin,
          (unsigned long)frequency_hz);
}

整个调用过程为:

while (1) {
  /* 启动256点DMA */
  dma_done = 0;
​
  ADC_StartBlockDMA(...);
​
  while (!dma_done) {}
​
  if (dma_error == E_NO_ERROR) {
      fft_prepare_input(adc_val);
​
      arm_rfft_fast_f32(&fft_instance,
                        fft_input,
                        fft_output,
                        0);
​
      fft_calculate_magnitude();
      fft_find_peak();
  }
}

DMA必须已经完成,才能处理当前 adc_val[],否则CPU读数组时DMA还在修改它。

初次测试预期

按现在约182.4 kSPS计算:

信号源频率

预计FFT表现

700 Hz

峰值主要在bin 1,也就是约712 Hz

1 kHz

能量分散在bin 1、bin 2附近

2.85 kHz

峰值比较集中地出现在bin 4

5.7 kHz

峰值比较集中地出现在bin 8

因此,第一次先输入约2.85 kHz正弦波,检查是否得到:

peak bin=4, frequency=2850 Hz

然后再换回1 kHz观察泄漏。

注意当前256点FFT不能精确报告1 kHz。它可能只报告712 Hz或1425 Hz,因为频率格子宽达约712 Hz。这不是采样率不够,而是采样时间太短。

如果改为1024点:

​image.png

频率判断就会细很多。


进行测试

把上面的FFT计算和peak获取加上后,测试从1k~50k的信号频率均有显示。不过受限于频点间隔 703Hz​,所以只能以这个倍数显示。


3.3 驱动屏幕进行最大频率的显示

首先跑TFT demo, 然后修改为频谱显示,完整代码如下:

注因为只用了256的前一半,所以要测试显示到中间下标应该是64。

​
/***** Includes *****/
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "icc.h"
#include "mxc_device.h"
#include "mxc_delay.h"
#include "led.h"
#include "pb.h"
#include "tft_st7735.h"
​
void TFT_Print(char *str, int len, int x, int y, int font)
{
  // fonts id
  text_t text;
  text.data = str;
  text.len = len;
  MXC_TFT_PrintFont(x, y, font, &text, NULL);
}
​
#include <math.h>
#include <stdint.h>
#include <string.h>
#include "tft_st7735.h"
​
#define FFT_SIZE       256
#define FFT_BINS       (FFT_SIZE / 2)
​
/* 绘图区,底部留出频率文字的位置 */
#define PLOT_X0       2
#define PLOT_X1       125
#define PLOT_Y0       3
#define PLOT_Y1       108
​
#define PLOT_WIDTH     (PLOT_X1 - PLOT_X0 + 1)
#define PLOT_HEIGHT   (PLOT_Y1 - PLOT_Y0 + 1)
​
/* RGB565颜色 */
#define COLOR_BG       0x0000
#define COLOR_AXIS     0xFFFF
#define COLOR_TRACE   0x07E0
​
/*
* 第一版先使用相对dB。
* 后续完成ADC幅度标定后,再改成真正的dBFS。
*/
#define DB_MAX         0.0f
#define DB_MIN       (-80.0f)
​
static int16_t old_y[PLOT_WIDTH];
static float smooth_db[FFT_BINS];
static uint8_t first_frame = 1;
​
static float clamp_float(float value, float min, float max)
{
  if (value < min) {
      return min;
  }
​
  if (value > max) {
      return max;
  }
​
  return value;
}
​
static int spectrum_db_to_y(float db)
{
  float ratio;
​
  db = clamp_float(db, DB_MIN, DB_MAX);
​
  ratio = (db - DB_MIN) / (DB_MAX - DB_MIN);
​
  /*
    * 屏幕坐标向下增大:
    * DB_MAX映射到顶部,DB_MIN映射到底部。
    * 留一个像素,防止曲线擦掉边框。
    */
  return (PLOT_Y1 - 1) -
          (int)(ratio * (float)(PLOT_HEIGHT - 3));
}
​
void Spectrum_Init(void)
{
  int i;
  char buff[] = "test";
  // MXC_TFT_SetBackGroundColor(COLOR_BG);
  MXC_TFT_SetBackGroundColor(COLOR_BG); // Clear screen
  MXC_TFT_SetForeGroundColor(WHITE); // set chars to white
  MXC_TFT_ClearScreen();
​
  /* 频谱区域边框 */
  MXC_TFT_Rectangle(PLOT_X0,
                    PLOT_Y0,
                    PLOT_X1,
                    PLOT_Y1,
                    COLOR_AXIS);
​
  /*
    * Fs = 180kHz,所以横轴范围为0~90kHz。
    * 如果文字太挤,可以暂时不显示。
    */
   
  TFT_Print("0", 1, 2, 113, (int)&Liberation_Sans12x12[0]);
  TFT_Print("45k", 3, 52, 113, (int)&Liberation_Sans12x12[0]);
  TFT_Print("90k", 3, 98, 113, (int)&Liberation_Sans12x12[0]);
​
  for (i = 0; i < PLOT_WIDTH; i++) {
      old_y[i] = PLOT_Y1 - 1;
  }
​
  for (i = 0; i < FFT_BINS; i++) {
      smooth_db[i] = DB_MIN;
  }
​
  first_frame = 1;
}
​
void Spectrum_Draw(const float *fft_mag)
{
  int x;
  int k;
  int k_start;
  int k_end;
  uint16_t new_y[PLOT_WIDTH];
​
  float max_mag;
  float db;
  float pixel_db;
​
  /*
    * 找到这一帧的最大幅值。
    *
    * 忽略bin 0,因为它主要是ADC直流偏置。
    * 第一版用相对最大值显示,因此最强信号显示为0dB。
    */
  max_mag = 0.000001f;
​
  for (k = 1; k < FFT_BINS; k++) {
      if (fft_mag[k] > max_mag) {
          max_mag = fft_mag[k];
      }
  }
​
  /*
    * 转换为相对dB并进行平滑。
    * 0.75代表保留上一帧,0.25代表加入当前帧。
    */
  for (k = 1; k < FFT_BINS; k++) {
      db = 20.0f * log10f((fft_mag[k] + 0.000001f) / max_mag);
​
      db = clamp_float(db, DB_MIN, DB_MAX);
​
      smooth_db[k] =
          0.75f * smooth_db[k] +
          0.25f * db;
  }
​
  /*
    * 把127个有效bin映射到屏幕横向像素。
    *
    * bin 0不显示。
    * 如果多个bin落到同一个像素,取其中最大的值,
    * 这样窄带尖峰不会被平均掉。
    */
  for (x = 0; x < PLOT_WIDTH; x++) {
      k_start = 1 +
                x * (FFT_BINS - 1) / PLOT_WIDTH;
​
      k_end = 1 +
              (x + 1) * (FFT_BINS - 1) / PLOT_WIDTH;
​
      if (k_end <= k_start) {
          k_end = k_start + 1;
      }
​
      if (k_end > FFT_BINS) {
          k_end = FFT_BINS;
      }
​
      pixel_db = DB_MIN;
​
      for (k = k_start; k < k_end; k++) {
          if (smooth_db[k] > pixel_db) {
              pixel_db = smooth_db[k];
          }
      }
​
      new_y[x] = spectrum_db_to_y(pixel_db);
  }
​
  /*
    * 先用背景色擦除上一条曲线。
    * 第一帧没有旧曲线,不需要擦除。
    */
  //if (!first_frame) {
      for (x = 0; x < PLOT_WIDTH - 1; x++) {
          MXC_TFT_Line(PLOT_X0 + x,
                        old_y[x],
                        PLOT_X0 + x + 1,
                        old_y[x + 1],
                        COLOR_BG);
      }
  //}
​
  /* 绘制当前频谱 */
  for (x = 0; x < PLOT_WIDTH - 1; x++) {
      MXC_TFT_Line(PLOT_X0 + x,
                    new_y[x],
                    PLOT_X0 + x + 1,
                    new_y[x + 1],
                    COLOR_TRACE);
  }
​
  /*
    * 擦除曲线时可能碰到边框,所以重新画边框。
    */
  MXC_TFT_Rectangle(PLOT_X0,
                    PLOT_Y0,
                    PLOT_X1,
                    PLOT_Y1,
                    COLOR_AXIS);
​
  memcpy(old_y, new_y, sizeof(old_y));
  first_frame = 0;
}
​
int main(void)
{
  MXC_Delay(MXC_DELAY_SEC(2));
​
  /* Enable cache */
  MXC_ICC_Enable(MXC_ICC0);
​
  /* Set system clock to 100 MHz */
  MXC_SYS_Clock_Select(MXC_SYS_CLOCK_IPO);
  SystemCoreClockUpdate();
​
  /* Initialize TFT display */
  MXC_TFT_Init();
  // TFT_test();
  Spectrum_Init();
  float fft_mag[256] = {0};
  fft_mag[14] = 10000.0f;
  fft_mag[64] = 45000.0f;
  while(1){
       
      Spectrum_Draw(fft_mag);
      MXC_Delay(10000);
  }
  return 0;
}
​


最终版本加上了peak,核心绘制函数为:

void Spectrum_Draw(const float *fft_mag)
{
  int x;
  int k;
  int k_start;
  int k_end;
  uint16_t new_y[PLOT_WIDTH];
​
  float max_mag;
  float db;
  float pixel_db;
  int max_bin;
​
  /*
    * 找到这一帧的最大幅值。
    *
    * 忽略bin 0,因为它主要是ADC直流偏置。
    * 第一版用相对最大值显示,因此最强信号显示为0dB。
    */
  max_mag = 0.000001f;
​
  for (k = 1; k < FFT_BINS; k++) {
      if (fft_mag[k] > max_mag) {
          max_mag = fft_mag[k];
      }
  }
​
  /*
    * 转换为相对dB并进行平滑。
    * 0.75代表保留上一帧,0.25代表加入当前帧。
    */
  for (k = 1; k < FFT_BINS; k++) {
      db = 20.0f * log10f((fft_mag[k] + 0.000001f) / max_mag);
​
      db = clamp_float(db, DB_MIN, DB_MAX);
​
      smooth_db[k] =
          0.75f * smooth_db[k] +
          0.25f * db;
  }
  max_bin = Spectrum_FindMaxBin();
​
  /*
    * 把127个有效bin映射到屏幕横向像素。
    *
    * bin 0不显示。
    * 如果多个bin落到同一个像素,取其中最大的值,
    * 这样窄带尖峰不会被平均掉。
    */
  for (x = 0; x < PLOT_WIDTH; x++) {
      k_start = 1 +
                x * (FFT_BINS - 1) / PLOT_WIDTH;
​
      k_end = 1 +
              (x + 1) * (FFT_BINS - 1) / PLOT_WIDTH;
​
      if (k_end <= k_start) {
          k_end = k_start + 1;
      }
​
      if (k_end > FFT_BINS) {
          k_end = FFT_BINS;
      }
​
      pixel_db = DB_MIN;
​
      for (k = k_start; k < k_end; k++) {
          if (smooth_db[k] > pixel_db) {
              pixel_db = smooth_db[k];
          }
      }
​
      new_y[x] = spectrum_db_to_y(pixel_db);
  }
​
  /* 1. 擦除旧的Peak文字和标记 */
  Spectrum_ClearMaxPeak();
  /*
    * 2. 擦除旧频谱曲线
    * 先用背景色擦除上一条曲线。
    * 第一帧没有旧曲线,不需要擦除。
    */
  if (!first_frame) {
      for (x = 0; x < PLOT_WIDTH - 1; x++) {
          MXC_TFT_Line(PLOT_X0 + x,
                        old_y[x],
                        PLOT_X0 + x + 1,
                        old_y[x + 1],
                        COLOR_BG);
      }
  }
​
  /* 3. 绘制当前频谱 */
  for (x = 0; x < PLOT_WIDTH - 1; x++) {
      MXC_TFT_Line(PLOT_X0 + x,
                    new_y[x],
                    PLOT_X0 + x + 1,
                    new_y[x + 1],
                    COLOR_TRACE);
  }
​
  /* 4. 最后标记最大峰,并显示频率 */
  Spectrum_DrawMaxPeak(max_bin);
​
  /*
    * 擦除曲线时可能碰到边框,所以重新画边框。
    */
  MXC_TFT_Rectangle(PLOT_X0,
                    PLOT_Y0,
                    PLOT_X1,
                    PLOT_Y1,
                    COLOR_AXIS);
​
  memcpy(old_y, new_y, sizeof(old_y));
  first_frame = 0;
}
​



4. 频谱仪功能测试与演示

Code Fusion 软件使用截图:

image-20260919224152048.png


功能演示截图:

信号源16kHz

image-20260919225444822.png

45kHz

image-20260919225515874.png

5. 总结

通过本次实验,学会了Code Fusion的使用,作为VsCode插件来说,好用又快速。使用了ADI SDK进行开发,示例完整,添加ARM-DSP很方便。对FFT和频谱仪也多了一些了解。在自己的里程碑上也算是做了个简易频谱仪。

唯二不足的是这次没有尝试多核通信以及没有使用Zypher RTOS。


附件下载
max32690_spectrum_V2026092001.rar
团队介绍
无
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号