写驱动实现LED闪烁及基础概念查看
2023/7/20 初学内核,记录与分享,感叹内核学了后真的感觉很多东西都通透了,但是难度太大,只能浅浅初探。
前提
内核五大功能
➢ 进程管理:进程的创建,销毁,调度等功能
注:可中断,不可中断,就是是否被信号打断。从运行状态怎样改到可中断等待态,和不可中断等待态操作系统开始会对每个进程分配一个时间片,当进程里面写了sleep函数,进程由运行到休眠态,但是此时CPU不可能等着。有两种方法,1:根据时间片,CPU自动跳转,2:程序里面自己写能引起CPU调度的代码就可以
➢ 文件管理:通过文件系统ext2/ext3/ext4 yaff jiffs等来组织管理文件
➢ 网络管理:通过网络协议栈(OSI,TCP)对数据进程封装和拆解过程(数据发送和接收是通过网卡驱动完成的,网卡驱动不会产生文件(在Linux系统dev下面没有相应的文件),所以不能用open等函数,而是使用的socket)。
➢ 内存管理:通过内存管理器对用户空间和内核空间内存的申请和释放
➢ 设备管理: 设备驱动的管理(驱动工程师所对应的)
✧ 字符设备驱动: (led 鼠标 键盘 lcd touchscreen(触摸屏))
1.按照字节为单位进行访问,顺序访问(有先后顺序去访问)
2.会创建设备文件,open read write close来访问
✧ **块设备驱动 ** :(camera u盘 emmc)
1.按照块(512字节)(扇区)来访问,可以顺序访问,可以无序访问
2.会创建设备文件,open read write close来访问
✧ 网卡设备驱动:(猫)
1.按照网络数据包来收发的。
驱动
三要素:入口,出口,许可证
● 入口:资源的申请
● 出口:资源的释放
● 许可证:GPL(写一个模块需要开源,因为Linux系统是开源的,所以需要写许可协议)
1.基础模块
驱动格式
#include <linux/init.h>
#include<linux/module.h>
//__init将hello_init放到.init.text段中
static int __init hello_init(void)
{
return 0;
}
//__exit将hello_exit放到.exit.text段中
static void __exit hello_exit(void)
{
}
//告诉内核驱动的入口地址(函数名为函数首地址)
module_init(hello_init);
//告诉内核驱动的出口地址
module_exit(hello_exit);
//许可证
MODULE_LICENSE("GPL");
makefile格式
//板子内核路径
#KERNEL_PATH=/home/hq/kernel/kernel-3.4.39
//Ubuntu内核的路径
KERNEL_PATH=/lib/modules/$(shell uname -r)/build
PWD=$(shell pwd) //驱动文件的路径
all: //目标
make -C $(KERNEL_PATH) M=$(PWD) modules //(-C:进入顶层目录)
/*注:进入内核目录下执行make modules这条命令
如果不指定 M=$(PWD) 会把内核目录下的.c文件编译生成.ko*/
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m = hello.o //指定编译模块的名字
命令的使用
创建索引文件
ctags -R
在终端上
vi -t xxx
在代码中跳转
ctrl + ]
ctrl + t
sudo insmod hello.ko 安装驱动模块
sudo rmmod hello 卸载驱动模块
lsmod 查看模块
dmesg 查看消息
sudo dmesg -C 直接清空消息不回显
sudo dmesg -c 回显后清空
打印函数
概念
1. #include <linux/printk.h> //增加这个头文件
2. printk(KERN_ERR "Fail%d",a);//打印函数,KERN_ERR对应的是内核打印级别
3. grep "printk" * -nR 检索所有的打印函数
vi -t KERN_ERR(查看内核打印级别)
4.
#define KERN_EMERG "<0>" /* system is unusable */(系统不用)
#define KERN_ALERT "<1>" /* action must be taken immediately */(被立即处理)
#define KERN_CRIT "<2>" /* critical conditions */(临界条件,临界资源)
#define KERN_ERR "<3>" /* error conditions */(出错)
#define KERN_WARNING "<4>" /* warning conditions */(警告)
#define KERN_NOTICE "<5>" /* normal but significant condition */(提示)
#define KERN_INFO "<6>" /* informational */(打印信息时候的级别)
#define KERN_DEBUG "<7>" /* debug-level messages */ (调试级别)
0 ------ 7
最高的 最低的
5. 打印显示效果是有优先级的
inux@ubuntu:~$ cat /proc/sys/kernel/printk 查看优先级,分布为以下的四个级别
终端的级别 消息的默认级别 终端的最大级别 终端的最小级别
4 4 1 7
更改方式为 echo 4 3 1 7 > /pro/sys/kernel/printk(切换成su权限)
如果是更改开发板打印级别 vi rootfs/etc/init.d/rcS
echo 4 3 1 7 > /proc/sys/kernel/printk
驱动多文件编译
概念
hello.c add.c
Makefile
obj-m:=demo.o
demo-y+=hello.o add.o
(-y作用:将hello.o add.o放到demo.o中)
最终生成demo.ko文件
举例
#KERNEL_PATH=/home/hq/kernel/kernel-3.4.39
KERNEL_PATH=/lib/modules/$(shell uname -r)/build
PWD=$(shell pwd)
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m = printkk.o
demo-y+=add.o
demo-y+=sub.o
demo-y+=printkk.o
模块传递函数
概念
module_param(name, type, perm)
功能:接收命令行传递的参数
参数:
@name :变量的名字
@type :变量的类型
@perm :权限 0664 0775(其它用户对我的只有读和执行权限,没有写的权限)
modinfo hello.ko(查看变量情况)
MODULE_PARM_DESC(_parm, desc)
功能:对变量的功能进行描述
参数:
@_parm:变量
@desc :描述字段
只能传十进制,不可以写十六进制
module_param_array(name, type, nump, perm)
功能:接收命令行传递的数组
参数:
@name :数组名
@type :数组的类型
@nump :参数的个数,变量的地址
@perm :权限
举例
#include <linux/init.h>
#include<linux/module.h>
#include<linux/printk.h>
#include"head.h"
//入口申请资源
int a=10;
module_param(a,int,0664);
MODULE_PARM_DESC(a,"this is lcd light");
short b=11;
module_param(b,short,0774);
MODULE_PARM_DESC(b,"this is rgb");
char ch='c';
module_param(ch,byte,0664);
MODULE_PARM_DESC(ch,"this is ch val");
char *p=NULL;
module_param(p,charp,0664);
MODULE_PARM_DESC(p,"this is ch *p");
int ww[10]={0};
int num;
module_param_array(ww,int,&num,0664);
MODULE_PARM_DESC(p,"this is int array [10]");
static int __init printk_init(void)
{
int i;
printk("a is val=%d\n",a);
printk("b is val=%d\n",b);
printk("c is val=%c\n",ch);
printk("p is val=%s\n",p);
for (i = 0; i < num; i++)
{
printk("ww[%d]==%d\n",i,ww[i]);
}
return 0;
}
//出口函数释放资源
static void __exit printk_exit(void)
{
}
//入口
module_init(printk_init);
//出口
module_exit(printk_exit);
MODULE_LICENSE("GPL");
2.字符设备驱动
概念理解
硬件与软件的连接,需要驱动。
驱动是写在内核层的。
应用层是调用内核层提供的接口实现的。
所以,这三者之间一定是有个东西作为联系的。那就是设备号。
以LED为例 字符设备的 步骤:
1.注册字符设备驱动 - 得到一个字符设备驱动的框架,并且得到设备号
2.确定操作的硬件设备 - led灯(初始化灯)
3.初始化灯(先建立灯实际物理地址和虚拟地址之间的映射)- 基于操作系统开发,操作虚拟内存,
4.用户空间数据拷贝到内核空间数据的交互(用户使用的时候,驱动才会被真正运行,涉及数据交互)
5.在应用层创建一个设备文件(设备节点)
函数框架
int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
功能:注册一个字符设备驱动
参数:@major:主设备号
:如果你填写的值大于0,它认为这个就是主设备号
:如果你填写的值为0,操作系统给你分配一个主设备号
@name :名字 cat /proc/devices
@fops :操作方法结构体
返回值:major>0 ,成功返回0,失败返回错误码(负数) vi -t EIO
major=0,成功主设备号,失败返回错误码(负数)
cat /proc/devices 查看系统自动分配的主设备号
void unregister_chrdev(unsigned int major, const char *name)
功能:注销一个字符设备驱动
参数:
@major:主设备号
@name:名字
返回值:无
sudo mknod led (路径是任意) c/b 主设备号 次设备号 //手动创建设备文件
/-------数据传递,角度是占在用户角度来说
int copy_from_user(void *to, const void __user *from, int n)
功能:从用户空间拷贝数据到内核空间(用户需要写数据的时候)
参数:
@to :内核中内存的首地址
@from:用户空间的首地址
@n :拷贝数据的长度(字节)
返回值:成功返回0,失败返回未拷贝的字节的个数
int copy_to_user(void __user *to, const void *from, int n)
功能:从内核空间拷贝数据到用户空间(用户开始读数据)
参数:
@to :用户空间内存的首地址
@from:内核空间的首地址
@n :拷贝数据的长度(字节)
返回值:成功返回0,失败返回未拷贝的字节的个数
----------/
/-----物理地址转为虚拟地址
void * ioremap(phys_addr_t offset, unsigned long size)
功能:将物理地址映射成虚拟地址
参数:
@offset :要映射的物理的首地址
@size :大小(字节)(映射是以业为单位,一页为4K,就是当你小于4k的时候映射的区域都为4k)
返回值:成功返回虚拟地址,失败返回NULL((void *)0);
void iounmap(void *addr)
功能:取消映射
参数:
@addr :虚拟地址
返回值:无
----------------/
/-----设备节点自动创建
如果没有写节点自动创建,那么就需要加
sudo mknod <名字> c <设备号> <子设备号>
设备号 就需要 cat /proc/devices 中查看到设备号
例如: sudo mknod led c 230 0
这样在本地创建了字符设备命名为led的字符设备文件。
struct class *cls;
cls = class_create(owner, name) /void class_destroy(struct class *cls)//销毁
功能:向用户空间提交目录信息(内核目录的创建)
参数:
@owner :THIS_MODULE(看到owner就添THIS_MODULE)
@name :目录名字
返回值:成功返回struct class *指针
失败返回错误码指针 int (-5)
struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)(内核文件的创建),每个文件对应一个外设(硬件设备)
/void device_destroy(struct class *class, dev_t devt)//销毁
功能:向用户空间提交文件信息
参数:
@class :目录返回指针
@parent:NULL
@devt :设备号 (major<<12 |0 < = > MKDEV(major,0))
@drvdata :NULL
@fmt :文件的名字
返回值:成功返回struct device *指针
失败返回错误码指针 int (-5)
-------/
实例
实现控制LED闪烁
chrdev.c 文件
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <asm/io.h>
#include <linux/device.h>
#define NAME "chrdev_led" //这个名字最终效果就是设备号旁的名字
//定义宏表示实际物理首地址 这个地址是根据芯片手册找到对应的首地址
#define RED_BASE 0xc001a000 //#GPIOA28
#define GREE_BASE 0xc001e000 // #GPIOE13
#define BLUE_BASE 0xc001b000 //#GPIOB12
unsigned int major = 0; //定义是为了接受创建的设备号
char kbuf[32] = {0};//实现拷贝,与应用层用户进行数据交换的数组
//定义指针保存映射后的虚拟地址的首地址
unsigned int *red_addr = NULL; //这样写的好处在于+1就是移动四个字节,对应单片机一个寄存器的大小
unsigned int *gree_addr = NULL;
unsigned int *blue_addr = NULL;
struct class *cls = NULL;//是为了创建设备节点做的准备
struct device *dev = NULL;
int chrdev_open(struct inode *node_t, struct file *file_t)//当用户使用fopen就会调用这句
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);//测试用的
return 0;
}
ssize_t chrdev_read(struct file *file_t, char __user *ubuf, size_t n, loff_t *off_t)//当用户使用读操作会调用这句
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
//将内核空间的数据拷贝到用户空间
if (sizeof(kbuf) < n)
n = sizeof(kbuf); //ubuf是用户,应用层的数据
if (copy_to_user(ubuf, kbuf, n) != 0)//当用户使用读操作会进入这个函数,然后再将内核数据给用户
{
printk("copy_to_user err.");
return -EINVAL;
}
return 0;
}
ssize_t chrdev_write(struct file *file_t, const char __user *ubuf, size_t n, loff_t *off_t)
{
//当用户执行写操作驱动会进行这句操作
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
if (sizeof(kbuf) < n)
n = sizeof(kbuf);
//将用户空间的数据拷贝到内核空间
if (copy_from_user(kbuf, ubuf, n) != 0)
{
printk("copy_from_user err.");
return -EINVAL;
}
printk("kbuf=%s\n", kbuf);
if (kbuf[0] == 1)//根据用户写的内容做出相应的操作
{
//红灯亮
*red_addr |= (1 << 28);//这就是控制对应寄存器的IO控制高低电平的
}
else if (kbuf[0] == 0)
{
//红灯灭
*red_addr &= (~(1 << 28));
}
if (kbuf[1] == 1)
{
//红灯亮
*gree_addr |= (1 << 13);
}
else if (kbuf[1] == 0)
{
//红灯灭
*gree_addr &= (~(1 << 13));
}
if (kbuf[2] == 1)
{
//红灯亮
*blue_addr |= (1 << 12);
}
else if (kbuf[2] == 0)
{
//红灯灭
*blue_addr &= (~(1 << 12));
}
return 0;
}
int chrdev_close(struct inode *node_t, struct file *file_t)//用户执行close操作进入这句
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
return 0;
}
struct file_operations fops = { //用户之所以执行对应的操作就能跳转对应的位置就是因为这个结构体的原因
//这个结构体的名字就对应了驱动三模块的申请中需要的参数
.open = chrdev_open,
.read = chrdev_read,
.write = chrdev_write,
.release = chrdev_close,
};
//入口函数-申请资源
static int __init printk_init(void)
{
//注册字符设备驱动
major = register_chrdev(major, NAME, &fops);
if (major < 0)
{
printk("register_chrdev err.");
return -EINVAL;
}
//初始化灯-引脚功能(GPIO) 输出功能 灭
//1.基于操作系统开发。建立灯物理地址和虚拟地址之间映射
//红灯
red_addr = (unsigned int *)ioremap(RED_BASE, 40);//基于地址开辟一定的范围映射,实际上为4k大小
if (red_addr == NULL)
{
printk("ioremap err.");
return -EINVAL;
}
//gree
gree_addr = (unsigned int *)ioremap(GREE_BASE, 40);
if (gree_addr == NULL)
{
printk("ioremap gree err.");
return -EINVAL;
}
blue_addr = (unsigned int *)ioremap(BLUE_BASE, 40);
if (blue_addr == NULL)
{
printk("ioremap blue err.");
return -EINVAL;
}
//通过虚拟地址操作实际物理地址向对应寄存器写值
//配置引脚GPIO
//配置输入输出模式、复用选用、高点电平
*(red_addr + 9) &= (~(3 << 24));
*(red_addr + 1) |= (1 << 28); //输出模式
*red_addr &= (~(1 << 28));
*(gree_addr + 8) &= (~(3 << 26));
*(gree_addr + 1) |= (1 << 13);
*gree_addr &= (~(1 << 13));
*(blue_addr + 8) |= (1 << 25);
*(blue_addr + 8) &= (~(1 << 24));
*(blue_addr + 1) |= (1 << 12);
*blue_addr &= (~(1 << 12));
//设置自动创建设备节点
//1.提交目录信息
cls = class_create(THIS_MODULE, NAME);//这个NAME为上面的宏定义
if (IS_ERR(cls))//判断是否再这范围内的函数
{
printk("class_create err.");
return -EINVAL;
}
//2.提交文件信息
dev = device_create(cls, NULL, MKDEV(major, 0), NULL, "led");
if (IS_ERR(dev))
{
printk("class_create err.");
return -EINVAL;
}
return 0;
}
//出口函数-释放资源(先申请的后释放,后申请先释放)
static void __exit printk_exit(void)
{
//销毁创建的设备节点
device_destroy(cls,MKDEV(major,0));
class_destroy(cls);
//取消映射
iounmap(blue_addr);
iounmap(gree_addr);
iounmap(red_addr);
//注销设备驱动
unregister_chrdev(major, NAME);
}
module_init(printk_init);
module_exit(printk_exit);
MODULE_LICENSE("GPL");
app.c文件
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, const char *argv[])
{
int fd = open("/dev/led", O_RDWR);//这个是会生成在该文件
char buf[32] = {1, 0, 0}; //buf[0]-red buf[1]-gree buf[2]-blue
while (1) //红灯亮一秒灭一秒
{
buf[0] = 0;
buf[1] = 0;
buf[2] = 0;
write(fd, buf, sizeof(buf));
sleep(1);
buf[0] = 0;
buf[1] = 0;
buf[2] = 1;
write(fd, buf, sizeof(buf));
sleep(1);
buf[0] = 0;
buf[1] = 1;
buf[2] = 0;
write(fd, buf, sizeof(buf));
sleep(1);
buf[0] = 0;
buf[1] = 1;
buf[2] = 1;
write(fd, buf, sizeof(buf));
sleep(1);
buf[0] = 1;
buf[1] = 0;
buf[2] = 0;
write(fd, buf, sizeof(buf));
sleep(1);
buf[0] = 1;
buf[1] = 0;
buf[2] = 1;
write(fd, buf, sizeof(buf));
sleep(1);
buf[0] = 1;
buf[1] = 1;
buf[2] = 0;
write(fd, buf, sizeof(buf));
sleep(1);
buf[0] = 1;
buf[1] = 1;
buf[2] = 1;
write(fd, buf, sizeof(buf));
sleep(1);
}
close(fd);
return 0;
}
Makefile文件
KERNEL_PATH=/home/hq/kernel/kernel-3.4.39 #开发板内核路径
#KERNEL_PATH=/lib/modules/$(shell uname -r)/build
#pc电脑上的路径 gcc
PWD=$(shell pwd)
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
#在内核顶层目录执行make modules才可以将hello.c生成驱动
#-C 路径:找到这个路径执行这个命令
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m = chrdev.o
表现效果
a.out文件因为是在开发板运行,所以是交叉编译工具编译
make后会生成chrdev.ko驱动文件,进行安装后
输入cat proc/devices可以看到 发现,刚刚我强调的名字就是出现在这里
对应的设备号,因为是是自动创建设备节点了,所以在输入 cd dev
就看到了led了,现在在执行./a.out 就可以看到效果了