一)编写驱动核心源代码。
即编写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 ytristate表示在编译内核时,breath_leds模块支持三种编译方法:模块,内建和不编译。y表示内建,即编译进内核。
三)配置Makefile
1, 在breath_leds目录下新建Makefile:
obj-$(CONFIG_BREATH_LEDS) := breath_leds.o #obj-y := breath_leds.o2,在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设备文件。