Linux的外设主要分为三类:字符设备(character device)、块设备(block device)、网络接口(network interface)。
字符设备是能像字节流一样读写操作的设备,也就是说对它的读写是以字节为单位。比如串口的收发数据,另外常见的点灯、按键、IIC、SPI、LCD的驱动都是字符设备。
如下图为Linux系统层级关系图,在Linux系统下开发字符设备驱动需要按照该框架编写程序。
其中,开发字符设备驱动程序只需要关注应用程序和驱动程序层。应用程序和驱动程序层连接靠内核,我们先不深究其原理。在应用程序中,用户可以调用open、read、wirte和ioctl等函数,程序会自动调用相对应的库函数,接着程序会通过系统调用方式进入内核,内核程序调用相应的驱动程序open、read、write、ioctl函数,从而驱动硬件设备。
开发字符设备驱动之前,需要注册一个字符设备驱动,相反的,删除一个字符设备,需要注销,其调用的函数原型如下:
/* 函数:register_chrdev()
* 描述:注册一个字符设备
* 参数:major:主设备号
* name:设备名字,指向一串字符串
* fops:结构体file_operations类型指针,指向设备的操作函数集合变量
* 返回:
*/
static inline int register_chrdev(unsigned int major,
const char *name,
const struct file_operations *fops)
/* 函数:unregister_chrdev()
* 描述:注销一个字符设备
* 参数:major:主设备号
* name:设备名字,指向一串字符串
* 返回:
*/
static inline void unregister_chrdev(unsigned int major,
const char *name)
说明:
主设备号:每个字符设备都有一个主设备号,通过命令:“cat /proc/devices”可以查看当前使用的设备号。
在字符驱动框架中,需要定义一个file_operations结构体,该结构体是字符设备的操作函数集合。
Linux驱动有两种方式运行,一种是编译进内核,当内核运行时,会自动运行驱动;另一种是将驱动编译成模块(扩展名为:.ko),内核启动后可以使用insmod或modprobe加载驱动模块。
在上面,通过函数register_chrdev()注册了字符设备后,我们需要告诉内核有这个字符设备,即将模块加载到内核中,利用函数module_init()。模块的卸载相反,告诉内核删除这个字符模块,利用函数module_exit(),模块的加载和卸载函数如下:
module_init(xxx_init);
module_exit(xxx_exit);
注册或者卸载字符模块时,函数会调用驱动入口函数xxx_init()和驱动出口函数xxx_exit(),这两个函数的原型如下:
/* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
}
说明:
开发板:JZ2440V3
Linux内核版本:2.6.22.6
编译器:arm-linux-gcc-3.4.5-glibc-2.3.6
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define DEVICE_NAME "chrleds" /* 加载模式后,执行”cat /proc/devices”命令看到的设备名称 */
#define LED_MAJOR 251 /* 主设备号 */
static struct class *leds_class;
static struct class_device *leds_class_dev;
/* 寄存器物理地址
* JZ2440V3开发板上,D10->GPF4;D11->GPF5;D12->GPF6
*/
#define GPACON_BASE (0x56000000)
#define GPADAT_BASE (0x56000004)
#define GPFCON_BASE (GPACON_BASE + 0X50)
#define GPFDAT_BASE (GPADAT_BASE + 0X50)
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *GPFCON;
static void __iomem *GPFDAT;
/* 函数:led_open()
* 描述:应用程序执行open(...)时,就会调用该函数
* 参数:inode:传递给驱动的inode
* filp:设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* 返回: 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
int val;
// 配置GPF3引脚为输出
val = readl(GPFCON);
val &= ~(0x3<<(4*2));
val |= (1<<(4*2));
writel(val, GPFCON);
// 配置GPF4引脚为输出
val = readl(GPFCON);
val &= ~(0x3<<(5*2));
val |= (1<<(5*2));
writel(val, GPFCON);
// 配置GPF5引脚为输出
val = readl(GPFCON);
val &= ~(0x3<<(6*2));
val |= (1<<(6*2));
writel(val, GPFCON);
// 都输出0
val = readl(GPFDAT);
val &= ~(1<<4);
val &= ~(1<<5);
val &= ~(1<<6);
writel(val, GPFDAT);
return 0;
}
/* 函数:led_read()
* 描述:应用程序执行read(...)时,就会调用该函数,从设备读取数据
* 参数:filp:要打开的设备文件(文件描述符)
* buf:返回给用户空间的数据缓冲区
* cnt:要读取的数据长度
* offt:相对于文件首地址的偏移
* 返回:读取的字节数,如果为负值,表示读取失败
*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int err;
err = copy_to_user(buf, (const void *)&leds_status, 1);
if(err < 0) {
printk("kernel read failed!\r\n");
return -EFAULT;
}
return 0;
}
/* 函数:led_write()
* 描述:应用程序执行write(...)时,就会调用该函数,向设备写数据
* 参数:filp:设备文件,表示打开的文件描述符
* buf:要写给设备写入的数据
* cnt:要写入的数据长度
* offt:相对于文件首地址的偏移
* 返回:写入的字节数,如果为负值,表示写入失败
*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
char ledstat;
int val;
char err;
err = copy_from_user(&ledstat, buf, 1);
if(err < 0)
{
printk("kernel write failed!\r\n");
return -EFAULT;
}
printk("%d \r\n",ledstat);
if(ledstat == 0x0)
{
/* 关闭所有LED灯 */
val = readl(GPFDAT);
val |= (1 << 4);
val |= (1 << 5);
val |= (1 << 6);
writel(val, GPFDAT);
printk("Close All leds!\r\n");
}
else
{
/* 打开所有LED灯 */
val = readl(GPFDAT);
val &= ~(1 << 4);
val &= ~(1 << 5);
val &= ~(1 << 6);
writel(val, GPFDAT);
printk("Open All leds!\r\n");
}
return 0;
}
/*
* 这个结构是字符设备驱动程序的核心
* 当应用程序操作设备文件时所调用的open、read、write等函数
* 最终会调用这个结构中指定的对应函数
*/
static struct file_operations chrled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
};
/* 函数:led_init()
* 描述:执行insmod命令时就会调用这个函数
* 参数:无
* 返回:无
*/
static int __init led_init(void)
{
int val = 0;
/* 1、寄存器地址映射 */
GPFCON = ioremap(GPFCON_BASE, 4);
if (!GPFCON) {
return -EIO;
}
GPFDAT = ioremap(GPFDAT_BASE, 4);
if (!GPFDAT) {
return -EIO;
}
/* 注册字符设备
* 参数为主设备号、设备名字、file_operations结构;
* 这样,主设备号就和具体的file_operations结构联系起来了,
* 操作主设备为LED_MAJOR的设备文件时,就会调用s3c24xx_leds_fops中的相关成员函数
* LED_MAJOR可以设为0,表示由内核自动分配主设备号
*/
val = register_chrdev(LED_MAJOR, DEVICE_NAME, &chrled_fops);
if (val < 0) {
printk(DEVICE_NAME " can't register major number\n");
return val;
}
/* 新建一个类 */
leds_class = class_create(THIS_MODULE, DEVICE_NAME);
if (IS_ERR(leds_class))
return PTR_ERR(leds_class);
/* 在类下创建一个设备 */
leds_class_dev = class_device_create(leds_class, NULL, MKDEV(LED_MAJOR, 0), NULL,DEVICE_NAME);
if (unlikely(IS_ERR(leds_class_dev)))
return PTR_ERR(leds_class_dev);
printk(DEVICE_NAME " initialized!\n");
return 0;
}
/* 函数:led_exit()
* 描述:执行rmmod命令时就会调用这个函数
* 参数:无
* 返回:无
*/
static void __exit led_exit(void)
{
/* 卸载驱动程序 */
unregister_chrdev(LED_MAJOR, DEVICE_NAME);
class_device_unregister(leds_class_dev);
class_destroy(leds_class);
/* 取消映射 */
iounmap(GPFCON);
iounmap(GPFDAT);
}
/* 指定驱动程序的初始化函数和卸载函数 */
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
/* 描述驱动程序的一些信息,不是必须的 */
MODULE_AUTHOR("https://me.csdn.net/qq_31782183");
MODULE_VERSION("1.0.0");
MODULE_DESCRIPTION("S3C2410/S3C2440 LED Driver");
在驱动程序开发中,不能直接操作物理地址,而是用虚拟地址映射到物理地址,程序中操作虚拟地址,利用函数ioremap()完成映射;相反,解除映射关系利用函数iounmap(),函数的原型如下:
#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)
/* 函数:__arm_ioremap()
* 描述:完成虚拟地址到物理地址的映射
* 参数:phys_addr:物理地址起始地址
* size:要映射内存的空间大小
* mtype:ioremap类型 MT_DEVICE
* MT_DEVICE_NONSHARED
* MT_DEVICE_CACHED
* MT_DEVICE_WC
* 返回:虚拟地址的起始地址
*/
void __iomem * __arm_ioremap(phys_addr_t phys_addr,
size_t size,
unsigned int mtype)
{
return arch_ioremap_caller(phys_addr, size, mtype,
__builtin_return_address(0));
}
/* 函数:iounmap()
* 描述:完成虚拟地址到物理地址的映射
* 参数:addr:虚拟地址起始地址
* 返回:无
*/
void iounmap (volatile void __iomem *addr)
使用函数ioremap()将寄存器的物理地址映射到虚拟地址后,我们就可以通过指针访问虚拟地址,从而访问物理地址,建议使用如下函数:
读函数:
u8 readb(const volatile void __iomem *addr) /* 对应8bit写操作 */
u16 readw(const volatile void __iomem *addr) /* 对应16bit写操作 */
u32 readl(const volatile void __iomem *addr) /* 对应32bit写操作 */
写函数:
/* value:写入的数值
* addr:写入的地址(虚拟地址)
*/
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)
#include
#include
#include
#include
/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char **argv)
{
int fd;
char *filename;
char dat[1];
if(argc != 3)
{
printf("Please input:\n");
printf("%s /dev/chrleds on\n", argv[0]);
printf("%s /dev/chrleds off\n", argv[0]);
return 0;
}
filename = argv[1];
/* 打开led驱动 */
fd = open(filename, O_RDWR);
if(fd < 0)
{
printf("file %s open failed!\r\n", filename);
return 0;
}
if(!strcmp("on",argv[2]))
{
dat[0] = 1;
write(fd, dat, 1);
}
else if (!strcmp("off", argv[2]))
{
dat[0] = 0;
write(fd, dat, 1);
}
else
{
printf("Please input:\n");
printf("%s /dev/chrleds on\n", argv[0]);
printf("%s /dev/chrleds off\n", argv[0]);
}
return 0;
}
内容如下:
KERNELDIR := /home/lib/linux/systemcode/linux-2.6.22.6
CURRENT_PATH := $(shell pwd)
obj-m := chrleds.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
rm -rf modules.order
make
编译完成,会生成chrleds.ko的驱动模块文件。
arm-linux-gcc chrledsApp.c -o chrledsApp
编译完成后,会生成chrledsApp应用程序。
mount -t nfs -o tcp,nolock 192.168.1.1:/home/lib/linux/nfs/fs_mini_mdev /mnt
启动开发板后,执行该语句,将自己的nfs系统挂接,方便测试。
将chrleds.ko和chrledsApp两个文件拷贝到挂接系统的/lib/modules/2.6.22.6 目录下,输入以下命令:
insmod chrleds.ko
驱动加载成功后,会创建"/dev/chrleds"设备节点。
./chrledsApp /dev/chrleds on /* 打开LED灯 */
或
./chrledsApp /dev/chrleds off /* 关闭LED灯 */
如果要卸载模块,输入如下命令:
rmmod chrleds.ko