搜索
bottom↓
回复: 1

《STM32MP1 M4裸机HAL库开发指南》第十五章 HAL库跑马灯实验

[复制链接]

出0入234汤圆

发表于 2022-10-31 18:28:03 | 显示全部楼层 |阅读模式
本帖最后由 正点原子 于 2022-10-31 18:27 编辑

1)实验平台:正点原子STM32MP157开发板
2)购买链接:https://item.taobao.com/item.htm?&id=629270721801
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-318813-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子STM32MP157技术交流群:691905614 lQLPJxaFi2zaB4UWWrDAMgIsFEW2pwLb3abnwDMA_90_22.png
lQDPJxaFi2nfFizMjM0CbLCPlxn_FVheIQLb3aGrwFQA_620_140.jpg

lQLPJxaFi2nfFhLMkM0BXrDNvOUyeU_FPgLb3aGvQNIA_350_144.png


第十五章 HAL库跑马灯实验


        上一章我们完成了基本的HAL库版本的MDK工程创建,我们将利用这个新建好的工程来完成本章节的实验。本章节,我们通过一个经典的跑马灯程序,带领大家了解HAL库的API函数操作IO的使用的方法,我们将通过代码控制开发板上的两个 LED 灯 DS0 和 DS1 交替闪烁,实现类似跑马灯的效果。
        本章将分为如下几个小节:
       15.1、GPIO相关的API函数;
        15.2、硬件设计;
        10.3、程序设计;
        10.4、编译和测试;
        10.5、章节小结;

        
15.1 GPIO相关的API函数
        下面,我们介绍stm32mp1xx_hal_gpio.c文件中几个重要的API函数,后面的实验我们将用到这些函数。
        在前面的几个实验中,我们会带领大家去分析HAL库里的API函数,通过分析几个外设的API函数,我们学会查看HAL库里的函数,到后面的实验时我们就不会再去挨个分析API函数了,毕竟他们的使用方法都很相似,只要学会看懂几个,其它的都能够看懂。
15.1.1 HAL_GPIO_Init函数
●函数功能:根据GPIO_Init中的指定参数初始化GPIOx外设。
●函数参数:
GPIOx,其中x可以是(A..K和Z,下同)。
GPIO_Init,指向GPIO_InitTypeDef结构的指针,该结构包含指定GPIO外设的配置信息。
●函数返回值:无
●注意:HAL库的EXTI外部中断的设置功能整合到此函数里面,而不是单独独立一个文件,关于EXTI我们到外部中断实验再细讲。
        HAL_GPIO_Init函数由于篇幅原因这里不列出函数的具体内容,大家可以直接在HAL库文件中查看。
1   void HAL_GPIO_Init(GPIO_TypeDef  *GPIOx, GPIO_InitTypeDef *GPIO_Init)
2   {
3     /*省略代码*/
4           {
5                            /*GPIO模式配置代码*/
6           }
7           {
8                            /*外部中断配置代码*/
9           }
10      }
11    }
12  }
1. 形参GPIOx
形参GPIOx是端口号,可以有以下的选择,这是整个芯片可以选择的GPIO组,正点原子的STM32MP157开发板引出了144 个GPIO,并没有把所有的GPIO(184个)都引出来。
stm32mp157dxx_cm4.h文件代码
        #define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)
    #define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)
    #define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)
    #define GPIOD               ((GPIO_TypeDef *) GPIOD_BASE)
    #define GPIOE               ((GPIO_TypeDef *) GPIOE_BASE)
    #define GPIOF               ((GPIO_TypeDef *) GPIOF_BASE)
    #define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
    #define GPIOH               ((GPIO_TypeDef *) GPIOH_BASE)
    #define GPIOI               ((GPIO_TypeDef *) GPIOI_BASE)
    #define GPIOJ               ((GPIO_TypeDef *) GPIOJ_BASE)
    #define GPIOK               ((GPIO_TypeDef *) GPIOK_BASE)   
        GPIO_TypeDef结构体类型如下。可以看到GPIO_TypeDef结构体对GPIO相关的寄存器进行了封装,通过操作结构体成员即可操作寄存器。
stm32mp157dxx_cm4.h文件代码
   typedef struct
   {
     __IO uint32_t MODER;
     __IO uint32_t OTYPER;
     __IO uint32_t OSPEEDR;
     __IO uint32_t PUPDR;
     __IO uint32_t IDR;
     __IO uint32_t ODR;
     __IO uint32_t BSRR;
    __IO uint32_t LCKR;
    __IO uint32_t AFR[2];
    __IO uint32_t BRR;
  /*篇幅原因,此处省略剩下代码*/  
  } GPIO_TypeDef;
2. 形参GPIO_Init
        GPIO_Init是一个GPIO_InitTypeDef 类型结构体,其定义如下:
stm32mp1xx_hal_gpio.h文件代码
   typedef struct
   {
     uint32_t Pin;                            /*选择的引脚号*/
     uint32_t Mode;                           /*引脚模式配置*/
     uint32_t Pull;                     /*上拉/拉配置*/
     uint32_t Speed;                    /*引脚速度等级配置*/
     uint32_t Alternate;           /*引脚复用配置*/
   }GPIO_InitTypeDef;
        GPIO_InitTypeDef 结构体对选择的引脚号、引脚工作模式、引脚上/下拉模式配置、引脚速度等级以及引脚复用配置进行了封装。在调用HAL_GPIO_Init函数以后,GPIOx所对应引脚工作模式将被初始化成我们配置的模式。
该结构体很重要,下面对每个成员进行介绍。
成员Pin表示引脚号,范围:GPIO_PIN_0到 GPIO_PIN_15,另外还有GPIO_PIN_All和GPIO_PIN_MASK可选。
成员Mode是GPIO的模式选择,有以下选择项:
stm32mp1xx_hal_gpio.h文件代码
    #define  GPIO_MODE_INPUT       ((uint32_t)0x00000000U)  /* 输入模式 */
    #define  GPIO_MODE_OUTPUT_PP   ((uint32_t)0x00000001U) /* 推挽输出 */
    #define  GPIO_MODE_OUTPUT_OD   ((uint32_t)0x00000011U) /* 开漏输出 */
    #define  GPIO_MODE_AF_PP      ((uint32_t)0x00000002U)   /* 推挽式复用 */
    #define  GPIO_MODE_AF_OD      ((uint32_t)0x00000012U)   /* 开漏式复用 */

    #define GPIO_MODE_AF           GPIO_MODE_AF_PP            /* 输入复用 */

    #define  GPIO_MODE_ANALOG     ((uint32_t)0x00000003U)   /* 模拟模式  */
    /* 外部中断,上升沿触发检测 */
    #define  GPIO_MODE_IT_RISING           ((uint32_t)0x10110000U)
    /* 外部中断,下降沿触发检测 */
    #define  GPIO_MODE_IT_FALLING          ((uint32_t)0x10210000U)
    /* 外部中断,上升和下降双沿触发检测 */
    #define  GPIO_MODE_IT_RISING_FALLING   ((uint32_t)0x10310000U)   
    /* 外部事件,上升沿触发检测 */
    #define  GPIO_MODE_EVT_RISING          ((uint32_t)0x10120000U)
    /* 外部事件,下降沿触发检测 */
    #define  GPIO_MODE_EVT_FALLING         ((uint32_t)0x10220000U)
    /* 外部事件,上升和下降双沿触发检测 */
    #define  GPIO_MODE_EVT_RISING_FALLING  ((uint32_t)0x10320000U)   
成员Pull用于配置上下拉电阻,有以下选择项:
stm32mp1xx_hal_gpio.h文件代码
    #define  GPIO_NOPULL    ((uint32_t)0x00000000U)                  /* 无上下拉 */
    #define  GPIO_PULLUP    ((uint32_t)0x00000001U)                  /* 上拉 */
    #define  GPIO_PULLDOWN  ((uint32_t)0x00000002U)                  /* 下拉 */
成员Speed用于配置GPIO的速度,有以下选择项:
stm32mp1xx_hal_gpio.h文件代码
        #define  GPIO_SPEED_FREQ_LOW       ((uint32_t)0x00000000U)  /* 低速 */
    #define  GPIO_SPEED_FREQ_MEDIUM    ((uint32_t)0x00000001U)  /* 中速 */
    #define  GPIO_SPEED_FREQ_HIGH       ((uint32_t)0x00000002U)  /* 快速 */
    #define  GPIO_SPEED_FREQ_VERY_HIGH ((uint32_t)0x00000003U)  /* 高速 */  
        成员Alternate用于配置具体的复用功能,关于复用功能,我们后面有专门的实验来讲解。
15.1.2 HAL_GPIO_DeInit函数
●函数功能:将GPIOx外设寄存器初始化为其默认复位值(各个寄存器复位时默认的值)。
●函数参数:GPIOx和GPIO_Pin
●函数返回值:无
●注意:HAL库的EXTI外部中断的设置功能整合到此函数里面,而不是单独独立一个文件,关于EXTI我们到外部中断实验再细讲。
        HAL_GPIO_Init函数由于篇幅原因这里不列出函数的具体内容,大家可以直接在HAL库文件中查看。
1   void HAL_GPIO_DeInit(GPIO_TypeDef  *GPIOx, uint32_t GPIO_Pin)
2   {
3   /*此处函数省略*/
4   }
15.1.3 HAL_GPIO_ReadPin函数
●函数功能:读取我们想要知道的引脚的电平状态。
●函数参数:GPIOx和GPIO_Pin
●函数返回值:输入端口引脚值,为0或1
1   GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
2   {
3     GPIO_PinState bitstatus;
4   
5     /* 用于检查参数是否有效 */
6     assert_param(IS_GPIO_PIN(GPIO_Pin));
7   
8     if((GPIOx->IDR & GPIO_Pin) != (uint32_t)GPIO_PIN_RESET)
9     {
10      bitstatus = GPIO_PIN_SET;
11    }
12    else
13    {
14      bitstatus = GPIO_PIN_RESET;
15    }
16    return bitstatus;
17  }
        HAL_GPIO_ReadPin函数是读取我们想要知道的引脚的电平状态,函数的返回值为0或1。我们分析一下这段代码。
        第3行,GPIO_PinState是一个枚举类型,可选值是0或者1,此行定义了一个枚举常量bitstatus。
        第6行,断言,assert_param函数实际上是一个宏定义,它的作用就是检测传递给函数的参数是否是有效的参数,如果检测的参数有效,则返回true,否则返回false。
        第8至第11行,IDR是GPIOx端口的输入数据寄存器,用于读取 GPIOx 的某个IO口输入电平状态,每个位控制一个IO口,如果IDR的第0位为 0,则说明该GPIOx 端口的第0个IO口输入的是低电平,如果IDR的第0位为 1,则表示该GPIOx 端口的第0个IO口输入的是是高电平。
        其中,GPIO_PIN_RESET表示0,GPIO_PIN_SET 表示1。
        其中的"->"是表示访问结构体的指针对应对象下的IDR成员,->运算符优先级比位与&运算符优先级要高,GPIOx->IDR可以改写为(*GPIOx).IDR。GPIOx->IDR表示IDR寄存器的低16位的数值。GPIO_Pin为16位,表示要读取的第几个IO口(如果要读取第1个IO口,GPIO_Pin的值是0x0002U)。(GPIOx->IDR & GPIO_Pin)就表示读取IO口对应的IDR寄存器的值,如果IDR和GPIO_Pin进行位与后不等于0(说明对应的IDR位是1,表示此IO口输入的是高电平),则返回bitstatus为1,反之则返回0,这样就实现了读取对应引脚的电平状态。这里的GPIO_Pin是哪一个,由我们后面调用函数的时候给定的具体的参数来决定。
15.1.4 HAL_GPIO_WritePin函数
●函数功能:让某个引脚设置为高电平或者低电平。
●函数参数:GPIOx和GPIO_Pin
●函数返回值:无
1   void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin,\         GPIO_PinState PinState)
2   {
3     /* 用于检查参数是否有效 */
4     assert_param(IS_GPIO_PIN(GPIO_Pin));
5     assert_param(IS_GPIO_PIN_ACTION(PinState));
6   
7     if (PinState != GPIO_PIN_RESET)
8     {
9       GPIOx->BSRR = GPIO_Pin;
10    }
11    else
12    {
13      GPIOx->BSRR = (uint32_t)GPIO_Pin << GPIO_NUMBER;
14    }
15  }
        在前面第10.1.5小节我们已经分析GPIOx_BSRR寄存器的功能了,对BSRR寄存器的高16位写1,ODR的对应位将被复位,所以对应IO口为低电平,对BSRR寄存器的低16位写1, ODR的对应位将被置1,对应IO口为高电平,如果写 0,表示无动作。
        HAL_GPIO_WritePin函数中GPIO_Pin 为16位类型数据,表示要操作的IO口,PinState 为枚举类型,其中GPIO_PIN_RESET为0,GPIO_PIN_SET为1。GPIOx->BSRR表示BSRR寄存器的高16位和低16位的值。
        当PinState为0的时候,GPIOx->BSRR = GPIO_Pin表示将GPIO_Pin的值赋予BSRR的低16位,GPIO_Pin中哪一位是1,则对应的BSRR的那个位就被置1,对应的IO口输出高电平。        当PinState为1的时候,(uint32_t)GPIO_Pin为32位,GPIO_NUMBER为16位,<<表示左移,GPIO_Pin左移16位后,原来的低16位移动到高16位的位置,低16位为0。GPIOx->BSRR = (uint32_t)GPIO_Pin << GPIO_NUMBER表示对BSRR寄存器高16位所对应的 GPIO_Pin 位写1,对应引脚为低电平。
        这里的GPIO_Pin由我们后面调用函数的时候给定的具体的参数来决定。
15.1.5 HAL_GPIO_TogglePin函数
●函数功能:让某个GPIO口的电平翻转。
●函数参数:GPIOx和GPIO_Pin
●函数返回值:无
1   void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
2   {
3     /* Check the parameters */
4     assert_param(IS_GPIO_PIN(GPIO_Pin));         /* 用于检查参数是否有效*/
5   
6     if ((GPIOx->ODR & GPIO_Pin) != 0x00u)
7     {
8       GPIOx->BRR = (uint32_t)GPIO_Pin;
9     }
10    else
11    {
12      GPIOx->BSRR = (uint32_t)GPIO_Pin;
13    }
14  }
        BRR寄存器我们在第10.1.5小节有讲解过,BRR为位清除寄存器,对低16位写1,对应IO口为低电平,写0表示无效。
        第6行,读取GPIO的ODR寄存器某一位是否等于0,如果不于0,表示此IO口输出低电平,则执行第8行代码。
        第8行表示将BRR寄存器的对应位写1,表示将此IO口设置为低电平。
        第12行,如果读取GPIO的ODR寄存器某一位是等于0,表示此IO口输出低电平,通过对BSRR寄存器的对应位写1后,此IO口输出高电平。
        根据上述分析,此函数对GPIO的某个IO口实现电平翻转。
15.1.6 HAL_GPIO_LockPin函数
●函数功能:锁住 某个GPIO 引脚所涉及到的寄存器,锁定的寄存器为MODER, OTYPER, OSPEEDR, PUPDR, AFRL和AFRH。
●函数参数:GPIOx和GPIO_Pin
●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(忙碌)、HAL_TIMEOUT(超时)
●注意:
        ①锁住的是某个引脚对应的寄存器的某个位,不是将寄存器的所有位都锁住。例如设置要锁住GPIOA的引脚GPIO_PIN_0,则GPIOA对应的寄存器的某一位:
MODER(第0~1位)、OTYPER(第0位)、OSPEEDR(第0~1位)、PUPDR(第0~1位)、AFRL(第0~3位),AFRH寄存器由于是用于配置GPIO的8至第15位,所以没有被锁住。
        ②锁定的GPIO引脚的那个位,不能再修改寄存器对应的位的配置(上面提到的寄存器),直到下一次复位以后才可以更改(复位以后LCKR寄存器的默认值是0x00000000,没有激活锁定键)。关于LCKR寄存器可以看前面的第10.1.5小节。
        我们来看看这个函数的代码实现过程:
1   HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef* GPIOx, uint16_t \         GPIO_Pin)
2   {
3     __IO uint32_t tmp = GPIO_LCKR_LCKK;                 /* 0x00010000 */
4
5     /* 检查参数 */
6     assert_param(IS_GPIO_LOCK_INSTANCE(GPIOx));
7     assert_param(IS_GPIO_PIN(GPIO_Pin));
8
9     /* 应用锁定键写入顺序 */
10    tmp |= GPIO_Pin;
11    /* 将LCKx位置1:LCKK ='1'+ LCK [15-0] */
12    GPIOx->LCKR = tmp;
13    /* 复位LCKx位:LCKK ='0'+ LCK [15-0] */
14    GPIOx->LCKR = GPIO_Pin;
15    /*将LCKx位置1:LCKK ='1'+ LCK [15-0] */
16    GPIOx->LCKR = tmp;
17    /* 读取LCKK寄存器。 必须完成此读取才能完成键锁定序列  */
18    tmp = GPIOx->LCKR;
19
20    /* 再次读LCKR寄存器的值以确认锁定处于活动状态 */
21    if((GPIOx->LCKR & GPIO_LCKR_LCKK) != RESET)
22    {
23      return HAL_OK;
24    }
25    else
26    {
27      return HAL_ERROR;
28    }
29  }   
        第3行,GPIO_LCKR_LCKK的值为0x00010000,表示第16位为1,其它位为0,此值写入tmp中;
        第10行,将用户指定要锁的某个位和tmp相或,这样一来就确定了要锁定键。
        第11到第18行,是LOCK键写入顺序为①~③步骤,写完以后记得读:
    ① WR LCKR[16] = ‘1’ + LCKR[15:0]         /* 锁键位写1+[15:0]位的值 */
    ②WR LCKR[16] = ‘0’ + LCKR[15:0]         /* 锁键位写0+[15:0]位的值 */
③WR LCKR[16] = ‘1’ + LCKR[15:0]         /* 锁键位写1+[15:0]位的值 */
④读取LCKK寄存器(写完以后必须要读)
        关于stm32mp1xx_hal_gpio.c文件的API函数我们就介绍到这里,关于EXTI部分,我们后面对应的实验会进行讲解。
15.2 硬件设计
1. 例程功能
通过HAL库的API函数来驱动LED,实现LED0和LED1交替闪烁。
2. 硬件资源
lQLPJxbUXlhIwAstzQFasKDTauS1Rp9-A10Cyw6AcAA_346_45.png
表15.2. 1硬件资源

3. 原理图
        硬件原理图和第十一章节一样,本章节控制的是开发板的LED0和LED1,当引脚为低电平时LED亮,当引脚为高电平时,LED灭。和前面几章不同的是,本章节我们通过HAL库来控制GPIO。
第十五章 HAL库跑马灯实验9705.png
图15.2. 2 LED与STM32MP157连接原理图

15.3 程序设计
        本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\3、M4裸机驱动例程\ MP157-M4 HAL库V1.2\实验4  HAL库跑马灯实验。
15.3.1 程序设计流程
        本章节我们通过HAL库的API函数来驱动LED,实现LED0和LED1交替闪烁。其中,我们会用到HAL_GPIO_WritePin和HAL_GPIO_TogglePin函数。实验程序的设计流程如下:
1)初始化HAL库
        HAL_Init();
2)开启GPIOI和GPIOF时钟
__HAL_RCC_GPIOI_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE()
3)设置GPIO引脚电平
        通过HAL_GPIO_WritePin和HAL_GPIO_TogglePin设置GPIO口的电平。
4)实现LED0和LED1交替闪烁
15.3.2 添加驱动文件
        复制一份上一章模板的工程,文件夹名字命名为“实验4 HAL库跑马灯实验”,我们在工程的Drivers\BSP下新建LED文件夹,然后再LED文件夹里新建led.c和led.h文件,如下:
第十五章 HAL库跑马灯实验10272.png
图15.3.2.1新建led.c和led.h文件

        接下来关联led.c文件,如下:
第十五章 HAL库跑马灯实验10318.png
图15.3.2.2关联led.c文件

        本节实验我们先不用Drivers/SYSTEM下的文件代码:我们先把usart文件夹删掉,只留下delay和sys文件夹,其中delay和sys文件夹里的文件现有的驱动代码我们本节实验也不用,我们后续自己编写延时函数。关于工程模板中的delay和sys以及usart文件的代码,我们后面会进行介绍。
15.3.3 添加LED驱动代码
1. 添加led.h文件代码
led.h文件代码如下,由硬件设计小节,我们知道LED灯在硬件上分别连接到PI0和PF3上,再结合HAL库,我们做了下面的引脚定义。这样的好处是进一步隔离底层函数操作,移植更加方便,函数命名更亲近实际的开发板。比如:当我们看到LED0_GPIO_PORT这个宏定义,我们就知道这是灯LED0的端口号;看到LED0_GPIO_PIN这个宏定义,就知道这是灯LED0的引脚号;看到LED0_GPIO_CLK_ENABLE这个宏定义,就知道这是灯LED0的时钟使能函数。大家后面学习时间长了就会慢慢熟悉这样的命名方式。
__HAL_RCC_GPIOx_CLK_ENABLE函数是HAL库的IO口时钟使能函数,x=A到K。
#ifndef __LED_H
#define __LED_H

/* PI0引脚 定义 */
#define LED0_GPIO_PORT                  GPIOI
#define LED0_GPIO_PIN                   GPIO_PIN_0
/* GPIOI时钟使能 */
#define LED0_GPIO_CLK_ENABLE()          do{ __HAL_RCC_GPIOI_CLK_ENABLE(); }while(0)   
/* PF3引脚 定义 */
#define LED1_GPIO_PORT                  GPIOF
#define LED1_GPIO_PIN                   GPIO_PIN_3
/* GPIOF口时钟使能 */
#define LED1_GPIO_CLK_ENABLE()          do{ __HAL_RCC_GPIOF_CLK_ENABLE(); }while(0)   
/* LED0和LED1端口定义 */
#define LED0(x)  do{ x ? \
                 HAL_GPIO_WritePin(LED0_GPIO_PORT, LED0_GPIO_PIN, GPIO_PIN_SET) : \
                 HAL_GPIO_WritePin(LED0_GPIO_PORT, LED0_GPIO_PIN, GPIO_PIN_RESET); \
                  }while(0)       /* LED0 = RED */
#define LED1(x)  do{ x ? \
                 HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_SET) : \
                 HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_RESET); \
                  }while(0)       /* LED1 = GREEN */

/* LED取反定义 */
#define LED0_TOGGLE()    do{ HAL_GPIO_TogglePin(LED0_GPIO_PORT, \                                                                                         LED0_GPIO_PIN); }while(0)   /* LED0 = !LED0 */
#define LED1_TOGGLE()    do{ HAL_GPIO_TogglePin(LED1_GPIO_PORT, \                                                                                         LED1_GPIO_PIN); }while(0)   /* LED1 = !LED1 */

void led_init(void);    /* 函数的声明,初始化 LED */

#endif
上面我们还定义了LED0(x)和LED1(x)两个宏,对于宏定义标识符LED0(x),它的值是通过条件运算符来确定:
当x=0时,宏定义的值为HAL_GPIO_WritePin(LED0_GPIO_PORT, LED0_GPIO_PIN, GPIO_PIN_RESET),也就是设置LED0_GPIO_PORT(PI0)输出低电平;当n!=0时,宏定义的值为HAL_GPIO_WritePin (LED0_GPIO_PORT, LED0_GPIO_PIN, GPIO_PIN_SET),也就是设置LED0_GPIO_PORT(PPI0)输出高电平。所以如果要设置LED0输出低电平,那么调用宏定义LED0(0)即可,如果要设置LED0输出高电平,调用宏定义LED0(1)即可。宏定义LED1(x)和LED0(x)同理。
此外,我们还定义了宏LED0_TOGGLE()和LED1_TOGGLE(),分别是控制LED0、LED1和LED2的翻转。这里利用HAL_GPIO_TogglePin函数实现IO口输出电平取反操作。
以上代码中,其中“\”表示换行符号,因为一行写不下了写到第二行,两行之间的代码用换行符来连接。我们主要是调用HAL库的__HAL_RCC_GPIOx_CLK_ENABLE函数(这里x表示A~K)来完成GPIO的时钟开启,开启时钟后才可以使用外设。然后调用HAL_GPIO_WritePin和HAL_GPIO_TogglePin函数操作GPIO的电平,比起我们前面编写控制IO口的代码要方便的多,这个就是API函数的优势。
2. 添加led.c文件代码
led.c文件代码如下,这里只有一个函数led_init,这是LED灯的初始化函数:
1   #include "./BSP/LED/led.h"
2
3   /**
4    * @brief       初始化LED相关IO口, 并使能时钟
5    * @param       无
6    * @retval      无
7    */
8   void led_init(void)
9   {
10      GPIO_InitTypeDef gpio_init_struct;                /* 定义gpio_init_struct结构体 */
11      LED0_GPIO_CLK_ENABLE(); /* LED0时钟使能 */
12      LED1_GPIO_CLK_ENABLE(); /* LED1时钟使能 */
13
14      gpio_init_struct.Pin = LED0_GPIO_PIN;                        /* LED0引脚 */
15      gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;                /* 推挽输出 */
16      gpio_init_struct.Pull = GPIO_PULLUP;                          /* 上拉 */
17      gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 高速 */
18      HAL_GPIO_Init(LED0_GPIO_PORT, &gpio_init_struct)         /* 初始化LED0引脚 */
19
20      gpio_init_struct.Pin = LED1_GPIO_PIN;                   /* LED1引脚 */
21      HAL_GPIO_Init(LED1_GPIO_PORT, &gpio_init_struct);     /* 初始化LED1引脚 */
22      
23      LED0(1);    /* 关闭 LED0 */
24      LED1(1);    /* 关闭 LED1 */
25  }
以上是LED灯的初始化过程,我们来分析一下HAL库是怎么完成初始化工作的:
第10行,定义一个GPIO_InitTypeDef类型的结构体gpio_init_struct,我们前面分析过,GPIO_InitTypeDef结构体的声明在stm32mp157dxx_cm4.h文件中;
第11~12行,使能GPIOI和GPIOF的时钟,也就是开启LED0和LED1的时钟;
第14~17行及20行,对结构体gpio_init_struct的成员进行操作,通过操作结构体成员完成GPIO对应寄存器的配置:
14行,指定引脚为PI0,即LED0所接的引脚;
15行,配置引脚为推挽输出模式;
16行,配置引脚为上拉;
17行,配置引脚为高速模式;
20行,指定引脚为PF3,即LED1所占用的引脚。前面15~17行已经配置了引脚模式了。
第18和21行,调用HAL库的HAL_GPIO_Init函数完成IO口的初始化,最终初始化PI0和PF3为推挽输出、上拉、高速模式。
第23和24行,关闭LED0和LED1,也就是进入led_init函数后,LED0和LED1是不亮的。
3. 添加延时函数
工程保存目录的Drivers\SYSTEM\delay\delay.h文件内容如下,即声明两个函数:
#ifndef __DELAY_H
#define __DELAY_H

void delay_short(volatile unsigned int n);
void delay(volatile unsigned int n);

#endif
delay.c文件代码如下,这两个函数我们在前面第十二和十三章节都有用到,也就是让CPU空循环,从而实现时间延时:
#include "./SYSTEM/Delay/delay.h"

/**
* @brief       短时间延时函数
* @param - n   要延时循环次数(空操作循环次数,模式延时)
* @retval      无
*/
void delay_short(volatile unsigned int n)
{
    while(n--){}
}

/**
* @brief       长延时函数
* @param - n   要延时的时间循环数
* @retval      无
*/
void delay(volatile unsigned int n)
{
    while(n--)
    {
        delay_short(0x7fff);
    }
}
4. 包含HAL库头文件
sys.h文件主要是包含了HAL库中几个重要的头文件,这几个文件的作用我们在前面的第九章节已经详细分析过了,如果不添加这些头文件,那么我们无法使用HAL库,工程编译则会报错。此外还要注意的是,我们没有编写配置时钟的初始化代码,复位后,系统默认以64MHz的HSI来运行。sys.h文件代码如下:
#ifndef __SYS_H
#define __SYS_H

#include "stm32mp1xx.h"
#include "stm32mp1xx_hal.h"
#include "core_cm4.h"

#endif
下面,我们在sys.c中添加如下代码:
#include "./SYSTEM/sys/sys.h"

#ifdef  USE_FULL_ASSERT

/**
* @brief       当编译提示出错的时候此函数用来报告错误的文件和所在行
* @param       file:指向源文件
*              line:指向在文件中的行数
* @retval      无
*/
void assert_failed(uint8_t* file, uint32_t line)
{
    while (1)
    {
    }
}
#endif
sys.c文件中主要是定义了assert_failed函数,不过是条件编译,如果定义宏USE_FULL_ASSERT,该函数才生效,本工程中是没有定义此宏的。我们前面在第九和第十章节介绍过,USE_FULL_ASSERT宏是用于开启或者关闭断言的,如果开启了断言,则可以调用assert_failed函数来检查我们设置的参数是否是正确的,如果需要开启断言,则定义USE_FULL_ASSERT宏即可。
5. 添加main.c文件代码
main.c文件代码如下:
1   #include "./SYSTEM/sys/sys.h"
2   #include "./BSP/LED/led.h"
3   #include "./SYSTEM/delay/delay.h"
4
5   /**
6    * @brief       主函数
7    * @param       无
8    * @retval      无
9    */
10   
11  int main(void)
12  {
13      HAL_Init(); /* 初始化HAL库 */
14      led_init(); /* 初始化LED  */
15
16      while(1)
17      {
18          LED0(0);     /* 打开LED0 */
19          LED1(1);     /* 关闭LED1 */
20          delay(100); /* 延时一段时间 */
21          LED0(1);     /* 关闭LED0 */
22          LED1(0);     /* 打开LED1 */
23          delay(100);/* 延时一段时间 */
24      }
25  }
第14行,调用HAL_Init函数完成HAL库的初始化,该函数在程序中必须优先调用。HAL_Init函数我们在前面第九章节详细分析过,如果对此函数没有印象了,可以参考前面第九章 9.4.2小节的知识点;
第14行,调用led_init完成LED的初始化,所以一开始的时候,LED0和LED1灯是不亮的状态;
第16~24行,while循环中实现LED0和LED1交替闪烁。
15.4 编译和测试
编译工程无报错后,进入仿真界面进行验证,可以看到LED0和LED1在交替闪烁。
15.5 章节小结
        本章节的实验,我们调用HAL库来操作LED灯对应的寄存器,从而点亮LED,这里我们对前面提到的ODR、BRR和BSRR寄存器做一个总结。
ODR寄存器
        操作:ODR寄存器可读可写,低16位有效,每个位控制一个IO。对位写1,IO口输出高电平,对位写0,IO口输出低电平。
        对ODR寄存器的读写操作必须以16位形式进行,如果用ODR寄存器来改写数据以控制输出时,采用“读改写”的形式。
        缺点:ODR寄存器会受中断的影响,关闭中断会导致延迟或丢失一事件的捕获,如果在中断中通过ODR操作IO口,操作越频繁影响越明显。如果在中断中操作IO口,最好还是用BRR或者BSRR寄存器,因为BSRR和BRR 寄存器可以实现对ODR 寄存器进行原子读取/
修改访问,这样可确保在读取和修改访问之间发生中断请求的时候也不会有问题。
        注:
        原子操作:原子(atomic)操作就是指不能再进一步分割的操作,一般原子操作用于变量或者位操作。一个程序在执行的时候可能被优先级更高的线程中断,而原子操作不会被线程调度机制打断,这种操作一旦开始,就一直运行到结束,运行期间不会有任何的上下文切换(不会切换到另一个线程)。简单的说,原子操作是指操作过程中不会被中断打断。
BRR寄存器
        BRR属于只写寄存器,只有低16位可用,对BRRR寄存器的写操作必须以16位形式进行,每个位控制一个IO。对位写1,IO口输出低电平,写0无效。即BRR只能控制管脚为低电平,不能控制管脚为高电平,所以BRR也称作位清除寄存器。BRR 寄存器支持原子操作。
        如果想对某个GPIO的(如GPIOA)的第1位进行复位,其它位保留原来的值,则可以使用使用BRR寄存器写1来实现:
GPIOA->BRR=0X02;
        当然也可以用过ODR寄存器来实现,不过写起来就不是很方便了:
GPIOA->ODR&=0XFFFE;
BSRR寄存器
        BSRR属于只写寄存器,32位有效,其中,对高16位写1,对应的ODR位将被复位,所以对应IO口为低电平;对16位写1,对应的ODR位将被置1,对应IO口为高电平;写0表示无效。所以,BSRR的高16位也称作位清除寄存器,低16位也称作设置/置位寄存器。BSRR 寄存器支持原子操作。
        如果想对某个GPIO(如GPIOA)的第1位置1,第14位置0,通过BSRR可以一步到位:
GPIOA->BSRR=0X4000 0002;
        以上是从对寄存器直接操作的层面来分析,当然了,我们使用HAL库以后可以不用直接操作寄存器了,不过进行开发时,有时候也会在HAL库中添加自己的代码,有些操作直接通过操作寄存器是很方便的。此外,HAL库也只是将对寄存器的操作做了封装,方便代码移植,如果要了解HAL库的实现过程,那也必须先了解寄存器,所以我们后面的实验也会先简单介绍寄存器以后,再介绍HAL库的API函数,这样结合起来学习,可以更好地理解和掌握HAL库。

出0入4汤圆

发表于 2022-10-31 20:31:11 | 显示全部楼层
ST的新片子除了HAL库和寄存器操作,有没有LIB库了啊
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-4-19 11:29

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表