STM32-定时器输出比较模式输出方波(DMA方式)
一、工具
1、STM32F103VET6单片机;
2、编译环境:TrueSTUDIO
3、辅助工具:STM32CubeMX
二、单片机系统时钟配置
三、单片机定时器配置
1、选择TIM2定时器,设置定时器的时钟源为内部时钟,通道3和通道4设置为输出比较模式。
2、定时器参数设置,主要分为时基的设置和通道的设置(具体设置参数介绍可参考博主发布的中断方式那篇文章),这里再次提醒Pulse的初始值设置为0。
3、DMA设置,在设置DMA的时候这里有几个要注意的地方:
- DMA请求(DMA Request):要选择对应定时器通道;
- DMA方向(Direction):部分要选择内存到外设(Memory To Peripheral);
- DMA模式(Mode):如果选择正常模式(Normal)输出的波形只会在第一次正确;如果选择循环模式(Circular)输出的波形会具有周期性;
- DMA数据宽度(DMA Width):都选择字(Word)。你可能好奇,定时器的ARR寄存器只有16bit,为何DMA要向其传送32bit的数据,经过个人测试发现如果使用半字(Half Word)输出的波形会不是我期望的,参考官方的例程也是这样设置,故这里我也这样设置,但在传递ARR值的时候需要注意数值不能大于16bit。
4、虽说是用DMA方式,但依然需要中断,但不再是定时器中断而是DAM中断,这里系统自动勾选,我们不用设置。
四、生成代码
1、定时器初始化代码:
static void MX_TIM2_Init(void) { /* USER CODE BEGIN TIM2_Init 0 */ /* USER CODE END TIM2_Init 0 */ TIM_ClockConfigTypeDef sClockSourceConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; TIM_OC_InitTypeDef sConfigOC = {0}; /* USER CODE BEGIN TIM2_Init 1 */ /* USER CODE END TIM2_Init 1 */ htim2.Instance = TIM2; htim2.Init.Prescaler = 7199; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 9999; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; if (HAL_TIM_Base_Init(&htim2) != HAL_OK) { Error_Handler(); } sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK) { Error_Handler(); } if (HAL_TIM_OC_Init(&htim2) != HAL_OK) { Error_Handler(); } sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK) { Error_Handler(); } sConfigOC.OCMode = TIM_OCMODE_TOGGLE; sConfigOC.Pulse = 0; sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; if (HAL_TIM_OC_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_3) != HAL_OK) { Error_Handler(); } if (HAL_TIM_OC_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_4) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN TIM2_Init 2 */ /* USER CODE END TIM2_Init 2 */ HAL_TIM_MspPostInit(&htim2); }
2、DMA初始化代码:
static void MX_DMA_Init(void) { /* DMA controller clock enable */ __HAL_RCC_DMA1_CLK_ENABLE(); /* DMA interrupt init */ /* DMA1_Channel1_IRQn interrupt configuration */ HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn); /* DMA1_Channel7_IRQn interrupt configuration */ HAL_NVIC_SetPriority(DMA1_Channel7_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA1_Channel7_IRQn); }
3、定时器的引脚、DMA初始化及其相关的代码:
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base) { if(htim_base->Instance==TIM2) { /* USER CODE BEGIN TIM2_MspInit 0 */ /* USER CODE END TIM2_MspInit 0 */ /* Peripheral clock enable */ __HAL_RCC_TIM2_CLK_ENABLE(); /* TIM2 DMA Init */ /* TIM2_CH3 Init */ hdma_tim2_ch3.Instance = DMA1_Channel1; hdma_tim2_ch3.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_tim2_ch3.Init.PeriphInc = DMA_PINC_DISABLE; hdma_tim2_ch3.Init.MemInc = DMA_MINC_ENABLE; hdma_tim2_ch3.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; hdma_tim2_ch3.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; hdma_tim2_ch3.Init.Mode = DMA_CIRCULAR; hdma_tim2_ch3.Init.Priority = DMA_PRIORITY_VERY_HIGH; if (HAL_DMA_Init(&hdma_tim2_ch3) != HAL_OK) { Error_Handler(); } __HAL_LINKDMA(htim_base,hdma[TIM_DMA_ID_CC3],hdma_tim2_ch3); /* TIM2_CH2_CH4 Init */ hdma_tim2_ch2_ch4.Instance = DMA1_Channel7; hdma_tim2_ch2_ch4.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_tim2_ch2_ch4.Init.PeriphInc = DMA_PINC_DISABLE; hdma_tim2_ch2_ch4.Init.MemInc = DMA_MINC_ENABLE; hdma_tim2_ch2_ch4.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; hdma_tim2_ch2_ch4.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; hdma_tim2_ch2_ch4.Init.Mode = DMA_CIRCULAR; hdma_tim2_ch2_ch4.Init.Priority = DMA_PRIORITY_VERY_HIGH; if (HAL_DMA_Init(&hdma_tim2_ch2_ch4) != HAL_OK) { Error_Handler(); } /* Several peripheral DMA handle pointers point to the same DMA handle. Be aware that there is only one channel to perform all the requested DMAs. */ __HAL_LINKDMA(htim_base,hdma[TIM_DMA_ID_CC2],hdma_tim2_ch2_ch4); __HAL_LINKDMA(htim_base,hdma[TIM_DMA_ID_CC4],hdma_tim2_ch2_ch4); /* USER CODE BEGIN TIM2_MspInit 1 */ /* USER CODE END TIM2_MspInit 1 */ } } void HAL_TIM_MspPostInit(TIM_HandleTypeDef* htim) { GPIO_InitTypeDef GPIO_InitStruct = {0}; if(htim->Instance==TIM2) { /* USER CODE BEGIN TIM2_MspPostInit 0 */ /* USER CODE END TIM2_MspPostInit 0 */ __HAL_RCC_GPIOA_CLK_ENABLE(); /**TIM2 GPIO Configuration PA2 ------> TIM2_CH3 PA3 ------> TIM2_CH4 */ GPIO_InitStruct.Pin = GPIO_PIN_2|GPIO_PIN_3; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); /* USER CODE BEGIN TIM2_MspPostInit 1 */ /* USER CODE END TIM2_MspPostInit 1 */ } }
4、DMA中断函数代码:
/** * @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_tim2_ch3); /* USER CODE BEGIN DMA1_Channel1_IRQn 1 */ /* USER CODE END DMA1_Channel1_IRQn 1 */ } /** * @brief This function handles DMA1 channel7 global interrupt. */ void DMA1_Channel7_IRQHandler(void) { /* USER CODE BEGIN DMA1_Channel7_IRQn 0 */ /* USER CODE END DMA1_Channel7_IRQn 0 */ HAL_DMA_IRQHandler(&hdma_tim2_ch2_ch4); /* USER CODE BEGIN DMA1_Channel7_IRQn 1 */ /* USER CODE END DMA1_Channel7_IRQn 1 */ }
5、开启定时器代码:
这里我要说明几个要注意的事项:
- 必须先初始化DMA再初始化定时器才能启动定时器成功;
- 如果你是用官方的库函数启动定时器,在启动一个通道后需要把定时器句柄的状态值(htim2.State)设置为HAL_TIM_STATE_READY才能成功启动下一个通道。通过观察启动函数的源码可以看到如果定时器的句柄状态值为HAL_TIM_STATE_READY时会把状态值改变成HAL_TIM_STATE_BUSY,当下一次在调用该函数就会因为状态值为HAL_TIM_STATE_BUSY直接返回,导致其它通道无法打开。定时器的句柄状态值在每个定时器初始化和配置函数完成后都会设置该值为HAL_TIM_STATE_READY。
- 设置CCR的值的时候也有讲究,如果设置的不对,输出的波形会是错的。
下面我说一下我个人在测试过程发现的规则:比如我的定时器设置的是一个周期计数10000次,如果我希望通道3按照{1000, 3000, 10000}这三个值输出波形,那么第一个1000会显示一个10%的电平信号,且电平会发生反转,第二个3000会显示一个20%的电平信号,且电平会发生反转,第三个10000(也就是周期结束)会显示一个70%的电平信号,且电平不会发生反转;因为DMA是循环模式,所以下一个周期的第一个1000显示的电平信号和第一个周期的第一个1000电平信号是一致的,后面依次类推。如果我希望通道4按照{2000, 5000, 10000}这三个值输出波形,那么第一个2000会显示一个20%的电平信号,且电平信号会发生反转,第二个5000会显示一个30%的电平信号,且电平信号会发生反转,第三个10000(也就是周期结束)会显示一个50%的电平信号,且电平不会发生反转;因为DMA是循环模式,所以下一个周期的第一个2000显示的电平信号和第一个周期的第一个2000电平信号是一致的,后面依次类推。
uint32_t ccr3_value[] = {1000, 3000, 10000}; uint32_t ccr4_value[] = {2000, 5000, 10000};
如果使用该定时器模式输出一个占空比为40%的波形,可以按照下面的方式定义CCR的值:
uint32_t ccr3_value[] = {4000, 10000, 0};
定时器的启动流程代码如下所示:
... MX_DMA_Init(); MX_TIM2_Init(); /* USER CODE BEGIN 2 */ HAL_TIM_OC_Start_DMA(&htim2, TIM_CHANNEL_3, ccr3_value, 3); htim2.State = HAL_TIM_STATE_READY; HAL_TIM_OC_Start_DMA(&htim2, TIM_CHANNEL_4, ccr4_value, 3); ...
同步启动方式代码如下所示:
void bsp_tim2_start(void) { #if 1 /* Set the DMA compare callbacks */ htim2.hdma[TIM_DMA_ID_CC3]->XferCpltCallback = TIM_DMADelayPulseCplt; htim2.hdma[TIM_DMA_ID_CC3]->XferHalfCpltCallback = TIM_DMADelayPulseHalfCplt; /* Set the DMA error callback */ htim2.hdma[TIM_DMA_ID_CC3]->XferErrorCallback = TIM_DMAError ; /* Enable the DMA channel */ HAL_DMA_Start_IT(htim2.hdma[TIM_DMA_ID_CC3], (uint32_t)ccr3_value, (uint32_t)&htim2.Instance->CCR3, 3); /* Set the DMA compare callbacks */ htim2.hdma[TIM_DMA_ID_CC4]->XferCpltCallback = TIM_DMADelayPulseCplt; htim2.hdma[TIM_DMA_ID_CC4]->XferHalfCpltCallback = TIM_DMADelayPulseHalfCplt; /* Set the DMA error callback */ htim2.hdma[TIM_DMA_ID_CC4]->XferErrorCallback = TIM_DMAError ; /* Enable the DMA channel */ HAL_DMA_Start_IT(htim2.hdma[TIM_DMA_ID_CC4], (uint32_t)ccr4_value, (uint32_t)&htim2.Instance->CCR4, 3); /* Enable the TIM Capture/Compare 3 DMA request */ __HAL_TIM_ENABLE_DMA(&htim2, TIM_DMA_CC3); TIM_CCxChannelCmd(htim2.Instance, TIM_CHANNEL_3, TIM_CCx_ENABLE); /* Enable the TIM Capture/Compare 4 DMA request */ __HAL_TIM_ENABLE_DMA(&htim2, TIM_DMA_CC4); TIM_CCxChannelCmd(htim2.Instance, TIM_CHANNEL_4, TIM_CCx_ENABLE); /* 使能定时器 */ __HAL_TIM_ENABLE(&htim2); #else HAL_TIM_OC_Start_DMA(&htim2, TIM_CHANNEL_3, ccr3_value, 3); htim2.State = HAL_TIM_STATE_READY; HAL_TIM_OC_Start_DMA(&htim2, TIM_CHANNEL_4, ccr4_value, 3); #endif }
五、波形显示
#endif
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 百万级群聊的设计实践
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期