3.3.GPIO输入
一、概述
上两节我们已经把GPIO的结构和8种输入输出模式都讲完了,所以本小节GPIO输入的部分我们就直接从外部硬件设备开始讲了
本小节还要介绍一下C语言相关知识点,包括 C语言的数据类型、宏定义、typedef、结构体、枚举 这些知识点,这些知识点都是库函数里经常且反复出现的东西,了解这些有助于理解库函数的执行逻辑
C语言指针的部分up主有录制视频,请前往B站 @江协科技 进行搜索
接下来我们看到GPIO输入模式下的硬件和电路吧
二、按键介绍

通过下面这个波形就可以看到
假设按键没按下是高电平,按下了就是低电平
那在按下 / 松手的瞬间,信号由高电平变为低电平时,就会来回抖几下
这个抖动比较快,通常在 5 ~ 10ms 之间,人眼时分辨不出来的,但是对于高速运行的单片机而言,5 ~ 10ms 的时间还是很漫长的
所以我们要对这个抖动进行过滤,否则就会出现按 / 松一下,单片机却反应了多次的现象
最简单的过滤方法就是加一段延时,把这个抖动时间耗过去
三、传感器模块介绍
1、文字

STM32套件里提供了四种传感器模块,分别是
光敏电阻传感器、热敏电阻传感器、
对射式红外传感器、反射式红外传感器
它们的电路结构和工作原理都差不多,这些传感器模块都是利用传感器元件,比如光敏电阻、热敏电阻、红外接收管等,这些电阻会随外界模拟量的变化而变化,比如
光线越强、光敏电阻的阻值就越小
温度越高,热敏电阻的阻值就越小
红外光线越强,红外接收管的阻值就越小
但是电阻的变化不容易被直接观察,所以我们通常将 传感器元件与定值电阻进行串联分压 ,这样就可以得到模拟电压的输出了,对电路来说,检测电压就非常容易了
另外这个模块还可以通过电压比较器,来对这个模拟电压进行二值化,这样就可以得到数字电压输出了
2、电路图
下图就是传感器模块的基本电路

我们先看这个部分,这个N1就是传感器元件所代表的可变电阻,它的阻值可以根据环境的光线、温度等模拟量进行变化
AO=AnalogAO = AnalogAO=Analog OutputOutputOutput (模拟输出)

上面这个R1,是和N1进行分压的定值电阻,R1和N1串联,一端接在VCC正极,一端接在GND负极,这就构成了基本的分压电路
左边这个C2是一个滤波电容,它是为了给中间的电压输出进行滤波的,用来滤除一些干扰,保证输出电压波形的平滑。
一般我们在电路里遇到这种一端接在电路中,另一端接地的电容,都考虑一下这个是不是滤波电容。如果是,那就是用来保证电路稳定的,并不是电路的主要框架,这时候我们在分析电路的时候,就可以先把这个电容先抹掉,这样就可以使我们的电路分析更加简单
那我们把这个电容抹掉,整个电路的主要框架就是
定值电阻和传感器电阻的分压电路
在这里可以用分压定理来分析一下传感器电阻的阻值变化对输出电压的影响
当然我们还可以用上下拉电阻+极限思维的思维来分析,当这个N1(此时N1作为下拉电阻)阻值变小时,下拉作用就会增强,中间的AO端的电压就会拉低
// 极端情况下,N1阻值为0,AO输出被完全下拉,输出0V
当N1阻值变大,下拉作用就会减弱,中间的引脚由于R1的上拉作用,电压就会升高
// 极端情况下,N1阻值无穷大,相当于断路,输出电压被R1拉高至VCC
这是很多传感器电路(测光、测温)的基本原理。
这个上拉电阻和下拉电阻,在单片机电路中会经常出现,比如弱上拉、弱下拉、强上拉、强下拉等
Attention!Attention!Attention!
这里的强和弱指的都是“靠近程度”这里的强和弱指的都是“靠近程度”这里的强和弱指的都是“靠近程度”
这就是AO电压的由来,仅需两个电阻分压即可得到,那接下来这个模块还支持有数字输出,这个数字输出就是对AO进行二值化的输出

这里二值化是通过这个芯片LM393来完成的,这个LM393是一个电压比较器芯片,里面有两个独立的电压比较器电路,然后剩下的是VCC和GND供电。那我们芯片的VCC就接到电路的VCC,芯片的GND也接到电路的GND。
这里有一个电容,是一个电源供电的滤波电容,这个电压比较器其实就是一个运算放大器
没学过模电或者没看过up主关于51单片机也没关系,我用chatgpt在下面给大家解释一下
//运算放大器、电压比较器、开环、同/反向输出端
一、什么是运算放大器(Operational Amplifier,简称“运放”)
你可以先把“运放”想成一个能比较两个电压的聪明放大器。
它有三个主要引脚:
+V (正电源)││┌────────────┐输入1 →│+ 同相输入端 │输入2 →│- 反相输入端 ││ │输出 ← │ 输出端 │└────────────┘││-V (负电源)
二、运放的基本功能:比较两个电压谁大
它做的事非常简单:
它不断比较“同相输入端 (+)” 和 “反相输入端 (–)” 的电压,
然后让输出电压朝着电压更大的那一边“狂奔”。
举个直觉例子:
| 情况 | +端电压 | –端电压 | 输出会怎样 |
|---|---|---|---|
| +端 > –端 | 比如 +3V vs +1V | 输出拼命往上跑(接近电源正端) | |
| +端 < –端 | 比如 +1V vs +3V | 输出拼命往下跑(接近电源负端) |
三、“开环”是什么意思
“开环”其实指“没有反馈”的状态。
- “反馈”指的是输出又被拿一部分送回输入端,形成一个“环路”。
- “开环”就是这个环没接上(完全开着),没有任何反馈。
在这种“开环”状态下,运放的增益(放大倍数)非常非常大(几十万倍甚至上百万倍),所以:
只要两端电压差稍微有一点点(比如0.0001V),输出就会直接“冲到电源极限”!
这时候它就不再是线性放大器,而更像一个“判断器”——
输出不是“中间值”,而是非黑即白的:
- 一旦 + 端稍大 → 输出接近正电源(比如 +5V)
- 一旦 – 端稍大 → 输出接近负电源(比如 0V 或 –5V)
四、电压比较器,其实就是“开环运放”的一种应用!
当运放以“开环方式”使用时,它不再用来“放大信号”,
而是用来比较电压谁大谁小。
于是我们把这种用法叫作:
电压比较器(Voltage Comparator)
逻辑如下:
如果 (+) 端 > (–) 端 → 输出 = 高电平(接近正电源)
如果 (+) 端 < (–) 端 → 输出 = 低电平(接近负电源)
这就是“二值化”的核心思想:
→ 把连续的模拟信号(比如一个变化的电压波形)
→ 转换成两种数字状态(高 or 低)。
五、“同相输入端”和“反相输入端”的区别总结
| 名称 | 符号 | 含义 | 结果 |
|---|---|---|---|
| 同相输入端 | + | 当它电压高时,输出会“顺向”升高 | “+”表示同相 |
| 反相输入端 | – | 当它电压高时,输出会“反向”降低 | “–”表示反相 |
六、应用例子:信号二值化(比如方波生成)
假设我们有一个缓慢变化的电压信号(例如传感器输出),
用比较器和一个固定电压(阈值)比较,就能得到:
- 当信号 > 阈值 → 输出高电平
- 当信号 < 阈值 → 输出低电平
这样我们就从“模拟信号”变成了“数字信号”,
这就是二值化的意义。
//电路整体的解读
这里触及到博主的知识盲区了,直接照搬up主原话效果不好,于是我还是用chatgpt帮大家解读
这是一个典型的光电/电压比较型传感器电路(你看到的那颗 LM393 就是“电压比较器”芯片)。
一、整体概念:这是一个“模拟转数字”的传感器
这个电路的作用就是:
把一个连续变化的信号(例如光强、电压、距离)
转换成一个 0 / 1 数字信号。
它包含两部分输出:
- AO:Analog Output(模拟输出)——电压连续变化
- DO:Digital Output(二值 / 数字输出)——只有高或低(0/1)
⚙️ 二、核心元件:LM393 比较器
看左边第一个模块:
+VCC│┌─────────────┐
IN+ → │ │
IN– → │ LM393比较器 │ → 输出 AO/DO│ │└─────────────┘│GND
LM393 是一个双路电压比较器,它有两个比较通道(所以你会看到两个三角形)。
它的作用非常简单:
如果 IN+ 电压 > IN– 电压 → 输出低电平(靠近 GND)
如果 IN+ 电压 < IN– 电压 → 输出高电平(靠近 VCC)
(注意 LM393 输出是“开集电极”,所以逻辑方向可能和直觉反过来,这点后面解释。)
三、每个部分的功能拆解
让我们从左往右看:
① 左边模块:LM393 比较器芯片本体
这是电路的“大脑”。
它会根据两个输入端的电压大小比较,决定输出信号的高低。
② 中间模块:输入比较部分
IN– ← 分压电阻 R1, R2(或者电位器)
IN+ ← 传感器信号输入(比如光敏电阻、电容、热敏电阻等)
这一块是用来给比较器的两个输入端提供参考电压的。
- R1、R2:构成一个电压分压器(相当于“阈值”或“参考线”)。
- IN+:连接传感器信号(随环境变化而变化的电压)。
于是比较器就能判断:
当前传感器信号(IN+)有没有超过设定阈值(IN–)。
③ 右边模块:输出与指示
输出端 → 两个LED(LED1, LED2)↓R3, R4限流↓DO数字输出(P1接口)
这一部分是结果显示与输出接口:
-
LED2 是DO输出指示灯,可以指示DO的输出电平
-
DO那里还接了个R5上拉电阻,保证默认输出为高电平
-
LED1 是电源输出指示灯
-
R3、R4 是限流电阻,保护LED不被烧。
-
P1 接口输出:
- AO:模拟信号(传感器原始电压)
- DO:数字信号(比较器结果:高/低电平)
四、整体工作流程(直觉理解)
我们用“光敏传感器”为例(假设 N1 是光敏电阻):
-
光线变强 → 光敏电阻电阻变小 → IN+ 电压上升
-
比较器比较 IN+ 和 IN–:
- 若 IN+ > IN– → 输出低电平(LED1亮,表示触发)
- 若 IN+ < IN– → 输出高电平(LED灭)
-
这样就能实现“亮/灭判断”或“是否触发”功能。
五、LM393 的特殊性(为什么逻辑反了)
LM393 的输出端是开集电极(Open Collector)类型,
意思是它只能下拉(导通到GND),不能主动输出高电平。
所以:
- 当 IN+ > IN– → 输出被拉到 GND → 电压变低(LED亮)
- 当 IN+ < IN– → 输出悬空 → 被上拉电阻R5拉到高电平(LED灭)
这就是为什么很多初学者看着逻辑“反过来”的原因。
六、AO / DO 的区别总结
| 名称 | 类型 | 信号特点 | 来源 |
|---|---|---|---|
| AO | 模拟信号 | 电压连续变化(0~VCC) | 来自传感器本身(N1节点) |
| DO | 数字信号 | 只有高 / 低 | 来自比较器 LM393 输出 |
七、你可以这样理解整个电路
AO:告诉你“信号多强”
DO:告诉你“是否超过设定阈值”
比如:
- AO = 1.5V,说明光线还不太强;
- 阈值(由电位器调) = 2.0V;
- 所以 DO = 1(表示“没亮”);
- 当光线变强 → AO 上升到 2.5V → DO 突然变成 0(LED亮)。
这就是二值化过程的真实电路实现!
//关于电位器、二值化阈值
原句:
“把电位器换成两个电阻进行分压,这样数字输出就是固定阈值的二值化了。”
这句话其实涉及三个东西:
- 电位器
- 两个电阻的分压
- 数字输出的“二值化阈值”
下面一步步解释。
1. 电位器是啥?
电位器就像一个“可调的电阻分压器”。
你拧旋钮,输出电压就变。
所以:
- 使用 电位器 → 输出电压 可以调
- 使用 两个定值电阻 → 输出电压 固定不变
2. 两个电阻分压是什么?
把两个电阻接成串联:
+5V —— R1 ——●—— R2 —— GND|输出
这个“●”点就是分压点。
输出电压由公式决定:
Vout=5V×R2R1+R2V_{out}=5V\times\frac{R_2}{R_1+R_2}Vout=5V×R1+R2R2
你换不动它,所以 这个电压是固定的。
3. 这跟“固定阈值的二值化”有什么关系?
假设你把这个分压点接到一个 比较器 的负输入(—)端:
传感器输出 → 比较器正端(+)
固定分压点 → 比较器负端(-)
比较器规则非常简单:
- 当正端 > 负端 → 输出变 高电平(1)
- 当正端 < 负端 → 输出变 低电平(0)
也就是说:
比较器是在比较两个电压,决定输出是 0 或 1 的“二值化器”
所以这句话真正的意思是:
“用两个电阻做一个固定电压,让它作为比较器的固定阈值。这样比较器的输出就变成了基于这个固定阈值的0/1信号。”
没那么神秘,就是给比较器准备一个“固定参照电压”。
打个非常形象的比喻
把比较器想成“是否超过 10 分的评分机”:
- 电位器版本:分数线随时能调(10 分 → 12 分 → 15 分…)
- 两个电阻版本:分数线写死,比如就是 10 分
比较器看到输入电压 > 阈值 → 给你发“1”
否则 → “0”
一句话总结
电位器可调阈值;两个电阻固定阈值。
阈值一旦固定,比较器输出就变成固定阈值的二值化信号。
按键硬件电路

左边四个是按键的四种接法,上面两个是下接按键的方式,下面两个是上接按键的方式。一般我们采用下接按键,这是电路设计的习惯和规范
- 左上角

当按键按下,PA0被直接下拉到GND,此时读取PA0的电压就是低电平。
低电平有了,还需要高电平状态
当松开按键,此时PA0处于悬空状态,我们准备一个上拉电阻来设置默认模式——高电平
所以,这个电路,按下按键——低电平,松开按键——高电平
- 右上角

相比于左边,它在外面接了一个上拉电阻,那么在我们松开按键的时候,PA0默认为高电平模式,所以在PA0引脚我们就可以配置成浮空输入/上拉输入。
如果是上拉输入,那就是内外两个上拉电阻共同作用了,这时高电平就会更强一点,对应高电平就更加稳定。当然这样的话,当引脚被强行拉到低时,损耗也会大一点
为什么按下按键的时候是低电平?
我们极限分析
当松开按键时,K1断路,电阻无穷大,此时PA0输入电压为3.3V,高电平
当按下按键时,K1电阻降低,下拉作用增强,PA0输入电压降低,相对来说是低电平
- 左下角

第三个图,外面配置了一个上拉电阻,按下按键就是高电平模式,现在还缺一个低电平模式
于是我们可以在PA0引脚配置一个下拉电阻,相当于默认模式(浮空)是低电平
当然这要求单片机的引脚可以配置成下拉输入的模式,这并不常见
- 右下角

这里松开按键,K1电阻无穷大,无上拉作用,PA0处于低电平
按下按键,K1电阻减小,上拉作用增强,PA0处于高电平
按键硬件电路小结
上面这两种解法按下时是 低电平,松手是 高电平
下面这两种解法按下时是 高电平,松手是 低电平
左边两种解法必须要求配置引脚的默认模式,右边可以不管
传感器电路

VCC和GND不必多说,DO数字输出随便接一个接口,比如PA0,用于读取数字量,AO等我们之后学ADC模数转换器的时候再用
C语言部分
C语言数据类型

这属于基础,我们在这主要是提几个需要注意的地方
- 在51单片机中,int 占16位,而在STM32中 int 占32位,如果要用16位的数据,要用short来表示
- 右边两个栏目写的是C语言stdint.h文件和ST对这些变量的重命名。1.左边名字长; 2.int 位数根据系统的不同可能不一样 3.有时候名字会名不对题,比如char本意是字符型数据的意思,但单片机中用它来存放整数。所以stdint给它赋予了一个新名字,意思就是8位整型数据,右边加个_t表示这是用typedef(等会会讲到)重新命名的变量类型
- 右边列举的ST定义的,这是ST库函数以前用的名字,我们在库函数手册可以查阅到,相当于老版本
- 推荐stdint关键字下的名字,这是新版库函数使用的方式,也是C语言stdint.h头文件里提供的官方定义
C语言宏定义

- 用途1
很常见的例子:我们在程序中经常用1代表高电平,0表示低电平
此外,还有1代表上拉输入、2代表下拉输入、3代表浮空输入,这时就不直观了,不便于理解。那我们就可以用宏定义将数据参数映射到一个字符串上,这样方便理解
-
用途2
比如我们写程序里面出现了10个GPIO_Pin_0,这个Pin0是需要经常修改的,那如果一个个修改就不太方便,这时我们就可以用一个字符串来替代GPIO_Pin_0,然后需要修改的时候,只需要修改一下定义即可 -
定义宏定义
意思就是用ABC这个字符串替代12345这个参数 -
引用宏定义
直接写int a = ABC,那它就等效于int a = 12345这个意思
这个GPIO_Pin_12其实就是一个宏定义字符串,我们跳转到定义,可以看到,GPIO_Pin_12替换的是0x1000这个数据

左边 uint16_t是一个强制类型转换,是为了严谨性考虑的,我们暂时无需理会
下面这个图是程序实际执行的内容,取GPIO_Pin_12是为便于理解0x1000代表12号接口


剩下的还有,这个RCC_APB2Periph_GPIOB,也是一个宏定义的替换


还有GPIOB,也是宏定义的替换

以上就是库函数中宏定义的用法,当然还有其他用法,以后遇到了再说
C语言typedef

说白了,这也是个换名字的语句
区别在于
- 宏定义的新名字在左边,typedef 的新名字在右边
- 宏定义不需要分号,typedef 后面需要加分号


- 宏定义任何名字都能换,而typedef只能专门给变量类型换名字
- 用typedef对变量类型改名更安全,它会检查是否是变量类型的名字。但宏定义只是无脑改名,管你对不对
C语言结构体

要理解库函数的运作逻辑,就要理解结构体
在C语言中,结构体也是一种数据类型
char、short、int等,这些我们可以称作基本数据类型,然后数组就是一大堆基本数据类型的集合,比如定义char a[10];这就是10个char型数据的集合。数组可以叫做组合数据类型,由许多基本数据类型组合而来,数组组合的只能是相同的数据,刚刚这里是10个char型数据的组合
要是我们想组合不同的数据类型呢?
于是C语言的结构体出现了。结构体也是一种组合数据类型,它的作用就是组合不同的数据类型
在一个复杂的程序里,用结构体将一些数据打包起来,将有利于管理或者传递这些数据,有助于理解程序
对于C语言的数据来说,主要就是两个功能,一个是定义数据,一个是引用数据。
既然结构体也是数据类型,那它就应该和其他数据类型差不多
,也分为定义和使用
程序演示:结构体基本形态
#include <stdio.h>int main() {int a;a = 66;printf("a = %d\n", a);int b[5];//数组只能定义相同类型的数据b[0] = 66;b[1] = 77;b[2] = 88;printf("b[0] = %d\n", b[0]);printf("b[1] = %d\n", b[1]);printf("b[2] = %d\n", b[2]);//定义结构体变量时,需要用花括号括起来需要定义的数据类型//它们可以是相同的数据类型,也可以不是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;
}
接下来演示结构体的特殊用法
#include <stdio.h>typedef struct{char x;int y;float z;
} StructName_t;//一般结构体的成员会比较多,所以我们会把它们换个行int main() {//感觉结构体名字太长,如果需要定义d,还需要写一大串,太麻烦了//所以我们刚刚讲的typedef就能发挥作用了//我们直接把结构体名字换成StructName_t,这样我们每次想使用这个结构体的时候//就直接用这个新名字当作数据类型,然后直接定义就好//结构体变量加.来引出结构体成员的数据//这样就可以进行数据的写入和读取了StructName_t 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 = %.2f\n", c.z);return 0;
}

为什么要加一种结构体指针的引用方式呢?这是因为,结构体是一种组合数据类型,在函数之间的数据传递中,通常用的是地址传递而不是值传递
接下来,我们先初步了解一下
指针、内存与地址
一、内存是什么?
内存(RAM)就是一大排“小格子”
你可以把计算机的内存想象成:
一排整整齐齐的小格子,每个格子可以放数字。
就像下面这样:
┌──────────┬──────────┬──────────┬──────────┬─────┐
│ 格子A │ 格子B │ 格子C │ .... │ │
└──────────┴──────────┴──────────┴──────────┴─────┘
二、每个格子都有独一无二的“门牌号”——地址(Address)
每个格子都有编号,就像宿舍的房间号。
例:
格子 地址
A 0x20000000
B 0x20000001
C 0x20000002
D 0x20000003
地址就是这种“十六进制”的数字,比如:
0x20000084
0x4001080C
地址就是一个普通数字,只是专门用来标记格子的编号。
三、格子里放什么?——放数据!
一个格子里可以放一个字节(8 bit)的数据(0~255)。
比如:
地址 内容
0x20000000 41
0x20000001 99
0x20000002 55
这就是内存最底层的样子。
四、变量的值是如何写入计算机的?
当你写:
int a = 5;
你做了两件事:
- 在内存中找了 4 个连续的小格子(因为 int 占 4 字节)
- 这 4 个格子的起始位置,就是 a 的地址
- 把数字 5 转成二进制,放进去
假设 a 放在:
地址:0x20000054 ~ 0x20000057
内容:0000 0000 0000 0000 0000 0000 0000 0101 (数字 5)
五、那指针是什么?
指针 = 一个专门存放地址的变量。一般用*表示指针的特征
例如:
int a = 5;
int *p = &a;
a是变量5是内容&a是“a 所在的格子的地址”*:接下来定义的这个变量是一个指针p里面存的是这个地址
a 是一个宿舍房间,5 是房间里的人。
p 是一个“写着房间号的小纸条”。
六、解引用(*p)是什么?
解引用 = 拿着纸条上的房间号 ,去房间里 ,把里面的人数取出来。
解引用 p = *p = 访问 p 中存放的地址,并且取“这个地址里的值”。
所以解引用 = 读取地址 + 读取地址内容
比如:
p = 0x4001080C
*p = 这块内存格子里的内容
七、 GPIO 寄存器为什么也是“地址”
比如 GPIOA 的 ODR 寄存器地址是:
0x4001080C
这是一个“格子编号”。
里面放着 32 位的数据(比如 PA0~PA15 的电平状态)。
那么
*(uint32_t*)0x4001080C
第一个* 作用:解引用,表示:“去这个地址把值读出来”
第二个* 作用:声明这是个指针,表示“这是个指针类型的变量”
“把 0x4001080C 当成一个指针,然后去这个指针指向的地址读取一个 uint32_t 的值。”
就和 *p 一样,只不过这里没有变量,是直接用地址数字。
八、总结
内存 = 一堆编号的格子。
变量 = 格子里的内容。
地址 = 格子的编号。
指针 = 存着编号的变量。
*p = 根据编号去把内容拿出来。
开始实战
我们先解读一下初始化函数的这段代码
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
第一个参数
变量类型
是GPIO_TypeDef*,它是一个结构体指针类型。我们刚刚学过的typedef简化结构体名称的技能就用上了。

变量名
- GPIOx:
某个 GPIO 端口的寄存器的地址指针
比如:GPIOA, GPIOB, GPIOC
这个GPIOx为什么是指针,我们看一下有关它的宏定义
可以看到,GPIOx存储着 寄存器基地址,是 寄存器基地址的指针

查看一下寄存器基地址的宏定义,看看它原本叫啥名

这个APB2PERIPH_BASE里面还套了两层

可以看到,最终存储的还是一串地址,因此我们可以说GPIOx是某个GPIO端口的寄存器的地址指针GPIOx是某个GPIO端口的寄存器的地址指针GPIOx是某个GPIO端口的寄存器的地址指针
第二个参数
变量类型
GPIO_InitTypeDef*是一个结构体指针类型,里面装的是“初始化参数”,例如:
GPIO_InitTypeDef init;
init.GPIO_Pin = GPIO_PIN_0;
init.GPIO_Mode = GPIO_MODE_OUTPUT_PP;
变量名
GPIO_InitStruct 是我们刚刚定义出来的结构体指针
在引用结构体成员时,可以直接用->这个符号来引用 也可以直接用*引出指针变量的内容,再用 . 来引用结构体
up主说的这两句话其实就是 “结构体指针访问结构体成员的两种方法”
方式 A:手里是“结构体本体”
比如:
typedef struct {int a;int b;
} MyType;MyType t; // 这是“结构体本体”
要访问成员就用 点号 .
t.a = 10;
t.b = 20;
很好理解。
情况 B:手里是“结构体指针”
比如:
MyType t; //t是结构体本身
MyType *p = &t; // p 是指向结构体的指针,并且存放了结构体的地址
你现在手里不是结构体,而是“指针”。
问题来了:
那我要访问结构体里的成员(a、b)怎么办?
方法 1:使用箭头符号 ->(最常用)
记:
p->a就是 “*p 里面的 a”
例子:
p->a;
它的意思是:
“访问指针 p 所指向的结构体里的 a 成员”
方法 2:写出完整形式:先解引用 *,再用点 .
结构体指针访问成员的原始写法其实是:
(*p).a = 10;
解释:
*p:根据 p 里存的地址,找到那里存放的结构体(*p).a:然后对这个结构体用点号访问成员 a
为什么需要括号 ()?
因为 *p.a 会让编译器误以为你想:
*(p.a)
这种是错的。
所以必须加括号:
(*p).a
那“箭头 ->”是什么?
就是 C 发明的一个“简写法”:
(*p).a
↓ 简写
p->a
也就是说:
->就是 “取指针指向的结构体,然后访问成员”
总结
结构体本体:
t.a
结构体指针:
p->a (推荐)
(*p).a (原始写法,不常用)
举例子
例如:
GPIOA->MODER = 0x01;
意思是:
GPIOA是一个结构体指针(指向寄存器地址 0x40020000)MODER是结构体里的一个成员(模式寄存器)
和前面的例子对应:
GPIOA->MODER
等价于
(*GPIOA).MODER
也就是:
访问 GPIOA 这个外设寄存器结构体里的 MODER 成员。
现在应该理解两句话了
“在引用结构体成员时,可以直接用
->这个符号来引用”
= 如果你有“结构体指针”,应该用p->成员
“也可以直接用
*引出指针变量的内容,再用.来引用结构体”
= 完整写法是(*p).成员,只是更麻烦
数据打包
回到主函数这里,这个结构体就是一个数据打包的过程
首先将参数写到结构体的这三个变量里,然后统一打包,将结构体传递到函数里,接着在函数里面,把这个结构体拆包出来,读取变量

这就是使用结构体的整个过程
C语言的枚举

这个枚举跟结构体差不多,也是一种数据类型
那么还是那两个事:定义和引用
#include <stdio.h>typedef enum{Monday = 1,Tuesday
} Week_t;int main() {//我们需要在花括号这里指定这个变量可以有哪些取值//注意这里是用‘,’隔开,而结构体是用‘;’隔开//另外,如果这里面的数是按照顺序累加的,后面的赋值可以省略//同样,这个变量类型名比较长,我们可以用typedef改一下名Week_t week;//赋值时,只能这样写week = Monday; //等效于week = 1week = Tuesday; //等效于week = 2//这个赋值只能按照枚举中的定义来return 0;
}
我们蜂鸣器那个项目中的RCC外设时钟的ENABLE就是一个枚举值

它是这样一个枚举,枚举变量是FunctionalState

同样的,下面这个GPIO_Mode_Out_PP也是枚举值

我们跳转下定义,的确如此

另外还有,这个枚举值也不是必须赋值给枚举变量的
我们可以随意定义一个变量,把枚举值赋给它都行
这样枚举中的定义,就和宏定义差不多了
最终总结
本小节,我们浅浅了解了运放、电压比较器、二值化阈值、AO与DO、开环工作、电位器、上下拉电阻电路分析,以及C语言的基本数据类型、宏定义、typedef、两个特殊的数据类型:结构体、枚举,并对指针、内存、地址、解引用、GPIO寄存器与指针的关系进行了初步的了解。博主花了两天时间才写完,希望对各位有所帮助。
下节预告:继续学习GPIO输入的代码部分
