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 下的数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值