编写驱动程序,首先要了解是什么类型的设备。linux下的设备分为三类,分别为:字符设备,块设备和网络设备。字符设备类型是根据是否以字符流为数据的交换方式,大部分设备都是字符设备,如键盘,串口等,块设备则是以块为单位进行管理的设备,如,磁盘。网络设备就是网卡等。
其次要了解应用程序和驱动程序的区别,两者的主要区别分为以下三点:
1.入口函数的任务不相同,应用程序完成一个任务,驱动只完成初始化工作,比如中断申请,寄存器设置,定时器设置。
2.运行时的cpu模式不相同,驱动具有很高的权限,应用程序是在用户态下运行,而驱动程序是在内核态下执行。
3. 驱动程序不能调用C库函数,只能使用内核为驱动程序提供的一些函数。
鉴于以上区别,驱动程序需要完成以下三点基本功能:
1:要对设备进行初始化和释放功能模块,就如上面的寄存器设置,中断的申请,向内核注册驱动程序(register_chrdev()),卸载驱动程序(unregister_chrdev())。
2:能进行数据传输,在read(),write()函数里具体实现,数据传输工作。
3:能进行控制操作,给用户提供的ioctl()函数里可实现一些用户的选择性设置功能。
模块的编译、安装、卸载:
#include <linux/module.h>
#include <linux/init.h>
int __init led_init (void)
{
printk ("led init\n");
return 0;
}
void __exit led_exit (void)
{
printk ("module exit\n");
return ;
}
module_init(led_init); //模块初始化
module_exit(led_exit); //模块退出
MODULE_LICENSE("GPL"); //许可证
Makefile文件:
KVERS = $(shell uname -r)
obj-m := gpio_driver_linux.o
build: kernel_modules
kernel_modules:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
clean:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean
make:
安装模块:
验证成功:
卸载模块:
裸机GPIO驱动:
代码:
#define reg_gpio_ctrl *(volatile int *)(To_Virtual(GPIO_REG_CTRL))
#define reg_gpio_data *(volatile int *)(To_Virtual(GPIO_REG_DATA))
//将物理地址映射为虚拟地址 ioremap() iounmap() linux/io.h
//volatile 编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问
//led 初始化
void led_init(void) {
reg_gpio_ctrl |= (1 << n); //设置io口输出
}
void led_on(void) {
reg_gpio_data |= (1 << n); //输出高电平
}
void led_off(void) {
reg_gpio_data &= ~(1 << n); //输出低电平
}
Linux下GPIO驱动:
代码:
#include <linux/module.h> //MODULE_LICENSE
#include <linux/init.h> //module_init
#include <linux/types.h> //dev_t
#include <linux/kdev_t.h> //dev_T
#include <linux/fs.h> //inode file_operations
#include <linux/device.h>//device_creat
#include <linux/cdev.h>
#include <asm/uaccess.h> //copy_from_user copy_to_user
#include <linux/io.h> //ioremap iounmap 物理地址转虚拟地址
#define LED_MAJOR 240
int led_major = LED_MAJOR; //主设备号
struct cdev *led_cdev;
struct class *led_cdev_class;
int led_open(struct inode *inode, struct file *filp) {
printk("open led\n");
return 0;
}
//read和write方法完成的任务是相似的,即,拷贝数据到应用程序空间,或者反过来从应用程序空间拷贝数据;
ssize_t led_read(struct file *filp, //其中参数filp是文件指针,
char __user *buf, //参数buff指向用户空间的缓冲区,这个缓冲区是一个存放新读入数据的空缓冲区;
size_t count, //参数count是请求传输的数据长度,
loff_t *f_pos) { //offp指明了用户在文件中进行存放操作的位置;
printk("read led\n");
return 1; //读取成功返回读取的字节数
}
ssize_t led_write(struct file *filp, //其中参数filp是文件指针,
const char __user *buf, //参数buff指向用户空间的缓冲区,这个缓冲区保存将要写入的数据
size_t conut, //参数count是请求传输的数据长度,
loff_t *f_pos) {//offp指明了用户在文件中进行存放操作的位置;
printk("write led\n");
return 1;
}
int led_release(struct inode *inode, struct file *filp) {
printk("release led\n");
return 0;
}
int led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
printk("ioctl led\n");
return 0;
}
struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.unlocked_ioctl = led_ioctl,
.release = led_release,
};
int led_init(void) {
printk("module_init led\n");
int result;
dev_t devid = MKDEV(led_major, 0); //MKDEV是将主设备号和次设备号转换成dev_t类型的一个内核函数。 参数:主设备号, 次设备号
//申请字符设备的设备号
if(led_major) {
//静态申请,通过设定的设备号初值申请
result = register_chrdev_region(devid, 1, "LED");
//第一个参数:要分配的设备编号范围的初始值(次设备号常设为0)
//第二个参数:连续编号范围
//第三个参数:编号相关联的设备名称. //LED最终会显示在dev/目录下
} else {
//动态申请设备号,申请后放到dev
result = alloc_chrdev_region(&devid, 0, 1, "LED");
//第一个参数:用来存放申请的设备号
//第二个参数:是请求的最小的次编号
//第三个参数:是请求的连续设备编号的总数
//第四个参数:设备名
led_major = MAJOR(devid); //从设备号中获取主设备号 //MINOR():获取次设备号
}
if(led_major < 0)
return result;
led_cdev = cdev_alloc();
if(led_cdev != NULL) {
cdev_init(led_cdev, &led_fops);
led_cdev->owner = THIS_MODULE;
if(cdev_add(led_cdev, devid, 1) != 0) {
//cdev_add就是将我们之前获得设备号和设备号长度填充到cdev结构中;
//参数1:字符结构体,参数2:设备号,参数3:个数
goto error;
}
} else {
return -1;
}
led_cdev_class = class_create(THIS_MODULE, "led_cdev_class");
if(IS_ERR(led_cdev_class)) {
return -1;
}
device_create(led_cdev_class, NULL, devid, NULL, "LED", NULL);
return 0;
error:
unregister_chrdev_region(devid, 1);
return result;
}
void led_exit(void) {
printk("module_exit led\n");
cdev_del(led_cdev);
unregister_chrdev_region(MKDEV(led_major, 0), 1);
class_destroy(led_cdev_class);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL"); //许可
结果:
模块安装成功:
设备号申请成功:
设备创建成功:
运行测试程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <fcntl.h>
int main() {
int led_fd = open("/dev/LED", O_RDWR);
if(led_fd < 0) {
perror("open\n");
return -1;
}
int data;
int ret = read(led_fd, &data, 1);
if(!ret) {
perror("read\n");
return -1;
}
data = 0;
ret = write(led_fd, &data, 1);
if(!ret) {
perror("write\n");
return -1;
}
ret = ioctl(led_fd, NULL, NULL);
if(!ret) {
perror("ioctl\n");
return -1;
}
close(led_fd);
return 0;
}
结果:
裸机GPIO驱动与LinuxGPIO驱动的区别:
裸机GPIO驱动:
- 裸机驱动一般针对没有操作系统支持的层面,不用考虑操作系统对它的调用.
- 底层,跟寄存器打交道,有些MCU提供了库
Linux下GPIO驱动:
- linux下驱动开发直接操作寄存器,不现实
- 根据Linux下的各种驱动框架,进行开发;一定要满足框架,也就是linux下各种驱动框架的掌握;
- 在linux下,驱动最终表现(对应的外设)是 /dev/xxx下的文件;打开、关闭、读写都可以