在学习完通用驱动框架1之后,下面就是使用通用驱动框架去写各种硬件的驱动程序了。
小插曲:就是本来已经买了通用模块,不过收到货发现多发了个stm32mp157的开发板,我就联系客服退回开发板,然后准备使用转接板的时候又发现转接板发成了6ull的,最终结果就是估计还有两天我才能完成后面的实验。
1. LED驱动程序的简单分析
首先,分析一下我们假设想使用GPIO控制LED灯,需要对GPIO进行哪些相关设置。GPIO和LED的硬件连接关系如下图所示:
- 驱动程序的入口函数中:请求GPIO引脚,并将GPIO设置为输出引脚,默认为高电平(灯灭);
- 在应用层调用
write
函数,使驱动程序中的write
函数被调用。驱动程序中,根据用户传递数据,决定并控制GPIO,最终控制LED的开和关; - 在应用层调用
read
函数,使驱动程序的read
函数被调用。根据用户传递的数据,读取GPIO的引脚电平,最终获得LED的状态是开还是关。
2. LED驱动代码的实现
2.1 入口函数 & 出口函数
入口函数中 ,主要完成初始化GPIO设置,将GPIO设置为输出引脚,采用的是GPIO子系统提供的函数接口。
static int __init gpio_drv_init(void)
{
int err;
int i;
int count = sizeof(gpios)/sizeof(gpios[0]);
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
/* set pin as output*/
for (i = 0; i < count; i++)
{
err = gpio_request(gpios[i].gpio, gpios[i].name);
if (err) {
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);
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");
return PTR_ERR(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__);
device_destroy(gpio_class, MKDEV(major, 0));
class_destroy(gpio_class);
unregister_chrdev(major, "100ask_led");
for (i = 0; i < count; i++)
{
gpio_free(gpios[i].gpio);
}
}
2.2 驱动程序之read()函数实现
假设用户通过buf
传递数据,那么 buf[0]
用来表示需要读哪个LED灯,比如传递0
,就代表led0
static ssize_t gpio_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
char temp_buf[2];
int err;
int count = sizeof(gpios) / sizeof(gpios[0]);
if (size != 2)
return -EINVAL;
/*接收app传递的数据,读哪个引脚*/
err = copy_from_user(temp_buf, buf, 1);
/*
* temp_buf[0] = 0 ---- led0
* temp_buf[0] = 1 ---- led1
*/
if (temp_buf[0] >= count)
return -EINVAL;
/* 获得引脚状态 */
temp_buf[1] = gpio_get_value(gpios[temp_buf[0]].gpio);
/* 将temp_buf中的数据传递给app */
err = copy_to_user(buf, temp_buf, 2);
return 2;
}
2.3 驱动程序之write()函数实现
假设用户通过buf
传递数据,其中:
buf[0]
用来表示控制哪个LED灯,比如传递0
,就代表led0
;buf[1]
就用来表示LED的状态,比如传递0
,就表示灯开,传递1
就表示灯灭。(这是根据原理图来的,低电平LED就打开,高电平LED就关闭)
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;
/* 接收app传递的数据,控制哪个LED的开关状态 */
err = copy_from_user(ker_buf, buf, size);
/* 判断LED编号是否超出控制范围 */
if (ker_buf[0] >= sizeof(gpios)/sizeof(gpios[0]))
return -EINVAL;
/* 设置GPIO值 */
gpio_set_value(gpios[ker_buf[0]].gpio, ker_buf[1]);
return 2;
}
3. 上机测试
3.1 确定引脚,计算引脚编号
原理图分析结果:
- LED2 的引脚为 PA10,引脚编号为:0+10=10;
- LED3 的引脚为 PG8, 引脚编号为:96+8=104;
- LED 外接 VDD_3V3 电压,当引脚输出低电平灯亮,当引脚输出高电平灯灭;
【公式计算】
细心的同学可能发现了在软件层面上,GPIO 组和 GPIO 编号存在一定的规律。比如 GPIOA 对应 0~15 ,GPIOB 对应 16 ~ 31…并以此类推。详细地, PA1 的编号就等于 1,PB1 的编号 16+1=17;
最后,可以总结为一个公式:GPIO 编号 = (X -1) * <32/16> + N
X
可以解释为第几组,比如GPIOA
可以解释为第一组,那么GPIOB
就是第二组;<32/16>
可以解释为有的一组有32
位,而有的就只有16
位;N
表示一组里的第(N+1)
个引脚,比如PA1
就是第二个引脚,所以N
就是1
;
那么,形如 LED3
的引脚 PG8
,就可以通过下列公式获得:(7-1)*16+8=6*16+8=104
3.2 测试程序编写
#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>
#include <stdlib.h>
static int fd;
/*
* ./led_test <0|1|2> on
* ./led_test <0|1|2> off
* ./led_test <0|1|2>
*/
int main(int argc, char **argv)
{
int ret;
char buf[2];
/* 1. 判断参数 */
if (argc < 2)
{
printf("Usage: %s <0|1|2|...> [on|off]\n", argv[0]);
return -1;
}
/* 2. 打开文件 */
fd = open("/dev/100ask_led", O_RDWR);
if (fd == -1)
{
printf("can not open file /dev/100ask_led\n");
return -1;
}
/* 3.根据参数设置led */
if(argc == 3)
{
/* write */
buf[0] = strtol(argv[1], NULL, 0);
if(strcmp(argv[2], "on") == 0)
{
buf[1] = 0;
}
else
{
buf[1] = 1;
}
ret = write(fd, buf, 2);
/* 设置成功 */
if(ret == 2)
{
printf("led %d status is %s\n", buf[0], buf[1] == 0 ? "on" : "off");
}
}
else
{
/* read */
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;
}
3.3 插入LED驱动模块并测试
4. 小结
- 首先,编写驱动程序之前就要确定好驱动程序的编写逻辑,我们要在驱动程序中做什么;
- 对于,类似GPIO子系统这种函数接口,内核代码中已经有很多现成的例子,要学会“抄”代码;
- 如果GPIO被占用了,一定要去查找原因,可能是内核其他驱动程序占用了。我们可以通过修改设备树等方法,解除占用。
附:完成LED驱动完成代码
#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;
char *name;
};
static struct gpio_desc gpios[2] = {
{10, "100ask_led0"},
{104, "100ask_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 temp_buf[2];
int err;
int count = sizeof(gpios) / sizeof(gpios[0]);
if (size != 2)
return -EINVAL;
/*接收app传递的数据,读哪个引脚*/
err = copy_from_user(temp_buf, buf, 1);
/*
* temp_buf[0] = 0 ---- led0
* temp_buf[1] = 1 ---- led1
*/
if (temp_buf[0] >= count) {
return -EINVAL;
}
temp_buf[1] = gpio_get_value(gpios[temp_buf[0]].gpio);
err = copy_to_user(buf, temp_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;
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]);
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
for (i = 0; i < count; i++)
{
/* set pin as output*/
err = gpio_request(gpios[i].gpio, gpios[i].name);
if (err) {
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);
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");
return PTR_ERR(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__);
device_destroy(gpio_class, MKDEV(major, 0));
class_destroy(gpio_class);
unregister_chrdev(major, "100ask_led");
for (i = 0; i < count; i++)
{
gpio_free(gpios[i].gpio);
}
}
module_init(gpio_drv_init);
module_exit(gpio_drv_exit);
MODULE_LICENSE("GPL");
以上是本文全部内容,如有问题欢迎评论区留言!