STM32H723ZGT6-测 1k - 200kHz 的频率,TIM+DMA+ADC

一、题目

测量脉冲波信号的频率,测量误差不大于 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库,如果两种办法同时操作的话会报错重复定义,只能选一种
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值