内核空间包含的软件就是uImage。
内核空间的软件在运行的适合,CPU核的工作模式为SVC管理模式,内核空间的软件权限最高。
内核空间的软件同样不允许直接访问外设的物理地址,要想访问外设的物理地址必须将物理地址映射到内核虚拟地址上,0XC000000~0XFFFFFFF。
内核空间的软件如果对内存进行非法的访问,直接会导致Linux系统奔溃,类似Windows蓝屏。
int module_init(void);
void module_exit(void);
MODULE_LICENSE("GPL");
内核程序使用的头文件位于Linux内核源码中。
内核程序的入口函数要用module_init宏来修饰,并且入口函数的返回数据必须是int型。
内核程序的出口函数用module_exit修饰,出口函数的返回值和形参都是void,当执行rmmod从内核uImage中卸载内核程序或者采用不选择的方式编译uImage后。这两种情况内核uImage会自动调用出口函数。
任何内核程序,只要是一个.c文件,必须添加MODULE_LICENSE(“GPL”),否则内核会出错并且部分内核函数及变量无法使用。
内核打印函数printk,只要是内核程序调用的函数,不是自己编写的就是内核源码提供的,一律不允许调用标准C库的函数。
内核程序的编译一定要关联内核源码(kernel),头文件和调用的函数都是位于内核源码中。
回顾内核程序的编译步骤:
总结:这种内核程序的编译过程极其繁琐,简化只需要一个Makefile即可解决。
cd /opt/drivers/day01/
vim Makefile
#将helloworld.o和uImage分开编译,单独编译
obj-m += helloworld.o
#all:伪目标,对应的命令当前执行make或者make all时执行。
#-C /opt/kernel :进入/opt/kernel目录下。
#SUBDIRS:子目录。
all:make -C /opt/kernel SUBDIRS=$(PWD) modules#当执行make clean时进行清除操作
clean:make -C /opt/kernel SUBDIRS=$(PWD) clean//将内核程序目标文件拷贝到根文件系统进行安装和卸载。
mkdir /opt/rootfs/home/drivers
cp helloworld.ko /opt/rootfs#下位机测试:
#重启下位机,进入uboot命令执行:
setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/opt/rootfs ip=192.168.1.110:192.168.1.8:192.168.1.1:255.255.255.0 init=/linxurc console=ttySAC0,115200 maxcpus=1saveenv
tftp 0x48000000 uImage
bootm 0x48000000#下位机Linux系统启动后执行:
cd /home/drivers
lshelloworld.ko
#将内核程序安装到uImage中
insmod helloworld.ko
lsmod
rmmod helloworld #从内核中卸载内核程序。
lsmod
mkdir /opt/drivers/day01/3.0
cd /opt/drivers/day01/3.0
vim helloworld.c
vim Makefile
make
cp helloworld.ko /opt/rootfs/home/drivers/
cd /home/drivers
echo 8 > /proc/sys/kernel/printk
insmod helloworld.ko
rmmod helloworld
#include <linux/init.h>
#include <linux/module.h>static int irq;
static char *pstring;//传参声明
module_param(irq, int, 0664);
module_param(pstring, charp, 0);
//insmod时执行
static int helloworld_init(void)
{printk("%s: irq = %d, pstring = %s n", __func__, irq, pstring):return 0;
}
//rmmod时执行
static void helloworld_exit(void)
{printk("%s: irq = %d, pstring = %sn", __func__, irq, pstring);
}module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL");#编写Makefile
obj-m += helloworld.o
all:make -C /opt/kernel SUBDIRS=$(PWD) modulesclean:make -C /opt/kernel SUBDIRS=$(PWD) clean#使用执行:
insmod helloworld.ko irq-250 pstring=china
#安装内核后,传递参数执行:
#查看当前参数值:
cat /sys/module/helloworld/parameters/irq
#修改参数值:
echo 2555 > /sys/module/helloworld/parameters/irq
明确:何为符号(symbol)
回顾应用程序符号导出(多文件之间的访问,多文件,使用头文件声明和包含)
#ifndef __TEST_H
#define __TEST_H//函数声明
extern void my_test(void);#endif
#include <linux/init.h>
#include <linux/module.h>//函数定义
void my_test(void)
{printk("%sn", __FUNCTION__);
}//显式将函数进行导出
EXPORT_SYMBOL(my_test);
MODULE_LICENSE("GPL");
#include <linux/init.h>
#include <linux/module.h>
#include "test.h"static int helloworld_init(void)
{//调用testmy_test();printk("%sn", __func__);return 0;
}static void helloworld_exit(void)
{//调用testmy_test();printk("%sn", __func__);
}module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL");
vim Makefile
obj-m += helloworld.o
obj-m += test.o
all:make -C /opt/kernel SUBDIRS=$(PWD) modulesclean:make -C /opt/kernel SUBDIRS=$(PWD) clean#复制到下位机上:
cp test.ko helloworld.ko /opt/rootfs/home/drivers
相同点:
不同点:
能够指定打印输出级别,共8级。分别是:
结论:数字越小,危险系数越高。此信息越应该打印输出。
用法:
test_printk.c
#include <linux/init.h>
#include <linux/module.h>static int printk_all_init(void)
{printk("<0>", "level 0n");printk("<1>", "level 1n");printk("<2>", "level 2n");printk("<3>", "level 3n");printk("<4>", "level 4n");printk("<5>", "level 5n");printk("<6>", "level 6n");printk("<7>", "level 7n");return 0;
}static void printk_all_exit(void)
{printk("<0>", "level 0n");printk("<1>", "level 1n");printk("<2>", "level 2n");printk("<3>", "level 3n");printk("<4>", "level 4n");printk("<5>", "level 5n");printk("<6>", "level 6n");printk("<7>", "level 7n");}
module_init(printk_all_init);
module_exit(printk_all_exit);
MODULE_LICENSE("GPL");
int gpio_request(unsigned gpio, const char *label)
void gpio_free(unsigned gpio)
int gpio_direction_output(unsigned gpio, int value)
int gpio_direction_input(unsigned gpio)
int gpio_set_value(unsigned gpio, int value)
int gpio_get_value(unsigned gpio)
卸载驱动关灯
回顾:标准C的结构体使用
//声明描述LED硬件信息的数据结构struct led_resource {int gpio; //LED灯对应GPIO引脚的软件编号char *name;//LED灯的名称int state; //LED的状态};//定义初始化四个LED灯的硬件信息对象 struct led_resource led_info[] = {{PAD_GPIO_C+12, "LED1", 0},{PAD_GPIO_C+11, "LED2", 0},{PAD_GPIO_C+10, "LED3", 0},{PAD_GPIO_C+9, "LED4", 0}};
//定义初始化四个LED灯的硬件信息对象 struct led_resource led_info[] = {{.name = "LED1",.gpio = PAD_GPIO_C+12},{.name = "LED2",.gpio = PAD_GPIO_C+11},{.name = "LED3",.gpio = PAD_GPIO_C+10},{.name = "LED4",.gpio = PAD_GPIO_C+9}};
结构体的标记初始化方式可以不用按照顺序,并且不用全部进行对成员初始化!
上位机实施步骤:mkdir /opt/drivers/day02/1.0 -pcd /opt/drivers/day02/1.0vim led_drv.cvim Makefilemakecp led_drv.ko /opt/rootfs/home/drivers下位机测试:cd /home/driversinsmod led_drv.ko //开灯rmmod led_drv //关灯
面试题:谈谈对linux系统调用的理解
以write系统调用函数为例,掌握系统调用的实现过程:
例如:
int main(void){write(1, "hello,worldn", 12);printf("hello,worldn");return 0;}
1.当应用程序(进程)调用write系统调用函数, 首先会跑到
C库的write函数的定义实现的地方
2.C库的write函数将会做两件事
3.一旦触发软中断异常,CPU核立马要处理软中断异常
硬件上自动将做:
4.linux内核软中断处理的入口地址相关的代码将做
以下工作:
5.切记:要边说边画图!
答:设备驱动的两大核心内容
字符设备驱动:对字符设备的访问按照字节流形式访问
例如:LED,按键,蜂鸣器,GPS(UART),GPRS(UART),BT(UART)
触摸屏(XY绝对坐标),LCD显示屏(像素点)
声卡,摄像头,各种硬件传感器(三轴,重力,光线,距离,温度等)
EEPROM存储器(I2C接口)
块设备驱动:对块设备的访问按照数据块进行,比如一次操作512字节
例如:硬盘,U盘,TF卡,SD卡,Nandflash,Norflash,EMMC
网络设备驱动:对网络设备驱动一般按照网络协议进行
例如:有线网卡和无线网卡
“一切皆文件”:任何硬件外设都是以文件的形式存放用户访问文件本质上就是在访问对应的硬件外设字符设备对应的文件称之为字符设备文件块设备对应的文件称之为块设备文件网络设备无设备文件,通过socket套接字进行访问
明确:字符设备文件本身代表的就是字符设备硬件本身
明确:字符设备文件存在于根文件系统必要目录的dev目录下
当然块设备文件也存于dev目录下
举例子:查看下位机的UART设备的字符设备文件
ls /dev/ttySAC* -lh 得到以下信息:
crw-rw---- 204, 64 /dev/ttySAC0
crw-rw---- 204, 65 /dev/ttySAC1
crw-rw---- 204, 66 /dev/ttySAC2
crw-rw---- 204, 67 /dev/ttySAC3
说明:
c:表示此设备文件对应的设备为字符设备
204:表示串口的主设备号
64/65/66/67:分别表示第一个,第二个,第三个,第四个串口的次设备号
ttySAC0:表示第一个串口的设备文件名
ttySAC1:表示第二个串口的设备文件名
ttySAC2:表示第三个串口的设备文件名
ttySAC3:表示第四个串口的设备文件名
注意:一个硬件外设个体有唯一的一个设备文件名
明确:访问字符设备文件本质就是在访问字符设备硬件
明确:字符设备文件的访问必须利用系统调用函数
例如:
打开UART0:注意:使用绝对路径!int fd = open("/dev/ttySAC0", O_RDWR) if (fd < 0)return -1; 从UART0读取数据:
char buf[1024] = {0};
read(fd, buf, 1024);向UART0写入数据:
write(fd, "hello,worldn", 12);关闭UART0:
close(fd);
主设备号作用:应用程序根据字符设备文件的主设备号
在茫茫的内核驱动中找到对应的唯一的
设备驱动,一个设备驱动仅有唯一的主设备号
次设备号作用:设备驱动根据次设备号能够找到应用程序
要访问的硬件外设个体
总结:一个驱动仅有一个主设备号,一个硬件个体仅有一个次设备号
应用根据主设备号找驱动,驱动根据次设备号找硬件个体
所以:主,次设备号对于linux内核是一个宝贵的资源,某个
设备驱动必须要想linux内核申请主,次设备号
问:如何申请呢?
设备号:设备号包含主,次设备号
linux内核用dev_t(unsigned int)数据类型描述设备号信息
设备号的高12位用来描述主设备号
设备号的低20位用来描述次设备号
设备号和主,次设备号之间的转换操作宏:
设备号=MKDEV(已知的主设备号,已知的次设备号);
主设备号=MAJOR(已知的设备号);
次设备号=MINOR(已知的设备号);
作业:认真研读掌握MINOR宏的源码实现过程!
问:既然设备号对于linux内核是一种宝贵的资源,设备驱动
如何向内核申请资源呢?
答:利用以下函数即可申请
int alloc_chrdev_region(dev_t *dev,
unsigned baseminor,
unsigned count,
const char *name
);
函数功能:向内核申请设备号
参数:
dev:保存申请的设备号
包括主设备号和起始的次设备号
baseminor:希望申请的起始次设备号,一般给0
count:申请的次设备号的个数
如果baseminor=0,count=2,那么申请的次设备号分别是0和1
name:申请设备号指定的名称,随便取
将来通过执行cat /proc/devices查看
设备驱动一旦不再使用设备号,记得要将设备号资源归还给linux内核:
void unregister_chrdev_region(dev_t dev,
unsigned count);
功能:释放申请的设备号资源
dev:申请的设备号
count:申请的次设备号的个数
//描述字符设备驱动struct char_device {dev_t dev; //描述申请的设备号int count; //描述申请的次设备号的个数int (*open)(...); //给用户提供打开设备的接口int (*close)(...); //给用户提供关闭设备的接口int (*read)(...); //给用户提供读设备接口int (*write)(...); //给用户提供写设备接口};
浮想联翩,应用程序和驱动接口调用关系:
应用程序open->软中断->内核的sys_open->驱动的open接口
应用程序close->软中断->内核的sys_close->驱动的close接口
应用程序read->软中断->内核的sys_read->驱动的read接口
应用程序write->软中断->内核的sys_write->驱动的write接口
//描述字符设备驱动给用户提供的操作接口struct file_operations {int (*open)(...); //给用户提供打开设备的接口int (*close)(...); //给用户提供关闭设备的接口int (*read)(...); //给用户提供读设备接口int (*write)(...); //给用户提供写设备接口};//描述字符设备驱动struct char_device {dev_t dev; //描述申请的设备号int count; //描述申请的次设备号的个数struct file_operations *ops;//给用户提供的操作接口};
Linux内核描述给用户提供操作接口的数据结构
//描述字符设备驱动给用户提供的操作接口struct file_operations {int (*open) (struct inode *, struct file *); //给用户提供打开设备的接口int (*release) (struct inode *, struct file *); //给用户提供关闭设备的接口...};字符设备驱动和应用程序调用关系:应用程序open->软中断->内核的sys_open->驱动的open接口应用程序close->软中断->内核的sys_close->驱动的release接口//描述字符设备驱动struct cdev {dev_t dev; //描述申请的设备号int count; //描述申请的次设备号的个数struct file_operations *ops;//给用户提供的操作接口...};
配套函数:
cdev_init(strcut cdev *cdev,
struct file_operations *fops)
函数功能:初始化字符设备驱动对象,就是给字符设备
驱动对象添加一个硬件操作接口
cdev:要初始化的字符设备对象
fops:给用户提供的硬件操作接口
cdev_add(struct cdev *cdev, dev_t dev, int count)
函数功能:向内核注册添加一个字符设备对象,一旦添加完毕
内核就有一个真实的字符设备驱动
cdev:要注册的字符设备对象
dev:申请的设备号
count:申请的次设备号的个数
cdev_del(struct cdev *cdev)
函数功能:从内核中卸载字符设备对象,一旦卸载完毕
内核就不会有一个真实的字符设备驱动
1.定义初始化硬件操作接口对象
struct file_operations led_fops = {
.open = led_open, //打开设备接口
.release = led_close, //关闭设备接口
};
2.定义初始化字符设备对象
struct cdev led_cdev; //定义字符设备对象
//led_cdev.ops = &led_fops
cdev_init(&led_cdev, &led_fops);//给字符设备对象添加硬件操作接口
3.最终向内核注册字符设备对象
cdev_add(&led_dev, 申请的设备号,次设备号的个数);
一旦注册成功,内核就有一个真实的字符设备驱动,并且
给用户提供硬件操作接口(open/release)
4.从内核卸载字符设备对象
cdev_del(&led_cdev);
5.最后编写led_open/led_close接口
具体内容根据用户需求来定
mkdir /opt/drivers/day02/2.0cd /opt/drivers/day02/2.0vim led_drv.c //驱动vim led_test.c //应用vim Makefilemakecp led_drv.ko /opt/rootfs/home/driversarm-cortex_a9-linux-gnueabi-gcc -o led_test led_testp led_test /opt/rootfs/home/drivers下位机测试:cd /home/driversinsmod led_drv.kocat /proc/devices //查看申请的主设备号Character devices:主设备号 设备名称1 mem5 /250 tarenamknod /dev/myled c 申请的主设备号 0 //创建设备文件myled./led_test //运行
本文发布于:2024-02-05 08:00:12,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170727861464755.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |