文章目录
- 1. 综述
- 2. GPIO(General-purpose input/output)通用输入输出端口
- 参考&收藏
1. 综述
1.1 集成开发环境
- STM32的集成开发环境(IDE)有两种:MDK、IAR,其中IAR向下兼容性差,建议使用IAR8.3版本;
- STM32的调试器可选:JLINK、CMSIS-DAP、ULINK、STLINK;
1.1.1 常用开发工具简介
1.2 常用资料手册简介
1.2.1 芯片手册
Data sheet;
芯片的所有数据可从官方数据手册中查看,如IC的Pin 定义、电器特性、机械特性、料号定义等,如下在芯片STM32F103x8B
的数据手册中查看其型号为LQFP100的 100个引脚的物理分布和定义:
1.2.2 参考手册
Reference Manual;
对芯片的外设的具体描述和功能介绍;
1.2.3 勘误手册
Errata Sheet;
描述IC某些功能的局限性(硬件bug)并给出解决方法;
1.2.4 Cotex-M3/M4权威指南
官方的STM32内核手册;
1.2.5 应用手册
Application Note;
官方的针对不同应用场合的描述性文档,一般配套有固件例程;
1.2.6 在ST官网上查找相应手册
- 进入ST官网https://www.st.com/content/st_com/en.html;
- 搜索栏输入对应芯片,如STM32F103;
- 下载目标文档;
1.2.7 在ST官网上下载对应芯片软件包
- 进入ST官网https://www.st.com/content/st_com/en.html;
- 搜索栏输入对应芯片软件包,如STM32CubeH7;
- 下载目标软件包;
软件包组成框图:
1.2.8 正点原子资料下载中心
http://www.openedv.com/docs/index.html#
1.3 CMSIS 软件包介绍
CMSIS(Cortex Microcontroller Software Interface Standard - 微控制器软件接口标准)是ARM 官方设计的驱动包;
因为ST 公司设计的Cortex 系列芯片采用的同一内核,只是核外的片上外设不同,这些差异导致在相同内核不同外设的芯片上移植工作变得非常困难,为了解决不同芯片厂商生产的Cortex 内核芯片的软件兼容性问题,ARM 与芯片厂商建立了CMSIS 标准;CMSIS 标准实际是新建了一个软件抽象层,STM32 的固件库就是按照CMSIS 建立起来的;
-
CMSIS软件包的获取途径:
- 芯片软件包中;
- MDK安装目录,ARM/PACK/ARM/CMSIS/版本号/CMSIS;
- GitHub,https://github.com/ARM-software/CMSIS_5;
- RAM官网;
-
CMSIS的组成框图:
如下图:
内核函数层: 用于访问内核寄存器的名称、地址定义,主要由ARM公司提供;
设备外设访问层: 这一层提供了片上的核外外设地址和中断定义,主要由芯片生产商提供。
1.3.1 CMSIS软件包中的几个常用的文件夹简介
- Core:Cortex-M处理器内核和外设的API,为处理器内核提供标准化接口;
- DAP:ARM官方推出的下载器固件;
- Documentation:CMSIS软件包的Help文档;
- Driver:ARM的驱动框架,该驱动包与HAL库的区别是该驱动包本身也会调用到HAL库的一些API,但同时它也封装了一些更好用的API,该驱动包路径(前提是安装了芯片的软件包):ARM\PACK\Keil\STM32H7xx_DFP\2.1.0\CMSIS\Driver,其支持外设如下图:
- DSP_Lib:ARM提供的DSP库,含源码;
- Include:封装了很多内核方面的API,是工程中务必包含的重要头文件路径;
- Lib:GCC和MDK格式的DSP库文件;
- NN:ARM推出的神经网络库,框图如下:
- RTOS:RTX4和CMSIS-RTOS V1封装层,含代码;
- RTOS2:RTX5和CMSIS-RTOS V2封装层,含代码;
- SVD:System View Description - 系统视图描述;对芯片的外设、存储器等进行详细描述,编译器需要用到该文件,不同系列芯片有不同的SVD,在MDK的Option - Target中可看到被调用的svd后缀文件;
- Utilities:一些实用的小文件;
1.4 固件库介绍
ST 公司为了方便开发人员,对寄存器进行了封装并发布了STM32固件库(Firmware Library),它是一个固件函数包,它由程序、数据结构和宏组成,包括了微控制器所有外设的性能特征;
固件库中的库函数包括了每一个外设的驱动描述和应用实例,并为开发者访问底层硬件提供了一个中间API ( application program interface )通过使用固件函数库,无需深入掌握底层硬件细节,开发者就可以轻松应用每一个外设;
使用标准外设库进行开发的最大优势在于可以使开发者不用深入了解硬件底层细节,就可以灵活规范的使用每一个外设;
固件库函数本质上是为用户提供一种可读性强的寄存器配置方法,是架设在寄存器与用户驱动层之间的接口,向下完成寄存器的配置,向上为用户提供可读性强的功能函数;
- STM32固件库函数文件在工程中的逻辑关系:
- ST 官方固件库手册:里面罗列了所有固件库函数的说明;
1.4.1 固件库文件夹简介
在官网获取对应芯片的固件库解压并打开后,其文件夹树如下图所示:
- Libraries 文件夹下包含CMSIS 和STM32F4xx_StdPeriph_Driver 两个目录,这两个目录包含固件库核心的所有子文件夹和文件,非常重要:
- CMSIS文件夹存放的是符合CMSIS规范的一些文件
- Driver 文件夹下是STM32Fx 标准外设固件库源码文件和对应的头文件,即封装好寄存器的函数;
- Project 文件夹下面有STM32F4xx_StdPeriph_Examples 和STM32F4xx_StdPeriph_Template 两个文件夹:
- Examples文件夹下是固件示例源码,Template文件夹下是工程模板;
下面着重介绍Libraries 文件夹下的两个文件夹:
-
CMSIS 文件夹:
Stm32f10x.h
这个头文件,实现了片上外设的所有寄存器的映射,是一个非常重要的头文件;system_stm32f10x.c
文件和它下面的头文件实现了STM32的时钟配置,操作的是片上的RCC 这个外设;
-
Driver 文件夹:
- 以
stm32f10x_gpio.c
及stm32f10x_gpio.h
文件为例,如果在开发的工程中用到了STM32的GPIO 外设,就至少要把这两个文件包含到工程里。这里函数对应的就是每个功能外设的驱动函数,因此它也是ST 标准库函数的主要内容; stm32f10x_it.c
这个文件是专门用来编写中断服务函数的,在我们对它修改前,这个文件已经定义了一些系统异常中断的接口,其它普通中断服务函数由我们自己添加;system_stm32f10x.c
这个文件包含了STM32芯片上电后初始化系统时钟扩展外部存储器使用的函数;stm32f10x_conf.h
这个文件被包含进stm32f10x.h
文件,它的作用同意管理外设的所有头文件,在需要的时候进行对应取消注释即可,参考:CSDN 博客:stm32f10x_conf.h;
- 以
1.5 HAL库介绍
在一些比较新的或高性能的芯片中会带有HAL库
HAL(Hardware Abstraction Layer - 硬件抽象层)库包含在芯片的软件包内,是芯片的外设驱动包,代码文件路径为(以STM32H7为例):Drivers\STM32H7xx_HAL_Driver;每个源文件开头都带有使用说明;
部分代码文件截图:
1.6 ARM 架构(含 Cortex-M 系列)数据类型
- 这些定义在头文件
stdint.h
有明确定义:
/* exact-width signed integer types */
typedef signed char int8_t; // 将有符号字符型 signed char 定义为 int8_t
typedef signed short int int16_t;
typedef signed int int32_t;
typedef signed __INT64 int64_t;
/* exact-width unsigned integer types */
typedef unsigned char uint8_t; // 将无符号字符型 unsigned char 定义为 uint8_t
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned __INT64 uint64_t;
/* 7.18.1.2 */
/* smallest type of at least n bits */
/* minimum-width signed integer types */
typedef signed char int_least8_t;
typedef signed short int int_least16_t;
typedef signed int int_least32_t;
typedef signed __INT64 int_least64_t;
/* minimum-width unsigned integer types */
typedef unsigned char uint_least8_t;
typedef unsigned short int uint_least16_t;
typedef unsigned int uint_least32_t;
typedef unsigned __INT64 uint_least64_t;
/* 7.18.1.3 */
/* fastest minimum-width signed integer types */
typedef signed int int_fast8_t;
typedef signed int int_fast16_t;
typedef signed int int_fast32_t;
typedef signed __INT64 int_fast64_t;
/* fastest minimum-width unsigned integer types */
typedef unsigned int uint_fast8_t;
typedef unsigned int uint_fast16_t;
typedef unsigned int uint_fast32_t;
typedef unsigned __INT64 uint_fast64_t;
/* 7.18.1.4 integer types capable of holding object pointers */
#if __sizeof_ptr == 8
typedef signed __INT64 intptr_t;
typedef unsigned __INT64 uintptr_t;
#else
typedef signed int intptr_t;
typedef unsigned int uintptr_t;
#endif
/* 7.18.1.5 greatest-width integer types */
typedef signed __LONGLONG intmax_t;
typedef unsigned __LONGLONG uintmax_t;
- 同样在头文件
stdint.h
中也有对数据类型的范围定义:
/* minimum values of exact-width signed integer types */
#define INT8_MIN -128
#define INT16_MIN -32768
#define INT32_MIN (~0x7fffffff) /* -2147483648 is unsigned */
#define INT64_MIN __INT64_C(~0x7fffffffffffffff) /* -9223372036854775808 is unsigned */
/* maximum values of exact-width signed integer types */
#define INT8_MAX 127
#define INT16_MAX 32767
#define INT32_MAX 2147483647
#define INT64_MAX __INT64_C(9223372036854775807)
/* maximum values of exact-width unsigned integer types */
#define UINT8_MAX 255
#define UINT16_MAX 65535
#define UINT32_MAX 4294967295u
#define UINT64_MAX __UINT64_C(18446744073709551615)
/* 7.18.2.2 */
/* minimum values of minimum-width signed integer types */
#define INT_LEAST8_MIN -128
#define INT_LEAST16_MIN -32768
#define INT_LEAST32_MIN (~0x7fffffff)
#define INT_LEAST64_MIN __INT64_C(~0x7fffffffffffffff)
/* maximum values of minimum-width signed integer types */
#define INT_LEAST8_MAX 127
#define INT_LEAST16_MAX 32767
#define INT_LEAST32_MAX 2147483647
#define INT_LEAST64_MAX __INT64_C(9223372036854775807)
/* maximum values of minimum-width unsigned integer types */
#define UINT_LEAST8_MAX 255
#define UINT_LEAST16_MAX 65535
#define UINT_LEAST32_MAX 4294967295u
#define UINT_LEAST64_MAX __UINT64_C(18446744073709551615)
/* 7.18.2.3 */
/* minimum values of fastest minimum-width signed integer types */
#define INT_FAST8_MIN (~0x7fffffff)
#define INT_FAST16_MIN (~0x7fffffff)
#define INT_FAST32_MIN (~0x7fffffff)
#define INT_FAST64_MIN __INT64_C(~0x7fffffffffffffff)
/* maximum values of fastest minimum-width signed integer types */
#define INT_FAST8_MAX 2147483647
#define INT_FAST16_MAX 2147483647
#define INT_FAST32_MAX 2147483647
#define INT_FAST64_MAX __INT64_C(9223372036854775807)
/* maximum values of fastest minimum-width unsigned integer types */
#define UINT_FAST8_MAX 4294967295u
#define UINT_FAST16_MAX 4294967295u
#define UINT_FAST32_MAX 4294967295u
#define UINT_FAST64_MAX __UINT64_C(18446744073709551615)
...
1.6.1 架构与内核的区别
-
架构:架构(Architecture)即框架结构的意思,以ARM 架构为例,ARM 架构就是由英国ARM 公司设计的一系列32位的RISC 微处理器架构总称,现有ARMv1~ARMv8种类;
-
内核:内核/核心(Core);
-
架构与内核的理解:以盖房子为例,平房、三层小洋楼、高层公寓、摩天大楼,这些基础部分为架构;而架构内布置设计,如三室两厅带双卫的设计图纸A即为内核;
架构与内核的关系就是,平房内可根据设计图纸A 布置空间,也可以根据图纸B 布置空间,三层小洋房也可以根据图纸A/B/C 布置空间,高层公寓也可以根据图纸A/B/C/D… 布置很多个空间;根据需求不同,会产生出不同的内核去满足需求;
1.7 堆栈
- 栈(stack)空间:用于局部变量、函数调时现场保护和返回地址、函数的形参等。
- 堆(heap)空间:主要用于动态内存分配,即使用
malloc
、calloc
、realloc
等函数分配的变量空间是在堆上。以 STM32H7 为例,堆栈是在startup_stm32h743xx.s
文件里面设置:
1.8 bootloader
STM32 的系统存储区自带 bootloader程序,此程序是 ST 在芯片出厂时烧录进去的,主要用于将用户应用程序下载到芯片内部 Flash;支持 USB,SPI,I2C,CAN,UART 等接口方式下载。
-
单片机烧录的方式:
- ISP:In System Programing - 在系统编程。⽐如使⽤STC-ISP对STC芯⽚编程,还有利用Flash loader对STM32编程等,切换BOOT0、BOOT1让芯片进boot程序。⽀持ISP的芯⽚⼀般在芯⽚内部固化了⼀段(⽤ISP升级的)boot程序。
- ICP:包括In Circuit Programing - 在电路编程和ICSP(In-Circuit Serial Programming)- 在电路串⾏编程。如:对EEPROM编程等。一般来说利⽤J-Link、ST-Link、e-Link32等⼯具进⾏编程也属于在电路编程(ICP)。
- IAP:In applicating Programing - 在应⽤编程:即Bootloader使用的单片机编程方式,可以简单理解为:在程序运⾏的过程中进⾏编程(升级程序,更新固件)。 IAP是⽤户⾃⼰的程序在运⾏过程中对User Flash的部分区域进⾏烧写,⽬的是为了在产品发布后可以⽅便地通过预留的通信⼝对产品中的固件程序进⾏更新升级。
-
Bootloader程序升级流程:
- 上电后检查是否需要对第二部分代码进行更新
- 如果不需要就转到步骤4
- 执行更新程序
- 设置系统调度,跳转到用户应用程序运行
1.9 STM32 存储器及其映射
STM32芯片的内核由ARM 设计,而FLASH、SRAM、寄存器组等存储空间却是ST 设计的,它们本身并不具备地址信息,而给这些存储器分配地址的过程就叫存储器映射;
STM32芯片的这些存储器空间,被组织在同一个容量为4G (0000 0000 ~ FFFF FFFF)的线性地址空间内,如下图:
1.9.1 片上外设存储器
STM32的片上外设由总线控制,它们分别挂载在了APB 和AHB 两个总线上,其中APB 挂载低速外设、AHB 挂载高速外设;
其中APB、AHB 总线又进一步划分为APB1、APB2和AHB1、AHB2;
- 片上外设寄存器的地址空间:
如上示意图:
- 外设基地址:片上外设总线的最低地址叫其总线的基地址,即APB1的最低位
((unit32_t)0x4000 0000)
;
#define PERIPH_BASE ((uint32 t)0x40000000) // 片上外设总线的基地址
- APB2的基地址:APB2总线的基地址从片上外设总线的基地址偏移而来,其偏移地址为
0x10000
#define APB2PERIPH_BASE (PERIPH BASE + 0x10000) // APB2总线的基地址
- GPIOA的基地址:GPIOA总线的基地址从APB2的基地址偏移而来,其偏移地址为
Ox0800
#define GPIOA_BASE (APB2PERIPH BASE + Ox0800)
以上述原理,即可准确定位STM32片上外设的具体地址;
1.10 嵌入式相关知识
-
计算机系统的三大平台:服务器、桌面计算机、嵌入式;
-
嵌入式系统的一般定义:以应用为中心、以计算机技术为基础,软硬件可裁剪,对功能、可靠性、成本、体积、功耗和应用环境有特殊要求的专用计算机系统,是将应用程序、操作系统和计算机硬件集成在一起的系统。
-
嵌入式系统的基本组成:计算机硬件、操作系统、应用程序;
-
嵌入式系统的系统分级:片级(芯片)、板级(主板)、系统级;
1.11 STM32F1系列与F407系列的区别
- F1 采用 Crotex M3 内核,F407 采用 Crotex M4 内核。
- F1 最高主频 72MHz, F407 最高主频 168MHz。
- F407 具有单精度浮点运算单元,F1 没有浮点运算单元。
- F407 的具备增强的 DSP 指令集。F407 的执行 16 位 DSP 指令的时间只有 F1 的 30%~ 70%。F407执行 32 位 DSP 指令的时间只有 F1 的 25%~ 60%。
- F1 内部 SRAM 最大 64K 字节, F407 SRAM 有 192K 字节(112K+64K+16K)。
- F407 有备份域 SRAM(通过 Vbat 供电保持数据),F1 没有备份域 SRAM。
- F407 从内部 SRAM 和外部 FMC 存储器执行程序的速度比 F1 快很多。F1 的指令总线 I-Bus 只接到Flash 上,从 SRAM 和 FMC 取指令只能通过 S-Bus,速度较慢。F407 的 I-Bus 不但连接到 Flash 上,而且还连接到 SRAM 和 FMC 上,从而加快从 SRAM 或 FMC 取指令的速度。
- F1 最大封装为 144 脚,可提供 112 个 GPIO;F407 最大封装有 176 脚,可提供 140 个 GPIO。
- F1 的 GPIO 的内部上下拉电阻配置仅仅针对输入模式有用,输出时无效。而 F407 的 GPIO 在设置为输出模式时,上下拉电阻的配置依然有效。即 F407 可以配置为开漏输出,内部上拉电阻使能,而 F1不行。
- F407 的 GPIO 最高翻转速度为 90MHz,F1 最大翻转速度只有 18MHz。
- F1 最多可提供 5 个 UART 串口,F407 最多可以提供 6 个 UART 串口。
- F1 可提供 2 个 I2C 接口,F407 可以提供 3 个 I2C 接口。
- F1 和 F407 都具有 3 个 12 位的独立 ADC,F1 可提供 21 个输入通道,F407 可以提供 24 个输入通道。F1 的 ADC 最大采样频率为 1Msps,2 路交替采样可到 2Msps(F1 不支持 3 路交替采样)。F4的 ADC 最大采样频率为 2.4Msps,3 路交替采样可到 7.2Msps。
- F1 只有 12 个 DMA 通道,F407 有 16 个 DMA 通道。F407 的每个 DMA 通道有 4*32 位 FIFO,F1
没有 FIFO。 - F1 的 SPI 时钟最高速度为 18MHz, F407 可以到 37.5MHz。
- F1 没有独立的 32 位定时器(32 位需要级联实现),F407 的 TIM2 和 TIM5 具有 32 位上下计数功
能。 - F1 和 F407 都有 2 个 I2S 接口,但是 F1 的 I2S 只支持半双工(同一时刻要么放音,要么录音),而 F407
的 I2S 支持全双工,放音和录音可以同时进行。 - 从编程的角度来说,M3 和 M4 几乎没有区别。而性能上区别可以看此贴:
https://www.armbbs.cn/forum.php?mod=viewthread&tid=21850
1.12 ARM 架构(含 Cortex-M 系列)数据类型
上述定义在头文件stdint.h
中;
2. GPIO(General-purpose input/output)通用输入输出端口
STM32 的所有IO 口都可作为中断输入;
STM32的大多数IO 口都是5V 兼容的,而IO 口的5V 兼容性可以通过数据手册查询,标记有FT 的就是5V 兼容的;
STM32引脚是用来做控制而不是做驱动使用的;
以STM32F103VET6 芯片为例,其一共有5组IO 口,每组IO 口有16个IO,它们分别是:
- GPIO 输入/输出总结:
-
电压与电平:
- 电压:指模拟信号,单位用
V
表示; - 电平:数字信号,以
0
或1
表示,一般有明确的电压区间定义;
- 电压:指模拟信号,单位用
更多关于STM32 GPIO 的介绍,可查看《STM32中文参考手册》第8章通用和复用功能IO(GPIO 和AFIO);
关于STM32 GPIO 的Pin 定义,可查看对应芯片的Datasheet;
2.1 GPIO 端口复用与重映射功能
- 端口复用功能:STM32的大部分端口都具有复用功能,复用就是一些端口不仅可以做为通用IO 口还可以复用为一些外设引脚,其作用是最大限度地利用端口资源;
如PA9,PA10可以复用为STM32的串口1引脚;
- 端口复用代码配置:以下代码介绍了将GPIO PA9、PA10复用为内置外设USART1_TX、USART1_RX引脚的步骤(串口1工作在全双工模式):
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能端口时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // 使能复用的片上外设功能时钟
//USART1_TX PA.9 复用推挽输出配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);
//USART1_RX PA.10 浮空输入配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
- 端口重映射功能:重映射就是可以把某些功能引脚映射到其他引脚,其作用是方便布线;
如从上图可知,串口1默认引脚是PA9,PA10,它们可以通过配置重映射到PB6,PB7
- 端口重映射代码配置:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 使能端口时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // 使能要重映射的内置外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 使能端口复用时钟
GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE) // 使能串口1重映射功能
- 部分重映射和完全重映射:关于重映射的功能,除了查看芯片参考手册外的重映射表外,还可以从
GPIO_PinRemapConfig
函数的第一个入口参数的取值范围了解到;在stm32f10x_gpio.h
文件中定义了取值范围为下面宏定义的标识符,这里贴一小部分:
#define GPIO_Remap_SPI1 ((uint32_t)0x00000001)
#define GPIO_Remap_I2C1 ((uint32_t)0x00000002)
#define GPIO_Remap_USART1 ((uint32_t)0x00000004)
#define GPIO_Remap_USART2 ((uint32_t)0x00000008)
#define GPIO_PartialRemap_USART3 ((uint32_t)0x00140010)
#define GPIO_FullRemap_USART3 ((uint32_t)0x00140030)
从上代码可知,USART1 、USART2只有一种重映射,而 USART3 存在部分重映射和完全重映射;
- 部分重映射就是部分管脚的位置和默认的一致,部分管脚重新映射到其他引脚;
- 完全重映射就是所有引脚都重新映射到其他管脚;
以下是手册中的 USART3 重映射表:
可见,部分重映射就是将 PB10、PB11、PB12 重映射到 PC10、PC11、PC12 上,而 PB13 和 PB14引脚位置不变。完全重映射就是将这两个脚也重新映射到 PD11 和 PD12 去。
GPIO_PinRemapConfig(GPIO_PartialRemap_USART3, ENABLE); // 串口3部分重映射
GPIO_PinRemapConfig(GPIO_FullRemap_USART3, ENABLE); // 串口3完全重映射
2.2 GPIO 基本结构
- 引脚保护:引脚上连接有两个保护二级管,当引脚电压高于VDD 时,上方的二极管导通吸收这个高电压;同理当引脚电压低于VSS 时下方的二极管导通,防止不正常电压引入芯片导致芯片烧毁;
- 输入数据寄存器:输入信号经过肖特基触发器,模拟信号将变为数字信号0或1,然后存储在输入数据寄存器中,通过读取输入数据寄存器IDR 即可知道IO 口的电平状态;
- 输出数据寄存器:用于存放电平输出值,通过修改ODR 寄存器的值,就可以修改GPIO 引脚的输出电平。而“设置/清除寄存器BSRR”可以通过修改ODR 寄存器的值影响电路的输出,实现对输出的置位或清除;
TTL 施密特触发器的基本原理:当输入电压高于正向闯值电压,输出为高;当输人电压低于负向闯值电压,输出为低;lO 口信号经过触发器后,模拟信号转化为0和1的数字信号,即高低电平;
2.3 4种输入模式
2.3.1 输入浮空
输入浮空下,模拟信号从IO 口进入,上下拉电阻开关都断开(浮空状态),通过TTL 施密特触发器把模拟信号转换为数字信号,再到IDR 寄存器,CPU 再读取寄存器;
浮空输入状态下,STM32读到的电平只根据IO 端口的电平有关;
注:在MCU 程序内对应GPIO 引脚配置为输入浮空模式下,当外部无输入信号且无外部上/下拉电阻时,MCU GPIO 此时呈现高阻态,其电压状态处于不确定状态(具体表现为使用万用表测量,可能只有一点伏,而且上下跳动不稳);
2.3.2 输入上拉
输入上拉与浮空输入的区别在于,输入上拉模式下上拉电阻(约30k~50k ohm)开关接通,外部信号电平通过上拉电阻被拉高到3.3V,即当外部无信号输入(悬空)时,IO 口默认为高电平;
2.3.3 输入下拉
输入下拉与浮空输入的区别在于,输入下拉模式下下拉电阻(约30k~50k ohm)开关接通,外部信号电平通过下拉电阻被拉低到GND,即当外部无信号输入(悬空)时,IO 口默认为低电平;
2.3.4 模拟输入
模拟输入模式下,模拟信号从IO 口进入,上下拉电阻开关都断开,TTL 施密特触发器截止,模拟信号直接输入到片上ADC,此时IDR 寄存器为空,CPU 不能在IDR 寄存器上读到引脚状态;
2.4.5 GPIO 输入检测电平范围
以下表所示,如供电电压VDD = 3.3V,高于0.7VDD = 2.31V 即判定为高电平,低于0.3VDD = 0.99V 即判定为低电平;
2.4 4种输出模式
2.4.1 开漏输出
CPU 先将输出值通过设置BSRR 寄存器和ODR 寄存器写人输出电平值,信号通过输出控制电路和N-Mos 到达输出端口,形成开漏输出;
- 此模式下P-MOS 管不工作;
- 开漏输出的驱动能力比推挽输出弱,即该模式下输出电流较小;
- Case1:CPU 控制IDR 寄存器输出
1
时,N-MOS 管截止,输出呈高阻态,输出的实际信号由IO 口的上下拉电阻决定,其实际输出的电平信号可通过输入浮空模式读回到输入数据寄存器,再由CPU 读取; - Case2:CPU 控制IDR 寄存器输出
0
时,N-MOS 管导通,输出电平信号被N-MOS 管拉到Vss(公共地),IO 口输出低电平0
,同理该输出电平信号也可通过输入浮空模式读回到输入数据寄存器,再由CPU 读取;
@_@
2.4.2 开漏复用输出
开漏复用输出与开漏输出的区别在于其信号来源于片上外设模块而不是CPU;
2.4.3 推挽输出
推挽输出与开漏输出的区别在于推挽输出模式下输出驱动器的P-MOS 管正常工作,即P-MOS 和N-MOS 管同时工作;
MCU 首先将输出值通过设置BSRR 寄存器和ODR 寄存器写入输出电平值,信号通过输出控制电路和P-MOS 和N-Mos 管到达输出端口,形成推挽输出;
-
Case1:CPU 控制IDR 寄存器输出
1
时,P-MOS 管导通,输出电平信号被P-MOS 管拉到Vdd(器件工作电压),IO 口输出高电平1
,该输出电平信号也可通过输入浮空模式读回到输入数据寄存器,再由CPU 读取; -
Case2:与推挽输出模式同理,CPU 控制IDR 寄存器输出
0
时,N-MOS 管导通,输出电平信号被N-MOS 管拉到Vss(公共地),IO 口输出低电平0
,同理该输出电平信号也可通过输入浮空模式读回到输入数据寄存器,再由CPU 读取;
- 当切换输入高低电平时,两个MOS管将轮流导通,一个负责拉电流,电流输入到负载,一个负责灌电流,负载电流流向芯片,使其带负载能力和开关速度都比普通的方式有很大的提高。
- 与开漏输出相比,推挽输出模式可以输出强高低电平,适合连接数字器件,开关速度快,带负载能力强;
⚠注意:STM32的IO口高电平为3.3V,即使用STM32的推挽输出模式输出一个高电平信号时,IO引脚电压为3.3V,假设使用IO引脚控制一个5V的继电器,那么电平就不匹配,这时需将该IO 的输出模式改为开漏输出,让输出的高电平电压取决于外部上拉信号。
2.4.4 推挽复用输出
推挽复用输出与推挽输出区别在于其信号来源于片上外设模块而不是CPU;
2.5 GPIO 寄存器
从1.9.1 片上外设存储器 节获知,GPIO 寄存器地址从总线基地址偏移而来;
STM32的每组GPIOx 口可控制 16个IO(如PA0~PA15),他们都是通过配置GPIOx 中的 7个寄存器来实现配置的,这7个寄存器被定义在下图的结构体变量中:
每个GPIO 寄存器都可在如下图的位表中查看具体每一位的定义与功能:(这里以GPIOx_BSRR
寄存器为例)
- IO 寄存器必须按 32位字被访问(不允许半字或字节访问);
- 有关STM32 片上外设寄存器详细说明,可查看其芯片的参考手册中的寄存器描述部分;
2.5.1 端口配置寄存器(CRL/CRH)
端口配置低寄存器(Port configuration register low,下简称CRL)、端口配置高寄存器(Port configuration register High,下简称CRH)他们只是控制的端口不同,控制原理相同;
由下图可知CRL 共有 32个位(bit),每 4个位控制一个IO 口,也就是它最多只能配置 8个IO 口,另外的 8个由CRH 来控制,这也是需要两个寄存器的原因;
- 端口配置低寄存器(Port configuration register low):控制标号为 0-7的口;
- 端口配置高寄存器(Port configuration register high):控制标号为 8-15的口;
例如:要配置PA0口为最大速度 10MHz的通用推挽输出模式,那就把GPIOA_CRL 的 0~3位配置为
0001
;
2.5.2 端口输入寄存器(IDR)
IDR(Port input data register)的低 16位对应 16个IO 口,它的功能是读取某个IO 口的电平状态,在输出模式下,也可以读取lO口的电平值;
例如:如当前PA0 的输入电平是低电平
0
,此时读取GPIOA_IDR 的第 0位,得到的值就是0
;
2.5.3 端口输出数据寄存器(ODR)
ODR(Port output data register),与IDR 的区别是ODR 是可读可写(rw)寄存器,通过写入该寄存器的某个位,对应的IO 口就会输出对应电平;
例如:把PA0 的输出电平配置为高
1
,配置GPIOA_ODR 的第 0位为1
;
ODR 的另一个重要的功能是当端口被配置为上拉/下拉输入模式时,上拉还是下拉由ODR 配置决定;
例如:已配置GPIOA_CRL 的低 4位为
1000
(即把PA0 配置为上拉/下拉输入模式) ,要使其配置为上拉输入模式,则在GPIOA_ODR中第 0位配置为1
;
2.5.4 端口位配置/清除寄存器(BSRR)
BSRR(Port bit set/reset register )用于配置IO 口的输出电平:
- 低 16位对应位配置为
1
,则对应IO 口输出为高电平,为0
则不产生效果 - 高 16位作用相反
- 它与ODR 的区别:见GPIO 输出模式电路图可知,输出信号来自于CPU 时,BSRR间接控制 ODR,从而决定IO 输出电平,而当输出信号来自于片上外设时,输出电平直接由ODR 决定;
例如:要配置CPU 输出信号到PA0 的电平为
1
,则对GPIOA_BSRR 的第 0位配置为1
即可;
2.5.5 端口位清除寄存器(BRR)
BRR(Port bit reset register)用于配置某位为0
,起到清除寄存器位的功能;
2.5.6 端口配置锁定寄存器(GPIOX_LCKR)
对于LCKP寄存器,当执行正确的写操作设置了位16时,该寄存器用来锁定端口位的配置,位0-15用于锁定GPIO 端口的配置;
当对相应的端口位执行了LOCK 后,在下次系统复位之前,将不能再更改端口位的配置,每个锁定位控制CRL 和CRH 寄存器中相应4个位;
2.6 常用代码
2.6 GPIO应用
2.6.1 点亮一个灯
- 硬件连接:如下,在原理图中使用PB5 引脚来作为LED0 的IO 口,硬件连接中LED 通过一个 510ohm 的电阻拉到 VCC3.3V;
- 控制原理:PB5 输出低电平,LED被导通点亮,GPIOB 输出模式选择最常用的推挽输出模式;
- 软件:
- 需要用到的官方库文件如下:
如没用到串口,
stm32f10x_usart.c
可以删除; - 需要用到的官方库文件如下:
main.c
#include "sys.h"
#include "delay.h"
#include "led.h"
//#include "usart.h" // 串口
int main(void)
{
delay_init(); //延时函数初始化
LED_Init(); //初始化与LED 连接的硬件接口
while(1)
{
LED0=0; // LED 开
delay_ms(300); //延时300ms
LED0=1;
delay_ms(300); //延时300ms
}
}
led.h
#ifndef __LED_H
#define __LED_H
#include "sys.h"
#define LED0 PBout(5) // PB5 输出宏定义
void LED_Init(void); // LED 函数初始化
#endif
led.c
#include "led.h"
// IO初始化
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure; // 定义结构体
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能PB端口时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //LED0-->PB.5 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
GPIO_Init(GPIOB, &GPIO_InitStructure); //根据设定参数初始化GPIOB.5
// GPIO_SetBits(GPIOB,GPIO_Pin_5); //PB.5 输出高
}
- 读取输入电平函数:
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
读取GPIOx 某个特定引脚的输入电平;uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx)
- 读取GPIOx 中所有IO 口的输入电平;- 读取输出电平函数:
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
- 读取GPIOx 某个特定引脚的输出电平;uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx)
- 读取GPIOx 中所有IO 口的输出电平;- 设置输出电平函数:
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
- 配置某特定引脚输出为1
;void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
- 配置某特定引脚输出为0
;void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal)
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal)
2.6.2 STM32F103芯片的PB3 PB4 PA15引脚如何用作普通端口
https://jingyan.baidu.com/article/4f34706e9fd5d4a387b56da6.html
2.6.3 集成RGB LED器件 - WS2812B控制
- 头文件
ws2812.h
:
#ifndef __WS2812_H
#define __WS2812_H
#include "sys.h"
#define WS_ARRAY_SIZE 100
#define IN_H PAout(6)=1;
#define IN_L PAout(6)=0;
// 相对精准的延时
// 注:一个空指令__NOP() 的时间约等于 1000/72 ≈ 14 ns
#define Wait10nop {__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();}
#define Wait250ns {__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();}
#define Wait400ns {Wait250ns;Wait10nop;} //388
#define Wait850ns {Wait250ns;Wait10nop;Wait10nop;Wait10nop;Wait10nop;__NOP();__NOP();__NOP();__NOP();__NOP();} //860
//预定义的颜色RGB值
#define WS_DARK 0,0,0
#define WS_WHITE 255,255,255
#define WS_RED 255,0,0
#define WS_GREEN 0,255,0
#define WS_BLUE 0,0,255
#define WS_YELLOW 255,255,0
#define WS_PURPLE 255,0,255
#define WS_CYAN 0,255,255
#define WS_BROWN 165,42,42
//ws2812初始化
void ws2812_init(void);
//设置第ws_num个灯珠的颜色rgb
void ws2812_rgb(u8 ws_num,u8 ws_r,u8 ws_g,u8 ws_b);
//设置前ws_count个灯珠颜色为rgb
void ws2812_rgb_all(u8 ws_count,u8 ws_r,u8 ws_g,u8 ws_b);
//将最新的ws_data[]数组中的值发送至WS2812B模块
void ws2812_refresh(u8 ws_count);
//颜色设置的数据包之间的时间间隔reset code > 50 us
void ws2812_reset(void);
void send_0(void);
void send_1(void);
#endif //__WS2812_H
- 源文件
ws2812.c
:
#include "ws2812.h"
#include "delay.h"
u8 ws_data[WS_ARRAY_SIZE]={0}; // 该数组用于记录待传输的RGB数据,每一个灯珠的颜色占用三个字节(24 bit)
/***************************************************************************
** 函数名称 : ws2812_init
** 功能描述 : 单片机GPIO初始化
** 输入变量 : 无
** 返 回 值 : 无
** 最后修改人 : xxx
** 最后更新日期: 20210122
** 说 明 : 使用PA6控制WS2812B
***************************************************************************/
void ws2812_init(void) //PA6
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_ResetBits(GPIOA,GPIO_Pin_6);
}
/***************************************************************************
** 函数名称 : ws2812_rgb
** 功能描述 : 设置某一个灯珠的颜色值
** 输入变量 :
ws_num:选择设置第几个LED,以级联顺序而定
ws_r:红色值
ws_g:绿色值
ws_b:蓝色值
** 返 回 值 : 无
** 最后修改人 : xxx
** 最后更新日期: 20210123
** 说 明 : 每一个灯珠的颜色占用三个字节(24 bit),数据传输顺序按GRB顺序传输,注意高位在前,
***************************************************************************/
void ws2812_rgb(u8 ws_num,u8 ws_r,u8 ws_g,u8 ws_b)
{
ws_data[(ws_num-1)*3]=ws_g;
ws_data[(ws_num-1)*3+1]=ws_r;
ws_data[(ws_num-1)*3+2]=ws_b;
}
/***************************************************************************
** 函数名称 : ws2812_rgb_all
** 功能描述 : 设置前几个灯珠的颜色值
** 输入变量 :
ws_count:选择设置前几个LED,以级联顺序而定
ws_r:红色值
ws_g:绿色值
ws_b:蓝色值
** 返 回 值 : 无
** 最后修改人 : xxx
** 最后更新日期: 20210123
** 说 明 : 灯珠的颜色占用三个字节(24 bit),数据传输顺序按GRB顺序传输,注意高位在前,
***************************************************************************/
void ws2812_rgb_all(u8 ws_count,u8 ws_r,u8 ws_g,u8 ws_b)
{
static u8 rgb_wsi;
for(rgb_wsi=1;rgb_wsi<=ws_count;rgb_wsi++)
{
ws_data[(rgb_wsi-1)*3]=ws_g;
ws_data[(rgb_wsi-1)*3+1]=ws_r;
ws_data[(rgb_wsi-1)*3+2]=ws_b;
}
}
/***************************************************************************
** 函数名称 : ws2812_refresh
** 功能描述 : 将最新的ws_data[]数组中的值发送至WS2812B模块
** 输入变量 : ws_count:要发送的数据包数,一个数据包为24bit
** 返 回 值 : 无
** 最后修改人 : xxx
** 最后更新日期: 20210123
** 说 明 : ws_data[] 数组中的每一个字节按位发送,因为高位在前,所以先发送每个字节的高位
***************************************************************************/
void ws2812_refresh(u8 ws_count)
{
u8 ws_ri=0;
for(;ws_ri<ws_count*3;ws_ri++)
{
if((ws_data[ws_ri]&0x80)==0) send_0(); else send_1(); // ws_data[ws_ri]&0x80 的功能是获取最高位的值,以下同理
if((ws_data[ws_ri]&0x40)==0) send_0(); else send_1();
if((ws_data[ws_ri]&0x20)==0) send_0(); else send_1();
if((ws_data[ws_ri]&0x10)==0) send_0(); else send_1();
if((ws_data[ws_ri]&0x08)==0) send_0(); else send_1();
if((ws_data[ws_ri]&0x04)==0) send_0(); else send_1();
if((ws_data[ws_ri]&0x02)==0) send_0(); else send_1();
if((ws_data[ws_ri]&0x01)==0) send_0(); else send_1();
}
//延时一段时间
ws2812_reset();
}
/***************************************************************************
** 函数名称 : ws2812_reset
** 功能描述 : 颜色设置的数据包之间的时间间隔,reset code > 50 us
** 输入变量 : 无
** 返 回 值 : 无
** 最后修改人 : xxx
** 最后更新日期: 20210123
** 说 明 :
***************************************************************************/
void ws2812_reset(void)
{
IN_L; // GPIO输出低电平
delay_us(100);
}
/***************************************************************************
** 函数名称 : send_0
** 功能描述 : 归零码输出低电平0信号
** 输入变量 : 无
** 返 回 值 : 无
** 最后修改人 : xxx
** 最后更新日期: 20210122
** 说 明 :
***************************************************************************/
void send_0(void)
{
IN_H;
Wait400ns;
IN_L;
Wait850ns;
}
/***************************************************************************
** 函数名称 : send_1
** 功能描述 : 归零码输出高电平1信号
** 输入变量 : 无
** 返 回 值 : 无
** 最后修改人 : xxx
** 最后更新日期: 20210122
** 说 明 :
***************************************************************************/
void send_1(void)
{
IN_H;
Wait850ns;
IN_L;
Wait400ns;
}
main.c
:
#include "stm32f10x.h"
#include "string.h"
#include "malloc.h"
#include <stdio.h>
#include <stdlib.h>
#include "delay.h"
#include "usart.h"
#include "common.h"
#include "led.h"
#include "key.h"
#include "ws2812.h"
extern u8 ws_data[];
/***************************************************************************
** 函数名称 : main
** 功能描述 : 工程入口函数
** 输入变量 : 无
** 返 回 值 :
0:程序执行正常
1:程序执行异常
** 最后修改人 : xxx
** 最后更新日期: 20210123
** 说 明 : 无
***************************************************************************/
int main(void)
{
int times = 0;
//初始化
//延时函数初始化
delay_init();
uart_init(115200);
ws2812_init();
printf("System Init OK ...\r\n");
while(1)
{
times++;
if(times > 8)
times = 0;
switch(times)
{
case 0:
ws2812_rgb(1, WS_RED);
ws2812_rgb(2, WS_GREEN);
ws2812_rgb(3, WS_BLUE);
ws2812_rgb(4, WS_WHITE);
ws2812_rgb(5, WS_PURPLE);
ws2812_rgb(6, WS_YELLOW);
ws2812_rgb(7, WS_BROWN);
ws2812_rgb(8, WS_BLUE);
ws2812_refresh(8); // 发送以上8组数据包到WS2812B
break;
case 1:
memset(ws_data,0,WS_ARRAY_SIZE*sizeof(u8));
ws2812_rgb(1, WS_RED);
ws2812_refresh(8);
break;
case 2:
memset(ws_data,0,WS_ARRAY_SIZE*sizeof(u8));
ws2812_rgb(2, WS_GREEN);
ws2812_refresh(8);
break;
case 3:
memset(ws_data,0,WS_ARRAY_SIZE*sizeof(u8));
ws2812_rgb(3, WS_BLUE);
ws2812_refresh(8);
break;
case 4:
memset(ws_data,0,WS_ARRAY_SIZE*sizeof(u8));
ws2812_rgb(4, WS_WHITE);
ws2812_refresh(8);
break;
case 5:
memset(ws_data,0,WS_ARRAY_SIZE*sizeof(u8));
ws2812_rgb(5, WS_PURPLE);
ws2812_refresh(8);
break;
case 6:
memset(ws_data,0,WS_ARRAY_SIZE*sizeof(u8));
ws2812_rgb(6, WS_YELLOW);
ws2812_refresh(8);
break;
case 7:
memset(ws_data,0,WS_ARRAY_SIZE*sizeof(u8));
ws2812_rgb(7, WS_BROWN);
ws2812_refresh(8);
break;
case 8:
memset(ws_data,0,WS_ARRAY_SIZE*sizeof(u8));
ws2812_rgb(8, WS_BLUE);
ws2812_refresh(8);
break;
}
delay_ms(1000);
}
}
2.6.4 1个GPIO控制2个LED的亮灭
环境:MCU GPIO电压3.3V,LED发光电流20mA.
【该理论未实际验证】
1个GPIO控制2个LED的亮灭,共4钟状态。当MCU GPIO资源紧张时可能用到,否则还是建议使用单个GPIO控制一个LED.
-
红灯亮,绿灯灭:GPIO输出高电平
-
红灯灭,绿灯亮:GPIO输出低电平
-
都灭:GPIO设为高阻态,由于两个LED灯总的导通压降要求大于3.3V,所以两个LED灯都不导通
在全灭时,如果led会微亮,可以调大限流电阻,或者换用导通压降更大的led
- 都亮:GPIO交替输出高低电平,只要高低电平切换的频率够高,由于视觉暂留效应,人眼看到的就是两个灯都常亮
注意事项:
- 注意MCU GPIO的电流驱动能力。GPIO能承受的电流要大于LED灯流过的电流。以下截图出自STM32芯片的数据手册,它的GPIO可以驱动或吸入8mA的电流。
- 两个LED漏电流都要很低,否则会出现全灭状态下LED微亮的情况。
2.7 IO扩展(PCA9554/PCA9555)
- 芯片地址定义、初始化:
extern u8 IIC_Channel; // IIC 通道
/* 不同PCA9554的输出初始值
该值需要根据实际应用设置,否则芯片输出可能出现上电后,控制一个输出口实际上多个输出口动作的bug
*/
u8 pca9554_data_temp_U1=0xff;
u8 pca9554_data_temp_U2=0xff;
u8 pca9554_data_temp_U3=0xff;
u8 pca9554_data_temp_U4=0x00;
/* 不同PCA9554的地址 */
u8 Add_MainBoard_U1 = 0x70;
u8 Add_Pca9554apw_U2 = 0x70;
u8 Add_Pca9554apw_U3 = 0x78;
u8 Add_Pca9554apw_U4 = 0x74;
/* PCA9554 初始化 */
void PCA9554_Init(void)
{
IIC_Channel = 3;
PCA9554A_write(Add_MainBoard_U1,0x03,0x00);
delay_ms(10);
IIC_Channel = 6;
PCA9554A_write(Add_Pca9554apw_U2,0x03,0xF8);
delay_ms(10);
IIC_Channel = 7;
PCA9554A_write(Add_Pca9554apw_U3,0x03,0xFF);
delay_ms(10);
IIC_Channel = 8;
PCA9554A_write(Add_Pca9554apw_U4,0x03,0x00);
delay_ms(10);
}
- 读出输入状态:
u8 i=0;
u8 in1=0;
i=PCA9554A_Read(Add_Pca9554apw_U3,0x00,7);// 一次性8个IO全读
in1= i&0x01;//0000 0001 ,提取目标值P7~P0
if(in1== 0x00){Uart4_Printf("in1 触发\r\n");}
else if(in1!= 0x00){Uart4_Printf("in1 未触发\r\n");}
- 输出控制:
IIC_Channel = 3;
PCA9554_OUT_U1(Add_MainBoard_U1,4,OFF);