一、工具
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