自制 USB 调试器:CH552 + SWD 方案实现

上周有个朋友问我:KEIL 调试器太贵了,有没有便宜点的方案?

我说:自己做一个呗,成本不到 20 块钱。

他还以为我在开玩笑。今天这篇文章就来认真聊聊,如何用沁恒的 CH552 单片机,制作一个支持 SWD 协议的 USB 调试器。不仅能调试 STM32,还能调试 GD32、HK32 等各种 ARM Cortex-M 内核的芯片。

需要准备什么?

元件型号价格
主控芯片CH552G¥3.5
USB 接口Type-C 母座¥1.2
调试接口2.54mm 排针¥0.5
晶振12MHz¥0.3
电容10pF × 2¥0.2
电阻1kΩ × 2¥0.1
LED 指示灯3mm 红色¥0.2
PCB 打样5×5cm¥5.0
总计¥11.0

没错,全套成本不到 12 块钱。如果你手头有现成的 CH552 开发板,那成本几乎为零。

为什么选 CH552?

CH552 是沁恒推出的一款增强型 8 位 USB 单片机,主要特性:

  • 主频最高 24MHz

  • 内置 USB 全速控制器

  • 10KB SRAM,2KB XRAM

  • 14KB Flash

  • 支持 USB Device 和 Host 模式

最关键的是:它便宜。相比 FT2232H(¥35+)、CP2102(¥8+),CH552 的成本优势太明显了。

步骤 1:硬件原理图

SWD 协议只需要 4 根线: | --- | --- | --- | | VCC | 电源 | 5V/3.3V 输出 | | GND | 地 | GND | | SWCLK | 时钟 | P1.4 | | SWDIO | 数据 | P1.5 |

CH552G
           ┌─────────────────┐
    USB D- │1  ○         ○ 40│ VCC
    USB D+ │2  ○         ○ 39│ GND
           │   ○         ○   │
           │   ○         ○   │ P1.4 (SWCLK)
           │   ○         ○   │ P1.5 (SWDIO)
           └─────────────────┘

注意事项: ⚠️ CH552 是 5V 供电,但 IO 口兼容 3.3V。如果目标芯片是 3.3V 系统,建议通过 LDO(如 AMS1117-3.3)降压后给目标板供电。

步骤 2:固件开发

我们需要实现两个功能:

  1. $1

  2. $1

2.1 工程配置

使用 SDCC 编译器,创建 Makefile

CC = sdcc
OBJ = usb_debugger.rel

all: $(OBJ)

  $(CC) -o usb_debugger.ihx $(OBJ)

  $(OBJ): usb_debugger.c
  $(CC) -c usb_debugger.c

flash:

  ch55xburn --erase --write usb_debugger.bin --verify

clean:
  rm -f .rel .lst .map .ihx *.bin

2.2 USB 描述符配置

// usb_debugger.c
#include 
#include 
#include 

// USB 设备描述符
__code UINT8 DevDesc[] = {
    18,                     // 描述符长度
    0x01,                   // 设备描述符类型
    0x10, 0x01,             // USB 版本 1.1
    0x00,                   // 设备类
    0x00,                   // 设备子类
    0x00,                   // 设备协议
    8,                      // 最大包大小
    0x43, 0x48,             // VID: 0x4843 (沁恒)
    0x52, 0x55,             // PID: 0x5552
    0x00, 0x01,             // 设备版本
    0x01,                   // 制造商字符串索引
    0x02,                   // 产品字符串索引
    0x00,                   // 序列号索引
    0x01                    // 配置数
};

// 配置描述符
__code UINT8 CfgDesc[] = {**
    9,                      // 配置描述符长度
    0x02,                   // 配置描述符类型
    32, 0x00,               // 总长度
    0x01,                   // 接口数
    0x01,                   // 配置值
    0x00,                   // 配置字符串索引
    0x80,                   // 属性
    50,                     // 最大电流 100mA
    // 接口描述符
    9,                      // 接口描述符长度
    0x04,                   // 接口描述符类型
    0x00,                   // 接口号
    0x00,                   // 备用设置
    0x02,                   // 端点数
    0x02,                   // 接口类 (CDC)
    0x02,                   // 接口子类
    0x01,                   // 接口协议
    0x00,                   // 接口字符串索引

    // CDC 功能描述符
    0x05, 0x24, 0x00, 0x10, 0x01,
    0x05, 0x24, 0x01, 0x00, 0x00,
    0x04, 0x24, 0x02, 0x02,
    0x05, 0x24, 0x06, 0x00, 0x01,

    // 端点描述符
    7,                      // 端点描述符长度
    0x05,                   // 端点描述符类型
    0x81,                   // 端点地址 (IN)
    0x03,                   // 传输类型 (中断)
    0x08, 0x00,             // 最大包大小
    0x01,                   // 轮询间隔

    7,                      // 端点描述符长度
    0x05,                   // 端点描述符类型
    0x02,                   // 端点地址 (OUT)
    0x03,                   // 传输类型 (中断)
    0x08, 0x00,             // 最大包大小
    0x01                    // 轮询间隔
};

// 字符串描述符
__code UINT8 LangDesc[] = { 0x03, 0x02, 0x09, 0x04 };
__code UINT8 ManuDesc[] = { 0x03, 0x0C, 0x43, 0x00, 0x48, 0x00, 0x35, 0x00, 0x35, 0x00, 0x32, 0x00 };
__code UINT8 ProdDesc[] = { 0x03, 0x1A, 0x53, 0x00, 0x57, 0x00, 0x44, 0x00, 0x20, 0x00, 0x44, 0x00, 0x65, 0x00, 0x62, 0x00, 0x75, 0x00, 0x67, 0x00, 0x67, 0x00, 0x65, 0x00, 0x72, 0x00 };

2.3 SWD 时序实现

SWD 协议的核心是时钟和数据线的时序控制:

// SWD 引脚定义
#define SWDIO_PIN P1_5
#define SWCLK_PIN P1_4

// SWDIO 方向控制 (0=输出,1=输入)

__sbit __at (0x94) DIRECTION;

// 初始化 SWD 接口

void SWD_Init() {

    SWCLK_PIN = 0;

    SWDIO_PIN = 0;

    DIRECTION = 0;  // 设置为输出

}

// 发送一个比特

void SWD_WriteBit(UINT8 bit) {

    SWDIO_PIN = bit;

    SWCLK_PIN = 1;

    _nop_();

    _nop_();

    SWCLK_PIN = 0;

}

// 读取一个比特

UINT8 SWD_ReadBit() {

    UINT8 bit;

    DIRECTION = 1;  // 设置为输入

    SWCLK_PIN = 1;

    _nop_();

    bit = SWDIO_PIN;

    SWCLK_PIN = 0;

    DIRECTION = 0;  // 恢复输出

    return bit;

}

// 发送 32 位数据

void SWD_WriteWord(UINT32 data) {

    for (UINT8 i = 0; i 
        SWD_WriteBit((data >> i) & 1);

    }

}

// 读取 32 位数据

UINT32 SWD_ReadWord() {

    UINT32 data = 0;

    for (UINT8 i = 0; i 
        data |= (UINT32)SWD_ReadBit() 
    }

    return data;

}

2.4 SWD 协议握手

连接目标芯片需要先进行握手:

// SWD 序列:从 SWD 切换到 SWJ
void SWD_Sequence_Switch() {
    // 发送至少 50 个 1
    for (UINT8 i = 0; i // 连接目标芯片

UINT8 SWD_Connect() {
    UINT32 idcode;

    // 发送切换序列
    SWD_Sequence_Switch();

    // 读取 IDCODE 寄存器
    SWD_WriteWord(0x9E);  // 读 IDCODE 请求
    SWD_WriteBit(0);      // 奇偶校验
    SWD_WriteBit(1);      // 停止位

    // 读取响应
    UINT8 ack = SWD_ReadBit() | (SWD_ReadBit() 
                (SWD_ReadBit() 

    if (ack != 0x01) {
        return 0;  // 连接失败
    }

    // 读取 IDCODE
    idcode = SWD_ReadWord();

    if (idcode == 0 || idcode == 0xFFFFFFFF) {
        return 0;  // 无效 ID
    }

    return 1;  // 连接成功
}

步骤 3:上位机软件

固件完成后,我们需要一个上位机来发送调试命令。这里提供一个简单的 Python 脚本:

# swd_debugger.py
import serial
import struct
import time

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')
        response = self.serial.readline()
        return response.strip() == b'OK'

    def read_memory(self, address, size=4):
        #读取内存
        cmd = struct.pack('
        self.serial.write(cmd)
        response = self.serial.read(size)
        return int.from_bytes(response, 'little')

    def write_memory(self, address, value):
        #写入内存
        cmd = struct.pack('
        self.serial.write(cmd)
        response = self.serial.readline()
        return response.strip() == b'OK'

    def halt(self):
        #暂停 CPU
        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(✅ 连接成功)

        # 读取芯片 ID
        idcode = debugger.read_memory(0xE00FFFD0)
        print(f芯片 ID: 0x{idcode:08X})

        # 暂停 CPU
        debugger.halt()
        print(CPU 已暂停)

        # 读取 PC 寄存器
        pc = debugger.read_memory(0xE000ED38)
        print(fPC: 0x{pc:08X})

        # 恢复运行
        debugger.resume()
        print(CPU 已恢复)
    else:
        print(❌ 连接失败)

步骤 4:测试验证

烧录固件后,插入电脑:

# 查看设备
$ lsusb
Bus 001 Device 042: ID 4843:5552 QinHeng Electronics CH552 SWD Debugger

#查看串口设备
$ ls /dev/ttyUSB*
/dev/ttyUSB0

运行测试脚本:

$ python3 swd_debugger.py
 连接成功
芯片 ID: 0x2BA01477
CPU 已暂停
PC: 0x08000234
CPU 已恢复

效果展示:**

常见问题排查

问题 1:设备无法识别

  • 原因: USB 描述符配置错误

  • 解决: 检查 DevDescCfgDesc 是否与代码一致,确保 VID/PID 正确

问题 2:SWD 连接失败

  • 原因: 接线错误或目标芯片未供电

  • 解决:

检查 SWCLK/SWDIO 是否接反

  • 确保目标芯片 VCC 有 3.3V 供电

  • 检查 GND 是否共地

问题 3:读写数据错误

  • 原因: 时序过快或奇偶校验错误

  • 解决:

降低 SWD 时钟频率(在 SWD_WriteBit 中增加延时)

  • 检查奇偶校验计算是否正确

问题 4:CH552 无法烧录

  • 原因: bootloader 模式未进入

  • 解决:

断电后按住 BOOT 键

  • 上电,等待 1 秒后松开 BOOT 键

  • 运行 ch55xburn 烧录

扩展功能

这个调试器还可以扩展更多功能:

  1. $1

  2. $1

  3. $1

  4. $1

总结

用 CH552 制作 SWD 调试器,成本不到 12 块钱,却能实现商业调试器的核心功能。对于学生、创客和小型团队来说,这是一个性价比极高的选择。

当然,这个方案也有局限性:

  • 最高 SWD 时钟约 1MHz(受限于 CH552 主频)

  • 不支持 ETM 跟踪

  • 需要自己编写上位机软件

但对于日常开发和调试来说,完全够用了。

希望这篇博客文章对您有所帮助!


相关资源:- CH552 数据手册- ARM SWD 协议规范- OpenOCD 源码参考- 本项目 GitHub