记录学习Linux驱动开发中遇到的一系列问题
一.Linux常用指令
记录一些在学习时常用的指令
一些特别常用的就不记录啦
%s/"cc"/"arm-buildroot-linux-gnueabihf-gcc"/g
,在控制台打开complie文件,替换所有,注意格式,就可以用vs看linux内核源码了。
二.GPIO子系统
一般使用help查看
1.引脚编号
- 使用
cat /sys/kernel/debug/gpio
查看gpio模块 - 使用
cd /sys/class/gpio/
和ls
可以看到每一组模块的起始地址 - 原理图的gpio4对应gpioship3
2.常用函数
- gpio_requst
- gpio_deriction_input
- gpio_direction_output
- gpio_get_value
- gpio_set_value
- gpio_free
三.中断函数
建议看韦东山老师的文档,讲解很详细。
四.使用模板操作硬件
1.LED
①代码
drv.c
#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;
} ;
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;
err = copy_from_user(tmp_buf, buf, 1);
if (tmp_buf[0] >= count)
return -EINVAL;//是一个负值,表示无效参数
tmp_buf[1] = gpio_get_value(gpios[tmp_buf[0]].gpio);
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;//因为在测试中要读取的是2个,如果不是两个就报错
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 < 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);/*major记得先全局定义,
为0时表示内核自动分配主设备号"100ask_led"是设备驱动的名称,用于在系统中标识该驱动;&gpio_key_drv是指向struct file_operations结构体的指针,它定义了字符设备驱动的
操作函数,例如打开、关闭、读取和写入等。*/
gpio_class = class_create(THIS_MODULE, "100ask_led_class");/*创建一个类
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);
}/*IS_ERR(gpio_class) 是一个宏,用于检查 gpio_class 的返回值是否为错误指针。
如果 gpio_class 是错误指针,表示创建类设备失败。
printk 函数用于在内核日志中打印一条错误消息,显示出错的代码文件、函数和行号。
unregister_chrdev 函数用于取消注册之前注册的字符设备驱动。
PTR_ERR(gpio_class) 是一个宏,用于获取错误指针的错误码。
return PTR_ERR(gpio_class) 将错误码返回给调用函数,以便进一步处理或报告错误。
这段代码的作用是在创建类设备失败时,打印错误信息,并进行必要的清理操作,
包括取消注册之前注册的字符设备驱动,并返回适当的错误码。这样可以在遇到错误时及时
处理异常情况,以确保代码的正确执行。*/
device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "100ask_led"); /*创建
一个具体的设备实例,并将其与之前创建的类设备关联起来。
gpio_class 是之前使用 class_create创建的类设备的指针,表示新创建的设备实例将属于该类设备。
第一个NULL表示新创建的设备实例没有父设备。
MKDEV(major, 0) 用于创建设备实例的设备号。major 是之前注册的字符设备驱动的主设备号,
而0表示次设备号,用于标识具体的设备实例。
第二个NULL表示没有提供驱动特定的数据。
"100ask_led" 是设备实例的名称,用于在系统中标识该设备实例。*/
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);
}/*用于释放GPIO 引脚资源。gpios[i].gpio 表示第 i 个 GPIO 引脚的标识符。通过
调用gpio_free 函数,可以释放该引脚所占用的资源,包括引脚的注册、配置和占用等。*/
}
/* 7. 其他完善:提供设备信息,自动创建设备节点*/
module_init(gpio_drv_init);//宏,用于指定模块初始化函数
module_exit(gpio_drv_exit);//宏,用于指定模块退出函数
MODULE_LICENSE("GPL");
test.c
#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>
static int fd;
//int led_on(int which);
//int led_off(int which);
//int led_status(int which);
/*
* ./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];
int i;
/* 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;
}
if (argc == 3)
{
/* write */
buf[0] = strtol(argv[1], NULL, 0);//strtol 函数用于将字符串转换为长整型数值。
if (strcmp(argv[2], "on") == 0) //这里的0是亮,是结合电路图得出的,实际应用时应根据电路图决定
buf[1] = 0;
else
buf[1] = 1;
ret = write(fd, buf, 2);//write指向file_operations里的,再指向定义的函数
}
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;
}
makefile
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88 # 板子所用内核源码的目录
all:
make -C $(KERN_DIR) M=`pwd` modules //进入内核,pwd表示在当前目录
$(CROSS_COMPILE)gcc -o led_test led_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order led_test
# 要想把a.c, b.c编译成ab.ko, 可以这样指定:
# ab-y := a.o b.o
# obj-m += ab.o
obj-m += led_drv.o
②思考
- count不能作为全局变量,因为使用到gpios,可能还未定义所以将其声明为局部变量。
int count = sizeof(gpios)/sizeof(gpios[0]);
不直接指定count=2是因为假设将来需要修改gpios 数组的大小,如果手动指定count为 2,那么在修改数组大小后,还需要手动更新count的值,否则会导致count和数组大小不匹配,可能导致意想不到的错误。而使用 sizeof(gpios)/sizeof(gpios[0]) 的方式,无论你修改了 gpios 数组的大小,都可以正确计算出数组元素的个数。这样,你就不需要手动更新 count 的值,代码更具有可维护性。return -1;
通常用于表示函数调用失败、资源不可用、不支持的操作等一般性错误。
return -EINVAL;
常用于在Linux内核编程中指示无效参数错误,例如驱动程序接口中的参数验证。- strcmp 是一个字符串比较函数,用于比较两个字符串的内容是否相等,相等返回0。
- 在test中考虑了一个问题,在使用read函数时,考虑为什么不能传递1,因为在内核中定义的函数只用到了buf[0],进行实验后发现无法读出。
思考再三后,原因在于内核中定义的是ker_buf[2],如果只传递1给 read 函数,它将只读取一个字节的数据,而不是完整的两个字节,这可能会导致数据读取不完整或错误。
③上机实验
1.打开开发板原理图,确定引脚标号
2.打开串口,输入
cat /sys/kernel/debug/gpio
进入该目录,128+3=131,这就是drv.c中的131来历
3.之后就是常规操作了,先make再insmod led_drv.ko,再输入./led_test 0 on就可以看到灯亮了