Saturday, February 20, 2016

Controling the I2C bus with AutoIT with the CH341 or CH341A USB chip

If I only knew of the lack of documentation I wouldn't have bought the chip maybe - but I wanted a simple solution to connect I2C devices to the PC. There's a board available with this chip for $10 to $15 incl. shipping usually (ebay or aliexpress, "usb i2c").  It's often called "ALL IN 1 Multifunction USB to SPI/I2C/IIC/UART/TTL" and supports both 3.3V and 5V. It comes without any driver or documentation. The manufacturer WCN/Winchiphead offers a lot of downloads including drivers, basic documentation (only available in Chinese) and sample code (not very well documented, all comments are Chinese again) - enough to get started.
It works as single master on the I2C bus, that's OK for over 99% of the use cases. Slave mode is not supported and it's save to assume that a multi-master setup isn't supported as well, so if you need these functions you'll have to find another solution.
The USB to SPI/I2C/IIC/UART/TTL/ISP Serial Adapter Module

On the top there are a 12 MHz quartz, a CH314A converter chip, two LEDs, four jumpers a a lot of connectors, and on the bottom are a few resistors and capacitors and a  AMS1117-3.3 voltage regulator chip to supply 3.3 volts.

Who needs more documentation?


The jumpers are more or less self-explaining. It's important to set the signal voltage level select jumpers as a pair, not individually. The external supply voltage jumper sets the voltage supplied at the VCC pin to either to 5V fix or to the voltage that is selected by the signal voltage level select jumpes, i.e. 3.3V or 5V depending on the jumper positions.

Windows driver with 64bit installer:
ch341a.zip (Source: http://www.abacom-online.de/div/ch341a.zip)
  Folder: .\Driver\CH341_EPP_MEM_I2C_IO\
The 64 bit installer should show:
WCH.CN
  |__ USB-EPP/I2C... CH341A
      |__ 06/05/2009, 2.2.2009.06

Windows 32 bit installer:
CH341SER.ZIP  (Source: http://wch.cn/downfile/5) with 32bit and 64bit installer
Download this file only if you need a 32 bit installer, but do not install the included driver. Use the driver supplied with the 64bit installer linked above.

API documentation: CH341DS2.PDF (Source: http://wch.cn/downfile/24)

A mirror for these files: https://drive.google.com/drive/folders/0B1xSSg6lZlzaR1BlSVNKLUhwRDQ?resourcekey=0-D_lXKzjEfqGHlsmPNmZ5aw&usp=sharing

 
After installing the driver and plugging in the device configured as IIC/SPI and you'll see it in the device manager:

A screenshot of the Device Manager with the device.
When configured as IIC/SPI device it's a unique interface device, so you'll have to write your own program to use this device. Here's a short list of the necessary functions for I2C only, based on a translation of the API documentation and corrected based on the provided examples and my own tests. Basically you'll need only four functions:
  • CH341OpenDevice - initialize the device
  • CH341SetStream - set the I2C speed
  • CH341StreamI2C - read and/or write data 
  • CH341CloseDevice - close the device
The functions CH341ReadI2C and CH341WriteI2C are very limited in their functionality, so I suggest to always use CH341StreamI2C instead. I have them here in the list just for the sake of completeness. Here's the API description:

//************************
//CH341OpenDevice
//
/*
Call the function CH341OpenDevice once before calling other functions.
*/
HANDLE WINAPI CH341OpenDevice(      // open CH341 device, return a handle
  ULONG iIndex                      // id of the USB device
);
/*
If the returned number is smaller then zero the device couldn't be opened.
*/


//************************
//CH341CloseDevice
//
/*
Call the function CH341CloseDevice to close the device.
*/
VOID CH341CloseDevice(              // close the CH341 device
  ULONG iIndex                      // id of the USB device
);


//************************
//CH341SetStream
// 
/*
Call the function CH341SetStream once to set the data rate.
*/
BOOL WINAPI CH341SetStream(         // configure the serial flow mode
  ULONG iIndex,                     // id of the USB device
  ULONG iMode                       // specify the mode, see below
); 
/*
// Bit 1,0: I2C interface speed / SCL frequency
//    00 = low / 20KHz
//    01 = standard / 100KHz (default)
//    10 = fast / 400KHz
//    11 = High Speed ​​/ 750KHz
// Bit 2: SPI's I / O Number / IO pin
//    0 = single-input single-output (D3 clock / D5 out / D7 into) (default)
//    1 = double into a double (D3 clock / D5 out D4 a / D7 D6 enter into)
// Bit 7: SPI byte bit order
//    0 = LSB first
//    1 = MSB first
// Other Reserved, must be 0
// To sum this up: Set iMode to 0, 1, 2 or 3 for 20, 100, 400 or 750 kHz.
*/


//************************
//CH341ReadI2C
// 
/*
Call this function to write and read one byte from a slave device on
the I2C bus.
*/
BOOL WINAPI CH341ReadI2C(           // read data
  ULONG iIndex,                     // id of the USB device
  UCHAR iDevice,                    // I2C device address (7 bits)
  UCHAR iAddr,                      // I2C device register (writes one byte)
  PUCHAR oByte                      // I2C data (reads one byte)
);


//************************

//CH341WriteI2C
// 
/*
Call this function to write two bytes of data on the I2C bus.
*/  
BOOL WINAPI CH341WriteI2C(          // write data
  ULONG iIndex,                     // id of the USB device
  UCHAR iDevice,                    // I2C device address (7 bits) 
  UCHAR iAddr,                      // I2C device register (writes first byte)
  UCHAR iByte                       // I2C data (writes second byte)
);


//************************
//CH341StreamI2C
// 
/*
Call this function to read, write, or write and read a specified amount of
bytes on the I2C bus. Read length or write length can be set to zero

BOOL WINAPI CH341StreamI2C(         // read/write data
  ULONG iIndex,                     // id of the USB device
  ULONG iWriteLength,               // length of the data (bytes to write)
  PVOID iWriteBuffer,               // pointer to the data
  ULONG iReadLength,                // length of the data (bytes to read)
  PVOID oReadBuffer                 // pointer to the data
);
/*
This function provides all functionality of CH341ReadI2C and CH341WriteI2C
plus much more flexibility. It's not that elegant because you have to prepare
the iWriteBuffer with the device address that is shifted by one bit to the
left by yourself (the least significant bit will be controlled by the chip
for read (1) and write (0) access automatically).
*/
 
Putting this all together in an undocumented test script for AutoIT:
#include <WinAPISys.au3>

;device ID
$id = 0


;open the dll
$dll = DllOpen("C:\Windows\System32\CH341DLL.DLL")
If $dll  <> -1 Then
   ConsoleWrite ("DllOpen: OK" & @CRLF)
Else
   ConsoleWrite ("DllOpen: Error, could not open the dll." & @CRLF)
   Beep(1000, 5000)
   Exit
EndIf

;open the device
$aResult = DllCall($dll, "BOOL", "CH341OpenDevice", "ULONG", $id)
If  $aResult[0] <> -1 Then
   ConsoleWrite ("CH341OpenDevice: OK" & @CRLF)
Else
   ConsoleWrite ("CH341OpenDevice: Error: Could not open device " & @CRLF)
   Beep(1000, 5000)
   Exit
EndIf

;set the I2C speed
$iMode = 0
$aResult = DllCall($dll, "BOOL", "CH341SetStream", "ULONG", $id, "ULONG", $iMode)
If  $aResult[0] Then
   ConsoleWrite("CH341SetStream: OK" & @CRLF)
   ConsoleWrite("  Param: $id="& $id & ", $iMode=" & $iMode & @CRLF)
Else
   ConsoleWrite("CH341SetStream: Error" & @CRLF)
   ConsoleWrite("  Param: $id="& $id & ", $iMode=" & $iMode & @CRLF)
   Beep(1000, 5000)
   Exit
EndIf

;Read and Write data
;to be used with CH341ReadI2C and CH341WriteI2C:
$i2c_addr = 85  ;b: 0101'0101 (I2C: 1010'101*, *=R/W bit)
;to be used with CH341ReadI2C and CH341WriteI2C:
$i2c_B1 = 0   ;b: 0000'0000
;to be used with CH341ReadI2C:
$sBYTE_r4 = DllStructCreate ("BYTE[4]" )
$pBYTE_r4 = DllStructGetPtr($sBYTE_r4)
;to be used with CH341WriteI2C:
$i2c_B2 = 231   ;b: 1110'0111

;to be used with CH341StreamI2C
$rlen = 0
$sBYTE_r = DllStructCreate ("BYTE[8]" )
$pBYTE_r = DllStructGetPtr($sBYTE_r)
$wlen = 3
$sBYTE_w = DllStructCreate ("BYTE;BYTE;BYTE;BYTE;BYTE;BYTE;BYTE;BYTE" )
DllStructSetData ( $sBYTE_w, 1, 170 ) ;b: 1010'1010 (I2C: 1010'101*, *=R/W bit) - I2C slave address
DllStructSetData ( $sBYTE_w, 2, 0 )  ;b: 0000'0000
DllStructSetData ( $sBYTE_w, 3, 231 ) ;b: 1110'0111
DllStructSetData ( $sBYTE_w, 4, 0 )
DllStructSetData ( $sBYTE_w, 5, 0 )
DllStructSetData ( $sBYTE_w, 6, 0 )
DllStructSetData ( $sBYTE_w, 7, 0 )
DllStructSetData ( $sBYTE_w, 8, 0 )
$pBYTE_w = DllStructGetPtr($sBYTE_w)
MsgBox(0, "PAUSED", "Click OK to continue with the tests")



ConsoleWrite("WRITE TEST: START" & @CRLF)
;CH341WriteI2C
$aResult = DllCall($dll, "BOOL", "CH341WriteI2C", "BYTE", $id, "BYTE", $i2c_addr,  "BYTE", $i2c_B1,  "BYTE", $i2c_B2)
If  $aResult[0] Then
   ConsoleWrite("CH341WriteI2C: OK" & @CRLF)
   ConsoleWrite("  Param: $id="& $id & ", $i2c_addr=" & $i2c_addr & ", $i2c_B1=" & $i2c_B1 & ", $i2c_B2=" & $i2c_B2 & @CRLF)
Else
   ConsoleWrite("CH341WriteI2C: Error" & @CRLF)
   ConsoleWrite("  Param: $id="& $id & ", $i2c_addr=" & $i2c_addr & ", $i2c_B1=" & $i2c_B1 & ", =$i2c_B2" & $i2c_B2 & @CRLF)
   Beep(1000, 5000)
   Exit
EndIf
MsgBox(0, "PAUSED", "Click OK to continue with the tests")


;CH341StreamI2C writing
$rlen = 0
$wlen = 3
$aResult = DllCall($dll, "BOOL", "CH341StreamI2C", "BYTE", $id, "BYTE", $wlen, "ptr", $pBYTE_w, "BYTE", $rlen,  "ptr", $pBYTE_r)
If  $aResult[0] Then
   ConsoleWrite("CH341StreamI2C: OK" & @CRLF)
   ConsoleWrite("  Param: $id="& $id & ", $wlen=" & $wlen & ", $rlen=" & $rlen & _
   ", $sBYTE_w1,2,3,4=" & DllStructGetData($sBYTE_w, 1) & _
   "," & DllStructGetData($sBYTE_w, 2) & _
   "," & DllStructGetData($sBYTE_w, 3) & _
   "," & DllStructGetData($sBYTE_w, 4) & _
   ", $sBYTE_r=" & DllStructGetData($sBYTE_r, 1) & @CRLF)
Else
   ConsoleWrite("CH341StreamI2C: Error" & @CRLF)
   ConsoleWrite("  Param: $id="& $id & ", $wlen=" & $wlen & ", $rlen=" & $rlen & _
   ", $sBYTE_w1,2,3,4=" & DllStructGetData($sBYTE_w, 1) & _
   "," & DllStructGetData($sBYTE_w, 2) & _
   "," & DllStructGetData($sBYTE_w, 3) & _
   "," & DllStructGetData($sBYTE_w, 4) & _
   ", $sBYTE_r=" & DllStructGetData($sBYTE_r, 1) & @CRLF)
   Beep(1000, 5000)
   Exit
EndIf
MsgBox(0, "PAUSED", "Click OK to continue with the tests")
ConsoleWrite("WRITE TEST COMLETE" & @CRLF)




ConsoleWrite("READ TEST: START" & @CRLF)
;CH341ReadI2C
$aResult = DllCall($dll, "BOOL", "CH341ReadI2C", "BYTE", $id, "BYTE", $i2c_addr,  "BYTE", $i2c_B1,  "PTR",  $pBYTE_r4)
If  $aResult[0] Then
   ConsoleWrite("CH341ReadI2C: OK" & @CRLF)
   ConsoleWrite("  Param: $id="& $id & ", $i2c_addr=" & $i2c_addr & _
   ", $i2c_B1=" & $i2c_B1 & _
   ", $sBYTE_r4=" & DllStructGetData($sBYTE_r4, 1) & @CRLF)
Else
   ConsoleWrite("CH341ReadI2C: Error" & @CRLF)
   ConsoleWrite("  Param: $id="& $id & ", $i2c_addr=" & $i2c_addr & _
   ", $i2c_B1=" & $i2c_B1 & _
   ", $sBYTE_r4=" & DllStructGetData($sBYTE_r4, 1) & @CRLF)
   Beep(1000, 5000)
   Exit
EndIf
MsgBox(0, "PAUSED", "Click OK to continue with the tests")


;CH341StreamI2C reading similar to CH341ReadI2C
$rlen = 1
$wlen = 2 ;I2C slave address and one byte
$aResult = DllCall($dll, "BOOL", "CH341StreamI2C", "BYTE", $id, "BYTE", $wlen, "ptr", $pBYTE_w, "BYTE", $rlen,  "ptr", $pBYTE_r)
If  $aResult[0] Then
   ConsoleWrite("CH341StreamI2C: OK" & @CRLF)
   ConsoleWrite("  Param: $id="& $id & ", $wlen=" & $wlen & ", $rlen=" & $rlen & _
   ", $sBYTE_w1,2,3,4=" & DllStructGetData($sBYTE_w, 1) & _
   "," & DllStructGetData($sBYTE_w, 2) & _
   "," & DllStructGetData($sBYTE_w, 3) & _
   "," & DllStructGetData($sBYTE_w, 4) & _
   ", $sBYTE_r=" & DllStructGetData($sBYTE_r, 1) & @CRLF)
Else
   ConsoleWrite("CH341StreamI2C: Error" & @CRLF)
   ConsoleWrite("  Param: $id="& $id & ", $wlen=" & $wlen & ", $rlen=" & $rlen & _
   ", $sBYTE_w1,2,3,4=" & DllStructGetData($sBYTE_w, 1) & _
   "," & DllStructGetData($sBYTE_w, 2) & _
   "," & DllStructGetData($sBYTE_w, 3) & _
   "," & DllStructGetData($sBYTE_w, 4) & _
   ", $sBYTE_r=" & DllStructGetData($sBYTE_r, 1) & @CRLF)
   Beep(1000, 5000)
   Exit
EndIf
MsgBox(0, "PAUSED", "Click OK to continue with the tests")


;CH341StreamI2C reading
$rlen = 8
$wlen = 1
$aResult = DllCall($dll, "BOOL", "CH341StreamI2C", "BYTE", $id, "BYTE", $wlen, "ptr", $pBYTE_w, "BYTE", $rlen,  "ptr", $pBYTE_r)
If  $aResult[0] Then
   ConsoleWrite("CH341StreamI2C: OK" & @CRLF)
   ConsoleWrite("  Param: $id="& $id & ", $wlen=" & $wlen & ", $rlen=" & $rlen & _
   ", $sBYTE_w1,2,3,4=" & DllStructGetData($sBYTE_w, 1) & _
   "," & DllStructGetData($sBYTE_w, 2) & _
   "," & DllStructGetData($sBYTE_w, 3) & _
   "," & DllStructGetData($sBYTE_w, 4) & _
   ", $sBYTE_r=" & DllStructGetData($sBYTE_r, 1) & @CRLF)
Else
   ConsoleWrite("CH341StreamI2C: Error" & @CRLF)
   ConsoleWrite("  Param: $id="& $id & ", $wlen=" & $wlen & ", $rlen=" & $rlen & _
   ", $sBYTE_w1,2,3,4=" & DllStructGetData($sBYTE_w, 1) & _
   "," & DllStructGetData($sBYTE_w, 2) & _
   "," & DllStructGetData($sBYTE_w, 3) & _
   "," & DllStructGetData($sBYTE_w, 4) & _
   ", $sBYTE_r=" & DllStructGetData($sBYTE_r, 1) & @CRLF)
   Beep(1000, 5000)
   Exit
EndIf
MsgBox(0, "PAUSED", "Click OK to finish")
ConsoleWrite("READ TEST COMLETE" & @CRLF)

The console output should like this if everything works fine (no I2c devices connected):
DllOpen: OK
CH341OpenDevice: OK
CH341SetStream: OK
  Param: $id=0, $iMode=0
WRITE TEST: START
CH341WriteI2C: OK
  Param: $id=0, $i2c_addr=85, $i2c_B1=0, $i2c_B2=231
CH341StreamI2C: OK
  Param: $id=0, $wlen=3, $rlen=0, $sBYTE_w1,2,3,4=170,0,231,0, $sBYTE_r=0x0000000000000000
WRITE TEST COMLETE
READ TEST: START
CH341ReadI2C: OK
  Param: $id=0, $i2c_addr=85, $i2c_B1=0, $sBYTE_r4=0xFF000000
CH341StreamI2C: OK
  Param: $id=0, $wlen=2, $rlen=1, $sBYTE_w1,2,3,4=170,0,231,0, $sBYTE_r=0xFF00000000000000
CH341StreamI2C: OK
  Param: $id=0, $wlen=1, $rlen=8, $sBYTE_w1,2,3,4=170,0,231,0, $sBYTE_r=0xFFFFFFFFFFFFFFFF
READ TEST COMLETE


And here are the pictures showing what's happening on the wire:
First write test (CH341WriteI2C)

Second write test (CH341StreamI2C)

First read test (CH341ReadI2C)

Second read test (CH341StreamI2C)

Third read test (CH341StreamI2C)
Settings for all pictures (RIGOL DS1052E):
Analog Ch  State   Scale    Position   Coupling  BW Limit  Invert
CH1        On      2.00V/   -4.00V     DC        Off       Off
CH2        On      2.00V/   0.00uV     DC        Off       Off

Analog Ch  Impedance   Probe
CH1        1M Ohm      1X
CH2        1M Ohm      1X

Time    Time Ref    Main Scale    Delay
Main    Center      200.0us/      840.0000us

Trigger  Source      Slope    Mode      Coupling     Level    Holdoff
Edge     CH1         Falling  Normal    DC            1.68V   500ns

Acquisition    Sampling    Memory Depth    Sample Rate
Normal         Realtime    Normal          1.000MSa    

6 comments:

  1. Hi Jenny,
    I want congratulate and thanks you so much for gently share your searchings/discoverings/work about the CH341/CH341A chip and the ALL IN 1 Multifunction USB to SPI/I2C/IIC/UART/TTL board. Like you I thought this board would be a simple and good solution to connect I2C devices to PC and suffered the same problem of lack of documentation and the available is only in Chinese and the diver available in the manufacturers’ page to be only for 32bits. The device only worked properly in my 64bits version after install the driver from the link of your blog. It be wonderful if you also can make an example in C.

    ReplyDelete
  2. What this tools can flash ic eeprom in module battery laptop?? Thank be for

    ReplyDelete
  3. thank you for the detailed and translated information.
    I got CH341 from aliexpress. And I could use it with 1 byte register address.
    do you know if 2 bytes register address is possible?

    ReplyDelete
    Replies
    1. I didn't carefully read your post.
      CH341StreamI2C can do that with left shiefted device address at write butter no '0'
      thank you!

      Delete
  4. Thanks a lot Very Very Helful, i have learnt Auto V3 many years ago so its very easy for me..

    ReplyDelete
  5. New Link: https://drive.google.com/drive/folders/0B1xSSg6lZlzaR1BlSVNKLUhwRDQ?resourcekey=0-D_lXKzjEfqGHlsmPNmZ5aw&usp=sharing

    ReplyDelete