剛剛結束對傳感器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)