Day 1 使用GPIO点亮一个LED灯
一. GPIO
1. 介绍
叫通用输入输出口,可以用来连接各种硬件设备,实现功能的实现,或者数据的传输/交互
2. 结构
-
上半部分(输入功能)
输出缓冲器被禁止,施密特触发输入被激活,根据输入配置(上拉,下拉或浮动)的不同,弱上拉和下拉电阻被连接, 出现在I/O脚上的数据在每个APB2时钟被采样到输入数据寄存器,对输入数据寄存器的读访问可得到I/O状
-
下半部分(输出功能)
输出缓冲器被激活
─ 开漏模式:输出寄存器上的’0’激活N-MOS,而输出寄存器上的’1’将端口置于高阻状态(PMOS从不被激活)。
─ 推挽模式:输出寄存器上的’0’激活N-MOS,而输出寄存器上的’1’将激活P-MOS。
● 施密特触发输入被激活
● 弱上拉和下拉电阻被禁止
● 出现在I/O脚上的数据在每个APB2时钟被采样到输入数据寄存器
● 在开漏模式时,对输入数据寄存器的读访问可得到I/O状态
● 在推挽式模式时,对输出数据寄存器的读访问得到最后一次写的值。
3. 八种模式
-
输入浮空 :意思就是浮在空中,输入的电平信号具有不确定性,可能是低电平也可能是高电平
-
输入上拉 :将输入电平拉高,使其变为高电平
-
输入下拉 :将输入电平拉低,使其变为低电平
-
模拟输入 :将模拟信号传输至模拟输入处,等待处理
-
开漏输出 :开漏输出(Open-Drain Output)是指输出引脚可以连接到负载(通常是电阻和/或其他器件),但无法提供高电平(高电压)信号。输出端相当于三极管的集电极. 要得到高电平状态需要上拉电阻才行.
-
推挽式输出 : 能够提供高电平和低电平。能够利用三极管放大特性直接驱动负载
-
推挽式复用功能
-
开漏复用功能
二.点亮LED配置步骤/过程
打开时钟->初始化GPIO->配置GPIO的参数->在while循环内实现亮灭控制
#include "stm32f10x.h" // Device header
int main(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
//配置RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//初始化GPIA
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)
{
//实现LED灯的亮灭
GPIO_ResetBits(GPIOA,GPIO_Pin_0);
Delay_ms(200);
GPIO_SetBits(GPIOA,GPIO_Pin_0);
Delay_ms(200);
}
}
遇到问题或疑问
- 为什么要初始化时钟?
原因在于外围设备的寄存器需要时钟才能工作。你可以把外设当做一个设备,而这个设备需要给它提供电源(时钟)才能工作。
- 输入输出模式英文怎么选?
-
- GPIO_Mode_AF_OD:复用开漏输出
-
- GPIO_Mode_AF_PP: 复用推挽输出
-
- GPIO_Mode_AIN:模拟输入
-
- GPIO_Mode_IN_FLOATING:浮空输入
-
- GPIO_Mode_IPD:下拉输入
-
- GPIO_Mode_IPU:上拉输入
-
- GPIO_Mode_Out_OD:开漏输出
-
- GPIO_Mode_Out_PP: 推挽输出
三. 代码逻辑及底层函数分析
- 定义一个初始化GPIOA的结构体GPIO_InitStructure;
- 初始化时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd函数解析
/**
* @brief Enables or disables the High Speed APB (APB2) peripheral clock.
* @param RCC_APB2Periph: specifies the APB2 peripheral to gates its clock.
* This parameter can be any combination of the following values:
* @arg RCC_APB2Periph_AFIO, RCC_APB2Periph_GPIOA, RCC_APB2Periph_GPIOB,
* RCC_APB2Periph_GPIOC, RCC_APB2Periph_GPIOD, RCC_APB2Periph_GPIOE,
* RCC_APB2Periph_GPIOF, RCC_APB2Periph_GPIOG, RCC_APB2Periph_ADC1,
* RCC_APB2Periph_ADC2, RCC_APB2Periph_TIM1, RCC_APB2Periph_SPI1,
* RCC_APB2Periph_TIM8, RCC_APB2Periph_USART1, RCC_APB2Periph_ADC3,
* RCC_APB2Periph_TIM15, RCC_APB2Periph_TIM16, RCC_APB2Periph_TIM17,
* RCC_APB2Periph_TIM9, RCC_APB2Periph_TIM10, RCC_APB2Periph_TIM11
* @param NewState: new state of the specified peripheral clock.
* This parameter can be: ENABLE or DISABLE.
* @retval None
*/
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState)
{
/* Check the parameters */
assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph));
assert_param(IS_FUNCTIONAL_STATE(NewState));
if (NewState != DISABLE)
{
RCC->APB2ENR |= RCC_APB2Periph;
}
else
{
RCC->APB2ENR &= ~RCC_APB2Periph;
}
}
typedef enum {DISABLE = 0, ENABLE = !DISABLE} FunctionalState; //FunctionalState是枚举类型起的别名,用于表示DISABLE/ENABLE两种状态
assert_param()断言函数解析
/* Exported types ------------------------------------------------------------*/
/* Exported constants --------------------------------------------------------*/
/* Uncomment the line below to expanse the "assert_param" macro in the
Standard Peripheral Library drivers code */
/* #define USE_FULL_ASSERT 1 */
/* Exported macro ------------------------------------------------------------*/
#ifdef USE_FULL_ASSERT
/**
* @brief The assert_param macro is used for function's parameters check.
//assert_param宏用于检查函数的参数
* @param expr: If expr is false, it calls assert_failed function which reports
* the name of the source file and the source line number of the call
* that failed. If expr is true, it returns no value.
//如果表达式错误(有语法问题),调用assert_failed 函数,它会打印出
//出错的源文件和发送错误的行号。如果表达式正确,不返回值。
* @retval None
*/
#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
void assert_failed(uint8_t* file, uint32_t line);
#else
#define assert_param(expr) ((void)0)
#endif /* USE_FULL_ASSERT */
这段函数主要表示:如果定义了USE_FULL_ASSERT,运行else上面的部分,其中assert_param(expr)作用是是,expr为真,运行(void)0;否则运行assert_failed((uint8_t *)__FILE__, __LINE__);如果没定义了USE_FULL_ASSERT,运行else中的#define assert_param(expr) ((void)0);
补充:
-
三目运算符:A ? B : C.A为真,执行B;为假,执行C
-
(void)0 与 0的区别?
(void)0 表示NULL/空。 //assert_param(expr) ((void)0)表示啥都不干
0 表示一种状态,在判断中表示假;如果用于显示LED表示熄灭状态
-
内置宏:用于对各个不同操作系统兼容使用,相互之间可以调用(如)FILE, LINE);
GPIO_Init解析
//这一部分是官方给的函数说明
/**
* @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 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 CRL Configuration ------------------------*/
/* Configure the eight low port pins */
//配置低八位GPIO端口 GPIO0~GPIO7地址是从0x0001~0x0080
if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00)
{
tmpreg = GPIOx->CRL;
//将tmpreg赋予寄存器CRL的初值
for (pinpos = 0x00; pinpos < 0x08; pinpos++) //遍历GPIO0~GPIO3
{
pos = ((uint32_t)0x01) << pinpos;
/* Get the port pins position */
currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
if (currentpin == pos) //表明已经遍历到了对应端口位
{
// pinpos的值左移两位(乘以4),因为寄存器中4个寄存器位配置一个引脚
pos = pinpos << 2;
/* Clear the corresponding low control register bits */
/*以下两个句子,把控制这个引脚的 4 个寄存器位清零,其它寄存器位不变*/
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)
{
/*下拉输入模式,引脚默认置 0,对 BRR 寄存器写 1 可对引脚置 0*/
GPIOx->BRR = (((uint32_t)0x01) << pinpos);
}
else
{
/* Set the corresponding ODR bit */
// 判断是否为上拉输入模式
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
{
/*上拉输入模式,引脚默认值为 1,对 BSRR 寄存器写 1 可对引脚置 1*/
GPIOx->BSRR = (((uint32_t)0x01) << pinpos);
}
}
}
}
GPIOx->CRL = tmpreg;
}
/*---------------------------- GPIO CRH Configuration ------------------------*/
/* Configure the eight high port pins */
//对GPIO8~15端口操作
if (GPIO_InitStruct->GPIO_Pin > 0x00FF)
{
tmpreg = GPIOx->CRH;
for (pinpos = 0x00; pinpos < 0x08; pinpos++)
{
//因为GPIO7的地址是0x08,相当于起始地址是0x08
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;
}
}
这个函数有 GPIOx 和 GPIO_InitStruct 两个输入参数,分别是 GPIO 外设指针和 GPIO初始化结构体指针。分别用来指定要初始化的 GPIO 端口及引脚的工作模式。要充分理解这个 GPIO 初始化函数,得配合我们刚刚分析的 GPIO 引脚工作模式真值表来看。
1)先取得 GPIO_Mode 的值,判断 bit4 是 1 还是 0 来判断是输出还是输入。如果是输出则设置输出速率,即加上 GPIO_Speed 的值,输入没有速率之说,不用设置。
2) 配置 CRL 寄存器。通过 GPIO_Pin 的值计算出具体需要初始化哪个引脚,算出后,然后把需要配置的值写入到 CRL 寄存器中,具体分析见代码注释。这里有一个比较有趣的是上/下拉输入并不是直接通过配置某一个寄存器来实现的,而是通过写BSRR 或者 BRR 寄存器来实现。这让很多只看手册没看固件库底层源码的人摸不着头脑,因为手册的寄存器说明中没有明确的指出如何配置上拉/下拉。
3)配置 CRH 寄存器过程同 CRL。
GPIO_ResetBits()函数解析
/**
* @brief Clears the selected data port bits.
* @param GPIOx: where x can be (A..G) to select the GPIO peripheral.
* @param GPIO_Pin: specifies the port bits to be written.
* This parameter can be any combination of GPIO_Pin_x where x can be (0..15).
* @retval None
*/
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_PIN(GPIO_Pin));
GPIOx->BRR = GPIO_Pin;
}
首先通过断言函数判断两个参数是否合理,然后将GPIO_Pin值写入BRR寄存器中,清除对应的位为0;
GPIO_SetBit函数解析
/**
* @brief Sets the selected data port bits.
* @param GPIOx: where x can be (A..G) to select the GPIO peripheral.
* @param GPIO_Pin: specifies the port bits to be written.
* This parameter can be any combination of GPIO_Pin_x where x can be (0..15).
* @retval None
*/
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_PIN(GPIO_Pin));
GPIOx->BSRR = GPIO_Pin;
}
首先通过断言函数判断两个参数是否合理,然后将GPIO_Pin值写入BSRR寄存器中,清除对应的位为1;
补充:
- 这里的BRR和BSRR寄存器分别是端口位清除寄存器和端口位设置/清除寄存器,这两个寄存器可以将每个端口或者说是引脚置高电平或低电平(可以联想51单片机中,对应的每一位想要点亮小灯时,引脚置高电平1时,LED熄灭;置低电平时,LED点亮的实例)。
- 上拉/下拉模式就是通过BRR和BSRR寄存器配置的,通过配置BRR寄存器全为0,配置出下拉模式;通过配置BSRR寄存器全为1,配置出上拉模式