stm32/gpio-out/relay-uln2003.md
继电器(ULN2003 驱动,2 路)
继电器(ULN2003 驱动,2 路)
#一、元信息
| 字段 | 值 |
|---|---|
| 芯片/平台 | STM32F103C8T6(HAL) |
| 接口类型 | GPIO 推挽输出 |
| 验证状态 | ✅ 已验证 |
| 验证环境 | Demo00 工程 / Keil MDK(AC5)/ 2026-08-18 |
| 驱动方式 | HAL |
#二、接线表
| 信号 | MCU 引脚 | 模块引脚 | 备注 |
|---|---|---|---|
| 继电器1 | PA14 | ULN2003 IN1 → 继电器线圈 | 高电平吸合(洋桃1号官方定义) |
| 继电器2 | PA13 | ULN2003 IN2 → 继电器线圈 | 高电平吸合(洋桃1号官方定义) |
| 指示LED1 | PB0 | 板载LED | 与继电器1联动(本工程附加) |
| 指示LED2 | PB1 | 板载LED | 与继电器2联动(本工程附加) |
⚠️ 引脚号极易写反:洋桃1号官方定义是 继电器1 = PA14,继电器2 = PA13,不是 PA13/PA14!写反表现为"继电器2 上电就吸合"或"触摸 A 控制的是继电器2"。
#三、踩坑清单
- 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 配置之后,不存在此窗口。
- HSE 起振失败会死锁,必须兜底:
SystemClock_Config()若 HSE 起振失败直接Error_Handler(),程序死锁 → 继电器保持最后状态、按键失效。解法:HSE 失败时回退 HSI 8MHz 继续运行(见"关键修复"),保证继电器始终可控。 - 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/2 与 PA13/PA14 的对应关系:官方是
RELAY1=GPIO_Pin_14、RELAY2=GPIO_Pin_13。初始代码定义反了,导致"继电器2上电就开"的假象(其实是 PA14 被当成了 2 号)。 - 禁用 SWJ 后 ST-Link/SWD 失效:PA13/PA14 不再有调试功能,之后只能用 CH340 串口(Boot0 拉高)下载 HEX;要恢复调试需按住 NRST 连接 ST-Link 并全片擦除。
- ULN2003 是反相达林顿:IN 高电平 → OUT 拉低,若继电器接在 OUT 与 VCC 之间则"IN 高 = 线圈通电"。带光耦/指示灯的模块还要注意极性。
- printf 卡死排查:程序里用了
printf而 Keil 未勾选 MicroLIB 时,独立运行(无调试器)会走 semihosting 触发 BKPT 卡死,主循环永远不执行,表现为"继电器/触摸全没反应"。排查外设"不响应"先确认主循环真的在跑(可先用纯 GPIO 点亮 LED 验证)。 - 烧录旧 HEX 的坑:改完代码必须重新 F7 编译,确认 Build Output 0 Error、
Demo00.hex时间戳/大小有更新再烧。Keil 增量编译可能只更新部分 .o,建议改多文件时全量 rebuild。 - 先用 LED 代替继电器验证控制链路:遇到继电器"不响应"时,先把输出换到普通 GPIO + LED,点亮 LED 验证 MCU 侧逻辑 OK,再切回继电器排查硬件(触点、供电、ULN2003 输入端)。
- 供电不足会放大一切问题:继电器线圈吸合需几十 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.c、relay.h) - ST 官方 GPIO 例程:github.com/STMicroelectronics/STM32CubeF1(GPIO 部分)
- 引脚复用说明:STM32F103 参考手册 RM0008 第 8 章 GPIO、AFIO 重映射(SWJ_CFG)