使用 CMSIS(Cortex Microcontroller Software Interface Standard)开发 STM32 是一种低耦合、硬件抽象层更清晰的方式,适合对性能和代码移植性有较高要求的项目。相比 HAL 库,CMSIS 更接近底层,但比直接操作寄存器更标准化。
CMSIS 是 ARM 推出的标准化软件接口,包括以下几个部分:
推荐使用以下环境:
以下以 STM32F103 为例说明基本流程:
使用 Clion,PlatformIO 新建工程,选择芯片型号,勾选 CMSIS框架。
环境会自动创建,包含:
core_cm3.h
(或对应内核)startup_stm32f10x.s
system_stm32f10x.c/h
CMSIS 提供对寄存器的结构体封装,可以使用类似以下方式配置 GPIO 和控制:
新建一个文件 src/main.c
#include "stm32f1xx.h"
int main(void) {
// 1. 启用 GPIOC 时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
// 2. 配置 PC13 为推挽输出
GPIOC->CRH &= ~(GPIO_CRH_MODE13 | GPIO_CRH_CNF13); // 清除
GPIOC->CRH |= (0x2 << GPIO_CRH_MODE13_Pos); // 输出 2MHz 推挽
while (1) {
GPIOC->ODR ^= (1 << 13); // 反转 PC13(LED)
for (volatile int i = 0; i < 500000; ++i); // 延时
}
}
CMSIS 提供了 SystemInit()
函数,建议在 main()
前调用(通常在启动文件中已调用)。
特性 | CMSIS | HAL |
---|---|---|
层级 | 贴近底层 | 高层抽象 |
易用性 | 一般 | 易用性强 |
可移植性 | 高(ARM Cortex-M 通用) | 限于 STM32 |
性能开销 | 更小(无多余封装) | 较大(函数调用多) |
学习曲线 | 陡峭 | 平缓 |
STM32F103C6T6 是一款常见的小容量 STM32 芯片,非常适合用于入门音频采集实验。
接下来,使用它实现 基于 CMSIS、无 HAL 库、使用 DMA+ADC+TIM 采样音频输入(如麦克风) 。
项目 | 参数 |
---|---|
Flash / RAM | 32KB / 10KB |
ADC通道 | 10 个(12-bit) |
DMA通道 | 7 个(DMA1) |
定时器 | TIM1, TIM2, TIM3, TIM4 |
最大主频 | 72 MHz |
外设资源有限 | 适合轻量采集用途 |
PA1
(ADC1_IN1)你只需要 4 个核心文件,手动搭建即可(适合 Keil / GCC / VS Code):
Project/
├── main.c // 主程序
├── system_stm32f10x.c // CMSIS 系统时钟设置
├── startup_stm32f103x6.s // 启动文件(适用于 C6T6)
└── stm32f10x.h // 头文件(CMSIS 和设备寄存器定义)
✅ 可用 STM32CubeMX 生成
.s
和system_*.c
文件,再手动移除 HAL 相关代码。
使用PlatformIO, 只需要配置platformio.ini
文件,添加main.c
实现。
#include "stm32f10x.h"
#define SAMPLE_RATE 8000
#define BUFFER_SIZE 512
volatile uint16_t adc_buffer[BUFFER_SIZE];
void ADC_DMA_Init(void) {
// 启用时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_ADC1EN;
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
// DMA1 Channel1 设置
DMA1_Channel1->CPAR = (uint32_t)&ADC1->DR;
DMA1_Channel1->CMAR = (uint32_t)adc_buffer;
DMA1_Channel1->CNDTR = BUFFER_SIZE;
DMA1_Channel1->CCR = DMA_CCR_MINC | DMA_CCR_CIRC | DMA_CCR_PSIZE_0 | DMA_CCR_MSIZE_0 | DMA_CCR_EN;
// ADC 设置
ADC1->SQR1 = 0; // 1个规则通道
ADC1->SQR3 = 1; // IN1(PA1)
ADC1->CR2 = ADC_CR2_DMA | ADC_CR2_EXTSEL_1 | ADC_CR2_EXTTRIG | ADC_CR2_ADON;
// 稍等后开启
for (volatile int i = 0; i < 10000; ++i);
ADC1->CR2 |= ADC_CR2_ADON;
}
void TIM2_Init(void) {
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
// TIM2 设置 8kHz 触发
TIM2->PSC = 0;
TIM2->ARR = (72000000 / SAMPLE_RATE) - 1;
TIM2->CR2 = TIM_CR2_MMS_1; // TRGO = Update Event
TIM2->CR1 = TIM_CR1_CEN;
}
int main(void) {
ADC_DMA_Init();
TIM2_Init();
while (1) {
// 在此处处理 adc_buffer[] 里的音频采样数据
}
}
startup_stm32f103x6.s
)如果你用的是 Keil,可选择如下:
Device: STM32F103C6
Startup: startup_stm32f103x6.s
SystemInit()
使用 72MHz)void SystemInit(void) {
// 选择 HSE + PLL 得到 72MHz 系统时钟
RCC->CR |= RCC_CR_HSEON;
while (!(RCC->CR & RCC_CR_HSERDY));
RCC->CFGR |= RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9; // HSE x9 = 72MHz
RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY));
RCC->CFGR |= RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
主要文件包含:
main.c
system_stm32f10x.c
startup_stm32f103x6.s
stm32f10x.h
Makefile
或 Keil 工程模板增加双缓冲(Ping-Pong)机制 可以避免在音频采样时出现 DMA 覆盖还未处理的数据的问题,确保数据完整性,是音频应用中非常重要的一步。
在采样过程中将缓冲区一分为二,交替处理:
adc_buffer[BUFFER_SIZE]
↓ ↓
[0........BUFFER_SIZE/2-1] ← 半区 A(正在处理)
[BUFFER_SIZE/2....END] ← 半区 B(正在采样)
↑ ↑
DMA CNDTR +主循环判断
循环交替进行
#define HALF_BUFFER_SIZE 256
#define BUFFER_SIZE (2 * HALF_BUFFER_SIZE)
volatile uint16_t adc_buffer[BUFFER_SIZE];
void DMA1_Channel1_IRQHandler(void) {
if (DMA1->ISR & DMA_ISR_HTIF1) {
DMA1->IFCR = DMA_IFCR_CHTIF1; // 清除中断标志
// ✅ 半缓冲区完成(adc_buffer[0] ~ adc_buffer[255])
process_audio_data(&adc_buffer[0], HALF_BUFFER_SIZE);
}
if (DMA1->ISR & DMA_ISR_TCIF1) {
DMA1->IFCR = DMA_IFCR_CTCIF1;
// ✅ 整缓冲区完成(adc_buffer[256] ~ adc_buffer[511])
process_audio_data(&adc_buffer[HALF_BUFFER_SIZE], HALF_BUFFER_SIZE);
}
}
void ADC_DMA_Init(void) {
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
DMA1_Channel1->CPAR = (uint32_t)&ADC1->DR;
DMA1_Channel1->CMAR = (uint32_t)adc_buffer;
DMA1_Channel1->CNDTR = BUFFER_SIZE;
DMA1_Channel1->CCR = DMA_CCR_MINC | DMA_CCR_CIRC | DMA_CCR_PSIZE_0 |
DMA_CCR_MSIZE_0 | DMA_CCR_TCIE | DMA_CCR_HTIE |
DMA_CCR_EN;
NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}
void process_audio_data(uint16_t *data, uint16_t len) {
// 可选:滤波、峰值检测、FFT、串口输出等
uint32_t sum = 0;
for (int i = 0; i < len; i++) {
sum += data[i];
}
uint16_t avg = sum / len;
// 例如:输出平均电平
}
特性 | 实现方式 |
---|---|
双缓冲 | adc_buffer 分为两半 |
触发机制 | DMA1 HT / TC 中断触发数据处理 |
实时性与稳定性 | 避免主循环中判断 CNDTR,响应更快 |
可扩展 | 可用于串口上传、FFT 等实时处理 |
上面具备了稳定的 ADC 采样和双缓冲机制,现在我们可以在回调处理函数中 增加数字信号处理(如滤波和 FFT),让 STM32F103C6T6 实现简单的语音分析、频谱分析或前端滤波处理。
功能 | 作用 |
---|---|
FIR/IIR 滤波 | 去噪、平滑、通带选择 |
FFT | 频谱分析、语音识别特征提取 |
RMS/均值 | 音量检测 |
process_audio_data()
中对采样数据处理arm_math.h
+ 源码(如 arm_cfft_f32.c
)#define ARM_MATH_CM3
arm_cortexM3_math.lib
或编译 .c
源码你可以从 [CMSIS-DSP GitHub](https://github.com/AR M-software/CMSIS_5/tree/develop/CMSIS/DSP) 下载。
#include "arm_math.h"
#define BLOCK_SIZE 256
#define NUM_TAPS 32
float32_t firStateF32[BLOCK_SIZE + NUM_TAPS - 1];
float32_t firCoeffs32[NUM_TAPS]; // 使用工具生成的系数
arm_fir_instance_f32 S;
void fir_filter_init(void) {
// 举例:生成一个低通系数数组(可用 MATLAB / Python 设计)
for (int i = 0; i < NUM_TAPS; i++) {
firCoeffs32[i] = 0.03125f; // 简单平均低通(不理想,建议换更精细系数)
}
arm_fir_init_f32(&S, NUM_TAPS, firCoeffs32, firStateF32, BLOCK_SIZE);
}
void process_audio_data(uint16_t *data, uint16_t len) {
float32_t input[BLOCK_SIZE];
float32_t output[BLOCK_SIZE];
// 转换为 float32
for (int i = 0; i < len; i++) {
input[i] = (float32_t)data[i];
}
// FIR 滤波
arm_fir_f32(&S, input, output, len);
// 可做后续处理,如峰值检测、FFT
}
arm_rfft_fast_instance_f32 fft_instance;
void fft_init(void) {
arm_rfft_fast_init_f32(&fft_instance, 256);
}
float32_t input[256];
float32_t fft_out[256]; // 输出为幅度谱
void process_audio_data(uint16_t *data, uint16_t len) {
for (int i = 0; i < len; i++) {
input[i] = (float32_t)data[i] - 2048.0f; // 居中
}
arm_rfft_fast_f32(&fft_instance, input, input, 0); // FFT 就地
arm_cmplx_mag_f32(input, fft_out, len / 2); // 计算复数模(频谱幅度)
// 此处 fft_out[i] 为第 i 个频率点的能量
}
采样率 8kHz,256 点 FFT ⇒ 分辨率 = 8000/256 ≈ 31.25 Hz
int main(void) {
SystemInit();
fir_filter_init(); // 可选
fft_init(); // 可选
ADC_DMA_Init();
TIM2_Init();
while (1) {
// 所有处理交由 DMA 中断完成
}
}
使用 OLED 显示频谱图
[ 麦克风 ] → [ ADC + DMA ] → [ 双缓冲 Ping-Pong ]
↓
[ FFT + 频谱分析 ]
↓
[ OLED 显示频谱 ]
ssd1306
驱动库(开源、兼容 STM32)u8g2
、Adafruit GFX
(更复杂)Y ↑
64│ ▁▁▂▄▅▆█
│ ████▇▇▅
0└────────────────→ X(频率 0 ~ 4kHz)
extern uint8_t SSD1306_Buffer[1024];
void display_fft_on_oled(float32_t *fft_out, uint16_t bins) {
ssd1306_Fill(Black);
for (int i = 0; i < 128; i++) {
uint16_t bin = i * (bins / 128); // 取样频率映射
float val = fft_out[bin] / 500.0f; // 调整缩放比例
if (val > 63) val = 63;
for (int y = 0; y < (uint8_t)val; y++) {
ssd1306_DrawPixel(i, 63 - y, White);
}
}
ssd1306_UpdateScreen();
}
Project/
├── main.c
├── adc_dma.c / .h // 采样控制
├── fft_process.c / .h // FFT + CMSIS-DSP
├── oled_display.c / .h // SSD1306 驱动
├── i2c_soft.c / .h // 软件 I2C 支持
├── arm_math.c / .h // CMSIS DSP
└── ...
项目 | 推荐设置 |
---|---|
FFT 点数 | 256 或 128 点 |
OLED 更新频率 | 每 50ms 更新一次即可 |
数据类型 | float32(CMSIS-DSP),如需省资源可用 q15_t |
音频频谱动态柱状图(实时刷新)
[▁▁▃▄▆███▆▅▄▃▂]