完整教程下載地址:http://www.armbbs.cn/forum.php?mod=viewthread&tid=86980
第46章 STM32H7的ADC應用之DMA方式多通道采樣
本章教程為大家講解ADC+DMA方式的多通道數據采集,實際項目中有一定的使用價值,使用一路ADC就可以采集多個通道的數據。
46.1 初學者重要提示
46.2 ADC穩壓基准硬件設計
46.3 ADC驅動設計
46.4 ADC板級支持包(bsp_adc.c)
46.5 ADC驅動移植和使用
46.6 實驗例程設計框架
46.7 實驗例程說明(MDK)
46.8 實驗例程說明(IAR)
46.9 總結
46.1 初學者重要提示
- 學習本章節前,務必優先學習第44章,需要對ADC的基礎知識和HAL庫的幾個常用API有個認識。
- 開發板右上角有個跳線帽,可以讓ADC的穩壓基准接3.3V或者2.5V,本章例子是接到3.3V。
- STM32H7的ADC支持偏移校准和線性度校准。如果使用線性度校准的話,特別要注意此貼的問題:http://www.armbbs.cn/forum.php?mod=viewthread&tid=91436 。
- ADC的專業術語詮釋文檔,推薦大家看看:http://www.armbbs.cn/forum.php?mod=viewthread&tid=89414 。
- STM32H7的ADC多通道並不是同步采樣的,本質上是通過內部的多路選擇器不斷切換實現的,一個采集完畢了才會采集另一個。
46.2 ADC穩壓基准硬件設計
注:學習前務必優先看第14章的2.1小節,對電源供電框架有個了解。
ADC要采集的准確,就需要有一個穩定的穩壓基准源,V7開發板使用的LM285D-2.5,即2.5V的基准源。硬件設計如下:

關於這個原理圖要注意以下問題:
LM285D-2.5輸出的是2.5V的穩壓基准,原理圖這里做了一個特別的處理,同時接了一個上拉電阻到VDDA(3.3V),然后用戶可以使用開發板右上角的跳線帽設置Vref選擇3.3V穩壓還是2.5V穩壓。
下面再來了解下LM285的電氣特性:

通過這個表,我們要了解以下幾點知識:
- LM285的典型值是2.5V,支持的最小值2.462V,最大值2.538V。工作電流是20uA到20mA,溫飄是±20ppm/℃
- Iz是Reference current參考電流的意思:
-
- 參考電流是20uA到1mA,溫度25℃,參考電壓最大變化1mV。
- 參考電流是20uA到1mA,全范圍溫度(−40°C to 85°C),參考電壓最大變化1.5mV。
- 參考電流是1mA到20mA,溫度25℃,參考電壓最大變化10mV。
- 參考電流是1mA到20mA,全范圍溫度(−40°C to 85°C),參考電壓最大變化30mV。
那么問題來了,V7開發板上LM285的參考電流是多少? 簡單計算就是:
(VDDA – 2.5V) / 1K =(3.3 – 2.5V) / 1K = 0.8mA。
46.3 ADC驅動設計
ADC做DMA數據傳輸的實現思路框圖如下:

下面將程序設計中的相關問題逐一為大家做個說明。
46.3.1 ADC軟件觸發
ADC轉換既可以選擇外部觸發也可以選擇軟件觸發。我們這里選擇的是軟件觸發方式的多通道轉換,即連續轉換序列,軟件觸發。對應的時序如下(在第44章的2.7小節有詳細講解軟件觸發和硬件觸發的時序):。
ADSTART表示軟件啟動轉換。
ADSTP表示停止轉換。
EOC表示一個通道轉換結束。
EOS表示所有通道轉換結束。

關於這個時序圖的解讀:
- 配置為連續轉換的話,軟件啟動ADSTART會開啟所有通道轉換,全部轉換完畢后,繼續進行下一輪轉換。調用了停止轉換ADSTP后,會停止轉換。
- 每個通過轉換完畢有個EOC標志,所有通道轉換完畢有個EOS標志。
46.3.2 ADC時鍾源選擇
根據第44章2.2小節的講解,我們知道ADC有兩種時鍾源可供選擇,可以使用來自AHB總線的系統時鍾,也可以使用PLL2,PLL3,HSE,HSI或者CSI時鍾。
如果采用AHB時鍾,不需要做專門的配置,而采用PLL2,PLL3時鍾需要特別的配置,下面是使用AHB或者PLL2時鍾的配置。
- 通過宏定義設置選擇的時鍾源
使用哪個時鍾源,將另一個注釋掉即可:
/* 選擇ADC的時鍾源 */ #define ADC_CLOCK_SOURCE_AHB /* 選擇AHB時鍾源 */ //#define ADC_CLOCK_SOURCE_PLL /* 選擇PLL時鍾源 */
- PLL2或者AHB時鍾源配置
#if defined (ADC_CLOCK_SOURCE_PLL) /* 配置PLL2時鍾為的72MHz,方便分頻產生ADC最高時鍾36MHz */ RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0}; PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_ADC; PeriphClkInitStruct.PLL2.PLL2M = 25; PeriphClkInitStruct.PLL2.PLL2N = 504; PeriphClkInitStruct.PLL2.PLL2P = 7; PeriphClkInitStruct.PLL2.PLL2Q = 7; PeriphClkInitStruct.PLL2.PLL2R = 7; PeriphClkInitStruct.PLL2.PLL2RGE = RCC_PLL2VCIRANGE_0; PeriphClkInitStruct.PLL2.PLL2VCOSEL = RCC_PLL2VCOWIDE; PeriphClkInitStruct.PLL2.PLL2FRACN = 0; PeriphClkInitStruct.AdcClockSelection = RCC_ADCCLKSOURCE_PLL2; if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK) { Error_Handler(__FILE__, __LINE__); } #elif defined (ADC_CLOCK_SOURCE_AHB) /* 使用AHB時鍾的話,無需配置,默認選擇*/ #endif
對於PLL2的時鍾輸出,直接使用STM32CubeMX里面的時鍾樹配置即可,效果如下:
選擇PLL2P輸出作為ADC時鍾源:

- ADC分頻設置
無論是使用AHB時鍾還是PLL2時鍾都支持分頻設置:
AHB支持下面三種分頻設置:
#define ADC_CLOCK_SYNC_PCLK_DIV1 ((uint32_t)ADC_CCR_CKMODE_0) #define ADC_CLOCK_SYNC_PCLK_DIV2 ((uint32_t)ADC_CCR_CKMODE_1) #define ADC_CLOCK_SYNC_PCLK_DIV4 ((uint32_t)ADC_CCR_CKMODE) #define ADC_CLOCKPRESCALER_PCLK_DIV1 ADC_CLOCK_SYNC_PCLK_DIV1 /* 這三個僅僅是為了兼容,已經不推薦使用 */ #define ADC_CLOCKPRESCALER_PCLK_DIV2 ADC_CLOCK_SYNC_PCLK_DIV2 #define ADC_CLOCKPRESCALER_PCLK_DIV4 ADC_CLOCK_SYNC_PCLK_DIV4
PLL2支持下面幾種分頻設置:
#define ADC_CLOCK_ASYNC_DIV1 ((uint32_t)0x00000000) #define ADC_CLOCK_ASYNC_DIV2 ((uint32_t)ADC_CCR_PRESC_0) #define ADC_CLOCK_ASYNC_DIV4 ((uint32_t)ADC_CCR_PRESC_1) #define ADC_CLOCK_ASYNC_DIV6 ((uint32_t)(ADC_CCR_PRESC_1|ADC_CCR_PRESC_0)) #define ADC_CLOCK_ASYNC_DIV8 ((uint32_t)(ADC_CCR_PRESC_2)) #define ADC_CLOCK_ASYNC_DIV10 ((uint32_t)(ADC_CCR_PRESC_2|ADC_CCR_PRESC_0)) #define ADC_CLOCK_ASYNC_DIV12 ((uint32_t)(ADC_CCR_PRESC_2|ADC_CCR_PRESC_1)) #define ADC_CLOCK_ASYNC_DIV16 ((uint32_t)(ADC_CCR_PRESC_2|ADC_CCR_PRESC_1|ADC_CCR_PRESC_0)) #define ADC_CLOCK_ASYNC_DIV32 ((uint32_t)(ADC_CCR_PRESC_3)) #define ADC_CLOCK_ASYNC_DIV64 ((uint32_t)(ADC_CCR_PRESC_3|ADC_CCR_PRESC_0)) #define ADC_CLOCK_ASYNC_DIV128 ((uint32_t)(ADC_CCR_PRESC_3|ADC_CCR_PRESC_1)) #define ADC_CLOCK_ASYNC_DIV256 ((uint32_t)(ADC_CCR_PRESC_3|ADC_CCR_PRESC_1|ADC_CCR_PRESC_0))
有了這些認識后再看實際的分頻配置就好理解了:
#if defined (ADC_CLOCK_SOURCE_PLL) /* 采用PLL異步時鍾,2分頻,即72MHz/2 = 36MHz */ AdcHandle.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV2; /* 采用AHB同步時鍾,4分頻,即200MHz/4 = 50MHz */ #elif defined (ADC_CLOCK_SOURCE_AHB) AdcHandle.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; #endif
46.3.3 ADC的DMA配置
由於函數HAL_ADC_Start_DMA封裝的DMA傳輸函數是HAL_DMA_Start_IT。而我們這里僅需要用到DMA傳輸,而用不到中斷,所以不開啟對應的NVIC即可,這里使用的是DMA1_Stream1,測量了PC0,Vbat/4,VrefInt和溫度四個通道。
1. /* 2. ****************************************************************************************************** 3. * 函 數 名: bsp_InitADC 4. * 功能說明: 初始化ADC,采用DMA方式進行多通道采樣,采集了PC0, Vbat/4, VrefInt和溫度 5. * 形 參: 無 6. * 返 回 值: 無 7. ****************************************************************************************************** 8. */ 9. void bsp_InitADC(void) 10. { 11. ADC_HandleTypeDef AdcHandle = {0}; 12. DMA_HandleTypeDef DMA_Handle = {0}; 13. ADC_ChannelConfTypeDef sConfig = {0}; 14. GPIO_InitTypeDef GPIO_InitStruct; 15. 16. /* ## - 1 - 配置ADC采樣的時鍾 ####################################### */ 17. #if defined (ADC_CLOCK_SOURCE_PLL) 18. /* 配置PLL2時鍾為的72MHz,方便分頻產生ADC最高時鍾36MHz */ 19. RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0}; 20. PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_ADC; 21. PeriphClkInitStruct.PLL2.PLL2M = 25; 22. PeriphClkInitStruct.PLL2.PLL2N = 504; 23. PeriphClkInitStruct.PLL2.PLL2P = 7; 24. PeriphClkInitStruct.PLL2.PLL2Q = 7; 25. PeriphClkInitStruct.PLL2.PLL2R = 7; 26. PeriphClkInitStruct.PLL2.PLL2RGE = RCC_PLL2VCIRANGE_0; 27. PeriphClkInitStruct.PLL2.PLL2VCOSEL = RCC_PLL2VCOWIDE; 28. PeriphClkInitStruct.PLL2.PLL2FRACN = 0; 29. PeriphClkInitStruct.AdcClockSelection = RCC_ADCCLKSOURCE_PLL2; 30. if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK) 31. { 32. Error_Handler(__FILE__, __LINE__); 33. } 34. #elif defined (ADC_CLOCK_SOURCE_AHB) 35. 36. /* 使用AHB時鍾的話,無需配置,默認選擇*/ 37. 38. #endif 39. 40. /* ## - 2 - 配置ADC采樣使用的時鍾 ####################################### */ 41. __HAL_RCC_GPIOC_CLK_ENABLE(); 42. 43. GPIO_InitStruct.Pin = GPIO_PIN_0; 44. GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; 45. GPIO_InitStruct.Pull = GPIO_NOPULL; 46. HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); 47. 48. /* ## - 3 - 配置ADC采樣使用的時鍾 ####################################### */ 49. __HAL_RCC_DMA1_CLK_ENABLE(); 50. DMA_Handle.Instance = DMA1_Stream1; /* 使用的DMA1 Stream1 */ 51. DMA_Handle.Init.Request = DMA_REQUEST_ADC3; /* 請求類型采用DMA_REQUEST_ADC3 */ 52. DMA_Handle.Init.Direction = DMA_PERIPH_TO_MEMORY; /* 傳輸方向是從外設到存儲器*/ 53. DMA_Handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外設地址自增禁止 */ 54. DMA_Handle.Init.MemInc = DMA_MINC_ENABLE; /* 存儲器地址自增使能 */ 55. DMA_Handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; /* 外設數據位寬選擇半字,即16bit */ 56. DMA_Handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; /* 存儲器數據位寬選擇半字,即16bit */ 57. DMA_Handle.Init.Mode = DMA_CIRCULAR; /* 循環模式 */ 58. DMA_Handle.Init.Priority = DMA_PRIORITY_LOW; /* 優先級低 */ 59. DMA_Handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE; /* 禁止FIFO*/ 60. DMA_Handle.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL; /* 禁止FIFO此位不起作用,用於設置閥值 */ 61. DMA_Handle.Init.MemBurst = DMA_MBURST_SINGLE; /* 禁止FIFO此位不起作用,用於存儲器突發 */ 62. DMA_Handle.Init.PeriphBurst = DMA_PBURST_SINGLE; /* 禁止FIFO此位不起作用,用於外設突發 */ 63. 64. /* 初始化DMA */ 65. if(HAL_DMA_Init(&DMA_Handle) != HAL_OK) 66. { 67. Error_Handler(__FILE__, __LINE__); 68. } 69. 70. /* 關聯ADC句柄和DMA句柄 */ 71. __HAL_LINKDMA(&AdcHandle, DMA_Handle, DMA_Handle); 72. 73. 74. /* ## - 4 - 配置ADC ########################################################### */ 75. __HAL_RCC_ADC3_CLK_ENABLE(); 76. AdcHandle.Instance = ADC3; 77. 78. #if defined (ADC_CLOCK_SOURCE_PLL) 79. AdcHandle.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV8; /* 采用PLL異步時鍾,8分頻,即72MHz/8 80. = 36MHz */ 81. #elif defined (ADC_CLOCK_SOURCE_AHB) 82. AdcHandle.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; /* 采用AHB同步時鍾,4分頻,即200MHz/4 83. = 50MHz */ 84. #endif 85. 86. AdcHandle.Init.Resolution = ADC_RESOLUTION_16B; /* 16位分辨率 */ 87. AdcHandle.Init.ScanConvMode = ADC_SCAN_ENABLE; /* 禁止掃描,因為僅開了一個通道 */ 88. AdcHandle.Init.EOCSelection = ADC_EOC_SINGLE_CONV; /* EOC轉換結束標志 */ 89. AdcHandle.Init.LowPowerAutoWait = DISABLE; /* 禁止低功耗自動延遲特性 */ 90. AdcHandle.Init.ContinuousConvMode = ENABLE; /* 禁止自動轉換,采用的軟件觸發 */ 91. AdcHandle.Init.NbrOfConversion = 4; /* 使用了4個轉換通道 */ 92. AdcHandle.Init.DiscontinuousConvMode = DISABLE; /* 禁止不連續模式 */ 93. AdcHandle.Init.NbrOfDiscConversion = 1; /* 禁止不連續模式后,此參數忽略,此位是用來配置不連續 94. 子組中通道數 */ 95. 96. AdcHandle.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 采用軟件觸發 */ 97. AdcHandle.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; /* 軟件觸發,此位忽略 */ 98. AdcHandle.Init.ConversionDataManagement = ADC_CONVERSIONDATA_DMA_CIRCULAR; /* DMA循環模式接收*/ 99. AdcHandle.Init.BoostMode = DISABLE; /* ADC時鍾低於20MHz的話,可以禁止boost */ 100. AdcHandle.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; /* ADC轉換溢出的話,覆蓋ADC的數據寄存器 */ 101. AdcHandle.Init.OversamplingMode = DISABLE; /* 禁止過采樣 */ 102. 103. /* 初始化ADC */ 104. if (HAL_ADC_Init(&AdcHandle) != HAL_OK) 105. { 106. Error_Handler(__FILE__, __LINE__); 107. } 108. 109. 110. /* 校准ADC,采用偏移校准 */ 111. if (HAL_ADCEx_Calibration_Start(&AdcHandle, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED) != HAL_OK) 112. { 113. Error_Handler(__FILE__, __LINE__); 114. } 115. 116. /* 配置ADC通道,序列1,采樣PC0引腳 */ 117. /* 118. 采用PLL2時鍾的話,ADCCLK = 72MHz / 8 = 9MHz 119. ADC采樣速度,即轉換時間 = 采樣時間 + 逐次逼近時間 120. = 810.5 + 8.5(16bit) 121. = 820個ADC時鍾周期 122. 那么轉換速度就是9MHz / 820 = 10975Hz 123. */ 124. sConfig.Channel = ADC_CHANNEL_10; /* 配置使用的ADC通道 */ 125. sConfig.Rank = ADC_REGULAR_RANK_1; /* 采樣序列里的第1個 */ 126. sConfig.SamplingTime = ADC_SAMPLETIME_810CYCLES_5; /* 采樣周期 */ 127. sConfig.SingleDiff = ADC_SINGLE_ENDED; /* 單端輸入 */ 128. sConfig.OffsetNumber = ADC_OFFSET_NONE; /* 無偏移 */ 129. sConfig.Offset = 0; /* 無偏移的情況下,此參數忽略 */ 130. sConfig.OffsetRightShift = DISABLE; /* 禁止右移 */ 131. sConfig.OffsetSignedSaturation = DISABLE; /* 禁止有符號飽和 */ 132. 133. if (HAL_ADC_ConfigChannel(&AdcHandle, &sConfig) != HAL_OK) 134. { 135. Error_Handler(__FILE__, __LINE__); 136. } 137. 138. /* 配置ADC通道,序列2,采樣Vbat/4 */ 139. sConfig.Channel = ADC_CHANNEL_VBAT_DIV4; /* 配置使用的ADC通道 */ 140. sConfig.Rank = ADC_REGULAR_RANK_2; /* 采樣序列里的第1個 */ 141. sConfig.SamplingTime = ADC_SAMPLETIME_810CYCLES_5; /* 采樣周期 */ 142. sConfig.SingleDiff = ADC_SINGLE_ENDED; /* 單端輸入 */ 143. sConfig.OffsetNumber = ADC_OFFSET_NONE; /* 無偏移 */ 144. sConfig.Offset = 0; /* 無偏移的情況下,此參數忽略 */ 145. sConfig.OffsetRightShift = DISABLE; /* 禁止右移 */ 146. sConfig.OffsetSignedSaturation = DISABLE; /* 禁止有符號飽和 */ 147. 148. if (HAL_ADC_ConfigChannel(&AdcHandle, &sConfig) != HAL_OK) 149. { 150. Error_Handler(__FILE__, __LINE__); 151. } 152. 153. /* 配置ADC通道,序列3,采樣VrefInt */ 154. sConfig.Channel = ADC_CHANNEL_VREFINT; /* 配置使用的ADC通道 */ 155. sConfig.Rank = ADC_REGULAR_RANK_3; /* 采樣序列里的第1個 */ 156. sConfig.SamplingTime = ADC_SAMPLETIME_810CYCLES_5; /* 采樣周期 */ 157. sConfig.SingleDiff = ADC_SINGLE_ENDED; /* 單端輸入 */ 158. sConfig.OffsetNumber = ADC_OFFSET_NONE; /* 無偏移 */ 159. sConfig.Offset = 0; /* 無偏移的情況下,此參數忽略 */ 160. sConfig.OffsetRightShift = DISABLE; /* 禁止右移 */ 161. sConfig.OffsetSignedSaturation = DISABLE; /* 禁止有符號飽和 */ 162. 163. if (HAL_ADC_ConfigChannel(&AdcHandle, &sConfig) != HAL_OK) 164. { 165. Error_Handler(__FILE__, __LINE__); 166. } 167. 168. /* 配置ADC通道,序列4,采樣溫度 */ 169. sConfig.Channel = ADC_CHANNEL_TEMPSENSOR; /* 配置使用的ADC通道 */ 170. sConfig.Rank = ADC_REGULAR_RANK_4; /* 采樣序列里的第1個 */ 171. sConfig.SamplingTime = ADC_SAMPLETIME_810CYCLES_5; /* 采樣周期 */ 172. sConfig.SingleDiff = ADC_SINGLE_ENDED; /* 單端輸入 */ 173. sConfig.OffsetNumber = ADC_OFFSET_NONE; /* 無偏移 */ 174. sConfig.Offset = 0; /* 無偏移的情況下,此參數忽略 */ 175. sConfig.OffsetRightShift = DISABLE; /* 禁止右移 */ 176. sConfig.OffsetSignedSaturation = DISABLE; /* 禁止有符號飽和 */ 177. 178. if (HAL_ADC_ConfigChannel(&AdcHandle, &sConfig) != HAL_OK) 179. { 180. Error_Handler(__FILE__, __LINE__); 181. } 182. 183. 184. /* ## - 6 - 啟動ADC的DMA方式傳輸 ####################################### */ 185. if (HAL_ADC_Start_DMA(&AdcHandle, (uint32_t *)ADCxValues, 4) != HAL_OK) 186. { 187. Error_Handler(__FILE__, __LINE__); 188. } 189. }
這里把幾個關鍵的地方闡釋下:
- 第11 - 13行,對作為局部變量的HAL庫結構體做初始化,防止不確定值配置時出問題。
- 第17 - 38行,前面2.2小節已經講解,ADC時鍾源選擇AHB時鍾還是PLL時鍾。
- 第41 – 46行,選擇PC0作為數據采集引腳。
- 第49- 68行,配置DMA的基本參數,注釋較詳細。這里是采用的ADC外設到內部SRAM的傳輸方向,數據帶寬設置16bit,循環傳輸模式。
- 第71行,這行代碼比較重要,應用中容易被遺忘,用於關聯ADC句柄和DMA句柄。在用戶調用ADC的DMA傳輸方式函數HAL_ADC_Start_DMA時,此函數內部調用的HAL_DMA_Start_IT會用到DMA句柄。
- 第75 - 107行,主要是ADC的配置,注釋較詳細,配置ADC3為16bit模式,掃描多通道,連續轉換,軟件觸發。
- 第111 – 114行,這里的是采用的ADC偏移校准,如果要采用線性度校准,務必要注意此貼的問題:http://www.armbbs.cn/forum.php?mod=viewthread&tid=91436 。
- 第119 -129行,配置ADC多通道采樣的第1個序列。這里使用的通道10是PC0引腳的復用功能,不是隨意設置的。另外注意轉換速度的計算,在程序里面有注釋。
- 第139 – 151行,配置ADC多通道采樣的第2個序列,采樣的Vbat/4電壓。
- 第154 – 166行,配置ADC多通道采樣的第3個序列,采樣的VrefInt電壓。
- 第169 – 181行,配置ADC多通道采樣的第4個序列,采樣的溫度。
- 第185 – 188行,啟動ADC的DMA方式數據傳輸。
46.3.4 DMA存儲器選擇注意事項
由於STM32H7 Cache的存在,凡是CPU和DMA都會操作到的存儲器,我們都要注意數據一致性問題。對於本章節要實現的功能,要注意讀Cache問題,防止DMA已經更新了緩沖區的數據,而我們讀取的卻是Cache里面緩存的。這里提供兩種解決辦法:
- 方法一:
關閉DMA所使用SRAM存儲區。
/* 配置SRAM的MPU屬性為Device或者Strongly Ordered,即關閉Cache */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x60000000; MPU_InitStruct.Size = ARM_MPU_REGION_SIZE_64KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER1; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
- 方法二:
設置SRAM的緩沖區做32字節對齊,大小最好也是32字節整數倍,然后調用函數SCB_InvalidateDCache_by_Addr做無效化操作即可,保證CPU讀取到的數據是剛更新好的。
本章節配套例子是直接使用的方法二。例子中變量的定義方式如下:
/* 方便Cache類的API操作,做32字節對齊 */ #if defined ( __ICCARM__ ) #pragma location = 0x38000000 uint16_t ADCxValues[4]; #elif defined ( __CC_ARM ) ALIGN_32BYTES(__attribute__((section (".RAM_D3"))) uint16_t ADCxValues[4]); #endif
對於IAR需要#pragma location指定位置,而MDK通過分散加載即可實現,詳情看前面第26章,有詳細講解。
46.3.5 讀取DMA緩沖數據
程序中配置的DMA緩沖區可以存儲4次ADC的轉換數據,正好ADCxValues[0]對應PC0引腳的采樣電壓,ADCxValues[1]對應Vbat/4電壓,ADCxValues[2]對應VrefInt采樣的電源,ADCxValues[3]對應溫度采樣值。
具體實現代碼如下:
/* ********************************************************************************************************* * 函 數 名: bsp_GetAdcValues * 功能說明: 獲取ADC的數據並打印 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_GetAdcValues(void) { float AdcValues[5]; uint16_t TS_CAL1; uint16_t TS_CAL2; /* 使用此函數要特別注意,第1個參數地址要32字節對齊,第2個參數要是32字節的整數倍 */ SCB_InvalidateDCache_by_Addr((uint32_t *)ADCxValues, sizeof(ADCxValues)); AdcValues[0] = ADCxValues[0] * 3.3 / 65536; AdcValues[1] = ADCxValues[1] * 3.3 / 65536; AdcValues[2] = ADCxValues[2] * 3.3 / 65536; /*根據參考手冊給的公式計算溫度值 */ TS_CAL1 = *(__IO uint16_t *)(0x1FF1E820); TS_CAL2 = *(__IO uint16_t *)(0x1FF1E840); AdcValues[3] = (110.0 - 30.0) * (ADCxValues[3] - TS_CAL1)/ (TS_CAL2 - TS_CAL1) + 30; printf("PC0 = %5.3fV, Vbat/4 = %5.3fV, VrefInt = %5.3fV, TempSensor = %5.3f℃\r\n", AdcValues[0], AdcValues[1], AdcValues[2], AdcValues[3]); }
46.4 ADC板級支持包(bsp_adc.c)
ADC驅動文件bsp_adc.c提供了如下函數:
- bsp_InitADC
- bsp_GetAdcValues
46.4.1 函數bsp_InitADC
函數原型:
void bsp_InitADC(void)
函數描述:
此函數用於初始化ADC,采用DMA方式進行多通道采樣,采集了PC0, Vbat/4, VrefInt和溫度。
注意事項:
- 關於此函數的講解在本章2.3小節。
使用舉例:
作為初始化函數,直接在bsp.c文件的bsp_Init函數里面調用即可。
46.4.2 函數bsp_GetAdcValues
函數原型:
void bsp_GetAdcValues(void)
函數描述:
此函數用於獲取ADC的轉換數據。
注意事項:
- 關於此函數的講解在本章2.4和2.5小節。
使用舉例:
根據需要,周期性調用即可。
46.5 ADC驅動移植和使用
ADC驅動的移植比較方便:
- 第1步:復制bsp_adc.c和bsp_adc.h到自己的工程目錄,並添加到工程里面。
- 第2步:這幾個驅動文件主要用到HAL庫的GPIO、TIM,DMA和ADC驅動文件,簡單省事些可以添加所有HAL庫.C源文件進來。
- 第3步,應用方法看本章節配套例子即可,另外就是根據自己的需要做配置修改。
46.6 實驗例程設計框架
通過程序設計框架,讓大家先對配套例程有一個全面的認識,然后再理解細節,本次實驗例程的設計框架如

第1階段,上電啟動階段:
- 這部分在第14章進行了詳細說明。
第2階段,進入main函數:
- 第1步,硬件初始化,主要是MPU,Cache,HAL庫,系統時鍾,滴答定時器,LED,串口和ADC。
- 第2步,周期性的打印ADC采集的多通道數據。
46.7 實驗例程說明(MDK)
配套例子:
V7-024-ADC+DMA的多通道采集
實驗目的:
- 學習ADC + DMA的多通道采集實現。
實驗內容:
- 例子默認用的PLL時鍾供ADC使用,大家可以通過bsp_adc.c文件開頭宏定義切換到AHB時鍾。
- 采用DMA方式進行多通道采樣,采集了PC0, Vbat/4, VrefInt和溫度。
- 每隔500ms,串口會打印一次。
- 板子正常運行時LED2閃爍。
PC0引腳位置(穩壓基准要短接3.3V):
上電后串口打印的信息:
波特率 115200,數據位 8,奇偶校驗位無,停止位 1
程序設計:
系統棧大小分配:
RAM空間用的DTCM:
硬件外設初始化
硬件外設的初始化是在 bsp.c 文件實現:
/* ********************************************************************************************************* * 函 數 名: bsp_Init * 功能說明: 初始化所有的硬件設備。該函數配置CPU寄存器和外設的寄存器並初始化一些全局變量。只需要調用一次 * 形 參:無 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_Init(void) { /* 配置MPU */ MPU_Config(); /* 使能L1 Cache */ CPU_CACHE_Enable(); /* STM32H7xx HAL 庫初始化,此時系統用的還是H7自帶的64MHz,HSI時鍾: - 調用函數HAL_InitTick,初始化滴答時鍾中斷1ms。 - 設置NVIV優先級分組為4。 */ HAL_Init(); /* 配置系統時鍾到400MHz - 切換使用HSE。 - 此函數會更新全局變量SystemCoreClock,並重新配置HAL_InitTick。 */ SystemClock_Config(); /* Event Recorder: - 可用於代碼執行時間測量,MDK5.25及其以上版本才支持,IAR不支持。 - 默認不開啟,如果要使能此選項,務必看V7開發板用戶手冊第xx章 */ #if Enable_EventRecorder == 1 /* 初始化EventRecorder並開啟 */ EventRecorderInitialize(EventRecordAll, 1U); EventRecorderStart(); #endif bsp_InitKey(); /* 按鍵初始化,要放在滴答定時器之前,因為按鈕檢測是通過滴答定時器掃描 */ bsp_InitTimer(); /* 初始化滴答定時器 */ bsp_InitUart(); /* 初始化串口 */ bsp_InitExtIO(); /* 初始化FMC總線74HC574擴展IO. 必須在 bsp_InitLed()前執行 */ bsp_InitLed(); /* 初始化LED */ bsp_InitADC(); /* 初始化ADC */ }
MPU配置和Cache配置:
數據Cache和指令Cache都開啟。配置了AXI SRAM區(本例子未用到AXI SRAM),FMC的擴展IO區和D3域的SRAM4。
/* ********************************************************************************************************* * 函 數 名: MPU_Config * 功能說明: 配置MPU * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ static void MPU_Config( void ) { MPU_Region_InitTypeDef MPU_InitStruct; /* 禁止 MPU */ HAL_MPU_Disable(); /* 配置AXI SRAM的MPU屬性為Write back, Read allocate,Write allocate */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x24000000; MPU_InitStruct.Size = MPU_REGION_SIZE_512KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER0; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /* 配置FMC擴展IO的MPU屬性為Device或者Strongly Ordered */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x60000000; MPU_InitStruct.Size = ARM_MPU_REGION_SIZE_64KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER1; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /* 配置SRAM4的屬性為Write through, read allocate,no write allocate */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x38000000; MPU_InitStruct.Size = ARM_MPU_REGION_SIZE_64KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER2; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /*使能 MPU */ HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); } /* ********************************************************************************************************* * 函 數 名: CPU_CACHE_Enable * 功能說明: 使能L1 Cache * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ static void CPU_CACHE_Enable(void) { /* 使能 I-Cache */ SCB_EnableICache(); /* 使能 D-Cache */ SCB_EnableDCache(); }
主功能:
主程序實現如下操作:
- 每隔500ms,串口會打印一次ADC采集 的PC0, Vbat/4, VrefInt和溫度。
/* ********************************************************************************************************* * 函 數 名: main * 功能說明: c程序入口 * 形 參: 無 * 返 回 值: 錯誤代碼(無需處理) ********************************************************************************************************* */ int main(void) { uint8_t ucKeyCode; /* 按鍵代碼 */ #if defined ( __CC_ARM ) TempValues1 = 0; /* 避免MDK警告 */ TempValues2 = 0; #endif bsp_Init(); /* 硬件初始化 */ PrintfLogo(); /* 打印例程名稱和版本等信息 */ PrintfHelp(); /* 打印操作提示 */ bsp_StartAutoTimer(0, 500); /* 啟動1個500ms的自動重裝的定時器 */ /* 進入主程序循環體 */ while (1) { bsp_Idle(); /* 這個函數在bsp.c文件。用戶可以修改這個函數實現CPU休眠和喂狗 */ /* 判斷定時器超時時間 */ if (bsp_CheckTimer(0)) { bsp_GetAdcValues(); /* 每隔500ms 進來一次 */ bsp_LedToggle(2); } /* 按鍵濾波和檢測由后台systick中斷服務程序實現,我們只需要調用bsp_GetKey讀取鍵值即可。 */ ucKeyCode = bsp_GetKey(); /* 讀取鍵值, 無鍵按下時返回 KEY_NONE = 0 */ if (ucKeyCode != KEY_NONE) { switch (ucKeyCode) { case KEY_DOWN_K1: /* K1鍵按下 */ printf("K1按鍵按下\r\n"); break; default: /* 其它的鍵值不處理 */ break; } } } }
46.8 實驗例程說明(IAR)
配套例子:
V7-024-ADC+DMA的多通道采集
實驗目的:
- 學習ADC + DMA的多通道采集實現。
實驗內容:
- 例子默認用的PLL時鍾供ADC使用,大家可以通過bsp_adc.c文件開頭宏定義切換到AHB時鍾。
- 采用DMA方式進行多通道采樣,采集了PC0, Vbat/4, VrefInt和溫度。
- 每隔500ms,串口會打印一次。
- 板子正常運行時LED2閃爍。
PC0引腳位置(穩壓基准要短接3.3V):
上電后串口打印的信息:
波特率 115200,數據位 8,奇偶校驗位無,停止位 1
程序設計:
系統棧大小分配:
RAM空間用的DTCM:
硬件外設初始化
硬件外設的初始化是在 bsp.c 文件實現:
/* ********************************************************************************************************* * 函 數 名: bsp_Init * 功能說明: 初始化所有的硬件設備。該函數配置CPU寄存器和外設的寄存器並初始化一些全局變量。只需要調用一次 * 形 參:無 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_Init(void) { /* 配置MPU */ MPU_Config(); /* 使能L1 Cache */ CPU_CACHE_Enable(); /* STM32H7xx HAL 庫初始化,此時系統用的還是H7自帶的64MHz,HSI時鍾: - 調用函數HAL_InitTick,初始化滴答時鍾中斷1ms。 - 設置NVIV優先級分組為4。 */ HAL_Init(); /* 配置系統時鍾到400MHz - 切換使用HSE。 - 此函數會更新全局變量SystemCoreClock,並重新配置HAL_InitTick。 */ SystemClock_Config(); /* Event Recorder: - 可用於代碼執行時間測量,MDK5.25及其以上版本才支持,IAR不支持。 - 默認不開啟,如果要使能此選項,務必看V7開發板用戶手冊第xx章 */ #if Enable_EventRecorder == 1 /* 初始化EventRecorder並開啟 */ EventRecorderInitialize(EventRecordAll, 1U); EventRecorderStart(); #endif bsp_InitKey(); /* 按鍵初始化,要放在滴答定時器之前,因為按鈕檢測是通過滴答定時器掃描 */ bsp_InitTimer(); /* 初始化滴答定時器 */ bsp_InitUart(); /* 初始化串口 */ bsp_InitExtIO(); /* 初始化FMC總線74HC574擴展IO. 必須在 bsp_InitLed()前執行 */ bsp_InitLed(); /* 初始化LED */ bsp_InitADC(); /* 初始化ADC */ }
MPU配置和Cache配置:
數據Cache和指令Cache都開啟。配置了AXI SRAM區(本例子未用到AXI SRAM),FMC的擴展IO區和D3域的SRAM4。
/* ********************************************************************************************************* * 函 數 名: MPU_Config * 功能說明: 配置MPU * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ static void MPU_Config( void ) { MPU_Region_InitTypeDef MPU_InitStruct; /* 禁止 MPU */ HAL_MPU_Disable(); /* 配置AXI SRAM的MPU屬性為Write back, Read allocate,Write allocate */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x24000000; MPU_InitStruct.Size = MPU_REGION_SIZE_512KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER0; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /* 配置FMC擴展IO的MPU屬性為Device或者Strongly Ordered */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x60000000; MPU_InitStruct.Size = ARM_MPU_REGION_SIZE_64KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER1; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /* 配置SRAM4的屬性為Write through, read allocate,no write allocate */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x38000000; MPU_InitStruct.Size = ARM_MPU_REGION_SIZE_64KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER2; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /*使能 MPU */ HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); } /* ********************************************************************************************************* * 函 數 名: CPU_CACHE_Enable * 功能說明: 使能L1 Cache * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ static void CPU_CACHE_Enable(void) { /* 使能 I-Cache */ SCB_EnableICache(); /* 使能 D-Cache */ SCB_EnableDCache(); }
主功能:
主程序實現如下操作:
- 每隔500ms,串口會打印一次ADC采集 的PC0, Vbat/4, VrefInt和溫度。
/* ********************************************************************************************************* * 函 數 名: main * 功能說明: c程序入口 * 形 參: 無 * 返 回 值: 錯誤代碼(無需處理) ********************************************************************************************************* */ int main(void) { uint8_t ucKeyCode; /* 按鍵代碼 */ #if defined ( __CC_ARM ) TempValues1 = 0; /* 避免MDK警告 */ TempValues2 = 0; #endif bsp_Init(); /* 硬件初始化 */ PrintfLogo(); /* 打印例程名稱和版本等信息 */ PrintfHelp(); /* 打印操作提示 */ bsp_StartAutoTimer(0, 500); /* 啟動1個500ms的自動重裝的定時器 */ /* 進入主程序循環體 */ while (1) { bsp_Idle(); /* 這個函數在bsp.c文件。用戶可以修改這個函數實現CPU休眠和喂狗 */ /* 判斷定時器超時時間 */ if (bsp_CheckTimer(0)) { bsp_GetAdcValues(); /* 每隔500ms 進來一次 */ bsp_LedToggle(2); } /* 按鍵濾波和檢測由后台systick中斷服務程序實現,我們只需要調用bsp_GetKey讀取鍵值即可。 */ ucKeyCode = bsp_GetKey(); /* 讀取鍵值, 無鍵按下時返回 KEY_NONE = 0 */ if (ucKeyCode != KEY_NONE) { switch (ucKeyCode) { case KEY_DOWN_K1: /* K1鍵按下 */ printf("K1按鍵按下\r\n"); break; default: /* 其它的鍵值不處理 */ break; } } } }
46.9 總結
本章節就為大家講解這么多,ADC多通道采樣在實際項目中也比較實用,望初學者熟練掌握。
