“NBDK-L4:基础实验教程”的版本间的差异
(→gyu_key.c) |
|||
第1,032行: | 第1,032行: | ||
} | } | ||
} | } | ||
− | </syntaxhighlight> | + | </syntaxhighlight>按键轮训函数,其实就是用来处理消抖的函数,我们根据从外部中断回调函数中的获取的时间(也就是中断触发的时间,上一个函数中记录的),对比现在实时的时间,判断是否超过20ms,如果超过20ms,则认为按键被按下。我们记录下按键信息,并且执行向应用层回调的函数。<syntaxhighlight lang="c++" line="1" start="171"> |
void KEY_Poll(void) | void KEY_Poll(void) | ||
{ | { | ||
第1,092行: | 第1,092行: | ||
{ | { | ||
pFkey_cb(key_event); | pFkey_cb(key_event); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight>留给应用层调用注册按键回调的函数,用于将轮询后确认的按键信息,传递给应用层使用。<syntaxhighlight lang="c++" line="1" start="155"> | ||
+ | void KEY_RegisterCb(key_cb cb) | ||
+ | { | ||
+ | if(cb != 0) | ||
+ | { | ||
+ | pFkey_cb = cb; | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == 05-光敏二极管实验 == | ||
+ | 光敏二极管实验,是通过STM32L4的ADC引脚,获取光敏二极管的采集值 | ||
+ | |||
+ | === STM32L476 外部中断简介 === | ||
+ | 首先我们看一下外部中断/事件的GPIO映射图。 | ||
+ | [[文件:NBDK-DS-EXTI.png|边框|居中|无框|606x606像素]] | ||
+ | |||
+ | 由上面的映射图可以知道,多个GPIO引脚(GPIOA、GPIOB、GPIOC、GPIOD等等的GPIO_Pin_0)都会触发同一个中断线(EXTI line0)。也就是说,当EXTI0被触发时,我们无法判断他是PA0触发,还是PB0触发,因此大家在设计自己的硬件的时候,需要选择合适的中断引脚。 | ||
+ | |||
+ | 源码中我们配置外部中断的步骤如下: | ||
+ | |||
+ | 1.使能GPIO时钟 | ||
+ | |||
+ | 2.GPIO初始化,配置GPIO的边沿触发条件 | ||
+ | |||
+ | 3.设置EXTI线,配置GPIO与EXTI的关系 | ||
+ | |||
+ | 4.中断向量初始化 | ||
+ | |||
+ | === 硬件设计 === | ||
+ | 选择STM32L4引脚PC0、PC1、PC2、PC3作为按键的控制引脚。 | ||
+ | [[文件:NBDK-SCH-BUTTON.png|边框|居中|无框|434x434像素]] | ||
+ | === 实验准备 === | ||
+ | # 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。 | ||
+ | # 将SW1拨到DBG端,SW2拨到MCU。 | ||
+ | # 使用Keil打开基础实验 03-蜂鸣器实验工程。 | ||
+ | # 使用Xshell打开Jlink虚拟出的COM口 | ||
+ | # 下载程序,并完成功能测试。 | ||
+ | |||
+ | === 实验验证 === | ||
+ | 下载完成后,分别按下开发板上的S1、S2、S3、S4按键,可以看到Xshell中Jlink虚拟的COM口分别打印如下: | ||
+ | [[文件:NBDK-XSHELL-BTN.png|边框|居中|无框|759x759像素]] | ||
+ | |||
+ | === 源码详解 === | ||
+ | 本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。 | ||
+ | |||
+ | ==== stm32l4xx_hal_conf.h ==== | ||
+ | 此文件位于“04-按键中断实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件,一般情况下,我们默认需要使用的为前5个,包含芯片、flash、电源、时钟以及NVIC。 | ||
+ | |||
+ | 此例程我们只要展示的是外部GPIO中断,所以我们额外使能 HAL_GPIO_MODULE_ENABLED。另外为了辅助展示按键信息,我们额外添加了串口相关的DMA、UART这两个宏定义。<syntaxhighlight lang="c" line="1" start="103"> | ||
+ | // 使能的宏 | ||
+ | #define HAL_MODULE_ENABLED // 芯片 | ||
+ | #define HAL_FLASH_MODULE_ENABLED // Flash | ||
+ | #define HAL_PWR_MODULE_ENABLED // 电源 | ||
+ | #define HAL_RCC_MODULE_ENABLED // 时钟 | ||
+ | #define HAL_CORTEX_MODULE_ENABLED // NVIC | ||
+ | |||
+ | #define HAL_GPIO_MODULE_ENABLED // GPIO | ||
+ | #define HAL_DMA_MODULE_ENABLED // DMA | ||
+ | #define HAL_UART_MODULE_ENABLED // UART | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== main.c ==== | ||
+ | main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。 | ||
+ | |||
+ | 接下来我们初始化了串口部分,目的是打印按键按下的调试信息。 | ||
+ | |||
+ | 接下来是初始化按键,并且注册了按键回调函数(回调函数负责的是不同层之间的数据传输)。 | ||
+ | |||
+ | 在最后的while()循环中,我们调用按键轮训函数,这样一旦有外部中断触发,我们首先会进行一下按键消抖,确认是否为误判。如果判断是正常触发,则认为是有按键按下,此时按键处理文件gyu_key.c中会将按键信息,通过上面说的回调函数,传到应用层(mian.c)中进行处理。 | ||
+ | |||
+ | <syntaxhighlight lang="c++" line="1" start="36"> | ||
+ | int main(void) | ||
+ | { | ||
+ | /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ | ||
+ | // 重置所有外设、flash界面以及系统时钟 | ||
+ | HAL_Init(); | ||
+ | |||
+ | // 配置系统时钟(包含振荡器、系统时钟、总线时钟等等) | ||
+ | SystemClock_Config(); | ||
+ | |||
+ | // 初始化USART1 | ||
+ | MX_USART1_UART_Init(); | ||
+ | |||
+ | // 初始化按键引脚 | ||
+ | MX_KEY_Init(); | ||
+ | |||
+ | //注册按钮回调函数 | ||
+ | KEY_RegisterCb(AppKey_cb); | ||
+ | |||
+ | // | ||
+ | while (1) | ||
+ | { | ||
+ | KEY_Poll(); // 按键轮训,监测是否有按键被按下 | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight>在应用层的按键回调函数中,我们可以看到,当我们分别按下S1、S2、S3、S4按键后,STM32L4会通过串口向外部打印按键信息。<syntaxhighlight lang="c" line="1" start="69"> | ||
+ | void AppKey_cb(uint8_t key) | ||
+ | { | ||
+ | // 如果有相应按键被按下,则串口打印调试信息 | ||
+ | if(key & KEY_UP) | ||
+ | { | ||
+ | printf("key_up press\r\n"); | ||
+ | } | ||
+ | if(key & KEY_LEFT) | ||
+ | { | ||
+ | printf("key_left press\r\n"); | ||
+ | } | ||
+ | if(key & KEY_DOWN) | ||
+ | { | ||
+ | printf("key_down press\r\n"); | ||
+ | } | ||
+ | if(key & KEY_RIGHT) | ||
+ | { | ||
+ | printf("key_right press\r\n"); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== gyu_util.c ==== | ||
+ | 时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。 | ||
+ | |||
+ | 主要包含了三个部分的初始化配置。 | ||
+ | |||
+ | 1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。 | ||
+ | |||
+ | 2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。 | ||
+ | |||
+ | 3.外设时钟配置,选择外设时钟来源。 | ||
+ | |||
+ | 为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。 | ||
+ | |||
+ | 基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。<syntaxhighlight lang="c++" line="1" start="49"> | ||
+ | void SystemClock_Config(void) | ||
+ | { | ||
+ | RCC_OscInitTypeDef RCC_OscInitStruct; // 定义RCC内部/外部振荡器结构体 | ||
+ | RCC_ClkInitTypeDef RCC_ClkInitStruct; // 定义RCC系统,AHB和APB总线时钟配置结构体 | ||
+ | RCC_PeriphCLKInitTypeDef PeriphClkInit; // 定义RCC扩展时钟结构体 | ||
+ | |||
+ | // 配置LSE驱动器功能为低驱动能力 | ||
+ | __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW); | ||
+ | |||
+ | // 初始化CPU,AHB和APB总线时钟 | ||
+ | RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE | ||
+ | |RCC_OSCILLATORTYPE_LSE; // 设置需要配置的振荡器为HSI、HSE、LSE | ||
+ | // 配置HSE | ||
+ | RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 激活HSE时钟(开发板外部为8MHz) | ||
+ | // 配置LSE | ||
+ | RCC_OscInitStruct.LSEState = RCC_LSE_ON; // 激活LSE时钟(32.768KHz,低驱动) | ||
+ | // 配置HSI | ||
+ | RCC_OscInitStruct.HSIState = RCC_HSI_ON; // 激活HSI时钟 | ||
+ | RCC_OscInitStruct.HSICalibrationValue = 16; // 配置HSI为16MHz | ||
+ | // 配置PLL | ||
+ | RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开PLL | ||
+ | RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // 选择HSE时钟作为PLL入口时钟源,8MHz | ||
+ | RCC_OscInitStruct.PLL.PLLM = 1; // 配置PLL VCO输入分频为1,8/1 = 8MHz | ||
+ | RCC_OscInitStruct.PLL.PLLN = 20; // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz | ||
+ | RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; // SAI时钟7分频,160/7 = 22.857143MHz | ||
+ | RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz | ||
+ | RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; // 系统主时钟分区2分频,160/2 = 80MHz | ||
+ | // RCC时钟配置,出错则进入错误处理函数 | ||
+ | if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) | ||
+ | { | ||
+ | _Error_Handler(__FILE__, __LINE__); | ||
+ | } | ||
+ | |||
+ | // 初始化CPU,AHB和APB总线时钟 | ||
+ | RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK | ||
+ | |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2 | ||
+ | RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 配置系统时钟为PLLCLK输入,80MHz | ||
+ | RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB时钟为系统时钟1分频,80/1 = 80MHz | ||
+ | RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // APB1时钟为系统时钟1分频,80/1 = 80MHz | ||
+ | RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2时钟为系统时钟1分频,80/1 = 80MHz | ||
+ | // RCC时钟配置,出错则进入错误处理函数 | ||
+ | if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4) | ||
+ | { | ||
+ | _Error_Handler(__FILE__, __LINE__); | ||
+ | } | ||
+ | |||
+ | // 初始化外设时钟 | ||
+ | PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2 | ||
+ | |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1 | ||
+ | |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC; // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC | ||
+ | PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; // 配置串口USART1时钟为PCLK2,80MHz | ||
+ | PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; // 配置串口USART2时钟为PCLK1,80MHz | ||
+ | PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI; // 配置LPUART时钟为HSI,16MHz | ||
+ | PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1; // 配置I2C2时钟为PCLK1,80MHz | ||
+ | PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE; // 配置LPTIM1时钟为LSE,32.768KHz | ||
+ | PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1; // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义 | ||
+ | PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE; // 配置PLLSAI1时钟为HSE,8MHz | ||
+ | PeriphClkInit.PLLSAI1.PLLSAI1M = 1; // 配置PLLSAI1分频为1 | ||
+ | PeriphClkInit.PLLSAI1.PLLSAI1N = 8; // 配置PLLSAI1倍增为8 | ||
+ | PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7; // SAI时钟7分频,64/7 = 9.142857MHz | ||
+ | PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz | ||
+ | PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2; // 系统主时钟分区2分频,64/2 = 32MHz | ||
+ | PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK; // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz | ||
+ | // 外设时钟配置,出错则进入错误处理函数 | ||
+ | if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) | ||
+ | { | ||
+ | _Error_Handler(__FILE__, __LINE__); | ||
+ | } | ||
+ | |||
+ | // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz | ||
+ | if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) | ||
+ | { | ||
+ | _Error_Handler(__FILE__, __LINE__); | ||
+ | } | ||
+ | |||
+ | // 配置系统定时器中断时间,配置为HCLK的千分频 | ||
+ | HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000); | ||
+ | |||
+ | // 配置系统定时器,配置为HCLK | ||
+ | HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK); | ||
+ | |||
+ | // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0) | ||
+ | HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== gyu_key.c ==== | ||
+ | 首先我们看一下按键的初始化函数,在按键初始化函数中我们配置按键引脚的状态,四个按键引脚都被配置为默认上拉,下降沿中断触发。并且开启EXTI0、EXTI1、EXTI2、EXTI3这四个外部中断线。<syntaxhighlight lang="c++" line="1" start="75"> | ||
+ | void MX_KEY_Init(void) | ||
+ | { | ||
+ | // 定义GPIO结构体 | ||
+ | GPIO_InitTypeDef GPIO_InitStruct; | ||
+ | |||
+ | // 使能GPIOC引脚时钟(按键引脚:PC0、PC1、PC2、PC3) | ||
+ | __HAL_RCC_GPIOC_CLK_ENABLE(); | ||
+ | |||
+ | // 配置按键引脚 | ||
+ | GPIO_InitStruct.Pin = KEY_LEFT_Pin|KEY_DOWN_Pin|KEY_RIGHT_Pin|KEY_UP_Pin; // 选择PC0、PC1、PC2、PC3 | ||
+ | GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿中断触发 | ||
+ | GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉 | ||
+ | HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); // 初始化引脚 | ||
+ | |||
+ | // 配置中断优先级,并且使能中断 | ||
+ | { | ||
+ | // 配置PC0的中断,也就是EXTI line0 | ||
+ | HAL_NVIC_SetPriority(KEY_LEFT_EXTI_IRQn, 10, 0); | ||
+ | HAL_NVIC_EnableIRQ(KEY_LEFT_EXTI_IRQn); | ||
+ | |||
+ | // 配置PC1的中断,也就是EXTI line1 | ||
+ | HAL_NVIC_SetPriority(KEY_DOWN_EXTI_IRQn, 10, 0); | ||
+ | HAL_NVIC_EnableIRQ(KEY_DOWN_EXTI_IRQn); | ||
+ | |||
+ | // 配置PC2的中断,也就是EXTI line2 | ||
+ | HAL_NVIC_SetPriority(KEY_RIGHT_EXTI_IRQn, 10, 0); | ||
+ | HAL_NVIC_EnableIRQ(KEY_RIGHT_EXTI_IRQn); | ||
+ | |||
+ | // 配置PC3的中断,也就是EXTI line3 | ||
+ | HAL_NVIC_SetPriority(KEY_UP_EXTI_IRQn, 10, 0); | ||
+ | HAL_NVIC_EnableIRQ(KEY_UP_EXTI_IRQn); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight>如下,是我们在初始化函数中打开的四个中断线。<syntaxhighlight lang="c" line="1" start="43"> | ||
+ | // EXTI line0 中断函数 | ||
+ | void EXTI0_IRQHandler(void) | ||
+ | { | ||
+ | HAL_GPIO_EXTI_IRQHandler(KEY_LEFT_Pin); | ||
+ | } | ||
+ | |||
+ | // EXTI line1 中断函数 | ||
+ | void EXTI1_IRQHandler(void) | ||
+ | { | ||
+ | HAL_GPIO_EXTI_IRQHandler(KEY_DOWN_Pin); | ||
+ | } | ||
+ | |||
+ | // EXTI line2 中断函数 | ||
+ | void EXTI2_IRQHandler(void) | ||
+ | { | ||
+ | HAL_GPIO_EXTI_IRQHandler(KEY_RIGHT_Pin); | ||
+ | } | ||
+ | |||
+ | // EXTI line3 中断函数 | ||
+ | void EXTI3_IRQHandler(void) | ||
+ | { | ||
+ | HAL_GPIO_EXTI_IRQHandler(KEY_UP_Pin); | ||
+ | } | ||
+ | </syntaxhighlight>我们配置好上面所说的引脚外部中断后,一旦有按键被按下,则会触发中断,最终会跑到如下的HAL_GPIO_EXTI_Callback()函数中。我们在这个函数中,判断一下是哪一个中断线触发的中断,并且记录一下按键信息,以及触发的时间(记录触发的时间,是为了进行按键消抖,防止误操作)。<syntaxhighlight lang="c++" line="1" start="119"> | ||
+ | void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) | ||
+ | { | ||
+ | // 如果是UP键被触发,记录按键任务为KEY_UP,并记录当前时钟 | ||
+ | if(GPIO_Pin == KEY_UP_Pin) | ||
+ | { | ||
+ | key_check_press.key_event = KEY_UP; | ||
+ | key_check_press.start_tick = HAL_GetTick(); | ||
+ | } | ||
+ | // 如果是LEFT键被触发,记录按键任务为KEY_LEFT,并记录当前时钟 | ||
+ | if(GPIO_Pin == KEY_LEFT_Pin) | ||
+ | { | ||
+ | key_check_press.key_event = KEY_LEFT; | ||
+ | key_check_press.start_tick = HAL_GetTick(); | ||
+ | } | ||
+ | // 如果是DOWN键被触发,记录按键任务为KEY_DOWN,并记录当前时钟 | ||
+ | if(GPIO_Pin == KEY_DOWN_Pin) | ||
+ | { | ||
+ | key_check_press.key_event = KEY_DOWN; | ||
+ | key_check_press.start_tick = HAL_GetTick(); | ||
+ | } | ||
+ | // 如果是RIGHT键被触发,记录按键任务为KEY_RIGHT,并记录当前时钟 | ||
+ | if(GPIO_Pin == KEY_RIGHT_Pin) | ||
+ | { | ||
+ | key_check_press.key_event = KEY_RIGHT; | ||
+ | key_check_press.start_tick = HAL_GetTick(); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight>按键轮训函数,其实就是用来处理消抖的函数,我们根据从外部中断回调函数中的获取的时间(也就是中断触发的时间,上一个函数中记录的),对比现在实时的时间,判断是否超过20ms,如果超过20ms,则认为按键被按下。我们记录下按键信息,并且执行向应用层回调的函数。<syntaxhighlight lang="c++" line="1" start="171"> | ||
+ | void KEY_Poll(void) | ||
+ | { | ||
+ | uint8_t key_event = 0; | ||
+ | |||
+ | // 如果有按键任务 | ||
+ | if(key_check_press.key_event) | ||
+ | { | ||
+ | // 获取当前时钟 减去 记录的按键触发时钟,如果大于消抖延时,则继续向下判断 | ||
+ | if(HAL_GetTick() - key_check_press.start_tick >= KEY_DELAY_TICK ) | ||
+ | { | ||
+ | // 如果按键任务记录为KEY_UP | ||
+ | if(key_check_press.key_event & KEY_UP) | ||
+ | { | ||
+ | // 获取当前KEY_UP引脚电平,如果是低电平,则认为UP按键被按下 | ||
+ | if(HAL_GPIO_ReadPin(KEY_UP_GPIO_Port,KEY_UP_Pin) == GPIO_PIN_RESET) | ||
+ | { | ||
+ | key_event |= KEY_UP; // 记录app按键任务 | ||
+ | } | ||
+ | key_check_press.key_event ^= KEY_UP; // 删除按键中断任务 | ||
+ | } | ||
+ | |||
+ | // 如果按键任务记录为KEY_LEFT | ||
+ | if(key_check_press.key_event & KEY_LEFT) | ||
+ | { | ||
+ | // 获取当前KEY_LEFT引脚电平,如果是低电平,则认为LEFT按键被按下 | ||
+ | if(HAL_GPIO_ReadPin(KEY_LEFT_GPIO_Port,KEY_LEFT_Pin) == GPIO_PIN_RESET) | ||
+ | { | ||
+ | key_event |= KEY_LEFT; // 记录app按键任务 | ||
+ | } | ||
+ | key_check_press.key_event ^= KEY_LEFT; // 删除按键中断任务 | ||
+ | } | ||
+ | |||
+ | // 如果按键任务记录为KEY_DOWN | ||
+ | if(key_check_press.key_event & KEY_DOWN) | ||
+ | { | ||
+ | // 获取当前KEY_DOWN引脚电平,如果是低电平,则认为DOWN按键被按下 | ||
+ | if(HAL_GPIO_ReadPin(KEY_DOWN_GPIO_Port,KEY_DOWN_Pin) == GPIO_PIN_RESET) | ||
+ | { | ||
+ | key_event |= KEY_DOWN; // 记录app按键任务 | ||
+ | } | ||
+ | key_check_press.key_event ^= KEY_DOWN; // 删除按键中断任务 | ||
+ | } | ||
+ | |||
+ | // 如果按键任务记录为KEY_RIGHT | ||
+ | if(key_check_press.key_event & KEY_RIGHT) | ||
+ | { | ||
+ | // 获取当前KEY_RIGHT引脚电平,如果是低电平,则认为RIGHT按键被按下 | ||
+ | if(HAL_GPIO_ReadPin(KEY_RIGHT_GPIO_Port,KEY_RIGHT_Pin) == GPIO_PIN_RESET) | ||
+ | { | ||
+ | key_event |= KEY_RIGHT; // 记录app按键任务 | ||
+ | } | ||
+ | key_check_press.key_event ^= KEY_RIGHT; // 删除按键中断任务 | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | //如果有记录给app的按键任务,代表真的有按钮按下,则执行回调函数 | ||
+ | if(key_event && pFkey_cb) | ||
+ | { | ||
+ | pFkey_cb(key_event); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight>留给应用层调用注册按键回调的函数,用于将轮询后确认的按键信息,传递给应用层使用。<syntaxhighlight lang="c++" line="1" start="155"> | ||
+ | void KEY_RegisterCb(key_cb cb) | ||
+ | { | ||
+ | if(cb != 0) | ||
+ | { | ||
+ | pFkey_cb = cb; | ||
} | } | ||
} | } |
2019年1月22日 (二) 15:00的版本
1 教程介绍
1.1 工程简介
NBDK-L4开发板基础实验包含如下,在这里给大家简单说明一下每个例程中讲解的内容及关键节点。
实验名称 | 内容简介 | 功能 |
---|---|---|
01-led实验 | 驱动LED点亮 | GPIO推挽输出 |
02-马达实验 | 驱动马达振动 | GPIO推挽输出 |
03-蜂鸣器实验 | 驱动蜂鸣器发声 | GPIO推挽输出 |
1.2 工程目录简介
大家打开任意一个基础例程,都会看到如下的4个目录(Drivers、Inc、MDK-ARM、Src)及clean.bat文件。
其中clean.bat是用于清除工程编译生成的中间文件。例如我们想拷贝一个编译过的工程,工程有200M左右大小,我们点击clean.bat清除一下编译生成的中间文件,则工程大概会缩小到100M左右,此时工程只剩下了库文件、用户文件,以及编译生成的hex文件。
从上图可以看到,四个主目录下分别包含的一些文件,这边给大家简单的介绍一下这边文件大概的功能。
Drivers:
STM32驱动文件目录,也就是大家常说的hal库,里面包含了hal(硬件抽象层)相关的文件。
主要就是有RCC时钟、Flash内存,以及大家常用的外设(例如uart、spi、adc等等)的一些库文件。
Inc:
用户.h头文件,用户文件的头文件一般都放到这边,也可自己另存其他位置,但是需要在keil中添加头文件所在的路径。
添加新路径的方式如下图所示,可以看到../Inc这个路径已经事先添加进去了。
MDK-ARM:
工程目录,主要是两个工程文件“.uvoptx”以及".uvprojx"(keil打开的是这个)。剩下的文件比较重要的是Output目录下编译生成的“.hex”文件。
Src:
用户.c文件,用户自己开发的一些驱动文件(外设驱动等等),以及main文件所在的目录。
1.3 常用文件简介
针对试验工程中的常见文件,我们以开发者的方式来给大家做一个简单的介绍。具体每个文件中的源码的详细说明,大家可以参照每个试验下的源码详解。
文件名 | 说明 |
---|---|
stm32l4xx_hal_conf.h | 路径位于Inc目录下,里面主要是一些宏定义,用于选择本工程所使用的库文件。这边选择的库文件,就是目录简介中提到的HAL库。 |
main.c: | main()所在的文件,keil中我们配置了run to main(),也就是说工程从main()函数开始执行,所以main.c就是我们工程的主文件。 |
stm32l4xx_hal_msp.c | msp(main stack pointer)主栈堆指针初始化的文件。我们重定义外设引脚选择的时候,STM32Cube生成的硬件引脚重定义函数默认也位于此文件下,但是为了方便,我们一般将其复制到各自的驱动文件下。 |
stm32l4xx_it.c | 中断配置文件,用于存放工程的中断。STM32Cube生成的中断函数默认位于此文件下,同样为了方便起见,我们一般将各自的中断函数放到各自的驱动文件下。 |
gyu_util.c | 从STM32Cube生成的main文件中独立出来的部分。主要用于处理工程的时钟选择,包含系统时钟、总线时钟以及外设时钟。 |
stm32l4xx_hal_xx | HAL库文件。 |
gyu_xx | 由谷雨物联编写的文件,大部分是外设的驱动文件。 |
2 01-led实验
第一个实验我们给大家带来的是最简单的外设控制,也就是 IO 口操作,通过这个实验我们可以了解到如何让STM32L476RC的一个 IO 输出高低电平,并以此控制 LED 的点亮和熄灭。
2.1 STM32L476 IO简介
每个GPIO引脚都可以通过软件配置为输出(推挽或漏极开路),输入(带或不带上拉或下拉)或外设备用功能。 大多数GPIO引脚与数字或模拟备用功能共用。 由于它们在AHB2总线上的映射,可以实现快速I / O切换。 如果需要,可以锁定I / O备用功能配置序列,以避免虚假写入I / O寄存器。
经过上一段对GPIO口模式的说明,在这里对它的工作模式进行一个小结,它一共有八种组合,即有八种可配置的工作模式,分别是:
- 输入浮空
- 输入上拉
- 输入下拉
- 模拟
- 带上拉或下拉的开漏输出
- 带上拉或下拉的推挽输出
- 带上拉或下拉的复用功能推挽
- 带上拉或下拉的复用功能开漏
2.2 硬件设计
选择STM32L4引脚PA15作为LED的控制引脚,PA15高电平时点亮LED。
2.3 实验准备
- 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
- 使用Keil打开基础实验 01-led实验工程。
- 下载程序,并完成功能测试。
2.4 实验验证
下载完成后,可以看到开发板上的LED灯周期闪烁,点亮及熄灭的周期时间为500ms。
2.5 源码详解
本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。
2.5.1 stm32l4xx_hal_conf.h
此文件位于“01-led实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件,一般情况下,我们默认需要使用的为前5个,包含芯片、flash、电源、时钟以及NVIC。
此例程因为我们需要展示IO的使用,所以我们额外使能 HAL_GPIO_MODULE_ENABLED。
103 // 使能的宏
104 #define HAL_MODULE_ENABLED // 芯片
105 #define HAL_FLASH_MODULE_ENABLED // Flash
106 #define HAL_PWR_MODULE_ENABLED // 电源
107 #define HAL_RCC_MODULE_ENABLED // 时钟
108 #define HAL_CORTEX_MODULE_ENABLED // NVIC
109
110 #define HAL_GPIO_MODULE_ENABLED // GPIO
2.5.2 main.c
main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。
接下来我们初始化LED引脚配置,并且在while()循环中周期点亮、熄灭LED。
31 int main(void)
32 {
33 /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
34 // 重置所有外设、flash界面以及系统时钟
35 HAL_Init();
36
37 // 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
38 SystemClock_Config();
39
40 // 初始化LED引脚
41 LED_Init();
42
43 //
44 while (1)
45 {
46 LED_SET(GPIO_PIN_SET); // 设置LED引脚(PA15)输出高电平,LED点亮
47 HAL_Delay(500); // 延时500ms
48 LED_SET(GPIO_PIN_RESET); // 设置LED引脚(PA15)输出低电平,LED熄灭
49 HAL_Delay(500); // 延时500ms
50 }
51 }
2.5.3 gyu_util.c
时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。
主要包含了三个部分的初始化配置。
1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。
2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。
3.外设时钟配置,选择外设时钟来源。
为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。
基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。
49 void SystemClock_Config(void)
50 {
51 RCC_OscInitTypeDef RCC_OscInitStruct; // 定义RCC内部/外部振荡器结构体
52 RCC_ClkInitTypeDef RCC_ClkInitStruct; // 定义RCC系统,AHB和APB总线时钟配置结构体
53 RCC_PeriphCLKInitTypeDef PeriphClkInit; // 定义RCC扩展时钟结构体
54
55 // 配置LSE驱动器功能为低驱动能力
56 __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
57
58 // 初始化CPU,AHB和APB总线时钟
59 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE
60 |RCC_OSCILLATORTYPE_LSE; // 设置需要配置的振荡器为HSI、HSE、LSE
61 // 配置HSE
62 RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 激活HSE时钟(开发板外部为8MHz)
63 // 配置LSE
64 RCC_OscInitStruct.LSEState = RCC_LSE_ON; // 激活LSE时钟(32.768KHz,低驱动)
65 // 配置HSI
66 RCC_OscInitStruct.HSIState = RCC_HSI_ON; // 激活HSI时钟
67 RCC_OscInitStruct.HSICalibrationValue = 16; // 配置HSI为16MHz
68 // 配置PLL
69 RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开PLL
70 RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // 选择HSE时钟作为PLL入口时钟源,8MHz
71 RCC_OscInitStruct.PLL.PLLM = 1; // 配置PLL VCO输入分频为1,8/1 = 8MHz
72 RCC_OscInitStruct.PLL.PLLN = 20; // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz
73 RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; // SAI时钟7分频,160/7 = 22.857143MHz
74 RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz
75 RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; // 系统主时钟分区2分频,160/2 = 80MHz
76 // RCC时钟配置,出错则进入错误处理函数
77 if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
78 {
79 _Error_Handler(__FILE__, __LINE__);
80 }
81
82 // 初始化CPU,AHB和APB总线时钟
83 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
84 |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2
85 RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 配置系统时钟为PLLCLK输入,80MHz
86 RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB时钟为系统时钟1分频,80/1 = 80MHz
87 RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // APB1时钟为系统时钟1分频,80/1 = 80MHz
88 RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2时钟为系统时钟1分频,80/1 = 80MHz
89 // RCC时钟配置,出错则进入错误处理函数
90 if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)
91 {
92 _Error_Handler(__FILE__, __LINE__);
93 }
94
95 // 初始化外设时钟
96 PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2
97 |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1
98 |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC; // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC
99 PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; // 配置串口USART1时钟为PCLK2,80MHz
100 PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; // 配置串口USART2时钟为PCLK1,80MHz
101 PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI; // 配置LPUART时钟为HSI,16MHz
102 PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1; // 配置I2C2时钟为PCLK1,80MHz
103 PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE; // 配置LPTIM1时钟为LSE,32.768KHz
104 PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1; // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义
105 PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE; // 配置PLLSAI1时钟为HSE,8MHz
106 PeriphClkInit.PLLSAI1.PLLSAI1M = 1; // 配置PLLSAI1分频为1
107 PeriphClkInit.PLLSAI1.PLLSAI1N = 8; // 配置PLLSAI1倍增为8
108 PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7; // SAI时钟7分频,64/7 = 9.142857MHz
109 PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz
110 PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2; // 系统主时钟分区2分频,64/2 = 32MHz
111 PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK; // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz
112 // 外设时钟配置,出错则进入错误处理函数
113 if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
114 {
115 _Error_Handler(__FILE__, __LINE__);
116 }
117
118 // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz
119 if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
120 {
121 _Error_Handler(__FILE__, __LINE__);
122 }
123
124 // 配置系统定时器中断时间,配置为HCLK的千分频
125 HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
126
127 // 配置系统定时器,配置为HCLK
128 HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
129
130 // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)
131 HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
132 }
2.5.4 gyu_led.c
此文件用于配置LED控制引脚,在LED_Init()函数中我们初始化PA15为推挽输出,并且使能GPIOA时钟,初始化PA15默认输出低电平。
31 void LED_Init(void)
32 {
33 GPIO_InitTypeDef GPIO_InitStructure; // 定义引脚参数结构体
34
35 __HAL_RCC_GPIOA_CLK_ENABLE(); // 使能GPIOA时钟
36
37 GPIO_InitStructure.Pin= GPIO_PIN_15; // 引脚编号为15
38 GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
39 GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_LOW; // 低频率
40 GPIO_InitStructure.Pull = GPIO_PULLDOWN; // 下拉
41 HAL_GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化PA15
42
43 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, GPIO_PIN_RESET); // 设置PA15默认输出低电平
44 }
LED_SET()函数留给大家控制LED灯点亮或者熄灭,参数可选为GPIO_PIN_RESET(低电平)或者GPIO_PIN_SET(高电平)。
54 void LED_SET(GPIO_PinState pinSate)
55 {
56 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, pinSate); // 设置PA15输出
57 }
3 02-马达实验
振动马达实验通过控制 GPIO 引脚输出高低电平,用于控制马达振动或停止 。
3.1 STM32L476 IO简介
每个GPIO引脚都可以通过软件配置为输出(推挽或漏极开路),输入(带或不带上拉或下拉)或外设备用功能。 大多数GPIO引脚与数字或模拟备用功能共用。 由于它们在AHB2总线上的映射,可以实现快速I / O切换。 如果需要,可以锁定I / O备用功能配置序列,以避免虚假写入I / O寄存器。
经过上一段对GPIO口模式的说明,在这里对它的工作模式进行一个小结,它一共有八种组合,即有八种可配置的工作模式,分别是:
- 输入浮空
- 输入上拉
- 输入下拉
- 模拟
- 带上拉或下拉的开漏输出
- 带上拉或下拉的推挽输出
- 带上拉或下拉的复用功能推挽
- 带上拉或下拉的复用功能开漏
3.2 硬件设计
选择STM32L4引脚PC7作为马达的控制引脚,PC7高电平时马达起振。
3.3 实验准备
- 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
- 使用Keil打开基础实验 02-马达实验工程。
- 下载程序,并完成功能测试。
3.4 实验验证
下载完成后,按下开发板上按键S1,马达起振,按下S3,马达停止。
3.5 源码详解
本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。
3.5.1 stm32l4xx_hal_conf.h
此文件位于“02-马达实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件,一般情况下,我们默认需要使用的为前5个,包含芯片、flash、电源、时钟以及NVIC。
此例程因为我们需要展示IO的使用,所以我们额外使能 HAL_GPIO_MODULE_ENABLED。
103 // 使能的宏
104 #define HAL_MODULE_ENABLED // 芯片
105 #define HAL_FLASH_MODULE_ENABLED // Flash
106 #define HAL_PWR_MODULE_ENABLED // 电源
107 #define HAL_RCC_MODULE_ENABLED // 时钟
108 #define HAL_CORTEX_MODULE_ENABLED // NVIC
109
110 #define HAL_GPIO_MODULE_ENABLED // GPIO
3.5.2 main.c
main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。
接下来是初始化按键,有关按键的部分会在04-按键实验中给大家讲解。
这边我们主要关注的是马达的初始化,其实是和LED实验一样的,就是初始化一下马达控制IO。
36 int main(void)
37 {
38 /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
39 // 重置所有外设、flash界面以及系统时钟
40 HAL_Init();
41
42 // 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
43 SystemClock_Config();
44
45 // 初始化按键引脚
46 MX_KEY_Init();
47
48 //注册按钮回调函数
49 KEY_RegisterCb(AppKey_cb);
50
51 // 初始化马达
52 Motor_Init();
53
54 //
55 while (1)
56 {
57 KEY_Poll(); // 按键轮训,监测是否有按键被按下
58 }
59 }
在按键的处理回调函数中,我们可以看到,按键S1(UP)按下后,设置马达引脚高电平,按键S3(DOWN)按下后,设置马达引脚低电平
69 void AppKey_cb(uint8_t key)
70 {
71 // 如果有相应按键被按下,则串口打印调试信息
72 if(key & KEY_UP)
73 {
74 Motor_SET(GPIO_PIN_SET);
75 }
76 if(key & KEY_LEFT)
77 {
78 //
79 }
80 if(key & KEY_DOWN)
81 {
82 Motor_SET(GPIO_PIN_RESET);
83 }
84 if(key & KEY_RIGHT)
85 {
86 //
87 }
88 }
3.5.3 gyu_util.c
时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。
主要包含了三个部分的初始化配置。
1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。
2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。
3.外设时钟配置,选择外设时钟来源。
为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。
基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。
49 void SystemClock_Config(void)
50 {
51 RCC_OscInitTypeDef RCC_OscInitStruct; // 定义RCC内部/外部振荡器结构体
52 RCC_ClkInitTypeDef RCC_ClkInitStruct; // 定义RCC系统,AHB和APB总线时钟配置结构体
53 RCC_PeriphCLKInitTypeDef PeriphClkInit; // 定义RCC扩展时钟结构体
54
55 // 配置LSE驱动器功能为低驱动能力
56 __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
57
58 // 初始化CPU,AHB和APB总线时钟
59 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE
60 |RCC_OSCILLATORTYPE_LSE; // 设置需要配置的振荡器为HSI、HSE、LSE
61 // 配置HSE
62 RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 激活HSE时钟(开发板外部为8MHz)
63 // 配置LSE
64 RCC_OscInitStruct.LSEState = RCC_LSE_ON; // 激活LSE时钟(32.768KHz,低驱动)
65 // 配置HSI
66 RCC_OscInitStruct.HSIState = RCC_HSI_ON; // 激活HSI时钟
67 RCC_OscInitStruct.HSICalibrationValue = 16; // 配置HSI为16MHz
68 // 配置PLL
69 RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开PLL
70 RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // 选择HSE时钟作为PLL入口时钟源,8MHz
71 RCC_OscInitStruct.PLL.PLLM = 1; // 配置PLL VCO输入分频为1,8/1 = 8MHz
72 RCC_OscInitStruct.PLL.PLLN = 20; // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz
73 RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; // SAI时钟7分频,160/7 = 22.857143MHz
74 RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz
75 RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; // 系统主时钟分区2分频,160/2 = 80MHz
76 // RCC时钟配置,出错则进入错误处理函数
77 if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
78 {
79 _Error_Handler(__FILE__, __LINE__);
80 }
81
82 // 初始化CPU,AHB和APB总线时钟
83 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
84 |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2
85 RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 配置系统时钟为PLLCLK输入,80MHz
86 RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB时钟为系统时钟1分频,80/1 = 80MHz
87 RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // APB1时钟为系统时钟1分频,80/1 = 80MHz
88 RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2时钟为系统时钟1分频,80/1 = 80MHz
89 // RCC时钟配置,出错则进入错误处理函数
90 if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)
91 {
92 _Error_Handler(__FILE__, __LINE__);
93 }
94
95 // 初始化外设时钟
96 PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2
97 |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1
98 |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC; // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC
99 PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; // 配置串口USART1时钟为PCLK2,80MHz
100 PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; // 配置串口USART2时钟为PCLK1,80MHz
101 PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI; // 配置LPUART时钟为HSI,16MHz
102 PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1; // 配置I2C2时钟为PCLK1,80MHz
103 PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE; // 配置LPTIM1时钟为LSE,32.768KHz
104 PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1; // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义
105 PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE; // 配置PLLSAI1时钟为HSE,8MHz
106 PeriphClkInit.PLLSAI1.PLLSAI1M = 1; // 配置PLLSAI1分频为1
107 PeriphClkInit.PLLSAI1.PLLSAI1N = 8; // 配置PLLSAI1倍增为8
108 PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7; // SAI时钟7分频,64/7 = 9.142857MHz
109 PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz
110 PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2; // 系统主时钟分区2分频,64/2 = 32MHz
111 PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK; // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz
112 // 外设时钟配置,出错则进入错误处理函数
113 if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
114 {
115 _Error_Handler(__FILE__, __LINE__);
116 }
117
118 // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz
119 if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
120 {
121 _Error_Handler(__FILE__, __LINE__);
122 }
123
124 // 配置系统定时器中断时间,配置为HCLK的千分频
125 HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
126
127 // 配置系统定时器,配置为HCLK
128 HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
129
130 // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)
131 HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
132 }
3.5.4 gyu_motor.c
马达引脚初始化函数,初始化PC7推挽输出低电平。
31 void Motor_Init(void)
32 {
33 GPIO_InitTypeDef GPIO_InitStructure; // 定义引脚参数结构体
34
35 __HAL_RCC_GPIOC_CLK_ENABLE(); // 使能GPIOC时钟
36
37 GPIO_InitStructure.Pin= GPIO_PIN_7; // 引脚编号为7
38 GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
39 GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_LOW; // 低频率
40 GPIO_InitStructure.Pull = GPIO_PULLUP; // 上拉
41 HAL_GPIO_Init(GPIOC, &GPIO_InitStructure); // 初始化PC7
42
43 HAL_GPIO_WritePin(GPIOC, GPIO_PIN_7, GPIO_PIN_RESET); // 设置PC7默认输出低电平
44 }
马达引脚电平设置函数,设置为高电平,马达起振,设置低电平,马达停止。
54 void Motor_SET(GPIO_PinState pinSate)
55 {
56 HAL_GPIO_WritePin(GPIOC, GPIO_PIN_7, pinSate); // 设置PC7输出
57 }
4 03-蜂鸣器实验
蜂鸣器实验通过控制 GPIO 引脚输出高低电平,用于控制蜂鸣器发出蜂鸣声或者停止发声 。
4.1 STM32L476 IO简介
每个GPIO引脚都可以通过软件配置为输出(推挽或漏极开路),输入(带或不带上拉或下拉)或外设备用功能。 大多数GPIO引脚与数字或模拟备用功能共用。 由于它们在AHB2总线上的映射,可以实现快速I / O切换。 如果需要,可以锁定I / O备用功能配置序列,以避免虚假写入I / O寄存器。
经过上一段对GPIO口模式的说明,在这里对它的工作模式进行一个小结,它一共有八种组合,即有八种可配置的工作模式,分别是:
- 输入浮空
- 输入上拉
- 输入下拉
- 模拟
- 带上拉或下拉的开漏输出
- 带上拉或下拉的推挽输出
- 带上拉或下拉的复用功能推挽
- 带上拉或下拉的复用功能开漏
4.2 硬件设计
选择STM32L4引脚PB2作为蜂鸣器的控制引脚,PB2高电平时蜂鸣器发出蜂鸣声。
4.3 实验准备
- 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
- 使用Keil打开基础实验 03-蜂鸣器实验工程。
- 下载程序,并完成功能测试。
4.4 实验验证
下载完成后,按下开发板上按键S1,蜂鸣器发声,按下S3,蜂鸣器停止。
4.5 源码详解
本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。
4.5.1 stm32l4xx_hal_conf.h
此文件位于“03-蜂鸣器实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件,一般情况下,我们默认需要使用的为前5个,包含芯片、flash、电源、时钟以及NVIC。
此例程因为我们需要展示IO的使用,所以我们额外使能 HAL_GPIO_MODULE_ENABLED。
103 // 使能的宏
104 #define HAL_MODULE_ENABLED // 芯片
105 #define HAL_FLASH_MODULE_ENABLED // Flash
106 #define HAL_PWR_MODULE_ENABLED // 电源
107 #define HAL_RCC_MODULE_ENABLED // 时钟
108 #define HAL_CORTEX_MODULE_ENABLED // NVIC
109
110 #define HAL_GPIO_MODULE_ENABLED // GPIO
4.5.2 main.c
main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。
接下来是初始化按键,有关按键的部分会在04-按键实验中给大家讲解。
最后我们初始化蜂鸣器引脚,配置蜂鸣器引脚为默认输出低电平。
36 int main(void)
37 {
38 /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
39 // 重置所有外设、flash界面以及系统时钟
40 HAL_Init();
41
42 // 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
43 SystemClock_Config();
44
45 // 初始化按键引脚
46 MX_KEY_Init();
47
48 //注册按钮回调函数
49 KEY_RegisterCb(AppKey_cb);
50
51 //初始化蜂鸣器
52 Buzzer_Init();
53
54 //
55 while (1)
56 {
57 KEY_Poll(); // 按键轮训,监测是否有按键被按下
58 }
59 }
在按键的处理回调函数中,我们可以看到,按键S1(UP)按下后,设置蜂鸣器引脚高电平,按键S3(DOWN)按下后,设置蜂鸣器引脚低电平
69 void AppKey_cb(uint8_t key)
70 {
71 // 如果有相应按键被按下,则串口打印调试信息
72 if(key & KEY_UP)
73 {
74 Motor_SET(GPIO_PIN_SET);
75 }
76 if(key & KEY_LEFT)
77 {
78 //
79 }
80 if(key & KEY_DOWN)
81 {
82 Motor_SET(GPIO_PIN_RESET);
83 }
84 if(key & KEY_RIGHT)
85 {
86 //
87 }
88 }
4.5.3 gyu_util.c
时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。
主要包含了三个部分的初始化配置。
1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。
2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。
3.外设时钟配置,选择外设时钟来源。
为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。
基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。
49 void SystemClock_Config(void)
50 {
51 RCC_OscInitTypeDef RCC_OscInitStruct; // 定义RCC内部/外部振荡器结构体
52 RCC_ClkInitTypeDef RCC_ClkInitStruct; // 定义RCC系统,AHB和APB总线时钟配置结构体
53 RCC_PeriphCLKInitTypeDef PeriphClkInit; // 定义RCC扩展时钟结构体
54
55 // 配置LSE驱动器功能为低驱动能力
56 __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
57
58 // 初始化CPU,AHB和APB总线时钟
59 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE
60 |RCC_OSCILLATORTYPE_LSE; // 设置需要配置的振荡器为HSI、HSE、LSE
61 // 配置HSE
62 RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 激活HSE时钟(开发板外部为8MHz)
63 // 配置LSE
64 RCC_OscInitStruct.LSEState = RCC_LSE_ON; // 激活LSE时钟(32.768KHz,低驱动)
65 // 配置HSI
66 RCC_OscInitStruct.HSIState = RCC_HSI_ON; // 激活HSI时钟
67 RCC_OscInitStruct.HSICalibrationValue = 16; // 配置HSI为16MHz
68 // 配置PLL
69 RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开PLL
70 RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // 选择HSE时钟作为PLL入口时钟源,8MHz
71 RCC_OscInitStruct.PLL.PLLM = 1; // 配置PLL VCO输入分频为1,8/1 = 8MHz
72 RCC_OscInitStruct.PLL.PLLN = 20; // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz
73 RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; // SAI时钟7分频,160/7 = 22.857143MHz
74 RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz
75 RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; // 系统主时钟分区2分频,160/2 = 80MHz
76 // RCC时钟配置,出错则进入错误处理函数
77 if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
78 {
79 _Error_Handler(__FILE__, __LINE__);
80 }
81
82 // 初始化CPU,AHB和APB总线时钟
83 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
84 |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2
85 RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 配置系统时钟为PLLCLK输入,80MHz
86 RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB时钟为系统时钟1分频,80/1 = 80MHz
87 RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // APB1时钟为系统时钟1分频,80/1 = 80MHz
88 RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2时钟为系统时钟1分频,80/1 = 80MHz
89 // RCC时钟配置,出错则进入错误处理函数
90 if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)
91 {
92 _Error_Handler(__FILE__, __LINE__);
93 }
94
95 // 初始化外设时钟
96 PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2
97 |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1
98 |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC; // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC
99 PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; // 配置串口USART1时钟为PCLK2,80MHz
100 PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; // 配置串口USART2时钟为PCLK1,80MHz
101 PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI; // 配置LPUART时钟为HSI,16MHz
102 PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1; // 配置I2C2时钟为PCLK1,80MHz
103 PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE; // 配置LPTIM1时钟为LSE,32.768KHz
104 PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1; // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义
105 PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE; // 配置PLLSAI1时钟为HSE,8MHz
106 PeriphClkInit.PLLSAI1.PLLSAI1M = 1; // 配置PLLSAI1分频为1
107 PeriphClkInit.PLLSAI1.PLLSAI1N = 8; // 配置PLLSAI1倍增为8
108 PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7; // SAI时钟7分频,64/7 = 9.142857MHz
109 PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz
110 PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2; // 系统主时钟分区2分频,64/2 = 32MHz
111 PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK; // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz
112 // 外设时钟配置,出错则进入错误处理函数
113 if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
114 {
115 _Error_Handler(__FILE__, __LINE__);
116 }
117
118 // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz
119 if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
120 {
121 _Error_Handler(__FILE__, __LINE__);
122 }
123
124 // 配置系统定时器中断时间,配置为HCLK的千分频
125 HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
126
127 // 配置系统定时器,配置为HCLK
128 HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
129
130 // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)
131 HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
132 }
4.5.4 gyu_buzzer.c
蜂鸣器引脚初始化函数,初始化PB2推挽输出低电平。
31 void Buzzer_Init(void)
32 {
33 GPIO_InitTypeDef GPIO_InitStructure; // 定义引脚参数结构体
34
35 __HAL_RCC_GPIOB_CLK_ENABLE(); // 使能GPIOB时钟
36
37 GPIO_InitStructure.Pin= GPIO_PIN_2; // 引脚编号为2
38 GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
39 GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_LOW; // 低频率
40 GPIO_InitStructure.Pull = GPIO_PULLUP; // 上拉
41 HAL_GPIO_Init(GPIOB, &GPIO_InitStructure); // 初始化PB2
42
43 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET); // 设置PB2默认输出低电平
44 }
蜂鸣器引脚电平设置函数,设置为高电平,蜂鸣器发出蜂鸣声,设置低电平,蜂鸣器停止。
54 void Buzzer_SET(GPIO_PinState pinSate)
55 {
56 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, pinSate); // 设置PB2输出
57 }
5 04-按键中断实验
按键中断实验,是通过外部引脚中断来判断是否有按键被按下,按键部分的代码这个在马达以及蜂鸣器实验中其实已经展示过了,这边给大家做一个详细的讲解。我们分别选择PC0、PC1、PC2、PC3这4个引脚作为我们的按键引脚,对应EXTI line0、EXTI line1、EXTI line2、EXTI line3。
5.1 STM32L476 外部中断简介
首先我们看一下外部中断/事件的GPIO映射图。
由上面的映射图可以知道,多个GPIO引脚(GPIOA、GPIOB、GPIOC、GPIOD等等的GPIO_Pin_0)都会触发同一个中断线(EXTI line0)。也就是说,当EXTI0被触发时,我们无法判断他是PA0触发,还是PB0触发,因此大家在设计自己的硬件的时候,需要选择合适的中断引脚。
源码中我们配置外部中断的步骤如下:
1.使能GPIO时钟
2.GPIO初始化,配置GPIO的边沿触发条件
3.设置EXTI线,配置GPIO与EXTI的关系
4.中断向量初始化
5.2 硬件设计
选择STM32L4引脚PC0、PC1、PC2、PC3作为按键的控制引脚。
5.3 实验准备
- 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
- 将SW1拨到DBG端,SW2拨到MCU。
- 使用Keil打开基础实验 03-蜂鸣器实验工程。
- 使用Xshell打开Jlink虚拟出的COM口
- 下载程序,并完成功能测试。
5.4 实验验证
下载完成后,分别按下开发板上的S1、S2、S3、S4按键,可以看到Xshell中Jlink虚拟的COM口分别打印如下:
5.5 源码详解
本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。
5.5.1 stm32l4xx_hal_conf.h
此文件位于“04-按键中断实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件,一般情况下,我们默认需要使用的为前5个,包含芯片、flash、电源、时钟以及NVIC。
此例程我们只要展示的是外部GPIO中断,所以我们额外使能 HAL_GPIO_MODULE_ENABLED。另外为了辅助展示按键信息,我们额外添加了串口相关的DMA、UART这两个宏定义。
103 // 使能的宏
104 #define HAL_MODULE_ENABLED // 芯片
105 #define HAL_FLASH_MODULE_ENABLED // Flash
106 #define HAL_PWR_MODULE_ENABLED // 电源
107 #define HAL_RCC_MODULE_ENABLED // 时钟
108 #define HAL_CORTEX_MODULE_ENABLED // NVIC
109
110 #define HAL_GPIO_MODULE_ENABLED // GPIO
111 #define HAL_DMA_MODULE_ENABLED // DMA
112 #define HAL_UART_MODULE_ENABLED // UART
5.5.2 main.c
main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。
接下来我们初始化了串口部分,目的是打印按键按下的调试信息。
接下来是初始化按键,并且注册了按键回调函数(回调函数负责的是不同层之间的数据传输)。
在最后的while()循环中,我们调用按键轮训函数,这样一旦有外部中断触发,我们首先会进行一下按键消抖,确认是否为误判。如果判断是正常触发,则认为是有按键按下,此时按键处理文件gyu_key.c中会将按键信息,通过上面说的回调函数,传到应用层(mian.c)中进行处理。
36 int main(void)
37 {
38 /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
39 // 重置所有外设、flash界面以及系统时钟
40 HAL_Init();
41
42 // 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
43 SystemClock_Config();
44
45 // 初始化USART1
46 MX_USART1_UART_Init();
47
48 // 初始化按键引脚
49 MX_KEY_Init();
50
51 //注册按钮回调函数
52 KEY_RegisterCb(AppKey_cb);
53
54 //
55 while (1)
56 {
57 KEY_Poll(); // 按键轮训,监测是否有按键被按下
58 }
59 }
在应用层的按键回调函数中,我们可以看到,当我们分别按下S1、S2、S3、S4按键后,STM32L4会通过串口向外部打印按键信息。
69 void AppKey_cb(uint8_t key)
70 {
71 // 如果有相应按键被按下,则串口打印调试信息
72 if(key & KEY_UP)
73 {
74 printf("key_up press\r\n");
75 }
76 if(key & KEY_LEFT)
77 {
78 printf("key_left press\r\n");
79 }
80 if(key & KEY_DOWN)
81 {
82 printf("key_down press\r\n");
83 }
84 if(key & KEY_RIGHT)
85 {
86 printf("key_right press\r\n");
87 }
88 }
5.5.3 gyu_util.c
时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。
主要包含了三个部分的初始化配置。
1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。
2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。
3.外设时钟配置,选择外设时钟来源。
为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。
基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。
49 void SystemClock_Config(void)
50 {
51 RCC_OscInitTypeDef RCC_OscInitStruct; // 定义RCC内部/外部振荡器结构体
52 RCC_ClkInitTypeDef RCC_ClkInitStruct; // 定义RCC系统,AHB和APB总线时钟配置结构体
53 RCC_PeriphCLKInitTypeDef PeriphClkInit; // 定义RCC扩展时钟结构体
54
55 // 配置LSE驱动器功能为低驱动能力
56 __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
57
58 // 初始化CPU,AHB和APB总线时钟
59 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE
60 |RCC_OSCILLATORTYPE_LSE; // 设置需要配置的振荡器为HSI、HSE、LSE
61 // 配置HSE
62 RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 激活HSE时钟(开发板外部为8MHz)
63 // 配置LSE
64 RCC_OscInitStruct.LSEState = RCC_LSE_ON; // 激活LSE时钟(32.768KHz,低驱动)
65 // 配置HSI
66 RCC_OscInitStruct.HSIState = RCC_HSI_ON; // 激活HSI时钟
67 RCC_OscInitStruct.HSICalibrationValue = 16; // 配置HSI为16MHz
68 // 配置PLL
69 RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开PLL
70 RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // 选择HSE时钟作为PLL入口时钟源,8MHz
71 RCC_OscInitStruct.PLL.PLLM = 1; // 配置PLL VCO输入分频为1,8/1 = 8MHz
72 RCC_OscInitStruct.PLL.PLLN = 20; // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz
73 RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; // SAI时钟7分频,160/7 = 22.857143MHz
74 RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz
75 RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; // 系统主时钟分区2分频,160/2 = 80MHz
76 // RCC时钟配置,出错则进入错误处理函数
77 if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
78 {
79 _Error_Handler(__FILE__, __LINE__);
80 }
81
82 // 初始化CPU,AHB和APB总线时钟
83 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
84 |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2
85 RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 配置系统时钟为PLLCLK输入,80MHz
86 RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB时钟为系统时钟1分频,80/1 = 80MHz
87 RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // APB1时钟为系统时钟1分频,80/1 = 80MHz
88 RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2时钟为系统时钟1分频,80/1 = 80MHz
89 // RCC时钟配置,出错则进入错误处理函数
90 if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)
91 {
92 _Error_Handler(__FILE__, __LINE__);
93 }
94
95 // 初始化外设时钟
96 PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2
97 |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1
98 |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC; // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC
99 PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; // 配置串口USART1时钟为PCLK2,80MHz
100 PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; // 配置串口USART2时钟为PCLK1,80MHz
101 PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI; // 配置LPUART时钟为HSI,16MHz
102 PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1; // 配置I2C2时钟为PCLK1,80MHz
103 PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE; // 配置LPTIM1时钟为LSE,32.768KHz
104 PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1; // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义
105 PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE; // 配置PLLSAI1时钟为HSE,8MHz
106 PeriphClkInit.PLLSAI1.PLLSAI1M = 1; // 配置PLLSAI1分频为1
107 PeriphClkInit.PLLSAI1.PLLSAI1N = 8; // 配置PLLSAI1倍增为8
108 PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7; // SAI时钟7分频,64/7 = 9.142857MHz
109 PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz
110 PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2; // 系统主时钟分区2分频,64/2 = 32MHz
111 PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK; // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz
112 // 外设时钟配置,出错则进入错误处理函数
113 if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
114 {
115 _Error_Handler(__FILE__, __LINE__);
116 }
117
118 // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz
119 if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
120 {
121 _Error_Handler(__FILE__, __LINE__);
122 }
123
124 // 配置系统定时器中断时间,配置为HCLK的千分频
125 HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
126
127 // 配置系统定时器,配置为HCLK
128 HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
129
130 // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)
131 HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
132 }
5.5.4 gyu_key.c
首先我们看一下按键的初始化函数,在按键初始化函数中我们配置按键引脚的状态,四个按键引脚都被配置为默认上拉,下降沿中断触发。并且开启EXTI0、EXTI1、EXTI2、EXTI3这四个外部中断线。
75 void MX_KEY_Init(void)
76 {
77 // 定义GPIO结构体
78 GPIO_InitTypeDef GPIO_InitStruct;
79
80 // 使能GPIOC引脚时钟(按键引脚:PC0、PC1、PC2、PC3)
81 __HAL_RCC_GPIOC_CLK_ENABLE();
82
83 // 配置按键引脚
84 GPIO_InitStruct.Pin = KEY_LEFT_Pin|KEY_DOWN_Pin|KEY_RIGHT_Pin|KEY_UP_Pin; // 选择PC0、PC1、PC2、PC3
85 GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿中断触发
86 GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉
87 HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); // 初始化引脚
88
89 // 配置中断优先级,并且使能中断
90 {
91 // 配置PC0的中断,也就是EXTI line0
92 HAL_NVIC_SetPriority(KEY_LEFT_EXTI_IRQn, 10, 0);
93 HAL_NVIC_EnableIRQ(KEY_LEFT_EXTI_IRQn);
94
95 // 配置PC1的中断,也就是EXTI line1
96 HAL_NVIC_SetPriority(KEY_DOWN_EXTI_IRQn, 10, 0);
97 HAL_NVIC_EnableIRQ(KEY_DOWN_EXTI_IRQn);
98
99 // 配置PC2的中断,也就是EXTI line2
100 HAL_NVIC_SetPriority(KEY_RIGHT_EXTI_IRQn, 10, 0);
101 HAL_NVIC_EnableIRQ(KEY_RIGHT_EXTI_IRQn);
102
103 // 配置PC3的中断,也就是EXTI line3
104 HAL_NVIC_SetPriority(KEY_UP_EXTI_IRQn, 10, 0);
105 HAL_NVIC_EnableIRQ(KEY_UP_EXTI_IRQn);
106 }
107 }
如下,是我们在初始化函数中打开的四个中断线。
43 // EXTI line0 中断函数
44 void EXTI0_IRQHandler(void)
45 {
46 HAL_GPIO_EXTI_IRQHandler(KEY_LEFT_Pin);
47 }
48
49 // EXTI line1 中断函数
50 void EXTI1_IRQHandler(void)
51 {
52 HAL_GPIO_EXTI_IRQHandler(KEY_DOWN_Pin);
53 }
54
55 // EXTI line2 中断函数
56 void EXTI2_IRQHandler(void)
57 {
58 HAL_GPIO_EXTI_IRQHandler(KEY_RIGHT_Pin);
59 }
60
61 // EXTI line3 中断函数
62 void EXTI3_IRQHandler(void)
63 {
64 HAL_GPIO_EXTI_IRQHandler(KEY_UP_Pin);
65 }
我们配置好上面所说的引脚外部中断后,一旦有按键被按下,则会触发中断,最终会跑到如下的HAL_GPIO_EXTI_Callback()函数中。我们在这个函数中,判断一下是哪一个中断线触发的中断,并且记录一下按键信息,以及触发的时间(记录触发的时间,是为了进行按键消抖,防止误操作)。
119 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
120 {
121 // 如果是UP键被触发,记录按键任务为KEY_UP,并记录当前时钟
122 if(GPIO_Pin == KEY_UP_Pin)
123 {
124 key_check_press.key_event = KEY_UP;
125 key_check_press.start_tick = HAL_GetTick();
126 }
127 // 如果是LEFT键被触发,记录按键任务为KEY_LEFT,并记录当前时钟
128 if(GPIO_Pin == KEY_LEFT_Pin)
129 {
130 key_check_press.key_event = KEY_LEFT;
131 key_check_press.start_tick = HAL_GetTick();
132 }
133 // 如果是DOWN键被触发,记录按键任务为KEY_DOWN,并记录当前时钟
134 if(GPIO_Pin == KEY_DOWN_Pin)
135 {
136 key_check_press.key_event = KEY_DOWN;
137 key_check_press.start_tick = HAL_GetTick();
138 }
139 // 如果是RIGHT键被触发,记录按键任务为KEY_RIGHT,并记录当前时钟
140 if(GPIO_Pin == KEY_RIGHT_Pin)
141 {
142 key_check_press.key_event = KEY_RIGHT;
143 key_check_press.start_tick = HAL_GetTick();
144 }
145 }
按键轮训函数,其实就是用来处理消抖的函数,我们根据从外部中断回调函数中的获取的时间(也就是中断触发的时间,上一个函数中记录的),对比现在实时的时间,判断是否超过20ms,如果超过20ms,则认为按键被按下。我们记录下按键信息,并且执行向应用层回调的函数。
171 void KEY_Poll(void)
172 {
173 uint8_t key_event = 0;
174
175 // 如果有按键任务
176 if(key_check_press.key_event)
177 {
178 // 获取当前时钟 减去 记录的按键触发时钟,如果大于消抖延时,则继续向下判断
179 if(HAL_GetTick() - key_check_press.start_tick >= KEY_DELAY_TICK )
180 {
181 // 如果按键任务记录为KEY_UP
182 if(key_check_press.key_event & KEY_UP)
183 {
184 // 获取当前KEY_UP引脚电平,如果是低电平,则认为UP按键被按下
185 if(HAL_GPIO_ReadPin(KEY_UP_GPIO_Port,KEY_UP_Pin) == GPIO_PIN_RESET)
186 {
187 key_event |= KEY_UP; // 记录app按键任务
188 }
189 key_check_press.key_event ^= KEY_UP; // 删除按键中断任务
190 }
191
192 // 如果按键任务记录为KEY_LEFT
193 if(key_check_press.key_event & KEY_LEFT)
194 {
195 // 获取当前KEY_LEFT引脚电平,如果是低电平,则认为LEFT按键被按下
196 if(HAL_GPIO_ReadPin(KEY_LEFT_GPIO_Port,KEY_LEFT_Pin) == GPIO_PIN_RESET)
197 {
198 key_event |= KEY_LEFT; // 记录app按键任务
199 }
200 key_check_press.key_event ^= KEY_LEFT; // 删除按键中断任务
201 }
202
203 // 如果按键任务记录为KEY_DOWN
204 if(key_check_press.key_event & KEY_DOWN)
205 {
206 // 获取当前KEY_DOWN引脚电平,如果是低电平,则认为DOWN按键被按下
207 if(HAL_GPIO_ReadPin(KEY_DOWN_GPIO_Port,KEY_DOWN_Pin) == GPIO_PIN_RESET)
208 {
209 key_event |= KEY_DOWN; // 记录app按键任务
210 }
211 key_check_press.key_event ^= KEY_DOWN; // 删除按键中断任务
212 }
213
214 // 如果按键任务记录为KEY_RIGHT
215 if(key_check_press.key_event & KEY_RIGHT)
216 {
217 // 获取当前KEY_RIGHT引脚电平,如果是低电平,则认为RIGHT按键被按下
218 if(HAL_GPIO_ReadPin(KEY_RIGHT_GPIO_Port,KEY_RIGHT_Pin) == GPIO_PIN_RESET)
219 {
220 key_event |= KEY_RIGHT; // 记录app按键任务
221 }
222 key_check_press.key_event ^= KEY_RIGHT; // 删除按键中断任务
223 }
224 }
225 }
226 //如果有记录给app的按键任务,代表真的有按钮按下,则执行回调函数
227 if(key_event && pFkey_cb)
228 {
229 pFkey_cb(key_event);
230 }
231 }
留给应用层调用注册按键回调的函数,用于将轮询后确认的按键信息,传递给应用层使用。
155 void KEY_RegisterCb(key_cb cb)
156 {
157 if(cb != 0)
158 {
159 pFkey_cb = cb;
160 }
161 }
6 05-光敏二极管实验
光敏二极管实验,是通过STM32L4的ADC引脚,获取光敏二极管的采集值
6.1 STM32L476 外部中断简介
首先我们看一下外部中断/事件的GPIO映射图。
由上面的映射图可以知道,多个GPIO引脚(GPIOA、GPIOB、GPIOC、GPIOD等等的GPIO_Pin_0)都会触发同一个中断线(EXTI line0)。也就是说,当EXTI0被触发时,我们无法判断他是PA0触发,还是PB0触发,因此大家在设计自己的硬件的时候,需要选择合适的中断引脚。
源码中我们配置外部中断的步骤如下:
1.使能GPIO时钟
2.GPIO初始化,配置GPIO的边沿触发条件
3.设置EXTI线,配置GPIO与EXTI的关系
4.中断向量初始化
6.2 硬件设计
选择STM32L4引脚PC0、PC1、PC2、PC3作为按键的控制引脚。
6.3 实验准备
- 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
- 将SW1拨到DBG端,SW2拨到MCU。
- 使用Keil打开基础实验 03-蜂鸣器实验工程。
- 使用Xshell打开Jlink虚拟出的COM口
- 下载程序,并完成功能测试。
6.4 实验验证
下载完成后,分别按下开发板上的S1、S2、S3、S4按键,可以看到Xshell中Jlink虚拟的COM口分别打印如下:
6.5 源码详解
本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。
6.5.1 stm32l4xx_hal_conf.h
此文件位于“04-按键中断实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件,一般情况下,我们默认需要使用的为前5个,包含芯片、flash、电源、时钟以及NVIC。
此例程我们只要展示的是外部GPIO中断,所以我们额外使能 HAL_GPIO_MODULE_ENABLED。另外为了辅助展示按键信息,我们额外添加了串口相关的DMA、UART这两个宏定义。
103 // 使能的宏
104 #define HAL_MODULE_ENABLED // 芯片
105 #define HAL_FLASH_MODULE_ENABLED // Flash
106 #define HAL_PWR_MODULE_ENABLED // 电源
107 #define HAL_RCC_MODULE_ENABLED // 时钟
108 #define HAL_CORTEX_MODULE_ENABLED // NVIC
109
110 #define HAL_GPIO_MODULE_ENABLED // GPIO
111 #define HAL_DMA_MODULE_ENABLED // DMA
112 #define HAL_UART_MODULE_ENABLED // UART
6.5.2 main.c
main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。
接下来我们初始化了串口部分,目的是打印按键按下的调试信息。
接下来是初始化按键,并且注册了按键回调函数(回调函数负责的是不同层之间的数据传输)。
在最后的while()循环中,我们调用按键轮训函数,这样一旦有外部中断触发,我们首先会进行一下按键消抖,确认是否为误判。如果判断是正常触发,则认为是有按键按下,此时按键处理文件gyu_key.c中会将按键信息,通过上面说的回调函数,传到应用层(mian.c)中进行处理。
36 int main(void)
37 {
38 /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
39 // 重置所有外设、flash界面以及系统时钟
40 HAL_Init();
41
42 // 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
43 SystemClock_Config();
44
45 // 初始化USART1
46 MX_USART1_UART_Init();
47
48 // 初始化按键引脚
49 MX_KEY_Init();
50
51 //注册按钮回调函数
52 KEY_RegisterCb(AppKey_cb);
53
54 //
55 while (1)
56 {
57 KEY_Poll(); // 按键轮训,监测是否有按键被按下
58 }
59 }
在应用层的按键回调函数中,我们可以看到,当我们分别按下S1、S2、S3、S4按键后,STM32L4会通过串口向外部打印按键信息。
69 void AppKey_cb(uint8_t key)
70 {
71 // 如果有相应按键被按下,则串口打印调试信息
72 if(key & KEY_UP)
73 {
74 printf("key_up press\r\n");
75 }
76 if(key & KEY_LEFT)
77 {
78 printf("key_left press\r\n");
79 }
80 if(key & KEY_DOWN)
81 {
82 printf("key_down press\r\n");
83 }
84 if(key & KEY_RIGHT)
85 {
86 printf("key_right press\r\n");
87 }
88 }
6.5.3 gyu_util.c
时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。
主要包含了三个部分的初始化配置。
1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。
2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。
3.外设时钟配置,选择外设时钟来源。
为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。
基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。
49 void SystemClock_Config(void)
50 {
51 RCC_OscInitTypeDef RCC_OscInitStruct; // 定义RCC内部/外部振荡器结构体
52 RCC_ClkInitTypeDef RCC_ClkInitStruct; // 定义RCC系统,AHB和APB总线时钟配置结构体
53 RCC_PeriphCLKInitTypeDef PeriphClkInit; // 定义RCC扩展时钟结构体
54
55 // 配置LSE驱动器功能为低驱动能力
56 __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
57
58 // 初始化CPU,AHB和APB总线时钟
59 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE
60 |RCC_OSCILLATORTYPE_LSE; // 设置需要配置的振荡器为HSI、HSE、LSE
61 // 配置HSE
62 RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 激活HSE时钟(开发板外部为8MHz)
63 // 配置LSE
64 RCC_OscInitStruct.LSEState = RCC_LSE_ON; // 激活LSE时钟(32.768KHz,低驱动)
65 // 配置HSI
66 RCC_OscInitStruct.HSIState = RCC_HSI_ON; // 激活HSI时钟
67 RCC_OscInitStruct.HSICalibrationValue = 16; // 配置HSI为16MHz
68 // 配置PLL
69 RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开PLL
70 RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // 选择HSE时钟作为PLL入口时钟源,8MHz
71 RCC_OscInitStruct.PLL.PLLM = 1; // 配置PLL VCO输入分频为1,8/1 = 8MHz
72 RCC_OscInitStruct.PLL.PLLN = 20; // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz
73 RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; // SAI时钟7分频,160/7 = 22.857143MHz
74 RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz
75 RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; // 系统主时钟分区2分频,160/2 = 80MHz
76 // RCC时钟配置,出错则进入错误处理函数
77 if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
78 {
79 _Error_Handler(__FILE__, __LINE__);
80 }
81
82 // 初始化CPU,AHB和APB总线时钟
83 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
84 |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2
85 RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 配置系统时钟为PLLCLK输入,80MHz
86 RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB时钟为系统时钟1分频,80/1 = 80MHz
87 RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // APB1时钟为系统时钟1分频,80/1 = 80MHz
88 RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2时钟为系统时钟1分频,80/1 = 80MHz
89 // RCC时钟配置,出错则进入错误处理函数
90 if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)
91 {
92 _Error_Handler(__FILE__, __LINE__);
93 }
94
95 // 初始化外设时钟
96 PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2
97 |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1
98 |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC; // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC
99 PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; // 配置串口USART1时钟为PCLK2,80MHz
100 PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; // 配置串口USART2时钟为PCLK1,80MHz
101 PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI; // 配置LPUART时钟为HSI,16MHz
102 PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1; // 配置I2C2时钟为PCLK1,80MHz
103 PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE; // 配置LPTIM1时钟为LSE,32.768KHz
104 PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1; // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义
105 PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE; // 配置PLLSAI1时钟为HSE,8MHz
106 PeriphClkInit.PLLSAI1.PLLSAI1M = 1; // 配置PLLSAI1分频为1
107 PeriphClkInit.PLLSAI1.PLLSAI1N = 8; // 配置PLLSAI1倍增为8
108 PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7; // SAI时钟7分频,64/7 = 9.142857MHz
109 PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz
110 PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2; // 系统主时钟分区2分频,64/2 = 32MHz
111 PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK; // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz
112 // 外设时钟配置,出错则进入错误处理函数
113 if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
114 {
115 _Error_Handler(__FILE__, __LINE__);
116 }
117
118 // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz
119 if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
120 {
121 _Error_Handler(__FILE__, __LINE__);
122 }
123
124 // 配置系统定时器中断时间,配置为HCLK的千分频
125 HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
126
127 // 配置系统定时器,配置为HCLK
128 HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
129
130 // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)
131 HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
132 }
6.5.4 gyu_key.c
首先我们看一下按键的初始化函数,在按键初始化函数中我们配置按键引脚的状态,四个按键引脚都被配置为默认上拉,下降沿中断触发。并且开启EXTI0、EXTI1、EXTI2、EXTI3这四个外部中断线。
75 void MX_KEY_Init(void)
76 {
77 // 定义GPIO结构体
78 GPIO_InitTypeDef GPIO_InitStruct;
79
80 // 使能GPIOC引脚时钟(按键引脚:PC0、PC1、PC2、PC3)
81 __HAL_RCC_GPIOC_CLK_ENABLE();
82
83 // 配置按键引脚
84 GPIO_InitStruct.Pin = KEY_LEFT_Pin|KEY_DOWN_Pin|KEY_RIGHT_Pin|KEY_UP_Pin; // 选择PC0、PC1、PC2、PC3
85 GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿中断触发
86 GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉
87 HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); // 初始化引脚
88
89 // 配置中断优先级,并且使能中断
90 {
91 // 配置PC0的中断,也就是EXTI line0
92 HAL_NVIC_SetPriority(KEY_LEFT_EXTI_IRQn, 10, 0);
93 HAL_NVIC_EnableIRQ(KEY_LEFT_EXTI_IRQn);
94
95 // 配置PC1的中断,也就是EXTI line1
96 HAL_NVIC_SetPriority(KEY_DOWN_EXTI_IRQn, 10, 0);
97 HAL_NVIC_EnableIRQ(KEY_DOWN_EXTI_IRQn);
98
99 // 配置PC2的中断,也就是EXTI line2
100 HAL_NVIC_SetPriority(KEY_RIGHT_EXTI_IRQn, 10, 0);
101 HAL_NVIC_EnableIRQ(KEY_RIGHT_EXTI_IRQn);
102
103 // 配置PC3的中断,也就是EXTI line3
104 HAL_NVIC_SetPriority(KEY_UP_EXTI_IRQn, 10, 0);
105 HAL_NVIC_EnableIRQ(KEY_UP_EXTI_IRQn);
106 }
107 }
如下,是我们在初始化函数中打开的四个中断线。
43 // EXTI line0 中断函数
44 void EXTI0_IRQHandler(void)
45 {
46 HAL_GPIO_EXTI_IRQHandler(KEY_LEFT_Pin);
47 }
48
49 // EXTI line1 中断函数
50 void EXTI1_IRQHandler(void)
51 {
52 HAL_GPIO_EXTI_IRQHandler(KEY_DOWN_Pin);
53 }
54
55 // EXTI line2 中断函数
56 void EXTI2_IRQHandler(void)
57 {
58 HAL_GPIO_EXTI_IRQHandler(KEY_RIGHT_Pin);
59 }
60
61 // EXTI line3 中断函数
62 void EXTI3_IRQHandler(void)
63 {
64 HAL_GPIO_EXTI_IRQHandler(KEY_UP_Pin);
65 }
我们配置好上面所说的引脚外部中断后,一旦有按键被按下,则会触发中断,最终会跑到如下的HAL_GPIO_EXTI_Callback()函数中。我们在这个函数中,判断一下是哪一个中断线触发的中断,并且记录一下按键信息,以及触发的时间(记录触发的时间,是为了进行按键消抖,防止误操作)。
119 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
120 {
121 // 如果是UP键被触发,记录按键任务为KEY_UP,并记录当前时钟
122 if(GPIO_Pin == KEY_UP_Pin)
123 {
124 key_check_press.key_event = KEY_UP;
125 key_check_press.start_tick = HAL_GetTick();
126 }
127 // 如果是LEFT键被触发,记录按键任务为KEY_LEFT,并记录当前时钟
128 if(GPIO_Pin == KEY_LEFT_Pin)
129 {
130 key_check_press.key_event = KEY_LEFT;
131 key_check_press.start_tick = HAL_GetTick();
132 }
133 // 如果是DOWN键被触发,记录按键任务为KEY_DOWN,并记录当前时钟
134 if(GPIO_Pin == KEY_DOWN_Pin)
135 {
136 key_check_press.key_event = KEY_DOWN;
137 key_check_press.start_tick = HAL_GetTick();
138 }
139 // 如果是RIGHT键被触发,记录按键任务为KEY_RIGHT,并记录当前时钟
140 if(GPIO_Pin == KEY_RIGHT_Pin)
141 {
142 key_check_press.key_event = KEY_RIGHT;
143 key_check_press.start_tick = HAL_GetTick();
144 }
145 }
按键轮训函数,其实就是用来处理消抖的函数,我们根据从外部中断回调函数中的获取的时间(也就是中断触发的时间,上一个函数中记录的),对比现在实时的时间,判断是否超过20ms,如果超过20ms,则认为按键被按下。我们记录下按键信息,并且执行向应用层回调的函数。
171 void KEY_Poll(void)
172 {
173 uint8_t key_event = 0;
174
175 // 如果有按键任务
176 if(key_check_press.key_event)
177 {
178 // 获取当前时钟 减去 记录的按键触发时钟,如果大于消抖延时,则继续向下判断
179 if(HAL_GetTick() - key_check_press.start_tick >= KEY_DELAY_TICK )
180 {
181 // 如果按键任务记录为KEY_UP
182 if(key_check_press.key_event & KEY_UP)
183 {
184 // 获取当前KEY_UP引脚电平,如果是低电平,则认为UP按键被按下
185 if(HAL_GPIO_ReadPin(KEY_UP_GPIO_Port,KEY_UP_Pin) == GPIO_PIN_RESET)
186 {
187 key_event |= KEY_UP; // 记录app按键任务
188 }
189 key_check_press.key_event ^= KEY_UP; // 删除按键中断任务
190 }
191
192 // 如果按键任务记录为KEY_LEFT
193 if(key_check_press.key_event & KEY_LEFT)
194 {
195 // 获取当前KEY_LEFT引脚电平,如果是低电平,则认为LEFT按键被按下
196 if(HAL_GPIO_ReadPin(KEY_LEFT_GPIO_Port,KEY_LEFT_Pin) == GPIO_PIN_RESET)
197 {
198 key_event |= KEY_LEFT; // 记录app按键任务
199 }
200 key_check_press.key_event ^= KEY_LEFT; // 删除按键中断任务
201 }
202
203 // 如果按键任务记录为KEY_DOWN
204 if(key_check_press.key_event & KEY_DOWN)
205 {
206 // 获取当前KEY_DOWN引脚电平,如果是低电平,则认为DOWN按键被按下
207 if(HAL_GPIO_ReadPin(KEY_DOWN_GPIO_Port,KEY_DOWN_Pin) == GPIO_PIN_RESET)
208 {
209 key_event |= KEY_DOWN; // 记录app按键任务
210 }
211 key_check_press.key_event ^= KEY_DOWN; // 删除按键中断任务
212 }
213
214 // 如果按键任务记录为KEY_RIGHT
215 if(key_check_press.key_event & KEY_RIGHT)
216 {
217 // 获取当前KEY_RIGHT引脚电平,如果是低电平,则认为RIGHT按键被按下
218 if(HAL_GPIO_ReadPin(KEY_RIGHT_GPIO_Port,KEY_RIGHT_Pin) == GPIO_PIN_RESET)
219 {
220 key_event |= KEY_RIGHT; // 记录app按键任务
221 }
222 key_check_press.key_event ^= KEY_RIGHT; // 删除按键中断任务
223 }
224 }
225 }
226 //如果有记录给app的按键任务,代表真的有按钮按下,则执行回调函数
227 if(key_event && pFkey_cb)
228 {
229 pFkey_cb(key_event);
230 }
231 }
留给应用层调用注册按键回调的函数,用于将轮询后确认的按键信息,传递给应用层使用。
155 void KEY_RegisterCb(key_cb cb)
156 {
157 if(cb != 0)
158 {
159 pFkey_cb = cb;
160 }
161 }