STM32F103C8T6-ADC + DMA采集,TIM 控制采样率

一、题目

ADC采集(使用 PA0 引脚),采集电压信号,采样率可控,通过串口发送至串口助手上,同时显示到 OLED 上。

二、基础知识

1. 逐次逼近法

二分比较确定电压值
分辨率:12 位 ADC

2. 定时器

3. 使用 DMA

预分频器:设置为 N - 1,则可进行 N 分频
自动重装载寄存器:如果想定时 m 个脉冲,就设置自动重装载寄存器为 m - 1

4. 如何控制采样率?

让 ADC 以定时器(TIM3)触发作为采样时钟,在按键回调中修改定时器的频率(Period/Prescaler),从而改变 ADC 的触发频率。具体的内容在本文中没有实现,将 GPT 回答的内容附在文末。

三、CubeMX 配置

  • RCC: 将高速外部时钟源设置为晶振在这里插入图片描述

  • SYS:设置串口 debug在这里插入图片描述

  • NVIC: 不用设置

  • ADC1: 设置定时器 3 触发,设置 DMA在这里插入图片描述

  • TIM3: 设置内部时钟,自动重装载,预分频,ARR, 事件触发
    在这里插入图片描述

  • USART2:输出 ADC 测量结果。
    在这里插入图片描述

  • I2C2:在这里插入图片描述

  • 时钟设置:ADC1、ADC2 都依靠 APB2 的时钟线,频率不宜太快,在 F103 上不超过 14MHz。将 ADC 专用分频器改为 / 6,即将 ADC 频率改为 12MHz。
    在这里插入图片描述

四、代码编写:

  • 串口重定向:usart.c
/* USER CODE BEGIN 1 */
int fputc(int ch,FILE *f){ // 串口重定向
	HAL_UART_Transmit(&huart2, (uint8_t*)&ch, 1, 0xffff);
	return ch;
}

int fgetc(FILE *f){
	uint8_t ch = 0;
	HAL_UART_Receive(&huart2, &ch, 1, 0xffff);
	return ch;
}
/* USER CODE END 1 */
  • 修改 ADC 转换完成标志:stm32f1xx_it.c
/* USER CODE BEGIN PV */
extern uint8_t adcConvEnd; // 引入外部变量

/* USER CODE END PV */
/**
  * @brief This function handles DMA1 channel1 global interrupt.
  */
void DMA1_Channel1_IRQHandler(void)
{
  /* USER CODE BEGIN DMA1_Channel1_IRQn 0 */

  /* USER CODE END DMA1_Channel1_IRQn 0 */
  HAL_DMA_IRQHandler(&hdma_adc1);
  /* USER CODE BEGIN DMA1_Channel1_IRQn 1 */
	adcConvEnd = 1; // 触发 DMA 中断,告诉 CPU 采集完毕,程序根据该变量的变化得知采集完毕
//	HAL_ADC_Start_DMA(&hadc1, adc_buff, 200);
  /* USER CODE END DMA1_Channel1_IRQn 1 */
}
  • 添加 OLED 模块
  • 主函数之头文件
/* USER CODE BEGIN Includes */
#include "oled.h"
#include <stdio.h>
#include <string.h>
/* USER CODE END Includes */

定义采样数组长度

/* USER CODE BEGIN PD */
#define ADC_BUFFER_LENGTH 200
/* USER CODE END PD */
/* USER CODE BEGIN PV */
uint16_t adc_buff[200] = {0}; // 存放 ADC 采集的数据
uint32_t value = 0;
float voltage = 0.0f;
long long count = 0;
/* adcConvEnd 用于检测 ADC 是否采集完毕
	0:没有采集完毕
	1:采集完毕
*/
__IO uint8_t adcConvEnd = 0; // ADC 转换是否完成的变量
/* USER CODE END PV */

初始化和开始 DMA

  /* USER CODE BEGIN 2 */
	OLED_Init();
	OLED_Clear();
	
	HAL_TIM_Base_Start(&htim3); // 开启定时器
	HAL_ADCEx_Calibration_Start(&hadc1); // ADC 校准
	HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buff, ADC_BUFFER_LENGTH); // 开始 DMA 采集,采集 200 个存到 adc_buff 数组; 使用强制类型转换成 32 位
  /* USER CODE END 2 */

主循环: 使用一个计数器来降低 OLED 和串口的显示频率。因为是 normal 模式,在最后开启 DMA。

  while (1)
  {
		if (adcConvEnd){ // 使用 Normal 模式,转换完成后中断
			adcConvEnd = 0; // 清除标志
			value = adc_buff[0]; // 取第一个值
			voltage = (value / 4095.0f) * 3.3f; // 12 位 ADC
			count ++;
			if (count % 500 == 0){
				printf("ADC value = %d, Voltage = %.3f V\r\n", value, voltage); // 通过串口发送数据
				count %= 500;
				OLED_Refresh_Gram();
				OLED_ShowNum(0, 0, value, 4, 16);
				OLED_ShowNum(0, 16, voltage, 1, 16);
				OLED_ShowChar(9, 16, '.', 16, 1);

				if ((uint16_t)(voltage * 100) % 100 < 10 && 0 < (uint16_t)(voltage * 100) % 100){
					OLED_ShowChar(14, 16, '0', 16, 1);
				}else if ((uint16_t)(voltage * 100) == 0){
					OLED_ShowChar(14, 16, '0', 16, 1);
					OLED_ShowChar(22, 16, '0', 16, 1);
				}
				else{
					OLED_ShowNum(14, 16, (uint16_t)(voltage * 100) % 100, 2, 16);
				}
			}
			
			HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buff, ADC_BUFFER_LENGTH); // 中断结束,重新开始 DMA 采集
		}
			
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

注意事项

Keil打印浮点数
勾选MicroLIB库,否则没法使用printf

  • 重开之后,参考这篇文章:
    STM32HALADC+TIM+DMA采集交流信号基于cubemX
    • 使用外部触发 ADC 采样,使用 TIM3 的 TRGO 信号,能够达到 100kHz 采样的效果,这就是硬件触发的好处,软件触发(定时器)达不到这样的采样率。因为 CPU 不能以这样的速度执行。因此,使用 ADC、TIM、DMA 这三个采样不需要 CPU 的干预。
    • 串口重定向:在 usart.c 中添加 fputcfgetc 函数,就能实现使用 printfscanf 进行串口输入和输出,免去编写特定的串口发送和接收函数。
    • __IO 的写法等效于 volatile ,用来表示“读/写”(Read/Write)的寄存器或变量,这能确保编译器不会对带 __IO 的变量进行不安全的优化,比如寄存器缓存、指令重排等,从而保证每次访问的都是硬件或存储器的最新值。
    • 在 SYS 勾选 Debug: Serial Wire,否则无法 debug
    • 12 位 ADC 通常采集数据格式是 16 位(half-word),因此要将缓冲区定义为:uint16_t, 然后在 DMA 的调用函数中设置强制类型转换:(uint32_t*)adc_buff,否则采集到的 ADC 数据是乱码。

关于修改采样率如何实现?

要实现“先用 100k 的速率采集一组数据,然后不重新烧录、不复位,就在程序里把采样速率改到 200k 再采集”的关键,是“动态修改用于触发 ADC 的定时器参数”。在 STM32 中,常见的办法就是在运行中直接写定时器寄存器(或通过 HAL 宏函数)去修改计数器的自动重装载值(ARR)和预分频器(PSC)。大致步骤可以是:

  1. 确定如何由定时器触发 ADC
    一般是在 CubeMX 中将某个定时器的 Update Event(或比较通道)配置为 ADC 的触发源。例如你这里用的是 TIM3 触发 ADC1,那么只要我们改变了 TIM3 的计数周期,ADC 的触发频率也就跟着变。

  2. 通过 ARR 和 PSC 计算定时器输出频率
    在不考虑时钟树分频的前提下,定时器更新频率的计算公式通常是
    f update = f TIM时钟 ( PSC + 1 ) × ( ARR + 1 ) f_{\text{update}} = \frac{f_{\text{TIM时钟}}}{(\text{PSC}+1)\times(\text{ARR}+1)} fupdate=(PSC+1)×(ARR+1)fTIM时钟
    如果想要 100 kHz 的触发频率,先选好 PSCARR,让这个公式结果是 100000。想要改成 200 kHz,就再选一组新的 PSC/ARR,保证结果是 200000。

  3. 在代码中动态写寄存器

    • 直接访问寄存器的方式类似:
      TIM3->PSC = newPSC;
      TIM3->ARR = newARR;
      
    • 或者用 HAL 提供的宏函数:
      __HAL_TIM_SET_PRESCALER(&htim3, newPSC);
      __HAL_TIM_SET_AUTORELOAD(&htim3, newARR);
      

    通常还需要在改完之后清零计数器或重新启动定时器:

    __HAL_TIM_SET_COUNTER(&htim3, 0);
    // 如果需要,可以先 Stop,再 Start
    HAL_TIM_Base_Stop(&htim3);
    HAL_TIM_Base_Start(&htim3);
    

    这样就真正让 TIM3 按新的周期在跑。

  4. 两次采集的流程示例

    • 第一次:先用 100k 的参数启动定时器和 ADC DMA,采集你需要的 200 个点(或更多),等 DMA 传输完成或某种标志位达到以后,数据就收集完毕。
    • 动态改频率:在采集完毕之后(比如你检测到 adcConvEnd == 1),调用上面提到的修改定时器参数的代码,把 TIM3->ARRTIM3->PSC 设置成能得到 200kHz 的一组值,然后重新启动定时器。
    • 第二次:此时再启动一次 HAL_ADC_Start_DMA(...),ADC 触发就会以新的 200k 速率进行。完成后再取到新的采样数据。
  5. 不需要重新烧录代码
    上述所有操作都只是在程序运行过程中,通过对寄存器进行写操作来动态修改采样频率。不需要重新下载程序或复位就可以完成频率切换。

总结

  • 先在 CubeMX 中确保 TIMx -> ADC 的触发链接正确;
  • 根据目标采样率(100k / 200k),计算好对应 PSCARR
  • 在需要切换采样率时,给定时器相关寄存器赋上新的值(或通过 HAL 宏);
  • 重新启动定时器和 ADC DMA,即可在同一趟程序里不重新烧录就完成采样率的动态切换。这样就能先收一批 100kHz 下的数据,再收一批 200kHz 下的数据。
<think>好的,我现在需要帮助用户解决关于使用STM32F103C8T6进行ADC电流数据采集并计算电流相位的问题。首先,我得理清用户的需求。他们想要采集电流数据,然后处理这些数据来计算电流的相位。这可能涉及到信号处理和一些数学计算,比如傅里叶变换或者相关算法。 首先,我需要考虑STM32F103C8T6ADC功能。根据用户提供的引用[1]和[2],这款芯片的ADC是12位的,支持多通道采样,并且可以通过HAL库来配置。用户可能需要使用定时器触发ADC采样,以确保采样频率的稳定性,这对于后续的相位计算非常重要。因为如果采样频率不稳定,会导致FFT分析的结果不准确。 接下来,电流采集通常需要用到电流传感器,比如霍尔传感器或者互感器,将电流信号转换为电压信号,因为ADC只能测量电压。这部分可能需要用户外接电路,比如使用运算放大器进行信号调理,确保电压范围在ADC的输入范围内(0-3.3V)。此外,如果电流是交流的,可能需要偏置电路将信号抬升到正电压范围,因为ADC不能测量负电压。 然后是数据处理部分。计算相位通常需要知道电流信号相对于某个参考信号的相位差,比如电压信号。如果用户同时采集电压和电流,可以通过两者的相位差来计算功率因数等参数。这里的关键是如何从时域信号中提取相位信息。常用的方法是使用FFT,通过分析信号的频域成分来获取相位。另外,也可以使用过零检测法,但这种方法在存在谐波或噪声的情况下可能不够准确。 考虑到STM32F103C8T6的性能,进行实时的FFT处理可能会有一定的挑战,特别是如果采样点数较多的话。需要优化代码,可能使用查表法或者利用STM32的DSP库来加速计算。此外,使用DMA传输ADC数据可以减少CPU的负担,提高系统的效率。 用户可能还需要了解如何配置ADC的触发源,比如使用定时器触发,这样可以在固定的时间间隔启动ADC转换,确保等间隔采样。同时,双ADC模式或者交替采样可能对多通道同步采集有帮助,但需要确认STM32F103C8T6是否支持这些功能。 另外,相位计算中的参考信号问题也很重要。如果用户只有电流信号,可能需要另一个参考信号(比如电压信号)作为基准。否则,单独计算电流的绝对相位可能没有实际意义,因为相位是相对的。 最后,需要考虑数据处理后的输出方式,比如通过串口输出数据,或者通过I2C显示在屏幕上,如引用[2]提到的例程。这涉及到数据的格式转换和传输协议的处理。 总结一下,用户的问题需要分步骤解决:硬件电路设计、ADC配置与采样、数据处理算法实现、结果输出。每个步骤都需要考虑可能的难点和优化方法,比如信号调理电路的稳定性、采样率的设置、FFT算法的选择与实现,以及系统的实时性要求。</think>### STM32F103C8T6电流相位计算实现方法 #### 一、硬件设计关键点 1. **信号调理电路** - 使用电流互感器/霍尔传感器将电流信号转换为电压信号 - 添加偏置电路使交流信号抬升至0-3.3V范围:$V_{out} = I_{in} \times R + 1.65V$ - 配置二阶有源滤波器(截止频率高于被测信号最高频率)[^2] 2. **ADC配置建议** ```c hadc.Instance = ADC1; hadc.Init.ScanConvMode = ENABLE; hadc.Init.ContinuousConvMode = DISABLE; hadc.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO; // 定时器触发 hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc.Init.NbrOfConversion = 1; ``` #### 二、软件实现流程 1. **定时器触发采样配置** ```c htim3.Instance = TIM3; htim3.Init.Prescaler = 72-1; // 1MHz时钟 htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 1000-1; // 1kHz采样率 HAL_TIM_Base_Start(&htim3); ``` 2. **FFT相位计算实现** $$ \phi = \arctan\left(\frac{\sum_{n=0}^{N-1} x[n]\sin(2\pi f_0 n/N)}{\sum_{n=0}^{N-1} x[n]\cos(2\pi f_0 n/N)}\right) $$ 使用STM32 DSP库加速计算: ```c arm_rfft_fast_instance_f32 S; arm_rfft_fast_init_f32(&S, FFT_LENGTH); arm_rfft_fast_f32(&S, adc_buffer, fft_output, 0); ``` #### 三、相位差测量方案对比 | 方法 | 精度 | 计算量 | 抗噪性 | 适用场景 | |-----------|-----|------|------|----------------| | FFT法 | 高 | 大 | 中等 | 稳态周期信号 | | 过零检测法 | 低 | 小 | 差 | 纯净工频信号 | | 相关函数法 | 较高 | 较大 | 好 | 低信噪比环境 | | 正交锁相放大法 | 最高 | 中等 | 极好 | 微弱信号检测 | #### 四、关键代码片段 ```c // 相位差计算函数 float CalculatePhaseDiff(float *voltage, float *current, uint16_t length) { float dot = 0.0f, cross = 0.0f; for(int i=0; i<length; i++) { dot += voltage[i] * current[i]; cross += voltage[i] * current[(i + length/4) % length]; // 正交分量 } return atan2f(cross, dot) * 180.0f / M_PI; } ``` #### 五、优化建议 1. 启用ADCDMA传输模式减少CPU开销 2. 使用窗口平均滤波预处理数据:$x_{filtered}[n] = \frac{1}{5}\sum_{k=-2}^{2}x[n+k]$ 3. 对50Hz工频信号建议采样率设为2.56kHz(51.2倍频) 4. 在SRAM中预存正弦/余弦查询表加速运算
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值