http://blog.csdn.net/airk000/article/details/22655171
刚刚结束对传感器HMC5883L的驱动书写及调试工作,虽然之前对相关的各种知识点都有接触,但是在真正从头书写驱动的时候还是遇到了很多不大不小的麻烦,在这里自行总结一下,也是作为以后驱动书写的一个经验教训,更是对以往所学内核驱动相关知识的复习和总结。事实证明,看了多少书,也不如亲自动手实践学的快,记得牢。
关于I2C
因为手头有几个传感器,都需要用到I2C接口,所以在之前就将I2C子系统复习并深入研究了一番。以下我所提到的或贴出的部分代码也许不适合真正的板级驱动,因为是以模块化形式做测试的。
- 在此模块化驱动中,不仅要注册驱动(i2c_driver),同时也要对设备信息进行注册(i2c_client),我认为在这里不分前后顺序(就像“先有鸡还是先有蛋”的问题一样没有意义)。在前边分析i2c子系统的时候提到过,对于在i2c适配器注册后再添加的新的设备不能再用i2c_register_board_info了,这会导致设备完全不能被激活,而需要用的是i2c_new_device才能将设备动态的注册到系统中
- 在使用i2c_new_device时候不仅需要设备的i2c_board_info结构体,还需要其所依附的I2C适配器总线号。首先,关于总线号,可以通过i2cdetect命令进行查看:
- root@arm:/home/debian# i2cdetect -l
- i2c-0 i2c OMAP I2C adapter I2C adapter
- i2c-1 i2c OMAP I2C adapter I2C adapter
然后,在代码中可以这样使用:
- struct i2c_adapter *adap;
- int adap_nr = 1; // 总线号为1
- adap = i2c_get_adapter(adap_nr);
这样就获取了指定总线号的i2c_adapter指针,之后就可以利用这个指针给i2c_new_device用了。最后需要注意,在注册完设备信息后,要使用i2c_put_adapter(adap)将指针释放掉。
- 用于描述硬件信息的结构体可以做为i2c_client的私有数据保存,而这个结构体中往往也要保存对应的client。这种互相的对应关系应该在probe接口函数中进行:
- static int hmc5883l_probe(struct i2c_client *client, struct i2c_device_id *id)
- {
- ...
- i2c_set_clientdata(client, dev);
- dev->client = client;
- ...
- }
驱动未写,调试先行。如果在开始着手书写驱动前就能直接的通过工具的简单应用对器件进行调试查看的话,会对驱动的书写有很大的帮助。所以这里要说一下关于I2C在shell中的几个调试命令i2cdetect, i2cdump, i2cget, i2cset。首先是i2cdetect,一般用来探测和罗列总线(上边就演示了一下),一般使用方法是:罗列总线->探测有效设备
- i2cdetect -l //罗列现有I2C适配器信息
- i2cdetect -y -r 1 // 查看总线号为1的I2C适配器上挂载的所有设备,如果设备真实有效,则地址会显示出来,而不是UU,UU代表也许有实际设备,但设备可能是忙状态
查看效果如下:
- 0 1 2 3 4 5 6 7 8 9 a b c d e f
- 00: -- -- -- -- -- -- -- -- -- -- -- -- --
- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- 1e --
- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
- 50: -- -- -- -- UU UU UU UU -- -- -- -- -- -- -- --
- 60: -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- --
- 70: -- -- -- -- -- -- -- --
这里就可以看到,设备从地址为0x68和0x1e的设备有实际有效的硬件连接,分别是HMC5883L和AD0接地(不连)的MPU6050。0x54 55 56 57为EEPROM,设备忙。
其次是i2cdump,用来查看器件内部寄存器值,用法为i2cdump -y 总线号 设备地址
- root@arm:/home/debian# i2cdump -y 1 0x1e
- No size specified (using byte-data access)
- 0 1 2 3 4 5 6 7 8 9 a b c d e f 0123456789abcdef
- 00: 10 20 03 01 21 01 4b ff 50 03 48 34 33 00 00 3c ? ??!?K.P?H43..<
- 10: 00 00 00 00 00 00 00 00 00 00 00 94 09 04 e8 10 ...........?????
- 20: 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00 ........?.......
- 30: 00 00 00 14 11 55 56 00 a0 00 07 00 00 00 00 00 ...??UV.?.?.....
- 40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- 50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- 60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- 70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- 80: 10 20 03 01 21 01 4b ff 50 03 48 34 33 00 00 3c ? ??!?K.P?H43..<
- 90: 00 00 00 00 00 00 00 00 00 00 00 94 09 04 e8 10 ...........?????
- a0: 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00 ........?.......
- b0: 00 00 00 14 11 55 56 00 a0 00 07 00 00 00 00 00 ...??UV.?.?.....
- c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
然后是i2cget和i2cset,分别是对寄存器进行获取和写入。用法为i2cget -y 总线号 设备地址 寄存器地址 模式和i2cset -y 总线号 设备地址 寄存器地址 数值 模式。模式默认为b(byte)即读取8bit数据,i2cget可用模式有b/w/c,i2cset可用模式有b/w/c/i/s,其中w为word(16bit),i和s分别为I2C和SMBUS的block数据。
Mutex互斥锁
- 千万不要忘记初始化mutex互斥锁。静态初始化DEFINE_MUTEX(mutex_name),动态初始化mutex_init(struct mutex *lock)。忘记初始化就使用的话是会直接造成内核报错的。
- 在中断上下文中不要使用mutex互斥锁,因为如果出现了竞态,mutex有可能进入睡眠,而中断上下文中是绝对不允许睡眠的。所以千万不要使用,如果一定要在中断中使用锁机制来保护一些驱动资源,建议使用spinlock自旋锁(semaphore信号量也不允许使用,同样的原因)。
- 关注死锁。哪个操作需要进行锁一定要事先自行规划好,不要在某操作一进入的时候锁,而进入其子步骤后又锁,这样就直接死锁了,系统freeze掉。
关于中断
中断的使用很简单,但是却有很多值得注意的细节点。
- GPIO中断。如一些开发板上,外部扩展出来很多GPIO口,但是却找不到IRQ口,所以就需要将GPIO扩展为中断线。在代码中,使用gpio_to_irq(gpio_nr)函数(linux/gpio.h)就可以得到自动转换后的中断线号了,可以用来请求中断。
- 若在request_irq的时候最后给的参数不为NULL,那么在free_irq的时候,第二个参数也就必须与其一致,否则会使系统找不到要释放哪个中断的处理程序句柄(当然了,为NULL就都为NULL,这个没有问题,只要一致就可以)。
工作队列
工作队列分work_struct 和delayed_work。区别就是delayed_work会在指定的延迟后开始运行,而work_struct会立即被调度运行。
- 初始化。INIT_WORK(struct work_struct *work, void (*work_func)(struct work_struct *work))动态初始化work_struct。INIT_DELAYED_WORK(struct delayed_work *work, void (*work_func)(struct work_struct *work))动态初始化delayed_work,在delayed_work的work_func工作函数中,可以通过强制转换将*work转换为struct delayed_work类型。
- 调度。work_struct的调度为schedule_work(struct work_struct *work),而delayed_work则需要另外一个延迟参数schedule_delayed_work(struct delayed_work *work, unsigned long delay),这里的delay参数就是延迟多久后投入工作,单位是jiffies,常会用到类如msecs_to_jiffies(msecs)等转换函数。
- 如果未对工作函数指定队列,那么其会自动进入system_wq中,在驱动中也常会用到定义自己的工作队列,但是简单工作往往没有需要这样做。
- 对于频繁上报信息的工作,最好定义自己的工作队列,将此工作放入自己的工作队列中运行,而不是放入系统默认的system_wq中,这样会避免在系统忙的时候自己的工作被很快调度走,有自己的工作队列在这方面能够起到很大的作用。
completion同步
因为在调试过程中尝试了自检,而又涉及到中断,所以采用了completion作为同步机制,这里提出简单用法。
- 初始化。init_completion(struct completion *wait)
- 等待。wait_for_completion_timeout(struct completion *wait, unsigned long timeout),返回值为剩余时间,如果剩余时间为0,也就是说明超时了。
- 唤醒。complete(struct completion *wait)
代码所在:https://github.com/bbbLinux/projects_hmc5883l