PY32F002A SOP8 封装下 SWD 引脚复用的防砖指南
SOP8 只有 8 个引脚。VCC 和 VSS 吃掉两个,剩下 6 个。如果你需要两路通信接口、两路传感器输入、一路控制输出,6 个引脚刚好用完。然后你发现 PA13(SWDIO)、PA14(SWCLK)和 PF2(NRST)全都被迫让位给了业务功能。接着你烧不进去程序了。
这篇文章梳理 SOP8 封装的 SWD 引脚复用策略、恢复机制和防砖实践,不涉及具体产品设计,只讲通用方法。
SOP8 的引脚布局
PY32F002AL15S(SOP8)的引脚分配:
VCC 1 8 VSS
PA4/PA10 2 7 PA1
PA3 3 6 PA2/PF2-NRST
PA14-SWC 4 5 PA13-SWD
/PB3注意三个共享焊盘:
- Pin 2:PA4 和 PA10 二选一(通过 SYSCFG 寄存器选择)
- Pin 4:PA14(SWCLK)和 PB3 二选一
- Pin 6:PA2 和 PF2-NRST 二选一(PF2/NRST 通过 Option Bytes 切换)
每个被占用的调试引脚意味着什么:
| 引脚 | 被占用的后果 |
|---|---|
| PA13 (SWDIO) | 调试器无法与芯片通信 |
| PA14 (SWCLK) | 调试器没有时钟 |
| PF2 (NRST) | Connect Under Reset 不可用 |
三者全占 = 常规 SWD 调试通路全部切断。
SWD 引脚的生存窗口
ARM Cortex-M0+ 上电后,PA13 和 PA14 的默认状态是 AF0(SWDIO + SWCLK)。这个状态一直保持到用户固件执行 GPIO 初始化把它们切走。
上电 -> SystemInit -> main -> GPIO_Init(PA13, ...) -> SWD 消失
^
窗口在这里关闭,微秒级在 24MHz 的 PY32F002A 上,从复位释放到 GPIO 初始化执行完,通常不到 1 毫秒。除非你主动把窗口拉大,否则调试器不够时间连上来。
三层防线
第一层:启动延迟
在 main() 开头加一段 delay,再切 SWD 引脚。Milton 在他的 PY32 博文里推荐 2 秒:
int main(void)
{
HAL_Init();
SystemClock_Config();
#ifdef DEBUG_BUILD
HAL_Delay(2000); /* 调试版本给调试器 2 秒连接窗口 */
#endif
/* 下面的 GPIO 初始化会重映射 PA13/PA14 */
Remap_SWD_Pins_To_GPIO();
/* ... 业务逻辑 ... */
}用 #ifdef DEBUG_BUILD 包住,量产时一关宏就去掉了。
这层防线的漏洞: 如果固件在 delay 之前就 crash 了(时钟配置写错、HardFault),窗口直接消失。这只是买时间,不是真正的恢复机制。
第二层:Connect Under Reset
这是真正的防线。原理如下:
NRST = LOW -> Core 处于复位状态 -> SWD 引脚 = AF0(默认)
-> 调试器通过 SWD 连接成功
-> 调试器在 Reset Handler 入口设硬件断点
NRST = HIGH -> Core 释放复位 -> 立即停在断点 -> 用户代码还没跑调试器在芯片还处于复位状态时就接管了 SWD 总线,然后在第一条用户指令之前设下断点。用户代码连 main 都没进去。
前提是 NRST 引脚必须可用。几种调试器的操作方式:
| 调试器 | 操作 |
|---|---|
| J-Link | connect -> 选器件 -> 提示 "Connect under reset?" -> Yes |
| DAP-Link / PY32-Link | 接线时把 NRST 接上,工具自动使用 |
| OpenOCD | reset_config srst_only srst_nogate |
| pyocd | --connect=under-reset |
如果 J-Link 连不上,先检查 NRST 线接了没有。这是最常见的漏接。
第三层:Option Bytes 与 SOP8 的限制
PY32F002A 内建了一个系统内存 bootloader,通过 USART1 烧录。拉高 BOOT0(PF4)上电就能进入。
但 SOP8 封装没有引出 PF4/BOOT0 引脚。 这条路在 SOP8 上不存在。这也是 SOP8 的终极风险:如果 SWD 死了、NRST 也没了、BOOT0 又不存在,芯片就失去了所有外部恢复手段。
20 引脚及以上的封装(TSSOP20、QFN20)有 BOOT0,可以通过 USART ISP 救砖。选 SOP8 就意味着你必须靠前两层防线保护自己。
开发用大封装,量产切 SOP8
PY32F002A、PY32F003、PY32F030 共用同一套 HAL 驱动和同一个硅片。TSSOP20 封装引出了全部 18 个 IO,SWD、NRST、BOOT0 一个不缺。开发阶段用 TSSOP20,量产阶段切 SOP8,是 PY32 系列的标准玩法。
封装的切换只是表象,真正的约束是外设子集要对齐。你在 TSSOP20 上开发时,不能用 F003 多出来的 DMA、TIM3、USART2。这些在 F002A 上根本没有。只要你只使用 F002A 也有的外设子集,从 TSSOP20 切到 SOP8 就只是改 Makefile 里 MCU_TYPE 和 linker script 的事。
引脚分配上的过渡策略:
- TSSOP20 开发阶段: 把将来要挪到 PA13/PA14/PF2 的功能,先挂在 TSSOP20 多出来的空闲引脚上。比如最终要占用 PA13 做 GPIO 输出,开发阶段先用 PA8 代替,等切 SOP8 时再把代码里的
PA8改成PA13。 - SOP8 移植阶段: 硬件换成 SOP8,功能引脚挪到目标位置。此时固件保留启动 delay 和安全模式,NRST 保持为复位引脚。验证所有外设功能正常。
- 量产锁死: 最后一步才烧录关闭 NRST 的 Option Bytes。这一步不可逆(SOP8 没有 BOOT0 救不了),所以排在所有验证通过之后。
整个过程里,SWD 调试能力只在最后一步之前始终可用。开发调试用 TSSOP20 全功能 SWD,移植验证用安全模式保底,量产烧录一次性关闭 NRST。
启动延迟不能带到量产固件里,但可以在切 SWD 之前加一个条件判断:某个 GPIO 被外部拉低时,跳过 SWD 重映射,保持调试通道畅通。
选择检测引脚的原则:
- 正常运行时空闲为高电平(或被外部上拉)
- 外部拉低不会触发总线协议误判
- 最好是已有引脚,不额外占用
static int should_enter_safe_mode(void)
{
/*
* 选择某个正常运行时为高电平的引脚做安全模式检测。
* 例如 I2C SCL 空闲时为高,外部拉低不会触发 Start 信号
* (因为 Start 条件是 SCL 高电平期间 SDA 下降沿)。
*/
GPIO_InitTypeDef gpio = {0};
/* 重要:必须先使能 GPIO 端口时钟,否则 HAL_GPIO_Init 寄存器写入可能无效 */
__HAL_RCC_SAFE_MODE_PORT_CLK_ENABLE();
gpio.Pin = SAFE_MODE_PIN; /* 由项目配置决定 */
gpio.Mode = GPIO_MODE_INPUT;
gpio.Pull = GPIO_PULLUP;
HAL_GPIO_Init(SAFE_MODE_PORT, &gpio);
HAL_Delay(1);
return (HAL_GPIO_ReadPin(SAFE_MODE_PORT, SAFE_MODE_PIN) == GPIO_PIN_RESET);
}
int main(void)
{
HAL_Init();
SystemClock_Config();
if (!should_enter_safe_mode()) {
/* 正常启动:重映射 SWD 引脚到业务功能 */
Remap_SWD_Pins_To_GPIO();
}
/* 安全模式下 SWD 引脚保持 AF0,调试器可正常连接 */
/* 恢复安全模式检测引脚的业务功能(如 I2C AF) */
Restore_SafeModePin_AF();
/* ... 业务逻辑 ... */
}需要更新固件时,把检测引脚临时短接到 GND 再上电就行。
为什么选 I2C SCL 类型的引脚做检测: SCL 空闲时为高电平,外部拉低不会被从机误判,因为 I2C 的 Start 条件要求 SCL 高电平期间 SDA 出现下降沿。单拉低 SCL 不会触发任何 I2C 行为。类似的,SPI 的 SCK(CPOL=0 时空闲为低,CPOL=1 时空闲为高)也可以根据实际配置选择。
NRST 什么时候可以放开
PF2-NRST 通过 Option Bytes 控制。Option Bytes 的地址是 0x1FFF 0E80,bit 14 控制 NRST_MODE:
| NRST_MODE | 行为 |
|---|---|
| 0(默认) | PF2 是 NRST 复位引脚 |
| 1 | PF2 是普通 GPIO(NRST 功能关闭) |
把 PF2 改成 GPIO 的代码:
static void APP_FlashSetOptionBytes(void)
{
FLASH_OBProgramInitTypeDef OBInitCfg;
LL_FLASH_Unlock();
LL_FLASH_OB_Unlock();
OBInitCfg.OptionType = OPTIONBYTE_USER;
OBInitCfg.USERType = OB_USER_BOR_EN | OB_USER_BOR_LEV |
OB_USER_IWDG_SW | OB_USER_WWDG_SW |
OB_USER_NRST_MODE | OB_USER_nBOOT1;
OBInitCfg.USERConfig = OB_BOR_DISABLE | OB_BOR_LEVEL_3p1_3p2 |
OB_IWDG_SW | OB_WWDG_SW |
OB_RESET_MODE_GPIO | /* PF2 变 GPIO */
OB_BOOT1_SYSTEM;
LL_FLASH_OBProgram(&OBInitCfg);
LL_FLASH_Lock();
LL_FLASH_OB_Lock();
LL_FLASH_OB_Launch(); /* 触发软复位,OB 生效。此调用不返回 */
}关键防护: 调用前检查 OB 是否已经被改过,避免无限复位:
if (READ_BIT(FLASH->OPTR, FLASH_OPTR_NRST_MODE) == OB_RESET_MODE_RESET) {
APP_FlashSetOptionBytes(); /* 只执行一次 */
}NRST 改 GPIO 的时机: 放在上一节三步流程的最后一步。SOP8 硬件全部验证通过,量产烧录时才执行。在此之前 NRST 始终保持为复位引脚。
总结
SOP8 封装下占用 PA13/PA14/PF2 可以做,但要有纪律:
- 开发时不动 SWD 和 NRST。 用引脚更多的封装调试,引脚管够。
- 量产固件留安全模式。 一个 GPIO 拉低就让 SWD 保持打开,永远有一条路回去。
- NRST 是最后一道防线。 没了 NRST 就等于放弃了 Connect Under Reset,最可靠的 SWD 恢复手段。把它留到最后再关。
- SOP8 没有 BOOT0,ISP 救不了你。 防线全在前两层,软件上必须做足。
补充说明:PA10 和 PA4 在 SOP8 的 Pin 2 上是二选一关系。如果你要用硬件 I2C1,注意 PA4 不支持 I2C SDA 复用(只有 SPI1_NSS、USART1_CK),必须选 PA10(AF6 = I2C1_SDA)。类似的,Pin 4 在 PA14 和 PB3 之间二选一,如果你的外设需要独立于 SWCLK 的引脚,选 PB3(AF1 = TIM1_CH2)。选引脚前对照数据手册的复用功能映射表,避免选了不支持的组合。