所谓裸机在这里主要是指系统软件平台没有用到操作系统。在基于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 操作系统的驱动与裸机上的驱动有很多不同:
Linux中的三大类驱动:字符设备驱动、块设备驱动和网络设备驱动,三类驱动的调用框架是一样的,即:应用程序对设备进行操作时,只能通过库中的函数,这个函数就会进入内核,然后内核调用驱动,驱动再操作设备。
字符设备驱动是Linux 驱动中最基本的一类设备驱动,字符设备就是指在I/O传输过程中以字符为单位进行传输的设备,读写数据是分先后顺序的。比如GPIO、IIC、SPI,LCD等都属于字符设备。
在裸机上,驱动是直接对寄存器进行操作,在Linux下也是对寄存器的操作,但是Linux下驱动的编写需要符合Linux的驱动框架,Linux驱动开发重点是了解其驱动框架。
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
};
结构体中比较常用的几个成员:
Device Tree 起源于 OpenFirmware (OF),是一种采用树形结构描述硬件的数据结构,由一系列被命名的结点(node)和属性(property)组成,结点本身可包含子结点,主要由三大部分组成:头(Header)、结构块(Structure block)、字符串块(Strings block)。
树的主干就是系统总线,IIC 控制器、GPIO 控制器、SPI 控制器等都是接到系统主线上的分支。IIC 控制器有分为IIC1 和IIC2 两种,其中IIC1 上接了FT5206 和AT24C02这两个IIC 设备,IIC2 上只接了MPU6050 这个设备。DTS 文件的主要功能就是按照图中所示的结构来描述板子上的设备信息。
这是一个控制gpio的驱动代码,内容没什么用,用来了解一下开发流程即可,驱动代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define CAMERA_CNT 1 // 设备号个数
#define CAMERA_NAME "camera" // 名字
#define TAKEPICTURE 1 // 拍照
// 定义camera设备结构体
struct camera_dev{
dev_t devid; // 设备号
struct cdev cdev; // cdev
struct class *class; // 类
struct device *device; // 设备
int major; // 主设备号
int minor; // 次设备号
struct device_node *nd; // 设备节点
int focus_gpio; // 对焦GPIO编号
int shutter_gpio; // 快门GPIO编号
};
struct camera_dev camera; /* 相机设备 */
/**
* @brief : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int camera_open(struct inode *inode, struct file *filp)
{
filp->private_data = &camera; /* 设置私有数据 */
return 0;
}
/**
* @brief : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t camera_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/**
* @brief : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
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;
}
/**
* @brief : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @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,
};
/**
* @brief : 驱动入口函数
* @param : 无
* @return : 无
*/
static int __init camera_init(void)
{
int ret = 0;
// 设置拍照所使用的GPIO
// 1、获取设备节点:camera
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");
}
// 2、 获取设备树中的gpio属性,得到相机所使用的对焦合拍照编号
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);
// 3、设置相机引脚默认状态
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");
}
// 注册字符设备驱动
// 1、创建设备号
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);
// 2、初始化cdev
camera.cdev.owner = THIS_MODULE;
cdev_init(&camera.cdev, &camera_fops);
// 3、添加一个cdev
cdev_add(&camera.cdev, camera.devid, CAMERA_CNT);
// 4、创建类
camera.class = class_create(THIS_MODULE, CAMERA_NAME);
if (IS_ERR(camera.class)) {
return PTR_ERR(camera.class);
}
// 5、创建设备
camera.device = device_create(camera.class, NULL, camera.devid, NULL, CAMERA_NAME);
if (IS_ERR(camera.device)) {
return PTR_ERR(camera.device);
}
return 0;
}
/**
* @brief : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit camera_exit(void)
{
// 注销字符设备驱动
cdev_del(&camera.cdev); // 删除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
>;
};
在写一些简单的字符设备驱动的时候,可以按照上面驱动,直接写死,但是面对复杂的设备,像IIC、SPI、LCD等,就应该考虑下驱动的分离与分层,这样可以极大提高驱动的可重用性。
这是简单的驱动与设备的关系,是一对一的,也就是每有一个硬件就会有一个对应的设备,很明显这样很容易造成一个系统驱动垃圾堆,所以复杂的驱动一般用下面的方式:
每个设备控制器都提供一个统一的接口(也叫做主机驱动),每个设备的话也只提供一个驱动程序(设备驱动),每个设备通过统一的接口驱动来访问,这样就可以大大简化驱动文件。
块设备驱动块设备是针对存储设备的驱动,比如 SD卡、 EMMC、 NAND Flash、 Nor Flash、 SPI Flash、机械硬盘、固态硬盘等,驱动要远比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统。块设备驱动相比字符设备驱动的主要区别如下:
与字符设备开发框架大致类似,区别在于:
struct gendisk {
/* major, first_minor and minors are input parameters only,
* don't use directly. Use disk_devt() and disk_max_parts().
*/
int major; /* major number of driver */
int first_minor;
int minors; /* maximum number of minors, =1 for
* disks that can't be partitioned. */
char disk_name[DISK_NAME_LEN]; /* name of major driver */
char *(*devnode)(struct gendisk *gd, umode_t *mode);
unsigned int events; /* supported events */
unsigned int async_events; /* async events, subset of all */
/* Array of pointers to partitions indexed by partno.
* Protected with matching bdev lock but stat and other
* non-critical accesses use RCU. Always access through
* helpers.
*/
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; // FIXME: remove
struct kobject *slave_dir;
struct timer_rand_state *random;
atomic_t sync_io; /* RAID */
struct disk_events *ev;
#ifdef CONFIG_BLK_DEV_INTEGRITY
struct blk_integrity *integrity;
#endif
int node_id;
};
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);
/* ->media_changed() is DEPRECATED, use ->check_events() instead */
int (*media_changed) (struct gendisk *);
void (*unlock_native_capacity) (struct gendisk *);
int (*revalidate_disk) (struct gendisk *);
int (*getgeo)(struct block_device *, struct hd_geometry *);
/* this callback is with swap_lock and sometimes page table lock held */
void (*swap_slot_free_notify) (struct block_device *, unsigned long);
struct module *owner;
};
在上一节提到,块设备结构体没有与字符设备一样的read、write这样的读写操作函数,但它是通过请求队列 request_queue、请求 request 和 bio 结构这些操作进行读写的。
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)
对请求的处理很简单:获取、开启、对应函数分别为: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 | 处理完请求,直到下一个错误产生 |
bio 是request结构体里面的一个成员,每个 request 里面里面会有多个 bio,bio 保存着最终要读写的数据、地址等信息。
嵌入式网络硬件分为两部分:MAC 和 PHY,如果芯片支持网络,那么一般是指内部含有MAC,如果没有只能通过外置MAC,但是一般效率不高,因为内部含有MAC的芯片一般都有网络加速引擎。而 PHY一般是外置的。
MAC与PHY之间的接口一般是MII/RMII,它们是 IEEE-802.3 定义的以太网标准接口,连接图如下:
MII
RMII
Linux 在这中断、轮询的基础上提出了另外一种网络数据接收的处理方法:NAPI(New API),NAPI 是一种高效的网络处理技术。NAPI 的核心思想就是不全部采用中断来读取网络数据,而是采用中断来唤醒数据接收服务程序,在接收服务程序中采用 POLL 的方法来轮询处理数据。这种方法的好处就是可以提高短数据包的接收效率,减少中断处理的时间。Linux 内核使用结构体 napi_struct 表示 NAPI。
大致过程如下: