1. 本课目标

  • 理解 main() 中 STM32 程序的基本执行顺序。
  • 理解“打开外设时钟 -> 配置 GPIO -> 应用配置 -> 主循环”的固定结构。
  • 使用 PA0 输出高、低电平控制 LED。
  • 区分推挽输出、开漏输出。
  • 理解 GPIO_Pin_0 等宏的位掩码含义。
  • 区分整体写入 GPIO_Write() 和局部修改 GPIO_SetBits() / GPIO_ResetBits()

2. 知识主线

STM32 GPIO 输出

时钟

RCC_APB2PeriphClockCmd

GPIOA 属于 APB2

外设默认需要开启时钟

配置

GPIO_InitTypeDef

GPIO_Mode_Out_PP

GPIO_Pin_0

GPIO_Speed_50MHz

GPIO_Init

输出

GPIO_SetBits

GPIO_ResetBits

GPIO_WriteBit

GPIO_Write

程序结构

main

1

状态 -> 延时 -> 状态

位操作

GPIO_Pin_0 = 0x0001

GPIO_Pin_1 = 0x0002

| 选择多个引脚

工程边界

STM32F103C8

Keil ARMCC 5.06

标准外设库

硬件现象需单独验证

3. 最小 LED 程序

#include "stm32f10x.h"
#include "Delay.h"

int main(void)
{
    // GPIOA 在使用前必须打开 APB2 外设时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    // 用结构体集中保存 GPIO 配置
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;         // PA0
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

    // 将配置写入 GPIOA 的相关寄存器
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    while (1)
    {
        GPIO_ResetBits(GPIOA, GPIO_Pin_0); // PA0 输出低电平
        Delay_ms(500);

        GPIO_SetBits(GPIOA, GPIO_Pin_0);   // PA0 输出高电平
        Delay_ms(500);
    }
}

4. 程序执行流程

复位后进入 main

打开 GPIOA 时钟

填写 GPIO_InitTypeDef

GPIO_Init 应用配置

PA0 输出低电平

Delay_ms 500

PA0 输出高电平

Delay_ms 500

一次完整循环的理论等待时间约为:

500 ms + 500 ms = 1000 ms

实际 LED 的亮灭相位取决于开发板 LED 的电路接法。低电平可能点亮 LED,也可能熄灭 LED,不能只根据 API 名称推断实际亮灭。

5. 四个关键步骤

5.1 打开时钟

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

这是对 RCC 时钟控制器的调用。GPIOA 挂在 APB2 总线上,使用前需要开启对应时钟。漏掉这句时,GPIO 配置通常不会按预期工作。

5.2 配置结构体

GPIO_InitTypeDef GPIO_InitStructure;

它是一张配置表。结构体字段描述 GPIO 的模式、引脚和速度,GPIO_Init() 再根据这些字段配置底层寄存器。

5.3 推挽输出

GPIO_Mode_Out_PP

推挽输出可以主动输出高电平,也可以主动输出低电平,适合 LED 和普通数字控制信号。

开漏输出 GPIO_Mode_Out_OD 只能主动拉低;释放时需要外部上拉电阻形成高电平,常见于 I2C 和共享信号线。不能因为“都是输出模式”就把两者当作等价配置。

5.4 主循环

while (1)

单片机程序通常不会像桌面程序一样执行完后退出,而是在无限循环中持续执行控制逻辑。

6. 位掩码

源码中的宏本质上是不同的二进制位:

#define GPIO_Pin_0 ((uint16_t)0x0001)
#define GPIO_Pin_1 ((uint16_t)0x0002)
#define GPIO_Pin_2 ((uint16_t)0x0004)

因此可以用按位或选择多个引脚:

GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2

这表示同时选择 PA0、PA1、PA2。引脚选择使用 |,不要把“位集合”误写成普通加法。

7. 整体写入与局部修改

标准库实现中:

GPIO_Write(GPIOA, value);       // GPIOA->ODR = value,整体覆盖
GPIO_SetBits(GPIOA, pins);      // GPIOA->BSRR = pins,指定位置 1
GPIO_ResetBits(GPIOA, pins);    // GPIOA->BRR = pins,指定位置 0

选择原则

API影响范围适用场景
GPIO_SetBits指定引脚置 1单独打开 LED
GPIO_ResetBits指定引脚置 0单独关闭 LED
GPIO_WriteBit一个指定引脚根据条件设置单个引脚
GPIO_Write整个 GPIO 端口独占端口的流水灯或整体刷新

如果 GPIOA 的其他引脚连接了别的功能,直接 GPIO_Write() 可能误改它们;此时优先用 SetBitsResetBits 或先明确维护掩码。

8. 延时函数与前提

Delay.c 使用 Cortex-M3 的 SysTick:

SysTick->LOAD = 72 * xus;
SysTick->VAL = 0x00;
SysTick->CTRL = 0x00000005;
while (!(SysTick->CTRL & 0x00010000)) {}

其中 72 * xus 假设系统时钟为 72 MHz。若系统主频改变而延时计算没有同步修改,Delay_ms(500) 不一定仍然是 500 ms。这是本工程的时钟假设,不是所有 STM32 工程的通用常数。

该延时是阻塞式的:等待期间 CPU 主要在等待计数结束,暂时不能高效处理其他任务。复杂项目通常改用定时器中断、SysTick 节拍或 RTOS 调度。

9. 今日练习

练习 A:修改速度

把两处 Delay_ms(500) 改成 Delay_ms(100),预测完整循环约为 200 ms。

练习 B:双引脚交替

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_Init(GPIOA, &GPIO_InitStructure);

while (1)
{
    GPIO_SetBits(GPIOA, GPIO_Pin_0);
    GPIO_ResetBits(GPIOA, GPIO_Pin_1);
    Delay_ms(500);

    GPIO_ResetBits(GPIOA, GPIO_Pin_0);
    GPIO_SetBits(GPIOA, GPIO_Pin_1);
    Delay_ms(500);
}

练习 C:判断影响范围

回答:如果 PA1 当前为高电平,执行下面代码后 PA1 是否一定变低?

GPIO_Write(GPIOA, GPIO_Pin_0);

答案:是。因为 GPIO_Write() 整体写入 ODR,未设置的位会变为 0。

10. 事实、假设与验证边界

  • 已知事实:工程目标设备为 STM32F103C8,使用 Keil ARMCC 5.06 和 STM32 标准外设库;源码配置了 GPIOA 的 PA0 推挽输出。
  • 合理推断:若 LED 电路连接到 PA0 且供电、下载和接线正确,PA0 电平会周期性变化。
  • 尚未确认:本次没有收到编译输出、下载结果或实际 LED 现象,因此不能把硬件闪烁记为已验证事实。
  • 重要约束:LED 高低电平有效逻辑取决于开发板原理图;延时准确性依赖系统时钟为 72 MHz 的假设。

11. 下一课预告

下一章进入 GPIO 输入:按键读取。

知识链将变为:

读取输入 -> 判断按键状态 -> 修改 LED 输出

这会引入输入模式、上拉/下拉、电平有效逻辑和按键消抖。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐