一)编写驱动核心源代码。
即编写linux驱动程序,运行于内核空间的代码。这部分基本上和android没什么关系,完全按照linux驱动编程格式来的。包含一个*.c,一个*.h(可有可无),一个Kconfig。一个Makefile。
进入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的写通信函数,而没有实现读函数。另外这里我们只使用了软关断,硬件一直处于开启状态。
这里归纳下创建LED驱动的设备文件五步骤:
第一步:使用cdev_init函数初始化cdev
描述设备文件需要一个cdev结构体:
struct cdev {
struct kobject kobj; //封装设备文件的对象
struct module *owner; //指向内核模块指针
const struct file_operations *ops; //指向file_operations结构体的指针
struct list_head list; //指向上一个和下一个cdev结构体的指针
dev_t dev; //dev_t是int型,表示设备号,前12位表示主设备号,后20位表示次设备号
unsigned int count; //请求的连接设备编号范围,在建立多个设备文件的时候使用
};
cdev的大多数成员变量并不需要我们自己去初始化,只要调用cdev_init就能初始化大部分cdev成员变量,cdev_init函数代码:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list); //初始化首尾指针
kobject_init(&cdev->kobj, &ktype_cdev_default); //初始化设备文件对象
cdev->ops = fops; //关联file_operations
}
可以看出,cdev.owner变量并没有在该函数中初始化,所以cdev.owner需要自己初始化。
第二步:指定设备号
如果要直接指定设备号,需要使用register_chrdev_region函数注册字符设备区域,这样虽然比较直观,但是如果主设备号和次设备号已经存在,建立设备文件就会失败。而使用alloc_chrdev_region函数会自动分配一个未使用的主设备号。alloc_chrdev_region函数原型如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
其中,dev表示设备号指针,即分配的设备号存储地址,baseminor值用来决定怎么分配次设备号,如baseminor为10,则分配的第一个设备文件的次设备号为10,count表示分配的次设备号范围,即分配几个次设备号,如count为3,baseminor为10,则会分配三个次设备号(10、11、12),name表示设备文件名称。
第三步:使用cdev_add函数将字符设备添加到内核中的字符设备数组中
cdev_add函数将字符设备添加到probes数组中,probes数组中保存着已建立的字符设备。
第四步:使用class_create宏创建struct class
struct class 包含了一些与设备文件有关的变量以及一些回调函数指针变量。这一步是为下一步创建设备文件做准备。
第五步:使用device_create函数创建设备文件
最终会生成 /dev/breath_leds 节点。
二)配置Kconfig
在breath_leds目录下新建Kconfig:
config BREATH_LEDS
tristate "breath leds driver"
default y
tristate表示在编译内核时,breath_leds模块支持三种编译方法:模块,内建和不编译。y表示内建,即编译进内核。
三)配置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/
这样编译时才能编译到该驱动
四)配置系统的audoconfig
打开mediatek/config/$project/autoconfig/kconfig/project,加入
CONFIG_BREATH_LEDS=y
MTK自加的驱动模块基本上都没带Kconfig,难道是在该文件中统一指定CONFIG_XXX ?
五)编译
./mk $project n k bootimage
即可,打开手机文件系统,可看到/dev/breath_leds设备文件。