CH341A SPI Programming (Windows API)

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

CH341A is the USB interface chip capable of I2C, SPI and serial communication. See how to use it to control SPI devices with the C/C++ API. 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 SPI converter (not only SPI, but this article will focus only on SPI function). Let's see how to use the included library and header to communicate with SPI peripherals.
CH341A SPI Programming (Windows API)

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:
  • CH341OpenDevice(0); - opens the first CH341 device; cast its return value to int and if it's < 0, then an error occurred.
  • CH341ResetDevice(0); - resets the device; I don't know if it's really necessary; returns true if succeeded.
  • CH341SetStream(0, iMode); - configures number of I/O ports and bit order. See below
  • Functions to perform SPI transfers - are detailed in the next sections.
  • CH341CloseDevice(0); - closes the first device.
CH341A SPI iMode
Setting bit 2 of iMode to 1 enables the second SPI port. D3 remains the common clock source, but D4 and D6 are additional data I/O lines. If your hardware is MiniProgrammer, D4 and D6 are not connected to anything. Bit 7 configures endianness. Basically, for the MiniProgrammer this is the only bit you have to configure, therefore iMode can be 0x80 or 0x00.

SPI Transfers

The API has two main SPI stream functions. There is one for each port setup (one for single I/O and the other for double port I/O). The chip (slave) select pin can be passed as a parameter to each of these functions. The two port mode uses a common clock and common chip select pin, so its usability is somehow limited.
CH341A MiniProgrammer connected to logic analyzer
CH341A MiniProgrammer connected to logic analyzer
I connected my CH341A device to a logic analyzer. I used some 100 ohms resistors because CH341A has 5V levels while the logic analyzer expects 3.3V. No data is read because there is no slave to send data.

CH341StreamSPI4

I will not talk about CH341StreamSPI5 too. It works in the same mode, except a additional I/O buffer that holds data from the second port. Let's see the API definition:
BOOL WINAPI CH341StreamSPI4(  // Processing the SPI data stream, 4-wire interface, the clock line for the DCK / D3 pin, the output data line DOUT / D5 pin, the input data line for the DIN / D7 pin, chip line for the D0 / D1 / D2, the speed of about 68K bytes
/* SPI Timing: The DCK / D3 pin is clocked and defaults to the low level. The DOUT / D5 pin is output during the low period before the rising edge of the clock. The DIN / D7 pin is at a high level before the falling edge of the clock enter */
 ULONG   iIndex,  // Specify the CH341 device serial number
 ULONG   iChipSelect,  // Chip select control, bit 7 is 0 is ignored chip select control, bit 7 is 1 parameter is valid: bit 1 bit 0 is 00/01/10 select D0 / D1 / D2 pin as low active chip select
 ULONG   iLength,  // The number of bytes of data to be transferred
 PVOID   ioBuffer );  // Point to a buffer, place the data to be written from DOUT, and return the data read from DIN
Let's issue this command and check its output. I set iMode to 0.
CH341StreamSPI4 Output
The parameter iChipSelect should be set to 0 if CS is not used. Otherwise 0x80 if D0 is CS, 0x81 for D1 or 0x82 for D2. Clock frequency seems to be approximately 1.7 MHz, with no way of changing it. I don't understand why CS remains active for a long while after transfer is done. Specifically it stays low about 4 ms.
CH341StreamSPI4 CS
This function performs read and write too. The read data can be found in the same array that was passed as write buffer. The protocol looks rather strange. Data is sampled when clock goes low and you can't change this. While clock is low, data defaults to idle state (high), although next bit is low and this shouldn't be necessary. When streaming an 8-bit byte, clock makes 4 cycles followed by a small pause before next 4 cycles.

CH341BitStreamSPI

This function streams data in a bit-banging approach. Basically it writes directly to CH341A data port. Before calling it, port needs to be configured using CH341Set_D5_D0 to set data direction and initial state. D6 and D7 are always inputs and cannot be changed. This kind of approach does not allow other SPI modes but we can have up to three distinct slave select pins. Let's see API definitions.
BOOL WINAPI CH341Set_D5_D0(  // Set the I / O direction of the D5-D0 pin of CH341 and output data directly through the D5-D0 pin of CH341, which is higher than CH341SetOutput
/* ***** Use this API with caution to prevent the I / O direction from changing the input pin into an output pin that causes a short circuit between the output pins and other output pins ***** */
 ULONG   iIndex,  // Specify the CH341 device serial number
 ULONG   iSetDirOut,  // Set the D5-D0 pin I / O direction, a clear 0 is the corresponding pin for the input, a position of the corresponding pin for the output, parallel port mode default value of 0x00 all input
 ULONG   iSetDataOut );  // Set the output data of each pin of D5-D0. If the I / O direction is output, the corresponding pin output is low when a bit is cleared to 0, and the pin output is high when a bit is set
// The bits 5 to 0 of the above data correspond to the D5-D0 pin of CH341, respectively

BOOL WINAPI CH341BitStreamSPI(  // Processing the SPI bit data stream, 4 line / 5 line interface, the clock line for the DCK / D3 pin, the output data line DOUT / DOUT2 pin, the input data line for the DIN / DIN2 pin, chip select line D0 / D1 / D2, the speed of about 8K bit * 2
 ULONG   iIndex,  // Specify the CH341 device serial number
 ULONG   iLength,  // Ready to transfer the number of data bits, up to 896 at a time, it is recommended not to exceed 256
 PVOID   ioBuffer );  // Point to a buffer, place the data to be written from DOUT / DOUT2 / D2-D0, and return the data read from DIN / DIN2
/* SPI Timing: The DCK / D3 pin is clocked and defaults to the low level. The DOUT / D5 and DOUT2 / D4 pins are output during the low level before the rising edge of the clock. The DIN / D7 and DIN2 / D6 pins are clocked The falling edge of the previous high period is entered */
/* A bit in the ioBuffer is 8 bits corresponding to the D7-D0 pin, bit 5 is output to DOUT, bit 4 is output to DOUT2, bit 2-bit 0 is output to D2-D0, bit 7 is input from DIN, bit 6 from DIN2 Input, bit 3 data ignored */
/* Before calling the API, you should call CH341Set_D5_D0 to set the I / O direction of the D5-D0 pin of CH341 and set the default level of the pin */
You have no control of the clock. It is generated automatically and its pin corresponding bit is ignored. Let's see how it works.
CH341BitStreamSPI
The first byte I wrote to CH341A (0b000001) sets CS to high (D0 is bit 0, set to 1). All the remaining bits are 0, meaning bit 0 is transferred. The third byte has bit 5 set to 1. This corresponds to D5, MOSI. Therefore a high bit will be transferred. I'm writing a ninth byte just to clear the CS (D0), but CH341A performs an extra clock cycle. I should have used again CH341Set_D5_D0 to clear CS, without generating an extra clock cycle.

This approach is rather unusual and it mixes low level bit-banging with higher level automatic clock generation. Clock speed is about 300 kHz! The eight clock cycle is delayed.

These are CH341A API functions for SPI transfers. The API is available only for Windows. For Linux, there is a driver, but libusb is probably a better and easier choice.

6 comments :

  1. Any idea if these CH341A based boards can be used with something like avrdude.exe to program AVR MCU's like Attiny85 using SPI ISP function?

    ReplyDelete
    Replies
    1. In theory it should be possible to program AVR MCUs but I don't know about avrdude support for CH341A. I could find a similar tool at https://github.com/Trel725/chavrprog. Also, WCH offers a Chinese executable with ISP programming support in archive CH341DP.zip.

      Delete
  2. Is it possible to find CH314DLL.H in English? I would appreciate if you share it. The comments in the same file from archive CH341DP.zip seem to be in Chinese.

    ReplyDelete
  3. Please help me to run this library in codeblcoks.
    My code is compiled correclty but it doesn't work ( nothing happens on programmer )

    #include
    #include
    #include
    #include
    #include "CH341DLL.H"

    void write_test(void)
    {
    uint32_t i;

    uint8_t buff[] = { 0x30, 0xAB, 0xCD, 0xEF, 0x00, 0x00, 0x00 };

    for(i=0; i<0xFF; i++)
    {
    Sleep(5);
    CH341StreamSPI4(0, 1, 7, buff);
    }
    }


    int main()
    {
    if(CH341OpenDevice(0) < 0)
    {
    printf("\nCan't open device");
    return -1;
    }

    if(CH341ResetDevice(0)!=1)
    {
    printf("\nCan't reset device");
    return -1;
    }

    printf("\n CH341GetVersion %lu", CH341GetVersion());
    printf("\n CH341GetDrvVersion %lu", CH341GetDrvVersion());
    printf("\n CH341GetDeviceName %s", (char*)CH341GetDeviceName(0));
    printf("\n CH341GetVerIC %lu", CH341GetVerIC(0));

    CH341SetStream(0, 0x80);

    write_test();

    CH341CloseDevice(0);

    return 0;
    }

    Console output is :


    CH341GetVersion 33
    CH341GetDrvVersion 34
    CH341GetDeviceName \\?\usb#vid_1a86&pid_5512#6&4be4af4&0&3#{5446f048-98b4-4ef0-96e8-27994bac0d00}
    CH341GetVerIC 48
    Process returned 0 (0x0) execution time : 1.534 s
    Press any key to continue.

    ReplyDelete
    Replies
    1. Change CH341StreamSPI4(0, 1, 7, buff);
      to CH341StreamSPI4(0, 0x81, 7, &buff[0]);
      and try again

      Delete

Please read the comments policy before publishing your comment.