1.LED硬件结构
IMX6ULL-MINI电路板的硬件结构如下:
在 Linux 中,GPIO 的标识和控制通常是通过引脚号来进行的,引脚号是用于唯一标识特定的 GPIO 引脚。
IMX6ULL引脚号获取
在开发板中输入下面指令获取引脚映射
cat /sys/kernel/debug/gpio
i.max6ull开发板的GPIO5就是 gpiochip4(不同厂家开发板可能不同),gpiochip4起始引脚号为128,因此GPIO5_3的引脚号就是128+3 = 131;
2.GPIO子系统函数介绍
Linux的GPIO子系统中可以通过如下函数配置GPIO。
int gpio_request(unsigned gpio, const char *label);
void gpio_free(unsigned gpio);
int gpio_direction_input(unsigned gpio);
int gpio_direction_output(unsigned gpio, int value);
int gpio_get_value(unsigned gpio);
void gpio_set_value(unsigned gpio, int value);
函数具体解释如下:
1)gpio_request(unsigned gpio, const char *label)
作用: 向Linux 内核中用于请求申请一个 GPIO 引脚的函数。如果我们想对一个引脚进行操作,需要最先调用 gpio_request()这个函数。
gpio : 要请求的 GPIO 引脚号。这个引脚号可以自己直接给出(比如上面花了那么多篇幅讲解的)。还可以通过 of_get_named_gpio 函数从设备树获取指定 GPIO 属性信息(设备树的内容,后面会讲解,这里留个影响即可)。
label : 给GPIO起一个名字,因为直接一个引脚号不方便人阅读,所以可以给这个引脚号起一个名字。随便起名字,只要你自己喜欢,不影响。
返回值 : 如何返回0,表示申请GPIO成功。如果返回负数,表示申请GPIO出现错误
2)gpio_free(unsigned gpio)
作用 : 如果不使用某个 GPIO 了, 那么就需要调用 gpio_free 函数进行释放。
gpio : 要释放的GPIO引脚号。与gpio_request的GPIO引脚号是同一个东西。
返回参数 : 无
3)gpio_direction_input(unsigned gpio)
作用 : 将GPIO配置为输入方向。申请完GPIO之后,需要根据需求配置为输入或者输出,这个函数可以将GPIO设置为输入
gpio : 要设置为输入的GPIO 引脚号
返回参数 : 返回 0,表示成功将 GPIO 引脚设置为输入模式。返回负数,表示出错或无法设置 GPIO 引脚。
4)gpio_direction_output(unsigned gpio, int value)
作用 : 将GPIO配置为输出方向,并且设置默认输出值。申请完GPIO之后,需要根据需求配置为输入或者输出,这个函数可以将GPIO设置为输出
gpio : 设置为输出的GPIO 引脚号
value : GPIO 默认输出值。如果GPIO初始化成功之后,默认输出的电压。
返回参数 : 返回 0,表示成功将 GPIO 引脚设置为输出模式。返回负数,表示出错或无法设置 GPIO 引脚。
5)gpio_get_value(unsigned gpio)
作用 : 获取指定GPIO的电平信息
gpio : 要获取电平值的GPIO标号
返回参数 : 获取电平信息成功,高电平返回1,低电平返回0。GPIO电平获取失败返回负值。
6)gpio_set_value(unsigned gpio, int value)
作用 : 设置指定GPIO的电平值
gpio : 要设置指定GPIO的电平值
value : 要设置的电平值,如果传入0,则表示将GPIO设置为低电平。传入一个非0值,表示将GPIO设置为高电平
返回参数 : 无
3.应用层代码
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <signal.h>
/* 可执行文件名 | 表示要操作哪一盏灯(<>表示参数不可省略) | 灯状态
* ./led_test | <0|1|2|..> | on |硬件上开灯
* ./led_test | <0|1|2|..> | off |硬件上关灯
* ./led_test | <0|1|2|..> | |读取led状态
*/
static int fd;
int main(int argc, char **argv)
{
int ret;
char buf[2];
int i;
/* 1. 判断参数 */
if (argc < 2)
{
printf("Usage: %s <0|1|2|...> [on | off]\n", argv[0]);
return -1;
}
/* 2. 打开文件 */
/*打开文件,因为在驱动层中,device_create()函数创建的设备节点名字叫做100ask_led,而设备节点都存放在/dev目录下,所以这里是/dev/100ask_led*/
fd = open("/dev/100ask_led", O_RDWR);
if (fd == -1)
{
printf("can not open file /dev/100ask_led\n");
return -1;
}
if (argc == 3)//传入了三个参数,表示写入
{
/* write */
buf[0] = strtol(argv[1], NULL, 0);
/*strtol()函数是将字符转换为数字。我们在命令行中输入的1,其实是字符1,而不是数字1*/
if (strcmp(argv[2], "on") == 0)
/*strcmp()函数,用于判断两个字符串是否相等,如果相等返回0,如果不相等返回一个非0值 */
buf[1] = 0;
else
buf[1] = 1;
ret = write(fd, buf, 2);
}
else //表示读取电平信息
{
buf[0] = strtol(argv[1], NULL, 0);
ret = read(fd, buf, 2);
if (ret == 2)
{
printf("led %d status is %s\n", buf[0], buf[1] == 0 ? "on" : "off");
}
}
close(fd);
return 0;
}
4.驱动层代码
按照上一篇来讲的
驱动程序编写流程
1.确定主设备号,也可以让内核分配(用于让系统根据设备号判断是哪一个驱动)。
2.编写file_operations结构体,用于管理驱动程序。
3.首先file_operations中对应的drv_open/drv_read/drv_write 等函数,在应用层调用open,write,read等函数时,就是对应调用file_operations中对应函数。
4.把 file_operations结构体告诉内核: register_chrdev(注册)。谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数。
5.有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用 unregister_chrdev。
6.其他完善:提供设备信息,自动创建设备节点: class_create, device_create。
#include "asm-generic/errno-base.h"
#include "asm-generic/gpio.h"
#include "asm/uaccess.h"
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/timer.h>
//描述引脚
struct gpio_desc{
int gpio;
int irq; //中断,没有使用
char *name;
int key; //按键,没有使用
struct timer_list key_timer;//定时器 ,没有使用
} ;
//存放引脚编号。名字
static struct gpio_desc gpios[2] = {
{131, 0, "led0", },
//{132, 0, "led1", },
};
/* 主设备号 */
static int major = 0;
static struct class *gpio_class;//一个类,用于创建设备节点
/* 实现对应的open/read/write等函数,填入file_operations结构体 */
static ssize_t gpio_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
char tmp_buf[2];//存放驱动层和应用层交互的信息
int err;
int count = sizeof(gpios)/sizeof(gpios[0]);//记录定义的最大引脚数量
// 如果传入的值不是两个返回错误
if (size != 2)
return -EINVAL;
//驱动层从用户层得到数据
//tmp_buf : 驱动层数据
//buf : 应用层数据
// 1:数据长度为1个字节,因为控制LED,一个字节够用
err = copy_from_user(tmp_buf, buf, 1);
// 第0项表示要操作哪一个LED,如果操作的LED超出count,表示失败
if (tmp_buf[0] >= count)
return -EINVAL;
//读取引脚电平
tmp_buf[1] = gpio_get_value(gpios[tmp_buf[0]].gpio);
// 驱动层发送数据到应用层
// buf:应用层数据
// tmp_buf:驱动层数据
err = copy_to_user(buf, tmp_buf, 2);
return 2;
}
static ssize_t gpio_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
unsigned char ker_buf[2];
int err;
if (size != 2)
return -EINVAL;
err = copy_from_user(ker_buf, buf, size);
if (ker_buf[0] >= sizeof(gpios)/sizeof(gpios[0]))
return -EINVAL;
//设置gpios[ker_buf[0]].gpio引脚电平
gpio_set_value(gpios[ker_buf[0]].gpio, ker_buf[1]);
return 2;
}
/* 定义自己的file_operations结构体 */
static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_drv_read,
.write = gpio_drv_write,
};
/* 在入口函数 */
static int __init gpio_drv_init(void)
{
int err;
int i;
int count = sizeof(gpios)/sizeof(gpios[0]);
/*__FILE__ :表示文件
*__FUNCTION__ :当前函数名
*__LINE__ :在文件的哪一行
*/
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
for (i = 0; i < count; i++)
{
//申请指定GPIO引脚,申请的时候需要用到名字
err = gpio_request(gpios[i].gpio, gpios[i].name);
//如果返回值小于0,表示申请失败
if (err < 0) {
printk("can not request gpio %s %d\n", gpios[i].name, gpios[i].gpio);
return -ENODEV;
}
/* 设置为输出引脚 */
gpio_direction_output(gpios[i].gpio, 1);
}
/* 注册file_operations */
major = register_chrdev(0, "100ask_led", &gpio_key_drv); /* /dev/gpio_desc */
/*创建类,为THIS_MODULE模块创建一个类*/
gpio_class = class_create(THIS_MODULE, "100ask_led_class");
//如果返回错误,注销驱动程序
if (IS_ERR(gpio_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "100ask_led_class");
return PTR_ERR(gpio_class);
}
//类创建成功后,创建设备
/*输入参数是逻辑设备的设备名,即在目录/dev目录下创建的设备名
*参数一 : 在gpio_class类下面创建设备
*参数二 : 无父设备的指针
*参数三 : 主设备号+次设备号
*参数四 : 没有私有数据
*/
device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "100ask_led");
/* /dev/100ask_gpio */
return err;
}
/* 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
*/
static void __exit gpio_drv_exit(void)
{
int i;
int count = sizeof(gpios)/sizeof(gpios[0]);
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
//销毁gpio_class类下面的设备节点
device_destroy(gpio_class, MKDEV(major, 0));
//销毁gpio_class类
class_destroy(gpio_class);
//注销驱动
unregister_chrdev(major, "100ask_led");
for (i = 0; i < count; i++)
{
//释放gpio
gpio_free(gpios[i].gpio);
}
}
/* 7. 其他完善:提供设备信息,自动创建设备节点 */
module_init(gpio_drv_init); //入口函数
module_exit(gpio_drv_exit); //出口函数
MODULE_LICENSE("GPL");
5.Makefile文件
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册
KERN_DIR = /home/xxl/100ask_imx6ull_mini-sdk/Linux-4.9.88 # 板子所用内核源码的目录
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o led_test led_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order led_test
# 参考内核源码drivers/char/ipmi/Makefile
# 要想把a.c, b.c编译成ab.ko, 可以这样指定:
# ab-y := a.o b.o
# obj-m += ab.o
obj-m += led_drv.o
之后Make加载驱动->nfs挂载->insmod .ko文件->./led_test控制即可