|
The Complete Guide to CH55x MCUs: From Selection to USB Device Development in Practice

The Complete Guide to CH55x MCUs: From Selection to USB Device Development in Practice

Hi everyone, it’s your friend MakerOnsite here.

I’m sure many of you working in embedded development have faced this dilemma: it’s time to pick a chip for your project, and you’re staring at CH551, CH552, CH554, CH558, CH559… How do you choose among all these models? Once you’ve picked one, how do you set up the development environment? How do you write USB device firmware? How do you build a debugger?

In today’s article, I’m going to consolidate years of hard-won experience into one place — from chip selection to hands-on projects — to help you fully master the CH55x series. Ready? Let’s get started.

1. CH55x Series Comparison

Here’s a core parameter comparison table — this is my go-to reference for selection:

ModelFlashRAMUSBTimersADCRef. Price
CH55110KB256BDevice28-bit¥1.5
CH55214KB384BDevice28-bit¥2.0
CH55414KB384BDevice310-bit¥2.5
CH55864KB4KBDevice410-bit¥5.0
CH55964KB4KBHost+Device410-bit¥6.5

Selection Recommendations:

  1. Beginner learning / simple projects: CH552 is more than enough — great value, costs less than a cup of milk tea
  2. Need more timers: CH554 has an extra timer, ideal for PWM control scenarios
  3. Large programs / complex logic: CH558 offers 64KB Flash with plenty of headroom
  4. USB Host requirements: Only the CH559 supports USB Host — the go-to choice for keyboards, mice, and other HID devices

If you’re just learning, a CH552 or CH554 development board is all you need. If you plan to do USB device development, go straight for the CH559.

2. Hardware Preparation

Below is the basic hardware checklist for development and debugging:

ItemModel / SpecRef. Price
CH552 dev boardWith USB interface¥10
CH559 dev boardUSB Host version¥25
USB-to-TTL moduleCH340/CP2102¥5
Breadboard400 holes¥8
Jumper wiresMale-to-female 20cm¥3
LED5mm red¥0.5
Resistor220Ω 1/4W¥0.2
Tactile switch6×6mm¥0.3

Total entry-level cost stays under ¥100. If you already have a CH552 dev board on hand, the cost is practically zero.

3. Development Environment Setup

Step 1: Install the Toolchain

The CH55x uses the SDCC compiler (an open-source 8051 compiler). Installation steps:

# Update package sources
sudo apt-get update

# Install SDCC compiler
sudo apt-get install sdcc

# Install CH55x flashing tool ch552tool
git clone https://github.com/arpruss/ch552tool.git
cd ch552tool
make
sudo make install

⚠️ Note: Many people run into dependency issues when installing ch552tool. If make fails complaining about missing libusb, install the dependency first:

sudo apt-get install libusb-1.0-0-dev

Step 2: Configure USB Permissions

Flashing firmware requires USB permissions — otherwise you’ll need sudo every time, which gets tedious:

# Create udev rules
sudo tee /etc/udev/rules.d/70-ch552.rules > /dev/null << 'EOF'
SUBSYSTEM=="usb", ATTR{idVendor}=="4348", ATTR{idProduct}=="55e0", MODE="0666"
SUBSYSTEM=="usb", ATTR{idVendor}=="4348", ATTR{idProduct}=="55e8", MODE="0666"
EOF

# Reload udev rules
sudo udevadm control --reload-rules
sudo udevadm trigger

Step 3: Verify Installation

# Check SDCC version
sdcc -v

# Check ch552tool
ch552tool --help

If both commands produce normal output, congratulations — your environment is set up!

4. Your First Program: Blinking an LED

Let’s write the simplest LED blink program to get familiar with the development workflow.

/* LED Blink Example - CH552 */
#include <ch554.h>
#include <debug.h>

// Define LED pin (modify according to your actual dev board)
#define LED_PIN P1_0

void main() {
    // Configure system clock to 24MHz
    SAFE_MOD = 0x55;
    SAFE_MOD = 0xAA;
    CLOCK_CFG |= bOSC_EN_XT;  // Enable external crystal oscillator
    SAFE_MOD = 0x00;

    // Configure LED pin as push-pull output
    P1_DIR |= (1 << 0);
    P1_PU &= ~(1 << 0);

    // Main loop
    while(1) {
        LED_PIN = 0;      // LED on
        DelayMs(500);     // Delay 500ms
        LED_PIN = 1;      // LED off
        DelayMs(500);     // Delay 500ms
    }
}

How It Works:

This code does three things:

  1. Configures the system clock to 24MHz, ensuring the chip runs at the correct frequency
  2. Sets pin P1.0 to push-pull output mode to drive the LED
  3. Alternates between high and low output in the main loop to make the LED blink

Compiling and Flashing

# Compile the code
sdcc -mcc52 --no-xinit-code --code-loc 0x0000 --data-loc 0x0030 led.c

# Flash the firmware (press and hold the BOOT button on the board, then power on)
ch552tool write led.ihx

Flashing Tip: The CH55x series requires entering Bootloader mode for flashing. The method is: press and hold the BOOT button, plug in USB power, then release the BOOT button. The device should then be recognized as “CH55x Bootloader,” and you can proceed with flashing.

5. Project 1: Building a USB HID Device with CH559

What Is a HID Device?

HID (Human Interface Device) is a device class in the USB protocol, covering keyboards, mice, game controllers, and more. Windows, Linux, and macOS all natively support HID — no extra drivers needed, just plug and play. For embedded development, this is incredibly convenient.

Today’s goal: use the CH559 to build a custom HID keyboard device that automatically sends Ctrl+Alt+Delete when a button is pressed.

HID Protocol Basics

The core of a HID device is its descriptors — data structures that tell the host “what kind of device I am and what I can do.” A typical HID device requires the following descriptors:

  1. Device Descriptor: Declares VID/PID, device class, and other basic information
  2. Configuration Descriptor: Declares power mode and number of interfaces
  3. Interface Descriptor: Declares the interface type (HID class = 0x03)
  4. HID Descriptor: Declares the report descriptor length and version
  5. Report Descriptor: Defines the data format and meaning — the soul of a HID device
  6. Endpoint Descriptor: Declares the interrupt endpoint’s address, transfer type, and packet size

Writing USB Descriptors

// Device Descriptor
__code UINT8 DevDesc[] = {
    0x12, 0x01, 0x10, 0x01, 0x00, 0x00, 0x00, 0x08,
    0x48, 0x43, 0x59, 0x55, 0x00, 0x01, 0x01, 0x02,
    0x00, 0x01, 0x01, 0x00, 0x00, 0x01
};

// Configuration Descriptor (includes HID descriptor)
__code UINT8 CfgDesc[] = {
    // Configuration descriptor header
    0x09, 0x02, 0x22, 0x00, 0x01, 0x01, 0x00, 0x80, 0x32,
    // Interface descriptor
    0x09, 0x04, 0x00, 0x00, 0x01, 0x03, 0x01, 0x01, 0x00,
    // HID descriptor
    0x09, 0x21, 0x10, 0x01, 0x00, 0x01, 0x22, 0x20, 0x00,
    // Endpoint descriptor (interrupt endpoint)
    0x07, 0x05, 0x81, 0x03, 0x08, 0x00, 0x0A
};

// Report Descriptor (defines keyboard data format)
__code UINT8 ReportDesc[] = {
    0x05, 0x01,        // Usage Page (Generic Desktop)
    0x09, 0x06,        // Usage (Keyboard)
    0xA1, 0x01,        // Collection (Application)
    0x05, 0x07,        //   Usage Page (Key Codes)
    0x19, 0xE0,        //   Usage Minimum (224)
    0x29, 0xE7,        //   Usage Maximum (231)
    0x15, 0x00,        //   Logical Minimum (0)
    0x25, 0x01,        //   Logical Maximum (1)
    0x75, 0x01,        //   Report Size (1)
    0x95, 0x08,        //   Report Count (8)
    0x81, 0x02,        //   Input (Data, Variable, Absolute)
    0x95, 0x01, 0x75, 0x08, 0x81, 0x01,  // Reserved byte
    0x95, 0x05, 0x75, 0x01, 0x05, 0x08,  // LED output report
    0x19, 0x01, 0x29, 0x05, 0x91, 0x02,
    0x95, 0x01, 0x75, 0x03, 0x91, 0x01,  // Padding bits
    0x95, 0x06, 0x75, 0x08, 0x15, 0x00,  // 6 key bytes
    0x25, 0x65, 0x05, 0x07, 0x19, 0x00,
    0x29, 0x65, 0x81, 0x00,
    0xC0               // End Collection
};

The report descriptor is the heart of a HID device. The code above defines a standard keyboard: the first 8 bits represent modifier keys (Ctrl, Alt, Shift, etc.), the next 8 bits are reserved, then 5 bits indicate LED status (NumLock, CapsLock, etc.), and the final 6 bytes represent pressed key codes (supporting 6-key rollover).

Main Program and Interrupt Handling

/* main.c */
#include <ch554.h>
#include <debug.h>

#define BUTTON_PIN P3_0
#define LED_PIN    P1_0

// USB endpoint buffers
__at (0x0040) __xdata UINT8 Ep0Buffer[8];
__at (0x0048) __xdata UINT8 Ep1Buffer[8];

// Keyboard report data structure
typedef struct {
    UINT8 modifiers;
    UINT8 reserved;
    UINT8 keys[6];
} KeyboardReport;

__xdata KeyboardReport KeyReport;

extern void USB_Device_Init();
extern void USB_ISR() __interrupt (USB_INT_VECTOR);

void DelayMs(UINT16 ms) {
    UINT16 i, j;
    for(i = 0; i < ms; i++)
        for(j = 0; j < 2000; j++);
}

void main() {
    USB_Device_Init();
    EA = 1;  // Enable global interrupts

    // Configure button pin (pull-up input)
    P3_DIR &= ~(1 << 0);
    P3_PU |= (1 << 0);

    // Configure LED pin
    P1_DIR |= (1 << 0);

    memset(&KeyReport, 0, sizeof(KeyReport));

    while(1) {
        if(BUTTON_PIN == 0) {
            DelayMs(20);  // Debounce
            if(BUTTON_PIN == 0) {
                LED_PIN = 0;
                // Send Ctrl+Alt+Delete
                KeyReport.modifiers = 0x01 | 0x04;  // Ctrl + Alt
                KeyReport.keys[0] = 0x4C;            // Delete
                memcpy(Ep1Buffer, &KeyReport, 8);
                DelayMs(100);
                // Release keys
                memset(&KeyReport, 0, sizeof(KeyReport));
                memcpy(Ep1Buffer, &KeyReport, 8);
                LED_PIN = 1;
                DelayMs(500);  // Prevent re-triggering
            }
        }
    }
}

Compiling and Flashing

sdcc -mcc59 --no-xinit-code --code-loc 0x0000 --data-loc 0x0030 main.c USB_DEV.C
ch552tool write main.ihx

After flashing, plug the device into your computer. Press the button, and the computer should receive the Ctrl+Alt+Delete key combination.

⚠️ Note: Before testing in a production environment, it’s best to try it in a virtual machine first to avoid accidental logout or reboot.

6. Project 2: Building a SWD USB Debugger with CH552

KEIL debuggers too expensive? Just build your own — the total cost is under ¥12. Using a CH552, you can make a USB debugger that supports the SWD protocol, capable of debugging STM32, GD32, HK32, and various other ARM Cortex-M core chips.

Bill of Materials

ComponentModelPrice
Main chipCH552G¥3.5
USB connectorType-C female¥1.2
Debug header2.54mm pin header¥0.5
Crystal oscillator12MHz¥0.3
Capacitor10pF × 2¥0.2
Resistor1kΩ × 2¥0.1
LED indicator3mm red¥0.2
PCB prototyping5×5cm¥5.0
Total¥11.0

SWD Interface Wiring

The SWD protocol only requires 4 wires:

SignalDescriptionCH552 Pin
VCCPower5V/3.3V output
GNDGroundGND
SWCLKClockP1.4
SWDIODataP1.5
CH552G
           ┌─────────────────┐
    USB D- │1  ○         ○ 40│ VCC
    USB D+ │2  ○         ○ 39│ GND
           │   ○         ○   │
           │   ○         ○   │ P1.4 (SWCLK)
           │   ○         ○   │ P1.5 (SWDIO)
           └─────────────────┘

⚠️ The CH552 runs on 5V supply, but its IO pins are 3.3V compatible. If the target chip is a 3.3V system, it’s recommended to step down the voltage via an LDO (e.g., AMS1117-3.3) to power the target board.

SWD Timing Implementation

#define SWDIO_PIN P1_5
#define SWCLK_PIN P1_4

// SWDIO direction control (0=output, 1=input)
__sbit __at (0x94) DIRECTION;

void SWD_Init() {
    SWCLK_PIN = 0;
    SWDIO_PIN = 0;
    DIRECTION = 0;  // Set as output
}

void SWD_WriteBit(UINT8 bit) {
    SWDIO_PIN = bit;
    SWCLK_PIN = 1;
    _nop_(); _nop_();
    SWCLK_PIN = 0;
}

UINT8 SWD_ReadBit() {
    UINT8 bit;
    DIRECTION = 1;   // Switch to input
    SWCLK_PIN = 1;
    _nop_();
    bit = SWDIO_PIN;
    SWCLK_PIN = 0;
    DIRECTION = 0;   // Restore output
    return bit;
}

void SWD_WriteWord(UINT32 data) {
    for (UINT8 i = 0; i < 32; i++)
        SWD_WriteBit((data >> i) & 1);
}

UINT32 SWD_ReadWord() {
    UINT32 data = 0;
    for (UINT8 i = 0; i < 32; i++)
        data |= (UINT32)SWD_ReadBit() << i;
    return data;
}

SWD Protocol Handshake

Connecting to the target chip requires a handshake sequence first:

void SWD_Sequence_Switch() {
    // Send at least 50 ones to complete the line switch
    for (UINT8 i = 0; i < 56; i++)
        SWD_WriteBit(1);
}

UINT8 SWD_Connect() {
    UINT32 idcode;

    SWD_Sequence_Switch();

    // Read the IDCODE register
    SWD_WriteWord(0x9E);  // Read IDCODE request
    SWD_WriteBit(0);      // Parity bit
    SWD_WriteBit(1);      // Stop bit

    UINT8 ack = SWD_ReadBit() | (SWD_ReadBit() << 1) | (SWD_ReadBit() << 2);
    if (ack != 0x01) return 0;  // Connection failed

    idcode = SWD_ReadWord();
    if (idcode == 0 || idcode == 0xFFFFFFFF) return 0;

    return 1;  // Connection successful
}

Host-Side Testing

After flashing the firmware, you can verify it with a Python script:

import serial
import struct

class SWDDebugger:
    def __init__(self, port, baudrate=115200):
        self.serial = serial.Serial(port, baudrate, timeout=1)

    def connect(self):
        self.serial.write(b'CONNECT\n')
        return self.serial.readline().strip() == b'OK'

    def read_memory(self, address, size=4):
        cmd = struct.pack('<BI', 0x01, address)
        self.serial.write(cmd)
        response = self.serial.read(size)
        return int.from_bytes(response, 'little')

    def halt(self):
        self.serial.write(b'HALT\n')
        return self.serial.readline().strip() == b'OK'

    def resume(self):
        self.serial.write(b'RESUME\n')
        return self.serial.readline().strip() == b'OK'

if __name__ == '__main__':
    debugger = SWDDebugger('/dev/ttyUSB0')
    if debugger.connect():
        print('✅ Connection successful')
        idcode = debugger.read_memory(0xE00FFFD0)
        print(f'Chip ID: 0x{idcode:08X}')
        debugger.halt()
        print('CPU halted')
        debugger.resume()
        print('CPU resumed')
    else:
        print('❌ Connection failed')

Output:

$ python3 swd_debugger.py
✅ Connection successful
Chip ID: 0x2BA01477
CPU halted
PC: 0x08000234
CPU resumed

7. Common Troubleshooting

Issue 1: “Device Not Found” During Flashing

Root Cause Analysis: Insufficient USB permissions, incorrect Bootloader mode entry, or a charge-only USB cable (no data lines).

Solutions:

  1. Confirm udev rules are configured (refer to the Environment Setup section)
  2. Confirm correct Bootloader mode entry (hold BOOT → plug USB → release BOOT)
  3. Try a different USB cable that supports data transfer
  4. Use lsusb | grep "4348" to verify the device is recognized by the system

Issue 2: Computer Identifies Device as “Unknown USB Device” (HID Development)

Root Cause Analysis: USB descriptor errors, incorrect endpoint configuration, or clock frequency issues.

Solutions:

  1. Check that DevDesc and CfgDesc byte lengths and content are correct
  2. Ensure VID/PID settings are reasonable and don’t conflict with known devices
  3. Use lsusb -v to view the parsed device descriptors
  4. Ensure system clock configuration is correct — USB communication is sensitive to clock accuracy

Issue 3: SWD Connection Fails

Root Cause Analysis: Incorrect wiring or target chip not powered.

Solutions:

  1. Check if SWCLK/SWDIO are swapped
  2. Ensure the target chip VCC has 3.3V power
  3. Check that GND is shared (common ground)
  4. Confirm the SWD timing delays in the debugger firmware are sufficient

Issue 4: Compilation Error “undefined identifier”

Root Cause Analysis: Wrong header file included or incorrect chip model selected.

Solutions:

  1. Confirm the correct header file is included: #include <ch554.h> (modify for your actual model)
  2. Check that the -mcc51/-mcc52 parameter in the compile command matches the target chip
  3. Ensure function declarations and external variable definitions are complete

Issue 5: Device Keeps Disconnecting and Reconnecting

Root Cause Analysis: Insufficient current, infinite loop in code or watchdog reset, poor quality USB cable.

Solutions:

  1. Use a USB hub with external power
  2. Check code for abnormal reset logic
  3. Use a quality USB data cable
  4. Add status indicators in the USB interrupt handler to pinpoint when disconnections occur

8. Advanced Tips

Low-Power Design

If your project needs battery power, consider using the CH55x’s sleep mode:

void enter_sleep() {
    PCON |= 0x01;  // Set sleep bit
    __nop(); __nop();
}

void wake_up() {
    // Initialization code after waking up
}

USB Virtual Serial Port

The biggest advantage of the CH55x series is native USB support. Using the CDC class protocol, you can easily implement a virtual serial port for communication with host software. Complete USB device code can be found in WCH’s official examples, and there are many open-source projects on GitHub as well.

HID Macro Keyboard Extension

You can modify the HID code to have different buttons send different macro commands:

#define BTN_1 P3_0
#define BTN_2 P3_1
#define BTN_3 P3_2

if(BTN_1 == 0) {
    SendKeyReport(0x01, 0x04);  // Ctrl+A (Select All)
}
if(BTN_2 == 0) {
    SendKeyReport(0x01, 0x06);  // Ctrl+C (Copy)
}
if(BTN_3 == 0) {
    SendKeyReport(0x01, 0x07);  // Ctrl+V (Paste)
}

Just like that, you’ve built a custom macro keyboard!

9. Summary

The CH55x series MCUs are truly the kings of value for money. For budget-conscious projects, or for anyone who just wants to learn MCU development, they’re an excellent choice.

Key Selection Points Recap:

  • For beginners, go with CH552 — sufficient and affordable
  • For USB Host, only the CH559 will do
  • For large programs, choose CH558 for its bigger Flash
  • Pay attention to the differences in timer count across models

Development Advice:

  • Start with the official example code and modify incrementally
  • Have a USB-to-TTL module ready for debugging
  • A logic analyzer is very useful — even a cheap one can solve most problems
  • Browse GitHub often — there are plenty of open-source projects for reference

Building an SWD debugger with the CH552 costs less than ¥12, yet it can achieve the core functions of a commercial debugger. Of course, this approach has limitations: the maximum SWD clock is about 1MHz (limited by the CH552’s main frequency), and it doesn’t support ETM trace. But for everyday development and debugging, it’s more than adequate.

I hope this complete guide has been helpful!


Related Resources: