打印
[综合信息]

【深入浅出Cortex M4-SWM320 】寄存器到库函数的演变过程

[复制链接]
515|2
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
stormwind123|  楼主 | 2023-5-29 09:21 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
上一章,我们只是简简单单的点亮了一个LED小灯,我们就看了好几遍数据手册,而且每次需要查好多页,那如果读者以后需要开发大量的外设,例如定时器、串口、SDRAM等外设时,是不是需要查阅N 多倍的资料?如果读者是在校学生,时间充足,而且是为了学习,这样是完全可以的。一来不用考虑项目带来的压力,二来还可以深入原理的学习。但是如果读者走上了社会,需要参与某一个项目的研发,同时开发周期比较紧张时,这样势必会浪费太多的时间,那有没有更好的办法呢?答案当然是肯定的。
5.1库函数的基础——宏定义
这时,我们再回过头去,看看上节的点灯代码,便于读者查阅,直接贴到这里,源码如下,核心源码就两句话。
1.        int main()
2.        {
3.                 *(volatile unsigned long *)0x40000008 |= 1 << 21;         // 使能P口的时钟
4.                 *(volatile unsigned long *)0x40018004 |= 1 << 22;         //  P口的第22位为输出端口
5.                 
6.                 while(1)
7.                 {
8.                 }
9.        }
如果将寄存器做一个定义,源码如下:
10.    #define AHB_BASE                     0x40000000                                 // AHB总线的基地址
11.    #define SYS_BASE                       (AHB_BASE + 0x00000)            //SYS寄存器的基地址
12.    #define CLKEN                             0x08                                                        //偏移地址
13.    #define GPIOP_CLKEN              *(volatile unsigned long*)(SYS_BASE + CLKEN)
14.            
15.    #define APB_BASE                    0x40010000
16.    #define GPIOP_BASE                 (APB_BASE + 0x08000)
17.    #define DIR                                             0x08     
18.    #define GPIOP_OUTEN         *(volatileunsigned long *)(GPIOP_BASE + DIR)
这样一来,上面的代码就可以变为如下所示的源码。
GPIOP_CLKEN |= 1 << 21;        //使能P口的时钟
GPIOP_OUTEN |= 1 << 22;       //P口的第22位为输出端口
以上程序的改进,虽然基地址通过宏是得以简化,但是如果要把全部的寄存器都以宏定义的方式列举,那工作量还是很大,操作同意很麻烦,看来我们的改进还是不到位,那我们接着再来深入研究。

使用特权

评论回复

相关帖子

沙发
stormwind123|  楼主 | 2023-5-29 09:23 | 只看该作者
5.2库函数的基础——结构体
上一章中我们在操作寄存器的时候,操作的是都寄存器的绝对地址,如果每个外设寄存器都这样操作,那将非常麻烦。由上章可知,外设寄存器的地址都是基于外设基地址的偏移地址,都是在外设基地址上逐个连续递增的,每个寄存器4个字节,这种方式跟结构体中的成员变量类似。因此我们可以功能类似的一些寄存器定义成一种外设结构体,结构体的地址等于外设的基地址,结构体的成员等于寄存器,成员的排列顺序跟寄存器的顺序一样。这样我们操作寄存器的时候就不用每次都找到绝对地址,只要知道外设的基地址就可以操作外设的全部寄存器,即操作结构体的成员即可。
在点灯工程中的“SWM320.h”文件中,我们使用结构体封装GPIO、SYS外设的寄存器,详细代码如下。结构体成员的顺序按照寄存器的偏移地址从低到高排列,成员类型跟寄存器类型一样。
1.        typedef struct {
2.                 __IO uint32_t DATA;
3.        #define PIN0    0
4.        #define PIN1    1
5.        #define PIN2    2
6.        #define PIN3    3
7.        #define PIN4    4
8.        #define PIN5    5
9.        #define PIN6    6
10.    #define PIN7    7
11.    #define PIN8    8
12.    #define PIN9    9
13.    #define PIN10   10
14.    #define PIN11   11
15.    #define PIN12   12
16.    #define PIN13   13
17.    #define PIN14   14
18.    #define PIN15   15
19.    #define PIN16   16
20.    #define PIN17   17
21.    #define PIN18   18
22.    #define PIN19   19
23.    #define PIN20   20
24.    #define PIN21   21
25.    #define PIN22   22
26.    #define PIN23   23
27.    #define PIN24    24
28.     
29.             __IO uint32_t DIR;                              //0 输入 1 输出
30.     
31.             __IO uint32_t INTLVLTRG;               
//InterruptLevel Trigger  1 电平触发中断 0 边沿触发中断
32.     
33.             __IO uint32_t INTBE;                                            
//Both Edge,当INTLVLTRG设为边沿触发中断时,此位置1表示上升沿和下降沿都触发中断,置0时触发边沿由INTRISEEN选择
34.     
35.             __IO uint32_t INTRISEEN;                                   
//Interrupt RiseEdge Enable   1 上升沿/高电平触发中断        0 下降沿/低电平触发中断
36.     
37.             __IO uint32_t INTEN;                         //1中断使能 0 中断禁止
38.     
39.             __IO uint32_t INTRAWSTAT;             //中断检测单元是否检测到了触发中断的条件 1 检测到了中断触发条件    0 没有检测到中断触发条件
40.     
41.             __IO uint32_t INTSTAT;                      //INTSTAT.PIN0= INTRAWSTAT.PIN0 & INTEN.PIN0
42.     
43.             __IO uint32_t INTCLR;                     //写1清除中断标志,只对边沿触发中断有用
44.    } GPIO_TypeDef;
以上的结构体中,3~27行,是简单的宏定义,这个对有C基础的读者来说很简单吧。其中第2行是DATA,即数据寄存器,他相对于GPIOx基地址偏移量为0x00;同理,第29行是DIR(方向控制寄存器),相对于GPIOx基地址偏移量为0x04,别的寄存器和定义道理类似。
SYS(系统管理)结构体的封装源码如下:
1.        typedef struct {
2.                 __IO uint32_t CLKSEL;                                     //Clock Select
3.         
4.                 __IO uint32_t CLKDIV;
5.         
6.                 __IO uint32_t CLKEN;                                           //ClockEnable
7.         
8.                 __IO uint32_t SLEEP;
9.         
10.                        uint32_tRESERVED[60+64];
11.     
12.             __IO uint32_t PAWKEN;                                  //Port A Wakeup Enable
13.             __IO uint32_t PBWKEN;
14.             __IO uint32_t PCWKEN;
15.        
16.             uint32_t RESERVED2[1+4];
17.     
18.             __IO uint32_t PAWKSR;                                   //Port A Wakeup Status Register,写1清零
19.             __IO uint32_t PBWKSR;
20.             __IO uint32_t PCWKSR;
21.     
22.                        uint32_tRESERVED3[64-11];
23.     
24.             __IO uint32_t REMAP;              //0程序在ROM中执行          1 程序在FLASH中执行
25.     
26.             __IO uint32_t RSTCR;                                           //ResetControl Register
27.             __IO uint32_t RSTSR;                                            //ResetStatus Register
28.     
29.                        uint32_tRESERVED4[61+64];
30.     
31.             __IO uint32_t BKP[3];                                           //数据备份寄存器
32.        
33.        //RTC Power Domain: 0x4001E000
34.             uint32_tRESERVED5[(0x4001E000-0x40000508)/4-1];
35.        
36.        __IO uint32_t RTCBKP[7];                          //RTC电源域数据备份寄存器
37.            
38.             __IO uint32_t ADC_PGA_VCM;                //ADC前置可编程增益放大器共模电压设置
39.        
40.        __IO uint32_t LRCCR;                                  //Low speed RC Control Register
41.        __IO uint32_t LRCTRIM0;                          //Low speed RC Trim
42.        __IO uint32_t LRCTRIM1;
43.        
44.             uint32_t RESERVED6;
45.            
46.        __IO uint32_t RTCLDOTRIM;               //RTC Power Domain LDO Trim
47.        
48.        //Analog Control: 0x40031000
49.             uint32_tRESERVED7[(0x40031000-0x4001E030)/4-1];
50.        
51.        __IO uint32_t HRCCR;                                 //High speed RC Control Register
52.             __IO uint32_t HRC20M;                             //[24:0]High speed RC Trim Value for 20MHz
53.             __IO uint32_t HRC40M;                             //[24:0]High speed RC Trim Value for 40MHz
54.     
55.             uint32_t RESERVED8[3];
56.        
57.        __IO uint32_t BGTRIM;
58.        
59.        __IO uint32_t TEMPCR;             //温度传感器控制寄存器
60.        
61.        __IO uint32_t XTALCR;
62.        
63.        __IO uint32_t PLLCR;
64.        __IO uint32_t PLLDIV;
65.        __IO uint32_t PLLSET;
66.        __IO uint32_t PLLLOCK;                //[0] 1 PLL已锁定
67.        
68.        __IO uint32_t BODIE;
69.        __IO uint32_t BODIF;
70.        
71.        __IO uint32_t ADC1IN7;
72.        
73.             __IO uint32_t BODCR;
74.    } SYS_TypeDef;
这段代码在每个结构体成员前增加了一个“__IO”前缀,它的原型在“core_cm4.h”头文件中,源码为:#define     __IO    volatile,实质就是C语言中的关键字“volatile”,在C语言中该关键字用于表示变量是易变的,要求编译器不要优化。这些结构体内的成员,都代表着寄存器,而寄存器很多时候是由外设或 SWM320芯片状态修改的,也就是说即使CPU不执行代码修改这些变量,变量的值也有可能被外设修改、更新,所以每次使用这些变量的时候,我们都要求CPU去该变量的地址重新访问。若没有这个关键字修饰,在某些情况下,编译器认为没有代码修改该变量,就直接从 CPU的某个缓存获取该变量值,这时可以加快执行速度,但该缓存中的是陈旧数据,与我们要求的寄存器最新状态可能会有出入。

使用特权

评论回复
板凳
stormwind123|  楼主 | 2023-5-29 09:24 | 只看该作者
5.3库函数的基础——结构体指针
前面我们有过宏定义,那只是将一个寄存器地址定义了一个名称,以便简化操作,并未解决寄存器操作的操作繁杂过程,接下来我们再定义一组结构体指针,详细源码如下:
1.        #define AHB_BASE                     0x40000000
2.        #define APB_BASE                    0x40010000
3.         
4.        #define SYS_BASE                       (AHB_BASE + 0x00000)
5.         
6.        #define GPIOP_BASE                 (APB_BASE + 0x08000)
7.         
8.        #define SYS                                   ((SYS_TypeDef*) SYS_BASE)
9.         
10.    #define GPIOP                             ((GPIO_TypeDef *)GPIOP_BASE)
这些宏通过强制把外设的基地址转换成GPIO_TypeDef类型的地址,从而得到GPIOM、GPIOP等直接指向对应外设的指针,通过结构体的指针操作,即可访问对应外设的寄存器。继而我们可以将点亮LED的小灯程序变为如下的代码:
SYS->CLKEN |= 1 << 21;            // 使能P口的时钟
GPIOP->DIR |= 1 << 22;            // P口的第22位为输出端口
看到这里,读者可能会问,点个小灯,怎么越学越复杂,不是说函数库可以简化操作吗?为何还要我们做这么多工作?嗯,带着这些问题,我们接着再来看什么是函数库!
5.4库函数
再讲述库函数之前,我们先来了解一下什么是库函数?用库函数操作的好处又是什么呢?这里所说的库函数是指“SWM320标准函数库”,它是由华芯微特公司针对SWM320提供的函数接口,即API (Application ProgramInterface),开发者可调用这些函数接口来快速的配置SWM320的寄存器,使开发人员得以脱离最底层的寄存器操作,有开发快速、易于阅读、维护成本低等优点。
当我们调用库API的时候不需要挖空心思去了解库底层的寄存器操作,就像当年我们刚开始学习C语言的时候,用prinft()函数时只是学习它的使用方法,并没有去研究它的源码实现,但需要深入研究的时候,经过千锤百炼的库API源码就是最佳学习范例。
实际上,库是架设在寄存器与用户驱动层之间的桥梁,向下配置相关的寄存器,向上为用户提供配置寄存器的接口,继而简化了寄存器的配置。刚开始读者可能体会不到库函数的便捷性,但是随着使用的深入,库函数的强大会是读者如痴如醉。可能有读者也会说直接操作寄存器会加深对底层的了解,其实要知道,库函数也是直接操作寄存器,而且他的操作方式将C语言和硬件有机结合,其严谨、科学的操作方式更是读者学习编程、操作寄存器最后的模板,有读者发出用库操作的各种忧虑,笔者认为,那是你在忧虑自己的懒惰,如果读者愿意,可以将每个库函数自己模仿着实现一遍,这样会大大提高你的编程、寄存器配置能力。
接下来我们选一个最简单的GPIO初始化函数来作为例子,为读者解释个一二三,便于讲解,这里先上源码,具体源码如下:
1.        /******************************************************************************************************************************************
2.        * 函数名称: GPIO_Init()
3.        * 功能说明:   引脚初始化,包含引脚方向、上拉电阻、下拉电阻、开漏输出
4.        * 输    入: GPIO_TypeDef * GPIOx          指定GPIO端口,有效值包括GPIOA、GPIOB、GPIOC、GPIOM、GPION、GPIOP         
5.        * uint32_t  n    指定GPIO引脚,有效值包括PIN0、PIN1、PIN2、... ... PIN22、PIN23
6.        *                          uint32_t dir             引脚方向,0 输入        1 输出
7.        *                          uint32_t pull_up          上拉电阻,0 关闭上拉    1 开启上拉
8.        *                          uint32_t pull_down    下拉电阻,0 关闭下拉    1 开启下拉
9.        * 输    出: 无
10.    * 注意事项: 无
11.    ******************************************************************************************************************************************/
12.    void GPIO_Init(GPIO_TypeDef *GPIOx, uint32_t n, uint32_t dir, uint32_t pull_up, uint32_t pull_down)
13.    {
14.             switch((uint32_t)GPIOx)
15.             {
16.             case ((uint32_t)GPIOA):
17.                       SYS->CLKEN |= (0x01 <
18.                       
19.                       PORT_Init(PORTA, n, 0, 1);//PORTA.PINn引脚配置为GPIO功能,数字输入开启
20.                       if(dir == 1)
21.                       {                           
22.                                GPIOA->DIR |= (0x01 << n);
23.                       }
24.                       else
25.                       {
26.                                GPIOA->DIR &= ~(0x01 << n);
27.                       }
28.                       
29.                       if(pull_up == 1)
30.                                PORT->PORTA_PULLU |= (0x01 <
31.                       else
32.                                PORT->PORTA_PULLU &= ~(0x01<< n);
33.                       break;
34.                       // 为了节省篇幅,这里我们将类似的源码省略掉,因为读者掌握了GPIOA的操作,则GPIOB、GPIOC、GPIOM、GPION、GPIOP类似。
35.                       if(pull_up == 1)
36.                                PORT->PORTP_PULLU |= (0x01 <
37.                       else
38.                                PORT->PORTP_PULLU &= ~(0x01<< n);
39.                       break;
40.             }
41.    }
我们从函数的名称和注释可知,函数的功能是将其某个端口的某一位进行初始化(包括引脚的方向,是否使能上、下拉电阻,是否开漏输出)。函数有五个入口参数,第一个参数为我们定义的结构体指针,这个上面已经有讲述;第二个是GPIO的引脚位,我们要操作那个位,这里用PINx来选择即可;第三个是引脚的方向,也即是配置为输入还是输出;第四、五则为是否使能上、下拉电阻。
第17行是使能端口的时钟,这个前面我们也有用到,区别是,前面的SYS结构体指针是我们手工宏定义的,这里人性化的厂家已经在“SWM320.h”(读者可以将这个头文件打开,好好读一读)里为我们定义好了,我们只需应用即可。此时我们打开数据手册,查看“CLKEN”寄存器可知,要使能GPIOA的时钟,需要“CLKEN”寄存器的第0位置1,按5.3节的讲述,源码应该为:SYS->CLKEN |= 0x01 << 0;这时我们通过Keil软件的“Go To Definition xxx”跳转到“SWM320.h”头文件,可以看到官方做了一个这样的宏定义:
#define  SYS_CLKEN_GPIOA_Pos           0
恰恰与我们的分析相对应,也即使能GPIOA口的时钟;第19行的源码在“SWM320_port.c”中,后面章节会专门讲述,读者直接跳过即可;第20~27行,是一个if…else…语句,以上是如果我们给这个函数的三个参数为1,说明要将其引脚位设置为输出端口,那么执行if语句,这个语句前面我们也用过,只是前面给了特定的移位数值22,这里为“n”,也就我们的第二个参数,根据输入的参数来设置引脚的位数;第29~32是设置是否上拉,和输入、输出原理类似。读者是不是觉得函数很简单呢?
有了上面的库函数,我们的点灯程序就可以变得尤为简单,因为这些初始化函数,官方已经写好了,我们只需调用,函数源码如下:
1.        #include "SWM320.h"
2.         
3.        int main()
4.        {
5.                 GPIO_Init(GPIOP, PIN22, 1, 0, 0);
6.                 
7.                 while(1)
8.                 {
9.                 }
10.    }
该程序的核心就一行程序,调用“GPIO_Init()”函数,将其GPIOP口的第22脚设置为输出端口,不使能上、下拉电阻,这样就可以实现点亮一个LED小灯。
注意:读者需要建立完整的工程,并包含“SWM320_port.c”和“SWM320_gpio.c”文件,编译、下载,既可以实现FSST32核心板上LED的点亮。库函数的学习和使用过程还任重道远,我们这里只是演化了他的由来以及实现原理,后面我们会大量运用库函数来实现想要的功能,继而深入SWM320的学习和使用。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

416

主题

2134

帖子

2

粉丝