上一篇介绍了本实验所使用的主要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为呼吸灯驱动文件,其中有如下两句:

  1. module_init(breath_leds_init);
  2. module_exit(breath_leds_exit);

即表示呼吸灯驱动被装载时将执行breath_leds_init函数,Linux系统退出时将执行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中定义:

  1. struct cdev {
  2. struct kobject kobj; //封装设备文件的对象
  3. struct module *owner; //指向所用内核模块指针
  4. const struct file_operations *ops; //指向回调的指针
  5. struct list_head list; //指向上一个和下一个cdev机构体指针
  6. dev_t dev; //dev_t是int型数据类型,表示设备号。前12bit表示主设备号,后20bit表示次设备号
  7. unsigned int count; //请求连接的设备编号范围(最大值),建立多设备时使用
  8. };

调用cdev_init初始化大部分cdev成员变量,cdev_init函数代码:

  1. void cdev_init(struct cdev *cdev, const struct file_operations *fops)
  2. {
  3. memset(cdev, 0, sizeofsizeof *cdev);
  4. INIT_LIST_HEAD(&cdev->list); //初始化首尾指针
  5. kobject_init(&cdev->kobj, &ktype_cdev_default); //初始化设备文件对象
  6. cdev->ops = fops; //关联file_operations
  7. }

可以看出,cdev.owner变量并没有在该函数中初始化,所以cdev.owner需要自己另外初始化。本实验中对应代码为:

  1. static struct cdev leds_cdev;
  2. cdev_init(&leds_cdev, &dev_fops);
  3. 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宏可从设备号中获取主设备号和次设备号:

  1. int major = MAJOR(dev_number);
  2. 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 包含了一些与设备文件有关的变量以及一些回调函数指针变量。本实验对应代码如下:

  1. static struct classclass *leds_class = NULL;
  2. 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个函数的作用。函数原型如下:

  1. void device_destory(struct class *class, dev_t devt);
  2. void class_destory(struct class *cls);
  3. 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回调:

  1. static struct file_operations dev_fops =
  2. {
  3. .owner = THIS_MODULE,
  4. .write = breath_leds_write,
  5. };

其中.owner表示这些回调函数使用的模块范围,即只使用于本模块。

第5步:编写业务逻辑

这步无须赘言,指定了回调函数,不能什么都不干吧,必须实现相关功能,如实现上步指定的breath_leds_write函数。

第6步:编写Makefile文件

Linux内核源代码的编译规则都是通过Makefile文件定义的,所以每个Linux驱动必须要有一个Makefile文件。

第7步:编译

可直接编译进内核,也可作为模块单独编译,这步可通过写一个Kconfig进行编译选项配置。

大体上开发Linux驱动也就上面这些步骤,现在记录下本实验驱动开发过程:

一  编写驱动源代码

进入kernel/drivers/目录,新建breath_leds目录,进入该目录,新建breath_leds.c:

  1. #include <linux/module.h>
  2. #include <linux/init.h>
  3. #include <linux/fs.h>
  4. #include <linux/cdev.h>
  5. #include <linux/device.h>
  6. #include <linux/pci.h>
  7. #include <asm/uaccess.h>
  8. #include <linux/kernel.h>
  9. #include <mach/mt_gpio.h>
  10. #include <linux/delay.h>
  11. #define DEV_NAME "breath_leds" //设备名称
  12. #define DEV_COUNT 1 //设备文件数量
  13. #define BREATH_LEDS_MAJOR 0 //默认主设备号
  14. #define BREATH_LEDS_MINOR 1 //默认次设备号
  15. #define IIC_ADDR 0xa8 //i2c 地址
  16. #define SN3112_EN GPIO141
  17. #define SN3112_SCL GPIO102
  18. #define SN3112_SDA GPIO138
  19. #define LED1_3_CON_REG 0x13
  20. #define LED4_9_CON_REG 0x14
  21. #define LED10_12_CON_REG 0x15
  22. #define SET_SCL_PIN_HIGH() (mt_set_gpio_out(SN3112_SCL, GPIO_OUT_ONE))
  23. #define SET_SCL_PIN_LOW() (mt_set_gpio_out(SN3112_SCL, GPIO_OUT_ZERO))
  24. #define SET_SDA_PIN_HIGH() (mt_set_gpio_out(SN3112_SDA, GPIO_OUT_ONE))
  25. #define SET_SDA_PIN_LOW() (mt_set_gpio_out(SN3112_SDA, GPIO_OUT_ZERO))
  26. static ssize_t breath_leds_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos);
  27. static int major = BREATH_LEDS_MAJOR; //主设备号
  28. static int minor = BREATH_LEDS_MINOR; //次设备号
  29. static dev_t dev_num; //设备号
  30. static struct class *leds_class = NULL; //struct class
  31. //通过i2c发送一个字节
  32. static void send_iic_byte(unsigned short data)
  33. {
  34. unsigned short b_mask;
  35. unsigned short i;
  36. b_mask = 0x80; // 1 << 7 : first send MSB
  37. for (i=0; i<8; i++)
  38. {
  39. if ((b_mask & data) != 0)
  40. {
  41. SET_SDA_PIN_HIGH();
  42. }
  43. else
  44. {
  45. SET_SDA_PIN_LOW();
  46. }
  47. udelay(1);
  48. SET_SCL_PIN_HIGH();
  49. udelay(2);
  50. SET_SCL_PIN_LOW();
  51. udelay(1);
  52. b_mask >>= 1;
  53. }
  54. }
  55. //往sn3112寄存器reg_addr中写入数据data
  56. static unsigned short write_iic_reg(unsigned short reg_addr, unsigned short data)
  57. {
  58. unsigned short ack = 0; //for read iic ack
  59. unsigned short iic_addr = IIC_ADDR & 0xfe;
  60. //start condition
  61. SET_SCL_PIN_HIGH();
  62. SET_SDA_PIN_HIGH();
  63. udelay(1);
  64. SET_SDA_PIN_LOW();
  65. udelay(1);
  66. SET_SCL_PIN_LOW();
  67. udelay(1);
  68. //send iic addr
  69. send_iic_byte(iic_addr);
  70. //read ack
  71. SET_SCL_PIN_HIGH();
  72. udelay(1);
  73. SET_SDA_PIN_LOW();
  74. //ack = iic_read_ack();
  75. udelay(3);
  76. SET_SCL_PIN_LOW();
  77. udelay(1);
  78. //send reg addr
  79. send_iic_byte(reg_addr);
  80. //read ack
  81. SET_SCL_PIN_HIGH();
  82. udelay(1);
  83. SET_SDA_PIN_LOW();
  84. //ack = iic_read_ack();
  85. udelay(3);
  86. SET_SCL_PIN_LOW();
  87. udelay(1);
  88. //send data
  89. send_iic_byte(data);
  90. //read ack
  91. SET_SCL_PIN_HIGH();
  92. udelay(1);
  93. SET_SDA_PIN_LOW();
  94. //ack = iic_read_ack();
  95. udelay(3);
  96. SET_SCL_PIN_LOW();
  97. udelay(1);
  98. //stop condition
  99. SET_SCL_PIN_HIGH();
  100. udelay(1);
  101. SET_SDA_PIN_HIGH();
  102. udelay(1);
  103. return ack;
  104. }
  105. //每次写入数据后手动刷新
  106. static void refresh_leds(void)
  107. {
  108. write_iic_reg(0x16, 0x00);
  109. }
  110. static void turn_on_sn3112(void)
  111. {
  112. //硬开启
  113. //mt_set_gpio_out(SN3112_EN, GPIO_OUT_ONE);
  114. //软开启
  115. write_iic_reg(0x00, 0x01);
  116. }
  117. static void turn_off_sn3112(void)
  118. {
  119. //硬关断
  120. //mt_set_gpio_out(SN3112_EN, GPIO_OUT_ZERO);
  121. //软关断   
  122. write_iic_reg(0x00, 0x00);
  123. }
  124. static int param_level = 0xff;
  125. //初始化呼吸灯控制ic sn3112
  126. static void initial_sn3112(void)
  127. {
  128. int i;
  129. //使能sn3112
  130. mt_set_gpio_mode(SN3112_EN, GPIO_MODE_GPIO);
  131. mt_set_gpio_dir(SN3112_EN, GPIO_DIR_OUT);
  132. mt_set_gpio_out(SN3112_EN, GPIO_OUT_ONE);
  133. //配置时钟线为输出模式
  134. mt_set_gpio_mode(SN3112_SCL, GPIO_MODE_GPIO);
  135. mt_set_gpio_dir(SN3112_SCL, GPIO_DIR_OUT);
  136. //配置数据线为输出模式
  137. mt_set_gpio_mode(SN3112_SDA, GPIO_MODE_GPIO);
  138. mt_set_gpio_dir(SN3112_SDA, GPIO_DIR_OUT);
  139. write_iic_reg(0x00,0x01); //设置sn3112工作于标准模式
  140. //12路灯全开
  141. write_iic_reg(LED1_3_CON_REG,0x38);
  142. write_iic_reg(LED4_9_CON_REG,0x3f);
  143. write_iic_reg(LED10_12_CON_REG,0x07);
  144. //设置12路灯初始亮度,控制等亮度的寄存器位0x04~0x0f
  145. for (i=0x04; i<0x10; i++)
  146. {
  147. write_iic_reg(i, param_level);
  148. refresh_leds();
  149. }
  150. }
  151. static struct file_operations dev_fops =
  152. {
  153. .owner = THIS_MODULE,
  154. .write = breath_leds_write,
  155. };
  156. //rec_data[0]:亮度值0~255;rec_data[1]的bit0~3:哪一路led 1~12,bit7:是否打开sn3112,1为打开,0为关闭
  157. static unsigned char rec_data[2];
  158. //每路led对应的led值
  159. static int pwm_reg_index[12] = {0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f};
  160. static ssize_t breath_leds_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
  161. {
  162. memset(rec_data, 0, 2); //清零
  163. if (copy_from_user(rec_data, buf, 2))
  164. {
  165. return -EFAULT;
  166. }
  167. if ((rec_data[1] & 0x80) != 0)
  168. {
  169. turn_on_sn3112(); //开启sn3112
  170. unsigned short level = rec_data[0];
  171. unsigned short reg_index = (rec_data[1] & 0x0f) - 1; //数组索引从0开始
  172. write_iic_reg(pwm_reg_index[reg_index], level);
  173. refresh_leds();
  174. }
  175. else
  176. {
  177. turn_off_sn3112();
  178. }
  179. return count;
  180. }
  181. //定义cdev结构体,描述字符设备
  182. static struct cdev leds_cdev;
  183. //创建设备文件(/dev/breath_leds)
  184. static int leds_create_device(void)
  185. {
  186. int ret = 0;
  187. int err = 0;
  188. //初始化cdev成员,并建立cdev和file_operations之间的联系
  189. cdev_init(&leds_cdev, &dev_fops);
  190. leds_cdev.owner = dev_fops.owner; //cedv_init中没有指定适用模块,故需另指定
  191. if (major > 0) //主设备号大于0,通过指定设备号的方式注册字符设备
  192. {
  193. dev_num = MKDEV(major, minor); //获取设备号
  194. err = register_chrdev_region(dev_num, DEV_COUNT, DEV_NAME);
  195. if (err < 0)
  196. {
  197. printk("wming : register_chrdev_region() failed\n");
  198. return err;
  199. }
  200. }
  201. else
  202. {
  203. err = alloc_chrdev_region(&leds_cdev.dev, minor, DEV_COUNT, DEV_NAME); //通过自动分配方式注册字符设备,minor这里表示起始次设备号
  204. if (err < 0)
  205. {
  206. printk("wming : alloc_chrdev_region() failed\n");
  207. return err;
  208. }
  209. major = MAJOR(leds_cdev.dev); //获取主设备号
  210. minor = MINOR(leds_cdev.dev); //获取从设备号
  211. dev_num = leds_cdev.dev; //获取设备号
  212. }
  213. //将字符设备添加到内核的字符设备数组中
  214. ret = cdev_add(&leds_cdev, dev_num, DEV_COUNT);
  215. //创建struct class
  216. leds_class = class_create(THIS_MODULE, DEV_NAME);
  217. //创建设备文件
  218. device_create(leds_class, NULL, dev_num, NULL, DEV_NAME);
  219. return ret;
  220. }
  221. static int __init breath_leds_init(void)
  222. {
  223. int ret;
  224. ret = leds_create_device();
  225. initial_sn3112();
  226. return ret;
  227. }
  228. static void __exit breath_leds_exit(void)
  229. {
  230. device_destroy(leds_class, dev_num); //销毁字符设备
  231. if (leds_class)
  232. {
  233. class_destroy(leds_class); //销毁class结构体
  234. }
  235. unregister_chrdev_region(dev_num, DEV_COUNT); //注销字符设备区
  236. }
  237. module_init(breath_leds_init);
  238. module_exit(breath_leds_exit);
  239. module_param(param_level, int, S_IRUGO | S_IWUSR);
  240. MODULE_LICENSE("GPL");
  241. MODULE_AUTHOR("wming");
  242. MODULE_ALIAS("breath_leds");
  243. MODULE_DESCRIPTION("breathing leds");

由于是单向控制,这里我们只完成了模拟I2C的写通信函数,而没有实现读函数。另外这里我只使用了软关断,硬件一直处于开启状态。

二  配置Makefile

1, 在breath_leds目录下新建Makefile:

  1. obj-$(CONFIG_BREATH_LEDS) := breath_leds.o
  2. #obj-y := breath_leds.o

2,在breath_leds父目录下的Makefile中,加入:

obj-$(CONFIG_BREATH_LEDS)    += breath_leds/

这样编译时才能编译到该驱动。

三  配置Kconfig

在breath_leds目录下新建Kconfig:

  1. config BREATH_LEDS
  2. tristate "breath leds driver"
  3. 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的模块编译指令,不同的公司一般都会创造自己的一套编译指令。

MT6572平台加入呼吸灯功能——编写linux驱动相关推荐

  1. MT6572平台加入呼吸灯功能——控制芯片SN3112-12简介

    题外话:从Android mmi层转战驱动已达3月,作为方案公司的程序猿,对于MTK的驱动,最大的体会:填表,永无止境:不会填了,请call FAE.所以,时日匆匆,待到年华已逝,填表登峰造极之时,想 ...

  2. 灵动微电子MM32L0系列芯片做呼吸灯功能

    呼吸灯就是指电子产品上的LED灯的亮度随着时间由暗到亮逐渐增强,再由亮到暗逐渐衰减,有节奏感地一起一伏,就像是在呼吸一样,因而被广泛应用于手机.电脑等电子设备的指示灯中.在使用MCU开发相关的应用产品 ...

  3. linux更新驱动脚本,编写Linux驱动常见错误(不断更新)!

    工作中遇到的编写Linux驱动的常见错误和注意事项整理,将不断更新. 问题1.驱动的init函数声明错误 出错: [root@localhost]# insmod phyinfo.ko insmod: ...

  4. android呼吸灯动画,Android高德地图自定义定位蓝点实现呼吸灯功能

    还是先上个图吧: cluster.gif 说下实现的原理,首先这个定位小蓝点是由两张图片组成的,最底层的一张白色圆形图片以及上一层的蓝色圆形图片,只要不停的对蓝色图片进行透明度动画操作就可以实现这个效 ...

  5. 单片机实现LED呼吸灯功能

    ** 程序名: 两键控制亮度 编写人: 编写时间: 2017年12月15日 硬件支持: STC12C2052 内部RC或外部12MHZ晶振 接口说明: 8个LED彩灯接P1口,按键接P3.7和P3.3 ...

  6. 通过元气派达到呼吸灯的效果

    LED呼吸灯¶ 项目编写¶ 在 applications/genkipi/app 下新建 pwm_led01 文件夹 在pwm_led01 下新建 main.c 文件 在pwm_led01 下新建 B ...

  7. ESP32 Arduino开发之路(2)— 使用PWM实现LED呼吸灯

    ESP32 Arduino开发之路(2)- 使用PWM实现LED呼吸灯 一.前言 在ESP32上有一个LEDC外设模块专用于输出PWM波形,其介绍如下所示: 参考文章:在 ESP32 上使用 LEDC ...

  8. android8 呼吸灯,红米note8pro呼吸灯颜色如何设置?

    红米note8pro支持呼吸灯功能,当有未读通知的时候,指示灯就会闪烁提醒.此外,我们还可以根据自己的喜好设置呼吸灯的颜色,下面为大家带来详细的设置教程. 红米note8pro怎么设置呼吸灯颜色 1. ...

  9. 为什么一些手机厂商要砍掉呼吸灯?

    一些手机厂商砍掉手机呼吸灯,我觉得主要原因就是呼吸灯这个功能可有可无,从用户的使用情况来看,呼吸灯功能的争议比较大,有些用户会认为手机非常有必要带呼吸灯功能,能够给日常使用手机的过程中带来便利,而有的 ...

最新文章

  1. 如何使用应用程序库缓存
  2. c#程序设计语言杜松江版_当前最流行的10款编程语言,你会哪种?
  3. JOptionPane的常用4种对话框
  4. java 图形立即显示_java 图形显示
  5. 3分钟,看回归分析模型怎么做
  6. C/C++程序员必读的十本书(上)
  7. 向内存申请一个二维数组空间,并以**P返回的函数
  8. Docker学习总结(23)——Docker搭建大规模测试环境的实践
  9. 路印智能钱包现已上线Google Play商店
  10. 越成功的人越能直面人性的欲望
  11. Scala学习笔记(六):Scala程序
  12. bootstrap之文字排版
  13. Mysql第二章习题
  14. 线段树辅助——扫描线法计算矩形面积并
  15. 微信小程序--引用第三方组件
  16. js如何实现拆分字符串并依次输出
  17. 脑蛋白和计算机意义,冬眠蝙蝠的脑蛋白质组与肝蛋白质组研究
  18. 2022年7月软件著作权登记证书改革后版权局那边现在就是要求高质量发展,有源代码材料的只有积极配合版权工作,才好顺利下证了
  19. 将一维数据(序列)转化为二维数据(图像)的方法汇总GAFS, MTF, Recurrence plot,STFT
  20. C语言基础及STM32基础

热门文章

  1. first path segment in URL cannot contain colon
  2. Bartender3使用教程
  3. 51单片机LCD1602温度时钟万年历带闹钟DS18B20实践制作DIY- GC0046-出租车计价器
  4. STM32移植RT-Thread3.1.5 NANO
  5. I2C + 漏极开路与推挽输出
  6. 译文|借助 Pulsar Functions 迁移到无服务应用程序
  7. 如何利用Android源码定制你想要的系统桌面?
  8. 【120期分享】小清新PPT模板免费下载
  9. ubuntu php 开启pdo扩展
  10. s3c6410开发板NFS挂载linux