完整教程下载地址:http://www.armbbs.cn/forum.php?mod=viewthread&tid=86980
第45章 STM32H7的ADC应用之定时器触发配合DMA双缓冲
本章教程为大家讲解定时器触发配合DMA双缓冲做ADC数据采集,实际项目中有一定的使用价值,一个缓冲接收数据的时候,另一个缓冲可以做数据处理。
45.1 初学者重要提示
45.2 ADC稳压基准硬件设计
45.3 ADC驱动设计
45.4 ADC板级支持包(bsp_adc.c)
45.5 ADC驱动移植和使用
45.6 实验例程设计框架
45.7 实验例程说明(MDK)
45.8 实验例程说明(IAR)
45.9 总结
45.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 。
45.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。
45.3 ADC驱动设计
定时器触发ADC做DMA数据传输的实现思路框图如下:
下面将程序设计中的相关问题逐一为大家做个说明。
45.3.1 触发ADC的定时器选择和配置
ADC转换既可以选择外部触发也可以选择软件触发。定时器属于外部触发方式,使用定时器触发的好处是可以设置任何ADC能够支持的转换频率。
对于ADC1,ADC2,ADC3来说,规则通道支持的外部触发源如下:
#define ADC_EXTERNALTRIG_T1_CC1 ((uint32_t)0x00000000) #define ADC_EXTERNALTRIG_T1_CC2 ((uint32_t)ADC_CFGR_EXTSEL_0) #define ADC_EXTERNALTRIG_T1_CC3 ((uint32_t)ADC_CFGR_EXTSEL_1) #define ADC_EXTERNALTRIG_T2_CC2 ((uint32_t)(ADC_CFGR_EXTSEL_1 | ADC_CFGR_EXTSEL_0)) #define ADC_EXTERNALTRIG_T3_TRGO ((uint32_t)ADC_CFGR_EXTSEL_2) #define ADC_EXTERNALTRIG_T4_CC4 ((uint32_t)(ADC_CFGR_EXTSEL_2 | ADC_CFGR_EXTSEL_0)) #define ADC_EXTERNALTRIG_EXT_IT11 ((uint32_t)(ADC_CFGR_EXTSEL_2 | ADC_CFGR_EXTSEL_1)) #define ADC_EXTERNALTRIG_T8_TRGO ((uint32_t)(ADC_CFGR_EXTSEL_2 | ADC_CFGR_EXTSEL_1 | ADC_CFGR_EXTSEL_0)) #define ADC_EXTERNALTRIG_T8_TRGO2 ((uint32_t) ADC_CFGR_EXTSEL_3) #define ADC_EXTERNALTRIG_T1_TRGO ((uint32_t)(ADC_CFGR_EXTSEL_3 | ADC_CFGR_EXTSEL_0)) #define ADC_EXTERNALTRIG_T1_TRGO2 ((uint32_t)(ADC_CFGR_EXTSEL_3 | ADC_CFGR_EXTSEL_1)) #define ADC_EXTERNALTRIG_T2_TRGO ((uint32_t)(ADC_CFGR_EXTSEL_3 | ADC_CFGR_EXTSEL_1 | ADC_CFGR_EXTSEL_0)) #define ADC_EXTERNALTRIG_T4_TRGO ((uint32_t)(ADC_CFGR_EXTSEL_3 | ADC_CFGR_EXTSEL_2)) #define ADC_EXTERNALTRIG_T6_TRGO ((uint32_t)(ADC_CFGR_EXTSEL_3 | ADC_CFGR_EXTSEL_2 | ADC_CFGR_EXTSEL_0)) #define ADC_EXTERNALTRIG_T15_TRGO ((uint32_t)(ADC_CFGR_EXTSEL_3 | ADC_CFGR_EXTSEL_2 | ADC_CFGR_EXTSEL_1)) #define ADC_EXTERNALTRIG_T3_CC4 ((uint32_t)(ADC_CFGR_EXTSEL_3 | ADC_CFGR_EXTSEL_2 | ADC_CFGR_EXTSEL_1 | ADC_CFGR_EXTSEL_0)) #define ADC_EXTERNALTRIG_HR1_ADCTRG1 ((uint32_t) ADC_CFGR_EXTSEL_4) #define ADC_EXTERNALTRIG_HR1_ADCTRG3 ((uint32_t) (ADC_CFGR_EXTSEL_4 | ADC_CFGR_EXTSEL_0)) #define ADC_EXTERNALTRIG_LPTIM1_OUT ((uint32_t) (ADC_CFGR_EXTSEL_4 | ADC_CFGR_EXTSEL_1)) #define ADC_EXTERNALTRIG_LPTIM2_OUT ((uint32_t) (ADC_CFGR_EXTSEL_4 | ADC_CFGR_EXTSEL_1| ADC_CFGR_EXTSEL_0)) #define ADC_EXTERNALTRIG_LPTIM3_OUT ((uint32_t) (ADC_CFGR_EXTSEL_4 | ADC_CFGR_EXTSEL_2))
我们这里使用的是TIM1_CC1。
接下来就是TIM1的时钟配置问题,代码如下:
1. /* 2. ****************************************************************************************************** 3. * 函 数 名: TIM1_Config 4. * 功能说明: 配置TIM1,用于触发ADC,当前配置的100KHz触发频率 5. * 形 参: 无 6. * 返 回 值: 无 7. ****************************************************************************************************** 8. */ 9. static void TIM1_Config(void) 10. { 11. TIM_HandleTypeDef htim ={0}; 12. TIM_OC_InitTypeDef sConfig = {0}; 13. 14. 15. /* 使能时钟 */ 16. __HAL_RCC_TIM1_CLK_ENABLE(); 17. 18. /*----------------------------------------------------------------------- 19. bsp.c 文件中 void SystemClock_Config(void) 函数对时钟的配置如下: 20. 21. System Clock source = PLL (HSE) 22. SYSCLK(Hz) = 400000000 (CPU Clock) 23. HCLK(Hz) = 200000000 (AXI and AHBs Clock) 24. AHB Prescaler = 2 25. D1 APB3 Prescaler = 2 (APB3 Clock 100MHz) 26. D2 APB1 Prescaler = 2 (APB1 Clock 100MHz) 27. D2 APB2 Prescaler = 2 (APB2 Clock 100MHz) 28. D3 APB4 Prescaler = 2 (APB4 Clock 100MHz) 29. 30. 因为APB1 prescaler != 1, 所以 APB1上的TIMxCLK = APB1 x 2 = 200MHz; 31. 因为APB2 prescaler != 1, 所以 APB2上的TIMxCLK = APB2 x 2 = 200MHz; 32. APB4上面的TIMxCLK没有分频,所以就是100MHz; 33. 34. APB1 定时器有 TIM2, TIM3 ,TIM4, TIM5, TIM6, TIM7, TIM12, TIM13, TIM14,LPTIM1 35. APB2 定时器有 TIM1, TIM8 , TIM15, TIM16,TIM17 36. 37. APB4 定时器有 LPTIM2,LPTIM3,LPTIM4,LPTIM5 38. 39. TIM12CLK = 200MHz/(Period + 1) / (Prescaler + 1) = 200MHz / 2000 / 1 = 100KHz 40. ----------------------------------------------------------------------- */ 41. HAL_TIM_Base_DeInit(&htim); 42. 43. htim.Instance = TIM1; 44. htim.Init.Period = 1999; 45. htim.Init.Prescaler = 0; 46. htim.Init.ClockDivision = 0; 47. htim.Init.CounterMode = TIM_COUNTERMODE_UP; 48. htim.Init.RepetitionCounter = 0; 49. HAL_TIM_Base_Init(&htim); 50. 51. sConfig.OCMode = TIM_OCMODE_PWM1; 52. sConfig.OCPolarity = TIM_OCPOLARITY_LOW; 53. 54. /* 占空比50% */ 55. sConfig.Pulse = 1000; 56. if(HAL_TIM_OC_ConfigChannel(&htim, &sConfig, TIM_CHANNEL_1) != HAL_OK) 57. { 58. Error_Handler(__FILE__, __LINE__); 59. } 60. 61. /* 启动OC1 */ 62. if(HAL_TIM_OC_Start(&htim, TIM_CHANNEL_1) != HAL_OK) 63. { 64. Error_Handler(__FILE__, __LINE__); 65. } 66. }
这里把几个关键的地方阐释下:
- 第11 – 12行,对作为局部变量的HAL库结构体做初始化,防止不确定值配置时出问题。
- 第18 – 65行,注释已经比较详细,配置TIM1的频率是100KHz,这个速度就是ADC的触发频率。
TIM1CLK = 200MHz / (Period + 1) / (Prescaler + 1) = 200MHz/(1999+1)/(0+1) = 100KHz
占空比 = Pulse / (Period + 1) = 1000 / (1999+1)= 50%
这些知识点在前面的定时器章节有更详细的说明。
45.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
45.3.3 ADC的DMA双缓冲配置
由于函数HAL_ADC_Start_DMA封装的DMA传输函数是HAL_DMA_Start_IT,而不是专门的DMA双缓冲函数HAL_DMAEx_MultiBuferStart_IT。所以要实现双缓冲效果的话,可以使用半传输完成中断和传输完成中断配合实现双缓冲效果。这里使用的是DMA1_Stream1:
1. /* 2. ****************************************************************************************************** 3. * 函 数 名: bsp_InitADC 4. * 功能说明: 初始化ADC 5. * 形 参: 无 6. * 返 回 值: 无 7. ****************************************************************************************************** 8. */ 9. void bsp_InitADC(void) 10. { 11. ADC_HandleTypeDef AdcHandle = {0}; 12. DMA_HandleTypeDef DmaHandle = {0}; 13. ADC_ChannelConfTypeDef sConfig = {0}; 14. GPIO_InitTypeDef GPIO_InitStruct; 15. 16. 17. /* ## - 1 - 配置ADC采样的时钟 ####################################### */ 18. #if defined (ADC_CLOCK_SOURCE_PLL) 19. /* 配置PLL2时钟为的72MHz,方便分频产生ADC最高时钟36MHz */ 20. RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0}; 21. PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_ADC; 22. PeriphClkInitStruct.PLL2.PLL2M = 25; 23. PeriphClkInitStruct.PLL2.PLL2N = 504; 24. PeriphClkInitStruct.PLL2.PLL2P = 7; 25. PeriphClkInitStruct.PLL2.PLL2Q = 7; 26. PeriphClkInitStruct.PLL2.PLL2R = 7; 27. PeriphClkInitStruct.PLL2.PLL2RGE = RCC_PLL2VCIRANGE_0; 28. PeriphClkInitStruct.PLL2.PLL2VCOSEL = RCC_PLL2VCOWIDE; 29. PeriphClkInitStruct.PLL2.PLL2FRACN = 0; 30. PeriphClkInitStruct.AdcClockSelection = RCC_ADCCLKSOURCE_PLL2; 31. if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK) 32. { 33. Error_Handler(__FILE__, __LINE__); 34. } 35. #elif defined (ADC_CLOCK_SOURCE_AHB) 36. 37. /* 使用AHB时钟的话,无需配置,默认选择*/ 38. 39. #endif 40. 41. /* ## - 2 - 配置ADC采样使用的采集引脚 ####################################### */ 42. __HAL_RCC_GPIOC_CLK_ENABLE(); 43. 44. GPIO_InitStruct.Pin = GPIO_PIN_0; 45. GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; 46. GPIO_InitStruct.Pull = GPIO_NOPULL; 47. HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); 48. 49. /* ## - 3 - 配置ADC采样使用的时钟 ####################################### */ 50. __HAL_RCC_DMA1_CLK_ENABLE(); 51. DmaHandle.Instance = DMA1_Stream1; /* 使用的DMA1 Stream1 */ 52. DmaHandle.Init.Request = DMA_REQUEST_ADC1; /* 请求类型采用DMA_REQUEST_ADC1 */ 53. DmaHandle.Init.Direction = DMA_PERIPH_TO_MEMORY; /* 传输方向是从外设到存储器 */ 54. DmaHandle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设地址自增禁止 */ 55. DmaHandle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器地址自增使能 */ 56. DmaHandle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;/* 外设数据传输位宽选择半字,16bit */ 57. DmaHandle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;/* 存储器数据传输位宽选半字,16bit */ 58. DmaHandle.Init.Mode = DMA_CIRCULAR; /* 循环模式 */ 59. DmaHandle.Init.Priority = DMA_PRIORITY_LOW; /* 优先级低 */ 60. DmaHandle.Init.FIFOMode = DMA_FIFOMODE_DISABLE; /* 禁止FIFO*/ 61. DmaHandle.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL; /* 禁止FIFO此位不起作用,用于设置阀值 */ 62. DmaHandle.Init.MemBurst = DMA_MBURST_SINGLE; /* 禁止FIFO此位不起作用,用于存储器突发 */ 63. DmaHandle.Init.PeriphBurst = DMA_PBURST_SINGLE; /* 禁止FIFO此位不起作用,用于外设突发 */ 64. 65. /* 初始化DMA */ 66. if(HAL_DMA_Init(&DmaHandle) != HAL_OK) 67. { 68. Error_Handler(__FILE__, __LINE__); 69. } 70. 71. /* 开启DMA1 Stream1的中断 */ 72. HAL_NVIC_SetPriority(DMA1_Stream1_IRQn, 2, 0); 73. HAL_NVIC_EnableIRQ(DMA1_Stream1_IRQn); 74. 75. /* 关联ADC句柄和DMA句柄 */ 76. __HAL_LINKDMA(&AdcHandle, DMA_Handle, DmaHandle); 77. 78. /* ## - 4 - 配置ADC ########################################################### */ 79. __HAL_RCC_ADC12_CLK_ENABLE(); 80. AdcHandle.Instance = ADC1; 81. 82. #if defined (ADC_CLOCK_SOURCE_PLL) 83. AdcHandle.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV2; /* 采用PLL异步时钟,2分频,即72MHz/2 84. = 36MHz */ 85. #elif defined (ADC_CLOCK_SOURCE_AHB) 86. AdcHandle.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; /* 采用AHB同步时钟,4分频,即200MHz/4 87. = 50MHz */ 88. #endif 89. AdcHandle.Init.Resolution = ADC_RESOLUTION_16B; /* 16位分辨率 */ 90. AdcHandle.Init.ScanConvMode = ADC_SCAN_DISABLE; /* 禁止扫描,因为仅开了一个通道 */ 91. AdcHandle.Init.EOCSelection = ADC_EOC_SINGLE_CONV; /* EOC转换结束标志 */ 92. AdcHandle.Init.LowPowerAutoWait = DISABLE; /* 禁止低功耗自动延迟特性 */ 93. AdcHandle.Init.ContinuousConvMode = DISABLE; /* 禁止自动转换,采用的定时器触发转换 */ 94. AdcHandle.Init.NbrOfConversion = 1; /* 使用了1个转换通道 */ 95. AdcHandle.Init.DiscontinuousConvMode = DISABLE; /* 禁止不连续模式 */ 96. AdcHandle.Init.NbrOfDiscConversion = 1; /* 禁止不连续模式后,此参数忽略,此位是用来配置 97. 不连续子组中通道数 */ 98. AdcHandle.Init.ExternalTrigConv = ADC_EXTERNALTRIG_T1_CC1; /* 定时器1的CC1触发 */ 99. AdcHandle.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; /* 上升沿触发 */ 100. AdcHandle.Init.ConversionDataManagement = ADC_CONVERSIONDATA_DMA_CIRCULAR; /* DMA循环模式接收ADC 101. 转换的数据 */ 102. AdcHandle.Init.BoostMode = ENABLE; /* ADC时钟超过20MHz的话,使能boost */ 103. AdcHandle.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; /* ADC转换溢出的话,覆盖ADC的数据寄存器 */ 104. AdcHandle.Init.OversamplingMode = DISABLE; /* 禁止过采样 */ 105. 106. /* 初始化ADC */ 107. if (HAL_ADC_Init(&AdcHandle) != HAL_OK) 108. { 109. Error_Handler(__FILE__, __LINE__); 110. } 111. 112. /* 校准ADC,采用偏移校准 */ 113. if (HAL_ADCEx_Calibration_Start(&AdcHandle, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED) != HAL_OK) 114. { 115. Error_Handler(__FILE__, __LINE__); 116. } 117. 118. /* 配置ADC通道 */ 119. sConfig.Channel = ADC_CHANNEL_10; /* 配置使用的ADC通道 */ 120. sConfig.Rank = ADC_REGULAR_RANK_1; /* 采样序列里的第1个 */ 121. sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_5; /* 采样周期 */ 122. sConfig.SingleDiff = ADC_SINGLE_ENDED; /* 单端输入 */ 123. sConfig.OffsetNumber = ADC_OFFSET_NONE; /* 无偏移 */ 124. sConfig.Offset = 0; /* 无偏移的情况下,此参数忽略 */ 125. 126. if (HAL_ADC_ConfigChannel(&AdcHandle, &sConfig) != HAL_OK) 127. { 128. Error_Handler(__FILE__, __LINE__); 129. } 130. 131. /* ## - 5 - 配置ADC的定时器触发 ####################################### */ 132. TIM1_Config(); 133. 134. /* ## - 6 - 启动ADC的DMA方式传输 ####################################### */ 135. if (HAL_ADC_Start_DMA(&AdcHandle, (uint32_t *)ADCxValues, 128) != HAL_OK) 136. { 137. Error_Handler(__FILE__, __LINE__); 138. } 139. }
这里把几个关键的地方阐释下:
- 第11 - 13行,对作为局部变量的HAL库结构体做初始化,防止不确定值配置时出问题。
- 第18 - 39行,前面2.2小节已经讲解,ADC时钟源选择AHB时钟还是PLL时钟。
- 第42 – 47行,选择PC0作为数据采集引脚。
- 第50- 69行,配置DMA的基本参数,注释较详细。这里是采用的ADC外设到内部SRAM的传输方向,数据带宽设置16bit,循环传输模式。
- 第72 – 73行,配置DMA的中断优先级,并使能。
- 第76行,这行代码比较重要,应用中容易被遗忘,用于关联ADC句柄和DMA句柄。在用户调用ADC的DMA传输方式函数HAL_ADC_Start_DMA时,此函数内部调用的HAL_DMA_Start_IT会用到DMA句柄。
- 第79 - 110行,主要是ADC的配置,注释较详细,配置ADC1为16bit模式,采用定时器1的CC1作为外部触发。
- 第113 – 116行,这里的是采用的ADC偏移校准,如果要采用线性度校准,务必要注意此贴的问题:http://www.armbbs.cn/forum.php?mod=viewthread&tid=91436 。
- 第119 -129行,配置ADC通道参数。这里使用的通道10是PC0引脚的复用功能,不是随意设置的。另外注意这里的采用周期,取的最小值,方便实现ADC外部触发的最快速度。
- 第132行,配置ADC的定时器触发,在本章2.2小节有讲解。
- 第135 – 138行,启动ADC的DMA方式数据传输。
45.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[128]; #elif defined ( __CC_ARM ) ALIGN_32BYTES(__attribute__((section (".RAM_D3"))) uint16_t ADCxValues[128]); #endif
对于IAR需要#pragma location指定位置,而MDK通过分散加载即可实现,详情看前面第26章,有详细讲解。
45.3.5 DMA中断处理
调用函数HAL_ADC_Start_DMA开启了DMA的传输完成中断,半传输完成中断,传输错误中断和直接模式错误中断。通过传输完成中断和半传输完整中断可以实现双缓冲的处理:
/* ********************************************************************************************************* * 函 数 名: DMA1_Stream1_IRQHandler * 功能说明: DMA1 Stream1中断服务程序 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void DMA1_Stream1_IRQHandler(void) { /* 传输完成中断 */ if((DMA1->LISR & DMA_FLAG_TCIF1_5) != RESET) { /* 清除标志 */ DMA1->LIFCR = DMA_FLAG_TCIF1_5; /* 当前使用的缓冲0 */ if((DMA1_Stream1->CR & DMA_SxCR_CT) == RESET) { /* 1、当前正在使用缓冲0,此时可以动态修改缓冲1的数据。 比如缓冲区0是IO_Toggle,缓冲区1是IO_Toggle1,那么此时就可以修改IO_Toggle1。 2、变量所在的SRAM区已经通过MPU配置为WT模式,更新变量IO_Toggle会立即写入。 3、不配置MPU的话,也可以通过Cahce的函数SCB_CleanDCache_by_Addr做Clean操作。 */ } /* 当前使用的缓冲1 */ else { /* 1、当前正在使用缓冲1,此时可以动态修改缓冲0的数据。 比如缓冲区0是IO_Toggle,缓冲区1是IO_Toggle1,那么此时就可以修改IO_Toggle。 2、变量所在的SRAM区已经通过MPU配置为WT模式,更新变量IO_Toggle会立即写入。 3、不配置MPU的话,也可以通过Cahce的函数SCB_CleanDCache_by_Addr做Clean操作。 */ } } /* 半传输完成中断 */ if((DMA1->LISR & DMA_FLAG_HTIF1_5) != RESET) { /* 清除标志 */ DMA1->LISR = DMA_FLAG_HTIF1_5; } /* 传输错误中断 */ if((DMA1->LISR & DMA_FLAG_TEIF1_5) != RESET) { /* 清除标志 */ DMA1->LISR = DMA_FLAG_TEIF1_5; } /* 直接模式错误中断 */ if((DMA1->LISR & DMA_FLAG_DMEIF1_5) != RESET) { /* 清除标志 */ DMA1->LISR = DMA_FLAG_DMEIF1_5; } } /* ********************************************************************************************************* * 函 数 名: DMA1_Stream1_IRQHandler * 功能说明: DMA1 Stream1中断服务程序 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void DMA1_Stream1_IRQHandler(void) { /* 传输完成中断 */ if((DMA1->LISR & DMA_FLAG_TCIF1_5) != RESET) { HC574_TogglePin(GPIO_PIN_23); /* 1、使用此函数要特别注意,第1个参数地址要32字节对齐,第2个参数要是32字节的整数倍。 2、进入传输完成中断,当前DMA正在使用缓冲区的前半部分,用户可以操作后半部分。 */ SCB_InvalidateDCache_by_Addr((uint32_t *)(&ADCxValues[64]), 128); s_DmaFlag = 2; /* 清除标志 */ DMA1->LIFCR = DMA_FLAG_TCIF1_5; } /* 半传输完成中断 */ if((DMA1->LISR & DMA_FLAG_HTIF1_5) != RESET) { /* 1、使用此函数要特别注意,第1个参数地址要32字节对齐,第2个参数要是32字节的整数倍。 2、进入半传输完成中断,当前DMA正在使用缓冲区的后半部分,用户可以操作前半部分。 */ SCB_InvalidateDCache_by_Addr((uint32_t *)(&ADCxValues[0]), 128); s_DmaFlag = 1; /* 清除标志 */ DMA1->LIFCR = DMA_FLAG_HTIF1_5; } /* 传输错误中断 */ if((DMA1->LISR & DMA_FLAG_TEIF1_5) != RESET) { /* 清除标志 */ DMA1->LIFCR = DMA_FLAG_TEIF1_5; } /* 直接模式错误中断 */ if((DMA1->LISR & DMA_FLAG_DMEIF1_5) != RESET) { /* 清除标志 */ DMA1->LIFCR = DMA_FLAG_DMEIF1_5; } }
注释的比较清楚。如果输出的PWM频率较高,建议将DMA的缓冲区设置的大些,防止DMA中断的执行频率较高。
传输完成中断里面有个扩展IO翻转函数HC574_TogglePin(GPIO_PIN_23),大家可以通过示波器测量开发板上丝印为X23的排针,从而方便的查看DMA中断速度。
传输完成中断和半传输完成中断里面还有一个变量s_DmaFlag,当s_DmaFlag= 1时表示进DMA半传输完成中断,s_DmaFlag = 2表示进入DMA传输完成中断。
45.3.6 读取DMA双缓冲数据
DMA双缓冲方式配置一款RTOS是最方便的,可以在中断服务程序里面发消息给任务,让数据可以得到及时处理。而裸机方式的话,需要用户实时查询变量,检测到有数据了再进行处理。具体实现代码如下:
/* ********************************************************************************************************* * 函 数 名: bsp_GetAdcValues * 功能说明: 获取ADC的数据并打印 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_GetAdcValues(void) { uint32_t values; float temp; /* 当前DMA操作是后半个缓冲,读取前半个缓冲的前4个数值求平均 */ if(s_DmaFlag == 1) { DISABLE_INT(); s_DmaFlag = 0; values = (ADCxValues[0] + ADCxValues[1] + ADCxValues[2] + ADCxValues[3])/4; ENABLE_INT(); } /* 当前DMA操作是后前个缓冲,读取后半个缓冲的前4个数值求平均 */ else if(s_DmaFlag == 2) { DISABLE_INT(); s_DmaFlag = 0; values = (ADCxValues[64] + ADCxValues[65] + ADCxValues[66] + ADCxValues[67])/4; ENABLE_INT(); } /* 打印读出的串口值 */ temp = values *3.3 / 65536; printf("ADCxValues = %d, %5.3f\r\n", values, temp); }
45.4 ADC板级支持包(bsp_adc.c)
ADC驱动文件bsp_adc.c提供了如下三个函数:
- TIM1_Config
- bsp_InitADC
- bsp_GetAdcValues
45.4.1 函数TIM1_Config
函数原型:
static void TIM1_Config(void)
函数描述:
此函数用于配置TIM1工作在OC输出比较模式,使用TIM1的CC1作为ADC的触发源。
注意事项:
- 关于此函数的讲解在本章的2.1小节。
- 函数前面的static表示限制在bsp_adc.c文件里面调用。
45.4.2 函数bsp_InitADC
函数原型:
void bsp_InitADC(void)
函数描述:
此函数用于配置定时器触发ADC做DMA传输。
注意事项:
- 关于此函数的讲解在本章2.2小节。
使用举例:
作为初始化函数,直接在bsp.c文件的bsp_Init函数里面调用即可。
45.4.3 函数bsp_GetAdcValues
函数原型:
void bsp_GetAdcValues(void)
函数描述:
此函数用于获取ADC的转换数据。
注意事项:
- 关于此函数的讲解在本章2.4,2.5和2.6小节。
使用举例:
如果是裸机工程: 要实时调用此函数读取双缓冲里面的数据。
如果是RTOS工程:要在DMA的中断服务程序里面给ADC任务发消息,让ADC任务可以及时读取数据。
45.5 ADC驱动移植和使用
ADC驱动的移植比较方便:
- 第1步:复制bsp_adc.c和bsp_adc.h到自己的工程目录,并添加到工程里面。
- 第2步:这几个驱动文件主要用到HAL库的GPIO、TIM,DMA和ADC驱动文件,简单省事些可以添加所有HAL库.C源文件进来。
- 第3步,应用方法看本章节配套例子即可,另外就是根据自己的需要做配置修改。
45.6 实验例程设计框架
通过程序设计框架,让大家先对配套例程有一个全面的认识,然后再理解细节,本次实验例程的设计框架如下:
第1阶段,上电启动阶段:
- 这部分在第4章进行了详细说明。
第2阶段,进入main函数:
- 第1步,硬件初始化,主要是MPU,Cache,HAL库,系统时钟,滴答定时器,LED,串口和ADC。
- 第2步,应用程序设计部分,周期性的打印数据,方便查看。
- 第3步,DMA中断,以双缓冲方式存储ADC数据。
45.7 实验例程说明(MDK)
配套例子:
V7-019_ADC定时器触发+DMA双缓冲实现
实验目的:
- 学习ADC定时器触发 + DMA双缓冲的实现。
实验内容:
- 例子默认用的AHB时钟供ADC使用,大家可以通过bsp_adc.c文件开头宏定义切换到PLL2专用时钟。
- 使用的TIM1的OC1作为ADC的外部触发源,触发速度是100KHz,即ADC的采样率也是100KHz。
- 使用DMA的半传输完成中断和传输完成中断实现数据的双缓冲更新。
- 采集引脚使用的PC0,另外特别注意开发板上的Vref稳压基准跳线帽短接的3.3V。
- 每隔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打印一次PC0引脚的采集值。
/* ********************************************************************************************************* * 函 数 名: 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)) { /* 每隔500ms 进来一次 */ bsp_LedToggle(2); /* 这里仅仅是为了展示方便,500ms更新一次,如果是实际工程里面应用 裸机工程: 要实时调用下面的函数读取双缓冲里面的数据。 RTOS工程:要在DMA的中断服务程序里面给ADC任务发消息,让ADC任务可以及时读取数据。 */ bsp_GetAdcValues(); } /* 按键滤波和检测由后台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; } } } }
DMA中断处理:
/* ********************************************************************************************************* * 函 数 名: DMA1_Stream1_IRQHandler * 功能说明: DMA1 Stream1中断服务程序 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void DMA1_Stream1_IRQHandler(void) { /* 传输完成中断 */ if((DMA1->LISR & DMA_FLAG_TCIF1_5) != RESET) { HC574_TogglePin(GPIO_PIN_23); /* 1、使用此函数要特别注意,第1个参数地址要32字节对齐,第2个参数要是32字节的整数倍。 2、进入传输完成中断,当前DMA正在使用缓冲区的前半部分,用户可以操作后半部分。 */ SCB_InvalidateDCache_by_Addr((uint32_t *)(&ADCxValues[64]), 128); s_DmaFlag = 2; /* 清除标志 */ DMA1->LIFCR = DMA_FLAG_TCIF1_5; } /* 半传输完成中断 */ if((DMA1->LISR & DMA_FLAG_HTIF1_5) != RESET) { /* 1、使用此函数要特别注意,第1个参数地址要32字节对齐,第2个参数要是32字节的整数倍。 2、进入半传输完成中断,当前DMA正在使用缓冲区的后半部分,用户可以操作前半部分。 */ SCB_InvalidateDCache_by_Addr((uint32_t *)(&ADCxValues[0]), 128); s_DmaFlag = 1; /* 清除标志 */ DMA1->LIFCR = DMA_FLAG_HTIF1_5; } /* 传输错误中断 */ if((DMA1->LISR & DMA_FLAG_TEIF1_5) != RESET) { /* 清除标志 */ DMA1->LIFCR = DMA_FLAG_TEIF1_5; } /* 直接模式错误中断 */ if((DMA1->LISR & DMA_FLAG_DMEIF1_5) != RESET) { /* 清除标志 */ DMA1->LIFCR = DMA_FLAG_DMEIF1_5; } }
45.8 实验例程说明(IAR)
配套例子:
V7-019_ADC定时器触发+DMA双缓冲实现
实验目的:
- 学习ADC定时器触发 + DMA双缓冲的实现。
实验内容:
- 例子默认用的AHB时钟供ADC使用,大家可以通过bsp_adc.c文件开头宏定义切换到PLL2专用时钟。
- 使用的TIM1的OC1作为ADC的外部触发源,触发速度是100KHz,即ADC的采样率也是100KHz。
- 使用DMA的半传输完成中断和传输完成中断实现数据的双缓冲更新。
- 采集引脚使用的PC0,另外特别注意开发板上的Vref稳压基准跳线帽短接的3.3V。
- 每隔500ms,串口会打印一次。
- 板子正常运行时LED2闪烁。
PC0引脚位置(稳压基准要短接3.3V):
上电后串口打印的信息:
波特率 115200,数据位 8,奇偶校验位无,停止位 1
上电后串口打印的信息:
波特率 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打印一次PC0引脚的采集值。
/* ********************************************************************************************************* * 函 数 名: 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)) { /* 每隔500ms 进来一次 */ bsp_LedToggle(2); /* 这里仅仅是为了展示方便,500ms更新一次,如果是实际工程里面应用 裸机工程: 要实时调用下面的函数读取双缓冲里面的数据。 RTOS工程:要在DMA的中断服务程序里面给ADC任务发消息,让ADC任务可以及时读取数据。 */ bsp_GetAdcValues(); } /* 按键滤波和检测由后台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; } } } }
DMA中断处理:
/* ********************************************************************************************************* * 函 数 名: DMA1_Stream1_IRQHandler * 功能说明: DMA1 Stream1中断服务程序 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void DMA1_Stream1_IRQHandler(void) { /* 传输完成中断 */ if((DMA1->LISR & DMA_FLAG_TCIF1_5) != RESET) { HC574_TogglePin(GPIO_PIN_23); /* 1、使用此函数要特别注意,第1个参数地址要32字节对齐,第2个参数要是32字节的整数倍。 2、进入传输完成中断,当前DMA正在使用缓冲区的前半部分,用户可以操作后半部分。 */ SCB_InvalidateDCache_by_Addr((uint32_t *)(&ADCxValues[64]), 128); s_DmaFlag = 2; /* 清除标志 */ DMA1->LIFCR = DMA_FLAG_TCIF1_5; } /* 半传输完成中断 */ if((DMA1->LISR & DMA_FLAG_HTIF1_5) != RESET) { /* 1、使用此函数要特别注意,第1个参数地址要32字节对齐,第2个参数要是32字节的整数倍。 2、进入半传输完成中断,当前DMA正在使用缓冲区的后半部分,用户可以操作前半部分。 */ SCB_InvalidateDCache_by_Addr((uint32_t *)(&ADCxValues[0]), 128); s_DmaFlag = 1; /* 清除标志 */ DMA1->LIFCR = DMA_FLAG_HTIF1_5; } /* 传输错误中断 */ if((DMA1->LISR & DMA_FLAG_TEIF1_5) != RESET) { /* 清除标志 */ DMA1->LIFCR = DMA_FLAG_TEIF1_5; } /* 直接模式错误中断 */ if((DMA1->LISR & DMA_FLAG_DMEIF1_5) != RESET) { /* 清除标志 */ DMA1->LIFCR = DMA_FLAG_DMEIF1_5; } }
45.9 总结
本章节就为大家讲解这么多,DMA双缓冲方式记录ADC数据还是非常实用的,建议初学者熟练应用这种方案。