2014年8月28日 星期四

裝置實例-I2C (2)

    接下來就說明如何實現I2C控制器的驅動,當然也要讓這個裝置符合Linux的裝置架構,所以在說明它之前,要先知道它應該掛在哪個bus下。在嵌入式的系統中,這一類的裝置都是獨立屬於SoC記憶體空間的控制器,不依附像是PCI、USB...之類的匯流排,基於這個背景,Linux於是定義了一個虛擬的匯流排,platform_bus,相對於這個匯流排的裝置為platform_device,而驅動程式為platform_driver。因此在XXX_i2c.c中首要工作就是分別建立platform_device和platform_driver,並且註冊它們。

struct resource XXX_i2c_resource[]= { [0]={ .start = CMIC_I2C_CONFIG_BASE, .end = CMIC_I2C_CONFIG_END, .flags = IORESOURCE_MEM, }, [1]={ .start = CMIC_I2C_COMMAND_BASE, .end = CMIC_I2C_COMMAND_END, .flags = IORESOURCE_MEM, }, }

static struct platform_driver XXX_i2c_drv = { .driver = { .owner = THIS_MODULE, .name = "XXX-i2c", }, .probe = XXX_i2c_probe, .remove = XXX_i2c_remove, }

struct platform_device *XXX_i2c_dev; static int __init XXX_i2c_init(void) { int err = 0; XXX_i2c_dev = platform_device_alloc("XXX-i2c", -1); if (XXX_i2c_dev == NULL) { err = -ENOMEM; goto exit; } err = platform_device_add_resources(XXX_i2c_dev,XXX_i2c_resource, ARRAY_SIZE(XXX_i2c_resource)); if (err) goto exit_put; err = platform_device_add(XXX_i2c_dev); if(err) goto exit_put; err = platform_driver_register(&XXX_i2c_drv); if(err) goto exit_unregister; return 0; exit_unregister: platform_device_unregister(XXX_i2c_dev); exit_put: platform_device_put(XXX_i2c_dev); exit: return err; } static void __exit XXX_i2c_exit(void) { platform_driver_unregister(&XXX_i2c_drv); platform_device_unregister(XXX_i2c_dev); platform_device_put(XXX_i2c_dev); }

看完上面的程式碼,只有宣告platform_driver卻沒有發現platform_device,沒錯! 因為這一個驅動是編寫為一個module的形式,所以在載入函式中,platform_device採用動態分配的方式產生,platform_device裡面有一個resource的欄位,主要是用來描述這個控制器會用到的暫存器位址、io-port位址和中斷編號,建立好platform_device後便調用platform_device_add()來註冊這個platform_device。

    一般來說,platfrom_device都是在BSP套件中實現的,以一個arm架構的平台為例,它會被放在arch/arm/mach-XXX/XXX-mach.c中,以靜態的方式宣告如下,XXX可能是平台的名稱。

struce platform_device XXX_i2c_dev = { .name = "XXX-i2c", .id = -1, .num_resources = ARRAY_SIZE(XXX_i2c_resource), .resource = XXX_i2c_resource, };

一個SoC上通常會有很多個platform_device,所以它們會被歸納成一個陣列的形式,之後再調用platform_add_devices()函式統一註冊。

    接著看到platform_driver,完全就跟我們之前自己定義的test_driver一模一樣,一樣都有probe()和remove()方法,不過probe()方法在這裡除了分配記憶體外,還要請求資源並且把實體位址映射到虛擬的位址,之後才能用映射後的惠只存取各個暫存器,另外就是實現i2c_adapter這個資料結構了。卸載函式除了把裝置和驅動卸載外,不要忘了也要釋放資源!

    裝置和驅動都註冊之後不是一定就可以正常工作了喔,不要忘了bus_type的match()方法,match成功後才會調用驅動的probe()方法,那platform_bus會如何判斷驅動支援的裝置呢?讓我們來看看

static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev;

pdev = container_of(dev, struct platform_device, dev);
return (strncmp(pdev->name, drv->name, BUS_ID_SIZE) == 0);
}

是的,跟test_bus_match()做法是一樣的,只比較platform_device和platform_driver兩者的名稱。

struct XXX_i2c_adap { struct i2c_adapter adap; struct i2c_msg *msg; unsigned int msg_num; unsigned int msg_idx; unsigned int buf_len; struct resource *ioarea_cfg; struct resource *ioarea_cmd; void __iomem *regs_cfg; void __iomem *regs_cmd; unsigned int delay_time; };

static struct i2c_algorithm XXX_i2c_algo = { .master_xfer = XXX_i2c_xfer, .functionality = XXX_i2c_func, };

static ssize_t i2c_speed_get(struct device *dev, struct device_attribute *attr, char *buf) { struct i2c_adapter *adap = container_of(dev, struct i2c_adapter, dev); struct XXX_i2c_adap *i2c = container_of(adap, struct XXX_i2c_adap, adap); return sprintf(buf,"%s speed: 400KHz\n", i2c->adap.name); } static ssize_t i2c_speed_set(struct device *dev, struct device_attribute *attr, char *buf, size_t count) { struct i2c_adapter *adap = container_of(dev, struct i2c_adapter, dev); struct XXX_i2c_adap *i2c = container_of(adap, struct XXX_i2c_adap, adap); unsigned long tmp; strict_strtoul(buf, 0, &tmp); if (tmp == 400) __raw_writel(HIGH_SPEED, i2c->regs_cfg+XXX_I2C_TIMING); else if (tmp == 100) __raw_writel(NORMAL_SPEED, i2c->regs_cfg+XXX_I2C_TIMING); else { printk("invalid value\n"); return -EINVAL; } return count; } static DEVICE_ATTR(speed, S_IRUGO|S_IWUSR, i2c_speed_get, i2c_speed_set);

static void __inline i2c_adapter_setup(struct XXX_i2c_adap *i2c) { sprintf(i2c->adap.name, "XXX-i2c"); i2c->adap.owner = THIS_MODULE; i2c->adap.algo = &XXX_i2c_algo; i2c->adap.retries = 3; i2c->adap.class = I2C_CLASS_HWMON; i2c->adap.algo_data = i2c; i2c->delay_time = 50; } static int XXX_i2c_probe(struct platform_device *pdev) { int ret=0; unsigned long i2c_cmd = 0x00000000; struct XXX_i2c_adap *i2c; struct resource *res; i2c = kzalloc(sizeof(struct XXX_i2c_adap), GFP_KERNEL); if(i2c == NULL) return -ENOMEM; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (res == NULL) { ret = -ENOENT; goto free_i2c; } i2c->ioarea_cfg = request_mem_region(res->start, resource_size(res), pdev->name); if (i2c->ioarea_cfg == NULL) { ret = -ENXIO; goto free_i2c; } i2c->regs_cfg = ioremap(res->start, resource_size(res)); if (i2c->regs_cfg == NULL) { ret = -ENXIO; goto release_cfg; } res = platform_get_resource(pdev, IORESOURCE_MEM, 1); if (res == NULL) { ret = -ENOENT; goto unmap_cfg; } i2c->ioarea_cmd = request_mem_region(res->start, resource_size(res), pdev->name); if(i2c->ioarea_cmd == NULL) { printk(KERN_ERR"request mem failed\n"); ret = -ENXIO; goto unmap_cfg; } i2c->regs_cmd = ioremap(res->start, resource_size(res)); if (i2c->regs_cmd == NULL) { ret = -ENXIO; goto release_cmd; } i2c_adapter_setup(i2c); i2c->adap.dev.parent = &pdev->dev; ret = i2c_add_adapter(&i2c->adap); if(ret < 0) goto unmap_cmd; ret = device_create_file(&i2c->adap.dev, &dev_attr_speed); if (ret) goto out; platform_set_drvdata(pdev, i2c); addr = addr << 1; i2c_cmd |= I2C_START; return 0; out: i2c_del_adapter(&i2c->adap); unmap_cmd: iounmap(i2c->regs_cmd); release_cmd: release_mem_region(i2c->ioarea_cmd->start, (i2c->ioarea_cmd->end - i2c->ioarea_cmd->start));
unmap_cfg: iounmap(i2c->regs_cfg); release_cfg: release_mem_region(i2c->ioarea_cfg->start, (i2c->ioarea_cfg->end - i2c->ioarea_cfg->start)); free_i2c: kfree(i2c); return ret; } static int XXX_i2c_remove(struct platform_device *pdev) { struct XXX_i2c_adap *i2c; i2c = platform_get_drvdata(pdev);
device_remove_file(&i2c->adap.dev, &dev_attr_speed); iounmap(i2c->regs_cmd); release_mem_region(i2c->ioarea_cmd->start, (i2c->ioarea_cmd->end - i2c->ioarea_cmd->start)); iounmap(i2c->regs_cfg); release_mem_region(i2c->ioarea_cfg->start, (i2c->ioarea_cfg->end - i2c->ioarea_cfg->start)); i2c_del_adapter(&i2c->adap); kfree(i2c); return 0; }

    上述程式碼中,我們也自己定義的一個資料結構XXX_i2c_adap,當然包含了i2c_adapter結構。i2c_msg結構則是i2c-dev和XXX_i2c這兩層溝通需要傳遞的訊息,結構如下

struct i2c_msg {
__u16 addr;         /* slave address         */
__u16 flags;
__u16 len; /* msg data length */
__u8 *buf; /* pointer to msg data */
};

註解已經解釋的很清楚了,而flag的部分標示了這個msg是讀或是寫,所以讀和寫會是兩個分別的msg,另外還有其他作用,有需要可參考include/linux/uapi/linux/i2c.h檔案。在XXX_i2c_adap還有兩個記錄暫存器base address的欄位,regs_cfg和regs_cmd,之後可以透過這個base address加上每個暫存器的偏移量就可以存去取相對的暫存器了。

    因為我們在resource中宣告了兩塊的記憶體位址,所以在XXX_i2c_probe()函式裡就要分別請求他們的資源並且映射到虛擬的記憶體空間,regs_cfg和regs_cmd就記錄了這兩塊虛擬記憶體的base address,接下來就是建立i2c_adapter結構了,其中比較重要的部分是XXX_i2c_algo結構,其中XXX_i2c_xfer()主要是一些暫存器讀寫的方式和流程,讓實體的SCL和SDA線路上能產生相對應的訊號,master_xfer()這個callback function會在i2c-core.c的i2c_transfer()被呼叫。XXX_i2c_func()記錄這個驅動具備怎樣的功能。完成i2c_adapter結構的建立後就調用i2c_add_adapter()註冊,如果SoC上有多個I2C控制器,那就要依照順序初始化i2c_adapter中的nr欄位,並且改調用i2c_add_numbered_adapter()來註冊。另外還提供了一個speed的屬性檔讓使用者可以設定或是讀取I2C bus的速度,最後把我們自己定義的結構放到platform_device的私有資料欄位去,其實也是調用dev_set_drvdata()

static int __inline is_lastmsg(struct XXX_i2c_adap *i2c) { return i2c->msg_idx == (i2c->msg_num-1); } static int __inline is_lastdata(struct XXX_i2c_adap *i2c) { return i2c->buf_len == 1; } static void __inline clear_event_status(struct XXX_i2c_adap *i2c) { unsigned int event_tmp; event_tmp = __raw_readl(i2c->regs_cmd+XXX_I2C_EVENT_STAT); __raw_writel(event_tmp, i2c->regs_cmd+XXX_I2C_EVENT_STAT); } static int XXX_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg msgs[], int num) { int i, ret=0; unsigned long i2c_cmd, i2c_data, i2c_state, i2c_event, tmp; struct i2c_msg *msg = msgs; unsigned short addr; unsigned char *buffer; struct XXX_i2c_adap *i2c = adap->algo_data; clear_event_status(i2c); i2c_cmd = 0x00000000; i2c->msg_num = num; for (i2c->msg_idx=0; i2c->msg_idx < num; i2c->msg_idx++, msg++) { i2c->msg = msg; addr = msg->addr << 1; i2c->buf_len = msg->len; buffer = msg->buf; if (msg->len == 0) { addr |= (msg->flags == I2C_M_RD)?1:0; __raw_writel(addr, i2c->regs_cmd+XXX_I2C_MST_DATA_WR); break; } if (msg->flags & I2C_M_RD) { addr |= 1; i2c_cmd = BLOCK_READ|i2c->buf_len; __raw_writel(addr, i2c->regs_cmd+XXX_I2C_MST_DATA_WR); } else { __raw_writel(addr, i2c->regs_cmd+XXX_I2C_MST_DATA_WR); for (; i2c->buf_len > 0; i2c->buf_len--, buffer++) { if (is_lastmsg(i2c)) { if (is_lastdata(i2c)) { i2c_data = LAST_DATA|*buffer; __raw_writel(i2c_data, i2c->regs_cmd+XXX_I2C_MST_DATA_WR); i2c_cmd = BLOCK_WRITE; } } i2c_data = *buffer; __raw_writel(i2c_data, i2c->regs_cmd+XXX_I2C_MST_DATA_WR); } } } i2c_cmd |= I2C_START; __raw_writel(i2c_cmd, i2c->regs_cmd+XXX_I2C_MST_CMD); i2c_event = __raw_readl(i2c->regs_cmd+XXX_I2C_EVENT_STAT); while (!(i2c_event & START_BUSY_EVENT)) i2c_event = __raw_readl(i2c->regs_cmd+XXX_I2C_EVENT_STAT); __raw_writel(i2c_event, i2c->regs_cmd+XXX_I2C_EVENT_STAT); i2c_state = __raw_readl(i2c->regs_cmd+XXX_I2C_MST_CMD); if (!(i2c_state & I2C_BUSY)) { if (i2c_stat & I2C_LOST_ARB) { printk(KERN_ERR"Lost arbitration\n"); ret = -EAGAIN; } else if (i2c_state & I2C_DEV_NACK) { printk(KERN_ERR"No device acked\n"); ret = -ENODEV; } else if (i2c_state & I2C_REG_NACK) { printk(KERN_ERR"Register not found\n"); ret = -ENODEV; } else { if ((i2c->msg->flags & I2C_M_RD) && msg->len > 0) { for (i=0; i<i2c->msg->len; i++) { tmp = __raw_readl(i2c->regs_cmd+XXX_I2C_MST_DATA_RD); i2c->msg->buf[i] = tmp & 0xFF; } } ret = i2c->msg_idx; } } else { printk(KERN_ERR"Reset I2C controller\n"); __raw_writel(I2C_RESET, i2c->regs_cfg+XXX_I2C_CONFIG); mdelay(100); __raw_writel(I2C_ENABLE, i2c->regs_cfg+XXX_I2C_CONFIG); ret = -EFAULT; } udelay(i2c->delay_time); return ret; } static u32 XXX_i2c_func(struct i2c_adapter *adap) { return I2C_FUNC_I2C|I2C_FUNC_SMBUS_EMUL; }

    先來看XXX_i2c_func()這個函式,它只是回傳了這個I2C控制器有什麼樣的功能,這邊當然一定有基本的I2C功能,另外還可以模擬SMBUS,SMBUS跟I2C類似,差別是送出資料的格式不同,在訊號上時序的要求也有一點差異。

    XXX_i2c_xfer()函式就是主要I2C控制器的核心部分,這邊是透過對一些暫存器的讀寫讓資料能按照I2C要求的規格和時序傳送到每一個周邊裝置,其實每個SoC的做法都不一樣,下次遇到的時候可能全部要砍掉重練,不過還是記錄一下這次遇到的做法。這裡有一個需要注意的地方,slave address是7個bits(系統沒有需要10個bits周邊,所以沒有實做10個bits的傳輸),可是每次資料傳送都是送8個bits,所以會把msg的addr欄位往左shift一個bit,最後一個bit代表的是讀(1)或寫(0)的動作。



    要讓資料按照正確的格式送到I2C bus上,重點是要依照一定的流程對暫存器讀寫。我們可以把XXX_i2c_xfer()分成兩部分來看,第一部分從所有的i2c_msg中取得slave address、周邊設備的暫存器位址和要寫的資料並把他們都寫入I2C控制器的XXX_I2C_MST_WR暫存器,在XXX_I2C_MST_WR暫存器中的資料最後會被放到一個FIFO中,等待start被觸發後會依照順序送到I2C bus上,流程圖如下。



第一部分的程式碼就是依照這張流程圖寫出來的,還是有可以最佳化的空間。一開始判斷是否有資料要收送,資料長度為0的msg是SMBUS的QUICK command的部分,有一些工具程式會利用這個功能快速的探測整個I2C bus上所有的周邊。再講讀和寫的部份之前,先來看看I2C規格書裡的三張圖


Fig 11是寫資料到周邊,非常直覺應該一看就懂了。Fig 12是從周邊讀資料,可是目前很多週邊並不採用這種格式,反而是採用Fig 13的格式,一開始會先把周邊的slave address和要讀的暫存器位址送出去,這是一個寫的動作,接下來才是從周邊把資料讀回來,所以需要兩個i2c_msg來完成整的讀的流程。

    第一個部分在觸發start後就把資料都送出去了,接下來進到第二個部分,流程圖如下


首先會先等待整個傳輸的過程完成,這裡是一個busy waiting,也可以採用中斷的方式,當資料送出後就把這個task掛到wait queue上,等待中斷被觸發,中斷產生後喚醒task然後處理接下來的工作。接下來就是判斷I2C 控制器是不是仍然busy,會busy通常都是第一部分的流程出錯了,處理方式就是直接reset控制器。至於其他錯誤發生的情況,lost arbitration最常遇到的是在傳輸過程中時序的錯誤造成周邊誤判,導致雖然整個流程結束,可是SDA仍然被周邊佔用,Linux有提供一個機制來修復這個情形,方法是送出9個clock,為什麼是9個clock? 看一看規格書裡面的時序圖就知道囉! i2c_adapter的i2c_bus_recovery_info欄位就是做為這個用途,不過這需要硬體能夠配合,因為要單獨對SCL做high和low的控制。另外兩個錯誤就是找不到周邊或周邊的暫存器了。之前如果是讀動作,資料會被放在FIFO裡,再透過暫存器讀走就可以了。

    整個XXX_i2c.c的內容就寫到這裡,寫的有點多...幾乎大部分嵌入式系統I2C架構都差不多,會不一樣的地方應該主要都是跟硬體相關的部分,如果想深入了解I2C運作的情形,可以參考I2C規格書,如果想看看其他驅動的做法,在kernel原始碼drivers/i2c/資料夾下有其他平台的範例,不過還是要有其他平台的datasheet會比較知道他在做些什麼。下一次就來看看i2c-dev.c啦。

沒有留言:

張貼留言