怯肇乾,吳志亮
(鄭州精益達汽車零部件有限公司,河南 鄭州 100044)
廣泛應用的ARM-Linux及其Android的嵌入式軟硬件系統中,常常涉及到內部整合電路IIC (Inter-Integrated Circuit)總線的操作及其設備的添加與驅動的實現。基于ARM內核的各類微處理器芯片中,通常都會集成一到幾個IIC總線控制器,用以連接存儲器、傳感器、人機界面等IIC設備。沒有操作系統的常規應用中,根據IIC控制器和所選IIC設備的特點,按照IIC通信協議,通過直接明了的編程設計,很容易在系統中實現IIC設備的添加與驅動。而ARM-Linux嵌入式操作系統,對IIC采用分層分散的總線架構,雖然做到了穩定、高效、模塊化,卻使向其中添加需要的IIC設備及其驅動實現困難了很多,常規的驅動設計方法不能用了。ARMLinux下,IIC總線的體系框架是怎樣的?如何因地制宜向其中添加IIC設備并順利實現其驅動程序設計呢?本文以下將展開詳細闡述。
ARM-Linux下,IIC分為3個層次:IIC內核、IIC總線驅動和IIC設備驅動。IIC內核提供核心數據結構的定義和相關接口函數,實現驅動的注冊、注銷管理,設備的探測、檢查,以及無關具體適配器的讀寫通信代碼。IIC總線驅動定義具體的適配器結構i2c_adapter及其算法結構i2c_algorithm,控制適配器以主控方式產生IIC通信時序。IIC內核和IIC總線驅動共同完成硬件上的主機總線控制器驅動。IIC設備驅動定義描述具體設備的i2c_client,以具體的i2c_driver實現從機設備驅動,包括read、write以及ioctl等用戶層接口。ARMLinux-IIC總線驅動的框架結構及其所在的軟/硬件體系如圖1所示。

圖1 ARM-Linux-IIC總線驅動的框架結構圖Fig.1 A framework drawing of ARM-Linux-IICbus driving
從圖1可以看出,在ARM-Linux中添加IIC設備,首先是編寫針對該設備的具體i2c_driver驅動程序。然后是構造描述該IIC設備的i2c_client,并向系統注冊。構造的i2c_client是“橋梁”,完成指定i2c_adapter主機適配控制器與特定i2c_driver設備驅動的有機聯結,從而實現需要的IIC主從串行通信。
一般地,ARM-Linux操作系統提供有實現了IIC內核框架的i2c/i2c-core.c文件及其包括GPIO模擬IIC在內的常用IIC各類通信算法(在目錄i2c/algos/下),還有適用于大多數IIC從設備的i2c/i2c-dev.c文件。針對具體的ARM內核微處理器體系, 如 Samsung-CortexA8 的 S5PV110/S5PV210、TICortexA8 的 AM3515/AM3715、Freescale -CortexA8 的 i.MX515/i.MX535等,半導體廠家通常都為自己具體的IIC總線控制器提供有適配器驅動 (在目錄i2c/algos/下,如i2cs3c2410.c)。
綜上所述,在ARM-Linux中增加IIC新設備,可以借用IIC通用驅動i2c-dev.c快速實現,可以為其編寫特定的IIC驅動。ARM-Linux設備驅動通常采用靜態加載操作,它有兩種方式:適配 (adapter)和探測 (probe),適配是傳統方式(Legacy),探測是新類型(New Style),大多數設備驅動越來越趨向采用probe方式。也可以采用驅動設計人員習慣的動態加載方式編寫簡易的IIC“客服-驅動”。IIC總線,可以采用系統硬件實現的IIC總線控制器,也可以選用通用輸入-輸出端口GPIO模擬產生。如果理解不了ARM-Linux的IIC總線層次框架,也可以拋掉這種機制采用傳統方式編寫IIC字符型設備驅動程序。下面就上述幾種方法,分別說明ARM-Linux-IIC設備的具體添加與驅動實現過程。
需要注意的是,ARM-Linux中,由于IIC總線及其控制器使用的普遍,默認系統是加載IIC總線適配器的。如果選用的平臺沒有采用己有的IIC總線及其控制器,使用ARMLinux-IIC總線驅動框架,首先要在編譯形成系統映像文件前,通過menuconfig配置選擇IIC及其相應的適配器驅動,還有通用或特定的IIC設備驅動。
采用i2c-dev.c直接驅動新添加的IIC設備是最簡便的方法,它實際上是通過在應用層操作IIC適配器來控制i2c設備的。i2c-dev.c是通用的IIC設備驅動,它提供有通用的read()、write()和 ioctl()等設備文件操作接口,應用層可以借用這些接口訪問掛接在適配器上的IIC設備的存儲空間或寄存器,并控制IIC設備的工作方式。需要注意的是,i2c-dev.c對應的read()和write()是分別調用IIC內核心的i2c_master_recv()和 i2c_master_send()函數,通過構造一條 IIC 消息并引發適配器algorithm通信函數的調用來完成消息傳輸的,而大多數稍微復雜一點IIC設備的讀寫流程并不對應于一條消息,往往需要兩條甚至更多的消息來進行一次讀寫周期,即具有IIC重復開始位RepStart的情況,此時將不能正確地讀寫。對于兩條以上消息組成的讀寫即IIC總線RepStart的情況,需要采用ioctl(),組織i2c_msg消息數組并調用I2C_RDWR IOCTL命令來完成。下面以IIC接口無線射頻RFID卡讀寫模塊的標識字節設置操作為例,簡單加以說明。
采用read()和write()操作的主要程序代碼如下:
int main(void)
{ unsigned int fd; unsigned char buf[10];
fd=open("/dev/i2c-1", O_RDWR);
if(!fd) return 0;
ioctl(fd, I2C_SLAVE, 0x58); //設置從機設備地址
ioctl(fd, I2C_TIMEOUT, 1); //設置超時值
ioctl(fd, I2C_RETRIES, 1); //設置重試次數
buf[0]=0x03; buf[1]=0x13;buf[2]=0x13;buf[3]=0x03;
write(fd, buf, 4); //RFID 卡標識字節設置
read(fd, buf, 3); //回讀,以判斷設置是否成功
close(fd); return 0;
}
采用ioctl()操作的主要程序代碼如下,這里實現面向3個連續的IIC從機進行寫入:
int main(void)
{ struct i2c_rdwr_ioctl_data work_queue;
unsigned int i, fd, buf[4]= {0x03, 0x13, 0x13,0x03};
fd=open("/dev/i2c-1", O_RDWR);
if(!fd) return 0;
work_queue.nmsgs=3; //消息數量
work_queue.msgs= (struct i2c_msg*)
malloc (work_queue.nmsgs*sizeof (struct i2c_msg));
for(i=0; i< work_queue.nmsgs; ++i)
{ work_queue.msgs[i].len=4; //數據長度
work_queue.msgs[i].addr=0x58+i;//IIC 從機地址
work_queue.msgs[i].buf=buf; //數據指針
work_queue.msgs[i].flags=I2C_M_WR;//寫命令
}
ioctl(fd, I2C_TIMEOUT, 2); //設置超時
ioctl(fd, I2C_RETRIES, 1); //設置重試次數
ioctl(fd,I2C_RDWR,(unsigned long)&work_queue);
close(fd); return 0;
}
probe方式的驅動,供ARM-Linux-IIC驅動體系,在系統啟動后進行IIC基本功能性探測并加載,在系統關閉時進行“去除”缷載。這種類型的驅動基本上分為3部分:面向用戶的設備文件操作、面向IIC體系的驅動探測與去除、設備模塊的加載初始化與缷載去除,設計的關鍵在于具體i2c_client的構造和ic2_driver的實例化。上述IIC接口RFID卡讀寫模塊probe方式的主要驅動程序代碼編寫如下:
static struct i2c_client*rfid_client; //IIC客戶
static const struct i2c_device_id rfid_i2c_id[]={{"rfid_iic",0}};//IIC設備標識
static int rfid_open(struct inode*inode, struct file*filp )
//設備文件打開
{ filp->private_data=NULL;
try_module_get(THIS_MODULE); return 0;
}
static int rfid_release(struct inode*inode,struct file*filp)
//設備文件關閉
{ filp->private_data=NULL;
module_put(THIS_MODULE); return 0;
}
static int rfid_write (struct file*filp, const char*buffer,size_t count, loff_t*offset) //設備寫操作
{ char*tmp; int ret;
tmp=kzalloc(count, GFP_KERNEL);
//內核數據空間分配
if(tmp==NULL) return -ENOMEM;
if(copy_from_user(tmp, buffer, count))
//數據拷貝:用戶空間-->內核空間
{kfree(tmp); return -EFAULT; }
ret=i2c_master_send(rfid_client, tmp, count);
//調用系統函數,完成數據發送
kfree(tmp); return ret;
}
static int rfid_read (struct file*filp, char*buffer, size_t count,loff_t*offset) //設備讀操作
{ char *tmp; int ret;
tmp=kzalloc(count, GFP_KERNEL);
//內核數據空間分配
if(tmp==NULL)return -ENOMEM;
ret=i2c_master_recv(rfid_client, tmp, count);
//調用系統函數,執行數據接收
if(copy_to_user(buffer, tmp, count))
//數據拷貝:內核空間-->用戶空間
{kfree(tmp); return -EFAULT; }
kfree(tmp); return ret;
}
static structfile_operationsrfid_fops=//設備文件系統實例化
{ owner:THIS_MODULE,
read: rfid_read,
write:rfid_write,
open:rfid_open,
release: rfid_release,
};
static int__devinit rfid_probe (struct i2c_client*client,const struct i2c_device_id*id)//IIC設備探測
{ if (!i2c_check_functionality (client->adapter,I2C_FUNC_I2C)) return -ENODEV;
return 0;
}
static int rfid_remove(struct i2c_client*client)
//IIC設備去除
{ kfree(rfid_client);
i2c_set_clientdata(client, NULL); return 0;
}
static struct i2c_driver rfid_i2c_driver=
//IIC設備驅動實例化
{ .driver={.name="rfidDriver",}, //驅動命名
.probe=rfid_probe, //設備探測
.remove=__devexit_p(rfid_remove), //設備去除
.id_table=rfid_i2c_id, //設備信息標識
};
static int__init rfid_drv_init(void) //IIC 設備初始化
{ int ret;
struct i2c_board_info info;
struct i2c_adapter *adapter;
adapter=i2c_get_adapter(1);
//選擇 IIC 適配器號(0~2)
memset(&info,0,sizeof(struct i2c_board_info)); // 構造IIC設備(板信息-->IIC客戶)
strlcpy(info.type, "rfidClient", I2C_NAME_SIZE);
//設備名字
info.addr=0x58; //從機地址
rfid_client=i2c_new_device(adapter, &info);
//注冊特定的i2c_client
if(!rfid_client) return -ENODEV;
ret=register_chrdev(250, "rfidIIC", &rfid_fops);
//注冊字符設備
if(ret==0)
{ ret=i2c_add_driver(&rfid_i2c_driver);
//指定特定IIC設備驅動
if(ret==0) printk("Rfid(0x058)_Creation success! ");
}
else unregister_chrdev(250, "rfidIIC");
return ret;
}
static void__exit rfid_drv_exit(void) //IIC設備缷載
{ i2c_del_driver(&rfid_i2c_driver);//注銷 IIC 驅動
unregister_chrdev(250, "rfidIIC");
//注銷字符設備
}
MODULE_LICENSE("GPL");
module_init(rfid_drv_init); //Linux 模塊初始化
module_exit(rfid_drv_exit); //Linux 模塊去除
動態加載形式的設備驅動,便于調試,用時掛載,不用時隨時缷載,既使運行時,因而廣泛采用。將probe方式IIC設備驅動的xxx_probe()和xxx_remove()函數分別合并到xxx_init()和xxx_exit()函數,就可以得到動態加載形式的IIC設備驅動。i2c_client的構造仍是IIC設備驅動具體化的關鍵,因此特別稱這種類型的驅動為簡易“客服-驅動”型設備驅動。對上述IIC接口RFID卡讀寫模塊probe方式的驅動作簡易“客服-驅動”型設備驅動改造,變化部分的主要程序代碼如下:
static int__init rfid_drv_init(void) //IIC 設備初始化
{ int ret;
struct i2c_board_info info;
struct i2c_adapter *adapter;
if(!i2c_check_functionality(client->adapter,
//IIC設備基本功能性探測
I2C_FUNC_I2C))return-ENODEV;
adapter=i2c_get_adapter(1);
//選擇 IIC 適配器號(0~2)
memset(&info,0,sizeof(struct i2c_board_info));
//構造IIC設備(板信息-->IIC客戶)
strlcpy(info.type, "rfidClient", I2C_NAME_SIZE);
//設備名字
info.addr=0x58; //從機地址
rfid_client=i2c_new_device(adapter, &info);
//注冊特定的i2c_client
if(!rfid_client) return -ENODEV;
ret=register_chrdev(250, "rfidIIC", &rfid_fops);
//注冊字符設備
if(ret<0) unregister_chrdev(250, "rfidIIC");
return ret;
}
static void__exit rfid_drv_exit(void) //IIC 設備缷載
{ kfree(rfid_client); //i2c_client描述釋放
unregister_chrdev(250, "rfidIIC"); //注銷字符設備
}
MODULE_LICENSE("GPL");
module_init(rfid_drv_init); //Linux 模塊初始化
module_exit(rfid_drv_exit); //Linux 模塊去除
可以看到,相對probe方式的IIC設備驅,簡易“客服-驅動”型IIC設備驅動,沒有了xxx_i2c_driver及其相關函數的實例化設計,整個程序框架結構簡便多了,可設計性與可讀性增加了。
選擇GPIO端口模擬IIC總線驅動IIC設備,雖然對于系統整體效率不高,但是直截了當,易于操作實現。可以采用ARM-Linux已有的GPIO模擬程序,也可以選擇GPIO自行獨立設計。這里仍以上述IIC接口RFID卡讀寫模塊為例,自選GPIO模擬IIC時序,以動態加載形式的字符型IIC簡易“客服-驅動”設計,加以說明。主要程序代碼如下:
#define ByteDelayTimeout 0x0700; //字節傳輸超時值
#define BitDelayTimeout 0x1000; //位傳輸超時值
#define SCL_H{gpio_set_value (S5PV210_GPG3(5), 1);} //IIC串行時鐘線模擬
#define SCL_L{gpio_set_value (S5PV210_GPG3(5),0); }
#define SDA_H {gpio_set_value (S5PV210_GPG3(6),1);} //IIC串行數據線模擬
#define SDA_L{gpio_set_value(S5PV210_GPG3(6),0);}
#define SDA_IN {gpio_direction_input(S5PV210_GPG3(6));} //IIC 串行數據位輸入輸出
#define SDA_OUT {gpio_direction_output(S5PV210_GPG3(6),1); }
#define WHILE_SDA_HIGH (gpio_get_value (S5PV210_GPG3(6)))
static void ByteDelay(void) //字節傳輸延時
{ volatile unsigned int dwTimeout;
dwTimeout=ByteDelayTimeout;
while (--dwTimeout) {asm ("nop") ;}
}
static void BitDelay(void) //位傳輸延時
{ volatile unsigned int dwTimeout;
dwTimeout=BitDelayTimeout;
while ( --dwTimeout) {asm("nop"); }
}
static void I2C_Start(void) //IIC 傳輸啟動位模擬
{ SDA_OUT; SDA_H; BitDelay();
SCL_H; BitDelay(); SDA_L; BitDelay();
}
static void I2C_Stop(void) //IIC傳輸停止位模擬
{ SDA_OUT; SDA_L; BitDelay();
SCL_H; BitDelay(); SDA_H; BitDelay();
}
static void I2C_Ack(void) //IIC 傳輸響應位
{ SDA_OUT; SDA_L; BitDelay();
SCL_H; BitDelay(); SCL_L;
BitDelay(); SDA_IN; BitDelay();
}
static void I2C_Ack1(void) //IIC傳輸響應位(帶延時)
{ int i=0;
SCL_H; BitDelay(); SDA_IN;
while((WHILE_SDA_HIGH)&&(i<255)) i++; //無
應答延時一段時間后默認已經收到
SCL_L; BitDelay(); SDA_OUT; BitDelay();
}
static void I2C_Nack(void) //IIC傳輸非響應位
{ SDA_OUT; SDA_H; BitDelay();
SCL_H; BitDelay(); SCL_L;
BitDelay(); SCL_H;
}
static char Write_I2C_Byte(char byte)
//IIC總線字節寫操作
{ char i;
SCL_L; BitDelay ();
for(i=0 ; i<8; i++)
{if((byte&0x80)==0x80) SDA_H;
else SDA_L;
BitDelay(); SCL_H; BitDelay();
SCL_L; BitDelay(); byte <<=1;
}
return 1;
}
static char Read_I2C_Byte(void) //IIC總線字節讀操作
{ char i, buff=0;
SCL_L; BitDelay();
for(i=0; i< 8; i++)
{SDA_OUT;SDA_H;BitDelay();
SCL_H; SDA_IN; BitDelay();
if(WHILE_SDA_HIGH) buff|=0x01;
else buff&=~0x01;
if(i<7) buff<<=1 ;
SCL_L; BitDelay();
}
return buff;
}
static void InterfaceInit(void) //IIC接口的GPIO定義
{ gpio_direction_output(S5PV210_GPG3(5), 1);
//SCL OUT
gpio_direction_output(S5PV210_GPG3(6), 1);
//SDA OUT
gpio_set_value (S5PV210_GPG3(5), 1);
//初始高電平
gpio_set_value (S5PV210_GPG3(6), 1);
ByteDelay(); ByteDelay(); ByteDelay();
}
static int dvcIIC_open (struct inode*inode_ptr, struct file
*fptr) //設備文件打開
{ fptr->f_op=&dvcIIC_fops;
fptr->private_data=NULL;
try_module_get(THIS_MODULE); return 0;
}
static int dvcIIC_release (struct inode*inode_ptr, struct file*fptr)//設備文件關閉
{ //fptr->private_data=NULL;
module_put(THIS_MODULE); return 0 ;
}
static int dvcIIC_read(struct file*fptr, char*buffer, size_t count,loff_t*fp) //設備文件讀
{ int i;char data[100];
I2C_Start(); //啟動IIC傳輸,發送從機設備地址
Write_I2C_Byte((0x58<<1)+1);
I2C_Ack1();
for(i=0;i<count;i++) //按字節讀入數據
{data[i]=Read_I2C_Byte();I2C_Ack();}
I2C_Nack();I2C_Stop();
//發送非響應位/停止位,結束操作
if(copy_to_user(buffer,data,count))return-1;
//向用戶空間回傳數據
else return 0;
}
static int dvcIIC_write(struct file*fptr,const char*buffer,size_t size,loff_t*fp)//設備文件寫
{ int i;char data[100];
if(copy_from_user(data,buffer,size))return-1;
//從用戶空間接收數據
I2C_Start(); //啟動IIC傳輸,發送從機設備地址
Write_I2C_Byte((0x58<<1));I2C_Ack1();
for(i=0;i<size;i++) //按字節寫入數據
{Write_I2C_Byte(data[i]);I2C_Ack1();}
I2C_Stop();I2C_Nack(); //發送非響應位/停止位,結束操作
I2C_Stop();return 0;
}
static struct file_operations dvcIIC_fops=//設備文件操作接口
{ .owner=THIS_MODULE,
.open=dvcIIC_open,
.read=dvcIIC_read,
.write=dvcIIC_write,
.release=dvcIIC_release,
};
static int dvcIIC_init(void)//IIC設備初始化:字符設備注冊
{ int status;
InterfaceInit();
status=register_chrdev(239,"dvcIIC",&dvcIIC_fops);
if(status<0)return status;
return 0;
}
static void dvcIIC_exit(void) //IIC設備注銷
{ unregister_chrdev(239,"dvcIIC");}
MODULE_LICENSE("GPL");
module_init(dvcIIC_init); //驅動模塊初始化
module_exit(dvcIIC_exit); //驅動模塊去除
ARM-Linux-IIC設備的添加與驅動,可以根據主機控制器與從機設備的特點采用傳統字符型設備驅動的方式實現;也可以根據ARM-Linux-IIC總線設備層次驅動的軟件特點,對常規IIC設備采用通用i2c-dev.c快速驅動,對特定IIC設備設計probe流行方式的驅動或動態加載的簡易IIC“客服-驅動”;還可以選用GPIO模擬IIC總線快速驅動設備。其中,根據ARM-Linux-IIC層次驅動的總線架構設計IIC設備驅動,稍顯繁瑣,不易理解,但更符合ARM-Linux總線驅動模塊化的規范,能夠有效地融入ARM-Linux體系,充分利用Linux內核資源,實現Linux系統的穩定、高效,是應該主動選擇與推薦使用的。
[1]怯肇乾.基于底層硬體的軟件設計[M].北京:航空航天大學出版社,2008.
[2]宋寶華.Linux設備驅動開發詳解[M].北京:人民郵電出版社,2008.
[3]劉紅波.實例解析Linux內核I2C體系結構 [EB/OL](2009-12).http://www.dzsc.com/data/html/2009-12-22/81040.html.
[4]杜博,方向忠.嵌入式Linux系統下I2C設備驅動程序的開發[J].微計算機信息,2006,22(11):21-23.DU Bo,FANG Xiang-zhong.I2Cdevice driver development in embedded Linux OS[J].Microcomputer Information,2006,22(11):21-23.
[5]李力,厲謹.利用GPIO模擬I2C總線協議 [J].科技風,2009(12):33-36.LI Li,LI Jin.Simulating I2C bus communication with GPIO[J].Science-Technology Wind,2009(12):33-36.
[6]楊文鉑,刑鵬康.Linux下I2C設備驅動的一種適配器層直接實現方法[J].單片機與嵌入式系統應用,2011,11(6):16-19.YANGWen-bo,XING Peng-kang.A I2Cdevice driver method about adapter layer in linux OS[J].Single Chip Microcomputer and Embedded System Application,2011,11(6):16-19.
[7]蘇纓墩,鐘漢如.嵌入式Linux中的Nand Flash驅動詳解[J].工業儀表與自動化裝置,2011(4):56-60.SU Ying-dun,ZHONG Han-ru.A detail explication on Nand Flash drive in embedded Linux[J].Industrial Instrumentation&Automation,2011(4):56-60.