前言

在嵌入式課程的專案中,嘗試開發I2C LKM沒有成功,之後自行研究有了一些心得,遂以此為記

環境

Character Driver

  • 驅動是以character driver的形式實現
  • 訊號控制則是透過gpio
  • 測試之後發現效果不佳,即便透過spin_lock_irq()/ spin_unlock_irq()取消中斷,傳輸速度最快也只能達到50KHZ~60KHZ
  • 發現可能原因在於gpio系統函式存取時間過長
    image alt

I2C LKM

  • 另一方面,nanopi提供的使用者函式庫卻可以快速而精準的發出訊號,所以開始trace其功能實現
    image alt
  • 首先查閱cpu(Allwinner H3)的datasheet發現cpu提供了三個I2C控制器,透過memory mapping的方式供使用者操作
  • 使用者函式庫matrix提供的函式是以i2c_smbus_access中的ioctl進入系統核心
  • 雖然知道是透過ioctl檔案系統操作,但問題是相關操作對應之實現是放在哪個模組內?藉由Linux下I2C驅動架構全面分析一文,發現在kernel source code的driver下的i2c目錄包含有i2c相關實現
  • 其中i2c-core.c實現了核心功能以及/proc/bus/i2c介面;而i2c-dev.c則實現了檔案系統相關操作介面
  • 在i2c-dev.c中搜尋ioctl,發現檔案系統操作介面結構初始化如下
    static const struct file_operations i2cdev_fops = {
      .owner		= THIS_MODULE,
      .llseek		= no_llseek,
      .read		= i2cdev_read,
      .write		= i2cdev_write,
      .unlocked_ioctl	= i2cdev_ioctl,
      .open		= i2cdev_open,
      .release	= i2cdev_release,
    };
    
  • 再透過What is the difference between ioctl(), unlocked_ioctl() and compat_ioctl()?一文,了解到kernel自2.6.36之後已將ioctl取代為unlocked_ioctl及compat_ioctl
  • 在i2cdev_ioctl中,依據輸入指令執行不同工作,而使用者函式i2c_smbus_access呼叫ioctl時輸出指令為I2C_SMBUS,因此接下來對應的函式是i2cdev_ioctl_smbus
  • 在i2cdev_ioctl_smbus中會先呼叫copy_from_user讀取使用者參數並進行相關處理,然後再呼叫i2c_smbus_xfer
  • i2c_smbus_xfer定義在i2c-core.c,此時會判斷抽象層adapter是否實現smbus_xfer;若否則呼叫i2c_smbus_xfer_emulated,而i2c_adapter在i2c.h中,其結構如下
     /*
      i2c_adapter is the structure used to identify a physical i2c bus along
      with the access algorithms necessary to access it.
    */
    struct i2c_adapter {
      struct module *owner;
      unsigned int class;		  /* classes to allow probing for */
      const struct i2c_algorithm *algo; /* the algorithm to access the bus */
      void *algo_data;
    
      /* data fields that are valid for all devices	*/
      struct rt_mutex bus_lock;
    
      int timeout;			/* in jiffies */
      int retries;
      struct device dev;		/* the adapter device */
    
      int nr;
      char name[48];
      struct completion dev_released;
    
      struct mutex userspace_clients_lock;
      struct list_head userspace_clients;
    };
    
  • i2c_algorithm結構定義則如下
     /* 
      The following structs are for those who like to implement new bus drivers:
      i2c_algorithm is the interface to a class of hardware solutions which can
      be addressed using the same bus algorithms - i.e. bit-banging or the PCF8584
      to name two of the most common. 
    */
    struct i2c_algorithm {
      /* If an adapter algorithm can't do I2C-level access, set master_xfer
         to NULL. If an adapter algorithm can do SMBus access, set
         smbus_xfer. If set to NULL, the SMBus protocol is simulated
         using common I2C messages */
      /* master_xfer should return the number of messages successfully
         processed, or a negative value on error */
      int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
                 int num);
      int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,
                 unsigned short flags, char read_write,
                 u8 command, int size, union i2c_smbus_data *data);
    
      /* To determine what the adapter supports */
      u32 (*functionality) (struct i2c_adapter *);
    };
    
  • 所以接下來需要先判斷adapter是否實現smbus_xfer,再回到i2c-dev.c查看i2c_dev_init,其中有一段程式碼
    res = bus_register_notifier(&i2c_bus_type, &i2cdev_notifier); 
    
  • 其中i2cdev_notifier變數定義如下
    static struct notifier_block i2cdev_notifier = {
      .notifier_call = i2cdev_notifier_call,
    };
    
  • 在文章Linux usb子系統(一):子系統架構中對bus_register_notifier的用途有所說明

    大多數kernel子系統都是相互獨立的,因此某個子系統可能對其他子系統產生的事件感興趣。為了滿足這個需求,也即是讓某個子系統在發生某個事件時通知其他的子系統,Linuxkernel提供了通知鏈的機制。通知鏈表只能夠在kernel的子系統之間使用,而不能夠在kernel與用戶空間之間進行事件的通知。

  • 所以透過bus_register_notifier註冊之後,系統發現符合i2c類型的設即會呼叫i2cdev_notifier_call,而在函式中可看見有處理兩種狀況:BUS_NOTIFY_ADD_DEVICE、BUS_NOTIFY_DEL_DEVICE,所以發現新裝置會呼叫i2cdev_attach_adapter
  • 在i2cdev_attach_adapter中可看見註冊i2c的device參數、建立devfs以及初使化檔案系統等操作,但並未看見smbus_xfer;同時adapter是由輸入參數device而來,如果要確定則必須找到device參數初始化的地方
  • 因為沒有看到algorithm下之smbus_xfer實現,先假設系統沒有對應的實作,則此時i2c_smbus_xfer會呼叫i2c_smbus_xfer_emulated
  • 在i2c_smbus_xfer_emulated中首先會依據protocol對不同類型資料做處理產生輸出訊息,接著呼叫i2c_transfer傳遞訊息
  • 而i2c_transfer則會呼叫algorithm對應之master_xfer實作
  • 首先在algos資料夾下發現標準實作檔案i2c-algo-bit.c,此檔案有經過核心編譯變產生物件檔,同時裡面包含一組i2c_algorithm實作:
    const struct i2c_algorithm i2c_bit_algo = {
      .master_xfer	= bit_xfer,
      .functionality	= bit_func,
    };
    
  • 此時可以注意到在此algorithm並沒有指定smbus_xfer之實作,另外master_xfer則是對應到bit_xfer
  • 在bit_xfer中則是呼叫readbytes、sendbytes來分別讀、寫訊息
  • 在readbytes、sendbytes中則是又分別呼叫i2c_inb、i2c_outb來進行輸出入
  • 然而在i2c_inb、i2c_outb可以看到資料最終是透過sclhi、scllo、setsda、getsda等指令控制訊號;透過udelay控制時序,這跟我用的方法幾乎相同,但是這樣的作法可以達到精準的400khz(2.5us)週期性控制嘛?
  • 另外在busses資料夾下有一個i2c-sunxi.c實作檔,此檔案同樣經過編譯,而其i2c_algorithm介面如下:
    static const struct i2c_algorithm sunxi_i2c_algorithm = {
      .master_xfer	  = sunxi_i2c_xfer,
      .functionality	  = sunxi_i2c_functionality,
    };
    
  • 同樣的,在這裡也沒有看到沒有指定smbus_xfer對應之介面,所以可以合理懷疑確實沒有smbus_xfer之實作;另外其master_xfer對應到sunxi_i2c_xfer
  • 在sunxi_i2c_xfer中會依據重試次數呼叫sunxi_i2c_do_xfer
  • 而sunxi_i2c_do_xfer則會啟動Allwinner H3的twi(two wire interface)介面的相關設定,包含重置(twi_soft_reset)、啟動中斷(twi_enable_irq)以及相關參數等,最後再呼叫twi_start在對應register寫入TWI_CTL_STA指令、送出i2c start訊號,不過中斷服務函式是在哪裡註冊的呢?
  • 首先觀察模組初始化sunxi_i2c_adap_init,其中一開始呼叫了sunxi_twi_device_scan,很顯然是用來掃描twi週邊裝置
  • 觀察sunxi_twi_device_scan可以發現其功能在於設定twi資源以及裝置,包含twi memory-mapped io相關設定
  • 接著sunxi_i2c_adap_init會呼叫platform_device_register註冊twi裝置、sunxi_i2c_sysfs設定sysfs
  • 最後再呼叫platform_driver_register註冊twi驅動
  • 值得注意的是sunxi_i2c_adap_init是以subsys_initcall macro註冊,根據linux子系统的初始化_subsys_initcall():那些入口函数一文,其含意為”module_init 调用优先级为6低于subsys_initcall调用优先级4.”
  • 另外在檔案中發現註冊之驅動sunxi_i2c_driver內容如下:
    static struct platform_driver sunxi_i2c_driver = {
      .probe		= sunxi_i2c_probe,
      .remove		= __devexit_p(sunxi_i2c_remove),
      .driver		= {
          .name	= SUNXI_TWI_DEV_NAME,
          .owner	= THIS_MODULE,
          .pm		= SUNXI_I2C_DEV_PM_OPS,
      },
    };
    
  • Linux设备模型(5)_device和device driver中提到:

    probe、remove,这两个接口函数用于实现driver逻辑的开始和结束。Driver是一段软件code,因此会有开始和结束两个代码逻辑,就像PC程序,会有一个main函数,main函数的开始就是开始,return的地方就是结束。而内核driver却有其特殊性:在设备模型的结构下,只有driver和device同时存在时,才需要开始执行driver的代码逻辑。这也是probe和remove两个接口名称的由来:检测到了设备和移除了设备(就是为热拔插起的!)。

    设备驱动prove的时机有如下几种(分为自动触发和手动触发):

    • 将struct device类型的变量注册到内核中时自动触发(device_register,device_add,device_create_vargs,device_create)
    • 将struct device_driver类型的变量注册到内核中时自动触发(driver_register)
    • 手动查找同一bus下的所有device_driver,如果有和指定device同名的driver,执行probe操作(device_attach)
    • 手动查找同一bus下的所有device,如果有和指定driver同名的device,执行probe操作(driver_attach)
    • 自行调用driver的probe接口,并在该接口中将该driver绑定到某个device结构中—-即设置dev->driver(device_bind_driver)
  • 所以可以知道sunxi_i2c_probe會在platform_driver_register註冊driver自動被呼叫,而啟動後會開始設定i2c控制結構(struct sunxi_i2c)相關參數,其中有一行指令如下:
    ret = request_irq(irq, sunxi_i2c_handler, IRQF_DISABLED, i2c->adap.name, i2c);
    
  • 所以中斷服務函式sunxi_i2c_handle在初始化的時候就註冊好了,後續在傳送i2c指令時,在函式sunxi_i2c_do_xfer中啟動irq自然就會依照前面設定好的i2c參數開始執行數據收發
  • 另外裡面有一行指令如下:
    i2c->base_addr = ioremap(res->start, resource_size(res));
    
  • 依據Linux內核中ioremap映射的透徹理解一文

    一般來說,在系統運行時,外設的I/O內存資源的物理地址是已知的,由硬體的設計決定。但是CPU通常並沒有為這些已知的外設I/O內存資源的物理地址預定義虛擬地址範圍,驅動程序並不能直接通過物理地址訪問I/O內存資源,而必須將它們映射到核心虛地址空間內(通過頁表),然後才能根據映射所得到的核心虛地址範圍,通過訪內指令訪問這些I/O內存資源。Linux在arch/xxx/include/asm/io.h頭文件中聲明了函數ioremap,用來將I/O內存資源的物理地址映射到核心虛地址空間(3GB-4GB)中,原型如下(各體系結構不一樣):
    void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
    iounmap函數用於取消ioremap所做的映射,原型如下:
    void iounmap(void * addr);

  • 因此這行指令就是在將twi的mmio實體位置(依據cpu spec)映射到虛擬空間,以供後續存取twi控制器之用
  • sunxi_i2c_probec後續還呼叫了sunxi_i2c_hw_init,負責設定硬體如gpio腳位、clock等
  • 然後還呼叫了i2c_add_numbered_adapter,此函數定義在i2c-core.c中,從字面上就可以知道是用來註冊i2c對應之adapter;相較之下,在i2c-algo-bit.c珠其實沒有看到初始化函式
  • 知道了sunxi_i2c_handler啟動機制之後,接下來觀察此函式作用,從程式碼可以看到其中主要工作流程在其呼叫的sunxi_i2c_core_process
  • 進入sunxi_i2c_core_process之後首先會呼叫twi_query_irq_status取得目前狀態,再依照目前狀態執行後續動作
  • 而其中訊息輸出入是呼叫twi_put_byte、twi_get_byte等指令
  • 而在twi_put_byte、twi_get_byte之中其實只是呼叫writel、readl來寫、讀twi資料暫存器,由此可以明確知道i2c-sunxi.c的控制方法是透過mmio控制twi控制器來進行資料傳輸,而很顯然的這樣的硬體控制方法理論上是比較符合之前的實驗結果,然而因為device參數得傳遞部份還未完全釐清,是否有其他證據支持我們的測試結果就是使用這個方法?
  • 一般驅動裝置都會在sysfs產生對應檔案,此時或許可以透過觀察實際產生的檔案來回推i2c實作;觀察初始化程序sunxi_i2c_adap_init發現其中有呼叫建立sysfs的函式sunxi_i2c_sysfs
  • 在sunxi_i2c_sysfs中呼叫了device_create_file在裝置目錄下分別建立了info、status、unittest等屬性檔案,以及各屬性對應的資料顯示函式(show)
  • 觀察開發板上的sysfs的確可以看見相關檔案,也因此證實了實際上系統函式庫是使用硬體控制器來控制i2c訊號
  • 反之,i2c-algo-bit.c之中則沒有看到sysfs相關設定
  • 其實由我們的實驗結果就可以知道,透過軟體方式來控制訊號有其成本,在本實驗平台上大概7~8us一個週期就是極限了;也因此無怪乎開發板要提供硬體控制器

待釐清問題

  • i2cdev_detach_adapter之device參數在那以及如何傳入?
  • sunxi_i2c_adap_init是被誰、什麼時候呼叫?

程式碼

https://github.com/jarvis1984/DHT12