前言:
本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM(MX6U)裸机篇”视频的学习笔记,在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。
引用:
正点原子IMX6U仓库 (GuangzhouXingyi) - Gitee.com
《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.2.pdf》
正点原子资料下载中心 — 正点原子资料下载中心 1.0.0 文档
正文:
本文是 “正点原子[第二期]Linux之ARM(MX6U)裸机篇--第13.1, 13.2, 13.3 讲” 的读书笔记。第13.1, 13.2, 13.3 讲介绍如何使用通过GPIO 输入模式(input)来获取按键的输入。本节的示例程序是一个最简单的例子,它使用轮询的方法,在循环中每隔 10ms 检查一次按键输入引脚是低电平还是高电平来判断按键是否按下。
使用轮询的方法来检测按键的输入时,处理器将会一直忙运行造成处理器资源的浪费,但是作为本节入门实验的最简单例子来说,学习如何检测按键是否被按下已经足够了,后续的课程中将会学习如何改进按键检测的机制。
1. 查看电路原理图中按键使用的GPIO引脚
参考正点原子I.MX6ULL Mini 核心开发板的电路原理图,找到按键 'KEY0' ,并找到按键 KEY0 接在了I.MX6ULL 处理器的 'UART1_CTS' IO 引脚。
I.MX6ULL 处理器的 'UART1_CTS' 引脚作为按键的输入引脚,需要将UART1_CTS IO接口复用为 'gpio’ 功能并作为 'gpio input' 接口。和之前几节‘LED灯驱动程序’中将I.MX6ULL处理器引脚作为 gpio output 模式使用类似,将 io 引脚作为 gpio output 模式使用需要如下几步:
- 设置 MUX_CTL_UART1_CTS_B 寄存器,复用为 GPIO 模式,GPIO1_IO18。
- 设置 MUX_CTL_UART1_CTS_B 寄存器,配置io接口电气特性(速率,上拉电阻,压摆率,等)
- 设置 GPIO1 寄存器组 DR,GDIR 寄存器配置,GPIO1_IO18 位 gpio input 模式
2. 编写 bsp_key 源码实现按键引脚 gpio input 高/低电平的读取
从电路原理图中可以看到按键 KEY0 接到I.MX6ULL处理器 gpio1_io18 引脚,gpio1_io18有一个 10K 的上拉电阻,默认情况下按键打开 gpio 引脚读取到高电平,当按下按键后 gpio 引脚读取到低电平。通过读取 gpio1_io18 的电平输入,当读取到低电平时可以判断出按键被按下。
2.1 按键消抖
理想型按键电压变换过程如图 15.3.1 所示:
在15.3.1中,按键没有按下的时候按键值为1,当按键在 t1 时刻按下以后按键值就变为0,这是最理性的状态。但是实际上按键是机械结构,加上刚按下去的一瞬间人手可能也有抖动,实际电压变换过程如图 15.3.2 所示
在图15.3.2 中,t1 时刻按键被按下,但是由于抖动原因,知道 t2 时刻才稳定下来,t1 到 t2 这段时间就是抖动。一般这段时间就是十几 ms 左右,从图 15.3.2 可以看出在抖动期间会有多次触发,如果不消除这段抖动的话软件就会误判,本来按键就按下了一次,结果软件读取IO值发现电平多次跳变以为按下多次。所以我们需要跳过这段抖动时间再去读取按键的 io 值,也就是至少要在 t2 时刻以后再去读取IO值。在示例源码中,就是延时了大约10ms 后再去读取 gpio1_io18的值,如果此时按键的值依然是0,那么就表示这是一次有效的触发。
2.2 bsp/bsp_key.c 源码
根据上面按键KEY0使用的的分析,已经知道本次按键实验使用 KEY0 GPIO1_IO18 引脚作为 input 输入,当读取到gpio1_io18 引脚低电平时按键被按下,读取到高电平时按键松开,因为物理按键不是理想型的按键,在按键按下后的十几 ms 内会有多次的电平高低跳变如果不对按键读取掉的电平进行软件消抖可能会把一次按键按下错误的判断为多次按键输入,本节实验使用时延函数 delay 10ms 后再次读取一次 gpio 引脚输入电平来实现按键的软件消抖。
参考正点原子视频教程和文档,bsp_key.c 源码如下:
#include "bsp_delay.h"
#include "bsp_key.h"
#include "bsp_gpio.h"
/*
* @description : 按键初始化。
* @param – base : 无
* @return : 无
*/
void key_init(void)
{
gpio_pin_config_t config;
/* 1. 初始化IO复用,复用为GPIO1_IO18 */
IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0);
/*
*
* bit[0] 0 SRE,低偏摆率
* bit[2:1] 00 Reserved(未使用)
* bit[5:3] 000 DSE(当gpio位output时,驱动能力),本节gpio为input模式所以选择DSE=0关闭output
* bit[7:6] 10 SPEED,速率,选择100MHz
* bit[10:8] 000 Reserved(未使用)
* bit[11] 0 ODE,开路输出,本节gpio为input,开路输出关闭
* bit[12] 1 PKE, Pull/Keeper (上拉/保持器 开关),这里使能
* bit[13] 1 PUE, 选择是Keeper还是PULL,本节这里选择 1 (PULL)
* bit[15:14] 11 PUS, 上拉电阻阻值,本节选择22K欧姆上拉电阻
* bit[16] 0 HYS, 磁滞,本节不使用,选择0
* bit[31-17] 0 Reserved(未使用)
*
* 最终选择的电气特性寄存器值:
* 1111 0000 1000 0000 = 0xf080
*/
/* 2. 设置 UART1_CTS_B IO 的电气特性 */
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xf080);
/* 3. 初始化 GPIO1_IO18 设置为输入 */
config.directioin = kGPIO_DigitalInput;
gpio_init(GPIO1, 18, &config);
}
int key_read(void)
{
return gpio_pinread(GPIO1, 18);
}
/*
* @description : 获取按键值。
* @param – base : 无
* @return : 0 没有按键按下,其它值:对应的按键值。
*/
int key_getvalue(void)
{
int ret = 0;
static int release = 1;
if((release == 1) && (gpio_pinread(GPIO1, 18) == 0)){ /* KEY0 按下 */
release = 0; /* 标记按键按下 */
delay(10); /* 时延消抖 */
if(key_read() == 0){ /* 按键按下 */
ret = KEY0_VALUE;
}
}
else if((gpio_pinread(GPIO1, 18) == 1)){ /* KEY0 释放 */
release = 1; /* 标记按键释放 */
ret = 0;
}
return ret;
}
在复用 UART1_CTS_B IO 为 GPIO1_IO18,并且设置 UART1_CTS_B IO接口的电气特性时,这里设置的值为 ‘0xF080’
/* 2. 设置 UART1_CTS_B IO 的电气特性 */
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xf080);
‘0xF080’ 这个值是怎么确定的呢?和之前分析“LED灯驱动程序GPIO引脚 output 电气特性”寄存器值的方式一样,需要参考《I.MX6ULL参考手册》第32章中 UART1_CTS_B 寄存器中每一个bit的定义,根据 UART1_CTS_B 工作在 input 模式,选择低速率,上拉电阻阻值的选择等,确定每一个bit的值,最终确定此处应该选择的io接口电气特性寄存器值为‘0xF080’。
3.3 bsp/bsp_gpio.c 接口函数
在这些LED灯驱动程序,Beep蜂鸣器启动程序,和按键驱动程序中,对GPIOx->DR, GPIOx->GDIR 寄存器组的操作是相似的,本节实验中将会把对 gpio 操作的api接口函数抽象出来放到 bsp/bsp_gpio.c 中,实现代码的复用和封装,也方便后续的开发使用。这也是我们自己写的 BSP 接口函数。
bsp_gpio.h
#ifndef __BSP_GPIO_H__
#define __BSP_GPIO_H__
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "cc.h"
typedef enum _gpio_pin_direction
{
kGPIO_DigitalOutput = 0U, /*输出*/
kGPIO_DigitalInput = 1U, /*输入*/
} gpio_pin_direction_t;
typedef struct _gpio_pin_config
{
gpio_pin_direction_t directioin; /* GPIO 方向:输入还是输出 */
int outputLogic; /* 如果是输出的话,默认输出电平 */
} gpio_pin_config_t;
/* 初始化函数 */
void gpio_init(GPIO_Type *base, int pin, gpio_pin_config_t *config);
void gpio_pinwrite(GPIO_Type *base, int pin, int value);
int gpio_pinread(GPIO_Type *base, int pin);
#endif
bsp_gpio.c
#include "bsp_gpio.h"
/*
* @description : GPIO初始化。
* @param - base : 要初始化的寄存器组。
* @param - pin : 要初始化的寄存器脚号。
* @param - config : GPIO 配置结构体。
* @return : 无
*/
void gpio_init(GPIO_Type *base, int pin, gpio_pin_config_t *config)
{
if(config)
{
if(config->directioin == kGPIO_DigitalOutput){
base->GDIR |= (1<<pin); /* 输出 */
gpio_pinwrite(base, pin, config->outputLogic); /* 默认输出电平 */
}
else if(config->directioin == kGPIO_DigitalInput){
base->GDIR &= ~(1<<pin); /* 输入 */
}
}
}
/*
* @description : 指定 GPIO 输出高或者低电平。
* @param – base : 要输出的 GPIO 组。
* @param – pin : 要输出的 GPIO 脚号。
* @param - value : 要输出的电平, 1 输出高电平, 0 输出低低电平
* @return : 无
*/
void gpio_pinwrite(GPIO_Type *base, int pin, int value)
{
if(value == 0)
base->DR &= ~(1<<pin);
else
base->DR |= (1<<pin);
}
/*
* @description : 读取指定 GPIO 的电平值。
* @param – base : 要读取的 GPIO 组。
* @param – pin : 要读取的 GPIO 脚号。
* @return : 1 读取高电平, 0 读取低低电平。
*/
int gpio_pinread(GPIO_Type *base, int pin)
{
return ((base->DR >> pin) & 0x1);
}
3. 编译按键驱动实验程序
正点原子第13.1,13.2,13.3 视频教程里,正点哥在做实验时遇到了一个有趣的错误,在第13讲的视频教程里,正点哥发现当在 imx6u.lds 链接脚本里带上 '.bss' 分区的时,编译出来的 .bin 镜像烧录到SD卡上LED灯和蜂鸣器不能正常工作,去掉链接脚本里的 '.bss' 分区时编译出来的 .bin 镜像烧录SD卡,LED灯和蜂鸣器工作正常。在视频教程里,正点原子哥发现是链接脚本里的 .bss 段没有按照4字节对齐的原因,在视频教程里,正点原子哥本地变异的 .elf 文件的反汇编里 __bss_start 和 __bss_end 的确没有按照4字节对齐。因为 I.MX6ULL 是 ARM Contre-A7 32位的处理器,32位处理器读写内存时地址需要按照4字节对齐,如果内存起始地址不是4字节对齐的可能就会造成内存中内容读写错误的问题。
这个问题不一定会发生。这个其实和编译出来的 .elf 文件中的 .data 数据段的长度有关系,因为在链接脚本中 .bss 段时紧挨着 .data 数据段的,.data 数据段的起始地址是4字节对齐的,如果 .data数据段的长度本身是一个奇数(不能被4整除),那么 .bss_start = .data_start + .data_len 计算得到的 .bss 的起始地址就是一个非4字节对齐的地址,这样就会遇到正点原子哥视频里的问题。
在正点哥的的视频例程里,正点哥修改了 imx6u.lds 链接脚本,在定义 __bss_start 之前让 “. 当前定位符”按照4字节对齐,这样就解决了问题。
4. 烧录SD卡验证按键驱动功能
烧录SD卡验证按键驱动功能,使用正点原子提供的 'imxdownload' 烧录SD卡,然后把SD卡查到正点原子 I.MX6U APLHA/Mini 开发板上,上电验证LED灯是否闪烁,按下开发板上的按键蜂鸣器是否鸣叫,再次按下按键蜂鸣器是否停止鸣叫。
我在本地实验时,遇到了好几个问题,不过参考正点原子的按键示例源码反复修正了4次代码最终实现了按键开关蜂鸣器和LED灯闪烁的功能。
5. 总结
按键实验中遇到的问题记录和分析:
问题1: 按下按键之后不松开,蜂鸣器快速的10ms进行发出一次鸣叫。
原因: 在我最开始写的 key_getvalue() 函数中错误的将 'static uint8_t released' 的标志置零,正确的做法应该是在检测到按键gpio input 引脚的电平为高电平时才将 'static uint8_t released' 的标志置零。
问题2: 参考正点原子哥的Makefile,开启从编译 .o 文件的' -O2 ' 优化后,短时延函数失效。
原因: 短时延函数里的 'delay_short()' 空循环函数被编译器优化掉,造成通过空循环忙等的短时延函数失效。
解决方法: 在正点原子的示例源码中,delay_short() 函数的参数被声明为了 'volatile int ' 类型,把参数变量为 volatile 就可以避免掉编译器的优化,让编译器生成的汇编指令老老实实的按照我们的源程序执行空循环。
把变量声明为 "Volatile" 指示编译器不要进行优化。
按照示例源码的将short_dealy()的形参声明为 volatile ,然后重新编译烧录SD卡验证下是否解决问题。从反汇编源码来看,使用 "volatile" 关键字之后已经让编译器不再优化掉 short_delay() 的源码。