一、SWD协议基础回顾
SWD物理接口
cpp
两根线:
1. SWCLK: 时钟线,主机驱动,上升沿采样
2. SWDIO: 双向数据线,半双工通信
SWD传输帧结构
cpp
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│Start│APnDP│ RnW │ A2 │ A3 │Parity│Stop │Park │Trn │ACK │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
0 1 2 3 4 5 6 7 8-9 10-12
二、底层宏定义详解
1. 时钟周期生成宏
cpp
#define SW_CLOCK_CYCLE() \
PIN_SWCLK_CLR(); \ // 下降沿
PIN_DELAY(); \ // 延迟
PIN_SWCLK_SET(); \ // 上升沿
PIN_DELAY() // 延迟
时序图:
cpp
SWCLK: ______/¯¯¯¯¯¯¯¯\______
下降沿 上升沿
采样点 切换点
关键点:
-
SWD在上升沿采样数据
-
下降沿到上升沿之间是稳定时间
-
上升沿到下降沿之间是传播时间
2. 位写入宏
cpp
#define SW_WRITE_BIT(bit) \
PIN_SWDIO_OUT(bit); \ // 设置数据
PIN_SWCLK_CLR(); \ // 下降沿
PIN_DELAY(); \ // 稳定时间
PIN_SWCLK_SET(); \ // 上升沿(目标采样)
PIN_DELAY() // 保持时间
工作流程:
-
在时钟下降沿之后设置数据
-
在下一个上升沿之前确保数据稳定
-
目标在上升沿采样数据
3. 位读取宏
cpp
#define SW_READ_BIT(bit) \
PIN_SWCLK_CLR(); \ // 下降沿
PIN_DELAY(); \ // 建立时间
bit = PIN_SWDIO_IN(); \ // 采样数据
PIN_SWCLK_SET(); \ // 上升沿
PIN_DELAY() // 保持时间
关键时序:
cpp
目标驱动SWDIO → 主机下降沿 → 等待建立时间 → 主机采样 → 主机上升沿
三、SWJ_Sequence函数详解
函数功能:发送任意位序列
cpp
void SWJ_Sequence(uint32_t count, const uint8_t *data)
使用场景:
-
SWD线复位(51个1)
-
JTAG到SWD切换序列(0xE79E)
-
自定义测试序列
LSB优先发送实现
cpp
while (count--) {
if (n == 0U) { // 当前字节已发完
val = *data++; // 读取新字节
n = 8U; // 重置位计数器
}
if (val & 1U) { // 检查LSB
PIN_SWDIO_TMS_SET(); // 发送1
} else {
PIN_SWDIO_TMS_CLR(); // 发送0
}
SW_CLOCK_CYCLE(); // 产生时钟
val >>= 1; // 右移,准备下一位
n--; // 位计数器减1
}
位顺序示例(发送字节0xA5=10100101b):
cpp
原始字节: 0xA5 = 1 0 1 0 0 1 0 1
MSB← →LSB
LSB优先发送顺序: 1 0 1 0 0 1 0 1
实际发送: 1(bit0)→0(bit1)→1(bit2)→0(bit3)→0(bit4)→1(bit5)→0(bit6)→1(bit7)
四、SWD_TransferFunction核心实现
这是整个SWD协议的核心,让我们逐段详细分析。
1. 函数定义和参数
cpp
static uint8_t SWD_Transfer##speed (uint32_t request, uint32_t *data)
-
request: 8位请求值,包含APnDP、RnW、A2、A3
-
data: 指向32位数据的指针
-
写操作:提供要写入的数据
-
读操作:接收读取的数据
-
-
返回值: 3位ACK响应
2. 请求包发送(8位)
位0:起始位(Start Bit)
cpp
SW_WRITE_BIT(1U); // 固定为1
-
每个SWD传输都以逻辑1开始
-
用于同步
位1:APnDP位
cpp
bit = request >> 0; // 从request参数提取bit0
SW_WRITE_BIT(bit);
parity += bit;
-
0:访问DP(调试端口)
-
1:访问AP(访问端口)
位2:RnW位
cpp
bit = request >> 1; // 提取bit1
SW_WRITE_BIT(bit);
parity += bit;
-
0:写操作
-
1:读操作
位3-4:地址位A2、A3
cpp
bit = request >> 2; // A2
SW_WRITE_BIT(bit);
parity += bit;
bit = request >> 3; // A3
SW_WRITE_BIT(bit);
parity += bit;
-
组合指定寄存器地址:
-
A3=0,A2=0 → 地址0x0 (DP-IDCODE/ABORT)
-
A3=0,A2=1 → 地址0x4 (DP-CTRL/STAT)
-
A3=1,A2=0 → 地址0x8 (DP-SELECT)
-
A3=1,A2=1 → 地址0xC (DP-RDBUFF)
-
位5:奇偶校验位
cpp
SW_WRITE_BIT(parity);
-
计算前5位的奇偶性
-
奇校验:1的个数为奇数时校验位=1
-
用于检测传输错误
位6-7:停止位和空闲位
cpp
SW_WRITE_BIT(0U); // 停止位(固定0)
SW_WRITE_BIT(1U); // 停车位(固定1)
-
停止位:表示请求包结束
-
停车位:主机释放总线前的最后一个1
3. 方向切换(Turnaround)
cpp
PIN_SWDIO_OUT_DISABLE(); // 释放SWDIO,改为输入
for (n = DAP_Data.swd_conf.turnaround; n; n--) {
SW_CLOCK_CYCLE();
}
关键点:
-
SWDIO从输出模式 切换到输入模式
-
需要至少1个时钟周期的方向切换时间
-
避免总线冲突
时序:
cpp
主机: SWDIO输出 → 释放总线 → SWDIO输入
目标: 准备驱动ACK响应
4. ACK响应接收(3位)
cpp
SW_READ_BIT(bit);
ack = bit << 0; // ACK[0]
SW_READ_BIT(bit);
ack |= bit << 1; // ACK[1]
SW_READ_BIT(bit);
ack |= bit << 2; // ACK[2]
ACK响应码:
cpp
0b001 (1): OK - 传输成功
0b010 (2): WAIT - 重试
0b100 (4): FAULT - 错误
0b000 (0): 无响应(超时)
0b111 (7): 协议错误
5. 数据阶段(32位数据 + 1位奇偶)
情况A:读操作(ACK=OK)
cpp
if (ack == DAP_TRANSFER_OK) {
if (request & DAP_TRANSFER_RnW) { // 读操作
val = 0U;
parity = 0U;
for (n = 32U; n; n--) { // 读取32位数据
SW_READ_BIT(bit);
parity += bit;
val >>= 1;
val |= bit << 31; // LSB先接收,需要移位
}
SW_READ_BIT(bit); // 读取奇偶位
if ((parity ^ bit) & 1U) { // 奇偶校验
ack = DAP_TRANSFER_ERROR;
}
if (data) { *data = val; } // 存储读取的数据
读取32位数据的技巧:
-
LSB先接收,但需要存储为MSB在前的格式
-
每次接收1位,右移并放入最高位
-
最终得到正确的32位值
示例:读取0x12345678
cpp
接收顺序: LSB先 → 0,0,0,1,0,1,1,1,0,1,1,1,0,1,0,0,1,0,1,0,0,0,1,0,0,0,0,1,0,0,1,0
实际值: 0x12345678
情况B:写操作(ACK=OK)
cpp
} else { // 写操作
// 先完成方向切换
for (n = DAP_Data.swd_conf.turnaround; n; n--) {
SW_CLOCK_CYCLE();
}
PIN_SWDIO_OUT_ENABLE(); // 切换回输出模式
val = *data; // 获取要写入的数据
parity = 0U;
for (n = 32U; n; n--) { // 写入32位数据
SW_WRITE_BIT(val); // 写入LSB
parity += val;
val >>= 1; // 准备下一位
}
SW_WRITE_BIT(parity); // 写入奇偶位
}
写数据的关键:
-
方向切换:输入→输出
-
LSB先发送
-
计算并发送奇偶校验位
6. 错误处理(WAIT/FAULT响应)
WAIT响应处理(目标忙)
cpp
if (ack == DAP_TRANSFER_WAIT) {
if (DAP_Data.swd_conf.data_phase && (request & DAP_TRANSFER_RnW)) {
// 如果是读操作,需要跳过数据阶段
for (n = 32U+1U; n; n--) {
SW_CLOCK_CYCLE(); // 产生时钟但不采样
}
}
// 方向切换
for (n = DAP_Data.swd_conf.turnaround; n; n--) {
SW_CLOCK_CYCLE();
}
PIN_SWDIO_OUT_ENABLE();
if (DAP_Data.swd_conf.data_phase && !(request & DAP_TRANSFER_RnW)) {
// 如果是写操作,需要跳过数据阶段
PIN_SWDIO_OUT(0U);
for (n = 32U+1U; n; n--) {
SW_CLOCK_CYCLE(); // 产生时钟但不驱动数据
}
}
PIN_SWDIO_OUT(1U);
return ((uint8_t)ack);
}
WAIT响应的意义:
-
目标暂时无法处理请求
-
主机需要稍后重试
-
必须正确跳过数据阶段,保持协议同步
7. 协议错误恢复
cpp
// 协议错误(无效ACK)
for (n = DAP_Data.swd_conf.turnaround + 32U + 1U; n; n--) {
SW_CLOCK_CYCLE(); // 产生足够时钟跳过剩余传输
}
PIN_SWDIO_OUT_ENABLE();
PIN_SWDIO_OUT(1U);
return ((uint8_t)ack);
协议错误场景:
-
ACK=0b000(无响应)
-
ACK=0b111(协议错误)
-
ACK=0b011等(保留值)
五、快速和慢速模式实现
1. 快速模式(Fast)
cpp
#undef PIN_DELAY
#define PIN_DELAY() PIN_DELAY_FAST()
SWD_TransferFunction(Fast)
-
使用最小延迟
-
用于高时钟频率(>1MHz)
-
依赖硬件GPIO速度
2. 慢速模式(Slow)
cpp
#undef PIN_DELAY
#define PIN_DELAY() PIN_DELAY_SLOW(DAP_Data.clock_delay)
SWD_TransferFunction(Slow)
-
可配置延迟
-
用于低时钟频率或长线缆
-
更稳定的时序
3. 模式选择
cpp
uint8_t SWD_Transfer(uint32_t request, uint32_t *data) {
if (DAP_Data.fast_clock) {
return SWD_TransferFast(request, data);
} else {
return SWD_TransferSlow(request, data);
}
}
切换条件:
-
初始化阶段使用慢速模式
-
识别目标后可能切换到快速模式
-
连接不稳定时切回慢速模式
六、完整SWD传输时序图
读操作成功时序
cpp
时钟周期 操作 SWDIO状态
--------------------------------------------------
0-7 发送请求包 主机驱动
[1][APnDP][RnW=1][A2][A3][P][0][1]
8-9 方向切换 高阻态
10-12 接收ACK响应 目标驱动
[ACK0][ACK1][ACK2]=001(OK)
13-44 接收32位数据 目标驱动
LSB先,每时钟1位
45 接收奇偶位 目标驱动
46-47 方向切换 高阻态
48-? 空闲周期 主机驱动高电平
写操作成功时序
cpp
时钟周期 操作 SWDIO状态
--------------------------------------------------
0-7 发送请求包 主机驱动
[1][APnDP][RnW=0][A2][A3][P][0][1]
8-9 方向切换 高阻态
10-12 接收ACK响应 目标驱动
[ACK0][ACK1][ACK2]=001(OK)
13-14 方向切换 高阻态
15-46 发送32位数据 主机驱动
LSB先,每时钟1位
47 发送奇偶位 主机驱动
48-? 空闲周期 主机驱动高电平
七、关键配置参数
1. 时钟延迟(clock_delay)
cpp
DAP_Data.clock_delay
-
控制每个时钟边沿的延迟
-
值越大,时钟频率越低
-
计算公式:频率 = 1 / (2 × 延迟 × 时钟周期时间)
2. 方向切换周期(turnaround)
cpp
DAP_Data.swd_conf.turnaround
-
通常为1-2个时钟周期
-
确保总线方向切换稳定
-
长线缆需要更多周期
3. 空闲周期(idle_cycles)
cpp
DAP_Data.transfer.idle_cycles
-
传输间的空闲时钟数
-
给目标芯片处理时间
-
通常0-255个周期
4. 数据相位(data_phase)
cpp
DAP_Data.swd_conf.data_phase
-
控制WAIT/FAULT时是否处理数据阶段
-
0:忽略数据阶段
-
1:必须发送/接收数据阶段
八、实际传输示例分析
示例1:读取DP IDCODE(地址0x0)
请求字节计算:
cpp
More=0, RnW=1(读), APnDP=0(DP), A2=0, A3=0
请求字节 = 0b00000101 = 0x05
调用:
cpp
uint32_t idcode;
uint8_t ack = SWD_Transfer(0x05, &idcode);
传输过程:
-
发送请求包:
1 0 1 0 0 0 0 1 -
接收ACK:
0 0 1(OK) -
接收32位IDCODE:LSB先
-
接收奇偶位
-
返回ACK=OK,idcode=0x1BA01477
示例2:写AP CSW寄存器(地址0x0)
请求字节计算:
cpp
More=0, RnW=0(写), APnDP=1(AP), A2=0, A3=0
请求字节 = 0b00000010 = 0x02
**数据:** 0x23000012
调用:
cpp
uint32_t data = 0x23000012;
uint8_t ack = SWD_Transfer(0x02, &data);
九、性能优化技巧
1. 批量传输优化
cpp
// 批量读取时减少方向切换
for (i = 0; i < count; i++) {
SWD_Transfer(read_request, &data[i]);
// 只有最后一次需要完整的方向切换
}
2. 延迟调优
cpp
// 自适应延迟调整
if (error_count > threshold) {
DAP_Data.clock_delay++; // 增加延迟,提高稳定性
} else if (stable_count > threshold2) {
DAP_Data.clock_delay--; // 减少延迟,提高速度
}
3. 预取优化
cpp
// 提前发送下一个读请求
SWD_Transfer(read_req1, NULL); // 延迟读
// 处理其他任务...
SWD_Transfer(read_req2, &data1); // 获取数据1并发起新请求
十、调试和问题排查
1. 常见问题:无响应(ACK=0)
可能原因:
-
目标未上电
-
SWD线路连接错误
-
时钟频率过高
-
目标处于复位状态
排查步骤:
-
检查电源和地线
-
降低时钟频率
-
发送SWD复位序列(51个1)
-
检查nRESET引脚状态
2. 常见问题:WAIT响应
可能原因:
-
目标处理器忙
-
访问被保护区域
-
时钟不匹配
解决方法:
-
增加重试次数
-
检查目标状态
-
调整时钟延迟
3. 常见问题:奇偶校验错误
可能原因:
-
噪声干扰
-
时序不稳定
-
目标驱动能力不足
解决方法:
-
增加方向切换周期
-
降低时钟频率
-
检查PCB布线
十一、SWD协议状态机
主机状态机
cpp
IDLE → 发送请求 → 方向切换 → 接收ACK →
↓ ↓ ↓
OK? → 数据阶段 → 方向切换 → IDLE
↓ ↓
WAIT? → 重试 → 发送请求
↓
FAULT? → 错误处理 → IDLE
目标状态机
cpp
IDLE → 接收请求 → 解析 → 处理 →
↓ ↓ ↓
OK? → 准备数据 → 发送ACK → 数据阶段
↓ ↓
忙? → 发送WAIT → 等待
↓
错误? → 发送FAULT → IDLE
总结
这个SWD_Transfer实现展示了:
-
精确的比特级控制:每个时钟周期都精确控制
-
完整的协议处理:请求、响应、数据、错误处理
-
灵活的配置:支持不同速度和时序要求
-
高效的实现:使用宏和内联优化性能
理解这个实现对于:
-
开发自定义调试工具
-
调试SWD连接问题
-
优化调试性能
-
移植到新硬件平台
都至关重要。这是现代ARM调试生态系统的基石之一。