<< Back Home UK uk   Donate Donate

UART-to-I2C on Arduino

When developing smart DJI battery reader the UART-to-I2C (Arduino-based) bridge appeared. In general, there are similar devices in market. cp2112 is widely used, but it is not too simple, works via USB HID, costs $40 and can be delivered in 2-3 weeks. Also we have Coptonix #020101 with simple ASCII protocol. Its even possible to operate manually, but not too convenient because of no local echo. It has a bit ambiguous docs about reading reply from I2C slave, can't report bus errors and it is Slave. Costs about $25 and same 2-3 weeks for delivery.

Now we have firmware implementing I2C Master mode with following features

  • 3 protocols over USB UART (115200 8N1)
    • RAW (data length + address (1 or 2 bytes) and data itself, all in I2C bus format)
    • Coptonix #020101-like (Master mode), see below
    • manual: local echo, console commands, data in HEX, built-in bus scaner
  • 7/10 bit I2C address support
  • data length up to 255 bytes
  • python adapter class, implementing i2c_msg from smbus2.SMBus to simplify integration with I2C/smbus applications and/or porting from Raspbery to Linux/Windows
  • build-in logging feature in RAW mode, doesn't interfere with regular data flow

RAW mode (default) packet format

Write UART -> I2C, RD=0


 * 8bit data length (doesn't include address)

 *  7bit address: adr7+R/W
 *     xxxxxxx r/w 
 * 10bit address: 11110 adr2+R/W Ack adr8
 *     11110xx r/w xxxxxxxx

 * data bytes

Request Read I2C -> UART, RD=1


 * 8bit data length (doesn't include address), always 1 since data contains requested length

 *  7bit address: adr7+RD
 *     xxxxxxx r/w 
 * 10bit address: 11110 adr2+RD Ack adr8
 *     11110xx r/w xxxxxxxx

 * requested length (1 byte)

I2C Bus management

For management purposes we use request packets with invalid 0xff address with following command sequence

Mode - 0xff
0x01 0xff 0xff mode
  mode: 0 - RAW
        1 - Coptonix
        2 - Manual

Transaction - 0xfe
0x01 0xff 0xfe on/off
        0 - end transaction
        1 - begon transaction

Log level - 0xfd
0x01 0xff 0xfd level
        0 - disabled
        1 - enabled (2 and higher reserved)

Converter expects 1st byte with data length. Then it reads 1st address byte and 2nd one if necessary. Then all data bytes are read ans sent to specified I2C device. If data flow interrupts for more than CMD_TIMEOUT (1sec) controllers treats is as error and drops all previously received data.

It is possible to change converter mode manually with the following trick. In timeout condition input buffer is checked against several manual commands. If none is matched, all data is dropped.


UI2C Reply format

 * 8bit data length for packets of 1-0xef bytes
 *     or
 * 0xff length for packets of 0xf0-0xff bytes
 *     or
 * 0xff error code (see below)

 * data bytes (for READ requests only)

UI2C Erros

1:  data too long  (should not happen)
2:  NACK addr      (no such device)
3:  NACK data      (data transmission aborted)
4:  unknown
5:  timeout

I2C Bus addressed read

Master device often sends some command (or address) to Slave device and expect some reply. In order to keep bus acquired between WRITE and READ requests special sequence for begin/end transaction is used. By default READ and WRITE release bus imediately after completion

> 0x01 0xff 0xfe 0x01     begin transaction
< no reply

> 0x01 0x16 0x3f                        send 1 byte (0x3f) to device 0x0b (=0x16/2)
< 0x01                                  1 byte sent

> 0x01 0x17 0x20                        request read up to 0x20 bytes from device 0x0b 
                                                           (=0x17/2, lower bit means READ)
< 0x20 0xXX 0xXX .... 0xXX              0x20 bytes received + data itself

> 0x01 0xff 0xfe 0x00     end transaction
< no reply

Error reporting

> 0x01 0xff 0xfe 0x01     begin transaction
< no reply

> 0x02 0x16 0x22 0x3e                   send 2 byte (0x22 0x3e) to device 0x0b (=0x16/2)
< 0xff 0x03                             Error (0xff) NACK data (0x03) - device rejected request

> 0x01 0xff 0xfe 0x00     end transaction
< no reply

Command set: Manual, Coptonix

Manual mode differs only in extended comamnd set and local echo

requestactionCoptonix replyManual reply
a<CR>get remote I2C device addressaXX<CR>Dst Address: XXX<CR>
cXXX<CR>set remote I2C device addressc<CR>OK<CR>
s<CR>save remote I2C device address in EEPROM and make it defaults<CR>OK<CR>
wXXX...XXXX<CR>write date to remote devicew<CR>Status string<CR>
xXXX...XXXX<CR>write date to remote device and wait for reply x<CR>[iXXX...XXX]<CR>Formatted hex dump
Manual / Non-standard extension
xNN,XXX...XXXX<CR>write date to remote device and wait for reply, limit to NN bytes
Formatted hex dump
r<CR>switch to RAW modeUI2C vX.X RAW mode<CR>
m<CR>switch to Manual modeUI2C vX.X Manual mode<CR>
lN<CR>set log level to NLogging ON<CR>
or nothing
?<CR>scan I2C busformatted list of devices
v<CR>get FW revisionUI2C vX.X<CR>

Manual Example

version?                request version and wait ~1 sec
UI2C v1.0
mode=manual             switch to manual mode
UI2C v1.0 Manual mode
a                       get selected device
Dst Address: fe
w223e                   send bytes 0x22 0x3e
Status: 2 NACK addr
?                       scan bus
I2C device found at address 0x0B!
c0b                     select device 0x0b
w223e                   send bytes 0x22 0x3e
Status: 3 NACK data
x10,3f                  send byte 0x2f and wait for reply up to 0x10 bytes
Status: 0 OK                                                                    
58 0F 3F 3F 3F 3F 3F 3F
3F 3F 3F 3F 3F 3F 3F 3F

Coptonix Example

a                       get selected device
c0B                     select device 0x0b
w223E                   send bytes 0x22 3e
w3F                     send byte 0x3f and wait for reply

Python smbus2/i2c compatibility

Module ui2c is minimal replacement for smbus2 module and has same interface. Is makes easier adding compatiblity layer to existing applications. Also, for automated UI2C adapter detection there is method probe_ui2c_device(dev_name, speed=115200)

  • class i2c_msg - keep prepared read/write requests and related data buffers
    • read(address, length)
    • write(address, buf)
  • class UartI2C - handle I/O with I2C over UI2C
    • __init__(self, dev_name=None, speed=115200)
    • open(self, dev_name)
    • close(self)
    • i2c_rdwr(self, *i2c_msgs) - execute prepared requests inside single transaction
  • method probe_ui2c_device(dev_name, speed=115200)
  • property verbose - log level, 0-3
  • property ui2c_logging - enable UI2C internal logging, must be adjusted before open()/__init__()

Python Example

    # replaced imports
    #import smbus2
    #from smbus2 import i2c_msg

    import ui2c
    from ui2c import i2c_msg

    # check UI2C presence
    if(not ui2c.probe_ui2c_device(dev_name, speed):
        raise IOError("UI2C not detected")

    # open port
    bus = ui2c.UartI2C(dev_name, speed)

    # prepare write packet with command
    part_write = i2c_msg.write(dev_addr, [cmd.value])
    # prepare reply request (read) packet
    part_read =, 2 + (1 if bus.pec else 0))

    # write command and wait for reply in single transaction
    bus.i2c_rdwr(part_write, part_read)
    received_data = bytes(part_read)

    print("Received data:", received_data)

First, you open the UART-to-I2C adapter port using ui2c.UartI2C(dev_name, speed), where dev_name is the device name or port identifier, and speed is the baud rate.

Next, you define the I2C device address (dev_addr) and the command to be sent (cmd).

You then prepare a write packet with the command using i2c_msg.write(). The write packet contains the device address and the command byte.

Similarly, you prepare a read packet using The read packet specifies the device address and the number of bytes to be read.

To perform the I2C transaction, you call uart_i2c.i2c_rdwr(part_write, part_read), which sends the write packet first and then performs the read operation. The received data is stored in the part_read object.

Finally, you can retrieve the received data from part_read using bytes(part_read), and process or display it as needed.

Please make sure to replace dev_name with the actual device name or port identifier for your UART-to-I2C adapter, and adjust the dev_addr and cmd variables based on your specific device and command requirements.

Note that this example assumes you have the ui2c library installed.


i2c_msg Structure

Represents an I2C message for read or write operations.

struct i2c_msg {
    uint16_t addr;   /**< The address of the I2C device. */
    uint16_t flags;  /**< The flags for the I2C message. */
    uint16_t len;    /**< The length of the data buffer. */
    uint8_t* buf;    /**< The pointer to the data buffer. */


Create Read data request from an I2C device in the provided i2c_msg structure.

Returns: 1 on success, 0 otherwise.

int i2c_msg_read(struct i2c_msg* msg, int address, int length);


Create Write data request to an I2C device in the provided i2c_msg structure.

Returns: 1 on success, 0 otherwise.

int i2c_msg_write(struct i2c_msg* msg, int address, char* data, int length);


Frees the resources associated with an i2c_msg structure.

void i2c_msg_free(struct i2c_msg* msg);


Opens the serial port with UART-to-I2C adapter for communication.


  • dev_name - The name of the device (serial port) to open. Examples: "COM1" (Windows), "/dev/ttyUSB0" (Linux).
  • speed - The speed of the UART communication.
F_HANDLE ui2c_open(const char *dev_name, int speed);


Closes port with the UART-to-I2C adapter device.

void ui2c_close(F_HANDLE fd);


Probes the serial port for UART-to-I2C adapter presence.


  • fd - The handle to the serial port.

Returns: 1 if the device is present, 0 otherwise.

int ui2c_probe(F_HANDLE fd);


Probes a serial port for UART-to-I2C adapter presence by name.


  • dev_name - The name of the device (serial port) to probe. Examples: "COM1" (Windows), "/dev/ttyUSB0" (Linux).
  • speed - The speed of the UART communication.

Returns: 1 if the device is present, 0 otherwise.

int probe_ui2c_device(const char *dev_name, int speed);


Sets logging level on the UART-to-I2C adapter device.


  • fd - The handle to the UART-to-I2C adapter device.
  • uLevel - The logging level.
void ui2c_enable_logging(F_HANDLE fd, unsigned char uLevel);


Performs read and write operations with the I2C device via UART-to-I2C adapter according to the provided i2c_msg structures. Don't release I2C bus between operations.


  • fd - The handle to the UART-to-I2C adapter device.
  • msgs - An array of i2c_msg structures containing the read and write operations.
  • num_msgs - The number of i2c_msg structures in the array.

Returns: 0 on success, error code otherwise.

  • UI2C_2W_STATUS_OK 0x00 // OK
  • UI2C_2W_ERR_TOO_LONG 0x01 // data too logs
  • UI2C_2W_ERR_ADDR_NACK 0x02 // addr NACK
  • UI2C_2W_ERR_DATA_NACK 0x03 // data NACK
  • UI2C_2W_ERR_UNKNOWN 0x04 // general error
  • UI2C_2W_ERR_TIMEOUT 0x05 // timeout

int ui2c_rdwr(F_HANDLE fd, struct i2c_msg **msgs, int num_msgs);


Check if device with specified address exists on the I2C bus

Returns: 1 - if device is present, 0 - if device is not responding or error accured.

i2c_probe_dev(F_HANDLE fd, int dev_addr);

C Example


int main(int argc, char** argv) { // Define the i2c_msgs struct i2c_msg msg1, msg2; int addr = 0x0b; // default DJI battery address char data[] = { 0x3f }; i2c_msg_write(&msg1, addr, data, 1); // request hardware ID 0x3f command i2c_msg_read(&msg2, addr, 2); // read hardware ID, 2 bytes

// Initialize the UartI2C int uart_i2c; if(argc<2) { printf("Serial port with UI2C adapter not specified\n"); return -1; } printf("Try open %s\n", argv[1]); uart_i2c = ui2c_open(argv[1], 115200); if(uart_i2c <= 0) return -1;

printf("Probe UI2C adapter...\n"); if(!ui2c_probe(uart_i2c)) { printf("Probe UI2C failed\n"); return -1; }

printf("Probe device...\n"); if(!i2c_probe_dev(uart_i2c, addr)) { printf("Probe I2C device @0x%x failed\n", addr); return -1; }

// Perform the i2c_rdwr operation printf("Send/receive...\n"); struct i2c_msg* msgs[2] = { &msg1, &msg2 }; ui2c_rdwr(uart_i2c, &msgs[0], 2);

// Print the received data printf("Received ID: %4.4x\n", *((uint16_t*)(msg2.buf)));

// Cleanup i2c_msg_free(&msg1); i2c_msg_free(&msg2); ui2c_close(uart_i2c);

return 0; }


Firmware hex: ui2c-v1-hex.tar.gz
Sources: ui2c-v1.04.tar.gz Adruino code, Python API, C/C++ API


See also:

FB or mail (remove X)   Share
designed by Alter aka Alexander A. Telyatnikov powered by Apache+PHP under FBSD © 2002-2024