2. ADC采样原理与关键指标

各位同学,欢迎来到第二章。上一章我们搭好了开发环境,这一章咱们直接上手硬件操作——GPIO。说白了,GPIO就是单片机跟外界打交道的最基本通道。你想想看,一个单片机要控制LED、读取按键、驱动传感器,哪个离得开GPIO?

我个人习惯,学任何外设都先从寄存器入手。为什么?因为只有理解了寄存器,你才能真正明白芯片在干什么。HAL库固然方便,但出了问题你连debug都不知道从哪下手。好,咱们开始。

2.1 GPIO是什么?

GPIO全称General Purpose Input Output,通用输入输出口。每个GPIO引脚可以独立配置为输入或输出。STM32的GPIO比51单片机复杂得多,但功能也强大得多。

STM32F103系列每个GPIO端口有16个引脚(PA0-PA15、PB0-PB15等),每个引脚可以配置为多种模式。我刚开始用STM32时,看到那么多模式头都大了,后来发现其实就两大类:输入模式和输出模式。

模式类别 具体模式 典型用途
输入模式 浮空输入、上拉输入、下拉输入、模拟输入 读取按键、ADC采样
输出模式 推挽输出、开漏输出、复用推挽、复用开漏 驱动LED、控制外设

2.2 推挽输出与开漏输出的区别

这两个概念很多初学者搞混。我当年也踩过坑,有一次用开漏输出驱动LED,死活不亮,查了半天才发现是模式配错了。

推挽输出(Push-Pull):内部有两个MOS管,一个负责推(输出高电平),一个负责挽(输出低电平)。输出高电平时,上管导通,引脚直接接到VCC;输出低电平时,下管导通,引脚直接接到GND。说白了,推挽输出能主动输出高低电平,驱动能力强。

开漏输出(Open-Drain):只有下管,没有上管。输出低电平时,下管导通,引脚接地;输出高电平时,下管截止,引脚处于高阻态。所以开漏输出不能主动输出高电平,必须外接上拉电阻。

核心区别一句话总结:推挽输出能主动输出高低电平;开漏输出只能主动输出低电平,高电平靠外部上拉。

对比项 推挽输出 开漏输出
输出高电平 直接输出VCC 需外接上拉电阻
输出低电平 直接输出GND 直接输出GND
驱动能力 强 弱(取决于上拉电阻)
多引脚线与 不支持 支持
典型应用 驱动LED、普通IO I2C总线、电平转换

我的经验:驱动LED用推挽输出就够了。开漏输出主要用于I2C这类需要线与功能的总线,或者不同电压域之间的电平转换。

2.3 GPIO寄存器详解

STM32每个GPIO端口有7个寄存器,但常用的就4个。咱们以点亮LED为例,只需要用到CRL/CRH(配置模式)和ODR(输出数据)。

寄存器 名称 作用
CRL 端口配置低寄存器 配置引脚0-7的模式
CRH 端口配置高寄存器 配置引脚8-15的模式
IDR 端口输入数据寄存器 读取引脚电平
ODR 端口输出数据寄存器 设置引脚输出电平
BSRR 端口位设置/清除寄存器 原子操作设置/清除位
BRR 端口位清除寄存器 原子操作清除位
LCKR 端口配置锁定寄存器 锁定引脚配置

每个引脚在CRL或CRH中占用4位,其中CNF[1:0]配置模式,MODE[1:0]配置速度。具体配置如下:

MODE[1:0] 模式 说明
00 输入模式 复位后的默认状态
01 输出模式,10MHz 低速
10 输出模式,2MHz 中速
11 输出模式,50MHz 高速
CNF[1:0](输出模式) 配置
00 通用推挽输出
01 通用开漏输出
10 复用推挽输出
11 复用开漏输出

2.4 实战:使用寄存器点亮LED

好,理论讲完了,咱们直接上手。假设LED接在PB0引脚,低电平点亮。为什么用低电平?因为STM32的IO口驱动能力有限,高电平驱动LED电流不够,用低电平灌电流方式更可靠。这是我做第一个项目时学到的教训。

步骤很简单:

  1. 使能GPIOB时钟
  2. 配置PB0为推挽输出,50MHz
  3. 设置PB0输出低电平

先看时钟使能。STM32外设时钟默认是关闭的,需要手动开启。GPIOB挂在APB2总线上,对应的时钟使能位在RCC_APB2ENR寄存器的第3位。

// 使能GPIOB时钟
RCC->APB2ENR |= (1 << 3);   // 设置第3位为1

然后配置PB0的模式。PB0属于低8位,用CRL寄存器配置。PB0对应CRL的bit[3:0],其中bit[1:0]是MODE,bit[3:2]是CNF。

// 配置PB0为推挽输出,50MHz
GPIOB->CRL &= ~(0xF << 0);  // 先清零bit[3:0]
GPIOB->CRL |= (0x3 << 0);   // MODE=11(50MHz), CNF=00(推挽输出)

最后设置ODR寄存器,让PB0输出低电平:

// PB0输出低电平,点亮LED
GPIOB->ODR &= ~(1 << 0);   // 清零bit0

完整代码:

#include "stm32f10x.h"

int main(void)
{
    // 1. 使能GPIOB时钟
    RCC->APB2ENR |= (1 << 3);
    
    // 2. 配置PB0为推挽输出,50MHz
    GPIOB->CRL &= ~(0xF << 0);
    GPIOB->CRL |= (0x3 << 0);
    
    // 3. PB0输出低电平,点亮LED
    GPIOB->ODR &= ~(1 << 0);
    
    while(1)
    {
        // 程序停在这里,LED一直亮
    }
}

注意:操作寄存器时,一定要先清零再赋值。不清零直接赋值会导致配置错误。我曾经因为这个问题,查了整整一个下午的bug。

2.5 使用BSRR寄存器优化

上面用ODR寄存器设置电平有个问题:如果多个线程或中断同时操作ODR,可能会产生读-改-写冲突。BSRR寄存器可以解决这个问题,它支持原子操作。

// 使用BSRR设置PB0输出低电平(原子操作)
GPIOB->BSRR = (1 << (0 + 16));  // BR0位,低16位是设置,高16位是清除

BSRR的低16位用于设置对应引脚为高电平,高16位用于设置对应引脚为低电平。写1有效,写0不影响。这样就不需要先读再写了。

我的习惯:在实际项目中,我一般用BSRR操作输出,用ODR读取当前状态。BSRR的原子特性在RTOS环境下特别有用。

2.6 本章知识体系

下面这张图总结了GPIO操作的核心流程,从时钟使能到寄存器配置,再到输出控制,每一步都离不开对寄存器的理解。

GPIO寄存器操作核心流程 1. 使能GPIO时钟 2. 配置CRL/CRH寄存器 3. 选择推挽/开漏 + 速度 4. 设置ODR/BSRR 5. LED点亮/熄灭 关键寄存器 RCC->APB2ENR GPIOx->CRL / CRH GPIOx->ODR / BSRR 常见错误 忘记使能时钟 配置前未清零 模式选错(推挽/开漏)

2.7 避坑指南

我曾经踩过的坑:

  • 忘记使能时钟:配置了寄存器但没开时钟,引脚死活没反应。这是新手最常犯的错误,没有之一。
  • 配置前不清零:直接赋值导致其他引脚配置被覆盖。记住:先清零,再赋值。
  • 模式选错:用开漏输出驱动LED,结果LED微亮或不亮。驱动LED用推挽输出。
  • 速度配置过高:50MHz模式功耗大、噪声大。驱动LED用10MHz就够了。