[Bonjour STM32] No.4-demo 1.GPIO基本操作

[Bonjour STM32] 给萌新们的demo 1.GPIO基操

当你看过 白给鸽鸽写的总览 之后,相信你已经基本学会了MDK5+CubeMX开发STM32的基本流程了,那么我们来正式开始学习吧~

第一期我们来学习电子界的 "Hello World",也就是点亮一个灯~

什么是GPIO

GPIO的全称是 General Purpose Input/Output,即通用输入输出端口。它的物理结构是一堆MOS管和电阻堆在一起组成的,我们先叫它 Mos阵列 吧www,通过单片机内部的寄存器输出信号给Mos阵列,就可以控制这个Mos阵列对外输出的电平高低。下图就是一个基本的GPIO模型:

t1hJRU.png

左边的DOUT就是单片机内部寄存器传输过来的信号,右边的BUFout就是我们的GPIO输出,两个二极管用作保护GPIO不被过高的外部电压损坏(作用为钳位)。因为我们的单片机芯片内部引线都是非常细非常细的,不能承载很大的电流,所以寄存器级的数字信号直接输出是非常不安全且不实用的,我们加一级MOS对管之后,这个IO就具有了 灌电流/输出电流 的能力,一般这个电流限度在 10-20mA 左右,这已经足够在弱电电路中使用了~

如果我们要点灯(灯大灯亮灯会闪),我们只需要输出简单的高/低电平就足够了~,当然我们还需要一颗Tiny LED和一个限流电阻~

Connect a LED

我们这次用的开发板是STM32F103C8T6最小系统,也就是最常见的小蓝板(BluePill),它在单片机的PC13脚上接了一颗LED

t1InMD.png

左边是一颗连接到电源上的指示灯,上电就常亮,右边就是接在PC13引脚上的LED啦

从电路图中可以看出,当PC13引脚输出低电平(0)时,D2会亮起(因为D2的阳极接了3.3V正电源,阴极串联了限流电阻接到PC13嘛),这时我们的思路非常清晰——直接让PC13输出低电平就可以点亮他。

Configure CubeMX

来到CubeMX,配置我们的单片机工作状态——

t1oDne.png

按照老办法来,首先配置 RCC->High Speed Clock(HSE)Crystal/Ceramic Resonator,即使用外部高速晶振,然后配置 SYS->DebugSerial Wire,即用SWD接口进行下载/调试(别忘了连接ST-LINK~),配置单片机时钟主频 72MHZ(Max),然后点击右边的单片机的PC13引脚,选择GPIO_Output模式,现在你可以在左侧的窗口中的GPIO选项栏里看到已经配置的GPIO,点击它,可以配置更详细的参数:

t1o0XD.png

那我们来解释一下这些都是什么意思~

Output Level Mode Pull-up/down Max Output Speed User label
默认输出电平 模式(推挽/开漏) 启用内部上拉/下拉电阻 最高输出速度 用户标签
  • 默认输出电平: 就是单片机上电初始化之后(不运行用户代码)的默认电平
  • 输出模式: 输出大电流时一般选择推挽(Push Pull),为了适配别的芯片的开漏(Open Drain)输出接口时使用开漏即可
  • 上/下拉: GPIO内部有自带的上拉/下拉电阻,可以选择启用他们或者不使用
  • 最高输出速度: 最高输出速度可以理解为GPIO电平反转的最高频率,最高能达到MHZ级别,我们这里图方便选择High
  • 用户标签: 可以给这个I/O取个名字,你写代码的时候就会更直观了~不用回过头去看I/O Pinmap是怎么连接的~

我们在这里选择 默认低电平,推挽输出,无需上下拉电阻,高速,用户标签:LED

配置好之后,来到Project Manager界面。输入工程名称,选择 MDK-ARM,选择只复制需要的库文件为每个外设生成单独的.c/.h文件,然后Generate Code~

然后用MDK5打开工程,来到main.c文件的main函数,找到 while(1) ,在 USER CODE END WHILE 上一行开始编写代码

t3PnlF.png

编写一段代码如下(我截取了片段,免得你不知道该写在哪..):

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
        //  在这里写鸭
        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
        HAL_Delay(1000);
        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
        HAL_Delay(1000);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }

如果你有C/C++和单片机编程基础的话,相信上面的程序不难理解~它的功能是让这个LED亮1秒,再灭1秒,再亮起来,反复循环
看过本系列之前的文章的你应该知道,我们可以直接读HAL_GPIO_WritePin()这个函数的名字,来知(猜)晓(测)它的功能作用。(当然就是直接写GPIO啦!)

如果我们在配置GPIO的时候没有填写用户标签,那么这里的写法得变一下:

HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);

为什么呢?请听我慢慢港~

我们怎样才能知道这些我们从未接触过的函数怎么用呢?当然是前作中提到的 视奸库文件

比如我们将光标移动到这个函数的位置,点击右键,弹出可选项,选择 Goto Definition of xxx(函数名)

t3iLPP.png

然后我们来到了stm32f1xx_hal_gpio.c文件

t3Frz8.png

有人这时候可能要骂人了:哇臭鸽鸽怎么给我看这么复杂的东西啊喂喂喂

我想说的是,它一点也不复杂嗷~我们慢慢解析!

我们可以看到上面绿油油的一片,这些是函数注释,而且十分规范,前面有一个用@符号开头的名字,它们的意思分别是:

  • @brief -> 函数作用简介
  • @note -> 功能性注释
  • @param -> 全称parameter,意为函数参数
  • @retval -> return value,函数返回值,在这里为无

我们可以把这个函数看成一个Black Box,给他输入一些参数,他执行相应的功能,并返回一个结果(这个函数并没有返回值),以实现我们需要的功能

如果我们要调用这个函数,我们需要3个参数:

  • GPIOx -> GPIO的端口号,比如PC13就是GPIOC,PA8就是GPIOA
  • GPIO_Pin -> GPIO的具体编号,就是Px后面的数字啦,比如GPIO_Pin_13
  • PinState -> 指定GPIO的输出状态,只有2种状态: GPIO_PIN_RESET/GPIO_PIN_SET

其实这个文件就是帮我们实现GPIO功能的库文件(也就是我们白嫖的代码来源),所以你想要找与一个外设有关的功能函数都可以在这种文件里找到。它们的命名规则就是 stm32xxyy_hal_xxxx.c/h
所有的HAL库文件都可以在Drivers/STM32F1xx_HAL_Driver里找到

t3EExf.png

看到这里,是不是对GPIO/HAL库的使用有更深一层的了解了?(其实前文都讲过啦啊喂)
有人可能又要问了,你那个用户标签是怎么自动变成PC13对应的参数的啊?

别急,我们点开main程序旁边的小+号,找到并打开main.h

t3EkGt.png

然后我们稍微往下翻一点,聚焦到这2行上来:

t3EARP.png

ohhhhhhhhhh,搞了半天他是帮你用 宏定义 实现了这个转换啊(还以为是什么黑科技呢:(

流水灯

关于流水灯,我之前写了个Arduino版本的教程,你可以看看这里啦

原理我就不再阐述,我们直接上代码:

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
        //  在这里写鸭
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET);
        HAL_Delay(1000);
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET);
        HAL_Delay(1000);
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_SET);
        HAL_Delay(1000);
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
        HAL_Delay(1000);
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
        HAL_Delay(1000);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

注意,在使用这些GPIO之前一定要先初始化它们哦~
代码看起来有点头大,不过稍微细微观察你就能发现它的规律啦

顺便一提,GPIO的初始化代码在哪看呢?其实还是一样的套路,找到main()函数里的 MX_GPIO_Init()函数,对着它 Goto Definition

我直接把这段代码贴上来

/** Configure pins as 
        * Analog 
        * Input 
        * Output
        * EVENT_OUT
        * EXTI
*/
void MX_GPIO_Init(void)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* 使能GPIO 总时钟 */
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();

  /* 配置GPIO输出电平 */
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4, GPIO_PIN_RESET);

  /* 配置GPIO工作模式 : PA0 PA1 PA2 PA3 PA4 */
  GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;

  /* 上面是一个GPIO配置结构体,在这里一次性
  把PA端所有使用到的GPIO配置全部写入配置寄存器里 */
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

}

那么本期教程就到这里啦~

小知识-关于GPIO_SPEED

关于GPIO的速度,我也是有一些疑惑的…于是我google了一下,找到了如下解答:

The output speed register only affects pins which are configured as outputs. It controls the slew rate (drive strength) used for the output. Using an excessively high speed may cause ringing and EMI on outputs, so it is important to use the minimum speed required for your application.

简单翻译:输出速度寄存器仅仅在GPIO配置成输出模式时才能影响它的速度(废话;p)。这个寄存器控制输出的 压摆率 (电压在变化的一瞬间对时间求导的绝对值,单位V/s),如果使用过高的速度可能会导致振铃现象和较大的EMI输出(电磁辐射噪声),所以针对特定的应用情景选择合适的配置是很重要的。

(小声bb:咱们点个灯,也不用考虑EMI啦233)

发表评论