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.