|
本帖最后由 正点原子 于 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
{
char a;
int b;
float c;
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
10 typedef 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 */
25 typedef struct
26 {
27 __IO uint32_t TZCR; /* 偏移地址: 0x00 */
28 uint32_t RESERVED0[2]; /* 偏移地址: 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 */
44 typedef 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[2]; /* 偏移地址: 0x020-0x024 */
55 __IO uint32_t BRR; /* 偏移地址: 0x028 */
56 uint32_t RESERVED0; /* 偏移地址: 0x02C */
57 __IO uint32_t SECCFGR; /* 偏移地址: 0x030 */
58 uint32_t RESERVED1[229]; /* 偏移地址: 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 */
14 void 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 */
25 void 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 */
48 void 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 */
64 void delay_short(volatile unsigned int n)
65 {
66 while(n--){}
67 }
68
69 /**
70 * @brief 长延时函数
71 * @param - n 要延时的时间循环数
72 * @retval 无
73 */
74 void 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 */
87 int 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在交替闪烁。 |
阿莫论坛20周年了!感谢大家的支持与爱护!!
月入3000的是反美的。收入3万是亲美的。收入30万是移民美国的。收入300万是取得绿卡后回国,教唆那些3000来反美的!
|