STM32F103C8T6单片机硬核原理篇:讨论GPIO的基本原理篇章1——只讨论我们的GPIO简单输入和输出
目录
前言
输出时的GPIO控制部分
标准库是如何操作寄存器完成GPIO驱动的初始化的?
问题1:如何掌握GPIO的编程细节——跟寄存器如何打交道
问题2:哪些寄存器,去哪里找呢?
问题三,寄存器的含义,写入的值的含义是如何的?我们想要完成功能需要写什么东西才能达到我们的目的
输入的IDR寄存器
最直接的输出寄存器 ODR寄存器
BRR和BSRR寄存器
标准库/HAL库是如何操作寄存器初始化GPIO的?
标准库GPIO_Init初始化函数
标准库的习惯——检查我们的参数合法性
设置我们的GPIO的Mode
GPIO Mode Binary Encoding Table
区分和判断到底是设置哪一个CR寄存器
HAL库如何初始化引脚的
对GPIO触发写操作,设置GPIO的电平
标准库的GPIO_WriteBit函数
HAL库的HAL_GPIO_WritePin函数
对GPIO触发读操作
GPIO_ReadInputDataBit
HAL_GPIO_ReadPin
前言
你玩够了前面的实验,现在就要准备竖起耳朵听听原理了。GPIO是单片机中很重要的基础外设,是单片机跟外界沟通的桥梁。
查看原理,一个非常好的习惯就是学会阅读我们的单片机手册。


上面的两张图像是我们亲爱的单片机GPIO机制全家福,是我们分析源代码和原理的一个重要的辅助手段,哦,我想你应该再说——这我分析鸡毛啊,对,这个图是8种GPIO模式混合在了一起的图像,而且你发现,实际上,第一张图和第二章图在物理上只有一个区别,那就是最右侧的上拉部分的二极管从VDD换成了VDD_FT,还得的笔者的分析资源手册的部分吗?你会看到一些引脚被标记上了FT,这个意思,是说明我们的单片机是耐压为5V的引脚,凡是没有标记FT的,你需要小心,他们是耐压3.3V的,一旦给大了电压,您的开发板可能就要报废了(芯片引脚烧坏)。
我们上面谈到了——“8种GPIO模式”这个组合,一个很自然的想法在你的脑袋里Rise up了,哪八种啊?不是我回答,我没那个权力,是手册回答的——
| Mode | 模式 | 说明 |
|---|---|---|
| Input floating | 浮空输入 | 输入模式的一个子集:浮空的,换而言之,当没有明确的电平输入的时候,你的GPIO引脚的电平输入猜猜乐状态,如果没有非常明确的理由,不要使用这个模式!!! |
| Input pull-up | 输入上拉 | 输入模式的一个子集:上拉的一个输入,换而言之,没有输入的时候,我们的电平就是出于高电平,你读取这个GPIO的电平就是高电平 |
| Input-pull-down | 输入下拉 | 输入模式的一个子集:下拉的一个输入,换而言之,没有输入的时候,我们的电平就是出于低电平,你读取这个GPIO的电平就是低电平 |
| Analog | 模拟输入 | 输入模式的一个子集:电路层上的模拟,换而言之,我们直接读取了电平的模拟量,也就是能通过一定手段拿到这个GPIO引脚上的电压!(笔者到ADC会好好说明这个事情) |
| Output open-drain | 开漏输出 | 输出模式的一个子集:开漏是MOS管模电的术语,不太懂的朋友我打一个比方:就像是门缝里漏进来了高电平,这个高电平必须是外部别人给的,不是自己给的,这个时候的输出更像是输出一个控制,它本身不提供高电压,他对提供者(比如说你自己接的电池)进行控制,让他输出高电压。 |
| Output push-pull | 推挽输出 | 输出模式的一个子集:还记得笔者说的——强势的输出能力吗,这里,我们的GPIO自己就在使劲的驱动外设,这就是单片机自己强势的控制外部外设而不是依赖于给定的电源,这种就适合驱动小外设,比如说LED,常见的最简单传感器等等 |
| Alternate function push-pull | 复用的推挽输出 | 输出模式的一个子集:推挽输出上面刚说过,在这个事情上没有半毛钱变化.问题在于修饰词AF上,AF就是Alternate Function,也就是复用功能.这个事情对于大部分萌新而言会一脸懵逼,复用?什么复用?答案是,单片机之所以不菜,就是因为他有强大的复用能力,很快你就知道,片上的IIC芯片,SPI芯片等等如何对外沟通的?利用引脚跟外面打交道的,这个时候,GPIO就被征用,用来完成更加详细特定的外部功能了! |
| Alternate function open-drain | 复用的开漏输出 | 输出模式的一个子集:开漏输出还是一样的,复用笔者上一个也说过了,不再重复。 |
现在的学习阶段上,我们不去特别深究电路原理,等到我们真正涉及到电路设计的时候再去了解。目前为之,我们只是接触了最简单的输入和输出,为此,我们来看看手册对这些地方的描述。
输出时的GPIO控制部分
很多文档说的太过于失去把控,将电路实现细节和编程细节全部混合在一起,笔者初学的时候也是非常的头大,这里笔者尝试说明白。
注意,在输出的时候,只有一个地方我们是真正关心的。

笔者告诉你的是。当我们调用GPIO_WriteBits的时候,写入的是Output data寄存器,Outputdata寄存器将我们的行为传递给Output driver输出驱动器子系统,输出驱动器,最关心的就是两个MOS管,也就是P-MOS管和N-MOS管。作为初学,不要立马钻到P-MOS和N-MOS的实现细节里去了,你需要知道的是,当我们处于强势的推挽输出的时候,两个MOS管都工作,共同根据我们的设置切换对应的工作状态,顺着线给出我们的高低电平,从而从I/O Pin上给出单片机自己的高低电平原则。
那开漏呢?答案是PMOS管就不工作了!只有NMOS管干活,也就是他只能输出低电平,高电平呢?不是单片机管的!我上面就说过了,实际上是控制外部电源输出他自己的大电压。电路部分点到位置。
我们作为软件开发,可以不用如此的关心这些细节(让那些硬件佬们操心吧,我每一次看到硬件✌都会开磕),我们关心的是,对于软件编程,库寄存器到底在做什么。
标准库是如何操作寄存器完成GPIO驱动的初始化的?
按照逻辑顺序,我们假设我们已经开启了GPIO时钟,现在,GPIO已经就绪,我们需要安排它的工作任务,我们这里的部分就是在探讨——我们委派GPIO任务的全部编程细节。
回到我们如何使用标准库完成这个事情的:
// 初始化GPIO PA0 完成设置PA0为八大模式下的推挽输出状态gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;gpio_init.GPIO_Speed = GPIO_Speed_50MHz;gpio_init.GPIO_Pin = GPIO_Pin_0;GPIO_Init(GPIOA, &gpio_init);
问题1:如何掌握GPIO的编程细节——跟寄存器如何打交道
江科大的笔者听过,大佬是如何分析寄存器的,说真的把我吓跑了,因为真的不太理解。我们知道,单片机的核心实际上就是操作寄存器,甚至广义的说——你的电脑现在的工作,就是我们无时不刻疯狂的操作寄存器设置处理器的状态,接受和传递处理器的结果。所以,我们在硬件和软件上的衔接,就是寄存器,理解了寄存器要如何操作,我们实际上就可以完成对我们手头器件的编程了,看过笔者手搓操作系统系列的朋友应该会深有体会。
问题2:哪些寄存器,去哪里找呢?
数据手册的第一个重要含义,就是告诉你,当你面对一个外设的时候,他有哪些基本我们需要关心的寄存器,哪些我们需要关心的概念。比如说八大寄存器的模式啊,需要操作的寄存器啊,等等内容
约定俗称的讲,我们的单片机往往分为控制寄存器和数据寄存器,控制寄存器控制我们的片上外设/片外外设的行为,数据寄存器那就是真往里写东西的。换而言之,数据寄存器是货,控制寄存器是人,你指挥人,让他们正确的处理货物。所以,我们实际上就是看——如何编程这些寄存器呢?
我们的手册上给出了我们想要的东西,所以任何问题,先找手册,不是没有道理的,请看STM32F1xx系列的手册,在我们的第159页上的9.1,也就是第一大点,存在这样的一段话,笔者认为非常的关键,需要放到这里
Each of the general-purpose I/O ports has two 32-bit configuration registers (GPIOx_CRL, GPIOx_CRH), two 32-bit data registers (GPIOx_IDR, GPIOx_ODR), a 32-bit set/reset register (GPIOx_BSRR), a 16-bit reset register (GPIOx_BRR) and a 32-bit locking register (GPIOx_LCKR).
每个通用 I/O 端口都有两个 32 位配置寄存器(GPIOx_CRL、GPIOx_CRH)、两个 32 位数据寄存器(GPIOx_IDR、GPIOx_ODR)、一个 32 位置位/复位寄存器(GPIOx_BSRR)、一个 16 位复位寄存器(GPIOx_BRR)和一个 32 位锁定寄存器(GPIOx_LCKR)。
啊哈,这个没啥好说的,首先就是我们需要说的,关心的寄存器上,就是控制寄存器GPIOx_CRL和GPIOx_CRH,这就是GPIO为什么分组了,我们对每一个组的GPIO都配备了专门的控制寄存器和输入输出的缓存寄存器。我们对GPIO的编程,实际上就是围绕着这七个寄存器来说话的。
问题三,寄存器的含义,写入的值的含义是如何的?我们想要完成功能需要写什么东西才能达到我们的目的
这个问题就是数据手册存在的第二个重要的含义,举个例子,我们进一步阅读手册
Each I/O port bit is freely programmable, however the I/O port registers have to be accessed as 32-bit words (half-word or byte accesses are not allowed). The purpose of the GPIOx_BSRR and GPIOx_BRR registers is to allow atomic read/modify accesses to any of the GPIO registers. This way, there is no risk that an IRQ occurs between the read and the modify access.
每个 I/O 端口位均可自由编程,但 I/O 端口寄存器必须以 32 位字的形式进行访问(不允许半字或字节访问)。GPIOx_BSRR 和 GPIOx_BRR 寄存器的目的是允许对任何 GPIO 寄存器进行原子读取/修改访问。这样,就不会有在读取和修改访问之间发生 IRQ 的风险。
因此,我们知道,实际上我们不需要太关心ODR寄存器,他的作用实际上是作为BRR和BSRR寄存器的实际执行官,或者说,充当一个输出的影子。毕竟中断的存在的确会带来打断前后状态不一致的风险,我们自己操作寄存器的时候,也要操作的是GPIOx_BSRR 寄存器和GPIOx_BRR!不要更改ODR寄存器!
先关心的是控制,控制寄存器分为两个:低32位的GPIOx_CRL和高32位的GPIOx_CRH寄存器。我们在配置模式的时候,实际上也是针对我们的控制寄存器进行编程,请看171页的手册:

笔者下面教授我们如何思考和阅读这样的手册,第一个问题,想清楚我们要干啥。“我们现在要点LED”,”我们现在要让GPIO读取外部的电平...”,想明白这些事情,我们才能以正确的姿势阅读手册。
我们看,对于低32位的寄存器,我们实际上每一个Pin的信息占据4位。举个例子,CNF0和MODE0位就是针对我们的PIN_0说的,因此,当我们想要配置PA0的时候,我们实际上就只需要操作GPIOA的最低四位,这样就完事了。

那我们应该如何正确的写入值呢?看到上面的表格,上面的表格中,我们就完成了寄存器的模式设置。举个例子,在点灯部分的时候,我们就说过需要设置GPIO为推挽输出,因此,我们就需要设置我们的比特位是:CNF00,然后Mode呢,我们选择的是50MHZ的速度,因此就是0011.你可以使用自己写的点亮小灯的例程中进入调试模式来确认额这个事情


非常好!你已经迈出了学习看手册和验证的第一步了。下一步,请你试一试求证一下按钮输入输出的控制寄存器的值如何查找!
提示:我们学习按钮的时候,GPIO是按照输入上拉进行配置的。此外,你需要考证下我们的ODR寄存器的值,这个算额外的挑战
下一个问题就是,我们编程的时候,GPIO的输入和输出寄存器又是怎么一回事呢?请看手册
输入的IDR寄存器
IDR寄存器负责处理我们的输入,我们实际上ReadPin的时候,也是读取的IDR寄存器,他会存储我们的读取的电平的状态。
The Input Data register (GPIOx_IDR) captures the data present on the I/O pin at every APB2 clock cycle.
输入数据寄存器(GPIOx_IDR)在每个 APB2 时钟周期捕获 I/O 引脚上的数据。
关于何为APB2,这个事情笔者放到时钟树的介绍的时候,我们好好聊聊.

所以,我们想要得到GPIO外部引脚的电平的状态,只需要读取我们的IDR寄存器即可.我们后面分析标准库的ReadGPIOPinBits和HAL_GPIO_ReadPin函数的时候,还会回来好好看人家如何使用C语言代码完成这个工作的
最直接的输出寄存器 ODR寄存器
When configured as output, the value written to the Output Data register (GPIOx_ODR) is output on the I/O pin. It is possible to use the output driver in Push-Pull mode or Open-Drain mode (only the N-MOS is activated when outputting 0).
当配置为输出时,写入输出数据寄存器 (GPIOx_ODR) 的值将在 I/O 引脚上输出。输出驱动器可以在推挽模式或开漏模式下使用(输出 0 时仅激活 N-MOS)。
需要注意的是,我们的写入是非常直接的,最需要操作的安全办法是使用位操作来办事情.

These bits can be read and written by software and can be accessed in Word mode only. Note: For atomic bit set/reset, the ODR bits can be individually set and cleared by writing to the GPIOx_BSRR register (x = A .. G).
这些位可以由软件读取和写入,并且只能在字模式下访问。 注意:对于原子位设置/重置,可以通过写入 GPIOx_BSRR 寄存器(x = A .. G)单独设置和清除 ODR 位。
一般而言,我们也只会操作一个pin一个pin的操作,而且往往不希望中断打断我们的设置导致非常不安全的写入,因此,让我们进一步学习BSRR寄存器的写和BRR寄存器的写.
BRR和BSRR寄存器

BSRR寄存器是只能写入的,如手册所说!这里我们看到,这个32位的寄存器分成了上下两部分,高16位是Port Reset Bit,也就是说,往这里写东西的时候,对应的Pin上的ODR的位就会被复位为1,反之,则是0.
BRR寄存器的功能是对我们的指定的位进行清零.嗯,好像跟BSRR重复了?是这样的!

哦对了,新一点的STM32系列是没有BRR寄存器了,下面是笔者的STM32F407ZGT6对GPIO操作的手册,所以,ST半导体也认为这个寄存器实在是没有存在的必要,也就被精简掉了.
基本上我们的寄存器也就说完了.
标准库/HAL库是如何操作寄存器初始化GPIO的?
标准库GPIO_Init初始化函数
我们看看GPIO_Init函数针对设置GPIO为一个推挽输出的时候的程序流。
-
模式设置
-
如果我们的Pin是低8个,则设置设置Control Register的低位寄存器. 反之,设置Control Register的高位寄存器
标准库的习惯——检查我们的参数合法性
我们编程的时候,也最好检查一下参数合法性质的问题,不然程序出现了异常,就会导致整个系统崩溃重启,这可就不好了。
/*** @brief Initializes the GPIOx peripheral according to the specified* parameters in the GPIO_InitStruct.* @param GPIOx: where x can be (A..G) to select the GPIO peripheral.* @param GPIO_InitStruct: pointer to a GPIO_InitTypeDef structure that* contains the configuration information for the specified GPIO peripheral.* @retval None*/
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{uint32_t currentmode = 0x00, currentpin = 0x00, pinpos = 0x00, pos = 0x00;uint32_t tmpreg = 0x00, pinmask = 0x00;/* Check the parameters */assert_param(IS_GPIO_ALL_PERIPH(GPIOx));assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode));assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin));
之后的合法性检查的代码笔者将不会进行任何的说明,除非特殊!
设置我们的GPIO的Mode
/*---------------------------- GPIO Mode Configuration -----------------------*/currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F);if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00){ /* Check the parameters */assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed));/* Output mode */currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;}
这是干啥呢?答案是,设置我们的GPIO速度!嗯,你想想看,设置速度的信息,必须是输出,对吧。我们的(((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00实际上就是在检查我们的第5位。嗯,整个需要请出来我们的GPIO库对标准库的抽象
typedef enum
{ GPIO_Mode_AIN = 0x0,GPIO_Mode_IN_FLOATING = 0x04,GPIO_Mode_IPD = 0x28,GPIO_Mode_IPU = 0x48,GPIO_Mode_Out_OD = 0x14,GPIO_Mode_Out_PP = 0x10,GPIO_Mode_AF_OD = 0x1C,GPIO_Mode_AF_PP = 0x18
}GPIOMode_TypeDef;
GPIO Mode Binary Encoding Table
| Enum Name | Hex Value | Binary (8-bit) | CNF[1:0] | MODE[1:0] | Description |
|---|---|---|---|---|---|
| GPIO_Mode_AIN | 0x0 | 0000 0000 | 00 | 00 | Analog Input (ADC/DAC) |
| GPIO_Mode_IN_FLOATING | 0x04 | 0000 0100 | 01 | 00 | Floating Input (no pull-up/down) |
| GPIO_Mode_IPD | 0x28 | 0010 1000 | 10 | 00 | Input with Pull-Down |
| GPIO_Mode_IPU | 0x48 | 0100 1000 | 10 | 00 | Input with Pull-Up |
| GPIO_Mode_Out_OD | 0x14 | 0001 0100 | 01 | 11 | Output Open-Drain (max 50MHz) |
| GPIO_Mode_Out_PP | 0x10 | 0001 0000 | 00 | 11 | Output Push-Pull (max 50MHz) |
| GPIO_Mode_AF_OD | 0x1C | 0001 1100 | 01 | 11 | Alternate Function Open-Drain |
| GPIO_Mode_AF_PP | 0x18 | 0001 1000 | 00 | 11 | Alternate Function Push-Pull |
区分和判断到底是设置哪一个CR寄存器
我们的标准库的设置方法比较的,嗯,天才
#define GPIO_Pin_0 ((uint16_t)0x0001) /*!< Pin 0 selected */
#define GPIO_Pin_1 ((uint16_t)0x0002) /*!< Pin 1 selected */
#define GPIO_Pin_2 ((uint16_t)0x0004) /*!< Pin 2 selected */
#define GPIO_Pin_3 ((uint16_t)0x0008) /*!< Pin 3 selected */
#define GPIO_Pin_4 ((uint16_t)0x0010) /*!< Pin 4 selected */
#define GPIO_Pin_5 ((uint16_t)0x0020) /*!< Pin 5 selected */
#define GPIO_Pin_6 ((uint16_t)0x0040) /*!< Pin 6 selected */
#define GPIO_Pin_7 ((uint16_t)0x0080) /*!< Pin 7 selected */
#define GPIO_Pin_8 ((uint16_t)0x0100) /*!< Pin 8 selected */
#define GPIO_Pin_9 ((uint16_t)0x0200) /*!< Pin 9 selected */
#define GPIO_Pin_10 ((uint16_t)0x0400) /*!< Pin 10 selected */
#define GPIO_Pin_11 ((uint16_t)0x0800) /*!< Pin 11 selected */
#define GPIO_Pin_12 ((uint16_t)0x1000) /*!< Pin 12 selected */
#define GPIO_Pin_13 ((uint16_t)0x2000) /*!< Pin 13 selected */
#define GPIO_Pin_14 ((uint16_t)0x4000) /*!< Pin 14 selected */
#define GPIO_Pin_15 ((uint16_t)0x8000) /*!< Pin 15 selected */
#define GPIO_Pin_All ((uint16_t)0xFFFF) /*!< All pins selected */
发现规律了把,实际上就是GPIO_Pin_X就是左移了一个X位,所以你看:我们下面对位检测的处理是这样做的:
/*---------------------------- GPIO CRL Configuration ------------------------*//* Configure the eight low port pins */// 如果是低8pin,那么。。。if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00){// 我们需要处理的寄存器就是CRL寄存器tmpreg = GPIOx->CRL;for (pinpos = 0x00; pinpos < 0x08; pinpos++){// 我们遍历一下,看看对应GPIO_PIN的位是不是被置1了pos = ((uint32_t)0x01) << pinpos;/* Get the port pins position */currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;if (currentpin == pos){// 是的,我们放上了1,这个时候,我们需要做的就是萃取控制的4个位。// 对于Pin0,则是0~3位,对于Pin1,则是4~7位。。。一次类推,所以我们才需要左移2,相当于x4了pos = pinpos << 2;/* Clear the corresponding low control register bits */pinmask = ((uint32_t)0x0F) << pos;// 本来是1111,这一下翻转了成了0000tmpreg &= ~pinmask;/* Write the mode configuration in the corresponding bits */// current mode是只取了低4位的valuetmpreg |= (currentmode << pos);/* Reset the corresponding ODR bit */// 如果是下拉的输入,那么我们对对应的BRR寄存器置1,等价于清0了if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD){GPIOx->BRR = (((uint32_t)0x01) << pinpos);}else{/* Set the corresponding ODR bit */if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU){// 反之,那就是置高电平GPIOx->BSRR = (((uint32_t)0x01) << pinpos);}}}}// 写回去值GPIOx->CRL = tmpreg;}
剩下的CRH寄存器的逻辑完全一致,请看官自行分析,笔者的代码放到下面了
/*---------------------------- GPIO CRH Configuration ------------------------*//* Configure the eight high port pins */if (GPIO_InitStruct->GPIO_Pin > 0x00FF){tmpreg = GPIOx->CRH;for (pinpos = 0x00; pinpos < 0x08; pinpos++){pos = (((uint32_t)0x01) << (pinpos + 0x08));/* Get the port pins position */currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);if (currentpin == pos){pos = pinpos << 2;/* Clear the corresponding high control register bits */pinmask = ((uint32_t)0x0F) << pos;tmpreg &= ~pinmask;/* Write the mode configuration in the corresponding bits */tmpreg |= (currentmode << pos);/* Reset the corresponding ODR bit */if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD){GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));}/* Set the corresponding ODR bit */if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU){GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));}}}GPIOx->CRH = tmpreg;}
}
HAL库如何初始化引脚的
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
{// 一些无关的事情,笔者略去了
/* 配置端口引脚 - 遍历所有需要配置的引脚 */while (((GPIO_Init->Pin) >> position) != 0x00u){/* 获取当前引脚位置掩码 */ioposition = (0x01uL << position);
/* 获取当前需要配置的引脚 */iocurrent = (uint32_t)(GPIO_Init->Pin) & ioposition;
/* 如果当前引脚需要配置 */if (iocurrent == ioposition){// 这里是我们终点分析的逻辑 // < --------}
position++; // 检查下一个引脚}
}
我们先看看大致的框架,这里我们的代码写的更加明白一些,那就是当我们当然的与结果为1,说明我们的确需要配置GPIO的引脚,这个时候,我们就会开始进行配置。跟标准库的流程是完全一致的是,我们首先会设置好4位GPIO的configurations
/* 检查复用功能参数 */assert_param(IS_GPIO_AF_INSTANCE(GPIOx));/* 根据所需模式,填充config变量中的MODE[1:0]和CNF[1:0]位 */// 在这里,我们实际上是做自己的config向寄存器位的映射switch (GPIO_Init->Mode){/* 配置为推挽输出模式 */case GPIO_MODE_OUTPUT_PP:/* 检查速度参数 */assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));config = GPIO_Init->Speed + GPIO_CR_CNF_GP_OUTPUT_PP; // 速度模式 + 推挽输出配置break;/* 配置为开漏输出模式 */case GPIO_MODE_OUTPUT_OD:assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));config = GPIO_Init->Speed + GPIO_CR_CNF_GP_OUTPUT_OD; // 速度模式 + 开漏输出配置break;/* 配置为复用功能推挽模式 */case GPIO_MODE_AF_PP:assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));config = GPIO_Init->Speed + GPIO_CR_CNF_AF_OUTPUT_PP; // 速度模式 + 复用推挽配置break;/* 配置为复用功能开漏模式 */case GPIO_MODE_AF_OD:assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));config = GPIO_Init->Speed + GPIO_CR_CNF_AF_OUTPUT_OD; // 速度模式 + 复用开漏配置break;/* 配置为输入模式(也适用于事件和中断模式) */case GPIO_MODE_INPUT:case GPIO_MODE_IT_RISING:case GPIO_MODE_IT_FALLING:case GPIO_MODE_IT_RISING_FALLING:case GPIO_MODE_EVT_RISING:case GPIO_MODE_EVT_FALLING:case GPIO_MODE_EVT_RISING_FALLING:/* 检查上拉/下拉参数 */assert_param(IS_GPIO_PULL(GPIO_Init->Pull));if (GPIO_Init->Pull == GPIO_NOPULL){config = GPIO_CR_MODE_INPUT + GPIO_CR_CNF_INPUT_FLOATING; // 浮空输入}else if (GPIO_Init->Pull == GPIO_PULLUP){config = GPIO_CR_MODE_INPUT + GPIO_CR_CNF_INPUT_PU_PD; // 上拉输入/* 设置对应的ODR位(上拉) */GPIOx->BSRR = ioposition;}else /* GPIO_PULLDOWN */{config = GPIO_CR_MODE_INPUT + GPIO_CR_CNF_INPUT_PU_PD; // 下拉输入/* 清除对应的ODR位(下拉) */GPIOx->BRR = ioposition;}break;/* 配置为模拟输入模式 */case GPIO_MODE_ANALOG:config = GPIO_CR_MODE_INPUT + GPIO_CR_CNF_ANALOG; // 模拟输入break;/* 默认情况(参数已通过assert_param检查) */default:break;}
组合好了4个位之后,就是做实际上的寄存器修改了
/* 判断当前引脚属于低8位(CRL)还是高8位(CRH) */configregister = (iocurrent < GPIO_PIN_8) ? &GPIOx->CRL : &GPIOx->CRH;registeroffset = (iocurrent < GPIO_PIN_8) ? (position << 2u) : ((position - 8u) << 2u);/* 将新的引脚配置应用到寄存器 */MODIFY_REG((*configregister), ((GPIO_CRL_MODE0 | GPIO_CRL_CNF0) << registeroffset), (config << registeroffset));// 后面,我们略去了对中断部分的设置
MODIFY_REG实际上是HAL库封装的一个通用的宏,这个宏的原型如下:
#define SET_BIT(REG, BIT) ((REG) |= (BIT))#define CLEAR_BIT(REG, BIT) ((REG) &= ~(BIT))#define READ_BIT(REG, BIT) ((REG) & (BIT))#define CLEAR_REG(REG) ((REG) = (0x0))#define WRITE_REG(REG, VAL) ((REG) = (VAL))#define READ_REG(REG) ((REG))#define MODIFY_REG(REG, CLEARMASK, SETMASK) WRITE_REG((REG), (((READ_REG(REG)) & (~(CLEARMASK))) | (SETMASK)))
嗯,实际上就是合并了之前的操作,从语句直接编程了宏,编译速度的确会得到一定的提升。
对GPIO触发写操作,设置GPIO的电平
标准库的GPIO_WriteBit函数
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal)
{/* Check the parameters */assert_param(IS_GPIO_ALL_PERIPH(GPIOx));assert_param(IS_GET_GPIO_PIN(GPIO_Pin));assert_param(IS_GPIO_BIT_ACTION(BitVal)); if (BitVal != Bit_RESET){GPIOx->BSRR = GPIO_Pin;}else{GPIOx->BRR = GPIO_Pin;}
}
很容易想到吧!注意了,因为BSRR和BRR寄存器可以做到直接原子化的设置我们的GPIO状态,因此使用BSRR和BRR寄存器显然更加的安全
HAL库的HAL_GPIO_WritePin函数
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
{/* Check the parameters */assert_param(IS_GPIO_PIN(GPIO_Pin));assert_param(IS_GPIO_PIN_ACTION(PinState));if (PinState != GPIO_PIN_RESET){GPIOx->BSRR = GPIO_Pin;}else{GPIOx->BSRR = (uint32_t)GPIO_Pin << 16u;}
}
看到HAL库为什么新了吧,我们前面说过,BRR寄存器已经淘汰了,这就是一个体现,即使是旧单片机,我们也不去操作过时的外设,保证尽可能少的修改代码。
对GPIO触发读操作
GPIO_ReadInputDataBit
我认为无需多言,读取IDR寄存器即可
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{uint8_t bitstatus = 0x00;/* Check the parameters */assert_param(IS_GPIO_ALL_PERIPH(GPIOx));assert_param(IS_GET_GPIO_PIN(GPIO_Pin)); if ((GPIOx->IDR & GPIO_Pin) != (uint32_t)Bit_RESET){bitstatus = (uint8_t)Bit_SET;}else{bitstatus = (uint8_t)Bit_RESET;}return bitstatus;
}
HAL_GPIO_ReadPin
/*** @brief Reads the specified input port pin.* @param GPIOx: where x can be (A..G depending on device used) to select the GPIO peripheral* @param GPIO_Pin: specifies the port bit to read.* This parameter can be GPIO_PIN_x where x can be (0..15).* @retval The input port pin value.*/
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{GPIO_PinState bitstatus;/* Check the parameters */assert_param(IS_GPIO_PIN(GPIO_Pin));if ((GPIOx->IDR & GPIO_Pin) != (uint32_t)GPIO_PIN_RESET){bitstatus = GPIO_PIN_SET;}else{bitstatus = GPIO_PIN_RESET;}return bitstatus;
}
如你所见,一样是直接读取了我们的GPIO的IDR寄存器作为一个返回值
相关文章:
STM32F103C8T6单片机硬核原理篇:讨论GPIO的基本原理篇章1——只讨论我们的GPIO简单输入和输出
目录 前言 输出时的GPIO控制部分 标准库是如何操作寄存器完成GPIO驱动的初始化的? 问题1:如何掌握GPIO的编程细节——跟寄存器如何打交道 问题2:哪些寄存器,去哪里找呢? 问题三,寄存器的含义ÿ…...
UniApp集成极光推送详细教程
最近项目要集成推送服务,选型极光推送,记录一下开发过程。 1、极光官网注册登录 1.1选择极光推送产品,新建应用 1.2在下一步中选择Android/IOS的消息推送服务 1.3产品设置中输入应用包名(一经输入后不可更改,一定要正…...
探索Doris:日志分析的新宠,是否能取代老牌ES?
在大数据时代,日志存储与分析对于企业的运营和决策起着至关重要的作用。Elasticsearch(简称 ES)作为一款广泛应用的开源分布式搜索和分析引擎,长期以来在日志管理领域占据着举足轻重的地位。然而,随着技术的不断发展&a…...
HCIA/HCIP基础知识笔记汇总
HCIA/HCIP基础知识笔记汇总 ICT产业链: 上游:芯片制造、元器件生产、光纤光缆制造 中游:硬件组装、软件开发、网络建设维护 下游:电信服务、互联网服务、终端产品 VLAN端口类型: access :…...
AI战略群与星际之门:软银AI投资版图计划深度解析
一、星际之门:万亿美元级 AI 基础设施革命 1.1 项目背景与战略定位 在 AI 技术迅猛发展的今天,算力已成为推动其前进的核心动力。软银联合 OpenAI、甲骨文、英伟达、微软、arm推出的 “星际之门”(Stargate)计划,无疑是 AI 领域的一颗重磅炸弹。作为 AI 领域史上最大单笔…...
系统思考与时间管理
时间管理的真正秘诀:主动浪费时间? 巴菲特的私人飞机驾驶员觉得自己不够成功,于是向巴菲特请教应该怎么做。巴菲特让他列出了自己人生中最想实现的25个目标,并按重要程度排序,接着安排时间专注做前五件最重要的事情。…...
mac air m系列arm架构芯片安装虚拟机 UTM+debian 浏览器firefox和chrome
成果展示:debian虚拟机,你值得拥有! 预期结果 1、mac的m系列芯片,arm 架构且内存小,安装虚拟机。 考虑到mac m系列芯片8g内存,arm架构想安装一个轻量的虚拟机,偶然之间发现了debian,…...
大模预测法洛四联症的全方位研究报告
目录 一、引言 1.1 研究背景与意义 1.2 研究目的与创新点 二、法洛四联症概述 2.1 病理特征 2.2 临床表现 2.3 现有治疗手段 三、大模预测法洛四联症的原理与模型构建 3.1 大模预测基本原理 3.2 模型构建的数据收集与处理 3.3 模型训练与优化 四、术前风险预测与准…...
Keepalived+LVS+nginx高可用架构
注明:所有软件已经下载好,防火墙和SELinux已经全部关闭 一.搭建NFS 1.服务端 1.创建文件 [rootnfs ~]# mkdir -p /nfs/data 2、修改权限 [rootnfs ~]# chmod orw /nfs/data 3、写配置文件 [rootnfs ~]# cat /etc/exports /nfs/data 192.168.111.118(r…...
【力扣hot100题】(034)LRU缓存
做完这题已经没有任何力气写链表题了。 思路很简单,就是调试特别的痛苦。 老是频频报错,唉。 class LRUCache { public:struct ListNode{int key,val;ListNode* next; ListNode* prev;ListNode() : key(0), val(0), next(nullptr), prev(nullptr) {}L…...
【redis】缓存 更新策略(定期、实时生存),缓存预热、穿透、雪崩、击穿详解
什么是缓存 redis 最常用的场景 核心思路就是把一些常用的数据,放到触手可及(访问速度更快)的地方 ⽐如我需要去⾼铁站坐⾼铁. 我们知道坐⾼铁是需要反复刷⾝份证的 (进⼊⾼铁站, 检票, 上⻋,乘⻋过程中, 出站…)正常来说, 我的⾝份证是放在…...
好文和技术网站记录
后续不断记录一些本人觉得的好文和一些技术网站 技术网站 Java 全栈知识体系 https://www.pdai.tech/ 文章 利用 NginxKeepalived 实现高可用技术 https://cloud.tencent.com/developer/article/1647182?policyId1004...
使用STM32CubeMX和Keil在STM32上创建并运行一个简单的FreeRTOS多任务程序
目标 利用FreeRTOS运行两个任务,分别为点灯和OLED屏的显示。 利用STM32CubeMX生成Keil工程和相关初始化代码 知识回顾 之前已经利用STM32CubeMX生成过Keil工程和相关初始化代码了,可以去回顾一下,详情见:https://blog.csdn.ne…...
从查重报告入手的精准论文降重秘籍
每个同学在使用论文查重时,为何同一篇文章,可能重复率从10%—30%不等?归根结底还是使用了不同查重系统。其实不同的论文查重与论文AIGC检测系统的算法、数据及模型都不一样,那如何针对这些系统的“个性”精准降重,这篇…...
车辆控制解决方案
车辆控制解决方案 /* * Purpose: 优化车辆控制的功能 -> 用户在控制车辆状态时,实现控制按钮点击状态改变只触发一次onSwitchChange事件,不再下发控制指令,同时清除加载车辆实时状态的定时器status_interval直到有返回值再开启࿰…...
【机器学习】嘿马机器学习(算法篇)第14篇:决策树算法,学习目标【附代码文档】
本教程的知识点为:机器学习算法定位、 K-近邻算法 1.4 k值的选择 1 K值选择说明 1.6 案例:鸢尾花种类预测--数据集介绍 1 案例:鸢尾花种类预测 1.8 案例:鸢尾花种类预测—流程实现 1 再识K-近邻算法API 1.11 案例2:预测…...
Uubuntu20.04复现SA-ConvONet步骤
项目地址: tangjiapeng/SA-ConvONet: ICCV2021 Oral SA-ConvONet: Sign-Agnostic Optimization of Convolutional Occupancy Networks 安装步骤: 一、系统更新 检查系统是否已经更新到最新版本: sudo apt-get update sudo apt-get upgra…...
设计模式 三、结构型设计模式
一、代理模式 代理设计模式(Proxy Design Pattern)是一种结构型设计模式,它为其他对象提供了一个代理,以控制对这个对象的访问。 代理模式可以用于实现懒加载、安全访问控制、日志记录等功能。简单来说,代理模式 就是通…...
C语言函数实战指南:从零到一掌握函数设计与10+案例解析(附源码)
一、函数基础:程序的“积木块” (一)什么是函数? 函数是可重复使用的代码块,用于实现特定功能。如同乐高积木,通过组合不同函数,可快速构建复杂程序。例如: #include <stdio.h>// 函数定义:计算两数之和 int add(int a, int b) {return a + b; }int main() {…...
Prompt攻击是什么
什么是Prompt攻击 Prompt攻击(Prompt Injection/Attack) 是指通过精心构造的输入提示(Prompt),诱导大语言模型(LLM)突破预设安全限制、泄露敏感信息或执行恶意操作的攻击行为。其本质是利用模型对自然语言的理解漏洞,通过语义欺骗绕过防护机制。 Prompt攻击的精髓:学…...
【Linux网络#18】:深入理解select多路转接:传统I/O复用的基石
📃个人主页:island1314 🔥个人专栏:Linux—登神长阶 目录 一、前言:🔥 I/O 多路转接 为什么需要I/O多路转接? 二、I/O 多路转接之 select 1. 初识 select2. select 函数原型2.1 关于 fd_set 结…...
华院计算3项应用成果入选钢铁行业智能制造解决方案推荐目录(2024年)
近日,中国钢铁工业协会发布《钢铁行业智能制造解决方案推荐目录(2024年)》。由中国钢铁工业协会、钢铁行业智能制造联盟共同开展了2024年钢铁行业智能制造解决方案及数字化转型典型场景应用案例遴选、智能制造创新大赛(钢铁行业赛…...
python使用cookie、session、selenium实现网站登录(爬取信息)
一、使用cookie 这段代码演示了如何使用Python的urllib和http.cookiejar模块来实现网站的模拟登录,并在登录后访问需要认证的页面。 # 导入必要的库 import requests from urllib import request, parse# 1. 导入http.cookiejar模块中的CookieJar类,用…...
vector模拟实现2
文章目录 vector的模拟实现erase函数resize拷贝构造赋值重载函数模版构造及其细节结语 我们今天又见面啦,给生活加点impetus!!开启今天的编程之路 今天我们来完善vector剩余的内容,以及再探迭代器失效! 作者ÿ…...
观察者模式在Java单体服务中的运用
观察者模式主要用于当一个对象发生改变时,其关联的所有对象都会收到通知,属于事件驱动类型的设计模式,可以对事件进行监听和响应。下面简单介绍下它的使用: 1 定义事件 import org.springframework.context.ApplicationEvent;pu…...
详解相机的内参和外参,以及内外参的标定方法
1 四个坐标系 要想深入搞清楚相机的内参和外参含义, 首先得清楚以下4个坐标系的定义: 世界坐标系: 名字看着很唬人, 其实没什么大不了的, 这个就是你自己定义的某一个坐标系。 比如, 你把房间的某一个点定…...
在线sql 转 rust 模型(Diesel、SeaORM),支持多数据 mysql, pg等
SQL 转 Rust 在 Rust 语言中,常用 Diesel 和 SeaORM 进行数据库操作。手写 ORM 模型繁琐,gotool.top 提供 SQL 转 Diesel、SeaORM 工具,自动生成 Rust 代码,提高开发效率。 特色 支持 Diesel / SeaORM,生成符合规范…...
高并发内存池(二):Central Cache的实现
前言:本文将要讲解的高并发内存池,它的原型是Google的⼀个开源项⽬tcmalloc,全称Thread-Caching Malloc,近一个月我将以学习为目的来模拟实现一个精简版的高并发内存池,并对核心技术分块进行精细剖析,分享在…...
[Windows] VutronMusic v1.6.0 音乐播放器纯净版,可登录同步
VutronMusic-简易好看的PC音乐播放器 链接:https://pan.xunlei.com/s/VOMq7P_fTyhLUXeGerDVhrCTA1?pwduvut# VutronMusic v1.6.0 音乐播放器纯净版,可登录同步...
macvlan 和 ipvlan 实现原理及设计案例详解
一、macvlan 实现原理 1. 核心概念 macvlan 允许在单个物理网络接口上创建多个虚拟网络接口,每个虚拟接口拥有 独立的 MAC 地址 和 IP 地址。工作模式: bridge 模式(默认):虚拟接口之间可直接通信,类似交…...
