跟着江科大视频学 STM32 的第一阶段笔记:工程搭建、GPIO 结构、八种工作模式,以及看懂标准库所需的 C 语言结构体与枚举,最后是 LED、蜂鸣器、按键、传感器和常用库函数。
整个流程是:Keil 建工程 → 放固件库文件 → 分组 → 配置头文件路径和宏 → 配置调试器。
Start、Library、User、System 等文件夹,把固件库文件复制进来。USE_STDPERIPH_DRIVER。Reset and Run。Include Paths 会提示找不到头文件;少了 USE_STDPERIPH_DRIVER,标准外设库的代码不会正常生效,会出现一堆莫名的报错。
GPIO 挂在 APB2 总线上。CPU 通过寄存器控制端口,再经过驱动器把电平送到引脚:
之前写“只有 GPIOA、GPIOB”是不准确的,C8T6 是有 GPIOC 的,只是引脚不全:
| 端口 | 可用引脚 | 说明 |
|---|---|---|
| GPIOA | PA0 ~ PA15 | 完整 16 个引脚 |
| GPIOB | PB0 ~ PB15 | 完整 16 个引脚 |
| GPIOC | PC13 ~ PC15 | 只引出这 3 个;最小系统板上 PC13 通常接 LED,低电平点亮 |
| GPIOD | PD0 ~ PD1 | 与外部晶振引脚复用,板上焊了晶振时一般不能当普通 IO 用 |
平时用库函数就够了,但知道寄存器在管什么,遇到问题时更容易排查:
| 寄存器 | 作用 |
|---|---|
| GPIOx_CRL | 配置第 0~7 号引脚,每个引脚占 4 位 |
| GPIOx_CRH | 配置第 8~15 号引脚 |
| GPIOx_IDR | 输入数据寄存器,读引脚电平 |
| GPIOx_ODR | 输出数据寄存器,写引脚电平 |
| GPIOx_BSRR / BRR | 原子地置位 / 复位,库函数 SetBits、ResetBits 最终操作的就是它 |
标准库把模式封装成了枚举,一共 8 种:
| 模式名称 | 性质 | 特征 |
|---|---|---|
| 浮空输入 | 数字输入 | 可读引脚电平;引脚悬空时电平不确定 |
| 上拉输入 | 数字输入 | 内部接上拉电阻,悬空时默认高电平 |
| 下拉输入 | 数字输入 | 内部接下拉电阻,悬空时默认低电平 |
| 模拟输入 | 模拟输入 | GPIO 数字功能关闭,引脚直接接入内部 ADC |
| 开漏输出 | 数字输出 | 高电平为高阻态,低电平接 VSS(GND) |
| 推挽输出 | 数字输出 | 高电平接 VDD,低电平接 VSS,驱动能力强 |
| 复用开漏输出 | 数字输出 | 由片上外设控制,高电平为高阻态 |
| 复用推挽输出 | 数字输出 | 由片上外设控制,高电平接 VDD |
| 要做的功能 | 推荐模式 |
|---|---|
| 点亮 LED、驱动蜂鸣器 | 推挽输出 |
| 读按键 | 上拉输入或下拉输入 |
| I2C 通信 | 复用开漏输出(外接上拉电阻) |
| 串口、SPI、PWM | 复用推挽输出 |
| 接传感器 AO、读模拟电压 | 模拟输入 |
固件库里到处是 uint8_t、u16 这类写法,对照下面这张表就能看懂:
| 关键字 | 位数 | 表示范围 | stdint 写法 | ST 写法 |
|---|---|---|---|---|
| char | 8 | -128 ~ 127 | int8_t | s8 |
| unsigned char | 8 | 0 ~ 255 | uint8_t | u8 |
| short | 16 | -32768 ~ 32767 | int16_t | s16 |
| unsigned short | 16 | 0 ~ 65535 | uint16_t | u16 |
| int | 32 | -2147483648 ~ 2147483647 | int32_t | s32 |
| unsigned int | 32 | 0 ~ 4294967295 | uint32_t | u32 |
| long long | 64 | 约 ±9.2e18 | int64_t | — |
| unsigned long long | 64 | 0 ~ 约 1.8e19 | uint64_t | — |
| float | 32 | ±3.4e38 | — | — |
| double | 64 | ±1.7e308 | — | — |
写 STM32 代码推荐用 uint8_t / uint16_t / uint32_t:位数固定,换平台也不用改。比如以后会看到的 uint16_t GPIO_Pin,就是因为一个端口有 16 个引脚,用 16 位来存。
标准库把“一堆配置”打包成结构体,把“只能取某几个值”的选项做成枚举。看懂这两样,再看 GPIO_InitTypeDef 就不陌生了。
结构体的关键字是 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:
/* 给结构体类型起一个短名字 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;
}如果拿到的是结构体的地址(指针),就用 -> 访问成员;当然也可以先用 * 取出内容,再用 . 访问,两种写法等价。
StructName_t c;
StructName_t *p = &c; /* p 保存 c 的地址 */
p->x = 'A'; /* 指针写法:箭头 */
(*p).y = 66; /* 等价写法:先取内容,再用点 */
p->z = 1.23;库函数 GPIO_Init 的参数就是结构体指针,所以调用时要写成 &GPIO_InitStructure,把地址传进去。
枚举的关键字是 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;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 枚举。
R = (3.3V - LED 压降) / 目标电流,普通红色 LED 压降约 1.8~2V。用 1kΩ 时电流约 1.3mA,亮度偏低;想更亮可以用 330Ω 左右。GPIO_ResetBits(GPIOC, GPIO_Pin_13) 反而是点亮。GPIO_SetBits,在 S8050 电路里是“开”,在 S8550 电路里可能是“关”。写代码前先看原理图。
| 接法 | 平时电平 | 按下电平 | 推荐模式 |
|---|---|---|---|
| 按键接 GND,外部 10k 上拉 | 1 | 0 | 上拉输入(可用内部上拉省掉电阻) |
| 按键接 3V3,外部 10k 下拉 | 0 | 1 | 下拉输入 |
| 没有上下拉电阻 | 不确定 | — | 不能用,必须补上下拉 |
机械按键在按下和松开的瞬间会有 5~10ms 的抖动,电平快速跳变,程序可能误判成按了很多次。常用做法:
传感器元件的电阻会随外界模拟量变化,先和定值电阻分压得到模拟电压,再通过 LM393 电压比较器二值化,就得到了数字输出。
VCC、GND、DO(数字输出)、A0(模拟输出)。DO 接普通 GPIO,按数字输入读取,输出只有 0 或 1。A0 接 ADC 引脚,可以读到具体的模拟电压值。下面按用途分组,一个函数一行,方便随时查。调用前记得先开 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_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_AIN | 0x00 | 模拟输入 |
| GPIO_Mode_IN_FLOATING | 0x04 | 浮空输入 |
| GPIO_Mode_IPD | 0x28 | 下拉输入 |
| GPIO_Mode_IPU | 0x48 | 上拉输入 |
| GPIO_Mode_Out_OD | 0x14 | 开漏输出 |
| GPIO_Mode_Out_PP | 0x10 | 推挽输出 |
| GPIO_Mode_AF_OD | 0x1C | 复用开漏输出 |
| GPIO_Mode_AF_PP | 0x18 | 复用推挽输出 |
#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 对调即可。
#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)
{
/* 确认按下后要执行的代码 */
}
}
}
}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); /* 高电平:关闭 */| 现象 | 可能原因 | 怎么查 |
|---|---|---|
| LED 完全不亮 | 时钟没开、模式不对、极性反了、接线错 | 先用万用表量引脚电压,再检查模式和电路 |
| 按键读数乱跳 | 引脚悬空、没上拉/下拉、没消抖 | 确认模式是 IPU 或 IPD,加 10~20ms 延时再读 |
| 蜂鸣器一直响或不响 | 三极管类型和电平极性不匹配 | 看原理图确认是 PNP 还是 NPN,再调整 Set/Reset |
| 配置了引脚但没反应 | 用到了 JTAG/SWD 默认占用的引脚 | 先关闭 JTAG 或做重映射,保留 SWD 下载 |
| 编译报错找不到函数 | 没有加 USE_STDPERIPH_DRIVER 或头文件路径 | 检查 Define 和 Include Paths |
GPIO_InitTypeDef 结构体和 GPIO_Mode 枚举,知道为什么要取地址、为什么用 ->。