STM32G474 定时器触发 DMA 高速翻转 GPIO

MAX14808 驱动测试

本文是对 MAX14808 进行驱动测试时 STM32G474 定时器触发 DMA 高速翻转 GPIO 的实现过程。

硬件准备

  • MAX14808 Evaluation Board v1.0
  • STM32G474VET6 开发板
    • STM32G474 的最高时钟频率为 170 MHz,VET6 为 LQFP-100 封装。
  • STLINK-V3-MINIE
  • 示波器

软件准备

  • STM32CubeMX,固件版本为 STM32Cube_FW_G4_V1.6.1
  • Visual Studio Code
  • CMake(我从来不用 Keil,CubeIDE 也用不习惯)

思路

为了让 MAX14808 输出实验要求的高压脉冲,需要以一定的时序向每个通道的 INN, INP 引脚输入逻辑信号。

目标波形示意图

在输出高压脉冲时,INN, INP 需要以 10 MHz 的频率翻转。STM32 的 HAL 库 GPIO 操作是做不到的,需要用到 DMA。

网上关于 DMA 翻转 GPIO 的方法并非没有,但主要都是测试 STM32 翻转 GPIO 的极限速度,像本文中这种输出特定频率脉冲的博文在我写这份笔记前还没看到过(之前用 NUCLEO-U575ZI 测试 MAX14808 时参考了 Solved: STM32U5 Triggering DMA request from timer to write to GPIOD ODR register,但 STM32U5 的 GPDMA 与其他系列的用法区别太大了)。

我折腾了几天后搞出来以下结果。

配置 CubeMX

  1. 将 8 个通道的 16 个 IO 配置在 STM32G474 的 GPIOE0~E15 上,这样向 GPIOE->ODR 写入一个 16 位二进制数便可同时操作 16 个引脚输出高 / 低电平。

我把 ADC、SPI、OPAMP 之类的引脚都预留了,只是不生成对应主函数代码

  1. 配置 TIM2 用来生成 10 MHz 脉冲。MCU 主频先拉到最大 170 MHz,TIM2 不预分频,计数周期为 17 - 1,这样生成的脉冲就是 10 MHz。

TIM2 配置

  1. TIM2_UP 为事件生成 DMA 请求(网上的文章这里都用 MemToMem 的 DMA 请求,但 MemToMem 的 DMA 突发传输速度由 AHB 时钟频率决定,不能直接控制)。模式选 Normal 而不是 Circular,原因后面说。方向当然是 Memory to Peripheral,数据宽度选 Half Word(STM32 是 32 位处理器嘛,一个字是 32 位,半个字就是 16 位)。

DMA 配置

到目前为止我只提到了以 10 MHz 翻转 GPIO,但这个操作每 50 ms 才进行一次。假如我用 DMA Circular,将波形图的红线和绿线部分都扔进 DMA 搬运,那么波形数组内将会有 $50 \times 10^{-3} \cdot 10\times 10^6 = 5 \times 10^5$ 个数,内存估计爆了🤣。

所以我们只能让 DMA 由另一个周期为 50 ms 的时钟触发,每次触发只搬运脉冲和两条红线的部分,搬完就停,直到下一次被 “唤醒”。这就是为什么要把 DMA 配置为 Normal。

  1. 配置另一个时钟 TIM5(TIM2 和 TIM5 是 STM32G474 中的两个 32 位通用定时器,用它们计时更精准,我猜),预分频 1000 - 1,计数周期 8500 - 1,这样时钟频率就是 20 Hz。

TIM5 配置

  1. 开启 TIM5 的全局中断。我会在 TIM5 的中断中执行 DMA 传输。由于 DMA 由硬件执行而不经过 CPU,所以中断函数的运行速度很快。
  2. 再记录一下时钟树的配置。

时钟树

CubeMX 其余的部分不重要,就不放出来了。

代码

只是操作 GPIO,代码并不长。其实只有 main.c 的几个片段:

  1. 脉冲波形数组,PULSE_OFF 对应关断、PULSE_RECEIVE 对应打开 T/R 开关接收回波。根据前文思路,我只需要在波形数组的最后一位填 PULSE_RECEIVE,则传输结束后的剩余时间里 MAX14808 都处于接收状态。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
#define PULSE_OFF 0x0000
#define PULSE_VPP 0xAAAA
#define PULSE_VNN 0x5555
#define PULSE_RECEIVE 0xFFFF
/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */
uint16_t pulse_wave[] = {
  PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF,
  PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF,
  PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF,
  PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF,
  PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF,
  PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF,
  PULSE_VPP, PULSE_VNN, PULSE_VPP, PULSE_VNN,
  PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF,
  PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF,
  PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF,
  PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF,
  PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF,
  PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF, PULSE_OFF,
  PULSE_RECEIVE,
};

extern DMA_HandleTypeDef hdma_tim2_up;
/* USER CODE END PV */
  1. main 函数里启动 TIM5。MAX14808_InitMAX14808_SetMode 是我自定义的两个函数,主要是对 MODE0MODE1 两个引脚操作,控制 MAX14808 的整体工作状态,这里略过不提。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int main(void)
{
  ...
  /* USER CODE BEGIN 2 */
  MAX14808_Init();
  MAX14808_SetMode(MAX14808_MODE_OCTAL_THREE_LEVEL);
  HAL_TIM_Base_Start_IT(&htim5);  
  /* USER CODE END 2 */
  ...
}
  1. 编辑 TIM5 的中断回调函数,这里少一行代码都不行。为了让 DMA 能够重复触发,你必须: 关闭 DMA;停止并重置 TIM2;清除 DMA 状态;启动 DMA;启动 TIM2。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* USER CODE BEGIN Callback 0 */
  if (htim->Instance == TIM5) {
    __HAL_TIM_DISABLE_DMA(&htim2, TIM_DMA_UPDATE);
    HAL_TIM_Base_Stop(&htim2);
    __HAL_TIM_SET_COUNTER(&htim2, 0);
    HAL_DMA_Abort(&hdma_tim2_up);
    HAL_DMA_Start(&hdma_tim2_up, (uint32_t)pulse_wave, (uint32_t)&PULSE_GPIO_Port->ODR, sizeof(pulse_wave)/sizeof(pulse_wave[0]));
    __HAL_TIM_ENABLE_DMA(&htim2, TIM_DMA_UPDATE);
    HAL_TIM_Base_Start(&htim2);
  }
  /* USER CODE END Callback 0 */
  ...
}

实验

用示波器观察单个通道的 INN 和 INP 引脚:

单个通道的 INN 和 INP 引脚局部波形

波形失真比较严重,我们猜测原因是信号频率高、示波器探头线太长、信号通过杜邦线连接出现多次反射等。但频率、相位都是很精准的。

以下是 MAX14808 输出端空载观察到的波形:

MAX14808 输出端局部波形

MAX14808 输出端横轴缩放后波形

这个脉冲局部波形就更丑了,估计也是和电路连接方式有关。这些没办法靠写代码解决,只能等后面硬件方案改版再测试。

小结

由于之前没有系统的 STM32 软件开发经验,这次 MAX14808 的驱动实验我绕了很多弯路,甚至烧掉了我的 NUCLEO-U575ZI 开发板😵。但好在我最后找到了一个合格的控制方案,并且实验极大地加深了我对 STM32 MCU 的时钟和 DMA 的理解。

感谢黄学长的笔记给我的启发以及曹学长的支持。

Maxwell Jay at his most tender.
使用 Hugo 构建
主题 StackJimmy 设计