| 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
      ui2c.py
  build-in logging feature in RAW mode, doesn't interfere with regular data flow
 RAW mode (default) packet formatWrite UART -> I2C, RD=0
 WRITE 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
READ  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.
 
mode=coptonix
mode=manual
version?
 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
 
  | request | action | Coptonix reply | Manual reply | 
|---|
 | a<CR> | get remote I2C device address | aXX<CR> | Dst Address: XXX<CR> |  | cXXX<CR> | set remote I2C device address | c<CR> | OK<CR> |  | s<CR> | save remote I2C device address in EEPROM and make it default | s<CR> | OK<CR> |  | wXXX...XXXX<CR> | write date to remote device | w<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 | x<CR>[iXXX...XXX]<CR>
 | Formatted hex dump |  | r<CR> | switch to RAW mode | UI2C vX.X RAW mode<CR> |  | m<CR> | switch to Manual mode | UI2C vX.X Manual mode<CR> |  | lN<CR> | set log level to N | Logging ON<CR> or nothing
 |  | ?<CR> | scan I2C bus | formatted list of devices |  | v<CR> | get FW revision | UI2C 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
Scanning...
I2C device found at address 0x0B!
done
c0b                     select device 0x0b
OK
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
aFE
c0B                     select device 0x0b
c
w223E                   send bytes 0x22 3e
w3F                     send byte 0x3f and wait for reply
i5B0F3F3F3F3F3F3F....
 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)
 ui2c.py
  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 = i2c_msg.read(dev_addr, 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 i2c_msg.read(). 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. C/C++ LIBi2c_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. */
};i2c_msg_read
  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); i2c_msg_write
  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); i2c_msg_free
  Frees the resources associated with an i2c_msg structure.
 void i2c_msg_free(struct i2c_msg* msg); ui2c_open
  Opens the serial port with UART-to-I2C adapter for communication.
 
  Parameters:
 
  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); ui2c_close
  Closes port with the UART-to-I2C adapter device.
 void ui2c_close(F_HANDLE fd); ui2c_probe
  Probes the serial port for UART-to-I2C adapter presence.
 
  Parameters:
 
  fd- The handle to the serial port. 
  Returns: 1 if the device is present, 0 otherwise.
 int ui2c_probe(F_HANDLE fd); probe_ui2c_device
  Probes a serial port for UART-to-I2C adapter presence by name.
 
  Parameters:
 
  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); ui2c_enable_logging
  Sets logging level on the UART-to-I2C adapter device.
 
  Parameters:
 
  fd- The handle to the UART-to-I2C adapter device.uLevel- The logging level. void ui2c_enable_logging(F_HANDLE fd, unsigned char uLevel); ui2c_rdwr
  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.
 
  Parameters:
 
  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); i2c_probe_dev
  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
#include 
#include 
#include 
 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;
}
 DownloadGitHub: https://github.com/Alter-1/ui2cFirmware hex: ui2c-v1-hex.tar.gz
 Sources: ui2c-v1.04.tar.gz Adruino code, Python API, C/C++ API
 
 2023.06.28
 
 
 
 
See also:
 FB
  or mail alterX@alter.org.ua (remove X)
   
  Share     
 |