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