Blueprint

IOX-77 - An ESP32 based SuperBoard (75+ GPIOs)

The IOX-77 is a powerful ESP32-C3 based devboard. It comes with 75 GPIOs, which is even more than the Arduino Mega while being smaller and embedding WiFi and Bluetooth capabilities. It's like a 6 core configuration with 5 cheap CH32v003 MCUs communicating with the main core, the ESP32. With this devboard, I'll never run out of IOs for all my future projects, even those requiring dozens of IOs. This board is an upgraded ESP 32 devboard, gathering the computational power of the ESP32 and the many GPIOs offered by all the CH32.

Created by Clém Clém

Tier 3

29 views

0 followers

Clém Clém submitted IOX-77 - An ESP32 based SuperBoard (75+ GPIOs) for ship review ago

cubit010 cubit010 requested changes for IOX-77 - An ESP32 based SuperBoard (75+ GPIOs) ago

fixing perma reject by paglu
you should be able to resubmit now!

Tier: 3

Clém Clém submitted IOX-77 - An ESP32 based SuperBoard (75+ GPIOs) for ship review ago

Clém Clém added to the journal ago

ㅤㅤㅤㅤㅤ

Project overview, and...

A huge thanks to Blueprint



IOX-77_project overview
ㅤㅤ
ㅤㅤ
I'm really happy of how this project turned out, and I learnt a lot of things along the way, designing my first 4L PCB, learning how to program the CH32V003, coding an Arduino library and especially creating a well organized github repo.

Thank you to the Blueprint team, Thank you to those who helped me debug the board, Thank you all for making this project possible 💖

Clém Clém added to the journal ago

Correcting the Pinout

I corrected the pinout sheet I had done before because I don't know why, I marked all the analog capable pins with A1, A5, Ax, even though they are called by they real pin name (A_PA2, C_PD2...) So I changed all the pin name and added colored stripes to indicate which pins are Analog capable and which ones are PWM capable. I also changed a few pin names, because I kinda mixed everything up with those 100 pins, resulting in pin names appearing twice, or in the wrong place... So I carefully checked again with the PCB design files, and hopefully the pinout is right, now...

IOX_77_1

IOX_77_2

IOX_77_3

Clém Clém added to the journal ago

IOX-77 code: Part 9 - coding a demo & packaging the library

I polished the CH32 .c file and the library (.cpp and .h files) by removing all the commented printf and millis() flags that helped me debug it, checked again that it worked just fine, and started packaging the library: I coded a small demo code as an example, which test the analog PWM writing, analog Reading and digitalReading, as well as the sleep mode, while printing on the serial monitor all the data.

I checked one more time that the demo worked correctly on the IOX-77, and before packaging all these files in a neat IOX.zip library file, I changed all the while (initTime + 1000 > micros()); to while (micros() - initTime < 1000); because apparently doing the substaction prevents any problems when overflowing (which happens every ~70 minutes for micros()...)

I've finally a working firmware V1.0 🎉, after so much hours coding and debugging this project 🥹
I think this project comes to an end, I just need to polish the github repo, and I think I'll be good to go.

demo BP

Again, all the code files are in the github repo. neatly organized in the FINAL VERSION V1.0 folder
(with the library IOX.zip, all the files inside this .zip also added in the IOX folder, as well as the main.c code for the CH32

Clém Clém added to the journal ago

CH32 cluster code Part 8 - AnalogRead() & sleep mode

The last, difficult, but important part to implement in the code was the analogReading. Again, I heavily inspired me from the Curious Scientist examples, and just had to modify it in order to be able to enable/disable the channels on demand. I first created the IOX.analogRead() function.

Turned out it was harder than expected to create the logic behind those GPIO manipulations, (handling which pins was analog capable...) but I eventually got something that worked on one channel. Nice!

However when scaling up to 2 GPIOs simultaneously, problems started to appear... the ADC readings went whoosh... After trying many things to debug it, I discovered that the IOX.pinMode (pin, ANALOG _ IN) wasn't received correctly by the CH32, thus preventing the ADC from being correctly initialized... So I looked at the received command by the CH32 over I²C when the IOX.pinMode function was called, and the state (0xC0, ie ANALOG _ IN) wasn't received correctly! it was replaced by 0xFF! I started going in circles until I realized that it was just that the command was being overwritten by the next function called, IOX.analogWrite() which resulted in a weird combo of pinMode command + 0xFF from the new command... Turned out the CH32 simply needed more time to process the IOX.pinMode (pin, ANALOG _ IN) command, since it initialized the ADC everytime this function was called. So I simply increased the pause time to 5 ms and it was good to go.

I didn't stopped myself here, and decided to create the same '32 bit' reading history, like I did for the digital read, except that I'm not storing bits (HIGH and LOW) but uint16_t integers, so I won't keep the 32 last readings but the 16 last ones (cuz it takes some memory space...), and I won't send them raw over I²C, they will instead be used for the creation of an averaged value. Same thing here, I created the analogRead _ sample() averageAnalogReading() functions as well as some structure to manage the channels and keep the data: analogSampling[] and analogReadingHistory[8][16] (8 ADC channels, 16 values kept in the buffer)

It was pretty hard to manage all these functions and variables, and even though what I come up with might probably not be the better way to do all that, so far it seems to be working, so I guess it's fine...

Part 8 BP

2 ADC readings - me playing around with 2 potentiometers -, one averaged with readings separated by a 64ms interval, the other by a 32ms interval). Sometimes there is some weird spikes (which shouldn't appear since it should be a smoothed signal) but It's not important: It's just that the potentiometer which wasn't connected correctly disconnected from time to time, leaving the GPIO floating.

I also implemented the sleep mode, where one command can be sent to a CH32 over I²C (CMD 0xAA) to go to sleep. If the ESP32 need it back, it can just reset it.

The new functions in the IOX library:

void IOXClass::sleepCH32 (uint8_t CH32_addr) {
    uint32_t initTime = micros();
    uint8_t CMD [SIZE] = {0xAA, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
    sendI2CCommand (CMD, CH32_addr);
    while (initTime + 3000 > micros());
}

void IOXClass::sleepCH32_All () {
    sleepCH32 (CH32_A_ADDR);
    sleepCH32 (CH32_B_ADDR);
    sleepCH32 (CH32_C_ADDR);
    sleepCH32 (CH32_D_ADDR);
    sleepCH32 (CH32_E_ADDR);
}

and on the CH32 side:

void EnterSleepMode (void) {
    RCC_APB2PeriphClockCmd (RCC_APB2Periph_TIM1, DISABLE);
    RCC_APB1PeriphClockCmd (RCC_APB1Periph_TIM2, DISABLE);
    RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOA, DISABLE);
    RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOD, DISABLE);
    RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOC, DISABLE);

    PWR_EnterSTANDBYMode  (PWR_STANDBYEntry_WFI);
}

And once the ESP32 goes to sleep too, (with the line esp_light_sleep_start();) the current consumption goes in the sub mA domain. The IOX-77 can go from 51mA idle (All 5 CH32 active) to 18mA (all 5 CH32 disabled) down to <1mA in sleep mode.

Note: Again, there are a few way too much functions and variable required for handling the ADC, so I'm not adding them in the Bluprint journal, I keep them in the github repo instead (Advancement #2)

Clém Clém added to the journal ago

IOX code Part 7 - Arduino Library & Reset

I converted the ESP32 code in the Arduino IDE to a library: It's only local, for now, I still have the .cpp and .h files, but I will only build the library package once the ESP32 code is finished. For now, I'll keep the 3 files altogether so I can easily change the code. Converting the code to a library wasn't that hard, and there where only some minor modifications to do, like changing the function definitions from IOX_digitalWrite (uint8_t pin, uint8_t state) to IOXClass::digitalWrite (uint8_t pin, uint8_t state), but at the end of the day, I have a clean HAL for the IOX-77, with the functions highlighted with the Arduino colors, thanks to the keyword.txt I created:

BP Part7

I'm really happy with how it's looking so far!
I played around a bit coloring some constant variables, like A_LED_BUILTIN or D_PA2, so that it looks exactly like an Arduino devboard, ready to be used out of the box!

I also implemented the hardware reset of the CH32 (the ESP32 can reset each CH32 independently by pulling their NRST pin LOW), so at boot up, the CH32 are reset too.

And guess what? while testing this new implementation on one board, 4 CH32 reset correctly, but the CH32 D just didn't care 😭! After checking it's NRST pin, I noticed that the ESP32 simply didn't pulled it LOW... Let's go for another soldering session 😒

I desoldered the ESP32, added a bit of solderpaste on the faulty pad, resoldered it in place, and... now one CH32 isn't responding anymore! (maybe due to overheating? I heated that board wayyyyy too many times 😅) I ended up replacing the faulty CH32, and eventually got this board repaired. Phew!

Hopefully every solder join is decent, now... At least I hope so...

And that's it: I can now easily control each additional GPIO, like on any other Arduino devboard! Now the last thing to do is to implement the ADC and low power modes.

I can see the light at the end of the tunnel! 😂

NOTE: the advancement of the code (IOX library) is in the Github repo, to avoid overloading the Blueprint journal

Clém Clém added to the journal ago

Taking a break in programming: resoldering the IOX-77...

You may have noticed that on the previous build images, I had slight problems with the QFN soldering: on some ICs, there were solder bridges on some GPIOs, and out of the 15 CH32s, 1 weren't recognized on mounriver studio, and for 2 others, the I²C didn't worked, since I was able to upload a blink program, but the IOX-77 code didn't worked. So I left this aside for a while, because I wanted to work on the CH32 code, but I now have to tackle this, to get a fully working devboard with the expected 75 GPIOs, not an half-working board with defective ICs...

So I took my hot plate and tried to solder them again correctly. I removed the faulty ICs, and after trying multiple time to resolder them in place, adding a bit of solder paste, some flux... I finally succeeded to get 2 board fully working... On the 3rd board, I damaged one trace, the GPIO trace for the LED of the CH32 A... other than that, the board is working, but with one dead LED... I also soldered the 40P FPC, since there were solder bridged too on the .5mm pitched legs.

At least I now have 2 fully working devboards, that I thoroughly rinsed with water, vigorously brushed to remove all the flux, before putting them in an IPA bath, which resulted in 2 clean PCBs. I inspected them under the microscope, and every solder join seemed good enough.

Gallery:

BUILD IMG resoldered (2)

BUILD IMG resoldered (3)

BUILD IMG resoldered (5)

I then tested a board with a few 3mm LEDs I had laying around, with a quick test code, and everything worked flawlessly:

const uint8_t headerPins [16] = {C_PC3, B_PC0, B_PD0, B_PA2, B_PA1, A_PD4, A_PD5, A_PD2, A_PD3, A_PD6, A_PC0, A_PC3, C_PC7, A_PC4, A_PA1, A_PA2};
for (int p = 0; p < 16; p++) {
  IOX_pinMode (headerPins[p], OUTPUT);
  IOX_digitalWrite (headerPins[p], HIGH);
  delay (1000);
  IOX_digitalWrite (headerPins[p], LOW);
  IOX_pinMode (headerPins[p], INPUT);
}

BUILD IMG resoldered (4)

Clém Clém added to the journal ago

CH32 cluster code Part 6 - HAL (Arduino IDE)

The ESP32, the main MCU (whereas the CH32s are only smart GPIO expanders), will be programmed using the Arduino IDE, so I needed an easy way of controlling each GPIO, without thinking too much to which CH32 it belongs: I thus made a few pin definitions, using generic names related to the pin & CH32 owner (like A _ PC0, or C _ PD3). Each pin name links to a 1 byte long ID, which directly gives the CH32 owner and pin number like so:

B_PD0 = 0xB8; //CH32 B - pin 8

I also defined the address of each CH32, with the corresponding address to set on the CH32 code (shifted by 1 to the left). I now have 70 GPIOs easily accessible:

const uint8_t CH32_A_ADDR = 0x01;   //0x02
const uint8_t CH32_B_ADDR = 0x03;   //0x06
const uint8_t CH32_C_ADDR = 0x04;   //0x08
const uint8_t CH32_D_ADDR = 0x05;   //0x0A
const uint8_t CH32_E_ADDR = 0x06;   //0x0C

const uint8_t A_PA1 = 0xA0; //CH32 A - pin 0
const uint8_t A_PA2 = 0xA1; //CH32 A - pin 1
const uint8_t A_PC0 = 0xA2; //CH32 A - pin 2
const uint8_t A_PC3 = 0xA3; //CH32 A - pin 3
...
const uint8_t A_PD4 = 0xAB; //CH32 A - pin 11
const uint8_t A_PD5 = 0xAC; //CH32 A - pin 12
const uint8_t A_PD6 = 0xAD; //CH32 A - pin 13

const uint8_t B_PA1 = 0xB0; //CH32 B - pin 0
const uint8_t B_PA2 = 0xB1; //CH32 B - pin 1
const uint8_t B_PC0 = 0xB2; //CH32 B - pin 2
...
const uint8_t B_PD6 = 0xBD; //CH32 B - pin 13

const uint8_t C_PA1 = 0xC0; //CH32 C - pin 0
...
const uint8_t C_PD6 = 0xCD; //CH32 C - pin 13

const uint8_t D_PA1 = 0xD0; //CH32 D - pin 0
...
const uint8_t D_PD6 = 0xDD; //CH32 D - pin 13

const uint8_t E_PA1 = 0xE0; //CH32 E - pin 0
const uint8_t E_PA2 = 0xE1; //CH32 E - pin 1
...
const uint8_t E_PD5 = 0xEC; //CH32 E - pin 12
const uint8_t E_PD6 = 0xED; //CH32 E - pin 13

Now, every time a function acts on one GPIO, the GPIO ID e.g 0xB8 (retrieved from the name, e.g. B _ PD0) is split into two parts: the CH32 owner (e.g. B) which links to the right CH32 I²C Address (thanks to the function getAddr and the pin number, which is later on sent to the right CH32 in the command CMD.

With the new functions implemented, here is how it looks like:

uint8_t getAddr (uint8_t pin) {
  switch (pin >> 4) {
    case 0x0A :
      return CH32_A_ADDR;
    case 0x0B :
      return CH32_B_ADDR;
    case 0x0C :
      return CH32_C_ADDR;
    case 0x0D :
      return CH32_D_ADDR;
    case 0x0E :
      return CH32_E_ADDR;
  }
  return 0xFF;
}

as in this example below, the pin number is directly extracted from the pin ID with pin & 0x0F:

void IOX_pinMode (uint8_t pin, uint8_t state) {
  uint32_t initTime = micros();
  uint8_t CMD [SIZE] = {0x12, (uint8_t)(pin & 0x0F), (uint8_t)state, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
  sendI2CCommand (CMD, (uint8_t) getAddr (pin));
  while (initTime + 1000 > micros());
}

I can now seamlessly control GPIOs like in the Arduino framework:

IOX_pinMode (A_PC0, OUTPUT);
IOX_digitalWrite (D_PC0, HIGH);

BP Part 6

Clém Clém added to the journal ago

CH32 cluster code Part 5 - I²C & digitalRead() buffer

Now that I had a working I²C code basis, I built bit by bit a working communication protocol between the ESP32 and CH32s, and started adding more and more features. I decided to use directly the driver/i2c.h library, instead of relying on the wire.h library. but I still created 2 functions to make things easier: sendI2CCommand (CMD) and retrieveI2CData ().

The ESP32 can send 2 types of command, sendI2CCommand (CMD), where the ESP32 asks for the CH32 to do something, and one retrieveI2CData (), where the ESP32 retrieves the data prepared beforehand by the CH32. For example, the digitalWrite function only requires to send an order, so it works like this:

uint8_t CMD [SIZE] = {0x10, pin, state, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
sendI2CCommand (CMD);

where 0x10 is the name of the command (digitalWrite()), with the pin and state being parameters. I filled the remaining bytes with 0xFF since I decided that all I²C transmissions will consist of 8 bytes, which should be enough for most commands.

So I implemented the IOX_pinMode(), IOX_analogWritePWM(), IOX_digitalWrite() functions this way, using sendI2CCommand ()

For the digitalRead() function, I decided to go a little fancy with more than just a digital reading... I thought it would be cool for the ESP32 to read many digital readings at once (the 32 last readings in time) in one go instead of sending an I²C command every time the ESP32 needed the state of one pin. So I did something different that just the 'ESP32: Send me the pin state on pin XX -> CH32: here it is: X', and implemented the following:
If the ESP32 wants the pin XX to be read every X ms, he first sends a command using the function: IOX_digitalReadSample ()

void IOX_digitalReadSample (uint8_t pin, uint16_t interval, uint8_t multiplier) {
  uint8_t CMD [SIZE] = {0x13, pin, (interval >> 8) & 0xFF, interval & 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
  sendI2CCommand (CMD);
}

where the pin and reading interval (ms) can be chosen.
Then when the ESP32 wants the 32 last readings (so each reading is spaced in time by interval ms), the ESP32 can call IOX_digitalReadBuffer ():

uint32_t IOX_digitalReadBuffer (uint8_t pin) {
  uint32_t initTime = micros();
  uint8_t CMD [SIZE] = {0x14, pin, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
  sendI2CCommand (CMD);
  while (initTime + 500 > micros());
  retrieveI2CData();
  uint32_t reading = (uint32_t)RxData[2] << 24 | (uint32_t)RxData[3] << 16 | (uint32_t)RxData[4] << 8 |  (uint32_t)RxData[5];
  return reading;
}

With this setup, it's the CH32 that does all the readings by itself, and only send the last 32 readings when the ESP32 needs them, all in one go. I'll do a similar approach for the ADC, this way I could add some post processing (like averaging the reading) before sending it to the ESP32.

So the I²C communication for retrieving data is implemented this way:
Whenever the ESP32 wants to get some data back, it first sends a command to the CH32 indicating that he need to add the required data in the TxData buffer, ready to be retrieved by the ESP32 with the function retrieveI2CData();. It's to ensure the CH32 has enough time to update the TxData buffer that I add a small delay before doing retrieveI2CData(); (plus it prevent the bus from being overloaded)

On the CH32 side, if the IOX_digitalReadSample() has been received, the CH32 sample the desired pin constantly, every interval ms. For that I used a structure with all the possible GPIOs (13), with multiple variables (PINSTATE, PREV_MS, ..., INTERVAL...)

typedef struct {
    uint32_t PINSTATE;
    uint32_t PREV_MS;
    uint16_t INTERVAL;
    uint8_t ENABLE;
} digitalReadPinState;

digitalReadPinState digitalSampling[] = {
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0}
};

So... for the CH32 to digitalRead() each pin at a precise rate (set by the INTERVAL value), I needed to check the time in the main () loop, so everyone would have use millis() (or micros()) in the Arduino IDE... except that the CH32 HAL didn't have any millis() or similar function... thankfully it wasn't too hard to implement a similar behavior, with the Systick timer. (hopefully I found a code online that did almost exactly what I wanted!). With too main functions being:
uint32_t ms = 0;

void SysTick_init (u32 counter) {
    NVIC_EnableIRQ (SysTicK_IRQn);
    SysTick->SR &= ~(1 << 0);
    SysTick->CMP = (counter - 1);
    SysTick->CNT = 0;
    SysTick->CTLR = 0x000F;
}
void SysTick_Handler (void) {
    ms++;
    SysTick->SR = 0;
}

Hum... ok I won't descibe everything in detail, since it's start being a lot with the 500+ lines on the CH32 code, but in a nutshell, the main loop consists of handleI2C(); and digitalRead_sample();, where digitalRead_sample(); sample the GPIOs states if the it's enabled for the pin X:

void digitalRead_sample() {
    for (uint8_t p = 0; p <= 13; p++) {
        if (digitalSampling[p].ENABLE == 1 && (ms - digitalSampling[p].PREV_MS)>= digitalSampling[p].INTERVAL) {
            digitalSampling[p].PREV_MS += digitalSampling[p].INTERVAL;
            uint8_t newBit = IOXdigitalRead (p);
            digitalSampling[p].PINSTATE = digitalSampling[p].PINSTATE << 1;
            digitalSampling[p].PINSTATE = digitalSampling[p].PINSTATE | newBit;
            IOXdigitalWrite (2, toggle);
            toggle = !toggle;
        }
    }
}

you'll notice that I tested the 'pace' of my function toggling the built in LED (on PC0 aka 2 according to my pinMap[] structure), making sure it worked correctly, and I indeed got a sampling frequency of 1 kHz when I set the interval to 1ms (IOX_digitalReadSample (6, 1);)
(I read 500Hz on my multimeter, and the LED is toggled everytime the function is entered)

Here is what it looks like when you print the PINSTATE uint32_t retrieved by the ESP32 at each IOX_digitalReadBuffer(): when I press / release the button on PC6, the state of the pin change from 0 to 1 or from 1 to 0, and the new value (measured by the CH32) is added in the 32 bit queue, thus getting rid of the 32nd oldest reading. This is why the 1 and 0 seem to move to the left at each new print on the serial monitor:

BP Part 5

Whenever a packet is received over the I²C, the packets are saved in the RxData[] array, then at the end of the transaction, buffered in RxDataBuffer[]. the flag cmd_ready is set to 1, meaning that the command is ready to be analyzed by the function handleI2C();, which determines what the CH32 has to do depending on the command and parameters (with switch () case: syntaxes)

Another interesting thing to note, is that as I'm limited with packets of 1 byte transiting on the I2C bus, variables that are longer than that (uint16_t or uint32_t) are split in smaller packets, then reconstructed on the other side, with for example the variable freqOUT or duty_cyclefor the PWM:

void IOX_analogWritePWM (uint8_t pin, uint16_t duty_cycle, uint32_t freqOUT) {
  uint32_t initTime = micros();
  uint8_t CMD [SIZE] = {0x11, pin, (duty_cycle >> 8) & 0xFF, duty_cycle & 0xFF, (freqOUT >> 24) & 0xFF, (freqOUT >> 16) & 0xFF, (freqOUT >> 8) & 0xFF, freqOUT & 0xFF};
  sendI2CCommand (CMD);
  while (initTime + 1000 >= micros());
}

then on the CH32 side:

uint16_t duty_cycle = (uint16_t)RxDataBuffer[2] << 8 | (uint16_t)RxDataBuffer[3];
uint32_t freqOUT = (uint32_t)RxDataBuffer[4] << 24 | (uint32_t)RxDataBuffer[5] << 16 | (uint32_t)RxDataBuffer[6] << 8 | (uint32_t)RxDataBuffer[7];

It took me quite a while to get to this point, trying many things in C on the CH32 code (like for example pushing the CH32 to it's limit, implementing a micros variable like I did with millis, but it's not optimal with the interrupt of the Systick, since it would have been entered way too many times (1 millions times per seconds!) and it was a bit laggy, I couldn't get the digitalRead sampling to be at a higher frequency that 4.5KHz using micros instead of millis, probably because the micros interrupt was taking way too long.

Anyway I'm not explaining everything I've done in the firmware because I'm already talking too much, but in a nutshell (yeah, this time I stop digressing 😅), I implemented the functions IOX_pinMode(), IOX_analogWritePWM(), IOX_digitalWrite(), IOX_digitalReadSample() and IOX_digitalReadBuffer() over I²C, so I'm now able to fully control the GPIOs on one of the CH32 by simply coding the ESP32! You can't imagine how happy and relieved I was to see the first IOX_digitalWrite() working over I²C! After so much struggle getting the I²C to work! That was so fun seing an LED blynk-over-I²C 😂

Now, the last thing to do is to tackle the ADC reading on the CH32 side, and I'll then just have to make some HAL on the ESP32 side, cuz I'll have to do a few things for controlling all those GPIOs easily, without messing up which CH32 control which pin 😂

NOTE: the advancement of the code is in the Github repo, to avoid overloading the Blueprint journal

Clém Clém added to the journal ago

CH32 cluster code Part 4 - I²C

Ok... so... I decided to deal with the I2C, and it was definitely harder than expected... I couldn't really take inspiration from the curious scientist website, since he only use the CH32V003 as master (I needed it as a slave), so I tried to look online, searching for code that already existed to use the MCU as a slave, and... I spent two afternoon, (~8 hours 😭) going from searching resources online with little to no results at all, vibe coding it with chatGPT (which was so bad at it that it mixed everything up, adding lines of code in C for the STM family 😭), documenting myself on I2C, testing the communication with the ESP32, went back to searching online, vibecoding again using another approach, doing smaller tests in I2C between the ESP32 and an Arduino (which took me a while since I didn't even knew how to assign the SDA and SCL pins in the wire.h library (turned out I just had to add the pin number as parameters in the wire.begin(10, 8); line), I even thought at some point that my design was cooked since I used GPIO10 on the ESP32 (which is used by the flash, so I thought for a moment it would never work... [this is me from the future: Actually I made it working, but I don't know if it's reliable, but... whatever] ... This endless loop of FAIIIILURES helped me learning stuff about I2C... but didn't helped me build my code for this IOX-77 project... I thought multiple times that choosing I2C wasn't a good idea, that my IOX-77 V1 would never work and that I should design the V2 using SPI (easier to implement and faster...) but I couldn't give up, not now, after all these hours working on this project...

So I kept going, searching online again and again to a solution for my problem, and I stumbled upon a reddit talking about using the CH32 as a slave. To be honest I had already seen this thread multiple times, but they only mentioned (or it's what I thought at first) the CH32fun library, which didn't fitted my requirements, so... But they showed a link to the official code example made by WCH, and I was so happy to see that the example provided was exactly what I needed: It implemented the I2C both for a slave, and for a master! I finally had a working resources to build my custom code around it!!! without further ado, I tested the code with 2 of the CH32, one as slave and one as master, and (obviously) it worked just fine!. So I tried to replace the Master CH32 with the ESP32 as master (this time I vibecoded it with chatGPT, because I really wanted to see if it would work, and as I gave him the ressources (CH32 I2C example code), it was easier for him to generate a working code for the ESP32 (using wire.h) and he also created a near bare metal code without using the wire.h library, and they both worked just fine! ... or at least they ended up working just fine 😅 because obviously the drop in replacement code didn't worked the first time... It took me a while for me to figure out (or let's say for ChatGPT to figure out) that there were an address mismatch, because the CH32 shifts addresses internally (for like no reason at all 😭), so the address 0x02 isn't the same on the ESP32 code as on the CH32 code... So for it to work I just replaced the 0x02 by 0x04 on the CH32 code, (shifting it to the left), and IT WORKED!!! ... (I mean I haven't looked at it enough, maybe some transmissions ends up generating errors, but at least I was able to get some data to show up in the Serial Monitor!!!!) So now that I'm sure it's possible to get a working I2C com between these 2 MCUs, I'll look deeper into the code to fully understand it, and modify it for my needs. I'm so relieved the I2C is finally working!!!!

Yeah, so here are some previews of the code I used:

Code for the ESP32 (using wire.h):
code I2C working ESP32

Code for the ESP32 (near bare metal):
code I2C working ESP32 bare

Extract of the code for the CH32 (slightly modified demo from WCH (I added checkpoints via UART for debugging)):
code I2C working CH32

I think I'll build the code around the near bare metal code for the ESP32, because it's way more explicit that the wire.h library, which does everything by itself so everything is hidden from my comprehension. So I'll try to build a reliable communication in I2C using these ressources, and hopefully I'll get something working perfectly!

(note: I'm not taking in account the 8 hours I lost searching-online/vibecoding/debugging/pulling-my-hairs-out/going-back-to-online-search/ending-up-vibecoding-again..., because it didn't gave me any results... I really started doing useful things once I had found the miraculous demo example from WCH)

Clém Clém added to the journal ago

CH32 cluster code Part 3 - PWM

I then started dealing with PWM. It was a bit more complicated than digitalWrite() or digitalRead() functions, as it needs Timers, but following The Curious Scientist code, I modified it so that I can choose on which PIN the PWM is outputted, and instead of taking as parameters PRSC, ARR and CCR, it directly calculates these values from the desired output frequency and the duty cycle.

Extract of the PWM course by The Curious Scientist:
https://curiousscientist.tech/blog/ch32v003f4p6-timers-and-pwm?rq=PWM

Curious Scientist PWM website

PWM how it works

And here is the snippet for handling PWM (just after the digitalWrite() function):

CH32 p3 First functions CODE (3) PWM

So after checking that the desired PIN is PWM capable, it calculates the values for the Timer (PRSC (prescaler), CCR, and ARR) using the given formulas, and sets up the PWM while differentiating the actions depending on the pins (on which Timer (1 or 2) and on which channel (1 - 4) is this pin?) but basically do the exact same thing as explained in the guide by The Curious Scientist (huge shout out to him, by the way, his guides are priceless!)

And the main program, with the LEDs fading in and out:

CH32 p3 First functions CODE (3) PWM main

So after tweaking a few things to make it work perfectly (I first forgot to differentiate the channel depending on the pin, so it didn't worked on all pins), I got something that seems to work just fine: I haven't looked at the outputted signal (I don't have any oscilloscope), so I don't know if the frequency/duty cycle is correct, but at least fading the LED works well.

So overall I'm really happy with this function, because it still make the main program way more readable, with a similar HAL that in the Arduino environment (one line for controling PWM), but it's also way more powerful: a 4 times higher precision (0 - 1023) is achievable on this faster MCU, while even being able to control the outputted frequency.

EDIT: Actually I just remembered that my multimeter could measure frequency and duty cycle (tbh I've never used this feature before), and after checking on the pins PC0 (the LED of each CH32), it was indeed around 1KHz (it displayed smth like 994Hz, so I consider this result as a Pass) For the duty cycle, the results were unreadeable, which is obvious since the LED are fading so constantly changing their duty cycle, so I think I'll just make another small code to ensure it works well.

Clém Clém added to the journal ago

CH32 cluster code Part 2 - digitalRead()

The next function to implement was digitalRead(). And while the function itself was pretty straightforward, it showed me a problem that took me a while to debug...

So here is the function, and to test whether it worked or not, I made a quick test with a button (the LED lights up when the button is pressed). the first GPIO that I found accessible was chip #D, PA1, one of the 4 CH32 pins broken out on the classical 2.54 headers, so I quickly uploaded the test.

CH32 p3 First functions CODE (2)

And... when I uploaded the script, quickly after hooking up a button to the right pin, it... didn't worked... So obviously I first thought that there were an error in the software, that I missed something or did something incorrectly, but after triple checking the (simple) code, and letting AI having a look at my work, everything seemed just fine... So I tried with PA2, the pin just next to it, and same problem... what could have been wrong? I tried to see if the pin was faulty, by setting it to OUTPUT, and blinking it, like with the LED on PC0, and instead of getting 0V then 3.3V alterning, I got a poor lil 0.57V 😭. Maybe the PORT A was fried on this IC? no problem, I've got another 15 ready for testing 🤣 but still 0.57V continuous... I even ended up thinking for a brief moment that my multimeter was just hallucinating 🤣! But obviously it wasn't wrong since I tried other ports too, and it was working on PC7... I checked the PCB layout, to ensure it was well connected, that the .5V wasn't induced by nearby traces, and at the moment I started running out of ideas, I looked at the CH32 pinout, and realized that they could be used as an alternate function: OSC, 2 pins for an external oscillator... maybe that was the problem?

So I was like 'hey, chatGPT, do you think this problem could be related to the alternate function of the pins PA1 and PA2?' and it went '✅ you nailed it, that's very likely the cause'. It meant that the IC was configured by default to accept an external oscillator, which made PA1 and PA2 unusable. So instead of going through all the lines of code in all the .c files associated with the CH32V003 IDE, whithout really knowing what could cause the problem, I gave the codes to Chat GPT and he found pretty quickly in the system_ch32v00x.c file, that the uncommented line was #define SYSCLK_FREQ_48MHz_HSE 48000000, so a 48MHz external oscillator, instead of the #define SYSCLK_FREQ_48MHZ_HSI 48000000 I needed. Ok, to be fair, I could have found it myself, it was actually self explainatory 😅 (even neatly explained with comments), but yeah, it's definitely faster when AI does the job, and I didn't wanted to read 700+ lines while the IOX-77 was stuck at the very beginning of the development phase...

CH32 p3 First functions CODE (3)

With that being solved, the test code for the digitalRead() was working just fine, and even though it seems like a very small victory, I'm really happy it finally works... You definitely experience a one of a kind feeling once you've solved a Hardware/software problem, after debugging it for way to long 😅

Clém Clém added to the journal ago

CH32 cluster code Part 1 (out of many...) - basic GPIO

I started the code for one of the CH32, starting with the GPIO basis.
Each CH32 will be communicating with the ESP32 via I²C, giving orders like 'set PA2 as Pull up' or 'write HIGH on PB2'... so on the ESP32 side, I'll have every major GPIO related functions (pinMode(), digitalWrite(), analogRead() and so on) customized for the IOX-77, each of these functions will send basic commands via I2C, and then the CH32 executes it. I'll thus need a 'custom' pinMode(), digitalWrite(), etc. on the CH32 side, this is why I'm starting with the custom pinMode() function.

I really want to build the firmware myself, without vibecoding it, but as the documentation is very minimal (only a few guides explains how the CH32V003 environment works), ChatGPT is helping me to learn how to make it work, especially understand all the GPIO_InitStructure... and other weird looking lines like RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE) and all the other things I'm not used to. Then once I think I understand what each functions does and how it behaves, I try rewriting it on Mounriver Studio, tweaking it a bit so that it does exactly what I need, before asking ChatGPT to correct me if something won't work or if there is any syntax error. I already know that with all the hours I'll have to spend on coding the software from the ground up, I'll learn a lot of things along the way!

So for the custom pinMode function, I needed a function with 2 parameters, the pin name, and the mode (OUTPUT PUSH PULL, OUTPUT OPEN DRAIN, ANALOG INPUT, and so on...). For the pin name, the CH32 syntax needs the PORT and PIN NUMBERseparately, so I added a pinMap which links a number (the pin name) to two variables, the PORT and the PIN NUMBER.

I also created the custom pinMode() function and a custom digitalWrite() function.
So now with these two functions, I should still be able to make the LED blink, but with a lighter (visually) code. And... yeah it still works just fine. Perfect!

Here is what this snippet looks like:

CH32 p3 First functions CODE

Testing the other boards...

I also tested the 2 other boards this weekend, and the 10 CH32V003 blinked perfectly. It's also interesting to notice, like bitluni did before me, that the clock of each CH32 becames out of sync pretty fast, and the '1000s' blink becames a mesmerizing random light ballet. that's actually kinda fun!

CH32 p2 BP

Clém Clém added to the journal ago

Let's start with the CH32 cluster!

CH32 p1 BP

So, I started learning how to program those cheap Risc-V MCUs, and it actually seems fairly complicated... Here is what the 'blink' program looks like... nothing to do with the Arduino Framework...

CH32 p1 MNRS CODE

Actually, I think I'll enjoy programming those cuz I feel like I'm more professional that with Arduino... I feel like I'm doing bare metal programming (which actually isn't), but I mean I'm way more into hardware, not just 'digitalWriting' and 'pinModeing' stuff 😉...
Learning how to code them properly will be quite time consuming, so I don't know if I'll have enough background before Blueprint ends to have a fully working IOX-77, because I don't want to go straight to the fairly complicated I2C program, but instead learning progressively how to code those chips.

This is why I started with the blink program, which is not less than 33 lines 😓

Anyway, I don't have the 3 boards with me, only one, so I tested it with an FPC adapter and jumper wires, but only 3 ICs uploaded, and looking more carefully, the other 2 IC's SWIO pins are not well soldered, so it's not responding. Or they are just fried, but it's less likely I think... I'll try to resolder them properly one day, but I'll also try the other 2 boards to see if it's better.

I also uploaded a basic sketch on the ESP32 to reset all the chips at boot up, and hold their NRST pins HIGH once it's fully initialized:

CH32 p1 ESP32 CODE

Now that I've discovered the Mounriver Studio IDE, I'll progressively learn how to code them, thanks to the few really well documented websites (like the Curious Scientis. This article is for getting started with GPIOs: https://curiousscientist.tech/blog/ch32v003f4p6-setup-and-gpios?rq=CH32)

Clém Clém added to the journal ago

Building the devboard... and encountering the first bug...

This timeline isn't exact, because in reality I assembled 2 boards soon after I received the components and PCBs. It took me about 1 and an half hour to get them assembled, covering the pads with solder thanks to the stencil (what a time saver, considering that for all my previous projects I dispensed solder paste on each pad by hand... relaxing, but it's time consuming and more prone to bridges...)

I then placed all the components and reflow soldered the 2 boards. The first one wasn't looking great, since the GND and 3V3 were shorted (turns out it was the ESP soldering that didn't went well)... which wasn't the case on the second board. So I plugged it onto the computer, rushing to upload my first 'blank' code onto the ESP32... which was horrific since the board wasn't even recognized by the PC... I was so disappointed that it didn't work the first time, considering that I was using a prebuilt ESP32 C3 module, not the bare chip! what could have gone wrong! After the first moment of deception, I tried to figure out what wasn't working: I checked all the PWR rails, 4.90V on the 5V rail: perfect considering the voltage drop caused by the diode, and 3.28V on the 3V3 rail... Ok the board was powered correctly. I remembered that to programm ESP32, you need to execute a serie of button presses (Hold BOOT, plug the board into the computer, press and release RESET, release BOOT), so I tried multiple times, without succeeding... It was the end of the Weekend, so I told myself: You know what? screw it: I'll deal with this during the holidays...

So a few weeks later, I got back into it: I soldered another board, with only the required parts (ESP32 + USB + LDO), maybe the problem came from the CH32s or other mistake on the board... I tried a few more things, checking the schematic, D+ and D- routing, and eventually asked on slack, on #electronics, and huge shout out to @grimsteel for finding the error, and thanks to @Madhav too for his precious help.
I completely messed up the wiring of the BOOT and ENABLE (RST) buttons: I connected the pairs the wrong way, resulting in 2 wires instead of 2 buttons... No wonder the ESP32 couldn't upload, if it was held in Reset forever!

So I desoldered the two buttons on each board, and just tilted them to 60°, to realign the pairs correctly. Actually, I like them this way 😂! unfortunately, the ESP32 on the 'half soldered' board was recognized but failed to upload, stuck at 'connecting...' maybe I fried it with the numerous reflow rework (all the pads underneath the ESP32 module are everything but maker friendly...),
However, I tried on the 2 other boards, and they were uploading perfectly. I was so happy it finally worked 🎉! I soldered a third board, which uploaded well too, so I was able to get 3 fully assembled and working IOX-77 boards out of 4 expected: 75% success rate, not that bad honestly 😂😅! ... at least for the ESP32 part... Haven't tried yet the main feature of this devboard: the CH32 cluster...

BUILD IMG (1) BLP

I desoldered the two buttons on each board, and just tilted them to 60°

corrected sch btns Blueprint lighter 2

I connected the pairs the wrong way, resulting in 2 wires instead of 2 buttons...

I uploaded a basic Arduino test code, to see if I could toggle a GPIO (IO0), and whether the serial communication worked, and it worked just fine on the 3 boards.
Now, the last thing to do is to figure out how to programm the CH32V003, and make them behave as smart GPIO expanders with the ESP32...

Until I advance a bit more on it, here are a few pictures of these 3 IOX-77 superboards:

Gallery:

BUILD IMG (3) BLP

BUILD IMG (5)

BUILD IMG (4)

BUILD IMG (6)

BUILD IMG (2)

BUILD IMG (8) BLP

Clém Clém added to the journal ago

we've got the PCBs 🎉

I received the PCBs, and they look so cool!
To be honest, I though they would be a bit bigger, the pcb always looks big on the screen when designing it, but I guess having a small board footprint is always a good news 😊

IMG_20260110_163144641

IMG_20260110_170845968_HDR

Here is a size comparison with the Raspberry pi, so that you've got an idea on how small it is with it's 76 GPIOs :

IMG_20260110_163551588~2

Hope they aren't any electrical mistake, 'cause the inner layers are kind of inaccessibles 🤣 but I guess we'll find out this WE when the PCB will be soldered...

Clém Clém added to the journal ago

Ordered the components

I've already ordered the PCB and stencil a while ago, and now I placed the LCSC order, with the electronical components. I'm combining multiple LCSC order, so I don't have to pay multiple times shipping cost and handling fees, so I had to make a top-up form, pay for the additionnal cost in a donation to HCB (15 bucks of other components, for other projects or connectors to make custom shields for the IOX-77 devboard), and I've now placed the LCSC order which adds up to $57.28 ($40.29 worth of components for the IOX-77 project, and $15.49 in additional component).

Can't wait for the components to arrive! 😁

Here are the parts required for the IOX-77 project:

IOX-77 Parts ONLY 1

IOX-77 Parts ONLY 2

IOX-77 Parts ONLY 3

IOX-77 Parts ONLY 4

IOX-77 Parts ONLY 5

And here the additional parts (for other projects)

ADDITIONAL Parts ONLY 1

ADDITIONAL Parts ONLY 2

ADDITIONAL Parts ONLY 3

So in total:

Order combined price

006

Iamalive Iamalive 🚀 approved IOX-77 - An ESP32 based SuperBoard (75+ GPIOs) ago

Tier approved: 3

Grant approved: $63.00

Looks good!

Clém Clém added to the journal ago

Added the pinout

IOX-77 PINOUT

There are so many pins on this devboard! Thus, I created a PDF with the pinout neatly explained. (the PDF also available on the Github Repo)

Pinout 1-3

Pinout 2-3

Pinout 3-3

IOX-77 PINOUT

Clém Clém submitted IOX-77 - An ESP32 based SuperBoard (75+ GPIOs) for ship review ago

Iamalive Iamalive 🚀 requested changes for IOX-77 - An ESP32 based SuperBoard (75+ GPIOs) ago

It seems like your project description is partially made by AI, which we do not tolerate. Please fix this!

Clém Clém submitted IOX-77 - An ESP32 based SuperBoard (75+ GPIOs) for ship review ago

Clém Clém added to the journal ago

Preparing orders

IOX_77_RENDER_01_CG_08

As I said earlier, I've already made a few PCBs using SMD components, so I'm used to solder QFN20 and other SMD packages. I'll order the components on LCSC, and the PCBs/Stencil on JLCPCB. It's cheaper than choosing PCBA option, and let's be honest, it's way funnier to build your PCB, once you've spent so many hours designing it on a screen😉! In addition to that, I'll need to order a SWIO USB programmer, to programm the CH32V003 (we can find them for pretty cheap on aliexpress).

Here is the BOM for the electronic components: (one PCB)

BOM PYCHARM

And here are the additional parts (and other hardware):

BOM Hardware PYCHARM

Here is the JLCPCB order: 5 PCBs and a stencil. Thanks to the small size of the stencil (I chose 100mm by 100mm), the cost is as low as 3$! Making PCBs has became so cheap these days! 🤗

JLCPCB ORDER SCREENSHOT

When ordering components on LCSC, an important part of the cost comes from shipping (even though, it can be reduced to 9 bucks with Global Standard Direct Line). Moreover, the major part of the components are ordered in multiples of 10 or even 100 (0402 res and other). So at the end the only components that I can really choose the quantity are the ESP32, the two 1.27 headers and the five CH32V003 (all the other have a MOQ of at least 5pcs, or more). It means that while building one PCB cost you around 25 bucks in components, building 2 PCBs will only cost you like 5 bucks more...

So I though about it and made some calculations, and I think that building 4 PCBs is the sweet spot: I'm using 4 of the 5 PCBs, the total cost is still relatively low, and most importantly, the per board cost is way lower.

Here is the simulation:

PRICE SIMULATION

The total cost takes in account the JLCPCB, Aliexpress and LCSC orders, with the components needed to build the board. However, as I used uncommon FPCs and 1.27mm pitched headers to give my devboard a smaller footprint, I also need to buy a few extra components (male headers, FPC cables, and FPC connectors) in order to implement them on custom Shields that I'll design later on.

Here is the LCSC order (IOX-77 devboard components + extra connectors for the upcoming shields):

LCSC CART1

LCSC CART2

LCSC CART3

LCSC CART4

LCSC CART5

LCSC CART PRICE

So the total cost for 4 PCBs is ... less than 60 $ 🥳

GLOBAL BILL IOX77

(Small update: while checking whether everything was ok, I noticed that I completely forgot to add to the cart 0402 Resistors for the Ideal Diode Controller. I need 2 Res, with values varying from 50k to 2M (50K, 100K, 200K, 500K, 1M, 2M), this way I can tune the reverse current blocking speed vs continuous current consumption ratio.
Not a big deal, the cost increase by a itty bitty 30 cents 😅 )

Clém Clém added to the journal ago

Made some cool renders on Fusion 360

All the previous images were taken from Easy EDA viewing tool.
To finish in style, I decided to make a few higher quality renders.

Here are some renders of the IOX-77 made with Fusion 360:
IOX_77_RENDER_01_CG_08

IOX77_RENDER_02_CG

IOX_77_RENDER_CG_03

IOX_77_RENDER_CG_05

IOX_77_RENDER_CG_06

IOX_77_RENDER_01_CG_9

Clém Clém added to the journal ago

Cleaning a bit the silkscreen

I cleaned a bit the silkscreen layer by removing all original silkscreens around the components, and the board looks way better now. I also added custom silkscreen to highlight the GND and +3.3V pins, and a custom logo for my IOS-77 board. (Yeah, I changed the inital name because I thought IOX-77 was cooler, and yes, the devboard actually has 75 GPIOs if we only count the usable GPIOs, but hey, come on! 77 is a way cooler number 😅)

In the meantime, I also changed a few things on the PCB layout to make sure everything runs smoothly (I tried to remove all "bottlenecks" in the power planes, so that all power path are wide enough.)

I really like how this board turned out, and I can't wait to assemble the PCBs and see if it even works.

As it's not my first PCB project, I'm used to solder QFN-20 packages and 0603, but I leveled up the game with 0402 ones this time. I even have two ESD diodes in a SOD882 package (1mm by 0.6mm... I don’t even know if I'll be able to solder them... well, we have to try. And if I can't, all this work will be useless... No seriously, with a good stencil and precise hands, it's fine: the surface tension does all the job!

BOARD PCB 9 1

BOARD PCB 9 2

So here are the 4 Copper Layers of the PCB design:

Top Copper Layer:
BOARD PCB 10 1
Inner Layer 1:
BOARD PCB 10 2

Inner Layer 2:
BOARD PCB 10 3

Bottom Copper Layer:
BOARD PCB 10 4

Clém Clém added to the journal ago

Designing the PCB Part 6: Everything is connected 🥳

The PCB is finally done! Hurrah! 🥳

So, it took me quite a while to make sure everything was connected (the ratline layer should be empty then), and for the last 20 or so traces, I needed to move other traces to free up some space... Everything is quite tightly packed... But at the end of the day, I've got a neat and well designed PCB.

I added a few extra components (a TVS diode and decoupling cap near the ESP32-C3) and I changed the FPC connector on the right: It wasn't the same ref as the other ones, so it looked awkward. Now they are all the same so the board is more consistent.

I've designed many PCBs for my electronics project over the last 3 years (2L PCBs), but I have to admit that the routing on this project was harder than on the previous ones. In total, there are 71 components, 490 pads, 112 nets, and ... wait, what did I just read ? ... 289 vias! No wonder if the routing wasn't easy with all these vias on the way 😂! Fun fact: the total length of all the traces is more than 4 meters!

Have a look at this overkill devboard:

BOARD PCB 8 2

BOARD PCB 8 3

BOARD PCB 8 4

BOARD PCB 8 1

So, I'm really happy with how this turned out, and the last thing to do before submitting is arranging the silkscreen layer a bit, so the board is neat and pleasant to look at.

Clém Clém added to the journal ago

Designing the PCB Part 5: Routing progress 90%

I've spent another 2 and a half hours adding and routing the remaining components of the circuit (Power Management, USB C passives...).

While doing this, I noticed an error in the schematic: The VBUS coming out of the USB C wasn't connected to anything, so I replaced it with a +5V tag, as it should have been.

I even added two GND and +3.3V planes on the top and bottom layers, but I still need to add some prohibited regions because the initial plane created by Easy EDA isn't perfect (it is trying to seep through every single part of the circuit, while it shouldn't)

Here’s a preview:

BOARD PCB 7 1

BOARD PCB 7 2

Clém Clém added to the journal ago

Designing the PCB Part 4: GPIOs Routing 100% completed 🥳

Hurray! All the GPIOs are now connected to the 5 MCUs! 🥳
It's taking shape slowly but surely. Actually, I think the final board will look amazing, with all those little 8mil traces running everywhere on the 4 Layer PCB (yeah, I know, the inner layer won't be visible, but still, they are on the design file 😊)

BOARD PCB 06 2

BOARD PCB 06 1

BOARD PCB 06 3

BOARD PCB 06 4

Clém Clém added to the journal ago

Designing the PCB Part 3: Still adding GPIO traces

So, it's moving forward! I rerouted everything, linking the two left MCUs to both the FPC and headers. It was a quite long and tedious task, but still it was fun and I didn't even notice the time passing...

As I said, I had to make some modifications in the schematic to ensure that the traces align well with the header pads without having too much crossing. It was sometimes quite mind-boggling, but I eventually found out how to route everything.

Next time it should be easier, because I don't need to bother connecting the FPC, as the other MCUs only have their GPIOs connected to the 2 headers.BOARD PCB 04 1

Clém Clém added to the journal ago

Designing the PCB Part 3: I'm changing my mind

So... Hum... Yeah:

BOARD PCB 03 1

After another hour of routing the PCB, I definitely know it won't make it: the connections Headers-MCUs are just not efficient at all: when one pin of an MCU goes on the left on the top header, the next pin goes to the opposite side! So obviously, it makes routing way more difficult because the traces need to cross each other... So I'm adopting a different approach: I'll route every MCU pin to the closest Header pin, then I'll change the nets on the schematic according to this disposition, and hopefully I'll get better results.

Let's try it!

Clém Clém added to the journal ago

Designing the PCB Part 2

I added the 5 CH32 and their passive components. I started routing the GPIOs to the FPC/Headers, and I know it's going to be pretty difficult to fit all these traces even on a 4 layers board...

Here is the progression:

BOARD PCB 02 3

BOARD PCB 02 1

BOARD PCB 02 2

Clém Clém added to the journal ago

Designing the PCB Part 1 : Overall shape

I started the PCB by creating the board outline and by placing the major components (ESP32, GPIO interfaces...). I tried to have something practical (placing the FPCs, USB... on the sides of the board) while still being visually pleasant, and I think that this first sketch is pretty cool. We'll see in the future if we need more space for all the traces, but for the moment, I think it's not that bad! Have a look:

BOARD PCB 01
BOARD PCB 01 2

Clém Clém added to the journal ago

Schematic Part 4: Power Management

The last thing to add in our schematic is the Power Management.

The ESP32 operates at 3.3V, so the CH32 run at 3.3V too. There are 3 ways of powering the board:

(A) - By the USB-C at 5V
(B) - By a dedicated port between 3.5V and 5.5V (but the "+5V" rail will be at this voltage, so if it's not exactly 5V, we have to be careful about what we connect to it)
(C) - By another dedicated port with a steady 3.3V (directly fed into the +3.3V rail).

For the options (A) and (B), there is a 3.3V LDO voltage regulator (TLV75733PDBVR) who provides a steady 3.3V output. I chose this one because it had a small quiescent current (25µA). However, this IC wasn't protected against reverse current: If I power the board directly with 3.3V (option (C)), the output of the LDO would be at 3.3V, while the IN of the LDO would still be at 0V (no input connected), and this scenario could damage the LDO. So it took me a while to figure out what to do, but I ended up using an Ideal Diode Controller (the DZDH0401DW) along with a ultra-low RDSon P-channel MOSFET(the SI2393DS). This way, current can only flow in one direction, from the LDO to the +3.3V rail, without inducing an important voltage drop (it would have been the case with a Schottky diode)

So here are the schematics for the LDO and the Ideal Diode Controller:

ESPIO schematic 04 Pwr Mng 2png

ESPIO schematic 04 Pwr Mng 1

And the whole schematic :

ESPIO schematic 04 Whole page

So, I think the Schematic is good to go, next time I'll start the best part of this project: Routing! 🤗

Clém Clém added to the journal ago

Schematic Part 3: Accessing the IOs

Now that the 5 CH32 offer their 70 GPIOs, it's time to arrange them on the devboard. I've decided to use multiple access points :

  • A 40 Pins .5mm FPC connector gives access to 32 of the GPIOs.
  • Two 2x34 small pitch female Dupont Headers (1.27mm pitch, instead of the typical 2.54mm ones), with all 70 GPIOs, the remaining ESP32's IOs, I2C lines, multiple 3.3V and GND pins, and even a +5V pin (the +5V is accessible when the devboard is powered via the USB)
  • a more user-friendly 2.54mm 2x8 header, with the ESP32 IOs, I2C lines, Power pins and 4 CH32 (E) IOs (including two 5V tolerant pins)
  • A 6P .5mm FPC with I2C and UART (from the ESP32)
  • Lastly, another .5mm FPC (8P) with all SWIO pins, which will be used for programming.

I think that with all of that, adding sensors, shields, modules, etc, will be very neat and easy.

Here are some screenshots:

The 2.54mm header, and two 6P and 8P FPCs:

ESPIO schematic 03 FPC1

The two 1.27mm header:

ESPIO schematic 03 FPC3

And the 40P FPC:
ESPIO schematic 03 FPC2

Browsing parts on LCSC and adding all the nets to the schematic was really fun, even if it was indeed a repetitive task. However, thinking about routing all this mess makes me a bit nervous... 😅 but hey, one step at a time!

And here is the updated progression of the schematic:

ESPIO schematic 03 Whole page

Now, the last thing to do on the schematic is handling the Power management.

Clém Clém added to the journal ago

Schematic Part 2: The CH32V003

I added 5 CH32v003F4U6 (QFN-20). They all share the same I2C bus with the Master (ESP32). I gave each CH32V003 an orange 0603 LED: it could be really useful for debugging...

Each CH32V003 has the ability to go in Standby Mode, where the current consumption is a tiny 10µA (so 50µA in total). The idea is that when we want to limit the overall current consumption, the ESP32 sends a signal to each CH32V003 Via I2C, ordering them to enter Standby Mode. Once the ESP32 needs them again, it can wake them up by resetting them: It's the ESP32 that commands the 5 NRST pins (Negative Resets pins). This way, if one of the CH32V003 isn't responding anymore, the ESP32 can force the reset, and hopefully, it's going to work again. It's also practical when we want to reset the entire devboard: The ESP32 can handle everything, no need to have one button for each CH32.

PC5 and PC6 are 5V tolerant, so I added the suffix "FT".

The CH32 can be programmed using the SWIO protocol, with a specialized programmer (the WCH-Link) I will later on add a header (or an FPC connector) to access all 5 SWIO pins, +3.3V, and GND for programming.

This is what one of the CH32 implementations looks like:

ESPIO schematic 02 Zoom CH32v003

And here is the progression of the schematic:

ESPIO schematic 02 CH32v003

Two hours is quite long for implementing 5 CH32v003, but I needed to do some research to ensure that the CH32V003 could act as I wanted it to, so I went through part of the CH32V003 datasheet, and checked multiple CH32V003 devboard schematics.

Clém Clém added to the journal ago

Started the Schematic: Basic ESP32 board

I started the schematic of the devboard by adding all the minimal components for the ESP32-C3-MINI-1-N4 module, like the USB C, a bunch of 0402 resistors, decoupling caps, ESD protection diodes, the boot and Chip Enable Buttons... During these first 2 hours, I also documented myself by looking at other ESP-C3-MINI devboards (like the Adafruit Rust) and the schematic example provided by ESPRESSIF.

I chose the ESP32-C3-MINI-1-N4 module, instead of the bare ESP32 QFN chip, because I preferred not to deal with the Antenna stuff, as the RF tuning is way too complicated for me, and without tuning, the devboard isn't optimized, and the RF range can be reduced.

Here is the first part of the schematic:

ESPIO schematic 01 ESP32 minimal requirements

Clém Clém started IOX-77 - An ESP32 based SuperBoard (75+ GPIOs) ago

11/29/2025 12 PM - Started the Schematic: Basic ESP32 board

I started the schematic of the devboard by adding all the minimal components for the ESP32-C3-MINI-1-N4 module, like the USB C, a bunch of 0402 resistors, decoupling caps, ESD protection diodes, the boot and Chip Enable Buttons... During these first 2 hours, I also documented myself by looking at other ESP-C3-MINI devboards (like the Adafruit Rust) and the schematic example provided by ESPRESSIF.

I chose the ESP32-C3-MINI-1-N4 module, instead of the bare ESP32 QFN chip, because I preferred not to deal with the Antenna stuff, as the RF tuning is way too complicated for me, and without tuning, the devboard isn't optimized, and the RF range can be reduced.

Here is the first part of the schematic:

ESPIO schematic 01 ESP32 minimal requirements

11/29/2025 5 PM - Schematic Part 2: The CH32V003

I added 5 CH32v003F4U6 (QFN-20). They all share the same I2C bus with the Master (ESP32). I gave each CH32V003 an orange 0603 LED: it could be really useful for debugging...

Each CH32V003 has the ability to go in Standby Mode, where the current consumption is a tiny 10µA (so 50µA in total). The idea is that when we want to limit the overall current consumption, the ESP32 sends a signal to each CH32V003 Via I2C, ordering them to enter Standby Mode. Once the ESP32 needs them again, it can wake them up by resetting them: It's the ESP32 that commands the 5 NRST pins (Negative Resets pins). This way, if one of the CH32V003 isn't responding anymore, the ESP32 can force the reset, and hopefully, it's going to work again. It's also practical when we want to reset the entire devboard: The ESP32 can handle everything, no need to have one button for each CH32.

PC5 and PC6 are 5V tolerant, so I added the suffix "FT".

The CH32 can be programmed using the SWIO protocol, with a specialized programmer (the WCH-Link) I will later on add a header (or an FPC connector) to access all 5 SWIO pins, +3.3V, and GND for programming.

This is what one of the CH32 implementations looks like:

ESPIO schematic 02 Zoom CH32v003

And here is the progression of the schematic:

ESPIO schematic 02 CH32v003

Two hours is quite long for implementing 5 CH32v003, but I needed to do some research to ensure that the CH32V003 could act as I wanted it to, so I went through part of the CH32V003 datasheet, and checked multiple CH32V003 devboard schematics.

11/30/2025 10 AM - Schematic Part 3: Accessing the IOs

Now that the 5 CH32 offer their 70 GPIOs, it's time to arrange them on the devboard. I've decided to use multiple access points :

  • A 40 Pins .5mm FPC connector gives access to 32 of the GPIOs.
  • Two 2x34 small pitch female Dupont Headers (1.27mm pitch, instead of the typical 2.54mm ones), with all 70 GPIOs, the remaining ESP32's IOs, I2C lines, multiple 3.3V and GND pins, and even a +5V pin (the +5V is accessible when the devboard is powered via the USB)
  • a more user-friendly 2.54mm 2x8 header, with the ESP32 IOs, I2C lines, Power pins and 4 CH32 (E) IOs (including two 5V tolerant pins)
  • A 6P .5mm FPC with I2C and UART (from the ESP32)
  • Lastly, another .5mm FPC (8P) with all SWIO pins, which will be used for programming.

I think that with all of that, adding sensors, shields, modules, etc, will be very neat and easy.

Here are some screenshots:

The 2.54mm header, and two 6P and 8P FPCs:

ESPIO schematic 03 FPC1

The two 1.27mm header:

ESPIO schematic 03 FPC3

And the 40P FPC:
ESPIO schematic 03 FPC2

Browsing parts on LCSC and adding all the nets to the schematic was really fun, even if it was indeed a repetitive task. However, thinking about routing all this mess makes me a bit nervous... 😅 but hey, one step at a time!

And here is the updated progression of the schematic:

ESPIO schematic 03 Whole page

Now, the last thing to do on the schematic is handling the Power management.

11/30/2025 3 PM - Schematic Part 4: Power Management

The last thing to add in our schematic is the Power Management.

The ESP32 operates at 3.3V, so the CH32 run at 3.3V too. There are 3 ways of powering the board:

(A) - By the USB-C at 5V
(B) - By a dedicated port between 3.5V and 5.5V (but the "+5V" rail will be at this voltage, so if it's not exactly 5V, we have to be careful about what we connect to it)
(C) - By another dedicated port with a steady 3.3V (directly fed into the +3.3V rail).

For the options (A) and (B), there is a 3.3V LDO voltage regulator (TLV75733PDBVR) who provides a steady 3.3V output. I chose this one because it had a small quiescent current (25µA). However, this IC wasn't protected against reverse current: If I power the board directly with 3.3V (option (C)), the output of the LDO would be at 3.3V, while the IN of the LDO would still be at 0V (no input connected), and this scenario could damage the LDO. So it took me a while to figure out what to do, but I ended up using an Ideal Diode Controller (the DZDH0401DW) along with a ultra-low RDSon P-channel MOSFET(the SI2393DS). This way, current can only flow in one direction, from the LDO to the +3.3V rail, without inducing an important voltage drop (it would have been the case with a Schottky diode)

So here are the schematics for the LDO and the Ideal Diode Controller:

ESPIO schematic 04 Pwr Mng 2png

ESPIO schematic 04 Pwr Mng 1

And the whole schematic :

ESPIO schematic 04 Whole page

So, I think the Schematic is good to go, next time I'll start the best part of this project: Routing! 🤗

11/30/2025 5 PM - Designing the PCB Part 1 : Overall shape

I started the PCB by creating the board outline and by placing the major components (ESP32, GPIO interfaces...). I tried to have something practical (placing the FPCs, USB... on the sides of the board) while still being visually pleasant, and I think that this first sketch is pretty cool. We'll see in the future if we need more space for all the traces, but for the moment, I think it's not that bad! Have a look:

BOARD PCB 01
BOARD PCB 01 2

11/30/2025 11 PM - Designing the PCB Part 2

I added the 5 CH32 and their passive components. I started routing the GPIOs to the FPC/Headers, and I know it's going to be pretty difficult to fit all these traces even on a 4 layers board...

Here is the progression:

BOARD PCB 02 3

BOARD PCB 02 1

BOARD PCB 02 2

12/2/2025 6 PM - Designing the PCB Part 3: I'm changing my mind

So... Hum... Yeah:

BOARD PCB 03 1

After another hour of routing the PCB, I definitely know it won't make it: the connections Headers-MCUs are just not efficient at all: when one pin of an MCU goes on the left on the top header, the next pin goes to the opposite side! So obviously, it makes routing way more difficult because the traces need to cross each other... So I'm adopting a different approach: I'll route every MCU pin to the closest Header pin, then I'll change the nets on the schematic according to this disposition, and hopefully I'll get better results.

Let's try it!

12/2/2025 10 PM - Designing the PCB Part 3: Still adding GPIO traces

So, it's moving forward! I rerouted everything, linking the two left MCUs to both the FPC and headers. It was a quite long and tedious task, but still it was fun and I didn't even notice the time passing...

As I said, I had to make some modifications in the schematic to ensure that the traces align well with the header pads without having too much crossing. It was sometimes quite mind-boggling, but I eventually found out how to route everything.

Next time it should be easier, because I don't need to bother connecting the FPC, as the other MCUs only have their GPIOs connected to the 2 headers.BOARD PCB 04 1

12/3/2025 - Designing the PCB Part 4: GPIOs Routing 100% completed 🥳

Hurray! All the GPIOs are now connected to the 5 MCUs! 🥳
It's taking shape slowly but surely. Actually, I think the final board will look amazing, with all those little 8mil traces running everywhere on the 4 Layer PCB (yeah, I know, the inner layer won't be visible, but still, they are on the design file 😊)

BOARD PCB 06 2

BOARD PCB 06 1

BOARD PCB 06 3

BOARD PCB 06 4

12/4/2025 - Designing the PCB Part 5: Routing progress 90%

I've spent another 2 and a half hours adding and routing the remaining components of the circuit (Power Management, USB C passives...).

While doing this, I noticed an error in the schematic: The VBUS coming out of the USB C wasn't connected to anything, so I replaced it with a +5V tag, as it should have been.

I even added two GND and +3.3V planes on the top and bottom layers, but I still need to add some prohibited regions because the initial plane created by Easy EDA isn't perfect (it is trying to seep through every single part of the circuit, while it shouldn't)

Here’s a preview:

BOARD PCB 7 1

BOARD PCB 7 2

12/5/2025 - Designing the PCB Part 6: Everything is connected 🥳

The PCB is finally done! Hurrah! 🥳

So, it took me quite a while to make sure everything was connected (the ratline layer should be empty then), and for the last 20 or so traces, I needed to move other traces to free up some space... Everything is quite tightly packed... But at the end of the day, I've got a neat and well designed PCB.

I added a few extra components (a TVS diode and decoupling cap near the ESP32-C3) and I changed the FPC connector on the right: It wasn't the same ref as the other ones, so it looked awkward. Now they are all the same so the board is more consistent.

I've designed many PCBs for my electronics project over the last 3 years (2L PCBs), but I have to admit that the routing on this project was harder than on the previous ones. In total, there are 71 components, 490 pads, 112 nets, and ... wait, what did I just read ? ... 289 vias! No wonder if the routing wasn't easy with all these vias on the way 😂! Fun fact: the total length of all the traces is more than 4 meters!

Have a look at this overkill devboard:

BOARD PCB 8 2

BOARD PCB 8 3

BOARD PCB 8 4

BOARD PCB 8 1

So, I'm really happy with how this turned out, and the last thing to do before submitting is arranging the silkscreen layer a bit, so the board is neat and pleasant to look at.

12/6/2025 - Cleaning a bit the silkscreen

I cleaned a bit the silkscreen layer by removing all original silkscreens around the components, and the board looks way better now. I also added custom silkscreen to highlight the GND and +3.3V pins, and a custom logo for my IOS-77 board. (Yeah, I changed the inital name because I thought IOX-77 was cooler, and yes, the devboard actually has 75 GPIOs if we only count the usable GPIOs, but hey, come on! 77 is a way cooler number 😅)

In the meantime, I also changed a few things on the PCB layout to make sure everything runs smoothly (I tried to remove all "bottlenecks" in the power planes, so that all power path are wide enough.)

I really like how this board turned out, and I can't wait to assemble the PCBs and see if it even works.

As it's not my first PCB project, I'm used to solder QFN-20 packages and 0603, but I leveled up the game with 0402 ones this time. I even have two ESD diodes in a SOD882 package (1mm by 0.6mm... I don’t even know if I'll be able to solder them... well, we have to try. And if I can't, all this work will be useless... No seriously, with a good stencil and precise hands, it's fine: the surface tension does all the job!

BOARD PCB 9 1

BOARD PCB 9 2

So here are the 4 Copper Layers of the PCB design:

Top Copper Layer:
BOARD PCB 10 1
Inner Layer 1:
BOARD PCB 10 2

Inner Layer 2:
BOARD PCB 10 3

Bottom Copper Layer:
BOARD PCB 10 4

12/7/2025 - Made some cool renders on Fusion 360

All the previous images were taken from Easy EDA viewing tool.
To finish in style, I decided to make a few higher quality renders.

Here are some renders of the IOX-77 made with Fusion 360:
IOX_77_RENDER_01_CG_08

IOX77_RENDER_02_CG

IOX_77_RENDER_CG_03

IOX_77_RENDER_CG_05

IOX_77_RENDER_CG_06

IOX_77_RENDER_01_CG_9

12/9/2025 - Preparing orders

IOX_77_RENDER_01_CG_08

As I said earlier, I've already made a few PCBs using SMD components, so I'm used to solder QFN20 and other SMD packages. I'll order the components on LCSC, and the PCBs/Stencil on JLCPCB. It's cheaper than choosing PCBA option, and let's be honest, it's way funnier to build your PCB, once you've spent so many hours designing it on a screen😉! In addition to that, I'll need to order a SWIO USB programmer, to programm the CH32V003 (we can find them for pretty cheap on aliexpress).

Here is the BOM for the electronic components: (one PCB)

BOM PYCHARM

And here are the additional parts (and other hardware):

BOM Hardware PYCHARM

Here is the JLCPCB order: 5 PCBs and a stencil. Thanks to the small size of the stencil (I chose 100mm by 100mm), the cost is as low as 3$! Making PCBs has became so cheap these days! 🤗

JLCPCB ORDER SCREENSHOT

When ordering components on LCSC, an important part of the cost comes from shipping (even though, it can be reduced to 9 bucks with Global Standard Direct Line). Moreover, the major part of the components are ordered in multiples of 10 or even 100 (0402 res and other). So at the end the only components that I can really choose the quantity are the ESP32, the two 1.27 headers and the five CH32V003 (all the other have a MOQ of at least 5pcs, or more). It means that while building one PCB cost you around 25 bucks in components, building 2 PCBs will only cost you like 5 bucks more...

So I though about it and made some calculations, and I think that building 4 PCBs is the sweet spot: I'm using 4 of the 5 PCBs, the total cost is still relatively low, and most importantly, the per board cost is way lower.

Here is the simulation:

PRICE SIMULATION

The total cost takes in account the JLCPCB, Aliexpress and LCSC orders, with the components needed to build the board. However, as I used uncommon FPCs and 1.27mm pitched headers to give my devboard a smaller footprint, I also need to buy a few extra components (male headers, FPC cables, and FPC connectors) in order to implement them on custom Shields that I'll design later on.

Here is the LCSC order (IOX-77 devboard components + extra connectors for the upcoming shields):

LCSC CART1

LCSC CART2

LCSC CART3

LCSC CART4

LCSC CART5

LCSC CART PRICE

So the total cost for 4 PCBs is ... less than 60 $ 🥳

GLOBAL BILL IOX77

(Small update: while checking whether everything was ok, I noticed that I completely forgot to add to the cart 0402 Resistors for the Ideal Diode Controller. I need 2 Res, with values varying from 50k to 2M (50K, 100K, 200K, 500K, 1M, 2M), this way I can tune the reverse current blocking speed vs continuous current consumption ratio.
Not a big deal, the cost increase by a itty bitty 30 cents 😅 )

12/13/2025 - Added the pinout

IOX-77 PINOUT

There are so many pins on this devboard! Thus, I created a PDF with the pinout neatly explained. (the PDF also available on the Github Repo)

Pinout 1-3

Pinout 2-3

Pinout 3-3

IOX-77 PINOUT

1/1/2026 - Ordered the components

I've already ordered the PCB and stencil a while ago, and now I placed the LCSC order, with the electronical components. I'm combining multiple LCSC order, so I don't have to pay multiple times shipping cost and handling fees, so I had to make a top-up form, pay for the additionnal cost in a donation to HCB (15 bucks of other components, for other projects or connectors to make custom shields for the IOX-77 devboard), and I've now placed the LCSC order which adds up to $57.28 ($40.29 worth of components for the IOX-77 project, and $15.49 in additional component).

Can't wait for the components to arrive! 😁

Here are the parts required for the IOX-77 project:

IOX-77 Parts ONLY 1

IOX-77 Parts ONLY 2

IOX-77 Parts ONLY 3

IOX-77 Parts ONLY 4

IOX-77 Parts ONLY 5

And here the additional parts (for other projects)

ADDITIONAL Parts ONLY 1

ADDITIONAL Parts ONLY 2

ADDITIONAL Parts ONLY 3

So in total:

Order combined price

006

1/14/2026 - we've got the PCBs 🎉

I received the PCBs, and they look so cool!
To be honest, I though they would be a bit bigger, the pcb always looks big on the screen when designing it, but I guess having a small board footprint is always a good news 😊

IMG_20260110_163144641

IMG_20260110_170845968_HDR

Here is a size comparison with the Raspberry pi, so that you've got an idea on how small it is with it's 76 GPIOs :

IMG_20260110_163551588~2

Hope they aren't any electrical mistake, 'cause the inner layers are kind of inaccessibles 🤣 but I guess we'll find out this WE when the PCB will be soldered...

2/17/2026 - Building the devboard... and encountering the first bug...

This timeline isn't exact, because in reality I assembled 2 boards soon after I received the components and PCBs. It took me about 1 and an half hour to get them assembled, covering the pads with solder thanks to the stencil (what a time saver, considering that for all my previous projects I dispensed solder paste on each pad by hand... relaxing, but it's time consuming and more prone to bridges...)

I then placed all the components and reflow soldered the 2 boards. The first one wasn't looking great, since the GND and 3V3 were shorted (turns out it was the ESP soldering that didn't went well)... which wasn't the case on the second board. So I plugged it onto the computer, rushing to upload my first 'blank' code onto the ESP32... which was horrific since the board wasn't even recognized by the PC... I was so disappointed that it didn't work the first time, considering that I was using a prebuilt ESP32 C3 module, not the bare chip! what could have gone wrong! After the first moment of deception, I tried to figure out what wasn't working: I checked all the PWR rails, 4.90V on the 5V rail: perfect considering the voltage drop caused by the diode, and 3.28V on the 3V3 rail... Ok the board was powered correctly. I remembered that to programm ESP32, you need to execute a serie of button presses (Hold BOOT, plug the board into the computer, press and release RESET, release BOOT), so I tried multiple times, without succeeding... It was the end of the Weekend, so I told myself: You know what? screw it: I'll deal with this during the holidays...

So a few weeks later, I got back into it: I soldered another board, with only the required parts (ESP32 + USB + LDO), maybe the problem came from the CH32s or other mistake on the board... I tried a few more things, checking the schematic, D+ and D- routing, and eventually asked on slack, on #electronics, and huge shout out to @grimsteel for finding the error, and thanks to @Madhav too for his precious help.
I completely messed up the wiring of the BOOT and ENABLE (RST) buttons: I connected the pairs the wrong way, resulting in 2 wires instead of 2 buttons... No wonder the ESP32 couldn't upload, if it was held in Reset forever!

So I desoldered the two buttons on each board, and just tilted them to 60°, to realign the pairs correctly. Actually, I like them this way 😂! unfortunately, the ESP32 on the 'half soldered' board was recognized but failed to upload, stuck at 'connecting...' maybe I fried it with the numerous reflow rework (all the pads underneath the ESP32 module are everything but maker friendly...),
However, I tried on the 2 other boards, and they were uploading perfectly. I was so happy it finally worked 🎉! I soldered a third board, which uploaded well too, so I was able to get 3 fully assembled and working IOX-77 boards out of 4 expected: 75% success rate, not that bad honestly 😂😅! ... at least for the ESP32 part... Haven't tried yet the main feature of this devboard: the CH32 cluster...

BUILD IMG (1) BLP

I desoldered the two buttons on each board, and just tilted them to 60°

corrected sch btns Blueprint lighter 2

I connected the pairs the wrong way, resulting in 2 wires instead of 2 buttons...

I uploaded a basic Arduino test code, to see if I could toggle a GPIO (IO0), and whether the serial communication worked, and it worked just fine on the 3 boards.
Now, the last thing to do is to figure out how to programm the CH32V003, and make them behave as smart GPIO expanders with the ESP32...

Until I advance a bit more on it, here are a few pictures of these 3 IOX-77 superboards:

Gallery:

BUILD IMG (3) BLP

BUILD IMG (5)

BUILD IMG (4)

BUILD IMG (6)

BUILD IMG (2)

BUILD IMG (8) BLP

2/24/2026 - Let's start with the CH32 cluster!

CH32 p1 BP

So, I started learning how to program those cheap Risc-V MCUs, and it actually seems fairly complicated... Here is what the 'blink' program looks like... nothing to do with the Arduino Framework...

CH32 p1 MNRS CODE

Actually, I think I'll enjoy programming those cuz I feel like I'm more professional that with Arduino... I feel like I'm doing bare metal programming (which actually isn't), but I mean I'm way more into hardware, not just 'digitalWriting' and 'pinModeing' stuff 😉...
Learning how to code them properly will be quite time consuming, so I don't know if I'll have enough background before Blueprint ends to have a fully working IOX-77, because I don't want to go straight to the fairly complicated I2C program, but instead learning progressively how to code those chips.

This is why I started with the blink program, which is not less than 33 lines 😓

Anyway, I don't have the 3 boards with me, only one, so I tested it with an FPC adapter and jumper wires, but only 3 ICs uploaded, and looking more carefully, the other 2 IC's SWIO pins are not well soldered, so it's not responding. Or they are just fried, but it's less likely I think... I'll try to resolder them properly one day, but I'll also try the other 2 boards to see if it's better.

I also uploaded a basic sketch on the ESP32 to reset all the chips at boot up, and hold their NRST pins HIGH once it's fully initialized:

CH32 p1 ESP32 CODE

Now that I've discovered the Mounriver Studio IDE, I'll progressively learn how to code them, thanks to the few really well documented websites (like the Curious Scientis. This article is for getting started with GPIOs: https://curiousscientist.tech/blog/ch32v003f4p6-setup-and-gpios?rq=CH32)

3/7/2026 2 PM - CH32 cluster code Part 1 (out of many...) - basic GPIO

I started the code for one of the CH32, starting with the GPIO basis.
Each CH32 will be communicating with the ESP32 via I²C, giving orders like 'set PA2 as Pull up' or 'write HIGH on PB2'... so on the ESP32 side, I'll have every major GPIO related functions (pinMode(), digitalWrite(), analogRead() and so on) customized for the IOX-77, each of these functions will send basic commands via I2C, and then the CH32 executes it. I'll thus need a 'custom' pinMode(), digitalWrite(), etc. on the CH32 side, this is why I'm starting with the custom pinMode() function.

I really want to build the firmware myself, without vibecoding it, but as the documentation is very minimal (only a few guides explains how the CH32V003 environment works), ChatGPT is helping me to learn how to make it work, especially understand all the GPIO_InitStructure... and other weird looking lines like RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE) and all the other things I'm not used to. Then once I think I understand what each functions does and how it behaves, I try rewriting it on Mounriver Studio, tweaking it a bit so that it does exactly what I need, before asking ChatGPT to correct me if something won't work or if there is any syntax error. I already know that with all the hours I'll have to spend on coding the software from the ground up, I'll learn a lot of things along the way!

So for the custom pinMode function, I needed a function with 2 parameters, the pin name, and the mode (OUTPUT PUSH PULL, OUTPUT OPEN DRAIN, ANALOG INPUT, and so on...). For the pin name, the CH32 syntax needs the PORT and PIN NUMBERseparately, so I added a pinMap which links a number (the pin name) to two variables, the PORT and the PIN NUMBER.

I also created the custom pinMode() function and a custom digitalWrite() function.
So now with these two functions, I should still be able to make the LED blink, but with a lighter (visually) code. And... yeah it still works just fine. Perfect!

Here is what this snippet looks like:

CH32 p3 First functions CODE

Testing the other boards...

I also tested the 2 other boards this weekend, and the 10 CH32V003 blinked perfectly. It's also interesting to notice, like bitluni did before me, that the clock of each CH32 becames out of sync pretty fast, and the '1000s' blink becames a mesmerizing random light ballet. that's actually kinda fun!

CH32 p2 BP

3/7/2026 8 PM - CH32 cluster code Part 2 - digitalRead()

The next function to implement was digitalRead(). And while the function itself was pretty straightforward, it showed me a problem that took me a while to debug...

So here is the function, and to test whether it worked or not, I made a quick test with a button (the LED lights up when the button is pressed). the first GPIO that I found accessible was chip #D, PA1, one of the 4 CH32 pins broken out on the classical 2.54 headers, so I quickly uploaded the test.

CH32 p3 First functions CODE (2)

And... when I uploaded the script, quickly after hooking up a button to the right pin, it... didn't worked... So obviously I first thought that there were an error in the software, that I missed something or did something incorrectly, but after triple checking the (simple) code, and letting AI having a look at my work, everything seemed just fine... So I tried with PA2, the pin just next to it, and same problem... what could have been wrong? I tried to see if the pin was faulty, by setting it to OUTPUT, and blinking it, like with the LED on PC0, and instead of getting 0V then 3.3V alterning, I got a poor lil 0.57V 😭. Maybe the PORT A was fried on this IC? no problem, I've got another 15 ready for testing 🤣 but still 0.57V continuous... I even ended up thinking for a brief moment that my multimeter was just hallucinating 🤣! But obviously it wasn't wrong since I tried other ports too, and it was working on PC7... I checked the PCB layout, to ensure it was well connected, that the .5V wasn't induced by nearby traces, and at the moment I started running out of ideas, I looked at the CH32 pinout, and realized that they could be used as an alternate function: OSC, 2 pins for an external oscillator... maybe that was the problem?

So I was like 'hey, chatGPT, do you think this problem could be related to the alternate function of the pins PA1 and PA2?' and it went '✅ you nailed it, that's very likely the cause'. It meant that the IC was configured by default to accept an external oscillator, which made PA1 and PA2 unusable. So instead of going through all the lines of code in all the .c files associated with the CH32V003 IDE, whithout really knowing what could cause the problem, I gave the codes to Chat GPT and he found pretty quickly in the system_ch32v00x.c file, that the uncommented line was #define SYSCLK_FREQ_48MHz_HSE 48000000, so a 48MHz external oscillator, instead of the #define SYSCLK_FREQ_48MHZ_HSI 48000000 I needed. Ok, to be fair, I could have found it myself, it was actually self explainatory 😅 (even neatly explained with comments), but yeah, it's definitely faster when AI does the job, and I didn't wanted to read 700+ lines while the IOX-77 was stuck at the very beginning of the development phase...

CH32 p3 First functions CODE (3)

With that being solved, the test code for the digitalRead() was working just fine, and even though it seems like a very small victory, I'm really happy it finally works... You definitely experience a one of a kind feeling once you've solved a Hardware/software problem, after debugging it for way to long 😅

3/8/2026 - CH32 cluster code Part 3 - PWM

I then started dealing with PWM. It was a bit more complicated than digitalWrite() or digitalRead() functions, as it needs Timers, but following The Curious Scientist code, I modified it so that I can choose on which PIN the PWM is outputted, and instead of taking as parameters PRSC, ARR and CCR, it directly calculates these values from the desired output frequency and the duty cycle.

Extract of the PWM course by The Curious Scientist:
https://curiousscientist.tech/blog/ch32v003f4p6-timers-and-pwm?rq=PWM

Curious Scientist PWM website

PWM how it works

And here is the snippet for handling PWM (just after the digitalWrite() function):

CH32 p3 First functions CODE (3) PWM

So after checking that the desired PIN is PWM capable, it calculates the values for the Timer (PRSC (prescaler), CCR, and ARR) using the given formulas, and sets up the PWM while differentiating the actions depending on the pins (on which Timer (1 or 2) and on which channel (1 - 4) is this pin?) but basically do the exact same thing as explained in the guide by The Curious Scientist (huge shout out to him, by the way, his guides are priceless!)

And the main program, with the LEDs fading in and out:

CH32 p3 First functions CODE (3) PWM main

So after tweaking a few things to make it work perfectly (I first forgot to differentiate the channel depending on the pin, so it didn't worked on all pins), I got something that seems to work just fine: I haven't looked at the outputted signal (I don't have any oscilloscope), so I don't know if the frequency/duty cycle is correct, but at least fading the LED works well.

So overall I'm really happy with this function, because it still make the main program way more readable, with a similar HAL that in the Arduino environment (one line for controling PWM), but it's also way more powerful: a 4 times higher precision (0 - 1023) is achievable on this faster MCU, while even being able to control the outputted frequency.

EDIT: Actually I just remembered that my multimeter could measure frequency and duty cycle (tbh I've never used this feature before), and after checking on the pins PC0 (the LED of each CH32), it was indeed around 1KHz (it displayed smth like 994Hz, so I consider this result as a Pass) For the duty cycle, the results were unreadeable, which is obvious since the LED are fading so constantly changing their duty cycle, so I think I'll just make another small code to ensure it works well.

3/22/2026 - CH32 cluster code Part 4 - I²C

Ok... so... I decided to deal with the I2C, and it was definitely harder than expected... I couldn't really take inspiration from the curious scientist website, since he only use the CH32V003 as master (I needed it as a slave), so I tried to look online, searching for code that already existed to use the MCU as a slave, and... I spent two afternoon, (~8 hours 😭) going from searching resources online with little to no results at all, vibe coding it with chatGPT (which was so bad at it that it mixed everything up, adding lines of code in C for the STM family 😭), documenting myself on I2C, testing the communication with the ESP32, went back to searching online, vibecoding again using another approach, doing smaller tests in I2C between the ESP32 and an Arduino (which took me a while since I didn't even knew how to assign the SDA and SCL pins in the wire.h library (turned out I just had to add the pin number as parameters in the wire.begin(10, 8); line), I even thought at some point that my design was cooked since I used GPIO10 on the ESP32 (which is used by the flash, so I thought for a moment it would never work... [this is me from the future: Actually I made it working, but I don't know if it's reliable, but... whatever] ... This endless loop of FAIIIILURES helped me learning stuff about I2C... but didn't helped me build my code for this IOX-77 project... I thought multiple times that choosing I2C wasn't a good idea, that my IOX-77 V1 would never work and that I should design the V2 using SPI (easier to implement and faster...) but I couldn't give up, not now, after all these hours working on this project...

So I kept going, searching online again and again to a solution for my problem, and I stumbled upon a reddit talking about using the CH32 as a slave. To be honest I had already seen this thread multiple times, but they only mentioned (or it's what I thought at first) the CH32fun library, which didn't fitted my requirements, so... But they showed a link to the official code example made by WCH, and I was so happy to see that the example provided was exactly what I needed: It implemented the I2C both for a slave, and for a master! I finally had a working resources to build my custom code around it!!! without further ado, I tested the code with 2 of the CH32, one as slave and one as master, and (obviously) it worked just fine!. So I tried to replace the Master CH32 with the ESP32 as master (this time I vibecoded it with chatGPT, because I really wanted to see if it would work, and as I gave him the ressources (CH32 I2C example code), it was easier for him to generate a working code for the ESP32 (using wire.h) and he also created a near bare metal code without using the wire.h library, and they both worked just fine! ... or at least they ended up working just fine 😅 because obviously the drop in replacement code didn't worked the first time... It took me a while for me to figure out (or let's say for ChatGPT to figure out) that there were an address mismatch, because the CH32 shifts addresses internally (for like no reason at all 😭), so the address 0x02 isn't the same on the ESP32 code as on the CH32 code... So for it to work I just replaced the 0x02 by 0x04 on the CH32 code, (shifting it to the left), and IT WORKED!!! ... (I mean I haven't looked at it enough, maybe some transmissions ends up generating errors, but at least I was able to get some data to show up in the Serial Monitor!!!!) So now that I'm sure it's possible to get a working I2C com between these 2 MCUs, I'll look deeper into the code to fully understand it, and modify it for my needs. I'm so relieved the I2C is finally working!!!!

Yeah, so here are some previews of the code I used:

Code for the ESP32 (using wire.h):
code I2C working ESP32

Code for the ESP32 (near bare metal):
code I2C working ESP32 bare

Extract of the code for the CH32 (slightly modified demo from WCH (I added checkpoints via UART for debugging)):
code I2C working CH32

I think I'll build the code around the near bare metal code for the ESP32, because it's way more explicit that the wire.h library, which does everything by itself so everything is hidden from my comprehension. So I'll try to build a reliable communication in I2C using these ressources, and hopefully I'll get something working perfectly!

(note: I'm not taking in account the 8 hours I lost searching-online/vibecoding/debugging/pulling-my-hairs-out/going-back-to-online-search/ending-up-vibecoding-again..., because it didn't gave me any results... I really started doing useful things once I had found the miraculous demo example from WCH)

4/1/2026 - CH32 cluster code Part 5 - I²C & digitalRead() buffer

Now that I had a working I²C code basis, I built bit by bit a working communication protocol between the ESP32 and CH32s, and started adding more and more features. I decided to use directly the driver/i2c.h library, instead of relying on the wire.h library. but I still created 2 functions to make things easier: sendI2CCommand (CMD) and retrieveI2CData ().

The ESP32 can send 2 types of command, sendI2CCommand (CMD), where the ESP32 asks for the CH32 to do something, and one retrieveI2CData (), where the ESP32 retrieves the data prepared beforehand by the CH32. For example, the digitalWrite function only requires to send an order, so it works like this:

uint8_t CMD [SIZE] = {0x10, pin, state, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
sendI2CCommand (CMD);

where 0x10 is the name of the command (digitalWrite()), with the pin and state being parameters. I filled the remaining bytes with 0xFF since I decided that all I²C transmissions will consist of 8 bytes, which should be enough for most commands.

So I implemented the IOX_pinMode(), IOX_analogWritePWM(), IOX_digitalWrite() functions this way, using sendI2CCommand ()

For the digitalRead() function, I decided to go a little fancy with more than just a digital reading... I thought it would be cool for the ESP32 to read many digital readings at once (the 32 last readings in time) in one go instead of sending an I²C command every time the ESP32 needed the state of one pin. So I did something different that just the 'ESP32: Send me the pin state on pin XX -> CH32: here it is: X', and implemented the following:
If the ESP32 wants the pin XX to be read every X ms, he first sends a command using the function: IOX_digitalReadSample ()

void IOX_digitalReadSample (uint8_t pin, uint16_t interval, uint8_t multiplier) {
  uint8_t CMD [SIZE] = {0x13, pin, (interval >> 8) & 0xFF, interval & 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
  sendI2CCommand (CMD);
}

where the pin and reading interval (ms) can be chosen.
Then when the ESP32 wants the 32 last readings (so each reading is spaced in time by interval ms), the ESP32 can call IOX_digitalReadBuffer ():

uint32_t IOX_digitalReadBuffer (uint8_t pin) {
  uint32_t initTime = micros();
  uint8_t CMD [SIZE] = {0x14, pin, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
  sendI2CCommand (CMD);
  while (initTime + 500 > micros());
  retrieveI2CData();
  uint32_t reading = (uint32_t)RxData[2] << 24 | (uint32_t)RxData[3] << 16 | (uint32_t)RxData[4] << 8 |  (uint32_t)RxData[5];
  return reading;
}

With this setup, it's the CH32 that does all the readings by itself, and only send the last 32 readings when the ESP32 needs them, all in one go. I'll do a similar approach for the ADC, this way I could add some post processing (like averaging the reading) before sending it to the ESP32.

So the I²C communication for retrieving data is implemented this way:
Whenever the ESP32 wants to get some data back, it first sends a command to the CH32 indicating that he need to add the required data in the TxData buffer, ready to be retrieved by the ESP32 with the function retrieveI2CData();. It's to ensure the CH32 has enough time to update the TxData buffer that I add a small delay before doing retrieveI2CData(); (plus it prevent the bus from being overloaded)

On the CH32 side, if the IOX_digitalReadSample() has been received, the CH32 sample the desired pin constantly, every interval ms. For that I used a structure with all the possible GPIOs (13), with multiple variables (PINSTATE, PREV_MS, ..., INTERVAL...)

typedef struct {
    uint32_t PINSTATE;
    uint32_t PREV_MS;
    uint16_t INTERVAL;
    uint8_t ENABLE;
} digitalReadPinState;

digitalReadPinState digitalSampling[] = {
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0},
    {0x00000000, 0, 0, 0}
};

So... for the CH32 to digitalRead() each pin at a precise rate (set by the INTERVAL value), I needed to check the time in the main () loop, so everyone would have use millis() (or micros()) in the Arduino IDE... except that the CH32 HAL didn't have any millis() or similar function... thankfully it wasn't too hard to implement a similar behavior, with the Systick timer. (hopefully I found a code online that did almost exactly what I wanted!). With too main functions being:
uint32_t ms = 0;

void SysTick_init (u32 counter) {
    NVIC_EnableIRQ (SysTicK_IRQn);
    SysTick->SR &= ~(1 << 0);
    SysTick->CMP = (counter - 1);
    SysTick->CNT = 0;
    SysTick->CTLR = 0x000F;
}
void SysTick_Handler (void) {
    ms++;
    SysTick->SR = 0;
}

Hum... ok I won't descibe everything in detail, since it's start being a lot with the 500+ lines on the CH32 code, but in a nutshell, the main loop consists of handleI2C(); and digitalRead_sample();, where digitalRead_sample(); sample the GPIOs states if the it's enabled for the pin X:

void digitalRead_sample() {
    for (uint8_t p = 0; p <= 13; p++) {
        if (digitalSampling[p].ENABLE == 1 && (ms - digitalSampling[p].PREV_MS)>= digitalSampling[p].INTERVAL) {
            digitalSampling[p].PREV_MS += digitalSampling[p].INTERVAL;
            uint8_t newBit = IOXdigitalRead (p);
            digitalSampling[p].PINSTATE = digitalSampling[p].PINSTATE << 1;
            digitalSampling[p].PINSTATE = digitalSampling[p].PINSTATE | newBit;
            IOXdigitalWrite (2, toggle);
            toggle = !toggle;
        }
    }
}

you'll notice that I tested the 'pace' of my function toggling the built in LED (on PC0 aka 2 according to my pinMap[] structure), making sure it worked correctly, and I indeed got a sampling frequency of 1 kHz when I set the interval to 1ms (IOX_digitalReadSample (6, 1);)
(I read 500Hz on my multimeter, and the LED is toggled everytime the function is entered)

Here is what it looks like when you print the PINSTATE uint32_t retrieved by the ESP32 at each IOX_digitalReadBuffer(): when I press / release the button on PC6, the state of the pin change from 0 to 1 or from 1 to 0, and the new value (measured by the CH32) is added in the 32 bit queue, thus getting rid of the 32nd oldest reading. This is why the 1 and 0 seem to move to the left at each new print on the serial monitor:

BP Part 5

Whenever a packet is received over the I²C, the packets are saved in the RxData[] array, then at the end of the transaction, buffered in RxDataBuffer[]. the flag cmd_ready is set to 1, meaning that the command is ready to be analyzed by the function handleI2C();, which determines what the CH32 has to do depending on the command and parameters (with switch () case: syntaxes)

Another interesting thing to note, is that as I'm limited with packets of 1 byte transiting on the I2C bus, variables that are longer than that (uint16_t or uint32_t) are split in smaller packets, then reconstructed on the other side, with for example the variable freqOUT or duty_cyclefor the PWM:

void IOX_analogWritePWM (uint8_t pin, uint16_t duty_cycle, uint32_t freqOUT) {
  uint32_t initTime = micros();
  uint8_t CMD [SIZE] = {0x11, pin, (duty_cycle >> 8) & 0xFF, duty_cycle & 0xFF, (freqOUT >> 24) & 0xFF, (freqOUT >> 16) & 0xFF, (freqOUT >> 8) & 0xFF, freqOUT & 0xFF};
  sendI2CCommand (CMD);
  while (initTime + 1000 >= micros());
}

then on the CH32 side:

uint16_t duty_cycle = (uint16_t)RxDataBuffer[2] << 8 | (uint16_t)RxDataBuffer[3];
uint32_t freqOUT = (uint32_t)RxDataBuffer[4] << 24 | (uint32_t)RxDataBuffer[5] << 16 | (uint32_t)RxDataBuffer[6] << 8 | (uint32_t)RxDataBuffer[7];

It took me quite a while to get to this point, trying many things in C on the CH32 code (like for example pushing the CH32 to it's limit, implementing a micros variable like I did with millis, but it's not optimal with the interrupt of the Systick, since it would have been entered way too many times (1 millions times per seconds!) and it was a bit laggy, I couldn't get the digitalRead sampling to be at a higher frequency that 4.5KHz using micros instead of millis, probably because the micros interrupt was taking way too long.

Anyway I'm not explaining everything I've done in the firmware because I'm already talking too much, but in a nutshell (yeah, this time I stop digressing 😅), I implemented the functions IOX_pinMode(), IOX_analogWritePWM(), IOX_digitalWrite(), IOX_digitalReadSample() and IOX_digitalReadBuffer() over I²C, so I'm now able to fully control the GPIOs on one of the CH32 by simply coding the ESP32! You can't imagine how happy and relieved I was to see the first IOX_digitalWrite() working over I²C! After so much struggle getting the I²C to work! That was so fun seing an LED blynk-over-I²C 😂

Now, the last thing to do is to tackle the ADC reading on the CH32 side, and I'll then just have to make some HAL on the ESP32 side, cuz I'll have to do a few things for controlling all those GPIOs easily, without messing up which CH32 control which pin 😂

NOTE: the advancement of the code is in the Github repo, to avoid overloading the Blueprint journal

4/6/2026 - CH32 cluster code Part 6 - HAL (Arduino IDE)

The ESP32, the main MCU (whereas the CH32s are only smart GPIO expanders), will be programmed using the Arduino IDE, so I needed an easy way of controlling each GPIO, without thinking too much to which CH32 it belongs: I thus made a few pin definitions, using generic names related to the pin & CH32 owner (like A _ PC0, or C _ PD3). Each pin name links to a 1 byte long ID, which directly gives the CH32 owner and pin number like so:

B_PD0 = 0xB8; //CH32 B - pin 8

I also defined the address of each CH32, with the corresponding address to set on the CH32 code (shifted by 1 to the left). I now have 70 GPIOs easily accessible:

const uint8_t CH32_A_ADDR = 0x01;   //0x02
const uint8_t CH32_B_ADDR = 0x03;   //0x06
const uint8_t CH32_C_ADDR = 0x04;   //0x08
const uint8_t CH32_D_ADDR = 0x05;   //0x0A
const uint8_t CH32_E_ADDR = 0x06;   //0x0C

const uint8_t A_PA1 = 0xA0; //CH32 A - pin 0
const uint8_t A_PA2 = 0xA1; //CH32 A - pin 1
const uint8_t A_PC0 = 0xA2; //CH32 A - pin 2
const uint8_t A_PC3 = 0xA3; //CH32 A - pin 3
...
const uint8_t A_PD4 = 0xAB; //CH32 A - pin 11
const uint8_t A_PD5 = 0xAC; //CH32 A - pin 12
const uint8_t A_PD6 = 0xAD; //CH32 A - pin 13

const uint8_t B_PA1 = 0xB0; //CH32 B - pin 0
const uint8_t B_PA2 = 0xB1; //CH32 B - pin 1
const uint8_t B_PC0 = 0xB2; //CH32 B - pin 2
...
const uint8_t B_PD6 = 0xBD; //CH32 B - pin 13

const uint8_t C_PA1 = 0xC0; //CH32 C - pin 0
...
const uint8_t C_PD6 = 0xCD; //CH32 C - pin 13

const uint8_t D_PA1 = 0xD0; //CH32 D - pin 0
...
const uint8_t D_PD6 = 0xDD; //CH32 D - pin 13

const uint8_t E_PA1 = 0xE0; //CH32 E - pin 0
const uint8_t E_PA2 = 0xE1; //CH32 E - pin 1
...
const uint8_t E_PD5 = 0xEC; //CH32 E - pin 12
const uint8_t E_PD6 = 0xED; //CH32 E - pin 13

Now, every time a function acts on one GPIO, the GPIO ID e.g 0xB8 (retrieved from the name, e.g. B _ PD0) is split into two parts: the CH32 owner (e.g. B) which links to the right CH32 I²C Address (thanks to the function getAddr and the pin number, which is later on sent to the right CH32 in the command CMD.

With the new functions implemented, here is how it looks like:

uint8_t getAddr (uint8_t pin) {
  switch (pin >> 4) {
    case 0x0A :
      return CH32_A_ADDR;
    case 0x0B :
      return CH32_B_ADDR;
    case 0x0C :
      return CH32_C_ADDR;
    case 0x0D :
      return CH32_D_ADDR;
    case 0x0E :
      return CH32_E_ADDR;
  }
  return 0xFF;
}

as in this example below, the pin number is directly extracted from the pin ID with pin & 0x0F:

void IOX_pinMode (uint8_t pin, uint8_t state) {
  uint32_t initTime = micros();
  uint8_t CMD [SIZE] = {0x12, (uint8_t)(pin & 0x0F), (uint8_t)state, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
  sendI2CCommand (CMD, (uint8_t) getAddr (pin));
  while (initTime + 1000 > micros());
}

I can now seamlessly control GPIOs like in the Arduino framework:

IOX_pinMode (A_PC0, OUTPUT);
IOX_digitalWrite (D_PC0, HIGH);

BP Part 6

4/7/2026 12 PM - Taking a break in programming: resoldering the IOX-77...

You may have noticed that on the previous build images, I had slight problems with the QFN soldering: on some ICs, there were solder bridges on some GPIOs, and out of the 15 CH32s, 1 weren't recognized on mounriver studio, and for 2 others, the I²C didn't worked, since I was able to upload a blink program, but the IOX-77 code didn't worked. So I left this aside for a while, because I wanted to work on the CH32 code, but I now have to tackle this, to get a fully working devboard with the expected 75 GPIOs, not an half-working board with defective ICs...

So I took my hot plate and tried to solder them again correctly. I removed the faulty ICs, and after trying multiple time to resolder them in place, adding a bit of solder paste, some flux... I finally succeeded to get 2 board fully working... On the 3rd board, I damaged one trace, the GPIO trace for the LED of the CH32 A... other than that, the board is working, but with one dead LED... I also soldered the 40P FPC, since there were solder bridged too on the .5mm pitched legs.

At least I now have 2 fully working devboards, that I thoroughly rinsed with water, vigorously brushed to remove all the flux, before putting them in an IPA bath, which resulted in 2 clean PCBs. I inspected them under the microscope, and every solder join seemed good enough.

Gallery:

BUILD IMG resoldered (2)

BUILD IMG resoldered (3)

BUILD IMG resoldered (5)

I then tested a board with a few 3mm LEDs I had laying around, with a quick test code, and everything worked flawlessly:

const uint8_t headerPins [16] = {C_PC3, B_PC0, B_PD0, B_PA2, B_PA1, A_PD4, A_PD5, A_PD2, A_PD3, A_PD6, A_PC0, A_PC3, C_PC7, A_PC4, A_PA1, A_PA2};
for (int p = 0; p < 16; p++) {
  IOX_pinMode (headerPins[p], OUTPUT);
  IOX_digitalWrite (headerPins[p], HIGH);
  delay (1000);
  IOX_digitalWrite (headerPins[p], LOW);
  IOX_pinMode (headerPins[p], INPUT);
}

BUILD IMG resoldered (4)

4/7/2026 5 PM - IOX code Part 7 - Arduino Library & Reset

I converted the ESP32 code in the Arduino IDE to a library: It's only local, for now, I still have the .cpp and .h files, but I will only build the library package once the ESP32 code is finished. For now, I'll keep the 3 files altogether so I can easily change the code. Converting the code to a library wasn't that hard, and there where only some minor modifications to do, like changing the function definitions from IOX_digitalWrite (uint8_t pin, uint8_t state) to IOXClass::digitalWrite (uint8_t pin, uint8_t state), but at the end of the day, I have a clean HAL for the IOX-77, with the functions highlighted with the Arduino colors, thanks to the keyword.txt I created:

BP Part7

I'm really happy with how it's looking so far!
I played around a bit coloring some constant variables, like A_LED_BUILTIN or D_PA2, so that it looks exactly like an Arduino devboard, ready to be used out of the box!

I also implemented the hardware reset of the CH32 (the ESP32 can reset each CH32 independently by pulling their NRST pin LOW), so at boot up, the CH32 are reset too.

And guess what? while testing this new implementation on one board, 4 CH32 reset correctly, but the CH32 D just didn't care 😭! After checking it's NRST pin, I noticed that the ESP32 simply didn't pulled it LOW... Let's go for another soldering session 😒

I desoldered the ESP32, added a bit of solderpaste on the faulty pad, resoldered it in place, and... now one CH32 isn't responding anymore! (maybe due to overheating? I heated that board wayyyyy too many times 😅) I ended up replacing the faulty CH32, and eventually got this board repaired. Phew!

Hopefully every solder join is decent, now... At least I hope so...

And that's it: I can now easily control each additional GPIO, like on any other Arduino devboard! Now the last thing to do is to implement the ADC and low power modes.

I can see the light at the end of the tunnel! 😂

NOTE: the advancement of the code (IOX library) is in the Github repo, to avoid overloading the Blueprint journal

4/8/2026 - CH32 cluster code Part 8 - AnalogRead() & sleep mode

The last, difficult, but important part to implement in the code was the analogReading. Again, I heavily inspired me from the Curious Scientist examples, and just had to modify it in order to be able to enable/disable the channels on demand. I first created the IOX.analogRead() function.

Turned out it was harder than expected to create the logic behind those GPIO manipulations, (handling which pins was analog capable...) but I eventually got something that worked on one channel. Nice!

However when scaling up to 2 GPIOs simultaneously, problems started to appear... the ADC readings went whoosh... After trying many things to debug it, I discovered that the IOX.pinMode (pin, ANALOG _ IN) wasn't received correctly by the CH32, thus preventing the ADC from being correctly initialized... So I looked at the received command by the CH32 over I²C when the IOX.pinMode function was called, and the state (0xC0, ie ANALOG _ IN) wasn't received correctly! it was replaced by 0xFF! I started going in circles until I realized that it was just that the command was being overwritten by the next function called, IOX.analogWrite() which resulted in a weird combo of pinMode command + 0xFF from the new command... Turned out the CH32 simply needed more time to process the IOX.pinMode (pin, ANALOG _ IN) command, since it initialized the ADC everytime this function was called. So I simply increased the pause time to 5 ms and it was good to go.

I didn't stopped myself here, and decided to create the same '32 bit' reading history, like I did for the digital read, except that I'm not storing bits (HIGH and LOW) but uint16_t integers, so I won't keep the 32 last readings but the 16 last ones (cuz it takes some memory space...), and I won't send them raw over I²C, they will instead be used for the creation of an averaged value. Same thing here, I created the analogRead _ sample() averageAnalogReading() functions as well as some structure to manage the channels and keep the data: analogSampling[] and analogReadingHistory[8][16] (8 ADC channels, 16 values kept in the buffer)

It was pretty hard to manage all these functions and variables, and even though what I come up with might probably not be the better way to do all that, so far it seems to be working, so I guess it's fine...

Part 8 BP

2 ADC readings - me playing around with 2 potentiometers -, one averaged with readings separated by a 64ms interval, the other by a 32ms interval). Sometimes there is some weird spikes (which shouldn't appear since it should be a smoothed signal) but It's not important: It's just that the potentiometer which wasn't connected correctly disconnected from time to time, leaving the GPIO floating.

I also implemented the sleep mode, where one command can be sent to a CH32 over I²C (CMD 0xAA) to go to sleep. If the ESP32 need it back, it can just reset it.

The new functions in the IOX library:

void IOXClass::sleepCH32 (uint8_t CH32_addr) {
    uint32_t initTime = micros();
    uint8_t CMD [SIZE] = {0xAA, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
    sendI2CCommand (CMD, CH32_addr);
    while (initTime + 3000 > micros());
}

void IOXClass::sleepCH32_All () {
    sleepCH32 (CH32_A_ADDR);
    sleepCH32 (CH32_B_ADDR);
    sleepCH32 (CH32_C_ADDR);
    sleepCH32 (CH32_D_ADDR);
    sleepCH32 (CH32_E_ADDR);
}

and on the CH32 side:

void EnterSleepMode (void) {
    RCC_APB2PeriphClockCmd (RCC_APB2Periph_TIM1, DISABLE);
    RCC_APB1PeriphClockCmd (RCC_APB1Periph_TIM2, DISABLE);
    RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOA, DISABLE);
    RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOD, DISABLE);
    RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOC, DISABLE);

    PWR_EnterSTANDBYMode  (PWR_STANDBYEntry_WFI);
}

And once the ESP32 goes to sleep too, (with the line esp_light_sleep_start();) the current consumption goes in the sub mA domain. The IOX-77 can go from 51mA idle (All 5 CH32 active) to 18mA (all 5 CH32 disabled) down to <1mA in sleep mode.

Note: Again, there are a few way too much functions and variable required for handling the ADC, so I'm not adding them in the Bluprint journal, I keep them in the github repo instead (Advancement #2)

4/9/2026 - IOX-77 code: Part 9 - coding a demo & packaging the library

I polished the CH32 .c file and the library (.cpp and .h files) by removing all the commented printf and millis() flags that helped me debug it, checked again that it worked just fine, and started packaging the library: I coded a small demo code as an example, which test the analog PWM writing, analog Reading and digitalReading, as well as the sleep mode, while printing on the serial monitor all the data.

I checked one more time that the demo worked correctly on the IOX-77, and before packaging all these files in a neat IOX.zip library file, I changed all the while (initTime + 1000 > micros()); to while (micros() - initTime < 1000); because apparently doing the substaction prevents any problems when overflowing (which happens every ~70 minutes for micros()...)

I've finally a working firmware V1.0 🎉, after so much hours coding and debugging this project 🥹
I think this project comes to an end, I just need to polish the github repo, and I think I'll be good to go.

demo BP

Again, all the code files are in the github repo. neatly organized in the FINAL VERSION V1.0 folder
(with the library IOX.zip, all the files inside this .zip also added in the IOX folder, as well as the main.c code for the CH32

4/10/2026 9 AM - Correcting the Pinout

I corrected the pinout sheet I had done before because I don't know why, I marked all the analog capable pins with A1, A5, Ax, even though they are called by they real pin name (A_PA2, C_PD2...) So I changed all the pin name and added colored stripes to indicate which pins are Analog capable and which ones are PWM capable. I also changed a few pin names, because I kinda mixed everything up with those 100 pins, resulting in pin names appearing twice, or in the wrong place... So I carefully checked again with the PCB design files, and hopefully the pinout is right, now...

IOX_77_1

IOX_77_2

IOX_77_3

4/10/2026 2 PM - ㅤㅤㅤㅤㅤ

Project overview, and...

A huge thanks to Blueprint



IOX-77_project overview
ㅤㅤ
ㅤㅤ
I'm really happy of how this project turned out, and I learnt a lot of things along the way, designing my first 4L PCB, learning how to program the CH32V003, coding an Arduino library and especially creating a well organized github repo.

Thank you to the Blueprint team, Thank you to those who helped me debug the board, Thank you all for making this project possible 💖