一、题目
测量脉冲波信号的频率,测量误差不大于 2%,显示精度不低于 1Hz。本题因为采样率高,根据奈奎斯特采样定律,只能准确测出约 1kHz - 250kHz 的频率。
二、CubeMX
时钟配置
debug 配置
I2C2 配置
TIM3 配置
ADC 配置
DMA 配置
时钟树配置
三、在 Keil 中配置 DSP
在 Define 后添加:
,ARM_MATH_CM7
如果是 STM32F103, 添加:
,ARM_MATH_CM3
四、代码编写
添加头文件
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include "arm_math.h"
#include "arm_const_structs.h"
#include "oled.h"
/* USER CODE END Includes */
添加定义
/* USER CODE BEGIN PD */
#define ADC_BUFFER_LENGTH 1024 // 环形缓冲区数组长度
#define FFT_LEN ADC_BUFFER_LENGTH // FFT 处理的点数
/* USER CODE END PD */
变量定义, 尤其注意数据类型必须匹配,小数是小数
/* USER CODE BEGIN PV */
float32_t TIM3CH1_Freq = 0.0; // 频率
float32_t TIM3CH1_Duty = 0.0; // 占空比
/**
* @brief adc_max 和 adc_min 用来记录一次DMA采样过程中检测到的最大值和最小值
* adc_middle 表示最大值和最小值的平均值(用于占空比统计)
* adc_low 和 adc_high 分别计数低于 / 高于 adc_middle 的采样点个数
*/
float32_t adc_max = 0;
float32_t adc_min = 5;
float32_t adc_middle=0;
uint32_t adc_low=0;
uint32_t adc_high=0;
const unsigned char str1[2] = "Hz"; // 用于在OLED显示时拼接单位 "Hz"
/**
* @brief adc_buffer 存储ADC采样后的原始数据(DMA自动填充)
* processed_adc_buffer 将ADC原始数据转换成电压值(0~3.3V)
* FFT_Output 用于存储RFFT计算后的复数频域结果(实数FFT库输出的格式)
* FFT_MAG_Output 存储FFT的模长(幅度),后续用来计算最大峰值对应的频率
*/
uint16_t adc_buffer[ADC_BUFFER_LENGTH] = {0};
float32_t processed_adc_buffer[ADC_BUFFER_LENGTH] = {0.0};
float32_t FFT_Output[FFT_LEN] = {0.0};
float32_t FFT_MAG_Output[FFT_LEN];
/* USER CODE END PV */
FFT 函数, 注意去掉第一个频率点、仅要搜索一半频谱;注意最后输出的采样率如何计算。TIM3 的时钟频率为 275 MHz,经过预分频和 ARR 设置后为 500 kHz
/**
* @brief FFT_Function
* 对输入数组(processed_adc_buffer)进行快速傅里叶变换,并寻找最大幅值所在的频率
* @param FFT_Input_Buffer 需要进行FFT转换的输入数据(已归一化到0~1范围内)
* @param input_max 用于归一化处理的最大值
* @retval None
*/
void FFT_Function(float32_t *FFT_Input_Buffer, float32_t input_max){ // FFT 处理
arm_rfft_fast_instance_f32 FFT_Instance; // 声明一个CMSIS DSP库的FFT实例
arm_rfft_fast_init_f32(&FFT_Instance, FFT_LEN); // 初始化 FFT
for (int i = 0; i < FFT_LEN; i ++){
FFT_Input_Buffer[i] /= input_max; // 数据归一化(避免幅值过大)
}
arm_rfft_fast_f32(&FFT_Instance, FFT_Input_Buffer, FFT_Output, 0); // 将实数时域信号转换为复数频域信号, 最后一个参数表示执行正向 FFT
arm_cmplx_mag_f32(FFT_Output, FFT_MAG_Output, FFT_LEN); // 计算复数频域信号的模长(幅值)。FFT_Output中存储了实部虚部交错的数据
uint32_t maxIndex = 0; // 找到幅值最大的频域信号索引
float32_t maxValue = FFT_MAG_Output[1]; // 第一个频率点 FFT_MAG_Output[0] 是信号的直流分量,舍去
for (int i = 1; i < FFT_LEN / 2 + 1; i ++){ // 实数FFT对称性,仅需搜索一半频谱即可(加上Nyquist频率点)
if (FFT_MAG_Output[i] > maxValue){
maxValue = FFT_MAG_Output[i];
maxIndex = i;
}
}
TIM3CH1_Freq = (float32_t)maxIndex * 500000.0f / FFT_LEN;//求频率:最大结果的序号*采样率/FFT长度, 注意注意!采样率怎么算?
}
ADC 中断回调函数:此处可能有问题,因为无法正确算出占空比;注意在函数末尾重新赋初值。
/**
* @brief HAL_ADC_ConvCpltCallback
* 当ADC+DMA完成一次传输后,会自动调用此回调函数
* @param hadc ADC句柄
* @retval None
*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc){ // ADC 中断回调函数
UNUSED(hadc); // 屏蔽编译器“未使用参数”警告
if (hadc == &hadc1){
HAL_ADC_Stop(hadc); // 停止 ADC
for (uint16_t i = 0; i < ADC_BUFFER_LENGTH; i ++){
processed_adc_buffer[i] = (float)adc_buffer[i] * 3.3 / 4096.0; // 转换采集到的 ADC 的值
if (processed_adc_buffer[i] > adc_max){
adc_max = processed_adc_buffer[i];
} else if (processed_adc_buffer[i] < adc_min){
adc_min = processed_adc_buffer[i];
}
if (adc_middle > 0.1){ // 如果已经确定了 adc_middle,就开始计数落在中间值上下的数量
if(processed_adc_buffer[i] < adc_middle){
adc_low ++; // 低于中间值的个数
}
else if (processed_adc_buffer[i] > adc_middle){
adc_high ++; // 高于中间值的个数
}
}
}
adc_middle = (adc_max + adc_min) / 2; // 更新中间值
TIM3CH1_Duty = (float)adc_high / (adc_low + adc_high); // 计算占空比
FFT_Function(processed_adc_buffer, adc_max); // 执行FFT操作,得到频域最大峰值所对应的频率
adc_max = 0; // 重新赋初值
adc_min = 5;
adc_middle = 0;
adc_low = 0;
adc_high = 0;
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_LENGTH); // 中断结束,重新开始 DMA 采集
}
}
主函数初始化:
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start(&htim3); // 开启定时器
HAL_ADCEx_Calibration_Start(&hadc1, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED); // ADC 校准, 一般校准,单端模式
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_LENGTH); // 开始 DMA 采集,采集 1024 个存到 adc_buff 数组; 使用强制类型转换成 32 位
OLED_Init();
OLED_Clear();
/* USER CODE END 2 */
主循环
/* USER CODE BEGIN WHILE */
while (1)
{
OLED_Refresh_Gram();
OLED_ShowNum(0, 0, TIM3CH1_Freq, 8, 16); // 显示频率
OLED_ShowString(70, 0, str1, 16);
OLED_ShowNum(0, 16, (uint16_t)(TIM3CH1_Duty * 100) % 100, 2, 16); // 显示占空比的整数
OLED_ShowChar(16, 16, '.', 16, 1);
OLED_ShowChar(38, 16, '%', 16, 1);
if ((uint16_t)(TIM3CH1_Duty * 10000) % 100 < 10 && 0 < (uint16_t)(TIM3CH1_Duty * 10000) % 100){ // 显示占空比的小数
OLED_ShowChar(21, 16, '0', 16, 1);
}else if ((uint16_t)(TIM3CH1_Duty * 10000) % 100 == 0){
OLED_ShowChar(21, 16, '0', 16, 1);
OLED_ShowChar(28, 16, '0', 16, 1);
}
else{
OLED_ShowNum(21, 16, (uint16_t)(TIM3CH1_Duty * 10000) % 100, 2, 16);
}
/* USER CODE END WHILE */
五、注意点
- 看帖子更快,搜帖子的方式不对, 搜对标题很重要
- 注意:添加 DSP 库有两种方法,分别是通过 Keil 添加和通过 CubeMX 添加,链接如下:STM32 DSP库的快速添加 基于cubemx 调用,使用DSP库,如果两种办法同时操作的话会报错重复定义,只能选一种