上一篇介绍了本实验所使用的主要IC及其通信方式,这里开始记录正式的开发过程。所选用的平台为MTK的MT6572,做过MTK智能平台研发的亲们都知道,MTK将自己的东西都添加在自加的包mediatek下面,kernel部分也不例外。然而为了体现普遍性,本实验我严格按照google提供的Android框架结构来进行相应添加的,即驱动程序添加在kernel/drivers/目录下。由于Android系统是基于Linux内核搭建起来的,所以Android驱动基本上就是Linux驱动。这里先简要介绍下编写Linux驱动程序的步骤:
第1步:建立Linux驱动框架(驱动的装载和卸载)
Linux内核在使用驱动时首先要装载驱动,装载过程中进行一些初始化工作:创建设备文件,分配内存等;在Linux系统退出时则需要卸载驱动,卸载过程中进行删除设备文件,释放内存等。所以Linux驱动需要提供了两个函数来分别处理驱动初始化和退出的工作,而Linux内核提供了两个宏来指定这两个函数:module_init和module_exit宏。Linux驱动程序一般都需要指定这俩函数,因此包含这俩函数和指定函数的这俩宏的C程序文件可看作是Linux的驱动框架。如本实验中breath_leds.c为呼吸灯驱动文件,其中有如下两句:
即表示呼吸灯驱动被装载时将执行breath_leds_init函数,Linux系统退出时将执行breath_leds_exit函数。
module_init(breath_leds_init); module_exit(breath_leds_exit);
第2步:注册和注销设备文件
任何一个Linux驱动都需要一个设备文件,用来与应用程序完成交互。建立设备文件一般都在上一步指定的breath_leds_init函数中完成,相对应的,删除设备文件在上一步指定的breath_leds_exit函数中完成。由于该设备文件与应用程序是通过字节传输进行通信的,可作为字符设备进行创建,需要使用cdev_init, register_chrdev_region, cdev_add, class_create, device_create等函数。创建设备文件步骤如下:
1)使用cdev_init函数初始化cdev
描述字符设备文件需要一个cdev结构体,该结构体在<Linux内核源码>/include/linux/cdev.h中定义:
-
struct cdev {
-
struct kobject kobj;
//封装设备文件的对象
-
struct module *owner;
//指向所用内核模块指针
-
const
struct file_operations *ops;
//指向回调的指针
-
struct list_head list;
//指向上一个和下一个cdev机构体指针
-
dev_t dev;
//dev_t是int型数据类型,表示设备号。前12bit表示主设备号,后20bit表示次设备号
-
unsigned
int count;
//请求连接的设备编号范围(最大值),建立多设备时使用
-
};
调用cdev_init初始化大部分cdev成员变量,cdev_init函数代码:
-
void cdev_init(
struct cdev *cdev,
const
struct file_operations *fops)
-
{
-
memset(cdev,
0, sizeofsizeof *cdev);
-
INIT_LIST_HEAD(&cdev->list);
//初始化首尾指针
-
kobject_init(&cdev->kobj, &ktype_cdev_default);
//初始化设备文件对象
-
cdev->ops = fops;
//关联file_operations
-
}
可以看出,cdev.owner变量并没有在该函数中初始化,所以cdev.owner需要自己另外初始化。本实验中对应代码为:
-
static
struct cdev leds_cdev;
-
cdev_init(&leds_cdev, &dev_fops);
-
leds_cdev.owner = dev_fops.owner;
其中dev_fops为struct file_operations类型,稍候介绍。
2)使用register_chrdev_region或alloc_chrdev_region函数指定设备号
Linux设备文件的设备号分为主设备号和次设备号,用1个int类型(dev_t)表示,其中前12位表示主设备号,后20位表示从设备号。指定设备号有两种方法:
> 直接在代码中指定(register_chrdev_region)
> 动态分配(alloc_chrdev_region)
直接指定设备号虽然比较直观,但是如果主设备号和次设备号已经存在,建立设备文件就会失败。而使用alloc_chrdev_region函数会自动分配一个未使用的主设备号。alloc_chrdev_region函数原型如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const charchar *name);
其中,dev表示设备号指针,即分配的设备号存储地址,baseminor值用来决定怎么分配次设备号,如baseminor为10,则分配的第一个设备文件的次设备号为10,count表示分配的次设备号范围,即分配几个次设备号,如count为3,baseminor为10,则会分配三个次设备号(10、11、12),name表示设备文件名称。
如果要直接指定设备号,需要使用register_chrdev_region函数注册字符设备区域,函数原型如下:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
其中from参数表示指定的设备号,count参数表示次设备号范围,name表示设备文件名称。一般采用分别指定主设备号和次设备号的方式指定设备号,使用MKDEV宏将主设备号和次设备号组合成设备号,如下:
int dev_number = MKDEV(major, minor);
使用MAJOR和MINOR宏可从设备号中获取主设备号和次设备号:
-
int major = MAJOR(dev_number);
-
int minor = MINOR(dev_number);
3)使用cdev_add函数将字符设备添加到内核中的字符设备数组中
cdev_add函数将字符设备添加到probes数组中,probes数组中保存着已建立的字符设备。cdev_add函数原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
其中p表示设备文件指针,dev表示设备号,count表示设备数量。
4)使用class_create宏创建struct class,最后创建设备文件时会用到该结构体
struct class 包含了一些与设备文件有关的变量以及一些回调函数指针变量。本实验对应代码如下:
-
static
struct classclass *leds_class =
NULL;
-
leds_class = class_create(THIS_MODULE, DEV_NAME);
其中DEV_NAME是设备文件名称。
5)使用device_create函数创建设备文件
device_create用于创建设备文件,函数原型如下:
struct device *device_create(struct class *class. struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
可使用如下代码创建呼吸灯设备文件:
device_create(leds_class, NULL, dev_num, NULL, DEV_NAME);
销毁设备文件稍微简单一点,依次调用device_destory,class_destory,unregister_chrdev_region函数即可。看名字大家也知道这3个函数的作用。函数原型如下:
-
void device_destory(
struct
class *
class, dev_t devt);
-
void class_destory(
struct
class *cls);
-
void unregister_chrdev_region(dev_t from,
unsigned count);
第3步:指定驱动相关信息
驱动程序是自描述的,通过MODULE_AUTHOR,MODULE_LICENSE,MODULE_ALIAS,MODULE_DESCRIPTION等宏指定与驱动相关的信息。这些信息一般在文件末尾指定:
MODULE_AUTHOR宏指定模块作者;MODULE_LICENSE宏指定开源协议;MODULE_ALIAS宏指定模块别名;MODULE_DESCRIPTION宏指定模块描述信息。
第4步:指定回调函数
Linux驱动包含多种动作,即事件。如想设备文件写入数据时会触发“写”事件,Linux会调用对应驱动程序的write回调函数,从驱动程序读数据则调用read回调函数。驱动层与上层的通信主要就靠这些回调函数进行的。这些回调函数都由struct file_operations结构体指定。如本实验中我只需要向呼吸灯控制芯片SN3112-12写入数据,所以只实现了write回调:
-
static
struct file_operations dev_fops =
-
{
-
.owner = THIS_MODULE,
-
.write = breath_leds_write,
-
};
其中.owner表示这些回调函数使用的模块范围,即只使用于本模块。
第5步:编写业务逻辑
这步无须赘言,指定了回调函数,不能什么都不干吧,必须实现相关功能,如实现上步指定的breath_leds_write函数。
第6步:编写Makefile文件
Linux内核源代码的编译规则都是通过Makefile文件定义的,所以每个Linux驱动必须要有一个Makefile文件。
第7步:编译
可直接编译进内核,也可作为模块单独编译,这步可通过写一个Kconfig进行编译选项配置。
大体上开发Linux驱动也就上面这些步骤,现在记录下本实验驱动开发过程:
一 编写驱动源代码
进入kernel/drivers/目录,新建breath_leds目录,进入该目录,新建breath_leds.c:
-
#include <linux/module.h>
-
#include <linux/init.h>
-
-
#include <linux/fs.h>
-
#include <linux/cdev.h>
-
#include <linux/device.h>
-
#include <linux/pci.h>
-
#include <asm/uaccess.h>
-
#include <linux/kernel.h>
-
-
#include <mach/mt_gpio.h>
-
#include <linux/delay.h>
-
-
#define DEV_NAME "breath_leds" //设备名称
-
#define DEV_COUNT 1 //设备文件数量
-
#define BREATH_LEDS_MAJOR 0 //默认主设备号
-
#define BREATH_LEDS_MINOR 1 //默认次设备号
-
-
#define IIC_ADDR 0xa8 //i2c 地址
-
#define SN3112_EN GPIO141
-
#define SN3112_SCL GPIO102
-
#define SN3112_SDA GPIO138
-
-
#define LED1_3_CON_REG 0x13
-
#define LED4_9_CON_REG 0x14
-
#define LED10_12_CON_REG 0x15
-
-
#define SET_SCL_PIN_HIGH() (mt_set_gpio_out(SN3112_SCL, GPIO_OUT_ONE))
-
#define SET_SCL_PIN_LOW() (mt_set_gpio_out(SN3112_SCL, GPIO_OUT_ZERO))
-
#define SET_SDA_PIN_HIGH() (mt_set_gpio_out(SN3112_SDA, GPIO_OUT_ONE))
-
#define SET_SDA_PIN_LOW() (mt_set_gpio_out(SN3112_SDA, GPIO_OUT_ZERO))
-
-
static ssize_t breath_leds_write(
struct file *file,
const
char __user *buf, size_t count, loff_t *ppos);
-
-
static
int major = BREATH_LEDS_MAJOR;
//主设备号
-
static
int minor = BREATH_LEDS_MINOR;
//次设备号
-
-
static dev_t dev_num;
//设备号
-
static
struct
class *leds_class =
NULL;
//struct class
-
-
//通过i2c发送一个字节
-
static
void send_iic_byte(
unsigned
short data)
-
{
-
unsigned
short b_mask;
-
unsigned
short i;
-
-
b_mask =
0x80;
// 1 << 7 : first send MSB
-
for (i=
0; i<
8; i++)
-
{
-
if ((b_mask & data) !=
0)
-
{
-
SET_SDA_PIN_HIGH();
-
}
-
else
-
{
-
SET_SDA_PIN_LOW();
-
}
-
udelay(
1);
-
SET_SCL_PIN_HIGH();
-
udelay(
2);
-
SET_SCL_PIN_LOW();
-
udelay(
1);
-
b_mask >>=
1;
-
}
-
}
-
-
//往sn3112寄存器reg_addr中写入数据data
-
static
unsigned
short write_iic_reg(
unsigned
short reg_addr,
unsigned
short data)
-
{
-
unsigned
short ack =
0;
//for read iic ack
-
unsigned
short iic_addr = IIC_ADDR &
0xfe;
-
-
//start condition
-
SET_SCL_PIN_HIGH();
-
SET_SDA_PIN_HIGH();
-
udelay(
1);
-
SET_SDA_PIN_LOW();
-
udelay(
1);
-
SET_SCL_PIN_LOW();
-
udelay(
1);
-
-
//send iic addr
-
send_iic_byte(iic_addr);
-
-
//read ack
-
SET_SCL_PIN_HIGH();
-
udelay(
1);
-
SET_SDA_PIN_LOW();
-
//ack = iic_read_ack();
-
udelay(
3);
-
SET_SCL_PIN_LOW();
-
udelay(
1);
-
-
//send reg addr
-
send_iic_byte(reg_addr);
-
-
//read ack
-
SET_SCL_PIN_HIGH();
-
udelay(
1);
-
SET_SDA_PIN_LOW();
-
//ack = iic_read_ack();
-
udelay(
3);
-
SET_SCL_PIN_LOW();
-
udelay(
1);
-
-
//send data
-
send_iic_byte(data);
-
-
//read ack
-
SET_SCL_PIN_HIGH();
-
udelay(
1);
-
SET_SDA_PIN_LOW();
-
//ack = iic_read_ack();
-
udelay(
3);
-
SET_SCL_PIN_LOW();
-
udelay(
1);
-
-
//stop condition
-
SET_SCL_PIN_HIGH();
-
udelay(
1);
-
SET_SDA_PIN_HIGH();
-
udelay(
1);
-
-
return ack;
-
}
-
-
//每次写入数据后手动刷新
-
static
void refresh_leds(
void)
-
{
-
write_iic_reg(
0x16,
0x00);
-
}
-
-
static
void turn_on_sn3112(
void)
-
{
-
//硬开启
-
//mt_set_gpio_out(SN3112_EN, GPIO_OUT_ONE);
-
//软开启
-
write_iic_reg(
0x00,
0x01);
-
}
-
-
static
void turn_off_sn3112(
void)
-
{
-
//硬关断
-
//mt_set_gpio_out(SN3112_EN, GPIO_OUT_ZERO);
-
//软关断
-
write_iic_reg(
0x00,
0x00);
-
}
-
-
static
int param_level =
0xff;
-
-
//初始化呼吸灯控制ic sn3112
-
static
void initial_sn3112(
void)
-
{
-
int i;
-
-
//使能sn3112
-
mt_set_gpio_mode(SN3112_EN, GPIO_MODE_GPIO);
-
mt_set_gpio_dir(SN3112_EN, GPIO_DIR_OUT);
-
mt_set_gpio_out(SN3112_EN, GPIO_OUT_ONE);
-
-
//配置时钟线为输出模式
-
mt_set_gpio_mode(SN3112_SCL, GPIO_MODE_GPIO);
-
mt_set_gpio_dir(SN3112_SCL, GPIO_DIR_OUT);
-
-
//配置数据线为输出模式
-
mt_set_gpio_mode(SN3112_SDA, GPIO_MODE_GPIO);
-
mt_set_gpio_dir(SN3112_SDA, GPIO_DIR_OUT);
-
-
write_iic_reg(
0x00,
0x01);
//设置sn3112工作于标准模式
-
//12路灯全开
-
write_iic_reg(LED1_3_CON_REG,
0x38);
-
write_iic_reg(LED4_9_CON_REG,
0x3f);
-
write_iic_reg(LED10_12_CON_REG,
0x07);
-
-
//设置12路灯初始亮度,控制等亮度的寄存器位0x04~0x0f
-
for (i=
0x04; i<
0x10; i++)
-
{
-
write_iic_reg(i, param_level);
-
refresh_leds();
-
}
-
}
-
-
static
struct file_operations dev_fops =
-
{
-
.owner = THIS_MODULE,
-
.write = breath_leds_write,
-
};
-
-
//rec_data[0]:亮度值0~255;rec_data[1]的bit0~3:哪一路led 1~12,bit7:是否打开sn3112,1为打开,0为关闭
-
static
unsigned
char rec_data[
2];
-
//每路led对应的led值
-
static
int pwm_reg_index[
12] = {
0x04,
0x05,
0x06,
0x07,
0x08,
0x09,
0x0a,
0x0b,
0x0c,
0x0d,
0x0e,
0x0f};
-
-
static ssize_t breath_leds_write(
struct file *file,
const
char __user *buf, size_t count, loff_t *ppos)
-
{
-
memset(rec_data,
0,
2);
//清零
-
if (copy_from_user(rec_data, buf,
2))
-
{
-
return -EFAULT;
-
}
-
-
if ((rec_data[
1] &
0x80) !=
0)
-
{
-
turn_on_sn3112();
//开启sn3112
-
-
unsigned
short level = rec_data[
0];
-
unsigned
short reg_index = (rec_data[
1] &
0x0f) -
1;
//数组索引从0开始
-
-
write_iic_reg(pwm_reg_index[reg_index], level);
-
refresh_leds();
-
}
-
else
-
{
-
turn_off_sn3112();
-
}
-
-
return count;
-
}
-
-
//定义cdev结构体,描述字符设备
-
static
struct cdev leds_cdev;
-
-
//创建设备文件(/dev/breath_leds)
-
static
int leds_create_device(
void)
-
{
-
int ret =
0;
-
int err =
0;
-
-
//初始化cdev成员,并建立cdev和file_operations之间的联系
-
cdev_init(&leds_cdev, &dev_fops);
-
leds_cdev.owner = dev_fops.owner;
//cedv_init中没有指定适用模块,故需另指定
-
-
if (major >
0)
//主设备号大于0,通过指定设备号的方式注册字符设备
-
{
-
dev_num =
MKDEV(major, minor);
//获取设备号
-
err = register_chrdev_region(dev_num, DEV_COUNT, DEV_NAME);
-
if (err <
0)
-
{
-
printk(
"wming : register_chrdev_region() failed\n");
-
return err;
-
}
-
}
-
else
-
{
-
err = alloc_chrdev_region(&leds_cdev.dev, minor, DEV_COUNT, DEV_NAME);
//通过自动分配方式注册字符设备,minor这里表示起始次设备号
-
if (err <
0)
-
{
-
printk(
"wming : alloc_chrdev_region() failed\n");
-
return err;
-
}
-
major = MAJOR(leds_cdev.dev);
//获取主设备号
-
minor = MINOR(leds_cdev.dev);
//获取从设备号
-
dev_num = leds_cdev.dev;
//获取设备号
-
}
-
-
//将字符设备添加到内核的字符设备数组中
-
ret = cdev_add(&leds_cdev, dev_num, DEV_COUNT);
-
//创建struct class
-
leds_class = class_create(THIS_MODULE, DEV_NAME);
-
//创建设备文件
-
device_create(leds_class,
NULL, dev_num,
NULL, DEV_NAME);
-
-
return ret;
-
}
-
-
static
int __init breath_leds_init(
void)
-
{
-
int ret;
-
ret = leds_create_device();
-
initial_sn3112();
-
return ret;
-
}
-
-
static
void __exit breath_leds_exit(
void)
-
{
-
device_destroy(leds_class, dev_num);
//销毁字符设备
-
if (leds_class)
-
{
-
class_destroy(leds_class);
//销毁class结构体
-
}
-
unregister_chrdev_region(dev_num, DEV_COUNT);
//注销字符设备区
-
}
-
-
module_init(breath_leds_init);
-
module_exit(breath_leds_exit);
-
module_param(param_level,
int, S_IRUGO | S_IWUSR);
-
-
MODULE_LICENSE(
"GPL");
-
MODULE_AUTHOR(
"wming");
-
MODULE_ALIAS(
"breath_leds");
-
MODULE_DESCRIPTION(
"breathing leds");
由于是单向控制,这里我们只完成了模拟I2C的写通信函数,而没有实现读函数。另外这里我只使用了软关断,硬件一直处于开启状态。
二 配置Makefile
1, 在breath_leds目录下新建Makefile:
-
obj-$(CONFIG_BREATH_LEDS) := breath_leds.o
-
#obj-y := breath_leds.o
2,在breath_leds父目录下的Makefile中,加入:
obj-$(CONFIG_BREATH_LEDS) += breath_leds/
这样编译时才能编译到该驱动。
三 配置Kconfig
在breath_leds目录下新建Kconfig:
-
config BREATH_LEDS
-
tristate
"breath leds driver"
-
default y
tristate表示在编译内核时,breath_leds模块支持三种编译方法:以模块编译,编译进内核和不编译。y表示编译进内核,default y表示默认编译进内核。
四 配置系统的audoconfig
这步只针对MTK才有,MTK自加的驱动基本上都没有Kconfig,都进行了统一配置。打开mediatek/config/$project/autoconfig/kconfig/project,加入
CONFIG_BREATH_LEDS=y
五 编译
./mk $project n k bootimage
即可,打开手机,使用adb shell进入手机文件系统,可看到/dev/breath_leds设备文件。该指令是MTK的模块编译指令,不同的公司一般都会创造自己的一套编译指令。