一、裸机驱动开发流程
所谓裸机在这里主要是指系统软件平台没有用到操作系统。在基于ARM处理器平台的软件设计中,如果整个系统只需要完成一个相对简单而且独立的任务,那么可以不使用操作系统,只需要考虑在平台上如何正确地执行这个单任务程序。不过,在这种方式下同样需要一个Boot Loader,这个时候的Boot Loader一般是自己写的一个简单的启动代码加载程序。大家所熟悉的各种Boot Loader下的设备驱动,其实就是很好的裸机驱动程序。比如说U-Boot下的网卡驱动、串口驱动、LCD驱动等。
在裸机方式下,ARM的软件集成开发环境就显得极为重要,因为在这种方式下可以把所有代码都放在这个环境里面编写、编译和调试。在这种方式下测试驱动程序,首先要完成CPU的初始化,然后把需要测试的程序装载到系统的RAM区/或者SDRAM中。当然,如果需要处理一些复杂的中断处理的话,最好也把CPU的复位向量表放到RAM区中。把所有程序都调试好之后,再把最后的程序烧写到Flash里面去执行。
所以裸机驱动的开发相比于在Linux设备树系统下开发要麻烦的多,对技术人员要求也要高上很多,所以我也没写过什么复杂的裸机驱动,就一些简单的GPIO、中断、时钟什么的,大概流程就是:
1、熟悉外设;
2、使用外设所需要的引脚;
3、参考开发手册配置相应的寄存器;
4、驱动烧写;
5、测试;
二、Linux下DTB(设备树)驱动框架
Linux 操作系统的驱动与裸机上的驱动有很多不同:
- 具有分层结构
1、用户程序在用户模式下,而内核、驱动在sys管理模式下,裸机是在同一个模式下运行
2、用户程序与内核程序操纵的地址都是虚拟地址 - 考虑多用户,并发性
1、多个程序都在访问同一个设备(串口),它们具有互斥访问的特性,同一时间只有一个程序能占用设备 - 考虑协议
1、不同设备具有不同的驱动,但是协议是一成不变的,因此需要给应用程序提供一个通用的接口,这样驱动代码可以变,但协议的接口是不变的
Linux中的三大类驱动:字符设备驱动、块设备驱动和网络设备驱动,三类驱动的调用框架是一样的,即:应用程序对设备进行操作时,只能通过库中的函数,这个函数就会进入内核,然后内核调用驱动,驱动再操作设备。
三、字符设备驱动
字符设备驱动是Linux 驱动中最基本的一类设备驱动,字符设备就是指在I/O传输过程中以字符为单位进行传输的设备,读写数据是分先后顺序的。比如GPIO、IIC、SPI,LCD等都属于字符设备。
3.1 字符驱动开发框架
在裸机上,驱动是直接对寄存器进行操作,在Linux下也是对寄存器的操作,但是Linux下驱动的编写需要符合Linux的驱动框架,Linux驱动开发重点是了解其驱动框架。
整个Linux驱动开发的流程框架就是:
- 1、入口函数 module_init 挂载;
- 2、register_chrdev 注册字符设备;
- 3、alloc_chrdev_region 动态注册设备号;
- 4、file_operations 设备操作函数编写(重点);
- 5、unregister_chrdev_region 动态注销设备号;
- 6、unregister_chrdev 注销字符设备;
- 7、出口函数module_exit 卸载;
- 额外的,需要指出许可MODULE_LICENSE,否则编译无法通过,作者MODULE_AUTHOR可以不写。
Linux内核中结构体 file_operations,集合了 Linux内核字符设备驱动操作函数,内容如下所:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
结构体中比较常用的几个成员:
- owner拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
- llseek函数用于修改文件当前的读写位置。
- read函数用于读取设备文件。
- write函数用于向设备文件写入 (发送 )数据 。
- poll是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
- unlocked_ioctl函数提供对于设备的控制功能,与应用程序中的 ioctl函数对应。
- compat_ioctl函数与 unlocked_ioctl函数功能一样,区别在于在 64位系统上,32位的应用程序调用将会使用此函数。在 32位的系统上运行 32位的应用程序调用的是unlocked_ioctl。
- mmap函数用于将将设备的内存映射到进程空间中 (也就是用户空间 ),一般帧缓冲设备会使用此函数,比如 LCD驱动的显存,将帧缓冲 (LCD显存 )映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
- open函数用于打开设备文件。
- release函数用于释放 (关闭 )设备文件,与应用程序中的 close函数对应。
- fasync函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
- aio_fsync函数与 fasync函数的功能类似,只是 aio_fsync是异步刷新待处理的数据。
3.2 设备树
Device Tree 起源于 OpenFirmware (OF),是一种采用树形结构描述硬件的数据结构,由一系列被命名的结点(node)和属性(property)组成,结点本身可包含子结点,主要由三大部分组成:头(Header)、结构块(Structure block)、字符串块(Strings block)。
- 头:主要描述设备树的一些基本信息,例如设备树大小,结构块偏移地址,字符串块偏移地址等。偏移地址是相对于设备树头的起始地址计算的。
- 结构块:一个线性化的结构体,是设备树的主体,以节点node的形式保存了目标单板上的设备信息。
- 字符串块:通过节点的定义知道节点都有若干属性,而不同的节点的属性又有大量相同的属性名称,因此将这些属性名称提取出一张表,当节点需要应用某个属性名称时直接在属性名字段保存该属性名称在字符串块中的偏移量。
设备树的编写是通过DTS文件来编写的,这里不讲DTS的语法,DTS的结构图如下:
树的主干就是系统总线,IIC 控制器、GPIO 控制器、SPI 控制器等都是接到系统主线上的分支。IIC 控制器有分为IIC1 和IIC2 两种,其中IIC1 上接了FT5206 和AT24C02这两个IIC 设备,IIC2 上只接了MPU6050 这个设备。DTS 文件的主要功能就是按照图中所示的结构来描述板子上的设备信息。
3.4 驱动示例
这是一个控制gpio的驱动代码,内容没什么用,用来了解一下开发流程即可,驱动代码:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/ide.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define CAMERA_CNT 1
#define CAMERA_NAME "camera"
#define TAKEPICTURE 1
struct camera_dev{
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
int major;
int minor;
struct device_node *nd;
int focus_gpio;
int shutter_gpio;
};
struct camera_dev camera;
static int camera_open(struct inode *inode, struct file *filp)
{
filp->private_data = &camera;
return 0;
}
static ssize_t camera_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
static ssize_t camera_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char gpiostat;
struct camera_dev *dev = filp->private_data;
retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
gpiostat = databuf[0];
if(gpiostat == TAKEPICTURE) {
gpio_set_value(dev->focus_gpio, 1);
msleep(500);
gpio_set_value(dev->shutter_gpio, 1);
msleep(500);
gpio_set_value(dev->focus_gpio, 0);
gpio_set_value(dev->shutter_gpio, 0);
}
else if(gpiostat == 2)
{
gpio_set_value(dev->focus_gpio, 1);
gpio_set_value(dev->shutter_gpio, 1);
}
else
{
gpio_set_value(dev->focus_gpio, 0);
gpio_set_value(dev->shutter_gpio, 0);
}
return 0;
}
static int camera_release(struct inode *inode, struct file *filp)
{
return 0;
}
static struct file_operations camera_fops = {
.owner = THIS_MODULE,
.open = camera_open,
.read = camera_read,
.write = camera_write,
.release = camera_release,
};
static int __init camera_init(void)
{
int ret = 0;
camera.nd = of_find_node_by_path("/camera");
if(camera.nd == NULL) {
printk("camera node not find!\r\n");
return -EINVAL;
} else {
printk("camera node find!\r\n");
}
camera.focus_gpio = of_get_named_gpio(camera.nd, "focus-gpio", 0);
camera.shutter_gpio = of_get_named_gpio(camera.nd, "shutter-gpio", 0);
if(camera.focus_gpio < 0) {
printk("can't get focus-gpio");
return -EINVAL;
}
if(camera.shutter_gpio < 0) {
printk("can't get shutter-gpio");
return -EINVAL;
}
printk("focus-gpio num = %d, shutter-gpio num = %d\r\n", camera.focus_gpio, camera.shutter_gpio);
ret = gpio_direction_output(camera.focus_gpio, 0);
ret = gpio_direction_output(camera.shutter_gpio, 0);
if(ret < 0) {
printk("can't set gpio!\r\n");
}
if (camera.major) {
camera.devid = MKDEV(camera.major, 0);
register_chrdev_region(camera.devid, CAMERA_CNT, CAMERA_NAME);
} else {
alloc_chrdev_region(&camera.devid, 0, CAMERA_CNT, CAMERA_NAME);
camera.major = MAJOR(camera.devid);
camera.minor = MINOR(camera.devid);
}
printk("camera major=%d,minor=%d\r\n",camera.major, camera.minor);
camera.cdev.owner = THIS_MODULE;
cdev_init(&camera.cdev, &camera_fops);
cdev_add(&camera.cdev, camera.devid, CAMERA_CNT);
camera.class = class_create(THIS_MODULE, CAMERA_NAME);
if (IS_ERR(camera.class)) {
return PTR_ERR(camera.class);
}
camera.device = device_create(camera.class, NULL, camera.devid, NULL, CAMERA_NAME);
if (IS_ERR(camera.device)) {
return PTR_ERR(camera.device);
}
return 0;
}
static void __exit camera_exit(void)
{
cdev_del(&camera.cdev);
unregister_chrdev_region(camera.devid, CAMERA_CNT);
device_destroy(camera.class, camera.devid);
class_destroy(camera.class);
}
module_init(camera_init);
module_exit(camera_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("MWBW");
在写驱动的时候,有一个步骤是注册设备,这里的设备信息获取都是来自设备树,因此,驱动代码的编写只是一部分,另外还需要在对应的设备树文件中加入相应的设备节点,这个节点描述的信息要含有驱动所需要的信息,并配置引脚相应的电气属性。例如:
camera {
#address-cells = <1>;
#size-cells = <1>;
compatible = "jyaitech-camera";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_camera>;
focus-gpio = <&gpio1 5 GPIO_ACTIVE_LOW>;
shutter-gpio = <&gpio1 9 GPIO_ACTIVE_LOW>;
status = "okay";
};
pinctrl_camera: cameragrp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO05__GPIO1_IO05 0x10B0
MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x10B0
>;
};
3.4 驱动的分离与分层
在写一些简单的字符设备驱动的时候,可以按照上面驱动,直接写死,但是面对复杂的设备,像IIC、SPI、LCD等,就应该考虑下驱动的分离与分层,这样可以极大提高驱动的可重用性。
这是简单的驱动与设备的关系,是一对一的,也就是每有一个硬件就会有一个对应的设备,很明显这样很容易造成一个系统驱动垃圾堆,所以复杂的驱动一般用下面的方式:
每个设备控制器都提供一个统一的接口(也叫做主机驱动),每个设备的话也只提供一个驱动程序(设备驱动),每个设备通过统一的接口驱动来访问,这样就可以大大简化驱动文件。
四、块驱动
4.1 块驱动与字符驱动区别
块设备驱动块设备是针对存储设备的驱动,比如 SD卡、 EMMC、 NAND Flash、 Nor Flash、 SPI Flash、机械硬盘、固态硬盘等,驱动要远比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统。块设备驱动相比字符设备驱动的主要区别如下:
- 块设备只能以块为单位进行读写访问,块是 linux虚拟文件系统 (VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。
- 块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后在一次性将缓冲区中的数据写入块设备中。
- 字符设备是顺序的数据流设备,字符设备是按照字节进行读写访问的。字符设备不需要缓冲区,对于字符设备的访问都是实时的,而且也不需要按照固定的块大小进行访问。
- 块设备结构的不同其 I/O算法也会不同,比如对于 EMMC、 SD卡、 NAND Flash这类没有任何机械设备的存储设备就可以任意读写任何的扇区 (块设备物 理存储单元 )。但是对于机械硬盘这样带有磁头的设备,读取不同的盘面或者磁道里面的数据,磁头都需要进行移动,因此对于机械硬盘而言,将那些杂乱的访问按照一定的顺序进行排列可以有效提高磁盘性能, linux里面针对不同的存储设备实现了不同的 I/O调度算法。
4.2 块驱动开发框架
与字符设备开发框架大致类似,区别在于:
- 字符设备驱动我们注册的是字符设备,而块驱动注册的是块设备,注册函数:register_blkdev,注销函数:unregister_blkdev
- 块设备需要申请gendisk并对其进行配置,gendisk是一个描述磁盘设备的结构体,具体内容如下:
struct gendisk {
int major;
int first_minor;
int minors;
char disk_name[DISK_NAME_LEN];
char *(*devnode)(struct gendisk *gd, umode_t *mode);
unsigned int events;
unsigned int async_events;
struct disk_part_tbl __rcu *part_tbl;
struct hd_struct part0;
const struct block_device_operations *fops;
struct request_queue *queue;
void *private_data;
int flags;
struct device *driverfs_dev;
struct kobject *slave_dir;
struct timer_rand_state *random;
atomic_t sync_io;
struct disk_events *ev;
#ifdef CONFIG_BLK_DEV_INTEGRITY
struct blk_integrity *integrity;
#endif
int node_id;
};
- 块设备结构体没有与字符设备一样的read、write这样的读写操作函数,有的操作函数如下:
struct block_device_operations {
int (*open) (struct block_device *, fmode_t);
void (*release) (struct gendisk *, fmode_t);
int (*rw_page)(struct block_device *, sector_t, struct page *, int rw);
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
long (*direct_access)(struct block_device *, sector_t,
void **, unsigned long *pfn, long size);
unsigned int (*check_events) (struct gendisk *disk,
unsigned int clearing);
int (*media_changed) (struct gendisk *);
void (*unlock_native_capacity) (struct gendisk *);
int (*revalidate_disk) (struct gendisk *);
int (*getgeo)(struct block_device *, struct hd_geometry *);
void (*swap_slot_free_notify) (struct block_device *, unsigned long);
struct module *owner;
};
- 对磁盘的配置过程:
1、申请gendisk : struct gendisk *alloc_disk(int minors);
2、将 gendisk 添加到内核 : void add_disk(struct gendisk *disk);
3、设置 gendisk 容量 : void set_capacity(struct gendisk *disk, sector_t size);
4、引用计数的调整:
增加:truct kobject *get_disk(struct gendisk *disk)
减少:void put_disk(struct gendisk *disk)
5、删除gendisk: void del_gendisk(struct gendisk *gp)
4.3 块设备读写操作
在上一节提到,块设备结构体没有与字符设备一样的read、write这样的读写操作函数,但它是通过请求队列 request_queue、请求 request 和 bio 结构这些操作进行读写的。
4.3.1 请求队列 request_queue
1、首先申请并初始化一个 request_queue,然后在初始化 gendisk 的时候将request_queue 地址赋值给 gendisk 的 queue 成员变量。request_queue 的申请与初始化通过使用 blk_init_queue 函数完成。
2、分配请求队列并绑定制造请求函数。blk_init_queue 函数其实以及完成了这个操作,但是面对非机械设备,先通过struct request_queue *blk_alloc_queue (gfp_t gfp_mask)函数请求设备,再通过void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)函数绑定制造请求。
3、删除请求队列:void blk_cleanup_queue(struct request_queue *q)
4.3.2 请求request
对请求的处理很简单:获取、开启、对应函数分别为:request *blk_peek_request(struct request_queue *q)、void blk_start_request(struct request *req)、与请求相关的API函数:
函数 | 描述 |
---|
blk_end_request | 请求中指定字节数据被处理完成 |
blk_end_request_all | 请求中所有数据全部处理完成 |
blk_end_request_cur | 当前请求中的 chunk |
blk_end_request_err | 处理完请求,直到下一个错误产生 |
4.3.3 bio结构
bio 是request结构体里面的一个成员,每个 request 里面里面会有多个 bio,bio 保存着最终要读写的数据、地址等信息。
五、网络驱动
5.1 嵌入式网络结构
嵌入式网络硬件分为两部分:MAC 和 PHY,如果芯片支持网络,那么一般是指内部含有MAC,如果没有只能通过外置MAC,但是一般效率不高,因为内部含有MAC的芯片一般都有网络加速引擎。而 PHY一般是外置的。
MAC与PHY之间的接口一般是MII/RMII,它们是 IEEE-802.3 定义的以太网标准接口,连接图如下:
MII
RMII
5.2 NAPI处理机制
Linux 在这中断、轮询的基础上提出了另外一种网络数据接收的处理方法:NAPI(New API),NAPI 是一种高效的网络处理技术。NAPI 的核心思想就是不全部采用中断来读取网络数据,而是采用中断来唤醒数据接收服务程序,在接收服务程序中采用 POLL 的方法来轮询处理数据。这种方法的好处就是可以提高短数据包的接收效率,减少中断处理的时间。Linux 内核使用结构体 napi_struct 表示 NAPI。
大致过程如下:
- 初始化 NAPI : netif_napi_add;
- 使能 NAPI : inline void napi_enable(struct napi_struct *n);
- 检查 NAPI 是否可以进行调度: inline bool napi_schedule_prep(struct napi_struct *n);
- NAPI 调度: void __napi_schedule(struct napi_struct *n);
- NAPI 处理完成:inline void napi_complete(struct napi_struct *n);
- 关闭NAPI:void napi_disable(struct napi_struct *n);
- 删除 NAPI:void netif_napi_del(struct napi_struct *napi);
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)