打开主菜单

谷雨文档中心 β

更改

NBDK-L4:基础实验教程

删除4,028字节2019年1月22日 (二) 17:22
实验准备
# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
# 将SW1拨到DBG端,SW2拨到MCU。
# 使用Keil打开基础实验 0304-蜂鸣器实验工程。按键中断实验工程。
# 使用Xshell打开Jlink虚拟出的COM口
# 下载程序,并完成功能测试。
# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
# 将SW1拨到DBG端,SW2拨到MCU。
# 使用Keil打开基础实验 0305-蜂鸣器实验工程。光敏二极管实验工程。
# 使用Xshell打开Jlink虚拟出的COM口
# 下载程序,并完成功能测试。
=== 实验验证 ===
下载完成后,分别按下开发板上的S1、S2、S3、S4按键,可以看到Xshell中Jlink虚拟的COM口分别打印如下:下载完成后,打开COM口,可以看到每隔500ms打印一次采集到的ADC数据。红色字体部分,是正常的室内光照强度时采集的电压;绿色字体部分,是打开手机手电筒照射光敏二极管时采集的电压。可以看到明显的电压差值。[[文件:NBDK-XSHELL-BTNADCLIGHT.png|边框|居中|无框|759x759像素]] 
=== 源码详解 ===
本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。
==== stm32l4xx_hal_conf.h ====
此文件位于“04此文件位于“05-按键中断实验光敏二极管实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件,一般情况下,我们默认需要使用的为前5个,包含芯片、flash、电源、时钟以及NVIC。Inc”路径中,主要用途是选择使能此例程使用到的库文件。 此例程我们主要给大家展示STM32L4的ADC功能,所以我们宏定义中打开ADC相关的。
此例程我们只要展示的是外部GPIO中断,所以我们额外使能 HAL_GPIO_MODULE_ENABLED。另外为了辅助展示按键信息,我们额外添加了串口相关的DMA、UART这两个宏定义。<syntaxhighlight lang="c" line="1" start="103">
#define HAL_GPIO_MODULE_ENABLED // GPIO
#define HAL_DMA_MODULE_ENABLED // DMA
#define HAL_UART_MODULE_ENABLED // UART
#define HAL_DMA_MODULE_ENABLED // DMA
#define HAL_ADC_MODULE_ENABLED // ADC
</syntaxhighlight>
接下来我们初始化了串口部分,目的是打印按键按下的调试信息。
接下来是初始化按键,并且注册了按键回调函数(回调函数负责的是不同层之间的数据传输)。接下来是ADC引脚初始化。
在最后的while()循环中,我们调用按键轮训函数,这样一旦有外部中断触发,我们首先会进行一下按键消抖,确认是否为误判。如果判断是正常触发,则认为是有按键按下,此时按键处理文件gyu_key.c中会将按键信息,通过上面说的回调函数,传到应用层(mian.c)中进行处理。循环中,我们调用ADC值采集函数,每个500ms采集一次,并且将采集值转换成电压值,格式化打印到串口显示。
<syntaxhighlight lang="c++" line="1" start="3633">
int main(void)
{
uint16_t ad_value = 0;
 
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
// 重置所有外设、flash界面以及系统时钟
SystemClock_Config();
// 初始化USART1初始化串口USART1 MX_USART1_UART_Init(); // 初始化按键引脚 MX_KEY_Init(); //注册按钮回调函数初始化ADC1 KEY_RegisterCbMX_ADC1_Init(AppKey_cb);
//
while (1)
{
KEY_Poll HAL_Delay(500); // 按键轮训,监测是否有按键被按下 }}</syntaxhighlight>在应用层的按键回调函数中,我们可以看到,当我们分别按下S1、S2、S3、S4按键后,STM32L4会通过串口向外部打印按键信息。<syntaxhighlight lang="c" line ad_value ="1" start="69">void AppKey_cbHAL_ADC_Read(uint8_t key){ ; // 如果有相应按键被按下,则串口打印调试信息 if(key & KEY_UP) {获取ADC采集值 printf("key_up pressAdc_value = %.2f\r\n",ad_value*3.3/4095); } 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");// 转换ADC采集为真实电压,并且打印到串口显示
}
}
</syntaxhighlight>
 
==== gyu_util.c ====
时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。
</syntaxhighlight>
==== gyu_keygyu_adc.c ====首先我们看一下按键的初始化函数,在按键初始化函数中我们配置按键引脚的状态,四个按键引脚都被配置为默认上拉,下降沿中断触发。并且开启EXTI0、EXTI1、EXTI2、EXTI3这四个外部中断线。ADC初始化函数,配置12位采样分辨率,配置2.5倍ADC时钟周期的采样频率。 选择ADC通道16(CH16)作为此次的ADC引脚(也就是PB1)。<syntaxhighlight lang="c++" line="1" start="7540">void MX_KEY_InitMX_ADC1_Init(void)
{
// 定义GPIO结构体定义常规ADC通道结构体 GPIO_InitTypeDef GPIO_InitStructADC_ChannelConfTypeDef sConfig;
// 使能GPIOC引脚时钟(按键引脚:PC0、PC1、PC2、PC3)初始化ADC __HAL_RCC_GPIOC_CLK_ENABLEhadc1.Instance = ADC1; // ADC寄存器地址,定义为ADC1 hadc1.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1; // 没有预分频器的ADC异步时钟 hadc1.Init.Resolution = ADC_RESOLUTION_12B; // 12位采样分辨率 // 初始化ADC,如果失败,则进入错误处理程序 if (HAL_ADC_Init(&hadc1) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); }
// 配置按键引脚配置ADC通道 GPIO_InitStructsConfig.Pin Channel = KEY_LEFT_Pin|KEY_DOWN_Pin|KEY_RIGHT_Pin|KEY_UP_PinADC_CHANNEL_16; // 选择PC0、PC1、PC2、PC3配置为ADC通道16 GPIO_InitStructsConfig.Mode Rank = GPIO_MODE_IT_FALLINGADC_REGULAR_RANK_1; // 下降沿中断触发指定ADC规格组中的排名 GPIO_InitStructsConfig.Pull SamplingTime = GPIO_PULLUPADC_SAMPLETIME_2CYCLES_5; // 上拉采样时间配置为2.5个ADC时钟周期 HAL_GPIO_Init(GPIOC, &GPIO_InitStruct)sConfig.SingleDiff = ADC_SINGLE_ENDED; // 初始化引脚ADC通道结束设置为单端 sConfig.OffsetNumber = ADC_OFFSET_NONE; // 禁用ADC偏移 sConfig.Offset = 0; // 定义从原始量中减去的偏移量 if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) // 配置中断优先级,并且使能中断配置ADC通道
{
// 配置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 _Error_Handler(KEY_RIGHT_EXTI_IRQn, 10__FILE__, 0); HAL_NVIC_EnableIRQ(KEY_RIGHT_EXTI_IRQn__LINE__); // 配置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 中断函数syntaxhighlight>重新定义ADC的硬件引脚,可以看到,配置PB1为ADC引脚。<syntaxhighlight lang="c++" line="1" start="77">void EXTI2_IRQHandlerHAL_ADC_MspInit(voidADC_HandleTypeDef* adcHandle)
{
HAL_GPIO_EXTI_IRQHandlerGPIO_InitTypeDef GPIO_InitStruct; if(adcHandle->Instance==ADC1) { // 使能GPIOB引脚时钟(选择的ADC引脚为PB1) __HAL_RCC_GPIOB_CLK_ENABLE(KEY_RIGHT_Pin);}
// EXTI line3 中断函数使能ADC时钟void EXTI3_IRQHandler __HAL_RCC_ADC_CLK_ENABLE(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) {初始化ADC引脚配置 key_check_pressGPIO_InitStruct.key_event Pin = KEY_UPGPIO_PIN_1; // 选择引脚编号1 key_check_pressGPIO_InitStruct.start_tick Mode = HAL_GetTick()GPIO_MODE_ANALOG_ADC_CONTROL; } // 如果是LEFT键被触发,记录按键任务为KEY_LEFT,并记录当前时钟 if(GPIO_Pin == KEY_LEFT_Pin) {配置为ADC引脚 key_check_pressGPIO_InitStruct.key_event Pull = KEY_LEFTGPIO_NOPULL; 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_GetTickHAL_GPIO_Init(GPIOB, &GPIO_InitStruct); } // 如果是RIGHT键被触发,记录按键任务为KEY_RIGHT,并记录当前时钟 if(GPIO_Pin == KEY_RIGHT_Pin) { key_check_press.key_event = KEY_RIGHT; key_check_press.start_tick = HAL_GetTick();初始化PB1引脚
}
}
</syntaxhighlight>按键轮训函数,其实就是用来处理消抖的函数,我们根据从外部中断回调函数中的获取的时间(也就是中断触发的时间,上一个函数中记录的),对比现在实时的时间,判断是否超过20ms,如果超过20ms,则认为按键被按下。我们记录下按键信息,并且执行向应用层回调的函数。用于获取ADC采集值的函数,经过一系列的API函数调用,我们获取到最终的adcValue。<syntaxhighlight lang="c++" line="1" start="171104">void KEY_Polluint16_t HAL_ADC_Read(void)
{
uint8_t key_event uint16_t ad_value = 0; // 启动ADC转换 HAL_ADC_Start(&hadc1); // 等待ADC转换完成 HAL_ADC_PollForConversion(&hadc1, 50);
// 如果有按键任务检查是否已经完成转换 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按键被按下 ifHAL_IS_BIT_SET(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 ifHAL_ADC_GetState(key_check_press.key_event & KEY_RIGHThadc1) { // 获取当前KEY_RIGHT引脚电平,如果是低电平,则认为RIGHT按键被按下 if(HAL_GPIO_ReadPin(KEY_RIGHT_GPIO_Port,KEY_RIGHT_Pin) == GPIO_PIN_RESETHAL_ADC_STATE_REG_EOC) { key_event |= KEY_RIGHT; // 记录app按键任务 } key_check_press.key_event ^= KEY_RIGHT; // 删除按键中断任务 } } } //如果有记录给app的按键任务,代表真的有按钮按下,则执行回调函数 if(key_event && pFkey_cb)
{
pFkey_cb(key_event);// 获取采集到的ADC值 }}</syntaxhighlight>留给应用层调用注册按键回调的函数,用于将轮询后确认的按键信息,传递给应用层使用。<syntaxhighlight lang="c++" line="1" start ad_value ="155">void KEY_RegisterCb(key_cb cb){ ifHAL_ADC_GetValue(cb != 0&hadc1) { pFkey_cb = cb;
}
return ad_value; // 返回ADC采集值
}
</syntaxhighlight>
510
个编辑