Driver design for I2C bus devices in embedded Linux systems
2026-04-06 05:46:56··#1
Abstract: This paper analyzes the structure of the I2C driver in the Linux system and, taking the AT91RM9200 and X1227 as examples, introduces how to implement the I2C bus adapter and I2C device driver in an embedded Linux system. Keywords: Linux, I2C bus, I2C device driver Introduction The I2C bus is a two-wire serial bus introduced by PHILIPS, used to connect microcontrollers and their peripheral devices. It features simplicity and efficiency. Because its interface is directly on the component, the I2C bus occupies very little space, reducing the space on the circuit board and the number of chip pins, lowering interconnection costs, and is particularly suitable for embedded products. The Linux system has the advantages of being open source, free, and having abundant online resources, and has now become the mainstream choice for embedded systems. Therefore, how to implement I2C functionality in an embedded Linux system has become a problem in practical development. 1. I2C Bus The I2C bus transmits information between devices connected to the bus through the serial data SDA and serial clock SCL lines. Each device has a unique address for identification. According to the different functions during data transmission, devices are divided into master and slave. The master is the device that initializes the data transmission of the bus and generates the clock signal that allows transmission, usually a microcontroller. At this point, any addressed device is considered a slave, such as an LCD driver or E2PROM. The I2C bus protocol specifies that each master must have start, stop, transmit data, and acknowledge signals when communicating. These signals are the basic units in the communication process. The start signal is when the SDA line changes from high to low while the SCL line is high; the stop signal is when the SDA line changes from low to high while the SCL line is high; the acknowledge signal is when the SDA line is low while the SCL line is high; the non-acknowledgment signal is the opposite, when the SDA line is high while the SCL line is high. Each frame of data transmitted on the bus is 1 byte. The protocol specifies that the high 7 bits of the first byte after starting the bus are the address of the slave device, the 8th bit is the direction bit ("0" indicates a write operation from the master to the slave device; "1" indicates a read operation from the master to the slave device), and the remaining bytes are the operation data. The data transmission process is as follows: After sending a start signal on the I2C bus, the slave sends its 7-bit address and 1-bit read/write bit indicating the nature of this operation. Data transmission begins after receiving an acknowledgment signal and continues until a stop signal is sent. The master checks for an acknowledgment signal on the SDA line after sending each byte; if an acknowledgment signal is received, transmission continues; otherwise, data transmission stops. 2. I2C Bus Driver Structure in Linux The Linux system provides excellent support for the I2C bus. The relationship between the various parts of the Linux I2C framework, corresponding to the physical hardware connection, is shown in Figure 1. Figure 1: Linux Kernel I2C Bus Driver Architecture The I2C-related code in the kernel can be divided into three layers: 1. I2C core framework: Provides the definition of core data structures and related interface functions to implement the registration and deregistration management of I2C adapter drivers and device drivers, as well as upper-level, adapter-independent code for I2C communication methods, adding corresponding read/write methods to each I2C bus in the system. 2. I2C Bus Adapter Driver: Defines the `i2c_adapter` data structure describing the specific I2C bus adapter, implements the I2C bus communication methods on the specific I2C adapter, and describes them using the `i2c_algorithm` data structure. 3. I2C Device Driver: Defines the `i2c_client` data structure describing the specific device and possible private data structures, completes the device registration in the kernel using the function interfaces provided by the I2C core, and implements specific functions, including read, write, and ioctl interfaces for user-level operations. In general, the I2C bus driver in Linux is divided into two parts: the bus driver (BUS) and the device driver (DEVICE). The I2C core and I2C bus adapter driver complete the hardware host bus driver (BUS), while the I2C driver implements the slave device driver. In the design, the interface provided by the I2C core is unified and does not need to be modified. We only need to implement the specific I2C bus adapter driver and I2C device driver, which greatly improves the system's portability. In a previous product, I used the AT91RM9200 and X1227 to construct the clock module of an embedded system. In this design, the AT91RM9200 served as the I2C master, and the X1227 as the slave. The following section uses this as an example to detail the implementation of the drivers for these two parts. 3. AT91RM9200 I2C Bus Driver Implementation The AT91RM9200 is an ARM920T processor that provides a standard two-wire interface, TWI, i.e., the I2C interface, in master mode. The I2C operating mode and status are set through the TWI control register TWI_CR. The clock is generated by the programmed value in the TWI_CWGR register. This register defines the TWCK signal, enabling the interface to adapt to a wide range of clocks. Specifically, the implementation of the AT91RM9200 I2C bus adapter driver in Linux first initializes the AT91RM9200 I2C operating mode, and then loads the I2C bus driver. This requires two structure modules: struct i2c_adapter and struct i2c_algorithm. The i2c_adapter structure members are initialized as follows: `static struct i2c_adapter at91rm9200_adapter = { name: "AT91RM9200", id: I2C_ALGO_SMBUS, algo: &at91_algorithm, algo_data: NULL, inc_use:at91_inc, dec_use:at91_dec, ... ... };` This module does not provide read/write functions; the specific read/write methods are provided by the second module, `struct i2c_algorithm`. The `static struct i2c_algorithm at91_algorithm = { name: "at91 i2c", id: I2C_ALGO_SMBUS, smbus_xfer: at91_smbus_xfer, master_xfer: at91_xfer, functionality: at91_func, };` defines two modules that are registered with the operating system by calling the `i2c_add_adapter` interface function in the I2C core, thus installing the bus driver. Therefore, `i2c_algorithm` implements the specific methods of I2C communication. For the at91rm9200 I2C adapter in this paper, `at91_xfer` is the most crucial. Kernel analysis reveals that the data transfer interfaces provided to the host in the I2C core framework—`i2c_master_send`, `i2c_master_recv`, and `i2c_transfer`—are ultimately implemented by calling `at91_xfer`. The data transmission process is as follows: After the data sending master initializes to the Start state, it sends a 7-bit slave address to the DADR in the master mode register TWI_MMR to notify the slave device. The bits after the slave address indicate the transmission direction (write or read). A bit of 0 indicates a write operation (transmit operation); a bit of 1 indicates a data read request (receive operation). TWI transmission requires the slave to acknowledge each byte received. In the acknowledge clock pulse, the master releases the data line (HIGH) and pulls the slave low to generate an acknowledge. The master polls the data line in this clock pulse, using either polling or interrupt mode to check the status bit. If the slave does not acknowledge the byte, it sets the NAK bit in the status register TWI_SR. For a write operation, data is sent to the holding register TWI_THR, and the START bit in TWI_CR is set to start the transmission. Data is shifted in the internal shift register. When an acknowledge is detected, the TXRDY bit is set and remains there until new data is written to TWI_THR. The master generates a STOP state to terminate the transmission. After setting START, the sequence reading begins. When the RXRDY bit is set in the status register, the receive holding register (TWI_RHR) is used to receive a character. The RXRDY bit is reset when reading TWI_RHR. The TWI interface can execute multiple transmission formats: 7-bit slave address and 10-bit slave address. Three internal address bytes are configured via the master mode register TWI_MMR. If the slave only supports 7-bit addresses, IADRSZ must be set to 0. If the slave address is greater than 7 bits, the user must configure the address size IADRSZ and set other slave address bits in the internal address register TWI_IADR. 4. X1227 Device Driver Implementation The X1227 is a real-time clock with clock, calendar, CPU monitoring circuitry, and two polling alarms. The clock uses a low-cost 32.768kHz crystal as input and can accurately display the time in seconds, minutes, hours, date, day of the week, month, and year. It can automatically adjust for leap years up to 2096. The X1227 also has a watchdog timer with three selectable timeout periods. Additionally, the X1227 has a 4K-bit EEPROM array, which can be used as a memory for some user configuration data. The following uses the X1227 as an example to illustrate the design considerations of a specific I2C device driver. As mentioned earlier, the I2C bus driver only provides a read/write mechanism for a single bus and does not perform communication itself. Communication is handled by the I2C device driver, which communicates with the specific device through the I2C bus. A device driver is described by two modules: `struct i2c_client` and `struct i2c_driver`. `i2c_client` describes a specific I2C device, and the `i2c_driver` structure provides the communication method between the `i2c_adapter` and the `i2c_client`. The `struct i2c_driver x1227_driver = { name: x1227? id: I2C_DRIVERID_X1227, flags: I2C_DF_NOTIFY, attach_adapter: x1227_probe, detach_client: x1227_detach, command: x1227_command };` defines an I2C device driver that uses the I2C bus access methods provided by the adapter driver and the address clues provided in the device driver module to detect possible devices and their addresses. If a device is successfully discovered, a `struct i2c_client` is created to identify the device and registered with the adapter's data structure. `detach_client` is used to unregister the device from the bus and release the `i2c_client` and its corresponding private data structure. `command` is the underlying implementation of the ioctl functionality in the user interface. I2C device drivers need to implement two interfaces: one is the interface to the I2C core framework, which is registered during device initialization via the `i2c_add_driver` function. Once the i2c_driver is loaded, its attach_adapter function will be called. Another interface is to the user application layer, providing user programs with access to I2C devices. This includes interface functions for standard file operations such as open, release, read, write, and most importantly, ioctl. Each device driver has a data structure called file_operations to implement these interface functions. `static struct file_operations rtc_fops = { owner: THIS_MODULE, ioctl: x1227_rtc_ioctl, open: x1227_rtc_open, release: x1227_rtc_release, };` Here, `open` and `release` are used to open and close the X1227, while `x1227_rtc_ioctl` provides the user with a series of specific commands to control the clock chip: `RTC_GET_TIME` (reads the real-time clock time in a fixed data format), `RTC_SET_TIME` (sets the real-time clock time in a fixed data format), and E2PROM read/write operations, etc. For the X1227, it is generally registered as a single miscdevice device (all miscdevice devices share a common major device number, but have different minor device numbers). `static struct miscdevice x1227_rtc_miscdev = { RTC_MINOR, tc? &rtc_fops };` During initialization, the X1227 is registered via `misc_register(&x1227_rtc_miscdev)`, allowing user programs to access the X1227 through the device node `/dev/rtc` with major device number 10 and minor device number 135. To test the clock functionality of the X1227, the AT91RM9200 I2C bus driver module and the X1227 module are loaded sequentially during system startup. It should be noted that Linux distinguishes between a system clock and a hardware clock. The system clock refers to the clock in the current Linux kernel, while the hardware clock is the battery-powered motherboard clock, which is the X1227 discussed in this article. In Linux, the main commands for viewing and setting the clock are `date` and `hwclock`. First, set the system clock, for example, to 12:30 PM on August 17, 2006: `date 081712302006`. Then, set the hardware clock to the current system clock time using the command `/sbin/hwclock hwclock`. This will set the time in X1227 to the current system time. Then, typically, a startup script `/sbin/hwclock hwclock` is set at operating system startup to update the system clock using the time in X1227. The system clock then records the time until the system is restarted or shut down. 5. Conclusion This article introduces the implementation of an I2C bus adapter and I2C device driver. This design was successfully used on the main control module of a network testing device, implementing the device's real-time clock function, facilitating the monitoring of the entire system. The I2C bus is widely used in the current embedded field, such as audio/video control and storage device communication, and Linux has become the mainstream of embedded systems. From the perspective of the Linux kernel, I2C drivers have a clear hierarchical structure, providing a standardized framework for programmers to develop I2C-related drivers.