STM32_02_GPIO
复习C语言位操作公式
CPU控制硬件的流程
cpu软件的方式访问寄存器即可
问:寄存器地址如何获取
先找到控制器的地址
控制器的地址如何获取
4G地址空间划分:
CPU理论可访4G空间但实际受限,关键在于:
1. 硬件分配:地址空间被显卡、硬盘控制器等硬件设备占用,还有内存芯片容量、数量及内存控制器性能限制。
2. 系统机制:32位系统自身会占用部分空间(如内核占用),内存管理的分页/分段机制有开销,且系统为安全隔离限制访问。
3. 软件设计:早期32位程序受开发局限,未充分利用4G空间,即便硬件支持也无法完全访问 。
地址空间范围可参考下图:
1.blcok 0里面有个flash:MCU里面封装了512KB的片内的一个闪存就放在这(0x2000 0000 ~ 0x2000 FFFF);
2.block 1里面有个SRAM:MCU里面封装了64KB的片内的一个內存就放在这(0x800 0000 ~ 0x807 FFFF);
这里的编址和keil5中debug的地址范围是完全对应起来的。
3.block 2当中是给外设控制器编的址(512KB):
诸如这样的每一行都是一个控制器,后面是对应控制器的地址空间范围。
4.Port B为什么叫它GPIOB?
这一块连续的地址空间就是一块存储区,从控制器的起始地址开始,划分一个一个的4字节存储区,每4字节存储区就是一个寄存器。
寄存器地址 = 控制器地址 + 寄存器地址偏移量
寄存器地址偏移量 = 寄存器地址 - 控制器地址
(目前4字节寄存器都看成 unsigned int 类型)
#define CRL (0x40010c00)
#define CRH (0x40010c04)
#define IDR (0x40010c08)
#define ODR (0x40010c0C)
这里我们回顾一下数组的知识:
int a[5];
a[3]; == *(a + 3) // a是数组的首地址
由于这里的寄存器数量非常多,一个一个写会造成不小的麻烦,因此我们也可以采用数组当中的表示方式。在这里,整个存储区的首地址我们可以拿到(0x40010c00),因此可以表示为:
#define GPIOB (unsigned int)(0x40010c00) // 拿整个存储区的首地址作为一个起始量
#define CRL (GPIOB + 0x00) == 0x40010c00
#define CRH (GPIOB + 0x04) == 0x40010c04
#define IDR (GPIOB + 0x08) == 0x40010c08
#define ODR (GPIOB + 0x0C) == 0x40010c0C
思考一:
那么就有同学好奇了,明显地,直接写地址更加方便为什么还要通过这种方式呢?
-- 这是因为手册会直接把寄存器偏移量给我们,直接加就行了。
思考二:
我们现在的的确确拿到了寄存器CRL...的首地址,但这个是我们的最终目的吗?
-- 并不是,拿到首地址是为了表示这块存储区
例如拿到了CRH的的首地址,怎么表示这块存储区呢?
| - - - - |CRH
#define CRH (GPIOB + 0x04) == 0x40010c04
指针回顾:
int a = 100; // 假如a存储区地址位0x1000int* p = &a; // p == 0x1000*p表示了a这块存储区
拿到CRH的首地址,可以采用解引用的方式表示这块存储区吗?
*(GPIOB + 0x04)
在这里,(GPIOB + 0x04)是个整数,0x40010c04虽然是CRH的地址,但是类型是个整数类型,编译器不认这个事。
同样地,
最终得以表示这块存储区。
ST官方为了便于管理,给控制器分成了三组
- 每个控制器中都有对应的寄存器。
- 如果逐个循环访问这些寄存器,工作量会非常大。
- 为了解决上述问题,ST公司将多个控制器分组到三条总线上: 每一条总线称为一个总线组。 总共有三条总线。
- 每一条总线都有一个起始地址:
- APB1 - 0x4000 0000
- APB2 - 0x4001 0000
- AHB - 0x4002 0000
- 通过总线的起始地址加上控制器在总线中的偏移量,可以找到对应控制器的地址。
相关资料:
ps:第二列就是一个个控制器,第一列是控制器对应的地址范围
以APB1为例,它的起始地址为 0x4000 0000 ,那么控制器TIM6的首地址就可以表示为 APB1 + 0x1000 。(控制器首地址 = 总线基地址 + 控制器相对于总线的偏移量)
总线地址 + 控制器偏移量 -> 控制器首地址 + 寄存器偏移量 -> 寄存器地址 + 强制转换 + 解引用 -> 寄存器存储区
STM32地址空间划分
实战演练-掌控需求
用户需求:循环开关灯
LED位置

思路:
(1)灯是怎么接线的?
- 如果想了解某个硬件的接线方式,就去看原理图(通过 ctrl+f 查找连线情况)
(2)接到CPU后怎么控制?
- 如果想通过CPU控制它就看CPU芯片手册
(1)相关资料:
- 引脚 / 管脚(硬件线和CPU的接触点):
- CPU为每个引脚提供了一些功能:
但是在使用的时候只能选择其中的一个功能,简称复用功能(多个功能选一个)--- alternative function,AF。
硬件设计
二极管特性:单向流通性
问:如何让PB5为高电平 / 低电平?
---CPU芯片手册
(2)相关资料:
LED0的操作:
GPIO控制器 - General Purpose Input Output
- 谁输入 / 谁输出?
外设向引脚输入,CPU向引脚输出- 输入 / 输出什么
输入高电平 / 低电平
输出高电平 / 低电平
某个引脚配置为GPIO输出模式
GPIO分组
GPIO基本结构
输入模式
- 输入浮空模式:GPIO_Mode_IN_FLOATING
- TTL施密特触发器作用是把电压转成逻辑数字 0 / 1。
- 在数字电路里,通过电平高低来表示逻辑 0 和逻辑 1。以 3.3V 供电的系统为例,规定了一个电平范围来对应逻辑 0 和逻辑 1,一般认为 0V - 0.8V 这个范围属于低电平,代表逻辑 0;2.0V - 3.3V 属于高电平,代表逻辑 1;而 0.8V - 2.0V 之间属于电平不确定区域,在实际应用中应尽量避免信号电平处于这个区间。
- 前面提到输入是外设输入,如果现在没有外设输入,端口 / 引脚这个点是高电平 / 低电平默认不知道。
- 输入上拉模式:GPIO_MODE_IPU
- 上拉电阻:电阻就是普通电阻,之所以叫上拉电阻是因为接了一个电源电压,本质上它是普通电阻。
- 输入上拉模式就是把上拉电阻接通了。
- 如果输入3.3V高电平,则TTL把它转换为数字1发送给CPU;如果输入低电平,虽然可以拉高电平,但是贼弱,只能在没信号输入的时候稳定一下,真正低电平到来的时候瞬间就变成低电平了,TTL给它转成0给CPU核用。
上拉电阻这个点接通之后可以理解为高电平;下拉电阻这个点接通之后可以理解为低电平。
- 输入下拉模式:GPIO_Mode_IPD
- 输入下拉模式就是把下拉电阻接通了。
- 和输入上拉模式相似,当有高电平到来,就会瞬间变成高电平,经过TTL转成1。
- 模拟输入模式:GPIO_MODE_AIN
输出模式
- 开漏输出模式:GPIO_Mode_Out_OD
- 开漏复用输出模式:GPIO_Mode_AF_OD
开漏复用不是CPU核直接走这个寄存器,而是CPU核先访问I2C控制器,然后I2C控制器再用咱们这个控制器。
- 推挽输出模式:GPIO_Mode_Out_PP
- 把推挽输出里的“推”想成“送快递”:当要输出高电平(3.3V ),就像 CPU 核指挥电路里的“通道”,把 3.3V 电压这 “包裹”,顺着电流从高往低(经端口、灯泡到地 )的路,使劲“推”出去,让电流流动点亮灯泡,这主动送电流、让电“往前跑”的劲儿,就是“推” 。
- 推挽里的 “挽”,就像电路伸出 “手”,把外部 3.3V 这股电流 “拉拽” 回引脚 。原本电流在外面,通过电路的通路,像 “挽住” 电流往回带,让电流顺着路径流过来,配合 “推” 的动作,灵活控制引脚电平与电流走向,实现稳定输出~(简单说就是主动把外部电流 “拽” 回引脚的过程 )
- 推挽复用输出模式:GPIO_Mode_AF_PP
GPIO复用功能配置
常见:
- 相关配置(cpu芯片手册-STM32F1xx中文参考手册-第八章GPIO相关内容)
- 注意:如果GPIO功能作为复用功能,该如何配置(见P110)
LED0的操作
LED0的寄存器
- 相关寄存器(STM32F1XX中文参考手册.pdf,P114)
- 端口配置低寄存器(GPIOB_CRL)
基地址:0x40010C00
每四个bit位配置一个GPIO的模式和时钟频率
PB5对应的bit[23:20] = 0011(推挽输出,50MHz(驱动能力最强,但是功耗最大,EMI最大))- 端口输出数据寄存器(GPIOB_ODR)
基地址:0x40010C0C
每一个bit位配置一个GPIO的输出值1/0
PB5对应的bit[5] = 1/0(高低电平)- 端口位设置/清除寄存器(GPIOB_BSRR)
基地址:0x40010C10
每两个bit位配置一个GPIO的输出值1/0 PB5对应的bit[21] = 1(输出低电平)
PB5对应的bit[5] = 1(输出高电平)
配置PB5:
GPIOB控制器位于APB2总线
注意:将来看cpu芯片手册时除了用什么看什么之外,还得看时钟控制器
时钟控制器
如果某个控制器需要工作,打开GPIOB控制器时钟
RCC控制器
==>
==>
RCC寄存器地址
相关寄存器
代码实现:
stm32f10x.h:
#ifndef __STM32f10x_H__ #define __STM32f10x_H__// GPIOB_CRL GPIOB_ODR - GPIOB - APB2 // RCC_APB2ENR - RCC - AHB // 定义宏表示总线的地址 #define PERIPH_BASE (unsigned int)(0x40000000) #define APB2PERIPH_BASE (PERIPH_BASE + 0X10000) #define AHBPERIPH_BASE (PERIPH_BASE + 0X20000) // // 定义宏表示控制器首地址 // #define GPIOB_BASE (APB2PERIPH_BASE + 0X0C00) #define RCC_BASE (AHBPERIPH_BASE + 0X1000) // // 定义宏表示寄存器存储区 // #define GPIOB_CRL (*(unsigned int *)(GPIOB_BASE + 0X00)) #define GPIOB_CRH (*(unsigned int *)(GPIOB_BASE + 0X04)) #define GPIOB_ODR (*(unsigned int *)(GPIOB_BASE + 0X0C)) #define RCC_APB2ENR (*(unsigned int *)(RCC_BASE + 0X18))#endif
main.c:
#include "stm32f10x.h"void SystemInit(void){// ... 防止报错 } // // 延时函数 // void delay(unsigned int i) {while(i--); }int main(void){// 1.打开GPIOB控制器的时钟RCC_APB2ENR |= (1 << 3); // 其余位不变,第三位变为1// 2.配置GPIOB5为推挽输出,50MHz// GPIOB_CRL[23:20] = 0011GPIOB_CRL &= ~(0XF << 20); // [23:20] = 0000GPIOB_CRL |= (3 << 20); // [23:20] = 0011// 3.配置GPIOB5输出高电平,灭// GPIOB_ODR[5] = 1GPIOB_ODR |= (1 << 5);while(1){// 开灯 GPIOB_ODR[5] = 0GPIOB_ODR &= ~(1 << 5);// 延时 - 实现延时的原因是CPU的速度太快了delay(0xfffff);// 关灯GPIOB_ODR |= (1 << 5);// 延时delay(0xfffff);} }
第三位为1,即将GPIOB控制器的时钟打开。
现象
拓展
1.DS1灯
原理图:
3.3V --- DS1 ------------ PE5 | CPU
LED1
如果PE5输出高电平, DS1灭
如果PE5输出低电平, DS1亮配置PE5为推挽输出, 50MHz
GPIOE控制器的寄存器
GPIOE_CRL
偏移量 : 0x00 (GPIOE_BASE + 0X00)
[23:20] = 0011, GPIOE5为推挽输出,50MHzGPIOE_ODR
偏移量 : 0x0c (GPIOE_BASE + 0X0C)
[5] = 0 , 输出低电平, 亮
[5] = 1, 输出高电平, 灭打开GPIOE控制器时钟 - APB2
RCC_APB2ENR
[6] = 1, 打开GPIOE控制器时钟
2.BEEP蜂鸣器
这里的S8050是三极管,默认断开的,BEEP左边连接CPU的PB8,当PB8 输出高电平,BEEP鸣叫;当PB8,输出低电平, BEEP不响。
- 这里重点了解它做开关是怎么用的:电流从CPU的PB8过来,它有两种情况:截止状态和饱和状态。
- CPU的硬件线是接在三极管的基极上面,当基极的电流特别小或者它为0的时候,集电极和发射极是不导通的,它两之间是断开的,当CPU引脚那边是低电平过来,这边是0v就断开了,称为截止状态,蜂鸣器也就不鸣叫了。
- 反之,CPU 在 PB8 引脚输出高电平,使 NPN 型三极管 Q2 的基极获得合适偏置电压, 会有电流从基极流入,根据三极管电流放大特性,基极电流会被放大从而产生较大集电极电流,当基极电流持续增大到一定程度,三极管进入饱和状态 ,此时集电极与发射极之间的电压降极小,相当于短路,也就实现了集电极和发射极导通,从而产生鸣叫。
步骤:
1.PB8配置为推挽输出, 50MHz
GPIOB_CRH
偏移量 : 0x04 (GPIOB_BASE + 0X04)
[3:0] = 00112.输出高低电平
GPIOB_ODR
偏移量 : 0X0C (GPIOB_BASE + 0X0C)
[8] = 1, GPIOB8输出高电平, BEEP鸣叫
[8] = 0, GPIOB8输出低电平, BEEP不响3.打开控制器时钟(前面已打开,省略)
判断整数数据的第n位是否为0?
是0, 输出0;不是0, 输出非0 (用&运算)
代码示例:
stm32f10x.h:
#ifndef __STM32F10X_H_ #define __STM32F10X_H_// // GPIOB_CRL GPIOB_ODR - GPIOB - APB2 // RCC_APB2ENR - RCC - AHB // GPIOE_CRL GPIOE_ODR - GPIOE - APB2 // 定义宏表示总线的地址 // #define PERIPH_BASE (unsigned int)(0X40000000) #define APB2PERIPH_BASE (PERIPH_BASE + 0X10000) #define AHBPERIPH_BASE (PERIPH_BASE + 0X20000) // // 定义宏表示控制器首地址 // #define GPIOB_BASE (APB2PERIPH_BASE + 0X0C00) #define GPIOE_BASE (APB2PERIPH_BASE + 0X1800) #define RCC_BASE (AHBPERIPH_BASE + 0X1000) // // 定义宏表示寄存器存储区 // #define GPIOB_CRL (*(unsigned int *)(GPIOB_BASE + 0X00)) #define GPIOB_CRH (*(unsigned int *)(GPIOB_BASE + 0X04)) #define GPIOB_ODR (*(unsigned int *)(GPIOB_BASE + 0X0C))#define GPIOE_CRL (*(unsigned int *)(GPIOE_BASE + 0X00)) #define GPIOE_CRH (*(unsigned int *)(GPIOE_BASE + 0X04)) #define GPIOE_ODR (*(unsigned int *)(GPIOE_BASE + 0X0C))#define RCC_APB2ENR (*(unsigned int *)(RCC_BASE + 0X18)) #endif
main.c:
#include "stm32f10x.h"// SystemInit函数 - 初始化 void SystemInit(void) {// ... 防止报错 } // // 延时函数 // void delay(unsigned int i) {while(i--); }// 入口函数 main函数 int main(void) {// 1.打开GPIOB控制器的时钟 RCC_APB2ENR[3] = 1RCC_APB2ENR |= (1 << 3);// 1.2打开GPIOE控制器的时钟 RCC_APB2ENR[6] = 1RCC_APB2ENR |= (1 << 6);// 2.配置GPIOB5为推挽输出, 50MHz// GPIOB_CRL[23:20] = 0011GPIOB_CRL &= ~(0XF << 20); //[23:20]=0000GPIOB_CRL |= (3 << 20); // [23:20] = 0011// 2.2配置GPIOE5为推挽输出, 50MHz// GPIOE_CRL[23:20] = 0011GPIOE_CRL &= ~(0XF << 20); //[23:20]=0000GPIOE_CRL |= (3 << 20); // [23:20] = 0011// 2.3配置GPIOB8为推挽输出, 50MHz// GPIOB_CRL[3:0] = 0011GPIOB_CRH &= ~(0XF << 0); //[3:0]=0000GPIOB_CRH |= (3 << 0); // [3:0] = 0011// 3.配置GPIOB5输出高电平, 灭 // GPIOB_ODR[5] = 1GPIOB_ODR |= (1 << 5);// 3.2配置GPIOE5输出高电平, 灭 // GPIOE_ODR[5] = 1GPIOE_ODR |= (1 << 5);// 3.3配置GPIOB8输出低电平, 不响// GPIOE_ODR[8] = 0GPIOB_ODR &= ~(1 << 8);while(1){// 开灯 GPIOB_ODR[5] = 0// 打开蜂鸣器GPIOB_ODR &= ~(1 << 5); GPIOE_ODR &= ~(1 << 5); GPIOB_ODR |= (1 << 8);// 延时delay(0xfffff);// 关灯 + 关闭蜂鸣器GPIOB_ODR |= (1 << 5);GPIOE_ODR |= (1 << 5);GPIOB_ODR &= ~(1 << 8);// 延时 delay(0xfffff);} }
红绿灯闪烁+蜂鸣