← 返回首页

嵌入式 · STM32

STM32 GPIO 输入与输出

跟着江科大视频学 STM32 的第一阶段笔记:工程搭建、GPIO 结构、八种工作模式,以及看懂标准库所需的 C 语言结构体与枚举,最后是 LED、蜂鸣器、按键、传感器和常用库函数。

八种工作模式 输入 4 种 + 输出 4 种,选错模式是新手最常见的坑
先开时钟 GPIO 挂在 APB2 总线上,用之前必须 RCC 使能
结构体 + 枚举 GPIO_InitTypeDef 和 GPIO_Mode 是看懂库函数的两把钥匙
推挽 vs 开漏 输出模式怎么选,取决于驱动对象和是否需要外接上拉
  1. 开发环境与新建工程
  2. GPIO 基本结构与引脚(含更正)
  3. GPIO 的八种工作模式
  4. C 语言数据类型速查
  5. C 语言结构体与枚举(重点)
  6. LED 和蜂鸣器硬件电路
  7. 按键与传感器输入电路
  8. GPIO 常用库函数
  9. 能直接抄的代码模板
  10. 易错点与排查清单

一、开发环境与新建工程

整个流程是:Keil 建工程 → 放固件库文件 → 分组 → 配置头文件路径和宏 → 配置调试器。

  1. 建立工程文件夹,在 Keil 里新建工程,选择芯片型号(如 STM32F103C8T6)。
  2. 工程文件夹里建立 Start、Library、User、System 等文件夹,把固件库文件复制进来。
  3. 在 Keil 工程里建立同名分组,把对应文件添加进去。
  4. 工程选项 → C/C++ → Include Paths,添加所有存放头文件的文件夹。
  5. 工程选项 → C/C++ → Define,添加 USE_STDPERIPH_DRIVER。
  6. 工程选项 → Debug,选择调试器(ST-Link),Settings → Flash Download 勾选 Reset and Run。
STM32Project/ ├── Start/ 启动文件、内核文件 ├── Library/ 标准外设库 stm32f10x_xxx.c / .h ├── User/ main.c、stm32f10x_conf.h、stm32f10x_it.c └── System/ Delay 等自己写的模块
Reset and Run:下载完程序后自动复位并运行,不用每次手动按开发板上的复位键。
两个最容易漏的配置:少了 Include Paths 会提示找不到头文件;少了 USE_STDPERIPH_DRIVER,标准外设库的代码不会正常生效,会出现一堆莫名的报错。

二、GPIO 基本结构与引脚

GPIO 挂在 APB2 总线上。CPU 通过寄存器控制端口,再经过驱动器把电平送到引脚:

CPU / 内核 → APB2 总线 → GPIOA / GPIOB / GPIOC → 寄存器 → 驱动器 → PA0~PA15…

STM32F103C8T6 到底有哪些 GPIO

之前写“只有 GPIOA、GPIOB”是不准确的,C8T6 是有 GPIOC 的,只是引脚不全:

端口 可用引脚 说明
GPIOAPA0 ~ PA15完整 16 个引脚
GPIOBPB0 ~ PB15完整 16 个引脚
GPIOCPC13 ~ PC15只引出这 3 个;最小系统板上 PC13 通常接 LED,低电平点亮
GPIODPD0 ~ PD1与外部晶振引脚复用,板上焊了晶振时一般不能当普通 IO 用
更正:STM32F103C8T6(LQFP48 封装)不是只有 GPIOA、GPIOB,而是 GPIOA、GPIOB 完整可用,GPIOC 引出 PC13~PC15,另外还有与晶振复用的 PD0、PD1。想要完整的 GPIOC,需要换引脚更多的封装型号。

寄存器视角

平时用库函数就够了,但知道寄存器在管什么,遇到问题时更容易排查:

寄存器 作用
GPIOx_CRL配置第 0~7 号引脚,每个引脚占 4 位
GPIOx_CRH配置第 8~15 号引脚
GPIOx_IDR输入数据寄存器,读引脚电平
GPIOx_ODR输出数据寄存器,写引脚电平
GPIOx_BSRR / BRR原子地置位 / 复位,库函数 SetBits、ResetBits 最终操作的就是它
JTAG 引脚冲突:PA13、PA14、PA15、PB3、PB4 默认给调试口(JTAG/SWD)用。想拿它们当普通 IO,需要重映射并关闭 JTAG(保留 SWD),否则配置了也没反应。

三、GPIO 的八种工作模式

标准库把模式封装成了枚举,一共 8 种:

模式名称 性质 特征
浮空输入数字输入可读引脚电平;引脚悬空时电平不确定
上拉输入数字输入内部接上拉电阻,悬空时默认高电平
下拉输入数字输入内部接下拉电阻,悬空时默认低电平
模拟输入模拟输入GPIO 数字功能关闭,引脚直接接入内部 ADC
开漏输出数字输出高电平为高阻态,低电平接 VSS(GND)
推挽输出数字输出高电平接 VDD,低电平接 VSS,驱动能力强
复用开漏输出数字输出由片上外设控制,高电平为高阻态
复用推挽输出数字输出由片上外设控制,高电平接 VDD

输入模式怎么选

输出模式怎么选

场景速查

要做的功能 推荐模式
点亮 LED、驱动蜂鸣器推挽输出
读按键上拉输入或下拉输入
I2C 通信复用开漏输出(外接上拉电阻)
串口、SPI、PWM复用推挽输出
接传感器 AO、读模拟电压模拟输入
记忆口诀:输入看上下拉,输出看推挽开漏,外设用复用,ADC 用模拟。

四、C 语言数据类型速查

固件库里到处是 uint8_t、u16 这类写法,对照下面这张表就能看懂:

关键字 位数 表示范围 stdint 写法 ST 写法
char8-128 ~ 127int8_ts8
unsigned char80 ~ 255uint8_tu8
short16-32768 ~ 32767int16_ts16
unsigned short160 ~ 65535uint16_tu16
int32-2147483648 ~ 2147483647int32_ts32
unsigned int320 ~ 4294967295uint32_tu32
long long64约 ±9.2e18int64_t—
unsigned long long640 ~ 约 1.8e19uint64_t—
float32±3.4e38——
double64±1.7e308——

写 STM32 代码推荐用 uint8_t / uint16_t / uint32_t:位数固定,换平台也不用改。比如以后会看到的 uint16_t GPIO_Pin,就是因为一个端口有 16 个引脚,用 16 位来存。

五、C 语言结构体与枚举(重点)

标准库把“一堆配置”打包成结构体,把“只能取某几个值”的选项做成枚举。看懂这两样,再看 GPIO_InitTypeDef 就不陌生了。

1. 结构体 struct:把不同类型的数据打成一个包

结构体的关键字是 struct,用途是数据打包,把不同类型的变量放在一起。

直接定义结构体变量
#include <stdio.h> int main(void) { /* 定义一个结构体变量 c,它里面有三个成员 */ struct { char x; int y; float z; } c; c.x = 'A'; c.y = 66; c.z = 1.23; printf("c.x = %c\n", c.x); printf("c.y = %d\n", c.y); printf("c.z = %f\n", c.z); return 0; }

用 . 访问结构体成员,例如 c.x、c.y。这种写法每用一次都要写一遍完整定义,比较啰嗦,所以实际代码里更常用 typedef:

typedef struct(最常用的写法)
/* 给结构体类型起一个短名字 StructName_t */ typedef struct { char x; int y; float z; } StructName_t; int main(void) { StructName_t c; /* 以后直接用它定义变量 */ StructName_t d; c.x = 'A'; c.y = 66; c.z = 1.23; return 0; }

2. 结构体指针:用 -> 访问成员

如果拿到的是结构体的地址(指针),就用 -> 访问成员;当然也可以先用 * 取出内容,再用 . 访问,两种写法等价。

StructName_t c; StructName_t *p = &c; /* p 保存 c 的地址 */ p->x = 'A'; /* 指针写法:箭头 */ (*p).y = 66; /* 等价写法:先取内容,再用点 */ p->z = 1.23;

库函数 GPIO_Init 的参数就是结构体指针,所以调用时要写成 &GPIO_InitStructure,把地址传进去。

3. 枚举 enum:把取值限制在几个选项里

枚举的关键字是 enum,用途是定义一个取值受限制的整型变量,也可以理解成一组有名字的宏。它让代码更可读,也避免传错值。

/* 定义一个枚举变量 EnumName,只能取 FALSE 或 TRUE */ enum { FALSE = 0, TRUE = 1 } EnumName; EnumName = FALSE; EnumName = TRUE;

枚举类型名一般比较长,所以通常配合 typedef 起一个短名字:

typedef enum { FALSE = 0, TRUE = 1 } FlagStatus_t; FlagStatus_t flag = FALSE;

4. 库函数里是怎么对应的

GPIO_InitTypeDef 就是一个 typedef struct,里面的成员类型又用到了枚举:

typedef struct { uint16_t GPIO_Pin; /* 选哪个引脚 */ GPIOSpeed_TypeDef GPIO_Speed; /* 输出速度(枚举) */ GPIOMode_TypeDef GPIO_Mode; /* 工作模式(枚举) */ } GPIO_InitTypeDef;

而 GPIO_Init 的第二个参数是结构体指针:

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);

所以在库里读成员时用的是 ->:

/* GPIO_Init 内部会这样判断是不是输出模式 */ if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00) { /* 是输出模式,继续检查 GPIO_Speed */ }
把这条链记住:枚举负责“只能选这几个值” → 结构体把配置打包 → 调用时用 & 传地址 → 库函数内部用指针 -> 读取 → 最后写进寄存器。

另外,GPIO_Mode_Out_PP、GPIO_Mode_IPU 这些都属于 GPIOMode_TypeDef 枚举;GPIO_Speed_50MHz 属于 GPIOSpeed_TypeDef 枚举;GPIO_WriteBit 用到的 Bit_SET / Bit_RESET 属于 BitAction 枚举。

六、LED 和蜂鸣器硬件电路

LED 的两种接法

低电平点亮(灌电流): PA0 ──[ R1 1kΩ ]──►|── 3V3 LED PA0 输出 0 → 导通点亮;输出 1 → 熄灭 高电平点亮(拉电流): PA0 ──[ R1 1kΩ ]──►|── GND LED PA0 输出 1 → 导通点亮;输出 0 → 熄灭

蜂鸣器驱动

两种三极管极性相反:同样的 GPIO_SetBits,在 S8050 电路里是“开”,在 S8550 电路里可能是“关”。写代码前先看原理图。

七、按键与传感器输入电路

按键:上拉 / 下拉输入

上拉输入(按下读到 0): 3V3 ──[ R1 10kΩ ]──┬── PA0 │ [K1] │ GND 下拉输入(按下读到 1): PA0 ──┬──────────────► K1 ── 3V3 │ [R1 10kΩ] │ GND
接法 平时电平 按下电平 推荐模式
按键接 GND,外部 10k 上拉10上拉输入(可用内部上拉省掉电阻)
按键接 3V3,外部 10k 下拉01下拉输入
没有上下拉电阻不确定—不能用,必须补上下拉
输入引脚不能悬空。没有上拉或下拉时,引脚电平会随环境变化乱跳,读出来的值自然也不稳定。

按键抖动与消抖

机械按键在按下和松开的瞬间会有 5~10ms 的抖动,电平快速跳变,程序可能误判成按了很多次。常用做法:

传感器模块(光敏 / 热敏 / 红外)

传感器元件的电阻会随外界模拟量变化,先和定值电阻分压得到模拟电压,再通过 LM393 电压比较器二值化,就得到了数字输出。

传感器模块 STM32 VCC ────────────────── 3V3 GND ────────────────── GND DO ────────────────── PA0(数字输入,读 0 / 1) A0 ────────────────── PA1(模拟输入,接 ADC)

八、GPIO 常用库函数

下面按用途分组,一个函数一行,方便随时查。调用前记得先开 GPIO 的 APB2 时钟。

初始化与复位

函数 作用 参数 / 说明
GPIO_Init(GPIOx, &GPIO_InitStruct) 按结构体配置一个端口的引脚 GPIOx 为 GPIOA / GPIOB / GPIOC;第二个参数传结构体地址
GPIO_DeInit(GPIOx) 把端口复位成默认状态 需要重新配置某个端口前可以先调用
GPIO_StructInit(&GPIO_InitStruct) 给结构体字段填入默认值 默认是全部引脚、50MHz、浮空输入
GPIO_AFIODeInit() 复位 AFIO 外设 涉及引脚复用、重映射时才会用到
GPIO_PinLockConfig(GPIOx, GPIO_Pin) 锁定引脚配置,防止被误改 锁定后需要复位才能重新配置

读取输入

函数 作用 返回值 / 说明
GPIO_ReadInputDataBit(GPIOx, GPIO_Pin) 读一个引脚的电平 返回 0 或 1,读按键最常用
GPIO_ReadInputData(GPIOx) 读整个端口的输入数据 返回 16 位数据,每一位对应一个引脚

读取输出

函数 作用 返回值 / 说明
GPIO_ReadOutputDataBit(GPIOx, GPIO_Pin) 读输出寄存器某一位 读的是“准备输出的值”,不是引脚实际电压
GPIO_ReadOutputData(GPIOx) 读整个输出寄存器 返回 16 位数据

写输出

函数 作用 参数 / 说明
GPIO_SetBits(GPIOx, GPIO_Pin) 引脚置高电平 可一次多个引脚:GPIO_Pin_0 | GPIO_Pin_1
GPIO_ResetBits(GPIOx, GPIO_Pin) 引脚置低电平 和 SetBits 一样可以一次操作多个引脚
GPIO_WriteBit(GPIOx, GPIO_Pin, BitVal) 按位写 0 或 1 BitVal 取 Bit_RESET(0) 或 Bit_SET(1)
GPIO_Write(GPIOx, PortVal) 整个端口一起写 PortVal 是 16 位数,每一位对应一个引脚

复用、重映射与事件输出

函数 作用 参数 / 说明
GPIO_PinRemapConfig(GPIO_Remap, NewState) 把引脚的功能重映射到别的引脚 例如关闭 JTAG、把串口映射到 PB6/PB7
GPIO_EventOutputConfig(GPIO_PortSource, GPIO_PinSource) 配置事件输出引脚 比较少见,了解即可
GPIO_EventOutputCmd(NewState) 使能或关闭事件输出 配合上一个函数使用

GPIO_InitTypeDef 的三个字段

字段 作用 取值 / 说明
GPIO_Pin 选择要配置的引脚 GPIO_Pin_0 ~ GPIO_Pin_15,可用 | 组合多个
GPIO_Speed 设置输出速度 GPIO_Speed_2MHz / 10MHz / 50MHz;输入模式不关心
GPIO_Mode 设置工作模式 GPIO_Mode_AIN / IPU / IPD / Out_PP / Out_OD 等

GPIO_Mode 枚举值

枚举值 数值 对应模式
GPIO_Mode_AIN0x00模拟输入
GPIO_Mode_IN_FLOATING0x04浮空输入
GPIO_Mode_IPD0x28下拉输入
GPIO_Mode_IPU0x48上拉输入
GPIO_Mode_Out_OD0x14开漏输出
GPIO_Mode_Out_PP0x10推挽输出
GPIO_Mode_AF_OD0x1C复用开漏输出
GPIO_Mode_AF_PP0x18复用推挽输出

九、能直接抄的代码模板

LED 闪烁(PA0,推挽输出)
#include "stm32f10x.h" #include "Delay.h" int main(void) { /* 1. 打开 GPIOA 的时钟(APB2) */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* 2. 配置 PA0 为推挽输出 */ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); while (1) { GPIO_SetBits(GPIOA, GPIO_Pin_0); /* 输出高电平 */ Delay_ms(500); GPIO_ResetBits(GPIOA, GPIO_Pin_0); /* 输出低电平 */ Delay_ms(500); } }

如果 LED 是低电平点亮接法,把 SetBits 和 ResetBits 对调即可。

按键读取(PB1 上拉输入,按下为 0)
#include "stm32f10x.h" #include "Delay.h" int main(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; /* 上拉输入 */ GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); while (1) { if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) /* 按下为低电平 */ { Delay_ms(20); /* 软件消抖 */ if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) { /* 确认按下后要执行的代码 */ } } } }
蜂鸣器(PB12,低电平驱动的 S8550)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_ResetBits(GPIOB, GPIO_Pin_12); /* 低电平:S8550 导通,蜂鸣器响 */ Delay_ms(100); GPIO_SetBits(GPIOB, GPIO_Pin_12); /* 高电平:关闭 */

十、易错点与排查清单

最常见的六个坑:忘记开 GPIO 时钟;输入引脚悬空;输出模式选成开漏却没上拉;LED 或蜂鸣器极性搞反;PA13/PA14/PA15/PB3/PB4 没重映射就拿来当普通 IO;结构体变量没取地址就传进 GPIO_Init。
现象 可能原因 怎么查
LED 完全不亮 时钟没开、模式不对、极性反了、接线错 先用万用表量引脚电压,再检查模式和电路
按键读数乱跳 引脚悬空、没上拉/下拉、没消抖 确认模式是 IPU 或 IPD,加 10~20ms 延时再读
蜂鸣器一直响或不响 三极管类型和电平极性不匹配 看原理图确认是 PNP 还是 NPN,再调整 Set/Reset
配置了引脚但没反应 用到了 JTAG/SWD 默认占用的引脚 先关闭 JTAG 或做重映射,保留 SWD 下载
编译报错找不到函数 没有加 USE_STDPERIPH_DRIVER 或头文件路径 检查 Define 和 Include Paths

这一阶段要掌握的内容

← 返回首页