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

注意三个共享焊盘:

每个被占用的调试引脚意味着什么:

引脚 被占用的后果
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 的事。

引脚分配上的过渡策略:

  1. TSSOP20 开发阶段: 把将来要挪到 PA13/PA14/PF2 的功能,先挂在 TSSOP20 多出来的空闲引脚上。比如最终要占用 PA13 做 GPIO 输出,开发阶段先用 PA8 代替,等切 SOP8 时再把代码里的 PA8 改成 PA13
  2. SOP8 移植阶段: 硬件换成 SOP8,功能引脚挪到目标位置。此时固件保留启动 delay 和安全模式,NRST 保持为复位引脚。验证所有外设功能正常。
  3. 量产锁死: 最后一步才烧录关闭 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 可以做,但要有纪律:

  1. 开发时不动 SWD 和 NRST。 用引脚更多的封装调试,引脚管够。
  2. 量产固件留安全模式。 一个 GPIO 拉低就让 SWD 保持打开,永远有一条路回去。
  3. NRST 是最后一道防线。 没了 NRST 就等于放弃了 Connect Under Reset,最可靠的 SWD 恢复手段。把它留到最后再关。
  4. 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)。选引脚前对照数据手册的复用功能映射表,避免选了不支持的组合。