Funpack5-3- 双 MCU实现MAX32690 实时音频分析系统
该项目使用了MAX32690,MSDK,ARM-DSP,实现了回环音频分析仪的设计,它的主要功能为:使用载板自带的MAX9867 对声音进行回环播放和实时分析。
标签
嵌入式系统
Funpack活动
显示
开发板
USB
yunswj
更新2026-08-21
43


项目梗概

项目为主从机设计,MAX32690 做声音回环播放和 FFT 计算,通过内置的低功耗串口将计算好的频点转换成二进制协议发送给 ESP32-S3;在 S3 端充分利用双核,一个核心做协议数据的接收和解码,另外一个核心完成 UI 以及对串口数据的转发,经由 USB-CDC 给 PC 做上位机的控制。

image-20260818123348074



系统目标和分工

MAX32690:I2S 音频采集、DMA 环形缓冲、256 点时域窗、Hann 加窗、CMSIS-DSP RFFT、Q15 定量化、协议封装、LPUART0B 发送。

ESP32-S3:UART 接收、Magic 重同步、长度检查、CRC16、数据快照、ST7789 驱动、时域/FFT 可视化和运行指标。

分工原则是让 MAX32690 专注采集和 DSP,避免高频刷新自带屏幕;S3 利用双核、DMA 和较大 RAM 处理显示。
>





引脚与电气参数

串口最终连接

功能

MAX32690

ESP32-S3

方向

TX 数据

P3.1 / LPUART0B_TX

GPIO36 / UART1_RX

MAX → S3

RX/预留控制

P3.0 / LPUART0B_RX

GPIO35 / UART1_TX

S3 → MAX

参考

GND

GND

必须共地

串口为 3.3 V TTL,921600 baud,8N1,无流控。

ST7789

ST7789

S3 GPIO

SCLK

4

MOSI

5

RST

6

DC

7

CS

15

屏幕为 172×320 RGB565,X gap=34,SPI=40 MHz,使用 ESP-IDF esp_lcd 和 SPI DMA。

MAX32690 固件设计

LPUART0B 初始化

MSDK 中 MXC_UART3 对应 MAX32690 LPUART0;固件调用:

MXC_UART_Init(MXC_UART3, 921600, MXC_UART_IBRO_CLK);

MSDK 随后使能 UART3 时钟,选择 IBRO,并将 P3.0/P3.1 配成 ALT2;初始化失败会进入 blink_halt(),不会在错误状态下继续发送。

I2S DMA 与 PCM

标称采样率:48000 Hz。

DMA 半区:64 个 32-bit I2S 字。

RX DMA 回调切换 ping-pong 半区,提取 24-bit 样本并缩放成 int16_t

PCM 进入 512 点环形缓冲,前台始终取最新 256 点;captured_samples 作为样本时间戳和数据流调度基准。

DMA 回调只做短时间复制和指针切换;FFT、CRC 和 UART 发送在前台完成,以缩短中断占用时间。
>

TIME 帧

TIME 负载为 256 个 int16_t

payload = 256 × 2 = 512 bytes
window = 256 / 48000 = 5.333 ms

FFT 路径

256 个 PCM 转 float32_t并归一化;乘 Hann 窗,降低频谱泄漏;arm_rfft_fast_f32() 计算 256 点实数 FFT;计算正频率一半的 128 个幅值;幅值限幅并定量化到 0~65535 uint16_t

频率分辨率 = 48000 / 256 = 187.5 Hz/bin
FFT payload = 128 × 2 = 256 bytes

MAX 自带屏幕与 SW2

MAX 屏幕只显示 Fs、FFT 点数、输出率、UART、RX/TX 引脚和 STREAM:ON/PAUSE;SW2/P4.0 经软件去抖后切换数据流;I2S DMA 回环不随数据流暂停。

二进制协议

数字均为小端序。

Offset

Size

字段

说明

0

2

Magic

A5 5A

2

1

Version

1

3

1

Type

1=TIME,2=FFT

4

4

Sequence

逐帧增加

8

4

Sample index

样本时间戳

12

4

Sample rate

48000

16

2

Count

元素数

18

2

Payload length

负载字节数

20

N

Payload

TIME/FFT

20+N

2

CRC

CRC16-CCITT

CRC 初值 0xFFFF,多项式 0x1021,范围从 Magic 到 payload 末尾。

Header

Payload

CRC

总长

TIME

20

512

2

534 B

FFT

20

256

2

278 B

TIME+FFT 一组

40

768

4

812 B

Magic+长度+CRC 允许负载包含任意字节,且在丢字节后重新同步。

ESP32-S3 固件设计

双核任务

任务

CPU

Priority

职责

max32690_rx

0

12

UART、Magic、长度、CRC、数据更新

scope_display

1

6

数据快照、绘图、SPI LCD、FPS 统计

RX 任务优先级更高,避免全屏刷新阻塞串口;TIME/FFT 数组用 mutex 保护;显示任务短时间复制快照后立即释放锁。

解析器

UART RX buffer = 8192 B;逐字节搜索 A5 5A;先收齐 20-byte header,检查 version 和 payload_length <= 512;按长度收 payload+CRC,CRC 通过后才检查 type/count/length 并更新数据;超时、长度或 CRC 错误后放弃当前帧并重新搜索 Magic。

调试版启动时打印前 128 个原始 UART 字节,这是发现截帧 Bug 的关键;代价是启动时丢弃 128 B,之后会自动重同步;生产版应用宏关闭。
>

显示管线

S3 在 RAM 内合成一幅 172×320 RGB565 DMA framebuffer,再一次性传到 ST7789,避免大量小 SPI 数据传输和局部显示撤裂。

界面包含:



顶部:红/绿链路指示、真实 FPS、RX 合法帧率、E 每秒错误数。
>
WAVE:网格、中心线、自动缩放、深青+亮青双层波形。
>
中间栏:P:xx 峰值百分比、F:xxxx 主频、绿/橙/红电平条。
>
FFT:43 根对数幅度频谱柱,绿/黄/橙/红分层、白色顶线和主频位置标记。

时域缩放使用当前 256 点绝对峰值,最小基准限制为 512,再留 25% 顶部余量。

P = abs_peak / 32767 × 100%

P>60% 电平条变橙,P>85% 变红。主频忽略 DC bin 0,在 bin 1~127 中搜索最大幅值:

Fdominant = dominant_bin × 48000 / 256

频谱高度采用 log10(1 + 9×magnitude/65535),便于同时看到强主频和弱谐波。

Bug 修复

完全无数据

初始日志为 frames=0 errors=0;静态确认 MXC_UART3、P3.0/P3.1 ALT2、UART3 时钟和发送函数都已编入 ELF;再交换 S3 软件 RX/TX 定义;改为 RX=GPIO36 后错误计数开始持续增长,证明 GPIO36 上有串口信号,并确定了最终交叉接线。

有信号但没有合法帧

交换后 errors 上升而 frames=0;通过直接对 S3 原始抓包得到:

A5 5A 01 01 6E 24 00 00 40 4F 44 00 80 A5 5A 01
02 6F 24 00 00 C0 4F 44 00 80 A5 5A 01 01 70 24

证据表明 Magic、Version、Type 和 Sequence 都正确,因此引脚与 921600 baud 正确;但每帧只到约 13 B 就紧接下一个 Magic,完整 header、payload 和 CRC 都未发完。

MXC_UART_Write() 非阻塞 FIFO 语义 Bug

旧代码只调用一次:

length = 22 + payload_bytes;
return MXC_UART_Write(STREAM_UART, packet, &length);

阅读 MSDK 的源码 uart_revb.c 发现,MXC_UART_Write() 内部调用非阻塞 WriteCharacterRaw();TX FIFO 满后它会立即返回错误,并把 *len 改成实际写入量。(在 C 盘目录下有 MSDK)

上层又使用 (void)stream_packet(...) 丢弃返回值,因此每帧都只填满一次 FIFO。

修复改用会等待 FIFO 空位的 API:

for (int i = 0; i < length; i++) {
int error = MXC_UART_WriteCharacter(STREAM_UART, packet[i]);
if (error != E_NO_ERROR) return error;
}

修复固件经 OpenOCD Verified OK 后,S3 的 frames 从 0 持续增加到 1313 以上,且 time=1, fft=1,证明两类帧和 CRC 都完整到达。

显示空白与可观测性

早期只有色块和图形,不能直接区分“屏幕未刷新”、“UART 无字节”、“有字节但 CRC 错”和“信号太小”;修复是增加 FPS、RX、E、链路指示、自动缩放、峰值、主频和电平条。这些元素同时改善视觉和故障可观测性。

性能计算

UART 带宽

8N1 每 byte 需 10 bit:

有效字节率 = 921600 / 10 = 92160 B/s
一组线路时间 = 812×10/921600 = 8.811 ms
纯 UART 理论上限 = 113.5 groups/s

实际还包含 FFT、幅值计算、定量化、CRC 和阻塞发送,不能按纯 UART 上限设计。

组率

字节率

线路 bit/s

UART 占用率

20 groups/s(实测)

16,240 B/s

162,400

17.62%

25 groups/s

20,300 B/s

203,000

22.03%

30 groups/s(配置目标)

24,360 B/s

243,600

26.43%

50 groups/s(早期目标)

40,600 B/s

406,000

44.05%

早期目标 50 groups/s 时实测约 65~67 frames/s,即 32.5~33.5 groups/s;说明瓶颈在 MAX 前台 FFT+阻塞 UART,而非物理 UART 带宽。目标调为 30 groups/s 后,S3 实测约 40 frames/s,即约 20 组/秒,错误率大部分时间为 0/s。

配置 30 而实测约 20 groups/s,表明 captured_samples、I2S DMA 实际回调节拍或前台时间存在约 2/3 的比例差异;当前不影响显示稳定,但若要精确显示,可以使用逻辑分析仪测 LRCLK/DMA 回调率,并用硬件定时器调度。

协议效率

数据 = 768 B/group
协议开销 = 2×(20+2) = 44 B/group
总长 = 812 B/group
开销率 = 44/812 = 5.42%
有效负载率 = 94.58%

S3 RX 缓冲余量

实测 20 groups/s:8192/16240 = 0.504 s
目标 30 groups/s:8192/24360 = 0.336 s

RX 任务独占 CPU0 且优先级高,正常运行时不应接近这个积压量。

LCD 带宽和 FPS

framebuffer = 172×320×2 = 110080 B = 107.5 KiB
25 FPS 像素率 = 110080×25 = 2.752 MB/s = 22.016 Mbit/s
40 MHz SPI 占用率 = 22.016/40 = 55.04%
一幅理论传输时间 = 22.016 ms
40 ms 帧周期剩余 = 17.984 ms

剩余时间用于清屏、网格、折线、频谱柱、字符和调度;实测稳定 25 FPS,说明当前有足够余量;串口当前约 20 组 TIME+FFT/秒,每组约 50 ms;LCD 每帧 40 ms,因此偶尔两幅 LCD 使用同一数据快照,但观感仍为稳定 25 FPS。

构建情况

MAX32690

FLASH = 145524 B / 3328 KiB = 4.27%
SRAM = 12032 B / 1 MiB = 1.15%
text + data + bss = 154484 B

通过 CMSIS-DAP/SWD 与 OpenOCD 2 MHz 烧录,结果为 Programming Finished / Verified OK / Resetting Target

ESP32-S3

MAX_C.bin = 222272 B
app partition = 1048576 B
剩余 = 826304 B,约 78.8%

COM16 烧录时 bootloader、partition table 和 app 均通过 hash 校验。

已知限制和后续优化

偶发UART TX 忙等待,发现是逐字节阻塞占用 MAX CPU;下一步应使用 UART TX DMA 双缓冲或中断环形队列;目前的FFT 分辨率有限,一个187.5 Hz/bin 适合趋势显示,不适合精密测频;512 点为 93.75 Hz/bin,1024 点为 46.875 Hz/bin

现在是我原始抓包是调试功能,如果有生产版应关闭启动 128-byte dump。

可以加入反向控制协议;可用 S3 TX→MAX RX 增加 STREAM 启停、增益、FFT 点数、输出率配置和 ACK。

ESP32-S3 到电脑 COM16 转发

S3 在 UART1 收到完整帧并通过 CRC16 后,使用 ESP32-S3 原生 USB Serial/JTAG 把同一帧原样写到 COM16;USB TX 环形缓冲为 8192 B,单帧最大 534 B;转发发生在 CRC 通过之后,所以电脑不会再收到 S3 已知损坏的 MAX32690 帧。

S3 应用日志已取消 USB Serial/JTAG secondary console,避免 INFO 文字混入二进制流;复位时的 ROM 启动字符仍可能短暂出现,Python GUI 会搜索 A5 5A 并自动跳过它们。(具体可以看代码的设计)
>

实机 COM16 连续读取 6 秒结果:

frames = 213
TIME = 102
FFT = 111
CRC errors = 0
header errors = 0
USB bytes = 85723

平均电脑接收率约 213/6 = 35.5 frames/s;测试从任意帧中间开始,TIME/FFT 数量的短时不对称为开窗时刻和 USB 启动缓冲造成;关键结果是 213 帧全部通过了电脑端的格式和 CRC 校验。

源码

MAX32690 源码

文件

职责

I2S_DMA_Target - /I2S_DMA_Target/main.c

系统入口、I2S DMA、PCM 环、FFT、协议、LPUART0B、屏幕配置页、SW2

max9867.c

MAX9867 Codec 寄存器配置

tft_st7735_noirq.c

MAX32690 自带屏幕的非中断显示适配

project.mk

MAX32690 EvKit、softfp、CMSIS-DSP、字体选项

ESP32-S3 源码

文件

职责

audio_scope_main.c

ST7789、UART1 解析、USB COM16 转发、FreeRTOS 双核任务、图形渲染

main/CMakeLists.txt

声明 SPI/GPIO/UART/USB Serial-JTAG/LCD 组件依赖

sdkconfig

ESP32-S3、8 MB Flash、UART0 日志主控制台、关闭 USB secondary console

Python 上位机源码

文件

职责

python_gui_receiver/MAX32690_scope_gui.py

pyserial、流式解析、Tkinter GUI、Canvas 绘图、信号分析、CSV 导出

python_gui_receiver/requirements.txt

仅依赖 pyserial>=3.5

python_gui_receiver/README.md

安装、COM16 使用、协议和指标说明

数据结构的设计

MAX32690 侧

对象

类型/大小

选择理由

i2s_rx_buffer

uint32_t[128],512 B

两个 64-word DMA 半区;32-bit 对齐符合 I2S/DMA 寄存访问

rxBufPtr

全局 uint32_t *

DMA 回调和重配置共享当前 RX 半区;保留原厂 demo 的文件级生命期

snap_temp

uint32_t[64],256 B

先 burst copy 安全半区,再做 24→16 bit 转换,缩短访问 DMA 共享 RAM 的时间

snap_ring

int16_t[512],1024 B

512 是 2 的幂,索引可用 & 511 回绕;正好容纳两个 256-point FFT 窗

stream_time

int16_t[256],512 B

与线上 TIME payload 完全同构,无需二次序列化

stream_fft

uint16_t[128],256 B

只传正频率一半;Q15 比 float32 节省 50% 带宽

packet

静态 uint8_t[534]

覆盖最大 TIME 帧;静态分配避免把 534 B 压到前台栈上

FFT 输入/输出/幅值

float32_t 数组

CMSIS-DSP fast RFFT 接口直接使用 float32,简化窗函数和幅值计算

stream_sequence

uint32_t

32-bit 回绕周期远大于一次运行,S3/PC 可检测 gap

captured_samples

uint32_t

作为采样域时间戳,比毫秒 tick 更容易与音频对齐

dma_flag、DMA 指针和回调与前台共享的状态必须保持简单。volatile 只保证编译器每次真实访问,不保证复合操作的原子性。当前 Cortex-M4 对齐 32-bit load/store 是单指令,所以单个索引/计数器读写可用;若将来扩展成多字段状态,应使用短临界区。

ESP32-S3 侧

对象

类型/大小

选择理由

UART RX driver buffer

8192 B

当前实测吞吐下可缓冲约 0.5 s,吸收短时 LCD/USB 抖动

frame

RX task 栈上 534 B

只有 RX task 拥有,无需锁;大小为 header+512 payload+CRC

s_time_data

int16_t[256],512 B

保留最新完整 TIME 快照,旧帧可覆盖,显示不需要队列历史

s_fft_data

uint16_t[128],256 B

保留最新 FFT 快照

s_data_lock

FreeRTOS mutex

保证显示任务不会复制到“半幅新、半幅旧”的数组

LCD framebuffer

RGB565 DMA RAM,110080 B

整幅内存合成+一次 SPI DMA,比大量小块交易稳定

USB TX buffer

8192 B

大于 15 个 TIME 帧,允许 USB host 短时调度延迟

task stack

RX 4096 B,Display 4096 B

RX 需局部 frame;Display 需 TIME/FFT 快照和格式化字符空间

S3 的数据所有权流程是:

UART driver ring-> RX task private frame-> CRC pass -> mutex-protected latest TIME/FFT -> display task private copies -> framebuffer -> SPI DMA

USB 转发使用 RX task 中已校验的私有 frame,不需要再从共享数组反向重建数据包,因此 sequence、sample index 和 CRC 全部原样保留。

Python 侧

对象

Python 结构

选择理由

StreamFrame

@dataclass(slots=True)

字段明确,slots 避免每帧创建 __dict__,降低高频对象开销

ReaderStats

@dataclass(slots=True)

统计快照可整体传给 GUI,避免 GUI 读到一组不一致计数器

流式缓冲

bytearray

支持原地 extend/find/del,适合串口任意分块输入和 Magic 重同步

payload 结果

tuple

解析后不可变,可安全从 worker 线程传到 Tk 主线程

线程交互

queue.Queue(maxsize=2048)

Queue 自带锁;Tkinter 不允许 worker 直接更新控件

停止通知

threading.Event

可被 timeout read 周期性检查,无需强制终止线程

FFT 平滑

list[float]

每个 bin 需原地 EMA 更新,可变 list 比每帧重建 tuple 更直接

2048 个 GUI 事件在 40 frames/s 下约等于 51 s 的帧队列容量;这个设计能吸收短时 GUI 卡顿,但如果 GUI 停止消费超过几十秒,worker 会在 put() 上阻塞,最终可导致 OS/UART 缓冲溢出。

对长时记录应增加独立文件写线程,不应把 GUI Queue 当作无限数据库。

关键源码附录

下面收录影响数据正确性、并发性和性能的核心源码;其中芯片厂商初始化模板、通用绘图像素函数和大量 UI 布局样板不重复粘贴;完整可编译源码以最后列出的三个主源文件为准。

MAX32690 协议封装和完整发送

static int stream_packet(uint8_t type, const void *payload,
uint16_t count, uint16_t payload_bytes)
{
static uint8_t packet[20 + FFT_SIZE * sizeof(int16_t) + 2];
uint16_t crc;
int length;

packet[0] = 0xA5;
packet[1] = 0x5A;
packet[2] = 1;
packet[3] = type;
put_u32_le(packet + 4, stream_sequence++);
put_u32_le(packet + 8, captured_samples);
put_u32_le(packet + 12, SAMPLE_RATE);
put_u16_le(packet + 16, count);
put_u16_le(packet + 18, payload_bytes);
memcpy(packet + 20, payload, payload_bytes);
crc = crc16_ccitt(packet, 20U + payload_bytes);
put_u16_le(packet + 20 + payload_bytes, crc);

length = 22 + payload_bytes;
for (int i = 0; i < length; i++) {
int error = MXC_UART_WriteCharacter(STREAM_UART, packet[i]);
if (error != E_NO_ERROR) {
return error;
}
}
return E_NO_ERROR;
}

packet 是静态单缓冲,因此 stream_packet() 不可重入;当前只有前台调用,符合该约束;sequence 在组包时即增加。若 UART 中途返回错误,下一帧会形成 sequence gap,这是有意的可观测性;CRC 对已组装的线上字节计算,不依赖 CPU 结构体对齐。

MAX32690 TIME/FFT 生成

static void stream_latest_frame(void)
{
int rd = (snap_write - FFT_SIZE) & (SNAP_RING_SIZE - 1);
const float32_t scale = 1.0f / 32768.0f;
const float32_t fft_scale = 2.0f / (float32_t)FFT_SIZE;

for (int i = 0; i < FFT_SIZE; i++) {
int16_t sample = snap_ring[rd];
rd = (rd + 1) & (SNAP_RING_SIZE - 1);
stream_time[i] = sample;
fft_in[i] = (float32_t)sample * scale * fft_window[i];
}
arm_rfft_fast_f32(&fft_inst, fft_in, fft_out, 0);
fft_mag[0] = fabsf(fft_out[0]) / (float32_t)FFT_SIZE;
for (int i = 1; i < FFT_SIZE / 2; i++) {
float32_t re = fft_out[2 * i];
float32_t im = fft_out[2 * i + 1];
fft_mag[i] = sqrtf(re * re + im * im) * fft_scale;
}
for (int i = 0; i < FFT_SIZE / 2; i++) {
float32_t q15 = fft_mag[i] * 65535.0f;
if (q15 < 0.0f) q15 = 0.0f;
if (q15 > 65535.0f) q15 = 65535.0f;
stream_fft[i] = (uint16_t)(q15 + 0.5f);
}
(void)stream_packet(PACKET_TYPE_TIME, stream_time,
FFT_SIZE, sizeof(stream_time));
(void)stream_packet(PACKET_TYPE_FFT, stream_fft,
FFT_SIZE / 2, sizeof(stream_fft));
}

这里保留了 TIME 与 FFT 使用同一个 256 点窗口的语义,便于电脑端把二者视为同一时刻附近的数据;不过当前两帧 sequence 相邻而没有“group id”。

S3 CRC通过后的本地发布与USB转发

uint16_t received_crc = get_u16_le(
frame + FRAME_HEADER_SIZE + payload_length);
uint16_t calculated_crc = crc16_ccitt(
frame, FRAME_HEADER_SIZE + payload_length);

if (received_crc == calculated_crc) {
accept_frame(frame, payload_length);

size_t frame_length =
FRAME_HEADER_SIZE + payload_length + FRAME_CRC_SIZE;
int written = usb_serial_jtag_write_bytes(
frame, frame_length, pdMS_TO_TICKS(20));
if (written == (int)frame_length) {
s_usb_forwarded++;
} else {
s_usb_dropped++;
}
} else {
s_crc_errors++;
}

先校验再发布,保证 LCD 和 PC 只消费完整帧;USB write timeout 为 20 ms:正常 USB 吞吐下足够,但 host 长时间不读时可能让 RX task 等待 20 ms。更严格的实时版本应使用独立 USB forwarding task 和有界 frame queue,使 USB host 状态完全不能反压 UART1 parser。

S3共享数据快照

xSemaphoreTake(s_data_lock, portMAX_DELAY);
memcpy(time_copy, s_time_data, sizeof(time_copy));
memcpy(fft_copy, s_fft_data, sizeof(fft_copy));
have_time = s_have_time;
have_fft = s_have_fft;
good = s_good_frames;
errors = s_crc_errors + s_sync_losses;
xSemaphoreGive(s_data_lock);

render_scope(framebuffer, time_copy, fft_copy,
have_time, have_fft, linked, fps, rx_fps, err_rate);
esp_lcd_panel_draw_bitmap(
s_panel, 0, 0, LCD_WIDTH, LCD_HEIGHT, framebuffer);

锁只覆盖约 768 B 数组复制和标量读取;最耗时的绘图、对数计算和 SPI 传输全部在锁外;否则 22 ms 以上的 LCD DMA 提交路径会让高优先级 RX task 等锁并造成 UART 积压。

Python协议结构声明

MAGIC = b"\xA5\x5A"
HEADER = struct.Struct("<2sBBIIIHH")

@dataclass(slots=True)
class StreamFrame:
frame_type: int
sequence: int
sample_index: int
sample_rate: int
count: int
values: tuple[int, ...]
received_at: float

<2sBBIIIHH 明确指定 little-endian 且没有 native alignment padding,计算结果固定为 20 B;不能使用 @2sBBIIIHH,因为 native 模式会让协议受主机 ABI/对齐规则影响。

Python流式重同步核心

magic_at = self.buffer.find(MAGIC)
if magic_at < 0:
keep = 1 if self.buffer.endswith(MAGIC[:1]) else 0
drop = len(self.buffer) - keep
del self.buffer[:drop]
break

if magic_at:
del self.buffer[:magic_at]
if len(self.buffer) < HEADER_SIZE:
break

fields = HEADER.unpack_from(self.buffer)
payload_len = fields[-1]
if payload_len > MAX_PAYLOAD or payload_len != fields[-2] * 2:
del self.buffer[0]
self.stats.header_errors += 1
continue

frame_len = HEADER_SIZE + payload_len + CRC_SIZE
if len(self.buffer) < frame_len:
break

received_crc = struct.unpack_from(
"<H", self.buffer, HEADER_SIZE + payload_len)[0]
calculated_crc = crc16_ccitt(
memoryview(self.buffer)[:HEADER_SIZE + payload_len])
if received_crc != calculated_crc:
del self.buffer[0]
self.stats.crc_errors += 1
continue

未找到 Magic 时特意保留末尾单个 A5,因为下一个 serial read 可能从 5A 开始。CRC 失败时只丢 1 B,而不是丢掉声称的整帧;这样即使假 Magic 出现在噪声或 payload 内,也能找到后续真正 Magic。

Python时域分析源码

dc = sum(values) / len(values)
ac_square = sum((value - dc) ** 2 for value in values) / len(values)
rms = math.sqrt(ac_square)
peak = max(abs(value - dc) for value in values)
crest = peak / rms if rms > 1e-9 else 0.0
rms_db = 20 * math.log10(max(1.0, rms) / 32767.0)
peak_db = 20 * math.log10(max(1.0, peak) / 32767.0)

RMS 和 peak 在减去 DC 后计算,避免 Codec 偏置被误认为交流能量;DC 本身单独显示。max(1.0, value) 防止静音时 log10(0)

Python频域分析源码

fft_size = len(values) * 2
bin_hz = frame.sample_rate / fft_size
dominant = max(range(1, len(values)), key=values.__getitem__)
fundamental = values[dominant]
total = sum(values[1:])
centroid = (
sum(i * bin_hz * values[i] for i in range(1, len(values))) / total
if total else 0.0
)

harmonic_power = 0.0
for harmonic in range(2, 6):
center = dominant * harmonic
if center >= len(values):
break
lo, hi = max(1, center - 1), min(len(values), center + 2)
harmonic_power += max(values[lo:hi]) ** 2
thd = math.sqrt(harmonic_power) / fundamental * 100 if fundamental else 0.0

每个谐波在理论 bin 左右各搜索 1 bin,用来容忍 187.5 Hz 分辨率、Hann 泄漏和主频不落在整数 bin 的情况;该 THD 是趋势估算,不等同于仪器级 THD+N;它没有做插值基波拟合、窗函数相干增益精确校正或噪声积分。

Python FFT固定dBFS显示和EMA

retention = int(self.smoothing_var.get()) / 100.0
for index, value in enumerate(values):
self.fft_smoothed[index] = (
retention * self.fft_smoothed[index]
+ (1.0 - retention) * value
)

dbfs = max(
-80.0,
20.0 * math.log10(max(1.0, magnitude) / 65535.0)
)
normalized = (dbfs + 80.0) / 80.0

70% retention 的离散时间常数约为 -1/ln(0.7)=2.80 个 FFT update;在 20 updates/s 下约 140 ms。因此既能压低柱状图抖动,又不会像 90% 那样产生近 0.5 s 的明显滞后。

小结

整体和普通的采集项目倒是区别不大,就是 ADI 的 SDK Bug 比较多,调的比较心累,另外最后感谢电子森林与得捷电子联合推出的 《Funpack》 系列活动,对于我来说是个很好的学习机会,理论结合实践;我们下期活动再见!


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