当前位置: 首页 > news >正文

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)相关资料:

  1. 引脚 / 管脚(硬件线和CPU的接触点):
  2. CPU为每个引脚提供了一些功能:

    但是在使用的时候只能选择其中的一个功能,简称复用功能(多个功能选一个)--- alternative function,AF。


硬件设计

二极管特性:单向流通性

问:如何让PB5为高电平 / 低电平?

        ---CPU芯片手册

(2)相关资料:

LED0的操作:


GPIO控制器 - General Purpose Input Output

  1. 谁输入 / 谁输出?
    外设向引脚输入,CPU向引脚输出
  2. 输入 / 输出什么
    输入高电平 / 低电平
    输出高电平 / 低电平

某个引脚配置为GPIO输出模式

GPIO分组


GPIO基本结构

输入模式
  • 输入浮空模式:GPIO_Mode_IN_FLOATING

  1. TTL施密特触发器作用是把电压转成逻辑数字 0 / 1。
  2. 在数字电路里,通过电平高低来表示逻辑 0 和逻辑 1。以 3.3V 供电的系统为例,规定了一个电平范围来对应逻辑 0 和逻辑 1,一般认为 0V - 0.8V 这个范围属于低电平,代表逻辑 0;2.0V - 3.3V 属于高电平,代表逻辑 1;而 0.8V - 2.0V 之间属于电平不确定区域,在实际应用中应尽量避免信号电平处于这个区间。
  3. 前面提到输入是外设输入,如果现在没有外设输入,端口 / 引脚这个点是高电平 / 低电平默认不知道。

  • 输入上拉模式:GPIO_MODE_IPU

  1. 上拉电阻:电阻就是普通电阻,之所以叫上拉电阻是因为接了一个电源电压,本质上它是普通电阻。
  2. 输入上拉模式就是把上拉电阻接通了。
  3. 如果输入3.3V高电平,则TTL把它转换为数字1发送给CPU;如果输入低电平,虽然可以拉高电平,但是贼弱,只能在没信号输入的时候稳定一下,真正低电平到来的时候瞬间就变成低电平了,TTL给它转成0给CPU核用。

上拉电阻这个点接通之后可以理解为高电平;下拉电阻这个点接通之后可以理解为低电平。

  • 输入下拉模式:GPIO_Mode_IPD

  1. 输入下拉模式就是把下拉电阻接通了。
  2. 和输入上拉模式相似,当有高电平到来,就会瞬间变成高电平,经过TTL转成1。

  • 模拟输入模式:GPIO_MODE_AIN


输出模式
  • 开漏输出模式:GPIO_Mode_Out_OD

  • 开漏复用输出模式:GPIO_Mode_AF_OD

开漏复用不是CPU核直接走这个寄存器,而是CPU核先访问I2C控制器,然后I2C控制器再用咱们这个控制器。

  • 推挽输出模式:GPIO_Mode_Out_PP

  1. 把推挽输出里的“推”想成“送快递”:当要输出高电平(3.3V ),就像 CPU 核指挥电路里的“通道”,把 3.3V 电压这 “包裹”,顺着电流从高往低(经端口、灯泡到地 )的路,使劲“推”出去,让电流流动点亮灯泡,这主动送电流、让电“往前跑”的劲儿,就是“推” 。
  2. 推挽里的 “挽”,就像电路伸出 “手”,把外部 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为推挽输出,50MHz

GPIOE_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] = 0011 

2.输出高低电平
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);}
}

红绿灯闪烁+蜂鸣

http://www.dtcms.com/a/395977.html

相关文章:

  • Flink SlotSharingGroup 机制详解
  • Final Cut Pro X fcpx音视频剪辑编辑(Mac中文)
  • 【LeetCode_88】合并两个有序数组
  • PromptPilot 发布:AI 提示词工程化新利器,首月零元体验
  • MySQL-详解数据库中的触发器
  • JVM调优实战及常量池详解
  • 字典树(Trie)
  • AI浏览器概述:Browser Use、Computer Use、Fellou
  • 「docker」三、3分钟快速安装docker
  • Altium Designer(AD)自定义PCB形状
  • 基于ZYNQ的创世SD NAND卡读写TXT文本实验
  • 文心快码入选2025人工智能AI4SE“银弹”标杆案例
  • 什么是SDN(Software Defined Netwok)
  • GitLab-如何基于现有项目仓库,复制出新的项目仓库
  • 本科大二第三周学习周报
  • 三、自定义Button模板触发器(纯XAML)
  • tar 将多个文件或目录打包成一个单独的归档文件
  • 2025新版 WSL2 + Docker Desktop 下载安装详细全流程指南 实现容器化管理,让开发效率起飞
  • 【LangChain4j】大模型实战-SpringBoot(阿里云百炼控制台)
  • Spring Security / Authorization Server 核心类中英文对照表
  • SqlHelper自定义的Sql工具类
  • 每周读书与学习->初识JMeter 元件(二)
  • 西门子 S7-200 SMART PLC 实操案例:中断程序的灵活应用定时中断实现模拟量滤波(上)
  • 测试分类(1)
  • 广州创科——湖北房县汪家河水库除险加固信息化工程(续集)
  • QT(5)
  • 仓颉语言宏(Cangjie Macros)全面解析:从基础到实战
  • linux RAID存储技术
  • 【每日一问】交流电和直流电有什么区别?
  • Postman使用指南