嵌入式Linux驱动开发基础知识
1. 具体单板的LED驱动程序
1.1 编写LED驱动程序的详细步骤
- 看原理图确定引脚,确定引脚输出什么电平才能点亮/熄灭LED
- 看主芯片手册,确定寄存器的操作方法:哪些寄存器?哪些位?地址是?
- 编写驱动:先写框架,再写硬件操作的代码
(上次部分写出了框架,这次在其中补充具体硬件操作的代码)
注意:在芯片手册中确定的寄存器地址被称为物理地址,在linux内核中无法直接使用。需要使用内核提供的ioremap把物理地址映射为虚拟地址,使用虚拟地址。
ioremap函数的使用:
(1)函数原型:
void __iomem *ioremap(resource_size_t res_cookie, size_t size)
//使用时要包含头文件
//#include <asm/io.h>
(2)作用:
把物理地址phys_addr开始的一段空间(大小为size),映射为虚拟地址;返回值是该段虚拟地址的首地址。
virt_addr = ioremap(phys_addr,size);
实际上,它是按页(4096字节)进行映射的,是整页整页地映射的。
假设phys_addr = 0x10002,size = 4,ioremap的内部实现是:
a. phys_addr按页取整,得到地址0x10000
b. size按页取整,得到4096
c. 把起始地址0x10000,大小为4096的这一块物理地址空间,映射到虚拟地址空间,假设得到的虚拟空间起始地址为0xf0010000
d. 那么phys_addr = 0x10002对应的virt_addr = 0xf0010002
(3)不再使用该段虚拟地址时,要iounmap(virt_addr):
void iounmap(volatile void __iomem *cookie)
1.2 AM335X的LED驱动程序
//AM335X的LED驱动程序
//LED驱动程序 leddrv.c文件
//1.驱动程序
//(1)包含头文件
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include "led_operation.h"
//(2) 确定主设备号
static int major = 0; //让内核自动分配
static struct class *led_class;
struct led_operations *p_led_opr;
#define MIN(a,b) (a<b?a:b)
//(4) 实现对应的open/read/write等函数,填入file_operations结构体
static ssize_t led_drv_read (struct file *file,const char __user *buf, size_t size, loff_t *offset)
{
//举例,放入一些打印信息
printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
return 0;
}
static ssize_t led_drv_write (struct file *file,char __user *buf, size_t size, loff_t *offset)
{
char status;
int err;
struct inode *inode = file_inode(file);
int minor = iminor(node);
printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
//从buffer里面拿到应用程序下发过来的数据,拷贝到kernel——buf(驱动中的buffer)去
err=copy_from_user(&status, const buf, 1);
//根据次设备号和status控制LED
p_led_opr->ctl(minor,status);
return 1;
}
static int led_drv_open (struct inode *node, struct file *file)
{
int minor = iminor(inode);
printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
//根据次设备号初始化LED
p_led_opr->init(minor);
return 0;
}
static int led_drv_close (struct inode *node, struct file *file)
{
printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
return 0;
}
//(3) 定义自己的file_operation结构体
static struct file_operation led_drv = {
.owner = THIS_MOUDLE;
.open = led_drv_open;
.read = led_drv_read;
.write = led_drv_write;
.release= led_drv_close;
};
//(5) 把file_operations结构体告诉内核:注册驱动程序
//(6) 谁来注册驱动程序?需要一个入口函数:安装驱动程序时,就会去调用这个入口函数(入口函数中会去调用注册函数)
static int __init led_init(void)
{
int err;
//注册函数
major = register_chrdev(0,"led",&led_drv);
//创建了class
led_class = class_create(THIS_MOUDLE,"led_class");
err = PTR_ERR(led_class);
if(IS_ERR(led_class)){
unregister_chrdev(major,"led");
return -1;
}
//还需要创建一个device,多创建几个LED
device_create(led_class,NULL,MKDEV(major,0),NULL,"led");
device_create(led_class,NULL,MKDEV(major,1),NULL,"led0");
p_led_opr = get_board_led_opr();
return 0;
}
//(7) 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
static void __exit led_exit(void)
{
//销毁device
device_destroy(led_class,MKDEV(major,0));
device_destroy(led_class,MKDEV(major,1));
//类销毁
class_destroy(led_class);
//取消注册函数
unregister_chrdev(major,"led");
}
//(8) 其他完善:提供设备信息,自动创建设备节点
//将led_init修饰为入口函数
module_init(led_init);
//将led_exit修饰为出口函数
module_exit(led_exit);
MODULE_LICENSE("GPL");//说明驱动程序遵守GPL协议
************************************************************************************************************
//led——operation.h文件
#ifndef _LED_OPR
#define _LED_OPR
struct led_operations {
int num;
//初始化LED,which-哪个LED
int (*init) (int which);
//控制LED,which-哪个led,status:1-亮,0-灭
int (*ctl) (int which,char status);
};
struct led_operations *get_board_led_opr(void);
#endif
**************************************************************************************************************
//单板上需要实现的程序 board_am335x.c,这里是针对AM335X的具体程序
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <asm/io.h>
#include "led_operation.h"
static volatile unsigned int *CM_PER_GPIO1_CLKCTRL;
static volatile unsigned int *conf_gpmc_ad0;
static volatile unsigned int *GPIO1_OE;
static volatile unsigned int *GPIO1_CLEARDATAOUT;
static volatile unsigned int *GPIO1_SETDATAOUT;
static int board_demo_led_init (int which)
{
if(which == 0)
{
//不需要每次都对寄存器指针进行初始化,而是事先判断一下
if(!CM_PER_GPIO1_CLKCTRL)
{
CM_PER_GPIO1_CLKCTRL = ioremap(0x44E00000 + 0xAC, 4);
conf_gpmc_ad0 = ioremap(0x44E10000+0x800, 4);
GPIO1_OE = ioremap(0x4804C000 + 0x134, 4 );
GPIO1_CLEARDATAOUT = ioremap( 0x4804C000 + 0x190, 4);
GPIO1_SETDATAOUT = ioremap(0x4804C000 + 0x194, 4);
}
//printk("%s %s line %d, led %d\n",__FILE__,__FUNCTION__,__LINE__, which);
//a. 使能GPIO1
//set PRCM to enalbe GPIO1
//set CM_PER_GPIO1_CLKCTRL(0x44E00000 + 0xAC)
//val:(1<<18)|0x2
*CM_PER_GPIO1_CLKCTRL = (1<<18)|0x2;
/*b. 设置GPIO1_16的功能,让它工作于GPIO模式
*set Control Module to set GPIO1_16(R13) used as GPIO
* conf_gpmc_ad0 as mode7
*addr: 0x44E10000+0x800
*val: 7
*/
*conf_gpmc_ad0 = 7;
/*c.设置GPIO1_16的方向,让它作为输出引脚
*set GPIO1's registers, to set GPIO1_16's dir(output)
*GPIO1_OE
*addr : 0x4804C000 + 0x134
*clear bit 16
*/
*GPIO1_OE &= ~(1<<16);
}
return 0;
}
static int board_demo_led_ctl(int which, char status)
{
//printk("%s %s line %d, led %d,%s\n",__FILE__,__FUNCTION__,__LINE__,which,status?"on":"off");
if (which == 0)
{
//on 的情况,观察原理图可知需要点亮的话让引脚输出低电平
if(status)
{
/*e. 清除GPIO1_16的数据,让它输出低电平
*AM335X芯片支持set-and-clear protocol,设置GPIO_CLEARDATAOUT的bit 16为1即可让引脚输出0;
*set GPIO1_16's registers, to output 0
*GPIO_CLEARDATAOUT
*addr: 0x4804C000 + 0x190
*/
*GPIO1_CLEARDATAOUT = (1<<16);
}
else //灭的情况
{
/* d. 设置GPIO1_16的数据,让它输出高电平
*AM335X芯片支持set-and-clear protocol,设置GPIO_SETDATAOUT的bit 16 为1即可让引脚输出1
* set GPIO1_16's registers, to output 1
* GPIO_SETDATAOUT
* addr : 0x4804C000 + 0x194
*/
*GPIO1_SETDATAOUT = (1<<16);
}
}
return 0;
}
static struct led_operatioms board_deemo_led_opr = {
.num =1,
.init = board_demo_led_init,
.ctl = board_demo_led_ctl,
};
struct led_operations *get_board_led_opr(void)
{
return &board_demo_led_opr;
}
********************************************************************************************************
//ledtest.c文件
int main(int argc, char **argv)
{
int fd;
char status;
//1.判断参数
if(argc !=3)
{
printf("Usage: %s <dev> <on | off>\n",argv[0]);
return -1;
}
//2.打开文件
fd = open(argv[1],O_RDWR);
if(fd == -1)
{
printf("can not open file %s\n",argv[1]);
return -1;
}
//3. 写文件
if(0 == strcmp(argv[2],"on"))
{
status = 1;
write(fd,&status,1);
}
else
{
status = 0;
write(fd,&status,1);
}
close(fd);
return 0;
}
2. 驱动设计的思想—面向对象/分层/分离
linux驱动 = 驱动框架 + 硬件操作
= 驱动框架 + 单片机
2.1 面向对象
在Linux当中,可以认为面向对象就是用某一个结构体来表示对象。
- 字符设备驱动程序抽象出一个file_operations结构体;
- 程序针对硬件部分抽象出led_operations结构体。
2.2 分层
上下分层,例如前面写的LED驱动程序就分为2层:
- 上层实现硬件无关的操作,比如注册字符设备驱动:leddrv.c
- 下层实现硬件相关的操作,比如board_A.c实现单板A的LED操作
- leddrv.c:实现file_operations,注册驱动
- board_A.c或者board_B.c等等:实现硬件操作,构造各自的led_operations
2.3 分离
继续改进方式:分离
在board_A.c中,实现了一个led_operations,为LED引脚实现了初始化函数、控制函数:
static struct led_operations board_demo_led_opr = {
.num = 1,
.init = board_demo_led_init,
.ctl = board_demo_led_ctl,
};
- 如果硬件上更换一个引脚来控制LED怎么办?
需要去修改上面结构体中的init、ctl函数,就是每一个函数都要做修改。和硬件捆绑的太死了,不灵活 - 实际情况是,每一款芯片它的GPIO操作都是类似的。
以假设举例,比如:GPIO1_3、GPIO5_4这2个引脚接到LED:
(1)GPIO1_3属于第一组,即GPIO1
有方向寄存器DIR、数据寄存器DR等,基础地址是addr_base_addr_gpio1。
设置为output引脚:修改GPIO1的DIR寄存器的bit3
设置输出电平:修改GPIO1的DR寄存器的bit3
(2)GPIO5_4属于第5组,即GPIO5
有方向寄存器DIR、数据寄存器DR等,基础地址是addr_base_addr_gpio5。
设置为output引脚:修改GPIO5的DIR寄存器的bit4
设置输出电平:修改GPIO5的DR寄存器的bit4
这两个都是类似的GPIO操作,因此对于同一个主芯片,一般会提供一个.c文件实现芯片上的GPIO操作。
//简单例子
//led_resource.h文件
#ifndef _LED_RESOURCE_H
#define _LED_RESOURCE_H
/* GPIO3_0 */
/* bit[31:16] = group */
/* bit[15:0] = which pin */
struct led_resource {
int pin;
};
//声明函数
struct led_resource *get_led_resource(void);
#endif
*********************************************************************************************************************
//board_A_led.c文件
#include "led_resource.h"
static struct led_resource board_A_led = {
.pin = (3<<16)|(1),
};
struct led_resource *get_led_resource(void)
{
return &board_A_led;
}
3. 驱动进化之路_总线设备驱动模型
驱动有3种编写方法:
(1)传统写法:使用哪个引脚,怎么操作引脚,都直接写死在代码中,最简单,完全不考虑扩展性,可以快速实现功能,修改引脚时,需要重新编译。
(2)总线设备驱动模型
总线设备驱动模型具体例子:
4. 驱动进化之路_设备树的语法
只考虑总线设备驱动模型会存在一些问题:
- 如果有很多个单板(例如:boardA.c,boardB.c等),更换了某个引脚全部都需要重新编译
- 所有的单板.c文件都会在linux内核中,于是内核中会存在大量的重复的没有技术含量的代码,使得linux的源代码非常的冗余。
- 所以,引用了设备树。
- 使用配置文件,而不用.c文件,使用设备树语法来写配置文件,将配置文件加入在linux内核中
设备树的由来:
如何描述这棵树,考虑到使用设备树的语法:
(1)DTS文件布局(layout):
/dts-v1/; //表示版本
[memory reservations] //格式为:/memreserve/<address><length>;
/{
[property definitions]
[chile nodes]
};
(2)node的格式:
设备树中的基本单元,被称作“node”,其格式为:
[label:]node-name[@unit-address]{
[properties definitions]
[child nodes]
};
常用的节点:
根节点、CPU节点、memory节点、chosen节点(不对应设备,虚拟的一个节点,可以在这个节点中指定bootargs,bootargs是一个传递给内核的参数)
常用的属性:
(1)#address-cells、#size-cells
cell指一个32位的数值,address-cells:address要用多少个32位数来表示;
size-cells:size要用多少个32位数来表示
(2)compatible
表示兼容,对于某个LED,内核中可能有A、B、C三个驱动都支持它,那可以这样写:
led{
compatible = "A","B","C";
};
内核启动时,就会为这个LED按这样的优先顺序为它找到驱动程序:A,B,C
(3)model
model属性与compatible属性相似,但是存在差别
compatible属性是一个字符串列表,表示你的硬件兼容A,B,C等驱动
model用来准确地定义这个硬件是什么
(4)reg
reg的本意是register,用来表示寄存器地址。
但在设备树里,它可以用来描述一段空间。对于ARM系统,寄存器和内存是统一编址的,即访问寄存器时用某块地址,访问内存时用某块地址,在访问方法上没有区别。
/dts-v1/;
/{
#address-cells = <1>;
#size-cells = <1>;
memory{
reg = <0x80000000 0x20000000>;
};
};
5. 驱动进化之路_内核或者驱动程序对设备树的处理与使用
-
从源代码文件dts文件开始,设备树的处理过程为:
(1)dts在PC机上被编译为dtb文件;
(2)u-boot把dtb文件传给内核;
(3)内核解析dtb文件,把每一个节点都转换为device_node结构体;
(4)对于某些device_node结构体,会被转换为platform_device结构体。 -
dtb中的每一个节点都会被转换成device_node结构体:根节点被保存在全局变量of_root中,从of_root开始可以访问到任意节点。
-
哪些设备树节点会被转换成为platform_device:
(1)根节点下含有compatile属性的子节点
(2)含有特定compatile属性的节点的子节点
如果一个节点的compatile属性,它的值是这4者之一:“simple-bus”,“simple-mfd”,“isa”,“arm,amba-bus”,那么它的子节点(需要含compatile属性)也可以转换为platform_device。
(3)总线I2C、SPI节点下子节点:不转换为platform_device
某个总线下到子节点,应该交给对应的总线驱动程序来处理,不应该被转换为platform_device。 -
具体例子:
-
如何修改设备树文件:
一个写得好的驱动程序,它会尽量确定所用资源,只把不能确定的资源留给设备树,让设备树来指定。
根据原理图确定“驱动程序无法确定的硬件资源”,再在设备树文件中填写对应内容。 -
填写内容的格式:
(1)看绑定文档
内核文档 Documents/devicetree/bindings/
好的厂家也会提供设备树的说明文档
(2)参考同类型单板的设备树文件
(3)最后没办法时,只能去研究驱动源码
学习资源(韦东山视频链接):http://dev.t-firefly.com/thread-100207-1-1.html