STM32中如何将可变长度正弦波采样数组存入Flash?
STM32F103C8T6 数字特雷门琴正弦波存储与CPU优化方案
存储方案优化
针对Flash存储、多维数组语法限制问题,推荐以下两种落地方式:
1. 编译期预生成单维const数组+索引映射
C语言多维数组的语法限制可以通过单维数组+计算索引绕过,同时const修饰的数组会被编译器分配到Flash,不占用RAM:
- 用Python/Shell脚本预计算所有需要的正弦波采样数据(覆盖特雷门琴的频率范围),直接生成C头文件中的单维数组
- 通过宏定义或简单计算模拟多维访问,可读性远优于旋转矩阵
示例脚本(Python):
import math # 配置参数 FS = 48000 # DAC采样率 MIN_FREQ = 200 MAX_FREQ = 1200 FREQ_STEP = 10 SAMPLES_PER_CYCLE = 16 # 每个正弦周期固定采样点数 samples = [] freq_sample_counts = [] for freq in range(MIN_FREQ, MAX_FREQ + 1, FREQ_STEP): cycle_samples = [] # 计算单个周期的采样值(转换为12位DAC整数范围0-4095) for i in range(SAMPLES_PER_CYCLE): sin_val = math.sin(2 * math.pi * i / SAMPLES_PER_CYCLE) dac_val = int((sin_val + 1) * 2047.5) cycle_samples.append(dac_val) samples.extend(cycle_samples) freq_sample_counts.append(SAMPLES_PER_CYCLE) # 生成C头文件 with open("sine_data.h", "w") as f: f.write("#ifndef SINE_DATA_H\n#define SINE_DATA_H\n") f.write(f"#define FREQ_COUNT {len(freq_sample_counts)}\n") f.write("const uint16_t sine_samples[] __attribute__((section(\".rodata\"))) = {\n") f.write(" " + ", ".join(map(str, samples)) + "\n};\n") f.write("const uint16_t freq_sample_counts[] = {\n") f.write(" " + ", ".join(map(str, freq_sample_counts)) + "\n};\n") f.write("#endif\n")
C代码中模拟多维访问:
#include "sine_data.h" // 通过频率索引和采样点索引直接定位数据 #define GET_SAMPLE(freq_idx, sample_idx) sine_samples[(freq_idx)*SAMPLES_PER_CYCLE + (sample_idx)]
2. 采样精度与长度优化
- 若DAC精度允许,将16位采样值压缩为8位,直接减半Flash占用
- 放弃固定3000点的统一长度,改为按正弦周期采样:每个频率仅存储1~2个完整周期的采样点,循环播放即可生成连续波形,大幅减少总数据量
CPU占用优化
高采样率下CPU开销大的核心原因是手动喂DAC数据,推荐通过硬件卸载解决:
1. DMA+定时器自动驱动DAC
STM32F1的DAC支持DMA触发,配置后完全无需CPU干预:
- 用定时器(如TIM6)生成采样率触发信号
- 将Flash中正弦数组的地址配置给DMA,设置循环传输模式
- DMA会自动按定时器频率从Flash读取采样值,写入DAC数据寄存器
示例配置片段:
// 初始化DMA(以DAC通道1为例) hdma_dac1_ch1.Instance = DMA1_Channel3; hdma_dac1_ch1.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_dac1_ch1.Init.PeriphInc = DMA_PINC_DISABLE; hdma_dac1_ch1.Init.MemInc = DMA_MINC_ENABLE; hdma_dac1_ch1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_dac1_ch1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_dac1_ch1.Init.Mode = DMA_CIRCULAR; hdma_dac1_ch1.Init.Priority = DMA_PRIORITY_MEDIUM; HAL_DMA_Init(&hdma_dac1_ch1); __HAL_LINKDMA(&hdac, DMA_Handle1, hdma_dac1_ch1); // 启动DMA传输(以第0个频率为例) HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t)sine_samples, freq_sample_counts[0], DAC_ALIGN_12B_R); // 启动定时器触发DMA HAL_TIM_Base_Start(&htim6);
2. 频率切换优化
切换特雷门琴频率时,无需停止DMA,仅修改DMA的内存起始地址和传输长度即可:
void switch_freq(uint8_t freq_idx) { HAL_DMA_Stop(&hdma_dac1_ch1); // 计算目标频率的采样数据起始地址 uint32_t start_addr = (uint32_t)&sine_samples[0]; for(uint8_t i=0; i<freq_idx; i++){ start_addr += freq_sample_counts[i] * sizeof(uint16_t); } // 更新DMA参数 hdma_dac1_ch1.Instance->CMAR = start_addr; hdma_dac1_ch1.Instance->CNDTR = freq_sample_counts[freq_idx]; // 重启DMA HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, start_addr, freq_sample_counts[freq_idx], DAC_ALIGN_12B_R); }
3. 消除浮点运算
全程使用整数采样值,预计算阶段完成所有浮点计算,运行时仅做数组索引和DMA参数修改,彻底避免CPU浮点开销。
内容的提问来源于stack exchange,提问作者hennep
相关产品推荐
相关产品推荐

