Saturday, August 23, 2025

How to Design a General I2C Detection Tool in QNX

When developing embedded systems on QNX 6.6, I2C communication is often a critical component for interfacing with sensors, GPIO expanders, and other peripheral devices. This article documents the design and implementation of a general-purpose I2C detection tool based on real-world experience troubleshooting I2C communication failures in a railway communication system.

However, standard Linux I2C tools (`i2c-detect`, `i2c-tools`) were not available on QNX system, necessitating a custom solution.

The source code can be found at https://github.com/happytong/i2cdetect_qnx

 

I2C Address Range Considerations

7-bit Addressing (Standard)

·      Total Range: 0x00 to 0x7F (0 to 127 decimal)

·      Usable Range: 0x08 to 0x77 (8 to 119 decimal)

·      Reserved Addresses:

-      0x00: General Call (broadcast)

-      0x01: CBUS Address

-      0x02-0x03: Reserved for different bus formats

-      0x04-0x07: High-Speed Master mode

-      0x78-0x7B: 10-bit addressing prefix

-      0x7C-0x7F: Reserved for future use

 

Common Device Address Ranges

| Device Type         | Address Range | Examples       |

|---------------------|---------------|----------------|

| EEPROMs             | 0x50-0x57     | 24C32, 24C64   |

| RTCs                | 0x68, 0x6F    | DS1307, DS3231 |

| GPIO Expanders      | 0x20-0x27     | MCP23017       |

| Digital Pots        | 0x28-0x2F     | DS1808         |

| Temperature Sensors | 0x48-0x4F     | LM75, TMP102   |

 

QNX I2C Architecture Overview

 

QNX I2C Driver Stack

 

Application Layer

    ↓

QNX I2C Driver API (devctl)

    ↓

QNX I2C Resource Manager

    ↓

Hardware I2C Controller

 

Key QNX I2C Concepts

 

·        Bus-Level Operations: QNX I2C driver operates at the bus level (`/dev/i2c1`, `/dev/i2c3`)

·        Device Control Interface: Uses `devctl()` with I2C-specific commands

·        Transaction-Based: Each I2C operation is atomic and stateless

·        Slave Address Per Transaction: Address specified in each send/receive operation

 

QNX I2C API Commands

 

DCMD_I2C_SEND            // Send data to I2C device

DCMD_I2C_RECV            // Receive data from I2C device

DCMD_I2C_SET_BUS_SPEED   // Configure bus speed

DCMD_I2C_DRIVER_INFO     // Get driver information

 

Design Requirements

 

Functional Requirements

·        Device Scanning: Detect all I2C devices on specified bus

·        Register Reading: Read from specific device registers

·        Register Writing: Write to specific device registers

·        Bus Validation: Verify I2C bus accessibility

·        Error Reporting: Comprehensive error analysis

·        Multi-Bus Support: Handle multiple I2C buses

 

Non-Functional Requirements

·        Portability: Work across different QNX I2C controllers

·        Reliability: Robust error handling and recovery

·        Performance: Efficient scanning algorithms

·        Usability: Clear command-line interface

·        Maintainability: Modular, well-documented code

 

Hardware Considerations

·        ARM/i.MX6D platform with multiple I2C buses (where this tool was tested)

·        Mixed device types (GPIO expanders, sensors, etc.)

·        Address range conflicts and hardware variations

·        Bus speed compatibility (100kHz standard)

 

Tool Architecture

 

Component Design

 

┌────────────────────────────────────────┐

│              i2cdetect Tool            │

├────────────────────────────────────────┤

│  Command Line Interface                │

│  ├── scan_bus()                        │

│  ├── read_register()                   │

│  ├── write_register()                  │

│  └── debug_device()                    │

├────────────────────────────────────────┤

│  QNX I2C Driver Interface              │

│  ├── devctl(DCMD_I2C_SEND)             

│  ├── devctl(DCMD_I2C_RECV)             

│  └── devctl(DCMD_I2C_DRIVER_INFO)      │

└────────────────────────────────────────┘

 

 

Implementation Details

 

Core Data Structures

 

    struct {

        i2c_send_t header;

        unsigned char data[2];  // [register_address, value]

} send_packet;

 

    struct {

        i2c_recv_t header;

        unsigned char data;

    } recv_packet;

 

Device Scanning Algorithm

 

int scan_i2c_bus(const char* device) {

    int fd = open(device, O_RDWR);

    int row, col, addr;

    if (fd < 0) {

        printf("Failed to open %s: %s\n", device, strerror(errno));

        return -1;

    }

 

    printf("Scanning I2C bus %s:\n", device);

    printf("     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f\n");

   

    for (row = 0; row < 8; row++) {

        printf("%02x: ", row * 16);

        for (col = 0; col < 16; col++) {

            addr = row * 16 + col;

            if (addr < 0x08 || addr > 0x77) {

                printf("   ");

                continue;

            }

           

            // Try to read from device - use 1-byte read for better detection

            struct {

                i2c_recv_t header;

                unsigned char data;

            } recv_packet;

           

            recv_packet.header.slave.addr = addr;

            recv_packet.header.slave.fmt = I2C_ADDRFMT_7BIT;

            recv_packet.header.len = 1;  // Try to read 1 byte

            recv_packet.header.stop = 1;

           

            int status = devctl(fd, DCMD_I2C_RECV, &recv_packet, sizeof(recv_packet), NULL);

            if (status == 0) {

                printf("%02x ", addr);

            } else {

                printf("-- ");

            }

        }

        printf("\n");

    }

   

    close(fd);

    return 0;

}

 

 

Register Read/Write Functions

 

int read_i2c_register(const char* device, int slave_addr, int reg_addr) {

    int fd = open(device, O_RDWR);

    if (fd < 0) {

        printf("Failed to open %s: %s\n", device, strerror(errno));

        return -1;

    }

 

    // First send register address

    struct {

        i2c_send_t header;

        unsigned char reg;

    } send_packet;

   

    send_packet.header.slave.addr = slave_addr;

    send_packet.header.slave.fmt = I2C_ADDRFMT_7BIT;

    send_packet.header.len = 1;

    send_packet.header.stop = 0;  // No stop, we'll read next

    send_packet.reg = reg_addr;

   

    int status = devctl(fd, DCMD_I2C_SEND, &send_packet, sizeof(send_packet), NULL);

    if (status != 0) {

        printf("Failed to send register address 0x%02x to slave 0x%02x: status %d\n",

               reg_addr, slave_addr, status);

        close(fd);

        return -1;

    }

   

    // Now read the data

    struct {

        i2c_recv_t header;

        unsigned char data;

    } recv_packet;

   

    recv_packet.header.slave.addr = slave_addr;

    recv_packet.header.slave.fmt = I2C_ADDRFMT_7BIT;

    recv_packet.header.len = 1;

    recv_packet.header.stop = 1;

   

    status = devctl(fd, DCMD_I2C_RECV, &recv_packet, sizeof(recv_packet), NULL);

    if (status != 0) {

        printf("Failed to read from slave 0x%02x: status %d\n", slave_addr, status);

        close(fd);

        return -1;

    }

   

    printf("Read from slave 0x%02x register 0x%02x: 0x%02x\n",

           slave_addr, reg_addr, recv_packet.data);

   

    close(fd);

    return recv_packet.data;

}

 

int write_i2c_register(const char* device, int slave_addr, int reg_addr, int value) {

    int fd = open(device, O_RDWR);

    if (fd < 0) {

        printf("Failed to open %s: %s\n", device, strerror(errno));

        return -1;

    }

 

    // Send register address and data in one operation

    struct {

        i2c_send_t header;

        unsigned char data[2];  // [register_address, value]

    } send_packet;

 

    send_packet.header.slave.addr = slave_addr;

    send_packet.header.slave.fmt = I2C_ADDRFMT_7BIT;

    send_packet.header.len = 2;  // Register address + data

    send_packet.header.stop = 1; // Complete transaction

    send_packet.data[0] = reg_addr;

    send_packet.data[1] = value;

 

    int status = devctl(fd, DCMD_I2C_SEND, &send_packet, sizeof(send_packet), NULL);

    if (status != 0) {

        printf("Failed to write 0x%02x to register 0x%02x of slave 0x%02x: status %d\n",

               value, reg_addr, slave_addr, status);

        close(fd);

        return -1;

    }

 

    printf("Wrote 0x%02x to slave 0x%02x register 0x%02x\n",

           value, slave_addr, reg_addr);

 

    close(fd);

    return 0;

}

 

Register Access Data Flow

 

Read Operation (pull):

·        Master must first tell the device which register to read from

·        Master must then request the data from that register

·        Device needs time to fetch data from the specified register

·        Direction change: Master Slave (tell register), then Slave Master (send data)

 

Master: START + ADDR+W + REG + RESTART + ADDR+R + STOP

Slave:   ACK     ACK     ACK             DATA     ACK

 

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐

│   Send Reg      │───  Repeated Start │───   Read Data    

│   Address       │     │   Condition     │     │   From Device   │

└─────────────────┘     └─────────────────┘     └─────────────────┘

 

Some simple I2C devices don't use register addressing, the first step can be skipped.

 

Write Operation (push):

·        Master pushes both register address and data to the device

·        Device receives everything in one stream: REG_ADDR + DATA

·        Device automatically knows: "Write this data to this register"

·        Single direction: Master Slave

 

Master: START + ADDR+W + REG + DATA + STOP

Slave:   ACK     ACK     ACK   ACK  

┌─────────────────┐     ┌─────────────────┐

│  Send Reg Addr  │───   Send Data    

│  + Data Packet  │     │   with Stop     │

└─────────────────┘     └─────────────────┘

 

 

Error Handling and Debugging

 

Common QNX I2C Error Codes

 

| Error Code | Value | Meaning         | Debugging Action           |

|------------|-------|-----------------|----------------------------|

| EOK        | 0     | Success         | Operation completed        |

| ENODEV     | 19    | No such device  | Check slave address        |

| ETIMEDOUT  | 110   | Timeout         | Check bus speed, pullups   |

| EIO        | 5     | I/O error       | Check hardware connections |

| EBUSY      | 16    | Bus busy        | Retry operation            |

| EINVAL     | 22    | Invalid argument| Check packet structure     |

 

 

Real-World Application

 

Case Study: Button detection

 

Problem: I2C chip MCP23017 is used to detect button press, and the application expected MCP23017 devices at 0x24 and 0x26, but was unable to see the slave address.

 

# ./i2cdetect /dev/i2c3 scan

Scanning I2C bus /dev/i2c3:

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f

00:                         -- -- -- -- -- -- -- --

10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

70: 70 -- -- -- -- -- -- --

 

Solution: After studied the hardware design, there was another I2C chip TCA9546 working as a switch to enable the I2C bus for MCP23017. After send the command to TCA9546, the MCP23017 was detected by this tool.

 

# ./i2cdetect /dev/i2c3

Usage: ./i2cdetect <device> <command> [args...]

Commands:

  scan                       - Scan I2C bus for devices

  debug                      - Debug scan showing status codes

  read <addr> <reg>          - Read register from device

  write <addr> <reg> <value> - Write value to register

  tca-set <channel_mask>     - Set TCA9546 channels (0x70)

  tca-get                    - Get current TCA9546 channel status

  tca-scan                   - Scan all TCA9546 channels

Examples:

  ./i2cdetect /dev/i2c3 scan          # Scan bus where TCA9546 is located

  ./i2cdetect /dev/i2c3 read 0x24 0x13

  ./i2cdetect /dev/i2c3 write 0x24 0x00 0xFF

  ./i2cdetect /dev/i2c3 tca-set 0x01    # Enable channel 0 only

  ./i2cdetect /dev/i2c3 tca-set 0x05    # Enable channels 0 and 2

  ./i2cdetect /dev/i2c3 tca-set 0x00    # Disable all channels

  ./i2cdetect /dev/i2c3 tca-get         # Show current channel status

  ./i2cdetect /dev/i2c3 tca-scan        # Scan devices on all channels

# ./i2cdetect /dev/i2c3 tca-set 0x04

TCA9546 channels set to: 0x04 (CH2)

# ./i2cdetect /dev/i2c3 scan

Scanning I2C bus /dev/i2c3:

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f

00:                         -- -- -- -- -- -- -- --

10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

20: -- -- -- -- 24 -- 26 -- -- -- -- -- -- -- -- --

30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

70: 70 -- -- -- -- -- -- --

 

Best Practices

 

1. Resource Management

 

// Always close file descriptors

void cleanup_i2c_bus(i2c_bus_config_t* bus_config) {

    if (bus_config->fd >= 0) {

        close(bus_config->fd);

        bus_config->fd = -1;

    }

    bus_config->initialized = false;

}

 

 

2. Error Recovery

 

// Implement retry logic for transient errors

int i2c_reliable_read(i2c_bus_config_t* bus_config, uint8_t slave_addr,

                     uint8_t reg_addr, uint8_t* data, int len) {

    const int MAX_RETRIES = 3;

   

    for (int retry = 0; retry < MAX_RETRIES; retry++) {

        int status = i2c_read_register(bus_config, slave_addr, reg_addr, data, len);

       

        if (status == EOK) {

            return 0;  // Success

        }

       

        if (status == ENODEV) {

            return status;  // Don't retry for missing device

        }

       

        // Brief delay before retry

        usleep(1000);  // 1ms

    }

   

    return -1;  // All retries failed

}

 

 

3. Bus Speed Considerations

 

// Set appropriate bus speed for device compatibility

int i2c_set_bus_speed(i2c_bus_config_t* bus_config, uint32_t speed_hz) {

    const uint32_t valid_speeds[] = {100000, 400000, 1000000};  // Standard speeds

   

    // Validate speed

    bool valid = false;

    for (int i = 0; i < sizeof(valid_speeds)/sizeof(valid_speeds[0]); i++) {

        if (speed_hz == valid_speeds[i]) {

            valid = true;

            break;

        }

    }

   

    if (!valid) {

        printf("Warning: Non-standard I2C speed %d Hz\n", speed_hz);

    }

   

    return devctl(bus_config->fd, DCMD_I2C_SET_BUS_SPEED, &speed_hz, sizeof(speed_hz), NULL);

}

 

 

4. Device Identification

 

// Attempt to identify common I2C devices

void i2c_identify_device(i2c_bus_config_t* bus_config, uint8_t addr, char* device_type) {

    uint8_t reg_data[4];

   

    // Try common identification registers

    if (i2c_read_register(bus_config, addr, 0x00, reg_data, 2) == EOK) {

        // Check for MCP23017 pattern

        if ((addr & 0xF8) == 0x20) {  // MCP23017 base address

            strcpy(device_type, "MCP23017 GPIO");

            return;

        }

       

        // Check for DS1808 pattern

        if ((addr & 0xF8) == 0x28) {  // DS1808 base address

            strcpy(device_type, "DS1808 Digital Pot");

            return;

        }

    }

   

    strcpy(device_type, "Unknown");

}

 

 

Conclusion

 

The general I2C detection tool for QNX provides essential capabilities for embedded system development and debugging. Key takeaways include:

 

Technical Insights

·        QNX I2C Architecture: Understanding the bus-level operation and transaction-based model is crucial

·        Resource Efficiency: Use single file descriptor per I2C bus for optimal performance

·        Error Handling: Comprehensive error analysis improves reliability and debugging efficiency

·        Hardware Flexibility: Configurable addressing supports varied hardware configurations

 

Development Benefits

·        Rapid Prototyping: Quick device discovery accelerates development cycles

·        Hardware Validation: Verify I2C connectivity before software integration

·        Production Debugging: Field troubleshooting for I2C communication issues

·        System Integration: Validate multi-device I2C systems

 

Future Enhancements

·        GUI Interface: Graphical tool for easier operation

·        Automated Testing: Scripted device validation sequences

·        Device Libraries: Expandable device identification database

·        Remote Operation: Network-based I2C debugging capabilities

 

This tool is handy in real-world QNX development, particularly in complex systems where hardware and software integration challenges are common. The modular design allows for easy adaptation to different QNX platforms and I2C hardware configurations.

The complete source code provides a solid foundation for QNX I2C development projects, demonstrating best practices for resource management, error handling, and system integration in embedded QNX environments.

How to Design a General I2C Detection Tool in QNX

When developing embedded systems on QNX 6.6, I2C communication is often a critical component for interfacing with sensors, GPIO expanders, a...