STM32学习笔记——GPIO输出与输入
2.1GPIO输出
常见的四个GPIO输出函数分析
1.GPIO_ResetBits
GPIO_ResetBits的作用是将指定引脚输出低电平,而不影响同一端口上其他未指定引脚的状态
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);- GPIOx:指向 GPIO 端口的指针,例如
GPIOA、GPIOB、GPIOC等。该参数必须是一个有效的 GPIO 外设基地址。 - GPIO_Pin:要设置低电平的引脚号,可以是单个引脚(如
GPIO_Pin_5)或多个引脚通过按位或运算组合(如GPIO_Pin_5 | GPIO_Pin_6)。每个引脚对应一个唯一的 16 位掩码,例如GPIO_Pin_0为0x0001,GPIO_Pin_5为0x0020。
注意:
1).假设 LED 正极接 VCC,负极通过限流电阻连接到 GPIO 引脚,则低电平点亮 LED
#include "stm32f10x.h" // Device header #include "Delay.h" int main(void) { RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA,ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO InitStructure.GPIO Mode=GPIO Mode Out PP; GPIO InitStructure.GPIO Pin=GPIO Pin 0; GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; GPIO_Init (GPIOA,&GPIO_InitStructure); GPIO_ResetBits( GPIOA,GPIO_Pin_0); }2).同时置低多个引脚时
GPIO_ResetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2); // 将 PA0、PA1、PA2 同时置低典型场景:在中断中安全地置低、关闭设备
2.GPIO_SetBits:置位函数(设置高电平)
将指定的一个或多个GPIO引脚输出高电平(逻辑1)。它与之前学习的GPIO_ResetBits是一对“镜像”操作
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);典型场景:在中断中安全地置高、控制单个或多个灯
3.GPIO_WriteBit:位写入函数(灵活的电平控制)
根据第三个参数BitVal的值,有选择地将指定引脚设置为高电平或低电平。它本质上是GPIO_SetBits和GPIO_ResetBits的“二合一”封装
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);其中BitAction是一个枚举类型:
typedef enum { Bit_RESET = 0, Bit_SET = 1 } BitAction;典型场景:需要根据变量动态决定高低电平、函数指针回调
#include "stm32f10x.h" // Device header #include "Delay.h" int main(void) { RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA,ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO InitStructure.GPIO Mode=GPIO Mode Out PP; GPIO InitStructure.GPIO Pin=GPIO Pin 0; GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; GPIO_Init (GPIOA,&GPIO_InitStructure); while (1) { GPIO_WriteBit( GPIOA,GPIO_Pin_0,Bit_RESET); Delay_ms (500) ; GPIO_WriteBit( GPIOA,GPIO_Pin_0,Bit_SET); Delay_ms (500) ; } }//呼吸灯4.GPIO_Write:端口写入函数(批量操作)
一次性将16位数据PortVal写入整个GPIO端口(如PA0~PA15)的输出数据寄存器(ODR)。这意味着它会同时改变端口上所有16个引脚的电平状态。
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);典型场景:快速初始化端口、批量控制数据总线、流水灯
#include "stm32f10x.h" // Device header #include "Delay.h" int main(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO InitStructure.GPIO Mode = GPIO Mode Out PP; GPIO InitStructure.GPIO Pin = GPIO Pin All; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); while (1) { GPIO_Write(GPIOA, ~0x0001); //0000 0000 0000 0001 Delay_ms(500); GPIO_Write(GPIOA, ~0x0002); //0000 0000 0000 0010 Delay_ms (500) ; GPIO_Write(GPIOA, ~0x0004); //0000 0000 0000 0100 Delay_ms(500) ; GPIO_Write(GPIOA, ~0x0008); //0000 0000 0000 1000 Delay_ms (500) ; GPIO_Write(GPIOA, ~0x0010); //0000 0000 0001 0000 Delay_ms (500) ; GPIO Write(GPIOA, ~0x0020); //0000 0000 0010 0000 Delay_ms (500) ; GPIO_Write(GPIOA, ~0x0040); //0000 0000 0100 0000 Delay_ms(500); GPIO_Write(GPIOA, ~0x0080); //0000 0000 1000 0000 Delay_ms(500) ; } }LED闪烁
int main(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); 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); while (1) { GPIO_ResetBits(GPIOB, GPIO_Pin_12); Delay_ms(500); GPIO_SetBits(GPIOB, GPIO_Pin_12); Delay_ms (500) ; } }2.2认识按键与传感器及学习C语言数据类型
按键
- 本质:一种机械开关,将用户的物理按压动作转换为电信号(高电平或低电平)。
- 作用:作为人机交互的输入单元,用于触发特定功能、改变系统状态、输入数据等。
- 常见类型:
- 轻触按键:最常见,手感清脆,寿命长。
- 自锁按键:按下后锁定状态,再次按下才弹起。
- 薄膜按键:常用于键盘、遥控器等,成本低。
- 微动开关:常用于鼠标、限位开关等,寿命极长,触发力小。
- 按键抖动:由于按键内部使用的是机械式弹簧片来进行通断的,所
以在按下和松手的瞬间会伴随有一连串的抖动
1. 硬件消抖(物理层)
- 原理:利用电容的充放电特性,将抖动产生的毛刺脉冲平滑掉。
- 电路:在按键两端并联一个0.1uF (100nF)的电容,电阻R上拉(或下拉)。
2. 软件消抖(逻辑层)—— 最常用、最灵活
原理:通过延时或定时器,避开抖动期,只在稳定期读取按键状态。- 延时消抖(延时20ms,避开抖动期)
传感器模块
1. 光学类传感器
- 光敏传感器模块:核心元件为光敏电阻,用于检测环境光强,常用于自动路灯、相机调光等场景。
- 红外避障模块:通过红外发射管和接收管检测前方障碍物,发射红外线被障碍物反射后由接收管接收,经比较电路输出信号。这是智能小车避障功能的经典方案。
- 循迹传感器模块:利用红外对管检测地面黑白线条的反射差异,常用于循迹小车。
2. 环境监测类传感器
- 温湿度传感器(如DHT11/DHT22):单总线数字输出,直接提供温度和湿度数据,无需ADC转换,非常适合STM32快速读取。
- 烟雾/气体传感器(如MQ-2):检测可燃气体、烟雾浓度,输出模拟量,需配合ADC使用。
- BMP180气压传感器:通过I2C接口输出气压和温度数据,常用于海拔测量和天气预报。
3. 运动与姿态类传感器
- MPU6050六轴陀螺仪+加速度计:通过I2C接口输出三轴加速度和三轴角速度数据,是无人机、平衡车、姿态检测项目的核心传感器。
- 超声波测距模块(如HC-SR04):通过声波往返时间计算距离,STM32通过定时器捕获脉冲宽度即可完成测距。
4. 开关与触控类传感器
- 触摸开关模块:基于电容感应原理,无需物理按键即可检测触摸动作,没有机械抖动问题。
- 倾斜开关/振动传感器:检测设备倾斜或振动状态,输出开关量,常用于防盗报警、姿态检测等场景。
整数类型总结
define——“先定义,后替换”
define 就是给某个值或某段代码取一个“外号”,然后在代码里用这个“外号”代替原来的内容,程序真正编译时,所有“外号”都会被还原成原来的内容。
什么是宏定义?
宏定义是C语言中一种预处理指令,它允许程序员用一个简短的“名字”来代表一段代码、常量或表达式。在程序编译之前,预处理器会自动将所有出现这个“名字”的地方替换成它代表的内容。
用通俗的话说,宏定义就像给一段代码或一个数值取了一个“外号”。比如,你给圆周率3.1415926取个外号叫“PI”,以后在代码中只要写PI,编译器就会自动把它替换成3.1415926。
define的两种核心用法:
1. 无参宏定义:给常量取个“外号”
#define 外号 真实内容2. 带参宏定义:像函数一样工作,但更“暴力”
带参宏让define变得更强大了,它看起来像一个函数,但实际上只是“文本替换”的升级版:
#define 宏名(参数列表) 替换文本例如:
#define SQUARE(x) ((x) * (x)) #define MAX(a, b) ((a) > (b) ? (a) : (b))使用SQUARE(5)会被替换为((5) * (5)),结果为25;MAX(3, 5)会被替换为((3) > (5) ? (3) : (5)),结果为5。
重要提醒:类函数宏的参数和整个表达式必须用括号包围,否则可能因运算符优先级导致意想不到的错误。
typedef
核心作用:给已有的数据类型起一个新的名字(别名)
- 在代码里,
int是本名,你可以用typedef给它起个外号叫myint,以后用myint定义变量就相当于用int定义。
重要区别:typedef并不创建新的类型,它只是为现有类型添加一个同义字。就像“三哥”和“张三”是同一个人,只是叫法不同。
- 先写出变量声明:
int a; - 用新类型名替换变量名:
int myint_t;(把a换成myint_t) - 在开头加上 typedef:
typedef int myint_t;
搞定!myint_t就是int的新别名。
| 对比项 | typedef | #define |
|---|---|---|
| 本质 | 类型别名,编译器处理 | 文本替换,预处理器处理 |
| 处理时机 | 编译时处理 | 预处理阶段(编译前) |
| 类型检查 | 有类型检查 | 无类型检查,只是机械替换 |
| 指针场景 | 安全,符合预期 | 容易出错 |
C语言结构体——struct
结构体是C语言中用于将不同类型的数据打包成一个整体的复合数据类型。它的核心思想是:把逻辑上相关但类型不同的数据组合在一起,作为一个整体来管理。
用生活中的例子来理解:
- 你想记录一个学生的信息,需要姓名(字符串)、年龄(整数)、成绩(浮点数)——这三个数据类型不同,但逻辑上属于同一个学生。
- 结构体就是用来“打包”这些相关数据的工具。方式一:直接定义结构体变量
结构体的定义方式
方式一:定义结构体类型
struct Point { char x; int y; float z; }; struct Point p1, p2; // 定义两个Point类型的变量方式二:配合typedef使用
typedef struct { char x; int y; float z; } Point;结构体成员的访问
1. 通过变量名访问:点运算符(.)
Point p1; // 定义结构体变量 p1.x = 'A'; // 访问成员x p1.y = 66; // 访问成员y p1.z = 1.23; // 访问成员z适用场景:当你有一个直接的结构体变量时使用。
2. 通过指针访问:箭头运算符(->)
Point *pPtr = &p1; // 定义结构体指针,指向p1 pPtr->x = 'A'; // 等价于 (*pPtr).x = 'A' pPtr->y = 66; pPtr->z = 1.23;适用场景:当你有一个指向结构体的指针时使用
这在嵌入式开发中非常常见:
- 函数参数传递结构体时,通常传指针(避免拷贝整个结构体,节省内存和时间)
- 链表、树等数据结构中,节点之间通过指针连接
- 访问硬件寄存器结构体(如GPIO、TIM等外设寄存器)
注意:pPtr->x完全等价于(*pPtr).x,但箭头写法更直观、更常用。
总结:
- 结构体 = 不同类型数据的打包器
- 定义结构体类型时,推荐用
typedef配合使用,简化类型名 - 访问成员有两种方式:
- 变量名用
.(点运算符) - 指针用
->(箭头运算符)
- 变量名用
- 结构体占用的内存不等于各成员大小之和——因为存在内存对齐,这是你下一步需要深入理解的关键点
枚举——enum
想象一下,你一周有七天:星期一、星期二……星期日。如果你在程序里用数字0代表星期一,1代表星期二……那代码里到处都是if (day == 0),别人根本看不懂“0”是什么意思。枚举(Enum)就是专门解决这个问题的——它允许你给这些数字起个有意义的名字,比如Monday、Tuesday,让代码像人话一样好懂。
枚举是一个“选择题”,它的值只能从你列出的选项里选一个,不能乱填。比如你定义了一个“颜色”枚举,里面只有红、绿、蓝,那变量就只能取这三个值,不能是“黄色”。
enum Weekday { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY };这里enum Weekday就是一个新的数据类型,MONDAY等是它的“成员”。默认情况下,MONDAY的值是0,TUESDAY是1,依次递增。
枚举的用法:
1.声明变量并赋值
enum Weekday today = MONDAY; // 今天星期一你不能写today = 8,因为枚举里没有第8个选项,编译器会报错——这就叫类型安全,防止你乱写数字。
2.配合 switch 语句使用
switch (today) { case MONDAY: printf("开工了!"); break; case FRIDAY: printf("周末在望!"); break; case SUNDAY: printf("休息日"); break; default: printf("普通工作日"); }typedef与enum的区别:
enum负责定义内容:它创建了一组具名的整数常量(如 0 和 1),并给这组常量起个“家族名”(Tag)。typedef负责定义别名:它为这个“家族名”创建一个更短、更好用的新类型名称,让你以后声明变量时不用每次都写enum关键字。
一句话理解:enum造出了“砖块”(数据值和类型规则),typedef给这堆砖块贴上了一个方便的“标签”(类型名),让你搬砖(声明变量)时更轻松。