更改

跳转至: 导航搜索

NBDK-L4:基础实验教程

添加17,867字节2019年1月22日 (二) 15:00
gyu_key.c
}
}
</syntaxhighlight>按键轮训函数,其实就是用来处理消抖的函数,我们根据从外部中断回调函数中的获取的时间(也就是中断触发的时间,上一个函数中记录的),对比现在实时的时间,判断是否超过20ms,如果超过20ms,则认为按键被按下。我们记录下按键信息,并且触发向应用层回调的函数。按键轮训函数,其实就是用来处理消抖的函数,我们根据从外部中断回调函数中的获取的时间(也就是中断触发的时间,上一个函数中记录的),对比现在实时的时间,判断是否超过20ms,如果超过20ms,则认为按键被按下。我们记录下按键信息,并且执行向应用层回调的函数。<syntaxhighlight lang="c++" line="1" start="171">
void KEY_Poll(void)
{
{
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;
}
}
510
个编辑

本PDF由谷雨文档中心自动生成,点击下方链接阅读最新内容。

取自“http://doc.iotxx.com/特殊:移动版差异/1345

导航菜单