1、 字符设备驱动
1.1 字符设备驱动的框架
1.2 注册字符设备驱动的函数
#include <linux/fs.h>
1. register_chrdev ----> 向内核注册字符设备
int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
功能:向内核注册字符设备驱动
参数:
@ major : 主设备号
major = 0 :主设备号由内核自动分配(不需要考虑主设备号是否被占用)
major > 0 : major就是主设备号(需要考虑主设备号是否被占用)
如何查看已经使用的设备号:cat /proc/devices
@ name : 设备文件的名字
@ fops : 操作方法结构体的指针变量的地址
struct file_operations {
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
};
返回值:
major = 0 : 成功返回分配成功之后的主设备号,失败返回错误码
major > 0 : 成功返回0, 失败返回错误码
错误码: EIO , 通过EIO错误码查看其他的错误码
由于错误码的宏定义是一个正数,因此再返回错误码时需要再前边加一个符号,
例子 : return -EIO;
#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* struct file_operations */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math argument out of domain of func */
#define ERANGE 34 /* Math result not representable */
2. unregister_chrdev ---> 注销字符设备
void unregister_chrdev(unsigned int major, const char *name)
功能:注销字符设备驱动
参数:
@ major : 主设备号
@ name : 字符设备的名字
返回值:无
1.3 字符设备驱动测试代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#define MYNAME "mycdev"
int major;
int mycdev_open (struct inode *inode, struct file *file)
{
printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
return 0;
}
ssize_t mycdev_read (struct file *file, char __user *ubuf, size_t size, loff_t *offset)
{
printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
return 0;
}
ssize_t mycdev_write (struct file *file, const char __user *ubuf, size_t size, loff_t *offset)
{
printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
return 0;
}
int mycdev_close (struct inode *inode, struct file *file)
{
printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
return 0;
}
struct file_operations fops = {
.open = mycdev_open,
.read = mycdev_read,
.write = mycdev_write,
.release = mycdev_close,
};
static int __init mycdev_init(void)
{
// 注册字符设备
major = register_chrdev(0, MYNAME, &fops);
if (major < 0) {
printk("register char device failed!\n");
return -EIO;
}
return 0;
}
static void __exit mycdev_exit(void)
{
unregister_chrdev(major, MYNAME);
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zk <zhouk_bj@hqyj.com>");
1.4 应用层的代码实现
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
char u_buf[100] = {0};
int main(int argc, const char *argv[])
{
/*your code*/
int fd;
fd = open("/dev/mycdev", O_RDWR);
if (fd < 0) {
perror("open");
return -1;
}
read(fd, u_buf, sizeof(u_buf));
write(fd, u_buf, sizeof(u_buf));
close(fd);
return 0;
}
1.5 修改Makefile文件,编译应用层的代码
modname ?=
arch ?= x86
ifeq ($(arch),x86)
KERNELDIR := /lib/modules/$(shell uname -r)/build
CROSS_COMPILE :=
else
KERNELDIR := /home/linux/DC22041/porting/linux/linux-stm32mp-5.10.61-stm32mp-r2-r0/linux-5.10.61/
CROSS_COMPILE := arm-linux-gnueabihf-
endif
CURRENTDIR := $(shell pwd)
CC := $(CROSS_COMPILE)gcc
all:
make -C $(KERNELDIR) M=$(CURRENTDIR) modules
$(CC) test.c -o test
install:
@cp $(modname).ko ~/nfs/rootfs/
@cp test ~/nfs/rootfs
help:
@echo "make arch=arm|x86 modname=modules drivers source file name"
clean:
make -C $(KERNELDIR) M=$(CURRENTDIR) clean
obj-m := $(modname).o
1.6 加载驱动并测试驱动的步骤
1> 加载驱动 : sudo insmod mycdev.ko
2> 查看驱动对应的主设备号 : cat /proc/devices, 需要查看自己的主设备号
3> 在/dev目录下创建设备文件 sudo mknod /dev/mycdev c 236 0
mknod : 创建设备文件的命令
/dev/mycdev : 将设备文件创建到/dev目录下,默认设备文件存到/dev目录下,当然也可以放到其他目录下
c : 表示字符设备文件, 如果写b:表示块设备文件
236 : 主设备号
0 : 次设备号
4> 运行应用程序: sudo ./test
5> 使用dmesg查看驱动中的open read write close函数是否被调用,如下图所示
6> 卸载驱动 sudo rmmod mycdev
1.7 自己编写驱动代码的思路
1> 先写模块化驱动的三要素:入口,出口,许可证
2> 在入口函数中调用register_chrdev函数,函数需要什么就准备对应的变量
3> 在出口函数中调用unregister_chrdev函数
4> 编译驱动的程序,如果有错误进行修改,没有错误编写应用程序进行测试。
2、 用户空间和内核空间直接数据的拷贝
2.1 用户空间和内核空间数据拷贝的API
#include <linux/uaccess.h>
1. copy_to_user
unsigned long copy_to_user(void __user *to, const void *from,
unsigned long n)
功能:将内核空间中的数据拷贝到用户空间
参数:
@ to : 用户空间的地址
@ from : 内核空间的地址
@ n : 拷贝数据的大小
返回值:
成功返回0,失败返回未拷贝的数据的个数
2. copy_from_user
unsigned long copy_from_user(void *to, const void __user *from,
unsigned long n)
功能:将用户空间中的数据拷贝到内核空间
参数:
@ to : 内核空间的地址
@ from : 用户空间的地址
@ n : 拷贝数据的大小
返回值:
成功返回0,失败返回未拷贝的数据的个数
2.2 系统调用函数和驱动中的read/write对应关系
2.3 用户空间和内核空间数据拷贝的驱动代码实现
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define MYNAME "mycdev"
int major;
char kbuf[128] = "I am kernel spcae data!";
int mycdev_open (struct inode *inode, struct file *file)
{
printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
return 0;
}
ssize_t mycdev_read (struct file *file, char __user *ubuf, size_t size, loff_t *offset)
{
int ret;
printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
// 校准拷贝数据的大小
if (size > sizeof(kbuf)) size = sizeof(kbuf);
// 将内核空间的数据拷贝到用户空间
ret = copy_to_user(ubuf,kbuf,size);
if (ret) {
printk("kernel space copy data to user space failed!\n");
return -EINVAL;
}
// 成功返回拷贝成功的字节的个数
return size;
}
ssize_t mycdev_write (struct file *file, const char __user *ubuf, size_t size, loff_t *offset)
{
int ret;
printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
// 校准拷贝数据的大小
if (size > sizeof(kbuf)) size = sizeof(kbuf);
// 将内核空间的数据拷贝到用户空间
ret = copy_from_user(kbuf,ubuf,size);
if (ret) {
printk("user space copy data to kernel space failed!\n");
return -EINVAL;
}
printk("kernel space = %s\n", kbuf);
return size;
}
int mycdev_close (struct inode *inode, struct file *file)
{
printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
return 0;
}
struct file_operations fops = {
.open = mycdev_open,
.read = mycdev_read,
.write = mycdev_write,
.release = mycdev_close,
};
static int __init mycdev_init(void)
{
// 注册字符设备
major = register_chrdev(0, MYNAME, &fops);
if (major < 0) {
printk("register char device failed!\n");
return -EIO;
}
return 0;
}
static void __exit mycdev_exit(void)
{
unregister_chrdev(major, MYNAME);
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zk <zhouk_bj@hqyj.com>");
2.4 用户空间和内核空间数据拷贝应用层代码实现
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
char u_buf[128] = {0};
int main(int argc, const char *argv[])
{
/*your code*/
int fd;
fd = open("/dev/mycdev", O_RDWR);
if (fd < 0) {
perror("open");
return -1;
}
read(fd, u_buf, sizeof(u_buf));
printf("user space = %s\n", u_buf);
strcpy(u_buf, "I am user space data!");
write(fd, u_buf, sizeof(u_buf));
close(fd);
return 0;
}
2.5 代码测试
1> 加载驱动 : sudo insmod mycdev.ko
2> 查看驱动对应的主设备号 : cat /proc/devices, 需要查看自己的主设备号
3> 在/dev目录下创建设备文件 sudo mknod /dev/mycdev c 236 0
4> 运行应用程序:
linux@ubuntu:02mycdev$ sudo ./test
user space = I am kernel spcae data!
5> 使用dmesg查看驱动中的open read write close函数是否被调用,如下图所示
6> 卸载驱动 sudo rmmod mycdev
3、led灯驱动的实现
3.1 在内核空间能否操作特殊功能寄存器地址
特殊功能寄存器的地址是物理地址,而内核空间的地址是3-4G的虚拟地址,
如果直接在内核空间操作特殊功能寄存器的物理地址空间,会导致内核出错。
如果想在内核空间操作特殊功能寄存器的物理地址,需要将物理地址映射为虚拟地址,
在内核空间操作虚拟地址最终相当于操作了特殊功能寄存器的物理地址。
mmu(memory manager unit) : 由内存管理单元完成物理地址到虚拟地址的映射。
使用ioremap函数完成物理地址到虚拟地址的映射; 使用iounmap函数完成取消物理地址到虚拟地址的映射。
3.2 物理地址到虚拟地址映射的API
#include <linux/io.h>
1. ioremap
void *ioremap(phys_addr_t paddr, unsigned long size)
功能:完成物理地址到虚拟地址的映射
参数:
@ paddr : 物理地址
@ size : 映射空间的大小,以字节为单位
返回值:
成功返回虚拟的地址
失败返回NULL
2. iounmap
void iounmap(const void __iomem *addr)
功能:取消物理地址到虚拟地址的映射
参数:
@ addr : 虚拟地址
返回值:无
3.3 完成LED灯的驱动
3.3.1 分析led1灯的电路图
3.3.2 分析芯片手册
1. RCC_MP_AHB4ENSETR[4] ----> 0x50000A28
写1,使能GPIOE外设的时钟
2. GPIOx_MODER[21:20] ----> 0x50006000
写0b01, 设置为通用的输出模式
3. GPIOx_OTYPER[10] ----> 0x50006004
写0b0 , 设置为推挽输出
4. GPIOx_OSPEEDR[21:20] ----> 0x50006008
写0b00, 设置为低速模式
5. GPIOx_PUPDR[21:20] -----> 0x5000600C
写0b00,禁止上拉和下拉
6. GPIOx_ODR[10] ----> 0x50006014
写0, PE10输出低电平
写1, PE10输出高电平
3.3.3 编写led灯的驱动代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#define MYNAME "myled"
#define RCC_MP_AHB4ENSETR_PHY_ADDR 0x50000A28
#define GPIOE_MODER_PHY_ADDR 0x50006000
#define GPIOE_OTYPER_PHY_ADDR 0x50006004
#define GPIOE_OSPEEDR_PHY_ADDR 0x50006008
#define GPIOE_PUPDR_PHY_ADDR 0x5000600C
#define GPIOE_ODR_PHY_ADDR 0x50006014
volatile unsigned int * rcc_virl_addr;
volatile unsigned int * gpioe_moder_virl_addr;
volatile unsigned int * gpioe_otyper_virl_addr;
volatile unsigned int * gpioe_ospeedr_virl_addr;
volatile unsigned int * gpioe_pupdr_virl_addr;
volatile unsigned int * gpioe_odr_virl_addr;
int major;
char kbuf[128] = {0};
int myled_open (struct inode *inode, struct file *file)
{
printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
return 0;
}
ssize_t myled_read (struct file *file, char __user *ubuf, size_t size, loff_t *offset)
{
int ret;
printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
// 校准拷贝数据的大小
if (size > sizeof(kbuf)) size = sizeof(kbuf);
// 将内核空间的数据拷贝到用户空间
ret = copy_to_user(ubuf,kbuf,size);
if (ret) {
printk("kernel space copy data to user space failed!\n");
return -EINVAL;
}
// 成功返回拷贝成功的字节的个数
return size;
}
/*
需要指定一个led灯控制的协议
kbuf[0] = 1 : 表示控制LED1灯
kbuf[0] = 2 : 表示控制LED2灯
kbuf[0] = 3 : 表示控制LED3灯
kbuf[1] = 0 :表示熄灭LED灯
kbuf[1] = 1 :表示点亮LED灯
*/
ssize_t myled_write (struct file *file, const char __user *ubuf, size_t size, loff_t *offset)
{
int ret;
printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
// 校准拷贝数据的大小
if (size > sizeof(kbuf)) size = sizeof(kbuf);
// 将用户空间的数据拷贝到内核空间
ret = copy_from_user(kbuf,ubuf,size);
if (ret) {
printk("user space copy data to kernel space failed!\n");
return -EINVAL;
}
switch (kbuf[0]) {
case 1:
if (kbuf[1] == 1)
*gpioe_odr_virl_addr |= (0x1 << 10);
else
*gpioe_odr_virl_addr &= (~(0x1 << 10));
break;
case 2:
break;
case 3:
break;
default:
break;
}
return size;
}
int myled_close (struct inode *inode, struct file *file)
{
printk("%s:%s:%d\n", __FILE__, __func__,__LINE__);
return 0;
}
struct file_operations fops = {
.open = myled_open,
.read = myled_read,
.write = myled_write,
.release = myled_close,
};
// 完成led灯的初始化,
int led_init(void)
{
int ret = 0;
// 完成物理地址到虚拟地址的映射
rcc_virl_addr = ioremap(RCC_MP_AHB4ENSETR_PHY_ADDR, 4);
if (rcc_virl_addr == NULL)
{
printk("memory map failed!\n");
ret = -ENOMEM;
}
gpioe_moder_virl_addr = ioremap(GPIOE_MODER_PHY_ADDR, 4);
if (gpioe_moder_virl_addr == NULL)
{
printk("memory map failed!\n");
ret = -ENOMEM;
}
gpioe_otyper_virl_addr = ioremap(GPIOE_OTYPER_PHY_ADDR, 4);
if (gpioe_otyper_virl_addr == NULL)
{
printk("memory map failed!\n");
ret = -ENOMEM;
}
gpioe_ospeedr_virl_addr = ioremap(GPIOE_OSPEEDR_PHY_ADDR, 4);
if (gpioe_ospeedr_virl_addr == NULL)
{
printk("memory map failed!\n");
ret = -ENOMEM;
}
gpioe_pupdr_virl_addr = ioremap(GPIOE_PUPDR_PHY_ADDR, 4);
if (gpioe_pupdr_virl_addr == NULL)
{
printk("memory map failed!\n");
ret = -ENOMEM;
}
gpioe_odr_virl_addr = ioremap(GPIOE_ODR_PHY_ADDR, 4);
if (gpioe_odr_virl_addr == NULL)
{
printk("memory map failed!\n");
ret = -ENOMEM;
}
// 使用gpioe外设的时钟
*rcc_virl_addr = (0x1 << 4);
// 初始化GPIO引脚为输出,推挽输出,低俗模式,禁止上拉下拉,默认输出低电平
*gpioe_moder_virl_addr &= (~(0x3 << 20));
*gpioe_moder_virl_addr |= (0x1 << 20);
*gpioe_otyper_virl_addr &= (~(0x1 << 10));
*gpioe_ospeedr_virl_addr &= (~(0x3 << 20));
*gpioe_pupdr_virl_addr &= (~(0x3 << 20));
*gpioe_odr_virl_addr &= (~(0x1 << 10));
return ret;
}
// 取消led灯的初始化
void led_delInit(void)
{
iounmap(gpioe_odr_virl_addr);
iounmap(gpioe_pupdr_virl_addr);
iounmap(gpioe_ospeedr_virl_addr);
iounmap(gpioe_otyper_virl_addr);
iounmap(gpioe_moder_virl_addr);
iounmap(rcc_virl_addr);
}
static int __init myled_init(void)
{
int ret;
// 注册字符设备
major = register_chrdev(0, MYNAME, &fops);
if (major < 0) {
printk("register char device failed!\n");
return -EIO;
}
ret = led_init();
if (ret < 0) {
printk("led init failed!\n");
return ret;
}
return 0;
}
static void __exit myled_exit(void)
{
led_delInit();
unregister_chrdev(major, MYNAME);
}
module_init(myled_init);
module_exit(myled_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zk <zhouk_bj@hqyj.com>");
3.3.4 编写led灯的应用程序
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
char u_buf[128] = {0};
int main(int argc, const char *argv[])
{
/*your code*/
int fd;
fd = open("/dev/myled", O_RDWR);
if (fd < 0) {
perror("open");
return -1;
}
while(1) {
u_buf[0] = 1;
u_buf[1] = 1;
write(fd, u_buf, sizeof(u_buf));
sleep(1);
u_buf[1] = 0;
write(fd, u_buf, sizeof(u_buf));
sleep(1);
}
close(fd);
return 0;
}
3.3.5 测试led灯的驱动和应用程序
1> 编译led灯的驱动及应用程序:make arch=arm modname=myled all
2> 拷贝驱动程序及led灯的应用程序到跟文件系统中:make install
3> 完成开发板开发阶段的系统的启动
4> 加载驱动 :
[root@fsmp1a /] insmod myled.ko
5> 查看驱动对应的主设备号 :
[root@fsmp1a /] cat /proc/devices, 需要查看自己的主设备号
6> 在/dev目录下创建设备文件
[root@fsmp1a /] mknod /dev/myled c 242 0
7> 运行应用程序:
[root@fsmp1a /] ./test
8> 观看led灯的现象
9> 卸载驱动
[root@fsmp1a /] rmmod mycdev
;
}
while(1) {
u_buf[0] = 1;
u_buf[1] = 1;
write(fd, u_buf, sizeof(u_buf));
sleep(1);
u_buf[1] = 0;
write(fd, u_buf, sizeof(u_buf));
sleep(1);
}
close(fd);
return 0;
}
3.3.5 测试led灯的驱动和应用程序
1> 编译led灯的驱动及应用程序:make arch=arm modname=myled all
2> 拷贝驱动程序及led灯的应用程序到跟文件系统中:make install
3> 完成开发板开发阶段的系统的启动
4> 加载驱动 :
[root@fsmp1a /] insmod myled.ko
5> 查看驱动对应的主设备号 :
[root@fsmp1a /] cat /proc/devices, 需要查看自己的主设备号
6> 在/dev目录下创建设备文件
[root@fsmp1a /] mknod /dev/myled c 242 0
7> 运行应用程序:
[root@fsmp1a /] ./test
8> 观看led灯的现象
9> 卸载驱动
[root@fsmp1a /] rmmod mycdev