正点原子 发表于 2022-10-29 09:46:01

《STM32MP1 M4裸机HAL库开发指南》第十三章

本帖最后由 正点原子 于 2022-10-29 09:45 编辑

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





第十三章 结构体实现外设定义实验

      在上一章的使用C语言编写LED灯的驱动实验中,每个寄存器地址都有其对应的宏定义,如果需要操作的寄存器很多,就需要写出所有寄存器地址的宏定义,这样操作起来是比较繁琐的。前面我们分析STM32Cube固件包的时候,了解到HAL库中将寄存器封装成结构体指针,使用中我们可以通过指针的方式来操作结构体的成员(寄存器):GPIOB->ODR = 0XFF。本章节我们就采用这样的方式来实现上一章节的实验。
      本章将分为如下几个小节:
      13.1、结构体实现外设定义介绍;
      13.2、硬件设计;
      13.3、软件设计;
      13.4、编译和测试;

13.1 结构体实现外设定义介绍
      HAL库已经有现成的API接口给我们使用了,我们可以直接调用这些API来操作外设,所以就不需要重复寄存器定义的工作,而本章节实验的目的是为了让大家更好地理解HAL库。
      前面我们在分析stm32mp157dxx_cm4.h文件的时候知道,STM32MP157的M4内核使用的寄存器都已经通过结构体的形式封装好了,我们可以在该文件中看到很多外设寄存器结构体声明,而声明一个结构体类型的时候是没有为它分配任何存储空间的,只有在定义结构体变量的时候,才会为变量分配存储空间,而C语言中结构体成员的地址是递增的,我们就利用这一点来访问寄存器。
图13.1.1 HAL库中寄存器的封装
在C语言中,结构体可以有不同的数据类型成员,成员在定义时依次存储在内存连续的空间中,结构体变量的首地址就是第一个成员的地址,内存偏移量就是各个成员相对于第一个成员地址的差(即,把低位内存分配给最先定义的变量)。理论上,结构体所占用的存储空间是各个成员变量所占的存储空间之和,但是为了提高CPU的访问效率,采用了内存对齐方式:
结构体的每一个成员起始地址必须是自身类型大小的整数倍,若不足,则不足部分用数据填充至所占内存的整数倍。
结构体大小必须是结构体占用最大字节数成员的整数倍,这样在处理数组时可以保证每一项都边界对齐
根据上面的说明,我们举例子分析如下:
      struct test
      {
            chara;
            int   b;
            floatc;
            double d;
      }mytest;
如果定义了该结构体,那么这个结构体所占用的内存怎么算呢?理论结果为17,实际上并不是17,而是24。为什么会这样呢?这个就是前面我们说的内存对齐。下面我们来分析该结构体内存怎么计算:
1)char型变量占1个字节,所以它的起始地址是0;
2)int类型占用4个字节,它的起始地址要求是4的整数倍数,那么内存地址1、2、3就需要被填充(被填充的内存不适于变量),b从4开始;
3)float类型也是占用4个字节,起始地址要求是4的倍数,所以c的起始地址就是8;
4)double类型变量占用8个字节,起始地址为16,12~15被填充。
这里,第一个成员a的地址为首地止,第二个成员b的偏移量为4,第三个成员c的偏移量是8,以此类推,是如下图所示:
图13.1.2结构地地址内存分配
上面的结构体成员a、b、c、d的类型是各不相同的,但在HAL库中,有很多的结构体,结构体中的成员类型基本上是一样的,例如GPIO的结构体,都是定义为uint32_t,即32位,每个成员占用4个字节,以第一个成员MODER为首地止0,第二个成员OTYPER相对于MODER偏移为4个字节,依次类推:
    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 VERR;            
      __IO uint32_t IPIDR;         
      __IO uint32_t SIDR;            
    } GPIO_TypeDef;
虽然结构体成员地址是连续的,但是还不能确定每个外设的地址,我们在ST的stm32mp157dxx_cm4.h文件中可以找到这样的定义:
#define PERIPH_BASE                           ((uint32_t)0x40000000)
#define MCU_AHB4_PERIPH_BASE      (PERIPH_BASE + 0x10000000)
#define GPIOB_BASE                            (MCU_AHB4_PERIPH_BASE + 0x3000)
#define GPIOB                                       ((GPIO_TypeDef *) GPIOB_BASE)
      这样一来就可以知道GPIOB基地址了,为:
      GPIOB_BASE= MCU_AHB4_PERIPH_BASE + 0x3000
                              = PERIPH_BASE + 0x10000000+0x3000
                              =0x40000000+0x10000000+0x3000
                              =0x50003000
      这个结果和我们在参考手册上查询到的结果一致:
图13.1.3参考手册部分截图
确定了GPIOB的基地址,再加上#define GPIOB((GPIO_TypeDef *) GPIOB_BASE)这句,那么GPIOB所有的寄存器的地址基本上就可以确定了。该语句的意思是,定义一个宏GPIOB,宏GPIOB是指向地址GPIOB_BASE 的结构体指针,这么一来,我们就可以通过以下方式将GPIO_ODR寄存器的值设置为0XFF了:
GPIOB->ODR=0XFF
这样一来,我们就不需要将GPIO外设所有用到的寄存器的地址都定义一遍了,我们只需要声明一个结构体,结构体中将GPIO的寄存器封装好,然后定义GPIO的基地址,再定义GPIO的宏为指向该基地址的指针,这样所有的寄存器地址也都确定下来了。不仅仅是GPIO,对于其它外设的寄存器也是一样的。
下面,我们就模仿HAL库的这种方法来控制LED0和LED1交替闪烁,效果和上一章节的实验一样。
13.2 硬件设计
      硬件原理图和第十一章节一样,本章节控制的是开发板的LED0和LED1,当引脚为低电平时LED亮,当引脚为高电平时,LED灭。
13.3 软件设计
      本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\3、M4裸机驱动例程\ MP157-M4 HAL库V1.2\实验2 结构体实现外设定义实验。
13.3.1 新建工程
      按照前面的步骤新建一个新的工程,并在工程中新建文件:启动文件startup_stm32mp15xx.s、main.c文件、main.h文件,这里startup_stm32mp15xx.s文件的内容和在上一章节的一样,可以直接将上一章节的代码拷贝过来:
图13.3.1.1启动文件要修改的地方
      新建好的工程目录如下所示:
图13.3.1.2新建的工程
13.3.2 添加用户代码
1. 添加main.h文件代码
      main.h文件主要是一些宏定义以及寄存器结构体封装,如下所示,因为RCC外设的寄存器比较多,我们这里不全部列举出来,省略掉了一部分:
1   #ifndef __MAIN_H
2   #define __MAIN_H
3
4   /*
5    * 数据类型
6    */
7   #define __IO volatile   /* volatile表示强制编译器减少优化 */
8   #define RESET   0
9   #define SET   1
10typedef unsigned int      uint32_t;
11   
12/*
13   * 各个外设基地址
14   */
15#define PERIPH_BASE               (0x40000000)
16#define MCU_AHB4_PERIPH_BASE    (PERIPH_BASE + 0x10000000)
17#define RCC_BASE                   (MCU_AHB4_PERIPH_BASE + 0x0000)
18
19#define GPIOF_BASE               (MCU_AHB4_PERIPH_BASE + 0x7000)
20#define GPIOI_BASE               (MCU_AHB4_PERIPH_BASE + 0xA000)
21
22/*
23   * RCC外设结构体
24   */
25typedef struct
26{
27      __IO uint32_t TZCR;                  /* 偏移地址: 0x00 */
28          uint32_t RESERVED0;          /* 偏移地址: 0x04 */
29      __IO uint32_t OCENSETR;             /* 偏移地址: 0x0C */
30      __IO uint32_t OCENCLRR;             /* 偏移地址: 0x10 */
31          uint32_t RESERVED1;            /* 偏移地址: 0x14 */
32      __IO uint32_t HSICFGR;            /* 偏移地址: 0x18 */
33      __IO uint32_t CSICFGR;            /* 偏移地址: 0x1C */
34/*******由于寄存器太多,这里省略了部分寄存器,具体代码可看本实验工程 ******/
35      __IO uint32_t MC_AHB4ENSETR;      /* 偏移地址: 0xAA8 */   
36      __IO uint32_t VERR;               /* 偏移地址: 0xFF4 */
37      __IO uint32_t IPIDR;                /* 偏移地址: 0xFF8 */
38      __IO uint32_t SIDR;               /* 偏移地址: 0xFFC */
39} RCC_TypeDef;
40
41/*
42   *GPIO外设结构体
43   */
44typedef struct
45{
46    __IO uint32_t MODER;            /* 偏移地址: 0x000 */
47    __IO uint32_t OTYPER;             /* 偏移地址: 0x004 */
48    __IO uint32_t OSPEEDR;            /* 偏移地址: 0x008 */
49    __IO uint32_t PUPDR;            /* 偏移地址: 0x00C */
50    __IO uint32_t IDR;                /* 偏移地址: 0x010 */
51    __IO uint32_t ODR;                /* 偏移地址: 0x014 */
52    __IO uint32_t BSRR;               /* 偏移地址: 0x018 */
53    __IO uint32_t LCKR;               /* 偏移地址: 0x01C */
54    __IO uint32_t AFR;             /* 偏移地址: 0x020-0x024 */
55    __IO uint32_t BRR;                /* 偏移地址: 0x028 */
56         uint32_t RESERVED0;          /* 偏移地址: 0x02C */
57    __IO uint32_t SECCFGR;            /* 偏移地址: 0x030 */
58         uint32_t RESERVED1;   /* 偏移地址: 0x034-0x3C4 */
59    __IO uint32_t HWCFGR10;         /* 偏移地址: 0x3C8 */
60    __IO uint32_t HWCFGR9;            /* 偏移地址: 0x3CC */
61    __IO uint32_t HWCFGR8;            /* 偏移地址: 0x3D0 */
62    __IO uint32_t HWCFGR7;            /* 偏移地址: 0x3D4 */
63    __IO uint32_t HWCFGR6;            /* 偏移地址: 0x3D8 */
64    __IO uint32_t HWCFGR5;            /* 偏移地址: 0x3DC */
65    __IO uint32_t HWCFGR4;            /* 偏移地址: 0x3E0 */
66    __IO uint32_t HWCFGR3;            /* 偏移地址: 0x3E4 */
67    __IO uint32_t HWCFGR2;            /* 偏移地址: 0x3E8 */
68    __IO uint32_t HWCFGR1;            /* 偏移地址: 0x3EC */
69    __IO uint32_t HWCFGR0;            /* 偏移地址: 0x3F0 */
70    __IO uint32_t VERR;               /* 偏移地址: 0x3F4 */
71    __IO uint32_t IPIDR;            /* 偏移地址: 0x3F8 */
72    __IO uint32_t SIDR;               /* 偏移地址: 0x3FC */
73} GPIO_TypeDef;
74
75/*
76   * RCC和GPIOI以及GPIOF相关宏定义
77   */
78#define RCC               ((RCC_TypeDef *) RCC_BASE)
79#define GPIOF               ((GPIO_TypeDef *) GPIOF_BASE)
80#define GPIOI               ((GPIO_TypeDef *) GPIOI_BASE)
81
82#endif
      第7行,宏__IO表示volatile,volatile表示强制编译器减少优化,告诉编译器必须每次去内存中取变量值,这样确保了数据的准确性;
      第15~20行,确定GPIOI、GPIOF和RCC的基地址分别为0x5000A000、0x50007000和0x50000000;
      第25~39行,声明RCC_TypeDef结构体,此结构体中对RCC外设的寄存器进行了封装;
      第44~73行,声明GPIO_TypeDef结构体,此结构体中对GPIO外设的寄存器进行了封装;
      第78~80行,分别定义了宏RCC是指向地址RCC_BASE的结构体指针,宏GPIOF是指向地址GPIOF_BASE的结构体指针,宏GPIOI是指向地址GPIOI_BASE 的结构体指针。
      参照以上外设地址的定义方法,我们还可以在此文件中添加更多外设的定义,本节实验只是控制PI0和PF3的电平变化,就不需要再定义其它外设了。
2. 添加main.c文件代码
      main.c文件代码如下,前面我们通过结构体的形式对寄存器进行了封装,那么这里我们就可以通过指针的方式来操作对应的寄存器了:
1   #include "main.h"
2
3   /* LED灯引脚定义 */
4   #define LED0_PORT       GPIOI
5   #define LED0_PIN      0
6   #define LED1_PORT       GPIOF
7   #define LED1_PIN      3
8
9   /**
10   * @brief   使能GPIOF和GPIOI时钟
11   * @param   无
12   * @retval    无
13   */
14void clk_enable(void)
15{
16      RCC->MC_AHB4ENSETR |= ((unsigned int)1 << 5);   /* 使能GPIOF时钟 */
17      RCC->MC_AHB4ENSETR |= ((unsigned int)1 << 8);   /* 使能GPIOI时钟 */
18}
19
20/**
21   * @brief   初始化GPIO为推挽输出、高速、上拉模式
22   * @param   无
23   * @retval    无
24   */
25void gpio_init(GPIO_TypeDef*GPIOx, unsigned char pin)
26{
27      /* 设置为输出 */
28      GPIOx->MODER &= ~((unsigned int)3 << (2 * pin));
29      GPIOx->MODER |= ((unsigned int)1 << (2 * pin));
30      
31      /* 设置为推完模式 */
32      GPIOx->OTYPER &= ~(1 << 15);
33      
34      /* 设置为高速模式 */
35      GPIOx->OSPEEDR &= ~((unsigned int)3 << (2 * pin));
36      GPIOx->OSPEEDR |= ((unsigned int)2 << (2 * pin));
37      
38      /* 设置为上拉 */
39      GPIOx->PUPDR &= ((unsigned int)3 << (2 * pin));
40      GPIOx->PUPDR |= ((unsigned int)1 << (2 * pin));
41}
42
43/**
44   * @brief   LED0开关函数
45   * @param   无
46   * @retval    无
47   */
48void pin_write(GPIO_TypeDef *GPIOx, unsigned char pin, unsigned char state)
49{
50      if(state == SET)
51      {
52          GPIOx->BSRR |= ((unsigned int)1 << pin);            /* 输出高电平 */
53      } else if(state == RESET)
54      {
55       GPIOx->BSRR |= ((unsigned int)1 << (15 + pin + 1)); /* 输出低电平 */
56      }
57}
58      
59/**
60   * @brief       短时间延时函数
61   * @param - n   要延时循环次数(空操作循环次数,模式延时)
62   * @retval      无
63   */
64void delay_short(volatile unsigned int n)
65{
66      while(n--){}
67}
68
69/**
70   * @brief       长延时函数
71   * @param - n   要延时的时间循环数
72   * @retval      无
73   */
74void delay(volatile unsigned int n)
75{
76      while(n--)
77      {
78          delay_short(0x7fff);
79      }
80}
81
82/**
83   * @brief       main函数
84   * @param       无
85   * @retval      无
86   */
87int main(void)
88{
89      clk_enable();                   /* 使能时钟   */
90      gpio_init(LED0_PORT, LED0_PIN); /* 初始化LED0    */
91      gpio_init(LED1_PORT, LED1_PIN); /* 初始化LED1    */
92      
93      while(1)
94      {
95          pin_write(LED0_PORT, LED0_PIN, RESET);/* LED0低电平打开 */
96          pin_write(LED1_PORT, LED1_PIN, SET);    /* LED1高电平关闭 */         
97          delay(100);
98          pin_write(LED0_PORT, LED0_PIN, SET);    /* LED0高电平关闭 */
99          pin_write(LED1_PORT, LED1_PIN, RESET);/* LED1低电平打开 */
100         delay(100);
101   }   
102 }
      以上的函数和上一章节的函数类似,不同的是是通过指针的方式来操作寄存器的,要注意的是,外设时钟开启以后才可以使用,所以不要忘记了使能外设时钟。代码比较简单,我们接下来直接编译和验证。
13.3.3 编译和测试
      编译后无报错,进入仿真,实现现象和上一章节的一样,LED0和LED1在交替闪烁。
页: [1]
查看完整版本: 《STM32MP1 M4裸机HAL库开发指南》第十三章