【江科大STM32学习笔记-03】GPIO通用输入输出口

1 STM32 GPIO 简介

GPIO(General Purpose Input/Output)即通用输入输出端口,是微控制器与外部设备进行数字信号交互的基本接口单元。STM32的每个GPIO引脚都可以独立编程配置为多种工作模式,提供灵活的设备控制能力。

STM32的GPIO引脚采用分组管理方式,每组包含16个引脚。以STM32F103C8T6为例,共有4组完整的GPIO端口,分别是 GPIOA、GPIOB、GPIOC、GPIOD。所有GPIO引脚都支持基本的输入输出功能。

最基本的输出功能是由STM32控制引脚输出高、低电平,实现开关控制,如把GPIO引脚接入到LED灯,那就可以控制LED灯的亮灭。 最基本的输入功能是检测外部输入电平,如把GPIO引脚连接到按键,通过电平高低区分按键是否被按下。这类接口在STM32芯片引脚资源中占据主要部分,为设备控制提供了灵活便捷的硬件支持。

电气特性:

  • 工作电压:标准0V-3.3V电平范围
  • 5V容忍:部分引脚标记为"FT"(5V Tolerant),可安全接收5V输入信号
  • 驱动能力:单引脚最大输出电流约20mA(具体参考数据手册)

2 GPIO硬件结构剖析

通过GPIO硬件结构框图,可以深入了解其内部工作原理。下图展示了GPIO从外部引脚到内部逻辑的完整信号路径:该图从最右端看起,最右端就是代表STM32芯片引出的外部引脚,其余部件都位于芯片内部。

下面我们按图中的编号对GPIO端口的结构部件进行说明。

2.1 保护二极管与上/下拉电阻

芯片引脚配备了两个保护二极管,用于防止外部过高或过低电压输入,避免异常电压损坏芯片。

  • 当引脚电压高于VDD时,上方的二极管导通,将电压钳位至VDD
  • 当引脚电压低于VSS时,下方的二极管导通,将电压钳位至VSS

注意事项: 虽然具备保护功能,但STM32引脚不能直接驱动大功率器件。驱动电机等大功率设备时,必须增加功率放大和隔离电路。

2.2 双MOS管输出级(推挽/开漏核心)

GPIO引脚信号经过保护二极管后分为两路:一路向上进入输入模式电路 ,另一路向下进入输出模式电路。输出模式部分由P-MOS和N-MOS管组成,支持"推挽输出"和"开漏输出"两种工作模式。

2.2.1 推挽输出模式

2.2.1.1 模式定义

推挽输出模式是GPIO最常用的输出配置,它利用一对P-MOS和N-MOS管协同工作,能够主动、有力地输出高电平和低电平。其名称形象地描述了这两个MOS管像两个人推挽一个开关一样协同工作。

2.2.1.2 硬件工作原理

在推挽输出模式下,两个MOS管受输出数据的互补信号控制,始终处于相反的工作状态,实现强驱动的输出能力。

软件输出值 P-MOS状态 N-MOS状态 引脚状态 电平表现
1(高电平) 导通(上拉) 截止(断开) 强上拉 稳定高电平(3.3V)
0(低电平) 截止(断开) 导通(下拉) 强下拉 稳定低电平(0V)
2.2.1.3 工作特性详解
2.2.1.3.1 互补驱动机制
  • 输出高电平:P-MOS导通,将引脚连接到VDD(3.3V);N-MOS截止,断开与GND的连接
  • 输出低电平:N-MOS导通,将引脚连接到GND(0V);P-MOS截止,断开与VDD的连接
  • 切换过程:电平转换时两管交替导通,避免同时导通造成的短路和过大电流
2.2.1.3.2 电流驱动能力
  • 拉电流:输出高电平时,电流从芯片通过P-MOS流向负载(芯片提供电流)
  • 灌电流:输出低电平时,电流从负载通过N-MOS流入芯片(芯片吸收电流)
  • 驱动能力:单个引脚通常可提供/吸收最高20mA电流(具体参考数据手册)
2.2.1.3.3 电压特性
  • 高电平:接近VDD电压(典型3.3V,压降很小)
  • 低电平:接近VSS电压(典型0V,压降很小)
  • 输出阻抗:导通时阻抗很低,确保电压稳定性

具体参考如下电路,它是推挽输出模式时的等效电路。

2.2.2 开漏输出模式

2.2.2.1 模式定义

开漏输出模式是一种特殊的输出配置,其核心特征是只能主动输出低电平,无法主动输出高电平。当需要输出高电平时,引脚会进入高阻态,必须依赖外部上拉电阻才能实现高电平输出。

2.2.2.2 硬件工作原理

在GPIO配置为开漏模式时,芯片内部电路会通过硬件逻辑强制禁用P-MOS管的控制信号,使其始终保持关闭状态,不受软件输出值的影响。

具体工作状态如下:

软件输出值 P-MOS状态 N-MOS状态 引脚状态 电平表现
0 强制关闭(截止) 导通 强下拉 稳定低电平(0V)
1 强制关闭(截止) 截止 高阻态 电平由外部电路决定
2.2.2.3 关键特性与要求:
2.2.2.3.1 必须外接上拉电阻

由于开漏输出无法主动输出高电平,在需要输出高电平的应用中必须外接上拉电阻(通常4.7kΩ-10kΩ)。上拉电阻的连接方式决定了最终的高电平电压值:

  • 上拉到3.3V:输出高电平时为3.3V
  • 上拉到5V:输出高电平时为5V(实现电平转换)
2.2.2.3.2 支持"线与"逻辑

多个开漏输出的引脚可以直接并联在同一总线上,实现硬件"线与"逻辑:

  • 所有引脚输出高阻态:总线上拉电阻提供高电平
  • 任一引脚输出低电平:总线被强制拉低至0V
  • 典型应用:I2C、SMBus、One-Wire等共享总线协议
2.2.2.3.3 电平转换能力

通过外接不同电压的上拉电源,可以实现MCU(3.3V)与更高电压设备(如5V系统)之间的安全通信,避免电平不匹配问题。

具体参考如下电路,它是开漏输出模式时的等效电路。

2.2.3 应用场景

推挽输出:需要快速切换0V~3.3V电平的场合(STM32默认推荐);

开漏输出:

  • I2C/SMBUS等需要"线与"功能的总线电路中;
  • 电平转换场合(如外接5V上拉电源输出5V电平,详见STM32_IO_5V输出图)

2.3 输出数据寄存器

上一节介绍的双 MOS 管结构电路,其输入信号就是由 端口输出数据寄存器 (GPIOx_ODR) 控制。软件只需向GPIOx_ODR的相应位写入0或1,即可直接控制对应引脚输出低电平或高电平。

cpp 复制代码
// 示例:设置GPIOB所有16个IO为高电平
GPIOB->ODR = 0xFFFF;

**位设置/清除寄存器(GPIOx_BSRR)**通过操作ODR的特定位来间接改变GPIO输出状态。这种方式比直接操作ODR更高效安全,避免了读-改-写操作可能导致的竞争风险。STM32专门提供BSRR寄存器来实现原子性的位操作,确保在多任务或中断环境下也能安全地修改单个引脚状态。


2.4 复用功能输出

复用功能输出是GPIO引脚工作模式中的重要概念。此处的"复用"特指STM32内部的其他外设模块可以接管GPIO引脚的控制权。当引脚配置为该模式时,其不再扮演通用输入/输出的角色,而是作为特定外设的专用功能引脚来使用,这实现了对芯片有限引脚资源的二次利用与高效管理。

从硬件结构上看,通往引脚输出驱动电路(即双MOS管结构)的信号来源有两个路径。第一条路径来自GPIO本身的数据寄存器(ODR)或位操作寄存器(BSRR),即软件直接控制。第二条路径则来自芯片内部的其他外设模块(如USART、SPI、定时器等)。这两路信号通过一个内部的选择开关进行切换。当引脚被配置为复用功能输出时,这个硬件开关会选择外设信号通路,从而使外设模块能够直接控制引脚的电平输出。如图所示:

例如,当将某个引脚配置为USART的发送引脚(TX)时,该引脚的输出控制权便完全移交给了USART外设。USART模块会根据待发送的数据,自动、实时地在引脚上产生对应的串行时序波形,而不需要CPU通过读写GPIO的ODR寄存器来模拟时序。这种硬件级别的接管极大地提高了通信的可靠性和效率,也减轻了CPU的负担。


2.5 输入数据寄存器

GPIO引脚的输入信号在芯片内部经过一系列处理,最终可由软件或外设读取。信号通路依次经过以下关键环节:

  • 可选上拉/下拉电阻:引脚输入信号首先经过可编程配置的内部上拉或下拉电阻。此电阻可在引脚外部悬空时,为其提供一个确定的高电平(上拉)或低电平(下拉),避免因电平不确定而产生的误判。
  • 施密特触发器:信号随后进入施密特触发器。该电路具有迟滞特性,能将缓慢变化或带有噪声的模拟电平整形为干净、陡峭的数字信号(逻辑0或1),从而显著增强输入信号的抗干扰能力和噪声容限。
  • 输入数据寄存器(GPIOx_IDR):经过整形后的数字信号被锁存至输入数据寄存器中。软件通过读取该寄存器(通常为16位,对应一个端口的0-15引脚),即可获取对应引脚当前的逻辑电平状态。例如:
cpp 复制代码
uint16_t pinStatus = GPIOB->IDR; // 读取GPIOB端口所有引脚的当前电平

2.6 复用功能输入

与复用功能输出相对应,GPIO引脚也可配置为复用功能输入。在此模式下,引脚上的输入信号将不再被送至IDR寄存器供软件读取,而是直接路由至指定的片上外设模块,由该外设进行实时采集和处理。

例如,将某个引脚配置为USART的接收引脚(RX)时,该引脚上的串行数据信号便会直接送入USART外设的接收器,由硬件完成数据的采样、帧校验和存储,从而高效、可靠地实现串口通信,无需软件频繁读取IDR并解析时序。

换句话说:

  • 普通输入模式 :信号路径为:引脚 → (可选上下拉)→ 施密特触发器 → IDR寄存器 → 软件读取。
  • 复用功能输入模式 :信号路径为:引脚 → (可选上下拉)→ 施密特触发器 → 指定外设模块(如USART、SPI等)。

2.7 模拟输入输出

当GPIO引脚用于处理真实的模拟电压信号(而非数字逻辑电平)时,必须完全绕过其内部的数字电路单元,以保持信号的原始模拟特性。这是通过配置为模拟模式来实现的。

2.7.1 模拟输入模式(通常用于ADC)

当引脚被配置为模拟输入,例如作为模数转换器(ADC)的采集通道时,其内部信号路径将发生以下关键变化:

  • 数字电路被旁路:信号将不经过施密特触发器。施密特触发器的作用是将模拟电压转换为非0即1的数字信号,如果模拟电压经过它,其连续变化的特性将被破坏,导致ADC无法进行精确的电压采样。
  • 直接连接:引脚上的模拟电压信号直接连接到ADC模块的采样保持电路。
  • 关闭数字资源:为降低功耗并防止干扰,内部的上拉和下拉电阻也被自动断开。

此模式确保ADC能够采集到引脚上原始、连续的电压值。

2.7.2 模拟输出模式(通常用于DAC)

对应地,当引脚作为数模转换器(DAC)的输出通道时:

  • 输出级被旁路:DAC模块产生的模拟电压信号不经过推挽或开漏输出结构中的MOS管。MOS管工作在开关状态,会破坏模拟信号的连续性。
  • 专用通道:模拟电压通过专用的模拟开关和线路直接输出到引脚。
  • 高阻抗输出:在模拟输出模式下,引脚通常呈现高输出阻抗,适合连接外部运放等电路进行后续处理。

此模式保证了DAC输出的电压信/号是平滑、精确的模拟量。


3 GPIO 工作模式

GPIO引脚的工作模式由其内部硬件结构决定,其引脚可通过软件配置为多种不同的工作模式,以适应数字输入/输出、模拟信号处理及外设功能复用等不同应用场景。在STM32的固件库中,GPIO支持8种具体的工作模式:

|----------|--------|---------------------------|
| 模式名称 | 性质 | 特征 |
| 浮空输入 | 数字输入 | 可读取引脚电平,若引脚悬空,则电平不确定 |
| 上拉输入 | 数字输入 | 可读取引脚电平,内部连接上拉电阻,悬空时默认高电平 |
| 下拉输入 | 数字输入 | 可读取引脚电平,内部连接下拉电阻,悬空时默认低电平 |
| 模拟输入 | 模拟输入 | GPIO无效,引脚直接接入内部ADC |
| 开漏输出 | 数字输出 | 可输出引脚电平,高电平为高阻态,低电平接VSS |
| 推挽输出 | 数字输出 | 可输出引脚电平,高电平接VDD,低电平接VSS |
| 复用开漏输出 | 数字输出 | 由片上外设控制,高电平为高阻态,低电平接VSS |
| 复用推挽输出 | 数字输出 | 由片上外设控制,高电平接VDD,低电平接VSS |

其枚举定义如下:

cpp 复制代码
typedef enum
{
    GPIO_Mode_AIN         = 0x00, // 模拟输入 (Analog Input)
    GPIO_Mode_IN_FLOATING = 0x04, // 浮空输入 (Floating Input)
    GPIO_Mode_IPD         = 0x28, // 下拉输入 (Input Pull-Down)
    GPIO_Mode_IPU         = 0x48, // 上拉输入 (Input Pull-Up)

    GPIO_Mode_Out_OD      = 0x14, // 开漏输出 (Output Open-Drain)
    GPIO_Mode_Out_PP      = 0x10, // 推挽输出 (Output Push-Pull)
    GPIO_Mode_AF_OD       = 0x1C, // 复用开漏输出 (Alternate Function Open-Drain)
    GPIO_Mode_AF_PP       = 0x18  // 复用推挽输出 (Alternate Function Push-Pull)
} GPIOMode_TypeDef;

配置寄存器如下:

从功能角度划分,这八种模式可归纳为三大类别:输入模式、输出模式和复用功能模式。

3.1 输入模式

在输入模式下,GPIO的输出驱动电路被禁用,引脚仅能接收外部信号。输入信号在芯片内部会经过施密特触发器,将模拟电压信号整形为稳定的数字信号(逻辑0或1),其结果被锁存至输入数据寄存器(GPIOx_IDR),供软件读取。

根据是否启用内部电阻及信号处理方式,输入模式又可细分为四种:

3.1.1 浮空输入(GPIO_Mode_IN_FLOATING,Floating Input)

  • 工作原理:内部上拉和下拉电阻均不连接,I/O端口的电平信号直接进入输入数据寄存器。
  • 特点:引脚电平完全由外部电路决定,悬空时电平状态不确定。
  • 适用场景:外部已提供确定上下拉的场景,如通信总线。

3.1.2 上拉输入(GPIO_Mode_IPU,Input Pull-Up)

  • 工作原理:内部上拉电阻(约40kΩ)使能,I/O端口的电平信号直接进入输入数据寄存器。
  • 特点:当外部无驱动时,引脚被内部上拉至高电平。
  • 适用场景:常用于连接常态为低电平的信号源,如接地式按键。

3.1.3 下拉输入(GPIO_Mode_IPD,Input Pull-Down)

  • 工作原理:内部下拉电阻(约40kΩ)使能,I/O端口的电平信号直接进入输入数据寄存器。
  • 特点:当外部无驱动时,引脚被内部下拉至低电平。
  • 适用场景:常用于连接常态为高电平的信号源。

3.1.4 模拟输入(GPIO_Mode_AIN,Analog Input)

  • 工作原理:数字输入通道(包括施密特触发器和上下拉电阻)被完全关闭。
  • 特点:引脚上的原始模拟电压信号直接馈入片内模拟外设(如ADC)。
  • 适用场景:用于高精度模拟电压采样。

3.2 输出模式

在输出模式下,GPIO引脚的输出电平由输出数据寄存器(GPIOx_ODR) 直接控制。同时施密特触发器开启,允许通过读取 输入数据寄存器GPIOx_IDR回读当前的引脚实际电平状态(开漏输出时需注意外部电路影响),便于检测实际输出是否正确。

输出模式根据其驱动结构分为两种基本类型:推挽输出和开漏输出。

3.2.1 推挽输出

P-MOS管和N-MOS管在控制信号下交替导通,能够主动且强有力地输出高电平(接近VDD)或低电平(接近VSS)。该模式驱动能力强、开关速度快,适用于驱动LED、控制继电器以及产生高速数字信号。

3.2.2 开漏输出

仅N-MOS管受控工作。输出低电平时N-MOS导通,引脚被强下拉至低电平;输出高电平时N-MOS截止,引脚呈现高阻态,必须依靠外部上拉电阻才能获得高电平。此模式支持"线与"逻辑,允许多个引脚并联,并可通过外接不同电压的上拉电阻实现电平转换,因此常用于I2C、SMBUS等总线通信。

此外,在输出模式下可配置引脚的最大输出速度(即 I/O 高低电平切换的最高频率),可配置为2MHz、10MHz或50MHz。更高的速度意味着更快的边沿变化和更强的驱动能力,但也会带来更大的噪声和功耗。在条件允许时,通常选择最高速度(50MHz)以保证信号的快速响应。

3.3 复用功能模式

复用功能模式用于将GPIO引脚的控制权转移给指定的片上外设(如USART、SPI、定时器等),使其作为该外设的专用输入或输出引脚。根据外设需求,复用功能同样可配置为推挽或开漏输出结构,其输出速度也可独立配置(2MHz/10MHz/50MHz)。

  • 复用功能输出:引脚输出信号不再来自ODR寄存器,而是由指定的外设模块(如USART的TX)自动产生并驱动。软件仅需配置外设并启动传输,无需手动操作GPIO电平。

  • 复用功能输入:引脚输入信号不再送入IDR寄存器供软件读取,而是直接路由至指定外设(如USART的RX)的接收电路,由外设硬件自动完成信号采集与处理。

例如,将某引脚配置为USART_TX(复用推挽输出)后,该引脚的电平将由USART外设根据发送数据自动控制,而不是 ODR;配置为USART_RX(通常为复用浮空输入)后,引脚上的串行数据将直接由USART接收器处理。


4 相关元器件简介

4.1 LED(发光二极管)

LED(Light Emitting Diode)是一种能够将电能直接转化为可见光的固态半导体器件。它由半导体晶片、电极和环氧树脂封装组成,具有功耗低、寿命长、响应速度快等特点。

4.1.1 LED灯发光原理

LED的核心是一个由P型半导体和N型半导体组成的PN结。当在PN结两端施加正向电压时,电子从N区向P区移动,空穴从P区向N区移动。在PN结附近,电子与空穴复合,以光子的形式释放能量,从而产生光辐射。不同半导体材料发出的光波长不同,决定了LED的颜色。

LED的驱动相对简单,通常采用限流电阻串联的方式连接到电源。在本实验电路中,LED正极连接3.3V电源,负极通过限流电阻连接到单片机GPIO引脚(我使用的都是蓝光LED,就不加限流电阻了)。当引脚输出低电平时,形成电流通路,LED点亮;输出高电平时,LED两端电位接近,无电流通过,LED熄灭。需要注意的是,不同颜色的LED具有不同的正向压降(通常1.8V-3.3V),设计电路时需据此计算合适的限流电阻值。

4.2 有源蜂鸣器

有源蜂鸣器是一种内部集成振荡电路的发声器件,只需在其两端施加直流电压即可持续发出固定频率的声音。与无源蜂鸣器不同,它不需要外部提供脉冲信号即可工作,使用更为简便。

4.2.1 有源蜂鸣器工作原理

有源蜂鸣器内部包含振荡电路和发声元件(通常为压电陶瓷片或电磁线圈)。当施加合适的直流电压时,内部振荡电路产生固定频率的交流信号驱动发声元件振动,从而产生声音。其发声频率由内部振荡电路决定,不可调节。

本文使用的有源蜂鸣器模块采用三线制接口。当I/O口输入低电平时,蜂鸣器通电发声;输入高电平时,蜂鸣器断电静音。模块内部已集成必要的驱动和限流电路,可直接与单片机GPIO连接使用。:

  • VCC:电源正极,连接3.3V或5V电源
  • GND:电源地,与单片机共地
  • I/O:控制信号输入端,连接单片机GPIO引脚

4.3 面包板

面包板(Breadboard)是一种无需焊接即可快速搭建和测试电子电路的实验工具。其名称源于电子发展早期,工程师们常在切面包用的木板上用图钉和导线连接电路。现代面包板内部由金属簧片构成,提供了标准间距(2.54mm)的插孔,兼容绝大多数通孔元件和杜邦线,使元件的插拔和电路变更极为便捷。

4.3.1 内部结构与连接原理

一块典型的面包板由多个独立的内部连通区域组成:

  • 中央元件区:中间通常有一条隔离槽。竖向的每一排(通常5个孔)在内部是电气连通的,不同排之间则相互绝缘。这种设计便于插入双列直插(DIP)封装的集成电路,其引脚可以分跨隔离槽两侧。
  • 侧边电源轨:面包板两侧通常各有两条长条形的孔列,标有"+"和"-"。每一条长列在横向的所有孔在内部是连通的,常用于分别连接电源正极(VCC)和地(GND),为板上各处供电。需要注意的是,有些面包板左右两侧的电源轨是断开的,使用时需用跳线连接。

4.4 按键

按键(Tactile Switch)是一种最基础的手动机械开关,通过按下按钮使内部两个触点物理连接,从而导通电路。其特点是结构简单、成本低廉、寿命长(通常可达数十万次按压)。

4.4.1 工作原理与类型

按键未按下时,其内部触点断开,电路为开路状态;当按下按钮时,内部弹片或结构发生形变,使两个引脚对应的触点连接,电路导通。松开后,依靠弹力自动复位,触点再次断开。

根据默认状态,主要分为两类:

  • 常开型:最常见类型,未按下时断开,按下时闭合。
  • 自锁型:按一次保持闭合并锁定,再按一次才复位断开。本简介主要指常开型两脚按键。

4.4.2 与MCU的接口电路

按键本身无法提供稳定的逻辑电平,必须结合上拉或下拉电阻与MCU的GPIO输入模式配合使用。两种常见配置如下:

  • 上拉电阻接法 :按键一端接GPIO引脚和上拉电阻(至VCC),另一端接地。未按下时,GPIO通过上拉电阻读到高电平;按下时,引脚被直接拉低至地,读到低电平。此时MCU引脚应配置为输入模式上拉输入模式

  • 下拉电阻接法 :按键一端接GPIO引脚和下拉电阻(至GND),另一端接VCC。逻辑与上拉接法相反。此时MCU引脚应配置为下拉输入模式

4.4.3 软件消抖

由于机械触点在闭合和断开的瞬间会产生短暂的、快速的弹跳(即连续多次通断),这会被MCU高速的输入采样误识别为多次按键。因此,在软件中必须通过延时(如10-50ms)或算法来过滤掉这段不稳定时期的状态,这一过程称为"按键消抖"。

4.5 光敏电阻传感器模块

光敏电阻传感器模块是一种用于检测环境光照强度的常用模块。其核心原理是模块上的光敏电阻阻值会随外界光照变化,通过内部电路将此模拟量变化转换为可供单片机直接读取的数字信号或模拟信号。模块集成电压比较器与电位器,使用便捷,实物如下图所示。

4.5.1 工作原理

模块的核心传感元件是光敏电阻,其由硫化镉等半导体材料制成,工作原理基于内光电效应。当无光照时,内部载流子极少,电阻值非常高(可达兆欧级);当受到光照时,光子能量激发产生大量电子-空穴对,使其电阻值迅速降低。光照越强,电阻值越小。

模块将此阻值变化通过电路转换为两种输出:

  • 模拟电压输出 (AO):光敏电阻与一个定值电阻构成分压电路。其分压点(即AO引脚)的电压值随光敏电阻阻值(即光照强度)连续变化。此电压可接入单片机的ADC引脚进行采样,从而获得精确的环境光强数值。
  • 数字开关输出 (DO):AO的电压被送入一个LM393电压比较器,与一个由可调电位器设定的参考电压进行比较。当环境光强低于设定阈值(即AO电压高于参考电压)时,DO输出高电平;当环境光强超过设定阈值时,DO输出低电平。模块上的"DO输出指示灯"会在输出低电平时点亮。

本实验主要关注模块的数字开关输出 (DO)输出即可:

  • 当环境光强低于设定阈值时,DO输出高电平;
  • 当环境光强超过设定阈值时,DO输出低电平,同时模块上的DO输出指示灯点亮。

4.5.2 模块功能与接口

模块提供以下四个引脚:

  • VCC:接电源正极(3.3V - 5V)
  • GND:接电源负极
  • AO:模拟信号输出。输出与光强成比例的电压信号。
  • DO:TTL数字信号输出。输出高低电平,可作为开关信号。

模块上包含两个可调部分:

  • 灵敏度调节电位器:用于顺时针(增强)或逆时针(减弱)调节数字输出(DO)的动作阈值。
  • 电源指示灯:指示模块已通电。

5 本章节实验

本章节共有5个实验,工程文件包这里我就不提供了,大家可以自行进入江协科技官网:资料下载 去下载。

5.1 LED闪烁

5.1.1 实验目标

  • 掌握STM32 GPIO时钟的开启方法,理解外设时钟使能的重要性。
  • 学习使用标准库函数完成GPIO的初始化配置(推挽输出模式、引脚选择、输出速度)。
  • 掌握三种GPIO电平控制方法:
  • GPIO_SetBits/GPIO_ResetBits
  • GPIO_WriteBit配合Bit_SET/Bit_RESET
  • GPIO_WriteBit配合强制类型转换的0/1值
  • 实现LED周期性闪烁效果,建立基本的嵌入式程序循环框架认知。

5.1.2 硬件设计

注意:不同颜色的LED具有不同的正向压降(通常1.8V-3.3V),设计电路时需据此计算合适的限流电阻值。由于我使用的都是蓝光LED,电压在3.3V左右,所以我就不加限流电阻了。如果需要加限流电阻,可以串联一个1K的电位器进行分压即可,电位器相当于一个滑动电阻。

本实验硬件电路连接如图所示:

5.1.3 软件设计

该STM32程序实现了通过GPIO控制LED闪烁的功能,其执行流程分为初始化配置和主循环执行两个阶段。

系统初始化阶段 (仅执行一次):

  • 开启外设时钟:首先,通过 RCC_APB2PeriphClockCmd 函数开启GPIOA端口的时钟,这是操作任何外设的必要前提。
  • 配置GPIO引脚:随后,初始化PA0引脚。程序定义了一个GPIO_InitTypeDef结构体变量,将其模式设置为推挽输出,速度设置为50MHz,并通过GPIO_Init函数完成配置,使PA0引脚准备就绪。

主循环执行阶段 (无限循环):

系统进入 while (1) 主循环后,将依次演示三种控制PA0引脚电平的方法,实现LED周期性亮灭:

  • 方法一:使用 GPIO_ResetBits(GPIOA, GPIO_Pin_0) 将PA0设置为低电平,LED亮;延时500ms后,使用 GPIO_SetBits 将其设置为高电平,LED灭;再次延时500ms。
  • 方法二:使用 GPIO_WriteBit 函数,通过参数 Bit_RESET 和 Bit_SET 来分别设置低电平和高电平,每次操作后同样跟随500ms延时。
  • 方法三:同样使用 GPIO_WriteBit 函数,但直接传入强制转换为 BitAction 类型的 0 和 1 来设置电平,每次操作后延时500ms。
  • 完成以上三种方法的演示后,程序流程将跳回主循环开始处,重新执行,从而使LED持续闪烁。

具体代码如下:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

int main(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
															//使用各个外设前必须开启时钟,否则对外设的操作无效
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;					//定义结构体变量
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;		//GPIO模式,赋值为推挽输出模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;				//GPIO引脚,赋值为第0号引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		//GPIO速度,赋值为50MHz
	
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将赋值后的构体变量传递给GPIO_Init函数
															//函数内部会自动根据结构体的参数配置相应寄存器
															//实现GPIOA的初始化
	
	/*主循环,循环体内的代码会一直循环执行*/
	while (1)
	{
		/*设置PA0引脚的高低电平,实现LED闪烁,下面展示3种方法*/
		
		/*方法1:GPIO_ResetBits设置低电平,GPIO_SetBits设置高电平*/
		GPIO_ResetBits(GPIOA, GPIO_Pin_0);					//将PA0引脚设置为低电平
		Delay_ms(500);										//延时500ms
		GPIO_SetBits(GPIOA, GPIO_Pin_0);					//将PA0引脚设置为高电平
		Delay_ms(500);										//延时500ms
		
		/*方法2:GPIO_WriteBit设置低/高电平,由Bit_RESET/Bit_SET指定*/
		GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);		//将PA0引脚设置为低电平
		Delay_ms(500);										//延时500ms
		GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);			//将PA0引脚设置为高电平
		Delay_ms(500);										//延时500ms
		
		/*方法3:GPIO_WriteBit设置低/高电平,由数据0/1指定,数据需要强转为BitAction类型*/
		GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)0);		//将PA0引脚设置为低电平
		Delay_ms(500);										//延时500ms
		GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)1);		//将PA0引脚设置为高电平
		Delay_ms(500);										//延时500ms
	}
}

5.1.4 实验现象

LED闪烁:

5.2 LED流水灯

5.2.1 实验目标

  • 掌握GPIO口的批量初始化方法:学习使用 GPIO_Pin_All 参数,一次性完成GPIO端口(此处为GPIOA)所有引脚的初始化配置。
  • 学习并应用GPIO端口数据写入函数:掌握 GPIO_Write 函数的使用,理解其如何通过一个16位数据同时控制一个端口所有引脚的电平状态。
  • 理解位操作与硬件控制的关系:通过分析 ~0x0001、~0x0002 等数据,学习如何利用按位取反和移位操作生成特定的控制数据,实现精准的引脚控制。
  • 实现流水灯效果:综合运用上述知识,编程实现LED从PA0到PA7依次顺序点亮的流水灯动态效果,并控制其闪烁间隔(100ms)。

5.2.2 硬件设计

5.2.3 软件设计

程序的执行流程可分为两个主要阶段:

初始化阶段 (仅执行一次):

  • 开启外设时钟:调用 RCC_APB2PeriphClockCmd 函数,开启GPIOA端口的时钟,为后续配置和操作提供基础。
  • 批量初始化GPIO引脚:定义并配置一个 GPIO_InitTypeDef 结构体,将模式设置为推挽输出,引脚选择为 GPIO_Pin_All (所有引脚),速度设为50MHz。随后调用 GPIO_Init 函数,一次性完成GPIOA端口全部16个引脚的初始化。

主循环阶段 (无限循环):

程序进入 while (1) 无限循环,依次执行以下步骤,形成流水灯效果:

  • 第一步:调用 GPIO_Write(GPIOA, ~0x0001)。数据 0x0001(二进制0000 0000 0000 0001)经按位取反后,只有PA0对应的位为0(低电平),其余位为1(高电平)。这使得连接在PA0的LED点亮,其他LED熄灭,持续100ms。
  • 第二步至第八步:与第一步原理相同,依次使用 ~0x0002(PA1低)、~0x0004(PA2低)......直至 ~0x0080(PA7低)。每次调用都使低电平位置左移一位,从而实现点亮LED的位置从PA0到PA7依次移动的效果,每步间均有100ms延时。
  • 循环重复:完成PA7引脚的控制后,程序流程跳回循环开始处,重新从PA0开始,使流水灯效果持续运行。

具体代码如下:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

int main(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
															//使用各个外设前必须开启时钟,否则对外设的操作无效
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;					//定义结构体变量
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;		//GPIO模式,赋值为推挽输出模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;				//GPIO引脚,赋值为所有引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		//GPIO速度,赋值为50MHz
	
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将赋值后的构体变量传递给GPIO_Init函数
															//函数内部会自动根据结构体的参数配置相应寄存器
															//实现GPIOA的初始化
	
	/*主循环,循环体内的代码会一直循环执行*/
	while (1)
	{
		/*使用GPIO_Write,同时设置GPIOA所有引脚的高低电平,实现LED流水灯*/
		GPIO_Write(GPIOA, ~0x0001);	//0000 0000 0000 0001,PA0引脚为低电平,其他引脚均为高电平,注意数据有按位取反
		Delay_ms(100);				//延时100ms
		GPIO_Write(GPIOA, ~0x0002);	//0000 0000 0000 0010,PA1引脚为低电平,其他引脚均为高电平
		Delay_ms(100);				//延时100ms
		GPIO_Write(GPIOA, ~0x0004);	//0000 0000 0000 0100,PA2引脚为低电平,其他引脚均为高电平
		Delay_ms(100);				//延时100ms
		GPIO_Write(GPIOA, ~0x0008);	//0000 0000 0000 1000,PA3引脚为低电平,其他引脚均为高电平
		Delay_ms(100);				//延时100ms
		GPIO_Write(GPIOA, ~0x0010);	//0000 0000 0001 0000,PA4引脚为低电平,其他引脚均为高电平
		Delay_ms(100);				//延时100ms
		GPIO_Write(GPIOA, ~0x0020);	//0000 0000 0010 0000,PA5引脚为低电平,其他引脚均为高电平
		Delay_ms(100);				//延时100ms
		GPIO_Write(GPIOA, ~0x0040);	//0000 0000 0100 0000,PA6引脚为低电平,其他引脚均为高电平
		Delay_ms(100);				//延时100ms
		GPIO_Write(GPIOA, ~0x0080);	//0000 0000 1000 0000,PA7引脚为低电平,其他引脚均为高电平
		Delay_ms(100);				//延时100ms
	}
}

5.2.4 实验现象

这里我只连接了3个LED,图省事儿,问题不大。

5.3 蜂鸣器

5.3.1 实验目标

  • 掌握不同GPIO端口及引脚的配置方法:学习如何开启和初始化GPIOB端口,并掌握对指定引脚(此处为PB12)进行独立配置的技能。
  • 学习通过GPIO直接驱动有源蜂鸣器:理解如何通过控制GPIO引脚输出高/低电平,来直接控制有源蜂鸣器的鸣叫与停止,认识一种简单的输出设备驱动方式。
  • 掌握通过精确时序控制生成特定提示音:通过组合不同时长的 Delay_ms 延时函数,编程实现"短鸣-短停-短鸣-长停"的周期性蜂鸣模式,学习用时间序列控制硬件行为。

5.3.2 硬件设计

5.3.3 软件设计

程序的执行流程清晰分为初始化与主循环两个阶段,具体步骤如下:

一、 初始化阶段 (仅执行一次)

  • 开启外设时钟:调用 RCC_APB2PeriphClockCmd 函数,开启GPIOB端口的时钟,为操作PB12引脚做好准备。
  • 初始化指定GPIO引脚:定义并配置 GPIO_InitTypeDef 结构体,将模式设置为推挽输出,引脚选择为 GPIO_Pin_12,速度设为50MHz。随后调用 GPIO_Init 函数,完成对PB12引脚的初始化配置。

二、 主循环阶段 (无限循环,产生特定蜂鸣模式)

程序进入 while (1) 无限循环,按照预设的"两短一长"模式周期性控制蜂鸣器:

  • 第一次短鸣:调用 GPIO_ResetBits(GPIOB, GPIO_Pin_12) 将PB12置为低电平,蜂鸣器开始鸣叫,持续 100ms。
  • 第一次短停:调用 GPIO_SetBits 将PB12置为高电平,蜂鸣器停止,持续 100ms。
  • 第二次短鸣:再次将PB12置为低电平,鸣叫 100ms。
  • 长间隔停止:将PB12置为高电平,蜂鸣器停止,但此次停止持续时间较长,为 700ms,从而与前面的短促动作形成明显间隔。
  • 循环重复:完成上述四步后,程序跳回循环开始处,重新执行,使"嘀-嘀-"的提示音模式持续运行。

具体代码如下:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

int main(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	//开启GPIOB的时钟
															//使用各个外设前必须开启时钟,否则对外设的操作无效
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;					//定义结构体变量
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;		//GPIO模式,赋值为推挽输出模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;				//GPIO引脚,赋值为第12号引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		//GPIO速度,赋值为50MHz
	
	GPIO_Init(GPIOB, &GPIO_InitStructure);					//将赋值后的构体变量传递给GPIO_Init函数
															//函数内部会自动根据结构体的参数配置相应寄存器
															//实现GPIOB的初始化
	
	/*主循环,循环体内的代码会一直循环执行*/
	while (1)
	{
		GPIO_ResetBits(GPIOB, GPIO_Pin_12);		//将PB12引脚设置为低电平,蜂鸣器鸣叫
		Delay_ms(100);							//延时100ms
		GPIO_SetBits(GPIOB, GPIO_Pin_12);		//将PB12引脚设置为高电平,蜂鸣器停止
		Delay_ms(100);							//延时100ms
		GPIO_ResetBits(GPIOB, GPIO_Pin_12);		//将PB12引脚设置为低电平,蜂鸣器鸣叫
		Delay_ms(100);							//延时100ms
		GPIO_SetBits(GPIOB, GPIO_Pin_12);		//将PB12引脚设置为高电平,蜂鸣器停止
		Delay_ms(700);							//延时700ms
	}
}

5.3.4 实验现象

蜂鸣器短鸣-短停-短鸣-长停式的周期性蜂鸣。

5.4 按键控制LED

5.4.1 实验目标

  • 实践模块化编程方法:学习将特定功能(LED驱动、按键扫描)封装成独立的源文件(.c)和头文件(.h),掌握通过调用 LED_Init()、Key_Init() 等接口函数来构建清晰主程序的开发模式。
  • 掌握GPIO输入模式配置与读取:学习如何将GPIO引脚(PA10, PA12)初始化为上拉输入模式,并运用 GPIO_ReadInputDataBit 函数读取引脚电平状态,以此检测外部按键动作。
  • 实现完整的按键检测机制:编写包含消抖延时、等待松手和返回键值等步骤的稳健按键扫描函数,理解其用于消除硬件抖动、确保单次按压触发一次动作的原理。
  • 应用LED翻转控制逻辑:在LED驱动模块中,不仅实现开关函数,更重点掌握 LEDx_Turn() 函数如何通过 GPIO_ReadOutputDataBit 读取当前输出状态,并进行逻辑取反,从而实现按键每按一次、LED状态切换一次的效果。

5.4.2 硬件设计

图片为江协教程所使用的硬件连接方式,我的面包板正负极位置不同,不方便在PB1, PB11放置按键,所以将两个按键放到了A10和A12引脚,只是位变了,原理相同。

5.4.3 软件设计

程序的执行遵循"初始化 -> 循环检测 -> 条件响应"的典型嵌入式系统流程,具体步骤如下:

一、 系统初始化阶段

  • 主程序初始化:在 main 函数中,依次调用 LED_Init() 和 Key_Init()。
  • LED模块初始化:在 LED_Init() 内部,开启GPIOA时钟,将PA1、PA2配置为推挽输出模式,并设置默认高电平(LED熄灭)。
  • 按键模块初始化:在 Key_Init() 内部,开启GPIOA时钟,将PA10、PA12配置为上拉输入模式。此时引脚默认被内部电阻拉高,读取值为1。

二、 主循环扫描与响应阶段

系统进入 while (1) 无限循环,持续执行以下步骤:

**1. 获取键值:**循环内首先调用 Key_GetNum()。该函数依次检测PA10和PA12的电平。若检测到低电平(按键按下),则执行延时20ms消抖,随后循环等待按键松手,松手后再次延时消抖,最后返回对应的键码(10或12)。若无按键按下,函数返回0。

**2. 判断并执行动作:**主程序根据返回的 KeyNum 进行判断:

  • 如果 KeyNum == 10,则调用 LED1_Turn()。该函数会读取PA1当前输出电平,并设置为其相反状态,实现LED1的亮灭翻转。
  • 如果 KeyNum ==12,则调用 LED2_Turn(),实现LED2的亮灭翻转。
  • 如果 KeyNum == 0,则不执行任何操作。

**3. 循环继续:**完成一次判断后,程序立刻跳回循环开始,再次调用 Key_GetNum() 等待下一次按键输入,从而实现持续的交互控制。

具体代码如下:

main.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"

uint8_t KeyNum;		//定义用于接收按键键码的变量

int main(void)
{
	/*模块初始化*/
	LED_Init();		//LED初始化
	Key_Init();		//按键初始化
	
	while (1)
	{
		KeyNum = Key_GetNum();		//获取按键键码
		
		if (KeyNum == 10)			//按键1按下
		{
			LED1_Turn();			//LED1翻转
		}
		
		if (KeyNum == 12)			//按键2按下
		{
			LED2_Turn();			//LED2翻转
		}
	}
}

key.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

/**
  * 函    数:按键初始化
  * 参    数:无
  * 返 回 值:无
  */
void Key_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	   //开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					   //将PA10和PA12引脚初始化为上拉输入,按键未按下时,默认高电平
}

/**
  * 函    数:按键获取键码
  * 参    数:无
  * 返 回 值:按下按键的键码值,范围:0~2,返回0代表没有按键按下
  * 注意事项:此函数是阻塞式操作,当按键按住不放时,函数会卡住,直到按键松手
  */
uint8_t Key_GetNum(void)
{
	uint8_t KeyNum = 0;		//定义变量,默认键码值为0
	
	if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_10) == 0)			//读PA10输入寄存器的状态,如果为0,则代表按键1按下
	{
		Delay_ms(20);											//延时消抖
		while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_10) == 0);	//等待按键松手
		Delay_ms(20);											//延时消抖
		KeyNum = 10;											//置键码为10
	}
	
	if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_12) == 0)			//读PA12输入寄存器的状态,如果为0,则代表按键2按下
	{
		Delay_ms(20);											//延时消抖
		while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_12) == 0);	//等待按键松手
		Delay_ms(20);											//延时消抖
		KeyNum = 12;											//置键码为12
	}
	
	return KeyNum;			//返回键码值,如果没有按键按下,所有if都不成立,则键码为默认值0
}

Led.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header

/**
  * 函    数:LED初始化
  * 参    数:无
  * 返 回 值:无
  */
void LED_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);		//开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);						//将PA1和PA2引脚初始化为推挽输出
	
	/*设置GPIO初始化后的默认电平*/
	GPIO_SetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_2);				//设置PA1和PA2引脚为高电平
}

/**
  * 函    数:LED1开启
  * 参    数:无
  * 返 回 值:无
  */
void LED1_ON(void)
{
	GPIO_ResetBits(GPIOA, GPIO_Pin_1);		//设置PA1引脚为低电平
}

/**
  * 函    数:LED1关闭
  * 参    数:无
  * 返 回 值:无
  */
void LED1_OFF(void)
{
	GPIO_SetBits(GPIOA, GPIO_Pin_1);		//设置PA1引脚为高电平
}

/**
  * 函    数:LED1状态翻转
  * 参    数:无
  * 返 回 值:无
  */
void LED1_Turn(void)
{
	if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_1) == 0)		//获取输出寄存器的状态,如果当前引脚输出低电平
	{
		GPIO_SetBits(GPIOA, GPIO_Pin_1);					//则设置PA1引脚为高电平
	}
	else													//否则,即当前引脚输出高电平
	{
		GPIO_ResetBits(GPIOA, GPIO_Pin_1);					//则设置PA1引脚为低电平
	}
}

/**
  * 函    数:LED2开启
  * 参    数:无
  * 返 回 值:无
  */
void LED2_ON(void)
{
	GPIO_ResetBits(GPIOA, GPIO_Pin_2);		//设置PA2引脚为低电平
}

/**
  * 函    数:LED2关闭
  * 参    数:无
  * 返 回 值:无
  */
void LED2_OFF(void)
{
	GPIO_SetBits(GPIOA, GPIO_Pin_2);		//设置PA2引脚为高电平
}

/**
  * 函    数:LED2状态翻转
  * 参    数:无
  * 返 回 值:无
  */
void LED2_Turn(void)
{
	if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_2) == 0)		//获取输出寄存器的状态,如果当前引脚输出低电平
	{                                                  
		GPIO_SetBits(GPIOA, GPIO_Pin_2);               		//则设置PA2引脚为高电平
	}                                                  
	else                                               		//否则,即当前引脚输出高电平
	{                                                  
		GPIO_ResetBits(GPIOA, GPIO_Pin_2);             		//则设置PA2引脚为低电平
	}
}

5.4.4 实验现象

  • 初始状态时,两个LED(LED1-PA1, LED2-PA2)均处于熄灭状态。
  • 当按下连接在 PA10 上的按键1并松开后,LED1的状态会立即发生翻转(若之前是熄灭则点亮,若之前是点亮则熄灭)。
  • 当按下连接在 PA12 上的按键2并松开后,LED2的状态会立即发生翻转。
  • 按键操作具有消抖和松手检测功能,短按一次仅触发一次动作,长按或抖动不会导致LED状态连续变化。

5.5 光敏传感器控制蜂鸣器

5.5.1 实验目标

  • 掌握模拟传感器信号的数字读取:学习将光敏传感器模块的数字输出端(DO)连接到MCU的GPIO引脚,并通过配置为上拉输入模式来读取其高低电平信号,将连续的光强变化转化为单片机可处理的数字逻辑。
  • 实践"感知-决策-控制"的嵌入式系统逻辑:构建一个完整的信号链:由光敏传感器(感知环境)、主程序(判断决策)、蜂鸣器(执行控制)组成。理解如何根据输入信号实时改变输出设备的状态。
  • 强化模块化编程与接口调用:继续实践模块化编程思想,通过调用 LightSensor_Get() 和 Buzzer_ON()/Buzzer_OFF() 等清晰接口,构建易于理解和维护的主程序逻辑。

5.5.2 硬件设计

5.5.3 软件设计

程序的执行是一个典型的周期性检测与控制流程,具体步骤如下:

一、 系统初始化阶段

  • 主程序调用:在 main 函数中,依次调用 Buzzer_Init() 和 LightSensor_Init()。
  • 蜂鸣器模块初始化:在 Buzzer_Init() 中,开启GPIOB时钟,将PB12初始化为推挽输出,并设置为高电平(默认关闭蜂鸣器)。
  • 光敏模块初始化:在 LightSensor_Init() 中,开启GPIOB时钟,将PB13初始化为上拉输入。此时,若无外部下拉,引脚默认被内部电阻拉至高电平(读取值为1)。

二、 主循环检测与控制阶段

系统进入 while (1) 无限循环,高速且不间断地执行以下步骤:

  1. 读取环境光强:在循环体内,首先调用 LightSensor_Get() 函数。该函数内部使用 GPIO_ReadInputDataBit 读取 PB13引脚 的当前电平。

  2. 进行逻辑判断:主程序对读取到的电平值进行 if 判断。当 LightSensor_Get() == 0 成立时,表示检测到足够的光照。否则环境较暗。

  3. 执行对应控制:

  • 若光照充足:条件成立,执行 Buzzer_ON()(该函数将PB12置低电平),蜂鸣器鸣响。
  • 若环境较暗:条件不成立,执行 Buzzer_OFF()(该函数将PB12置高电平),蜂鸣器安静。
  1. 快速循环:完成本次控制后,程序立即返回循环开始,重新读取传感器状态,从而实现对环境光变化的实时响应。

具体代码如下:

main.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "Buzzer.h"
#include "LightSensor.h"

int main(void)
{
	/*模块初始化*/
	Buzzer_Init();			//蜂鸣器初始化
	LightSensor_Init();		//光敏传感器初始化
	
	while (1)
	{
		if (LightSensor_Get() == 1)		//光线暗时,指示灯熄灭,DO输出高电平
		{
			Buzzer_ON();				//蜂鸣器开启
		}
		else							//光线亮时,指示灯点亮,DO输出低电平
		{
			Buzzer_OFF();				//蜂鸣器关闭
		}
	}
}

Buzzer.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header

/**
  * 函    数:蜂鸣器初始化
  * 参    数:无
  * 返 回 值:无
  */
void Buzzer_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		//开启GPIOB的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);						//将PB12引脚初始化为推挽输出
	
	/*设置GPIO初始化后的默认电平*/
	GPIO_SetBits(GPIOB, GPIO_Pin_12);							//设置PB12引脚为高电平
}

/**
  * 函    数:蜂鸣器开启
  * 参    数:无
  * 返 回 值:无
  */
void Buzzer_ON(void)
{
	GPIO_ResetBits(GPIOB, GPIO_Pin_12);		//设置PB12引脚为低电平
}

/**
  * 函    数:蜂鸣器关闭
  * 参    数:无
  * 返 回 值:无
  */
void Buzzer_OFF(void)
{
	GPIO_SetBits(GPIOB, GPIO_Pin_12);		//设置PB12引脚为高电平
}

/**
  * 函    数:蜂鸣器状态翻转
  * 参    数:无
  * 返 回 值:无
  */
void Buzzer_Turn(void)
{
	if (GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_12) == 0)		//获取输出寄存器的状态,如果当前引脚输出低电平
	{
		GPIO_SetBits(GPIOB, GPIO_Pin_12);						//则设置PB12引脚为高电平
	}
	else														//否则,即当前引脚输出高电平
	{
		GPIO_ResetBits(GPIOB, GPIO_Pin_12);						//则设置PB12引脚为低电平
	}
}

LightSensor.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header

/**
  * 函    数:光敏传感器初始化
  * 参    数:无
  * 返 回 值:无
  */
void LightSensor_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		//开启GPIOB的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);						//将PB13引脚初始化为上拉输入
}

/**
  * 函    数:获取当前光敏传感器输出的高低电平
  * 参    数:无
  * 返 回 值:光敏传感器输出的高低电平,范围:0/1
  */
uint8_t LightSensor_Get(void)
{
	return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13);			//返回PB13输入寄存器的状态
}

5.5.4 实验现象

当光敏传感器接收到较强光照时,蜂鸣器会立即停止鸣响。

当光照减弱或消失时,蜂鸣器会开始持续鸣响。

相关推荐
QZ_orz_freedom2 小时前
后端学习笔记-Redis
redis·笔记·学习
碎碎思2 小时前
走向开放硅:Baochip-1x 的 RISC-V MCU 架构与工程实践
单片机·嵌入式硬件·risc-v
搞全栈小苏2 小时前
嵌入式之 LVGL 的切换页面研究:杜绝内存泄漏(单片机与 Linux 平台)(链表与多进程方式)
linux·单片机·链表·lvgl
想放学的刺客2 小时前
单片机嵌入式试题(第20期)通信协议深度解析与系统调试实战
stm32·单片机·嵌入式硬件·物联网·51单片机
笑鸿的学习笔记2 小时前
git笔记之清理本地存在但远程不存在的分支
笔记·git
赤~峰2 小时前
S32DS for S32 Platform RTC输出时间
单片机·mcu
承渊政道2 小时前
C++学习之旅【C++Vector类介绍—入门指南与核心概念解析】
c语言·开发语言·c++·学习·visual studio
四谎真好看2 小时前
JavaWeb学习笔记(Day05)之请求响应
笔记·学习·学习笔记·javaweb
hetao17338372 小时前
2026-01-21~22 hetao1733837 的刷题笔记
c++·笔记·算法