完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
上一章,我们只是简简单单的点亮了一个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位为输出端口 以上程序的改进,虽然基地址通过宏是得以简化,但是如果要把全部的寄存器都以宏定义的方式列举,那工作量还是很大,操作同意很麻烦,看来我们的改进还是不到位,那我们接着再来深入研究。 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的某个缓存获取该变量值,这时可以加快执行速度,但该缓存中的是陈旧数据,与我们要求的寄存器最新状态可能会有出入。 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的学习和使用。 |
|
相关推荐
|
|
只有小组成员才能发言,加入小组>>
75个成员聚集在这个小组
加入小组【深入浅出Cortex M4-SWM320 第六章】跑马灯与启动文件
7222 浏览 0 评论
5187 浏览 2 评论
4266 浏览 9 评论
321浏览 0评论
395浏览 0评论
283浏览 0评论
330浏览 0评论
324浏览 0评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-12-4 00:53 , Processed in 0.465273 second(s), Total 40, Slave 33 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号