CH341A I2C Programming (Windows API)

 Posted by:   Posted on:   Updated on:  2023-11-12T09:15:38Z

CH341A is the USB interface chip capable of I2C, SPI and serial communication. Using it to control I2C devices is easy with the C/C++ API. Let's see how to program it and what it can do. Check out the correlation between API functions and bus signal sampled with a logic analyzer.

CH341A is an USB interface chip that can emulate UART communication, standard parallel port interface, parallel communication and synchronous serial (I2C, SPI). The chip is manufactured by Chinese company Jiangsu QinHeng Ltd.

CH341A is used by some cheap memory programmers. The IC is somehow limited in this configuration, because the programmer makes use only of the SPI and I2C interface. A popular device is the so-called "CH341A MiniProgrammer" that you can buy for 2 to 5 USD. And this is probably the cheapest device using CH341A.

If you got a "MiniProgrammer", you may want to use for more than memory chips programming. The device can actually be used as USB to I2C converter (not only I2C, but this article will focus only on I2C function). Let's see how to use the included library and header to communicate with I2C devices.
CH341A I2C Programming

Set up

You need the CH341PAR.ZIP file. This contains the C header and linker .LIB. It also contains the library CH341DLL.DLL which becomes a dependency for programs linked with CH341DLL.LIB. So, the files you need are:
  • CH341DLL.H - header; add #include directive to it in your source code;
  • CH341DLL.LIB - linker library; add it to linker with -lCH341DLL argument;
  • CH341DLL.DLL - library; place it in the same folder with the compiled executable or System32 folder.
Step by step, to init CH341 you need the following functions:
  1. CH341OpenDevice(0); - opens the first CH341 device; cast its return value to int and if it's < 0, then an error occurred.
  2. CH341ResetDevice(0); - resets the device; I don't know if it's really necessary; returns true if succeeded.
  3. CH341SetStream(0, iMode); - if iMode == 1, sets I2C speed to 100 kHz (default); otherwise iMode is 0 for 20 kHz, 2 for 400 kHz or 3 for 750 kHz; returns true if succeeded. Optional.
  4. Functions to perform I2C transfers - are detailed in the next sections.
  5. CH341CloseDevice(0); - closes the first device.

I2C Transfers

The API has three functions dedicated to I2C data transfers. The documentation isn't very straightforward, so I connected I2C pins to a logic analyzer (for pinout of the ZIF connector see my previous post about CH341A MiniProgrammer). Let's see how to use each of the I2C functions.

Note: in my examples no transfer is ack by slave device and all read bytes are always 0xFF. That is because there is no slave device. CH341A is connected only to the logic analyzer.

CH341WriteI2C

According to the API (comments translated from Chinese using Google):
BOOL WINAPI CH341WriteI2C(  // Write one byte of data to the I2C interface
 ULONG   iIndex,  // Specify the CH341 device serial number
 UCHAR   iDevice,  // The lower 7 bits specify the I2C device address
 UCHAR   iAddr,  // Specifies the address of the data unit
 UCHAR   iByte );  // The byte data to be written
Let's issue this command with some parameters and see what it outputs.
CH341WriteI2C
I used iDevice = 0x86, iAddr = 0x22 and iByte = 0x13. The first thing that catches attention is the device address (iDevice) which is chopped to 7 bits, leaving out the MSB. So, as API says, the address of the device must be passed to this function without R/W bit (LSB). The right way would have been to set iDevice = (0x86 >> 1);.

To summarize, CH341WriteI2C works in this way:
  1. I2C Start;
  2. I2C Write Address Byte with LSB = 0 (iDevice);
  3. I2C Write one data byte (iAddr);
  4. I2C Write second data byte (iByte);
  5. I2C Stop.

CH341ReadI2C

This is the function description from API:
BOOL WINAPI CH341ReadI2C(  // Reads one byte of data from the I2C interface
 ULONG   iIndex,  // Specify the CH341 device serial number
 UCHAR   iDevice,  // The lower 7 bits specify the I2C device address
 UCHAR   iAddr,  // Specifies the address of the data unit
 PUCHAR   oByte );  // Point to a byte unit, used to save the read byte data
Let's test it.
CH341ReadI2C
This time I shifted the address (iDevice) to the right and as you can see I got the desired result. The function writes iAddr byte (0xD4), then issues a repeated start sequence before reading one byte from the read address.

To summarize, CH341ReadI2C works in this way:
  1. I2C Start;
  2. I2C Write Address Byte with LSB = 0 (iDevice);
  3. I2C Repeated Start;
  4. I2C Write Address Byte with LSB = 1 (read request);
  5. I2C Read one byte from slave (oByte);
  6. I2C Stop.

CH341StreamI2C

It's less likely that the above two functions will meet the needs for most applications. Therefore the API provides a function that's more complex and versatile. Here it is the API definition.
BOOL WINAPI CH341StreamI2C(  // Processing I2C data stream, 2-wire interface, the clock line for the SCL pin, the data line for the SDA pin (quasi-bidirectional I / O), the speed of about 56K bytes
 ULONG   iIndex,  // Specify the CH341 device serial number
 ULONG   iWriteLength,  // Ready to write the number of data bytes
 PVOID   iWriteBuffer,  // Point to a buffer, place the data to be written, the first byte is usually the I2C device address and read and write direction bits
 ULONG   iReadLength,  // Ready to read the number of data bytes
 PVOID   oReadBuffer );  // Point to a buffer, return after reading the data
Let's try different situations.

Write only

Skip reading by setting iReadLength to 0 and oReadBuffer to NULL.
CH341StreamI2C
It easy to notice that CH341 writes to the bus everything that was passed in iWriteBuffer. It doesn't mess the device address anymore. So, remember you have to pass the non-shifted address of the device as the first byte of the write buffer.

To summarize, CH341StreamI2C with zero read length works like this:
  1. I2C Start;
  2. I2C Write all bytes from buffer, one by one;
  3. I2C Stop.

Read only

This time, set iWriteLength to 0 and iReadBuffer to NULL. The result is not very surprising.
CH341StreamI2C
CH341 issues the I2C Start sequence, then runs the clock. No address is sent over the bus, therefore this usage is wrong. Slave devices will not respond to the clock without a valid address. When it's done expecting input data, CH341 issues the stop sequence.

Write, then read

Let's see what happens if we write a byte, then read from the bus.
CH341StreamI2C
Although the first and only byte written was 0x86, the API changed the LSB to 1 (0x87), turning the write address into a read address. This is sent over the bus, then data is expected from slave.

To summarize, CH341StreamI2C with 1 byte write length and non-zero read length is used to read data from slave devices like this:
  1. I2C Start;
  2. I2C Write Address with LSB = 1 (read from slave);
  3. I2C Read - repeated iReadLength times;
  4. I2C Stop.
Let's see what happens if we write more than 1 byte, then read from the bus.
CH341StreamI2C
As expected, the two bytes are sent to the bus (the first taking the role of device address). Then a repeated start sequence is issued followed by a read routine. Interesting, CH341 took the first byte from the write buffer, set its LSB to 1 (to turn it into read address) and sent it over the bus. Then, data from slave is expected and the bus is stopped. This example emulates the behavior of CH341ReadI2C.

To summarize, CH341StreamI2C with more than 1 byte write length and non-zero read length is used to read data from slave devices that use sub-addressing (registers) like this:
  1. I2C Start;
  2. I2C Write Address with LSB = 0 (write mode);
  3. I2C Write iWriteLength bytes (starting at second byte from write buffer);
  4. I2C Repeated Start;
  5. I2C Write Address with LSB = 1 (read mode);
  6. I2C Read data - repeated iReadLength times;
  7. I2C Stop.
I hope you can successfully program your CH341 device now. Further reading: SPI Programming.

21 comments :

  1. these writeups have been immensely useful to me
    thanks enormously

    ReplyDelete
  2. I followed the steps but I get an error stating undefined reference to the functions in the header despite including the header file.

    ReplyDelete
    Replies
    1. Did you add the library to the linker with -lCH341DLL?

      Delete
    2. Yes I did, I am a student and using devC++, Is there a way I can contact you?I have spent hours figuring this, but can't get a solution.

      Delete
    3. Having the exact same issue. Know nothing about C or C++, so I also have no idea what to do.

      Delete
  3. have you tried with 2 byte register slave? (write 3 byte first)
    I found it is not working.
    I could use CH341StreamI2C with 1 byte address whtch is CH341ReadI2C function.
    you you have any idea?
    thank you.

    ReplyDelete
    Replies
    1. No, I don't think CH341WriteI2C and CH341ReadI2C work with 16-bit addresses. You may use CH341StreamI2C for this.

      Delete
  4. Also got undefined reference errors when I developed with codeblocks 20.03 (under windows 10) IDE using the gcc or g++ compiler. Might be related with the difficulties I experienced adding the library to the IDE. In the end, I changed to visual studio 2022 (version 17.1.1.) where I managed to compile a working c++ console application.

    p.s. (I'm not an experienced C/C++/codeblocks user, so maybe/probably I did something wrong.)

    ReplyDelete
    Replies
    1. I don't know about CodeBlocks. I only tested this with Qt Creator IDE.

      Delete
  5. I could use CH341StreamI2C ,CH341WriteI2C and CH341ReadI2C. These are all bool type return functions. According to I2C spec I expected when no acknowledge after slave address byte these function return false. My test result is, no matter slave device is connected or not , all these functions always return true. how to fix this?

    ReplyDelete
  6. Hi, any one have idea about CH341B drivers or any companiable drivers ....

    ReplyDelete
  7. Tested on Embarcadero RAD Studio - OK!!!!

    ReplyDelete
  8. Hi, You mentioned that you tested this with Qt Created IDE. I am using Qt Created IDE in Windows. I am a beginner however after linked the library file (LIBS += -L"$$_PRO_FILE_PWD_/i386/" -lCH341DLL) in .pro file and try to build it gives me errors like "UCHAR does not name a type, did you mean QCHAR_H?", "mPCH341_INT_ROUTINE was not declared in this scope", "BOOL does not name a type" and many other similar errors. I tried with MSVC2019 64bit, 32bit same issue. Any suggestion what I may be doing wrong? Thank you.

    ReplyDelete
    Replies
    1. Anyway, i found it. I forgot to add #include

      Delete
  9. Like the above comment, I am having trouble with all the errors in the .H file. But I have no idea what I need to include into the .H file to remove these errors. I Have not linked the .Lib file into the project. Not sure how (it's been a long time since I worked in c++), I'm using a c++ console app in visual studio. I've gone to project properties->Linker->Input->Additional Dependencies, but not sure how to add the LIB reference. Any help here would be appreciated, this seems to be the only way to interface with the USB to I2C adapter on windows.

    ReplyDelete
    Replies
    1. For C# implementation I simply used this to execute the methods.

      [DllImport("CH341DLLA64.DLL", EntryPoint = "CH341StreamI2C", CharSet = CharSet.Unicode, SetLastError = true)]
      public static extern bool StreamI2C(ulong iIndex, ulong iWriteLength, UInt32[]? iWriteBuffer, ulong iReadLength, out UInt32[] oReadBuffer);

      As long as the DLL is in system32 folder this works! However, I'm suspecting my USBtoI2C adapter is not correctly communicating with my device (Matrix Orbital LCD character display). As nothing is happening! Open method returns a handle (what do I do with this handle?). My write attempts return a success, my read attempts return null. screen does nothing.

      Delete

Please read the comments policy before publishing your comment.