目录

自制 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 根线:

SWD 信号 说明 CH552 引脚
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 < 32; i++) {
        SWD_WriteBit((data >> i) & 1);
    }
}

// 读取 32 位数据
UINT32 SWD_ReadWord() {
    UINT32 data = 0;
    for (UINT8 i = 0; i < 32; i++) {
        data |= (UINT32)SWD_ReadBit() << i;
    }
    return data;
}

2.4 SWD 协议握手

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

// SWD 序列:从 SWD 切换到 SWJ
void SWD_Sequence_Switch() {
    // 发送至少 50 个 1
    for (UINT8 i = 0; i < 50; i++) {
        SWD_WriteBit(1);
    }

    // 发送切换序列:0111 1001 1001 1100 1110 0111
    UINT8 switch_seq[] = {1,0,0,1,1,1,0,0,1,1,1,0,0,1,1,1};
    for (UINT8 i = 0; i < 16; i++) {
        SWD_WriteBit(switch_seq[i]);
    }

    // 再发送至少 50 个 1
    for (UINT8 i = 0; i < 50; i++) {
        SWD_WriteBit(1);
    }
}

// 连接目标芯片
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() << 1) | 
                (SWD_ReadBit() << 2) | (SWD_ReadBit() << 3);

    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('

步骤 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 跟踪
  • 需要自己编写上位机软件

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

GitHub 仓库: https://github.com/makeronsite/ch552-swd-debugger

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


相关资源: