一、题目
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 中添加
fputc
与fgetc
函数,就能实现使用printf
和scanf
进行串口输入和输出,免去编写特定的串口发送和接收函数。 __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)。大致步骤可以是:
-
确定如何由定时器触发 ADC
一般是在 CubeMX 中将某个定时器的 Update Event(或比较通道)配置为 ADC 的触发源。例如你这里用的是TIM3
触发ADC1
,那么只要我们改变了TIM3
的计数周期,ADC 的触发频率也就跟着变。 -
通过 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 的触发频率,先选好PSC
和ARR
,让这个公式结果是 100000。想要改成 200 kHz,就再选一组新的 PSC/ARR,保证结果是 200000。 -
在代码中动态写寄存器
- 直接访问寄存器的方式类似:
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 按新的周期在跑。
- 直接访问寄存器的方式类似:
-
两次采集的流程示例
- 第一次:先用 100k 的参数启动定时器和 ADC DMA,采集你需要的 200 个点(或更多),等 DMA 传输完成或某种标志位达到以后,数据就收集完毕。
- 动态改频率:在采集完毕之后(比如你检测到
adcConvEnd == 1
),调用上面提到的修改定时器参数的代码,把TIM3->ARR
和TIM3->PSC
设置成能得到 200kHz 的一组值,然后重新启动定时器。 - 第二次:此时再启动一次
HAL_ADC_Start_DMA(...)
,ADC 触发就会以新的 200k 速率进行。完成后再取到新的采样数据。
-
不需要重新烧录代码
上述所有操作都只是在程序运行过程中,通过对寄存器进行写操作来动态修改采样频率。不需要重新下载程序或复位就可以完成频率切换。
总结:
- 先在 CubeMX 中确保
TIMx -> ADC
的触发链接正确; - 根据目标采样率(100k / 200k),计算好对应
PSC
和ARR
; - 在需要切换采样率时,给定时器相关寄存器赋上新的值(或通过 HAL 宏);
- 重新启动定时器和 ADC DMA,即可在同一趟程序里不重新烧录就完成采样率的动态切换。这样就能先收一批 100kHz 下的数据,再收一批 200kHz 下的数据。