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

自制 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. USB CDC 虚拟串口(用于调试信息输出)
  2. SWD 协议时序模拟(用于芯片调试)

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 连接失败

  • 原因: 接线错误或目标芯片未供电
  • 解决:
    1. 检查 SWCLK/SWDIO 是否接反
    2. 确保目标芯片 VCC 有 3.3V 供电
    3. 检查 GND 是否共地

问题 3:读写数据错误

  • 原因: 时序过快或奇偶校验错误
  • 解决:
    1. 降低 SWD 时钟频率(在 SWD_WriteBit 中增加延时)
    2. 检查奇偶校验计算是否正确

问题 4:CH552 无法烧录

  • 原因: bootloader 模式未进入
  • 解决:
    1. 断电后按住 BOOT 键
    2. 上电,等待 1 秒后松开 BOOT 键
    3. 运行 ch55xburn 烧录

扩展功能

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

  1. JTAG 支持:增加 TMS/TDI/TDO 引脚
  2. 逻辑分析仪:利用剩余 IO 口采样信号
  3. UART 调试:增加串口透传功能
  4. OLED 显示:显示调试信息

总结

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

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

  • 最高 SWD 时钟约 1MHz(受限于 CH552 主频)
  • 不支持 ETM 跟踪
  • 需要自己编写上位机软件

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

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


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