《Essential Linux Device Drivers》第8章

来源:互联网 发布:网络风险投资 编辑:程序博客网 时间:2024/04/30 13:40
 

8 I2C协议

I2CThe Inter-Integrated Circuit)及其子集SMBusSystem Management Bus)均为同步串行接口,普遍存在于桌面电脑和嵌入式设备中。本章通过实现访问I2C EEPROMI2C RTC的驱动实例,让我们理解内核如何支持I2C/SMBus主机适配器和客户设备。在结束本章之前,让我们也浏览一下内核支持的两种其它的串行接口:串行外围接口(SPI)总线和1-wire总线。

所有这些串行接口(I2CSMBusSPI1-wire),都有两个共同的特性:

·         交换的数据总量较少。

·         数据传输率较低。

I2C/SMBus是什么?

I2C是广泛用于桌面和笔记本电脑中的串行总线,用于处理器和一些外设之间的接口,这些外设包括EEPROM音频编解码器以及监控温度、供电电压等参数的专用芯片。此外,I2C也在嵌入式设备中大行其道,用于和RTC,智能电池电路、多路复用器,端口扩展卡,光收发器,以及其它类似设备之间的通信。由于I2C被大量的微控制器所支持,在当前的市场上可找到大量便宜的I2C设备。

I2CSMBus为主-从协议,其通信双方为主机适配器(主控制器)和客户设备(从设备)。主机控制器在桌面电脑上通常为南桥芯片组的一部分;而在嵌入式设备上,通常为微控制器的一部分。图8.1显示了在PC兼容硬件上I2C总线的例子。

8.1. PC兼容硬件上I2C/SMBus.

I2C及其子集SMBus最初分别为Philips Intel所开发,均为2线接口。这2根线为时钟线和双向数据线,分别被称为串行时钟(Serial ClockSCL)和串行数据(Serial DataSDA)。由于I2C总线仅需要一对总线,因此在电路板上占用了更少的空间。因此带来的问题是带宽较窄。I2C在标准模式下支持最高100Kbps的传输率,在快速模式下最高可达400Kbps(然而,SMBus最高仅支持100Kbps)。因此它们仅适用于慢速设备。即使I2C支持双向数据交换,因为仅有一根数据线,故通信是半双工的。

I2CSMBus设备使用7位地址。协议也支持10位地址,但很多设备仅响应7位地址,故在总线上最多有127个设备。源于协议的主-从特性,设备地址也称为从地址。

I2C核心

I2C核心由主机适配器驱动和客户驱动可利用的函数和数据结构组成。核心中的公共代码简化了驱动开发者的工作。核心也间接使客户驱动独立于主机适配器,以使客户设备即使用于采用不同I2C主机适配器的电路板上,亦可保持客户驱动运行如常。核心层的此机制及其好处也可在内核中其它的很多设备驱动类中发现,如PCMCIAPCIUSB等。

除了核心,内核的I2C底层由如下组成:

·         I2C主机适配器的设备驱动。属于总线驱动,通常由适配器驱动和算法(algorithm)驱动组成。前者利用后者和I2C总线交互。

·         I2C客户设备的设备驱动。

·         i2c-dev,允许在用户模式下实现I2C客户驱动。

你更可能的是实现客户驱动,而不是适配器或algorithm驱动,因为相比于I2C主机适配器,有多得多的I2C设备。因此在本章中,我们将主要讨论客户驱动。

8.2展示了LinuxI2C子系统。它显示了I2C内核模块和I2C总线上的主机适配器和客户设备的交互。

8.2. Linux I2C子系统

由于SMBusI2C的子集,因此仅使用SMBus指令和你的设备交互的驱动可工作于SMBusI2C适配器。表8.1列出了I2C核心提供的和SMBus兼容的数据传输流程。

8.1. I2C核心提供的和SMBus兼容的数据访问函数

函数

作用

i2c_smbus_read_byte()

从设备读取一个字节(不定义位置偏移,使用以前发起的命令的偏移)

i2c_smbus_write_byte()

从设备写入一个字节(使用以前发起的命令的偏移)

i2c_smbus_write_quick()

向设备发送一个比特 (取代清单8.1中的Rd/Wr).

i2c_smbus_read_byte_data()

从设备指定偏移处读取一个字节

i2c_smbus_write_byte_data()

向设备指定偏移处写入一个字节

i2c_smbus_read_word_data()

从设备指定偏移处读取二个字节

i2c_smbus_write_word_data()

向设备指定偏移处写入二个字节

i2c_smbus_read_block_data()

从设备指定偏移处读取一块数据.

i2c_smbus_write_block_data()

向设备指定偏移处写入一块数据. (<= 32字节)

总线事务

在实现驱动例子之前,通过放大镜观察导线,让我们来更好的理解I2C协议。清单8.1展示了和I2C EEPROM交互的代码片断,以及在总线上发生的相应的事务。这些事务是在运行代码片断时通过相连的I2C总线分析仪捕获的。这些代码使用的时用户模式的I2C函数(在第19用户空间的驱动中,我们将讨论更多的用户模式的I2C编程)。

清单8.1. I2C总线上的事务

Code View:

/* ... */

/*

 * Connect to the EEPROM. 0x50 is the device address.

 * smbus_fp is a file pointer into the SMBus device.

 */

ioctl(smbus_fp, 0x50, slave);

 

/* Write a byte (0xAB) at memory offset 0 on the EEPROM */

i2c_smbus_write_byte_data(smbus_fp, 0, 0xAB);

 

/*

 * This is the corresponding transaction observed

 * on the bus after the write:

 * S 0x50 Wr [A] 0 [A] 0xAB [A] P

 *

 * S is the start bit, 0x50 is the 7-bit slave address (0101000b),

 * Wr is the write command (0b), A is the Accept bit (or

 * acknowledgment) received by the host from the slave, 0 is the

 * address offset on the slave device where the byte is to be

 * written, 0xAB is the data to be written, and P is the stop bit.

 * The data enclosed within [] is sent from the slave to the

 * host, while the rest of the bits are sent by the host to the

 * slave.

 */

/* Read a byte from offset 0 on the EEPROM */

res = i2c_smbus_read_byte_data(smbus_fp, 0);

 

/*

 * This is the corresponding transaction observed

 * on the bus after the read:

 * S 0x50 Wr [A] 0 [A] S 0x50 Rd [A] [0xAB] NA P

 *

 * The explanation of the bits is the same as before, except that

 * Rd stands for the Read command (1b), 0xAB is the data received

 * from the slave, and NA is the Reverse Accept bit (or the

 * acknowledgment sent by the host to the slave).

 */

 

                                    

 

设备例子:EEPROM

我们的第一个客户驱动例子时I2C总线上的EEPROM设置,如图8.1所示。几乎所有的笔记本和桌面电脑都有类似的EEPROM,用于存储BIOS配置信息。例子中的EEPROM有两个memory bank。相对应于每个memory bank,驱动提供/dev接口:/dev/eep/0 /dev/eep/1。应用程序在这些节点上操作,和EEPROM交换数据。

每个I2C/SMBus客户设备都分配有一个从地址,作为设备标识。例子中的EEPROM有两个从地址,SLAVE_ADDR1SLAVE_ADDR2,分别对应于两个memory bank

例子中驱动所使用的I2C指令和SMBus兼容,因此它可以工作于I2CSMBus EEPROM

初始化

正如所有的驱动类那样,I2C客户驱动也有init()入口点。初始化用于分配数据结构,向I2C核心注册驱动,将sysfsLinux设备模型联系在一起。这些在清单8.2中完成。

清单 8.2. 初始化EEPROM驱动

Code View:

/* Driver entry points */

static struct file_operations eep_fops = {

  .owner   = THIS_MODULE,

  .llseek  = eep_llseek,

  .read    = eep_read,

  .ioctl   = eep_ioctl,

  .open    = eep_open,

  .release = eep_release,

  .write   = eep_write,

};

 

static dev_t dev_number;          /* Allotted Device Number */

static struct class *eep_class;   /* Device class */

 

/* Per-device client data structure for each

 * memory bank supported by the driver

 */

 

struct eep_bank {

  struct i2c_client *client;      /* I2C client for this bank */

  unsigned int addr;              /* Slave address of this bank */

  unsigned short current_pointer; /* File pointer */

  int bank_number;                /* Actual memory bank number */

  /* ... */                       /* Spinlocks, data cache for

                                     slow devices,.. */

};

 

#define NUM_BANKS 2               /* Two supported banks */

#define BANK_SIZE 2048            /* Size of each bank */

 

struct ee_bank *ee_bank_list;     /* List of private data

                                     structures, one per bank */

 

 

/*

 * Device Initialization

 */

int __init

eep_init(void)

{

 

  int err, i;

 

  /* Allocate the per-device data structure, ee_bank */

  ee_bank_list = kmalloc(sizeof(struct ee_bank)*NUM_BANKS,

                         GFP_KERNEL);

  memset(ee_bank_list, 0, sizeof(struct ee_bank)*NUM_BANKS);

  /* Register and create the /dev interfaces to access the EEPROM

     banks. Refer back to Chapter 5, "Character Drivers" for

     more details */

  if (alloc_chrdev_region(&dev_number, 0,

                          NUM_BANKS, "eep") < 0) {

    printk(KERN_DEBUG "Can't register device/n");

    return -1;

  }

 

  eep_class = class_create(THIS_MODULE, DEVICE_NAME);

  for (i=0; i < NUM_BANKS;i++) {

 

    /* Connect the file operations with cdev */

    cdev_init(&ee_bank[i].cdev, &ee_fops);

 

    /* Connect the major/minor number to the cdev */

    if (cdev_add(&ee_bank[i].cdev, (dev_number + i), 1)) {

      printk("Bad kmalloc/n");

      return 1;

    }

    class_device_create(eep_class, NULL, (dev_number + i),

                           NULL, "eeprom%d", i);

  }

 

  /* Inform the I2C core about our existence. See the section

     "Probing the Device" for the definition of eep_driver */

  err = i2c_add_driver(&eep_driver);

 

  if (err) {

    printk("Registering I2C driver failed, errno is %d/n", err);

    return err;

  }

 

  printk("EEPROM Driver Initialized./n");

  return 0;

}

 

                                    

 

清单8.2发起了设备节点的创建,但为了完成此过程,需要添加如下内容至/etc/udev/rules.d/目录下合适的规则文件中:

KERNEL:"eeprom[0-1]*", NAME="eep/%n"

作为从内核收到的uevent的响应,将创建/dev/eep/0/dev/eep/1。需要从第nmemory bank读取数据的用户模式程序可以操作/dev/eep/n来达到其目的。

清单8.3实现了EEPROM驱动的open()函数。当应用程序打开/dev/eep/X时,内核将调用eep_open()eep_open()在私有区域中存储了每个设备相关的数据结构,因此可以从驱动中的其它函数中直接访问。

清单 8.3. 打开EEPROM驱动

 int

eep_open(struct inode *inode, struct file *file)

{

 

  /* The EEPROM bank to be opened */

  n = MINOR(file->f_dentry->d_inode->i_rdev);

 

  file->private_data = (struct ee_bank *)ee_bank_list[n];

 

  /* Initialize the fields in ee_bank_list[n] such as

     size, slave address, and the current file pointer */

  /* ... */

}

 

探测设备

I2C客户驱动,在主机控制器驱动和I2C核心的合作下,使其自身成为从设备,其过程如下:

1.     在初始化过程中,注册probe()方法。当相连的主机控制器被检测出,I2C核心将调用此方法。在清单8.2中,eep_init()通过调用i2c_add_driver()注册eep_probe()

static struct i2c_driver eep_driver =

{

  .driver = {

    .name         =  "EEP",           /* Name */

  },

  .id             = I2C_DRIVERID_EEP, /* ID */

  .attach_adapter = eep_probe,        /* Probe Method */

  .detach_client  = eep_detach,       /* Detach Method */

};

 

i2c_add_driver(&eep_driver);  `

设备标识符I2C_DRIVERID_EEP,对于每个设备应该是唯一的,并在include/linux/i2c-id.h中定义之。

2.     I2C核心调用客户驱动的probe()方法时,表明主机适配器已经存在。在probe()里将调用i2c_probe(),其参数为驱动所关联的从设备的地址,以及具体的探测函数attach()

清单8.4实现了EEPROM驱动的probe()方法:eep_probe()normal_i2c指明了EEPROM bank的地址,它是i2c_client_address_data结构体的成员。此结构体中其它的成员能被用于更多的地址控制。你可以通过设置ignore字段要求I2C核心忽略一段地址范围。如果你想绑定一个从地址到一个特殊的主机适配器上,你也可以使用probe成员指定(适配器、从地址)对。对于某些场合,这样做有其用处。例如,你的处理器支持两个I2C主机适配器,在总线1 有一个EEPROM,总线2上有一个温度传感器,两个设备从地址相同。

3.     主机控制器在总线上搜索步骤2中指定的从设备。为此,它产生一个总线事务,例如S SLAVE_ADDR WrS是起始位,SLAVE_ADDR是设备的数据手册中指定的7bit的从地址,Wr总线事务一节中所描述过的写命令。如果某个运行中的从设备存在于总线上,它将发送确认比特([A])加以回应。

 

4.     在步骤3中如果主机适配器检测到从设备,I2C核心会调用步骤2中在i2c_probe()的第三个参数中指定的attach()。对于EEPROM驱动,此例程为eep_attach(),它将注册和设备关联的客户数据结构,如清单8.5所示。如果你的设备需要初始的编程序列(例如,在数字视频接口(Digital Visual Interface DVI)传输芯片开始工作之前,必须对它的寄存器进行初始化),可在此例程中完成这些操作。

清单8.4. 探测EEPROM Bank的存在

#include <linux/i2c.h>

 

/* The EEPROM has two memory banks having addresses SLAVE_ADDR1

 * and SLAVE_ADDR2, respectively

 */

static unsigned short normal_i2c[] = {

  SLAVE_ADDR1, SLAVE_ADDR2, I2C_CLIENT_END

};

 

static struct i2c_client_address_data addr_data = {

  .normal_i2c = normal_i2c,

  .probe      = ignore,

  .ignore     = ignore,

  .forces     = ignore,

};

 

static int

eep_probe(struct i2c_adapter *adapter)

{

  /* The callback function eep_attach(), is shown

   * in Listing 8.5

   */

   return i2c_probe(adapter, &addr_data, eep_attach);

}

 

清单8.5. Attaching a Client

int

eep_attach(struct i2c_adapter *adapter, int address, int kind)

{

  static struct i2c_client *eep_client;

 

  eep_client = kmalloc(sizeof(*eep_client), GFP_KERNEL);

 

  eep_client->driver  = &eep_driver; /* Registered in Listing 8.2 */

  eep_client->addr    = address;     /* Detected Address */

  eep_client->adapter = adapter;     /* Host Adapter */

  eep_client->flags   = 0;

  strlcpy(eep_client->name, "eep", I2C_NAME_SIZE);

 

  /* Populate fields in the associated per-device data structure */

  /* ... */

 

  /* Attach */

  i2c_attach_client(new_client);

}

 

检查适配器的功能

每个主机适配器的功能都有限。一个适配器可能不支持表8.1中包含的所有命令。例如,它可能支持SMBus read_word命令,但不支持read_block命令。客户驱动在使用这些命令前必须检查适配器是否对其提供支持。

I2C核心提供两个函数以完成此功能:

1.     i2c_check_functionality()检查某个特定的功能是否被支持。

2.     i2c_get_functionality()返回包含所有被支持功能的掩码。

include/linux/i2c.h可看到所有可能支持功能的列表。

访问设备

为了从EEPROM读取数据,首先需要从与此设备节点关联的私有数据域中收集调用线程的信息。其次,使用I2C核心提供的SMBus兼容的数据访问例程(表8.1显示了可用的函数)读取数据。最后,发送数据至用户空间,并增加内部文件指针,以便下一次的read()/write()操作可以从上一次结束处开始。这些步骤在清单8.6中完成,此清单忽略了通常的完整性和错误检查。

清单8.6. EEPROM读取数据

Code View:

ssize_t

eep_read(struct file *file, char *buf,

         size_t count, loff_t *ppos)

{

  int i, transferred, ret, my_buf[BANK_SIZE];

 

  /* Get the private client data structure for this bank */

  struct ee_bank *my_bank =

                   (struct ee_bank *)file->private_data;

 

  /* Check whether the smbus_read_word() functionality is

     supported */

  if (i2c_check_functionality(my_bank->client,

                              I2C_FUNC_SMBUS_READ_WORD_DATA)) {

 

    /* Read the data */

    while (transferred < count) {

      ret = i2c_smbus_read_word_data(my_bank->client,

                                     my_bank->current_pointer+i);

      my_buf[i++] = (u8)(ret & 0xFF);

      my_buf[i++] = (u8)(ret >> 8);

      transferred += 2;

    }

 

    /* Copy data to user space and increment the internal

       file pointer. Sanity checks are omitted for simplicity */

    copy_to_user(buffer, (void *)my_buf, transferred);

    my_bank->current_pointer += transferred;

  }

 

  return transferred;

}

 

                                    

 

写数据至设备与此类似,使用的是i2c_smbus_write_XXX()函数。

一些EEPROM芯片有RFIDRadio Frequency Identification)发送器,用于无线发送存储的信息。用于自动化供应链处理,例如货物监控和资产跟踪。这些EEPROM通常通过一个访问保护bank来控制对数据bank的安全访问。对于此类情况,为了能够操作数据bank,驱动还不得不对访问保护bank中的相应位进行处理。

为了从用户空间访问EEPROM块,需要开发应用程序以操作/dev/eep/n。为了读出EEPROM块的内容,需要做如下操作:

bash> od –a /dev/eep/0

0000000   S   E   R   # dc4  ff soh   R   P nul nul nul nul nul nul nul

0000020   @   1   3   R   1   1   5   3   Z   J   1   V   1   L   4   6

0000040   5   1   0   H  sp   1   S   2   8   8   8   7   J   U   9   9

0000060   H   0   0   6   6 nul nul nul  bs   3   8   L   5   0   0   3

0000100   Z   J   1   N   U   B   4   6   8   6   V   7 nul nul nul nul

0000120 nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul

*

0000400

作为练习,试着去修改EEPROM驱动,创建EEPROM块的/sys接口,而不是/dev接口。你可以重用第5章清单5.7“使用Sysfs 控制并口LED电路板中的代码,帮助完成此工作。

其它函数

为了获得全功能的驱动,你需要添加剩余的入口点。这些和第5章所讨论的普通字符驱动差别不大,因此未提供代码清单:

·         为了支持lseek()系统调用,用于给内部文件指针赋以新值,需要实现llseek()函数。内部文件指针存储了有关EEPROM访问的状态信息。

·         为了验证数据的完整性,EEPROM驱动可实现ioctl()函数,用于校准并验证存储的数据的校验和。

·         EEPROM中不需要poll()fsync()方法。

·         如果你选择将驱动编译为一个模块,需要提供exit()方法,以注销设备,并清理客户设备特定的数据结构。从i2c核心卸载驱动只需执行如下操作:

i2c_del_driver(&eep_driver);

设备例子:实时时钟

让我们选取通过I2C总线和嵌入式控制器相连的RTC芯片的例子。连接框图见图8.3

8.3. 嵌入式系统上的I2C RTC

设定RTCI2C从地址为0x60,其寄存器空间组织如表8.2

8.2. I2C RTC的寄存器分布

寄存器名

描述

偏移

RTC_HOUR_REG

小时计数

0x0

RTC_MINUTE_REG

分钟计数

0x1

RTC_SECOND_REG

秒钟计数

0x2

RTC_STATUS_REG

标志记中断状态

0x3

RTC_CONTROL_REG

使能/禁止RTC

0x4

让我们的驱动基于前面讨论过的EEPROM驱动。我们将假设I2C客户驱动结构、从设备注册和II2C核心函数已经完成,仅实现和RTC通信的代码。

I2C核心检测到从地址为0x60的设备在I2C总线上时,将调用myrtc_attach()。其调用序列类似于清单8.5中的eep_attach()。假定在myrtc_attach()中你必须完成如下的芯片初始化:

1.     RTC状态寄存器(RTC_STATUS_REG)清零。

2.     通过设置RTC控制寄存器(RTC_CONTROL_REG)中的相应位,启动RTC(如果它还未开始运行)。

为了完成以上功能,让我们构建一个i2c_msg结构体,使用i2c_transfer()在总线上产生I2C事务。此传输机制为I2C所独有,和SMBus并不兼容。为了向前面讨论过的两个RTC寄存器写入数据,你必须构建两个i2c_msg消息。第一个消息设置寄存器偏移。在我们的例子中,RTC_STATUS_REG的值为3。第二个消息携带希望写入指定偏移的字节数。在此例中,共两个字节,一个字节写入RTC_STATUS_REG,另一个写入RTC_CONTROL_REG

Code View:

#include <linux/i2c.h> /* For struct i2c_msg */

int

myrtc_attach(struct i2c_adapter *adapter, int addr, int kind)

{

  u8 buf[2];

  int offset = RTC_STATUS_REG;  /* Status register lives here */

  struct i2c_msg rtc_msg[2];

 

  /* Write 1 byte of offset information to the RTC */

  rtc_msg[0].addr = addr;       /* Slave address. In our case,

                                   this is 0x60 */

  rtc_msg[0].flags = I2C_M_WR;  /* Write Command */

  rtc_msg[0].buf = &offset;     /* Register offset for

                                   the next transaction */

  rtc_msg[0].len = 1;           /* Offset is 1 byte long */

 

  /* Write 2 bytes of data (the contents of the status and

     control registers) at the offset programmed by the previous

     i2c_msg */

  rtc_msg[1].addr = addr;       /* Slave address */

  rtc_msg[1].flags = I2C_M_WR;  /* Write command */

  rtc_msg[1].buf = &buf[0];     /* Data to be written to control

                                   and status registers */

  rtc_msg[1].len = 2;           /* Two register values */

  buf[0] = 0;                   /* Zero out the status register */

  buf[1] |= ENABLE_RTC;         /* Turn on control register bits

                                   that start the RTC */

 

  /* Generate bus transactions corresponding to the two messages */

  i2c_transfer(adapter, rtc_msg, 2);

 

  /* ... */

  printk("My RTC Initialized/n");

}

 

                                    

因为RTC已经被初始化,并开始计时了,你可以通过读取RTC_HOUR_REGRTC_MINUTE_REGRTC_SECOND_REG来获取当前的时间。其操作如下:

Code View:

#include <linux/rtc.h> /* For struct rtc_time */

int

myrtc_gettime(struct i2c_client *client, struct rtc_time *r_t)

{

  u8 buf[3];      /* Space to carry hour/minute/second */

  int offset = 0; /* Time-keeping registers start at offset 0 */

  struct i2c_msg rtc_msg[2];

 

  /* Write 1 byte of offset information to the RTC */

  rtc_msg[0].addr = addr;       /* Slave address */

  rtc_msg[0].flags = 0;         /* Write Command */

  rtc_msg[0].buf = &offset;     /* Register offset for

                                   the next transaction */

  rtc_msg[0].len = 1;           /* Offset is 1 byte long */

 

  /* Read current time by getting 3 bytes of data from offset 0

     (i.e., from RTC_HOUR_REG, RTC_MINUTE_REG, and RTC_SECOND_REG) */

  rtc_msg[1].addr = addr;       /* Slave address */

  rtc_msg[1].flags = I2C_M_RD;  /* Read command */

  rtc_msg[1].buf = &buf[0];     /* Data to be read from hour, minute

                                   and second registers */

  rtc_msg[1].len = 3;           /* Three registers to read */

 

  /* Generate bus transactions corresponding to the above

     two messages */

  i2c_transfer(adapter, rtc_msg, 2);

  /* Read the time */

  r_t->tm_hour = BCD2BIN(buf[0]); /* Hour */

  r_t->tm_min  = BCD2BIN(buf[1]); /* Minute */

  r_t->tm_sec  = BCD2BIN(buf[2]); /* Second */

  return(0);

}

 

                                    

 

myrtc_gettime()实现了总线相关的RTC驱动的底层部分。RTC驱动的顶层部分应该和内核的RTC API保持一致,如第5章的“RTC子系统一节中所讨论的。此机制的好处是不管你的RTC是位于PC的南桥内部,还是如本例一样,位于嵌入式控制器的外部,应用程序可以不加改变而运行。

RTC通常用BCDBinary Coded Decimal)格式存储时间,每组位元(4位)表示09之间的数,而不是015。内核提供了宏BCD2BIN()用于将BCD码变换成十进制数,以及宏BIN2BCD()用于相反的操作。当从RTC寄存器读取数据时,myrtc_gettime()利用了宏BCD2BIN()用于转换。

drivers/rtc/rtc-ds1307.c提供了RTC驱动的实例,用于处理Dallas/Maxim DS13XX系列I2C芯片。

作为2线总线,I2C总线没有从设备用于中断请求的信号线,但一些I2C主机适配器可以中断CPU,触发数据传输请求。然而,此中断驱动操作对于I2C客户驱动是透明的,隐藏于I2C核心提供的服务例程里。假设图8.3I2C主机控制器是嵌入式SoC的一部分,并有中断CPU的能力,myrtc_attach()里对i2c_transfer()的调用将完成如下操作:

·         构建对应于rtc_msg[0]的事务,并使用主机控制器驱动提供的服务例程写入总线。

·         等待直到主机控制器触发发送结束中断,表明rtc_msg[0]已经在信号线上。

·         在中断处理例程里,查看I2C主机控制器状态寄存器,判断是否从RTC从设备里接收到确认信号。

·         如果主机控制器的状态和控制寄存器并非全部正确,则返回错误。

·         对于rtc_msg[1]重复同样过程。

 

I2C-dev

有时,当你需要支持大量慢速的I2C设备时,从用户空间对所有这些设备进行驱动就很有必要了。I2C层支持i2c-dev驱动以达到此目的。第19章的用户模式I2C中,有使用i2c-dev实现用户模式I2C驱动的例子。

使用LM-Sensors监控硬件

LM-Sensors项目,主页为www.lm-sensors.org,使Linux具有硬件监控能力。很多计算机系统使用传感器芯片来监控诸如温度、供电电压以及风扇转速等参数。周期性的检查这些参数是非常重要的。损坏了的CPU风扇可能会导致随机、异常的软件问题。如果是医疗设备系统出现故障,其后果将难以想象!

LM-Sensors利用传感器芯片的设备驱动来排除故障。它利用sensors程序产生状态报告,sensors-detect脚本检查你的系统,并帮助你产生相应的配置文件。

大多数芯片利用I2C/SMBus总线方式向CPU提供硬件监控接口。这些设备驱动是I2C客户驱动,但位于drivers/hwmon/目录,而不是drivers/i2c/chips/。具体例子可见National Semiconductor公司的LM87芯片,它能监控电压、温度和风扇。drivers/hwmon/lm87.c为其驱动的具体实现。I2C驱动ID号从10001999 都保留给了传感器芯片(参见include/linux/i2c-id.h)。

也有几个传感器芯片和CPU之间的接口采用ISA/LPC总线,而不是I2C/SMBus。其它输出模拟量的通过模数转换器(ADC)传送给CPU。这些芯片的驱动和I2C总线传感器驱动一起都位于drivers/hwmon/目录。非I2C总线传感器驱动的例子是drivers/hwmon/hdaps.c,它是加速度传感器驱动,出现在某些IBM/联想的笔记本电脑里,我们在第7输入驱动中讨论过。另一个非I2C总线的传感器的例子是Winbond 83627HF 超级 I/O芯片,由drivers/hwmon/w83627hf.c驱动。

串行外设接口总线(SPI

串行外设接口(Serial Peripheral Interface SPI)总线和I2C类似,也是串行的主-从接口,集成于很多微控制器内部。和I2C使用2线相比,它使用4线:串行时钟(Serial CLocK SCLK),片选(Chip Select CS, 主设备输出从设备输入(Master Out Slave In MOSI),主设备输入从设备输出(Master In Slave OutMISO)。MOSI用于传送数据至从设备,MISO用于从从设备读出数据。和I2C不同,由于SPI总线有专用的数据线用于数据的发送和接收,因此可以工作于全双工。SPI的典型速度为几MHz ,不像I2C为几十~几百KHz  ,因此SPI吞吐量大得多。

当前市面上可找到的SPI外设包括RF芯片、智能卡接口、EEPROMRTC、触摸传感器、以及ACD

内核提供了一个核心API用于通过SPI总线交换信息。典型的SPI客户驱动如下:

  1. SPI核心注册probe()remove()方法。suspend()resume()方法可选。

  #include <linux/spi/spi.h>

 

  static struct spi_driver myspi_driver = {

    .driver  = {

      .name  = "myspi",

      .bus   = &spi_bus_type,

      .owner = THIS_MODULE,

    },

    .probe   = myspidevice_probe,

    .remove  = __devexit_p(myspidevice_remove),

  }

 

  spi_register_driver(&myspi_driver);

SPI核心创建对应于此设备的spi_device结构体,当调用注册的驱动方法时,用作调用参数。

  1. 使用函数如spi_sync()spi_async()SPI设备交换数据。前者等待操作完成,后者当数据传输完成时,异步触发对注册的回调程序的调用。这些数据访问例程被从适当的地方调用,如SPI中断处理程序,sysfs方法,或者定时器处理程序。下面的代码片断演示了SPI数据的传输:

#include <linux/spi/spi.h>

 

struct spi_device *spi;  /* Representation of a

                            SPI device */

struct spi_transfer xfer;         /* Contains transfer buffer

                                     details */

struct spi_message sm;            /* Sequence of spi_transfer

                                     segments */

u8 *command_buffer;               /* Data to be transferred */

int len;                          /* Length of data to be

                                     transferred */

 

spi_message_init(&sm);            /* Initialize spi_message */

xfer.tx_buf = command_buffer;     /* Device-specific data */

xfer.len = len;                   /* Data length */

spi_message_add_tail(&xfer, &sm); /* Add the message */

spi_sync(spi, &sm);               /* Blocking transfer request */

作为SPI设备的例子,我们可参考第7章简单讨论过的触摸屏控制器ADS7846。其驱动完成如下操作:

  1. 使用spi_register_driver()SPI核心注册probe()remove()suspend()resume()方法。
  2. probe()方法使用input_register_device()向输入子系统注册驱动,并使用request_irq()请求中断
  3. 驱动从其中断服务程序中使用spi_async()收集触摸坐标。此函数当数据传输完成时,触发对注册的回调程序的调用。
  4. 如第7章所讨论的,回调函数通过输入事件接口/dev/input/eventX,使用input_report_abs()input_report_key(),依次报告触摸坐标和点击。诸如X Windowsgpm这些程序和事件接口紧密合作,响应触摸输入。

通过软件的方式控制I/O引脚,使其符合某种协议进行交互的驱动称为bit-banging驱动。SPI bit-banging 驱动的例子,可参考drivers/spi/spi_butterfly.c,它是用于和Atmel公司AVR处理器系列Butterfly板上的DataFlash芯片交互的驱动。将你的主机的并口和AVR Butterfly连接在一起,使用专用的dongle spi_butterfly可以进行bit-banging 操作。Documentation/spi/butterfly提供了关于此驱动更详细的描述。

当前没有类似于i2c-dev的、针对用户空间的SPI驱动。你只能编写内核驱动和SPI设备交互。

在嵌入式系统中,你可能会碰到处理器和集成各种功能的协处理器一起工作的解决方案。譬如,飞思卡尔的电源管理和音频组件(Power Management and Audio Component PMAC)芯片MC13783和基于ARM9i.MX27控制器协同工作就是这样的一个例子。PMAC集成了RTC,电池充电器,触摸屏接口,ADC模块和音频编码。处理器和PMAC之间通过SPI通信。SPI总线不含中断线,通过配置GPIO管脚,PMAC可以从外部中断处理器。

1-Wire总线

Dallas/Maxim开发的1-wire 协议使用1-wire (或w1)总线传送电源和信号。地回路通过其它途径解决。它提供了和慢速设备之间接口的简单途径,减少了空间、费用以及复杂性。使用此协议的设备实例是ibuttonwww.ibutton.com),用于感知温度,传送数据,或保存独特的ID号。

另一通过单一的引脚提供接口的w1芯片是Dallas/MaximDS2433,它是容量为4kb1-wire EEPROM。此芯片的驱动位于drivers/w1/slaves/w1_ds2433.c,通过sysfs节点提供对EEPROM的访问。

w1设备驱动相关的主要数据结构是w1_familyw1_family_ops,都定义于w1_family.h中。

调试

为了收集I2C的调试信息,在内核的配置菜单的Device Drivers-> I2C Support下,选中I2C Core debugging messagesI2C Algorithm debugging messagesI2C Bus debugging messagesI2C Chip debugging messages。类似的,为了调试SPI,需要在Device Drivers->SPI Support下选中Debug Support for SPI drivers

为了理解总线上I2C包的数据流,可在运行清单8.1时,I2C总线分析仪和你的电路板板连接在一起。lm-snesor包包括i2cdump工具,用于输出I2C总线上设备的寄存器中的内容。

Linux I2C的邮件列表位于:http://lists.lm-sensors.org/mailman/listinfo/i2c.

查看源代码

2.4版本的源码树中,所有I2C/SMBus相关的源码包含在一个单独的目录drivers/i2c/中,2.6版本内核中,I2C代码被分层次的组织:drivers/i2c/busses/目录包括适配器驱动,drivers/i2c/algos/目录包含algorithm驱动,drivers/i2c/chips/目录包含客户驱动。你也可以在源码树中其它的地方发现客户驱动。例如,drivers/sound/目录包含使用I2C接口的音频芯片组的驱动。在Documentation/i2c/目录下可找到提示以及更多的例子。

内核的SPI服务函数位于drivers/spi/spi.cADS7846触摸控制器的SPI驱动由drivers/input/touchscreen/ads7846.c实现。第17内存技术设备讨论的MTD子系统实现了SPI flash芯片驱动。其例子为drivers/mtd/devices/mtd_dataflash.c,它实现了访问AtmelDataFlash SPI芯片的驱动。

drivers/w1/目录包含了内核对w1协议的支持。w1接口主机控制器的驱动位于drivers/w1/masters/w1从设备驱动在drivers/w1/slaves/

8.3概括了本章使用的主要数据结构,及其在内核源码树中的位置。表8.4列出了本章所用到的主要内核编程接口,以及它定义的位置。

8.3. 数据机构概述

数据结构

位置

描述

i2c_driver

include/linux/i2c.h

代表一个I2C驱动

i2c_client_address_data

include/linux/i2c.h

I2C客户驱动所负责的从地址

i2c_client

include/linux/i2c.h

用于标识一个连接到I2C总线上的芯片

i2c_msg

include/linux/i2c.h

 

描述在I2C总线上欲产生的一次传输事务

spi_driver

include/linux/spi/spi.h

代表一个SPI驱动

spi_device

include/linux/spi/spi.h

代表一个SPI设备

spi_transfer

include/linux/spi/spi.h

 

SPI 传输缓冲区的细节

spi_message

include/linux/spi/spi.h

 

spi_transfer 分段序列

w1_family

drivers/w1/w1_family.h

代表w1从驱动

w1_family_ops

drivers/w1/w1_family.h

w1从驱动入口点

 

8.4. 内核编程接口概述

内核接口

位置

描述

i2c_add_driver()

include/linux/i2c.h drivers/i2c/i2c-core.c

I2C核心注册驱动入口点

i2c_del_driver()

drivers/i2c/i2c-core.c

I2C核心移除驱动

i2c_probe()

drivers/i2c/i2c-core.c

 

定义驱动所负责的从设备地址,如果i2c核心探测到某一地址,对应的attach() 函数将调用

i2c_attach_client()

drivers/i2c/i2c-core.c

 

向相应主机适配器所服务的客户列表增加一个客户

i2c_detach_client()

drivers/i2c/i2c-core.c

Detach一个活动的客户 .通常在客户驱动或关联的主机适配器注销时进行

i2c_check_functionality()

include/linux/i2c.h

 

验证主机适配器是否支持某功能

i2c_get_functionality()

include/linux/i2c.h

 

获得主机适配器所支持的所有功能的掩码

i2c_add_adapter()

drivers/i2c/i2c-core.c

注册主机适配器。

i2c_del_adapter()

drivers/i2c/i2c-core.c

注销主机适配器。

SMBus-compatible I2C data access routines

drivers/i2c/i2c-core.c

见表8.1

i2c_transfer()

drivers/i2c/i2c-core.c

通过I2C总线发送i2c_msg。此函数和SMBus不兼容。

spi_register_driver()

drivers/spi/spi.c

SPI核心注册驱动入口点。

spi_unregister_driver()

include/linux/spi/spi.h

注销 SPI驱动。

spi_message_init()

include/linux/spi/spi.h

初始化SPI message.

spi_message_add_tail()

include/linux/spi/spi.h

添加一条SPI消息到传输列表。

spi_sync()

drivers/spi/spi.c

通过SPI总线同步传输数据。此函数阻塞直至完成。

spi_async()

include/linux/spi/spi.h

使用完成回调机制,通过SPI总线异步传输数据。

原创粉丝点击