stm32/gpio-out/relay-uln2003.md

继电器(ULN2003 驱动,2 路)

知识库 · STM32

继电器(ULN2003 驱动,2 路)

#一、元信息

字段
芯片/平台STM32F103C8T6(HAL)
接口类型GPIO 推挽输出
验证状态✅ 已验证
验证环境Demo00 工程 / Keil MDK(AC5)/ 2026-08-18
驱动方式HAL

#二、接线表

信号MCU 引脚模块引脚备注
继电器1PA14ULN2003 IN1 → 继电器线圈高电平吸合(洋桃1号官方定义)
继电器2PA13ULN2003 IN2 → 继电器线圈高电平吸合(洋桃1号官方定义)
指示LED1PB0板载LED与继电器1联动(本工程附加)
指示LED2PB1板载LED与继电器2联动(本工程附加)

⚠️ 引脚号极易写反:洋桃1号官方定义是 继电器1 = PA14,继电器2 = PA13,不是 PA13/PA14!写反表现为"继电器2 上电就吸合"或"触摸 A 控制的是继电器2"。

#三、踩坑清单

  1. CubeMX 的 HAL_MspInit 过早禁用 SWJ(本工程第二贵教训,2026-08-18 实测):CubeMX 在 PA13/PA14 配置为 GPIO 输出时,会在 HAL_MspInit()HAL_Init() 一开头就调用)里执行 __HAL_AFIO_REMAP_SWJ_DISABLE()——比 relay_init 早得多
  • 后果:禁用 SWJ 后 PA13(SWDIO) 芯片内部固定上拉,此时引脚还是输入模式 → 输出高电平 → 继电器在 SystemClock_Config 等待 HSE 起振期间(最长几百 ms)被强制吸合;若 HSE 起振失败(电源不稳)→ Error_Handler() 死循环 → 继电器永远吸合、按键全部失效。现象:"上电后继电器一直导通、按 B/D 关不掉、LED 变暗"。
  • 解法:在 HAL_MspInit()USER CODE BEGIN MspInit 1 区域,禁用 SWJ 后立即把 PA13/PA14 配置为推挽输出并拉低(见下方"关键修复")。
  • 对比:官方标准库代码把"禁 SWJ"放在 RELAY_Init() 内且在 GPIO 配置之后,不存在此窗口。
  1. HSE 起振失败会死锁,必须兜底SystemClock_Config() 若 HSE 起振失败直接 Error_Handler(),程序死锁 → 继电器保持最后状态、按键失效。解法:HSE 失败时回退 HSI 8MHz 继续运行(见"关键修复"),保证继电器始终可控。
  2. SWJ 释放顺序必须对:PA13/PA14 默认是 SWDIO/SWCLK(SWD 调试功能),必须把 AFIO->MAPR.SWJ_CFG 置为 100 释放成 GPIO。
  • 错误做法:在 GPIO 初始化之前release_swd_pins() —— 释放时引脚电平不受控(可能被内部上拉),上电瞬间继电器误动作。
  • 正确顺序(实测有效):① 先配置 GPIO 为推挽输出 → ② 再禁用 SWJ → ③ 最后写低电平初始。HAL 写法 __HAL_AFIO_REMAP_SWJ_DISABLE()
  1. 继电器 1/2 与 PA13/PA14 的对应关系:官方是 RELAY1=GPIO_Pin_14RELAY2=GPIO_Pin_13。初始代码定义反了,导致"继电器2上电就开"的假象(其实是 PA14 被当成了 2 号)。
  2. 禁用 SWJ 后 ST-Link/SWD 失效:PA13/PA14 不再有调试功能,之后只能用 CH340 串口(Boot0 拉高)下载 HEX;要恢复调试需按住 NRST 连接 ST-Link 并全片擦除。
  3. ULN2003 是反相达林顿:IN 高电平 → OUT 拉低,若继电器接在 OUT 与 VCC 之间则"IN 高 = 线圈通电"。带光耦/指示灯的模块还要注意极性。
  4. printf 卡死排查:程序里用了 printf 而 Keil 未勾选 MicroLIB 时,独立运行(无调试器)会走 semihosting 触发 BKPT 卡死,主循环永远不执行,表现为"继电器/触摸全没反应"。排查外设"不响应"先确认主循环真的在跑(可先用纯 GPIO 点亮 LED 验证)。
  5. 烧录旧 HEX 的坑:改完代码必须重新 F7 编译,确认 Build Output 0 Error、Demo00.hex 时间戳/大小有更新再烧。Keil 增量编译可能只更新部分 .o,建议改多文件时全量 rebuild。
  6. 先用 LED 代替继电器验证控制链路:遇到继电器"不响应"时,先把输出换到普通 GPIO + LED,点亮 LED 验证 MCU 侧逻辑 OK,再切回继电器排查硬件(触点、供电、ULN2003 输入端)。
  7. 供电不足会放大一切问题:继电器线圈吸合需几十 mA,若板子靠 ST-Link 3.3V(只有几十 mA)供电,吸合瞬间电压跌落 → LED 变暗、MCU 复位、触摸误触发。继电器最好独立 5V/12V 供电,ST-Link 只给 MCU。

#四、关键修复(2026-08-18 实测:上电误吸合 + 关不掉)

#4.1 修复1:HAL_MspInit 禁用 SWJ 后立即拉低 PA13/PA14

stm32f1xx_hal_msp.c(CubeMX 生成,改动写在 USER CODE 区域内):

cvoid HAL_MspInit(void)
{
  __HAL_RCC_AFIO_CLK_ENABLE();
  __HAL_RCC_PWR_CLK_ENABLE();

  __HAL_AFIO_REMAP_SWJ_DISABLE();   /* CubeMX 生成的:一进 HAL_Init 就禁用 SWJ */

  /* USER CODE BEGIN MspInit 1 */
  /* 禁用SWJ后 PA13(SWDIO) 仍被内部上拉,必须立即配置为推挽输出并拉低,
     否则继电器会在 SystemClock_Config 等待 HSE 起振期间误吸合 */
  __HAL_RCC_GPIOA_CLK_ENABLE();
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  GPIO_InitStruct.Pin = GPIO_PIN_13 | GPIO_PIN_14;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_13 | GPIO_PIN_14, GPIO_PIN_RESET);
  /* USER CODE END MspInit 1 */
}

#4.2 修复2:HSE 起振失败回退 HSI

SystemClock_Config() 中,HAL_RCC_OscConfig 失败时不再直接 Error_Handler(),而是回退 HSI 8MHz 继续运行(代码见工程 main.c,结构:失败 → 配 HSI 无 PLL → FLASH_LATENCY_0 → 继续跑)。

#4.3 修复3:主循环按键双采样消抖

继电器吸合瞬间的电磁干扰可能让触摸误触发,主循环对每个按键连续两次采样(间隔 5ms)确认再动作(代码见工程 main.c)。

#五、驱动代码

(Demo00 最终版,2026-08-18 实测:触摸 A/B/C/D 控制继电器1/2 吸合/断开,LED 联动)

#5.1 main.h(引脚/宏定义)

c/* CubeMX 生成区 */
#define RELAY1_Pin GPIO_PIN_14
#define RELAY1_GPIO_Port GPIOA
#define RELAY2_Pin GPIO_PIN_13
#define RELAY2_GPIO_Port GPIOA
#define LED1_Pin GPIO_PIN_0
#define LED1_GPIO_Port GPIOB
#define LED2_Pin GPIO_PIN_1
#define LED2_GPIO_Port GPIOB

/* USER CODE BEGIN Private defines */
typedef enum {
    RELAY_ID_1 = 0,
    RELAY_ID_2
} Relay_ID_t;
/* USER CODE END Private defines */

#5.2 relay.h

c#ifndef RELAY_H
#define RELAY_H

#include "main.h"

void     relay_init(void);
void     relay_on(Relay_ID_t id);
void     relay_off(Relay_ID_t id);
void     relay_toggle(Relay_ID_t id);
uint8_t  relay_get_state(Relay_ID_t id);

#endif /* RELAY_H */

#5.3 relay.c

c#include "relay.h"

static GPIO_TypeDef *relay_port[2] = {RELAY1_GPIO_Port, RELAY2_GPIO_Port};
static const uint16_t relay_pin[2] = {RELAY1_Pin, RELAY2_Pin};

void relay_init(void)
{
    GPIO_InitTypeDef g = {0};

    /* 使能 GPIOA/GPIOB 和 AFIO 时钟 */
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();
    __HAL_RCC_AFIO_CLK_ENABLE();

    /* 1a. 配置继电器引脚 PA13/PA14 推挽输出 */
    g.Mode  = GPIO_MODE_OUTPUT_PP;
    g.Pull  = GPIO_NOPULL;
    g.Speed = GPIO_SPEED_FREQ_HIGH;
    g.Pin   = RELAY1_Pin | RELAY2_Pin;
    HAL_GPIO_Init(GPIOA, &g);

    /* 1b. 配置指示 LED 引脚 PB0/PB1 推挽输出 */
    g.Speed = GPIO_SPEED_FREQ_LOW;
    g.Pin   = LED1_Pin | LED2_Pin;
    HAL_GPIO_Init(GPIOB, &g);

    /* 2. 禁用 JTAG+SWD,释放 PA13/PA14(必须在 GPIO 配置之后!) */
    __HAL_AFIO_REMAP_SWJ_DISABLE();

    /* 3. 拉低初始电平,继电器断开、LED 熄灭 */
    HAL_GPIO_WritePin(RELAY1_GPIO_Port, RELAY1_Pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(RELAY2_GPIO_Port, RELAY2_Pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);
}

void relay_on(Relay_ID_t id)
{
    if (id <= RELAY_ID_2)
        HAL_GPIO_WritePin(relay_port[id], relay_pin[id], GPIO_PIN_SET);
}

void relay_off(Relay_ID_t id)
{
    if (id <= RELAY_ID_2)
        HAL_GPIO_WritePin(relay_port[id], relay_pin[id], GPIO_PIN_RESET);
}

void relay_toggle(Relay_ID_t id)
{
    if (id <= RELAY_ID_2)
        HAL_GPIO_TogglePin(relay_port[id], relay_pin[id]);
}

uint8_t relay_get_state(Relay_ID_t id)
{
    if (id > RELAY_ID_2) return 0;
    return (HAL_GPIO_ReadPin(relay_port[id], relay_pin[id]) == GPIO_PIN_SET) ? 1 : 0;
}

#六、调用示例

(✅ 2026-08-18 最终验证版:触摸 A/B/C/D 控制继电器1/2 + LED1/2,带双采样消抖)

c/* main() 初始化顺序 */
HAL_Init();                 /* 内部 HAL_MspInit 已禁用SWJ并立即拉低PA13/14 */
SystemClock_Config();       /* HSE 8MHz×9=72MHz,失败自动回退HSI */
MX_GPIO_Init();
MX_USART1_UART_Init();
relay_init();               /* 继电器+LED初始化,默认断开/熄灭 */

/* 主循环:触摸按键电平轮询,低有效,双采样消抖 */
while (1)
{
    if (HAL_GPIO_ReadPin(GPIOA, KEY_A_Pin) == GPIO_PIN_RESET)
    {
        HAL_Delay(5);   /* 消抖:间隔5ms后再确认一次 */
        if (HAL_GPIO_ReadPin(GPIOA, KEY_A_Pin) == GPIO_PIN_RESET)
        {
            relay_on(RELAY_ID_1);                                 /* 触摸A → 继电器1吸合 */
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);   /* LED1 亮 */
        }
    }
    if (HAL_GPIO_ReadPin(GPIOA, KEY_B_Pin) == GPIO_PIN_RESET)
    {
        HAL_Delay(5);
        if (HAL_GPIO_ReadPin(GPIOA, KEY_B_Pin) == GPIO_PIN_RESET)
        {
            relay_off(RELAY_ID_1);                                /* 触摸B → 继电器1断开 */
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); /* LED1 灭 */
        }
    }
    /* C → RELAY_ID_2 吸合,D → RELAY_ID_2 断开,同理(均带双采样) */
    HAL_Delay(5);
}

#七、参考库/链接

  • 洋桃1号官方范例(本地):D:\学习资料\【用户资料】洋桃1号开发板(2)\B5-教学视频示例程序\1-2-22-按键控制继电器程序\(标准库实现,Hardware/RELAY/relay.crelay.h
  • ST 官方 GPIO 例程:github.com/STMicroelectronics/STM32CubeF1(GPIO 部分)
  • 引脚复用说明:STM32F103 参考手册 RM0008 第 8 章 GPIO、AFIO 重映射(SWJ_CFG)