ADC 采样速率如何最大
介绍 ADC 主频
- STM32F407 的 ADC 连接在 APB2 总线上的 PCLK 线上,支持的最大时钟为 36 MHz,而 PCLK 时钟是由 HCLK 固定的二分频得到,所以如果总线频率拉到 168 M,APB2 则只能得到 84 M,这就导致 ADC 内部再分频的时候,刚好分不到 36 M,所以我们需要设置 HCLK 为 144 M,这样就可以得到 72 M,进而 ADC 二分频得到 36 M
- 此时如果我们设置 ADC 的转换周期是 15 clk,就可以得到理论最大采样率为 36 15 \frac{36}{15} 1536 = 2.4 M =2.4M =2.4M
2.4 M 2.4M 2.4M 极限采样率
- ADC 的极限采样率只能用定时器+DMA 方式实现
- 定时器是为了使采样间隔一致可控
- DMA 是为了让数据搬运不需要 CPU 的参与
参数
- ADC
- 36 M H z 36MHz 36MHz
- Resolution : 8 8 8 bits(11 ADC Clock Cycles)
- Sampling time: 3 3 3 Cycles
- 所以采一个点最快 11 个 clk,理论最大为 36 11 ≈ 3.27 M H z \frac{36}{11}\approx 3.27MHz 1136≈3.27MHz
- TIM:
72
M
H
z
72MHz
72MHz(APB1)
- Prescaler: 3 3 3
- Counter Period: 8 8 8
- 所以速率为 72 4 × 9 = 2 M H z \frac{72}{4 \times 9} =2MHz 4×972=2MHz
实际测试
- 实际测试发现,测得的数据确实为 2 M H z 2MHz 2MHz,输入 100 k H z 100kHz 100kHz 的信号,一个周期可以测得 20 20 20 个点
软件触发转换
ADC 设置
- 选择了 ADC 2 的 Chanel 7,这个可以随意选择
- External Trigger Conversion Source设置为 by software(软件触发)
代码部分
- MX_ADC2_Init
- CubeMX 会自动帮我们生成 ADC 的 Init 函数
- HAL_ADC_Start(&hadc2)
- 手动启动 ADC 的转换,启动的时候会有校准的时间,所以第一次启动的时候所需要的时间会长一些,如果只测一次,则使用完后用HAL_ADC_Stop(&hadc 2) 来关闭,否则不用关闭,他会一直转换
- HAL_ADC_PollForConversion(&hadc2, 200)
- 这个函数用来检测 ADC 转换是否完成,如果完成就返回 HAL_OK,之后就可以用HAL_ADC_GetValue(&hadc2) 读出 ADC 内部寄存器 dr 的值了。其中第二个参数是允许等待 ADC 转换完成的时间,即超时时间
用软件触发读取多数据的一个代码编写思路
for (i = 0; i < FFT_LENGTH; i++) /* 生成信号序列 */
{
dac_value = dac_vol * 4095 / 3.3;
HAL_ADC_Start(&hadc2);
while (HAL_ADC_PollForConversion(&hadc2, 200) != HAL_OK)
{
}
uint32_t adc_value = HAL_ADC_GetValue(&hadc2);
uint32_t Volt = 3300 * adc_value >> 12;
printf("%d\r\n",Volt);
fft_inputbuf[2 * i] =Volt; /* 生成输入信号实部 */
fft_inputbuf[2 * i + 1] = 0; /* 虚部全部0 */
}
arm_cfft_radix4_f32(&scfft, fft_inputbuf); /* FFT计算(基4) */
arm_cmplx_mag_f32(fft_inputbuf, fft_outputbuf, FFT_LENGTH); /* 把运算结果复数求模得幅 */
for (i = 0; i < FFT_LENGTH; i++)
{
printf("fft_outputbuf[%d]:%f\r\n", i, fft_outputbuf[i]);
}
- 重点是这一句
while (HAL_ADC_PollForConversion(&hadc2, 200) != HAL_OK)
{
}
- 即如果没转换完成,就一直卡在 while 里
软件触发的问题
似乎用软件触发会使得采样率提不上来
如果想要较高采样率,可以用 TIM 触发,如果想要极限采样率,则用 DMA 搬运数据
定时器TRGO信号触发转换
定时器设置
- Clock Source设置为internal Clock
- Prescaler和Counter Period根据时钟频率和定时器期望周期计算得到
- auto-reload preload关闭,否则更新不能及时生效
- TriggerOutput(TRGO) Parameters设置
- 主从模式关闭(我也不知道这是干嘛的)
- Trigger Event Selection
- 这个是设置什么东西会产生TRGO信号
- 设置为UEV事件的产生会产生TRGO信号,这样就使得计数器记满一个周期后,就有一个TRGO信号传给ADC进行转换
- TRGO是一个短的脉冲信号
- TIM3的全局中断不打开
ADC设置
- 打开任意一个ADC的任意一个通道,我这里打开ADC2-IN7
- Mode模式设置为独立模式
- Continuous Conversion Mode
- 开启了之后,ADC 无需 Start,ADC 会自动连续转换
- 问题:此时 ADC 转换的速率是不是可以拉到最快?也就是 2.4 M 极限不用 tim 触发,用这个模式配合 dma
- HAL ADC连续转换模式
- End Of Conversion Selection
- 有两个选项
- 一个通道转换完成就发出转换完成信号(HAL_OK)
- 所有通道转换完成才发出转换完成信号
- 这里设置为一个通道就发出,因为我只开了一个通道
- 有两个选项
- External Trigger Conversion Source
- 这个是用来设置触发ADC转换的方式的
- 常用的有软件触发
- 我们这里使用Timer3的TRGO信号触发
- 这个是用来设置触发ADC转换的方式的
- External Trigger Conversion Edge
- TRGO信号是一个短时的正脉冲
- 我们这里设置脉冲的上升沿触发转换
- 打开ADC的全局中断
- 注意:ADC1-2-3共用一个全局中断,即一个中断号ISR
NVIC设置
- 最后来到NVIC,设置ADC和滴答定时器的中断优先级
代码流程
-
HAL_ADC_GetValue(&hadc 2)
这个函数是在 adc 转换完成之后,将 adc 的值读出来 -
HAL_ADC_Start_IT(&hadc2)
将 ADC 以中断方式启动,即 ADC 开始测量 -
HAL_TIM_Base_Start(&htim3)
开启定时器的函数,执行后定时器就开始计时了 -
HAL_ADC_IRQHandler
ADC 的中断函数,即 ADC 采集转换完成后,执行这个中断函数 -
__weak void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef * hadc)
这个函数在HAL_ADC_IRQHandler里,是 ADC 转换完成的回调函数,用户在需要编写在 ADC 转换完成后需要执行什么操作
由于多个 ADC 共用一个中断号,所以他们的中断回调函数也是相同的,我们在编写这个函数的时候需要首先判断传入的 hadc 是哪个,再在 if 里执行对应的操作
这个函数有时候编写的时候在 main 函数里会识别不出来,然后编译就报错,点击报错会跳转到一个 error handler 的函数,这个时候没有别的办法,只能把整个工程删除重开,就会好了(这个 bug浪费了我一整天事件,不过似乎也有可能是因为我确实漏看了某个括号)
ADC 的 DMA 模式
CubeMX 配置
Timer
- Timer 部分很熟悉了,就不再详细说了
ADC
- 这里着重介绍一下DMA Continuous Requests和Continuous Conversion Mode
DMA Continuous Requests (这个理解好像不对)例如,HAL_ADC_START_DMA (&hadc 1, (uint 32_t *) buf, 200) 这个语句,使用 ADC 1 和 DMA,数据放入 buf 数组内,放 200 个数据。如果 DMA 连续请求去能的话,这个语句传完 200 个数据后自动关闭 ADC 1 和 DMA。反之,使能后,语句执行完后会又会连续从头开始传输数据,实际为 buf 数组中的值一直在更新
- Continuous Conversion Mode
- 开了这个会连续触发采样,一般在软件触发里常用;但是开了定时器就不需要了
- 如果开了软件,开了这个,那么 start 一次后 ADC 还会一直采样;如果开了软件,没开这个,那么 start 一次以后 ADC 不会采样,需要手动 start(换句话说,采样一次完成后 ADC 会自动 stop)
- ADC 还需要
- 开启 NVIC,从而在 DMA 的 buffer 满了以后,触发 ADC 的中断回调函数
- 开启 DMA,设置 ADC 传输数据的 DMA 通道
NVIC 与 DMA
- 开启 ADC 的 NVIC
- 创建一个 DMA 通道
- 添加一个通道
- 添加完成
- 模式
- 分为 normal 和 circular
- normal 指 buffer 满了以后,就停止 DMA 传输了,即只传一次;circular 指循环传输,即满了以后从头开始再传输,覆盖以前的内容
- 地址自增
- 第一个是外设地址自增,本次 ADC 的数据寄存器只有 dr 一个,即采集到的电压,所以不需要自增,始终读取该值即可
- 第二个是内存地址自增,即 DMA 传输到的地址是否自增,我们进行 fft,这里需要传入 1024 个数据,即 buffer 的大小是 1024,所以肯定要地址自增,才能让数据一个一个的按顺序传入 buffer
- 传输的数据宽度
- 数据宽度和传入的内存地址的类型有关,我们这里是 uint 32_t,所以就选 word(一个字);如果是 uint 16_t 或者 uint 8_t 之类的,就选 half word 或者 byte 就行
具体流程的理解(ADC 的 DMA 模式流程图)
- 我先 hal_adc_start_dma,然后 hal_time_base_start,然后定时器产生 trgo 信号后就会自动 adc 采集,adc 采集完成就会自动把数据搬运到缓冲区,缓冲区满了以后 adc 就自动停止了,进到 adc 的中断处理函数void HAL_ADC_ConvCpltCallback (ADC_HandleTypeDef* hadc) 了,这个也是我们需要重写的回调函数
- 如果我不想让 DMAcircular 采集,而是DMA 模式手动控制采集了一轮以后继续采集的话,调整成 normal 之后再把hal_adc_start_dma 重新开启,不过可能需要先手动 stop 一下?(HAL_ADC_Stop_DMA)(待定)