嵌入式Linux开发笔记(韦东山2)

嵌入式Linux驱动开发基础知识

1. 具体单板的LED驱动程序
1.1 编写LED驱动程序的详细步骤
  1. 看原理图确定引脚,确定引脚输出什么电平才能点亮/熄灭LED
  2. 看主芯片手册,确定寄存器的操作方法:哪些寄存器?哪些位?地址是?
  3. 编写驱动:先写框架,再写硬件操作的代码
    (上次部分写出了框架,这次在其中补充具体硬件操作的代码)
    注意:在芯片手册中确定的寄存器地址被称为物理地址,在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层:

  1. 上层实现硬件无关的操作,比如注册字符设备驱动:leddrv.c
  2. 下层实现硬件相关的操作,比如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. 驱动进化之路_设备树的语法

只考虑总线设备驱动模型会存在一些问题:

  1. 如果有很多个单板(例如:boardA.c,boardB.c等),更换了某个引脚全部都需要重新编译
  2. 所有的单板.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

猜你喜欢

转载自blog.csdn.net/qq_43348528/article/details/103800569