通过前面的分析,大家已经了解了预定义部分的内容。但如果每次设计程序都把需要的头文件内容全部写出来,不仅代码占用较多的篇幅,还会影响程序的结构化。因此,规范的程序设计都会把这部分内容通过头文件包含的形式引入进来。下面就来讨论如何实现这一形式。
头文件是以“.h”为后缀的文本文件,它的名称、数量都与开发环境有关,不同的开发环境(甚至同一开发环境的不同版本)所带的头文件都不一定相同。因此本节在讨论LPC824开发的头文件配置时,都以ARM-MDK5.26.2版本开发环境为准,其他版本的可参考使用。
先来看,同样实现LED交替闪烁的功能,若用包含头文件的写法,代码可换成如下形式:
#include
//************************使用外部晶振**********************************
void Ext_osc(void)
{
LPC_SYSCON->SYSAHBCLKCTRL |= (1 << 18); //使能IOCON时钟
LPC_IOCON->PIO0_8 &= ~(3 << 3); //把P0_8引脚配置为无上下拉电阻方式
LPC_IOCON->PIO0_9 &= ~(3 << 3); //把P0_9引脚配置为无上下拉电阻方式
LPC_SWM->PINENABLE0 &= ~(3 << 6); //把P0_8、P0_9引脚配置为XTALIN、XTALOUT引脚
LPC_SYSCON->SYSAHBCLKCTRL &= ~(1 << 18); //禁止IOCON时钟
}
//************************时钟初始化***********************************
void SysCLK_config(void)
{
uint8_t i;
LPC_SYSCON->PDRUNCFG &= ~(1 << 5); //给系统振荡器上电
LPC_SYSCON->SYSOSCCTRL = 0x00000000; //系统振荡器未旁路,1~12MHz输入
for (i = 0; i < 200; i++) __nop(); //延时等待振荡器稳定
LPC_SYSCON->SYSPLLCLKSEL = 0x00000001; //PLL输入选择外部晶体振荡
LPC_SYSCON->SYSPLLCLKUEN = 0x00;
LPC_SYSCON->SYSPLLCLKUEN = 0x01; //先写0后写1更新时钟源
while (!(LPC_SYSCON->SYSPLLCLKUEN & 0x01)); //等待更新完成
LPC_SYSCON->SYSPLLCTRL = 0x00000041; //M=2、P=4,倍频后的时钟为24MHz
LPC_SYSCON->PDRUNCFG &= ~(1 << 7); //给PLL上电
while (!(LPC_SYSCON->SYSPLLSTAT & 0x01)); //等待PLL锁定
LPC_SYSCON->MAINCLKSEL = 0x00000003; //主时钟选择PLL倍频后的时钟
LPC_SYSCON->MAINCLKUEN = 0x00;
LPC_SYSCON->MAINCLKUEN = 0x01; //先写0后写1更新时钟源
while (!(LPC_SYSCON->MAINCLKUEN & 0x01)); //等待更新完成
LPC_SYSCON->SYSAHBCLKDIV = 0x00000001; //AHB为1分频,AHB时钟为24MHz
}
//************************系统初始化***********************************
void SystemInit(void)
{
Ext_osc(); //调用使用外部晶振
SysCLK_config(); //调用时钟配置函数
}
//************************端口初始化***********************************
void Port_init(void)
{
LPC_GPIO_PORT->DIR0 = 0x1FFFFFFF; //设置端口为输出方向
LPC_GPIO_PORT->PIN0 = 0x10090080; //输出相应电平交替点亮LED
}
//************************定时器初始化*********************************
void SysTick_init(void)
{
SysTick->LOAD = (((12000)*1000)-1);
SysTick->VAL = 0;
SysTick->CTRL |= ((1<<1)|(1<<0));
}
//***************************主函数************************************
int main(void)
{
SystemInit(); //调用系统初始化
Port_init(); //调用端口初始化
SysTick_init(); //调用定时器初始化
while(1)
{
;
}
}
//************************定时器中断***********************************
void SysTick_Handler(void)
{
LPC_GPIO_PORT->PIN0 = ~LPC_GPIO_PORT->PIN0; //取反赋值
}
从上面的代码可以看出,代码量明显比原来的减小了很多。这是由于执行了第一句“#include
在默认配置下,“LPC82x.h”这个头文件位于路径“C:Keil_v5ARMPACKKeilLPC800_DFP1.2.0DeviceInclude”下(假设MDK-ARM安装在C盘下)。通过记事本打开它,可看到程序中用到的三个结构体“SYSCON”、“IOCON”、“GPIO”都定义在其中,并且都做了结构体指针的基址强制转换宏定义,所以主程序包含了头文件“LPC82x.h”后,这三个结构体就可以直接引用了。此外,头文件“LPC82x.h”中还定义了很多其他结构体,这些结构体都对应到相应模块的寄存器,原理和前面分析的一样,大家可自行分析。可见,只须包含一个头文件,就可以减少很多的工作量。
头文件还可以嵌套包含,即在一个头文件中还可以再包含其他的头文件。比如在“LPC82x.h”这个头文件中,并没有找到像“#typedef unsigned char uint8_t;”这样的定义。但为何编译能通过呢?仔细观察会发现,其实在“LPC82x.h”这个头文件中还包含了“system_LPC82x.h”这个头文件,而在“system_LPC82x.h”这个头文件中又包含了“stdint.h”这个头文件,在“stdint.h”这个头文件中就包含有像“#typedef unsigned char uint8_t;”这样的定义,这样做可以让功能结构非常明了。同样,像“#define __IO volatile”这样的定义位于头文件“core_cm0plus.h”中,结构体“SysTick”的定义也在头文件“core_cm0plus.h”中,而“core_cm0plus.h”又包含于头文件“LPC82x.h”中。默认情况下,“core_cm0plus.h”这个头文件位于路径“C:Keil_v5ARMPACKARMCMSIS5.4.0CMSISInclude”下,而“stdint.h”则位于路径“C:Keil_v5ARMARMCCinclude”下。
ARM公司曾于2008年发布过一个Cortex微处理器软件接口标准(CMSIS,Cortex Microcontroller Software Interface Standard),现在这个标准已经升级到了2.0版本。基于此标准的软件构架主要分为4层:用户应用层、操作系统层、CMSIS层和硬件寄存器层。其中CMSIS层起着承上启下的作用。一方面该层对硬件寄存器层进行了统一的实现,屏蔽了不同厂商对Cortex-M系列微处理器核内外设寄存器的不同定义;另一方面又向上层的操作系统和应用层提供接口,简化了应用程序开发的难度,让开发人员能够在完全透明的情况下进行一些应用程序的开发。CMSIS层的实现较为复杂,有兴趣的可自行查询学习。开发环境MDK-ARM中的头文件结构正是基于CMSIS规范的,CMSIS提供的文件很多,在实际开发中要结合实际的器件选型来进行选择。
基于CMSIS构架下,程序的结构化会非常的明晰,同时在开发环境的辅助下,有时复杂的工作也会变得出奇简洁。利用CMSIS构架,前面的程序还可以做到进行一步简洁,比如时钟配置部分和系统初始化部分,可以不写在主程序中,而只需要把文件“system_LPC82x.c”引入进来就可以了(该文件位于“C:Keil_v5ARMPACKKeilLPC800_DFP1.2.0DeviceSource”)。前面讨论过,在LPC824开发前,必须有时钟配置和系统初始化部分,否则程序编译不能通过。然而,通过CMSIS构架的规范,时钟配置和系统初始化的内容已经被写在了“system_LPC82x.c”文件中,只须把它引入进来就可以了。但引入时要注意,由于它不是“.h”的头文件形式,所以不需要用“include”语句,只需要在开发环境中把它添加进来就可以了。完成后的文件树结构如下图所示。
这样一来,上述的程序代码还会变得更加简洁,如下:
#include
//************************端口初始化***********************************
void Port_init(void)
{
LPC_GPIO_PORT->DIR0 = 0x1FFFFFFF; //设置端口为输出方向
LPC_GPIO_PORT->PIN0 = 0x10090080; //输出相应电平交替点亮LED
}
//************************定时器初始化*********************************
void SysTick_init(void)
{
SysTick->LOAD = (((12000)*1000)-1);
SysTick->VAL = 0;
SysTick->CTRL |= ((1<<1)|(1<<0));
}
//***************************主函数************************************
int main(void)
{
Port_init(); //调用端口初始化
SysTick_init(); //调用定时器初始化
while(1)
{
;
}
}
//************************定时器中断***********************************
void SysTick_Handler(void)
{
LPC_GPIO_PORT->PIN0 = ~LPC_GPIO_PORT->PIN0; //取反赋值
}
由此可见,在CMSIS构架的配合下,开发者可专注于主要的业务工作,而不必被其他细节所打扰,减少了很多麻烦事情。可能有人会问,那需要修改时钟配置时怎么办呢?最有效的方法当然是直接打开“system_LPC82x.c”文件,找到系统初始化函数“SystemInit()”,再去修改其中相应的时钟配置部分。其实,依靠开发环境的支持,修改时钟配置也变得简单了。MDK-ARM支持图形化的时钟配置,在环境中双击左边的“system_LPC82x.c”文件,会以文本形式打开该文件,在该标签页的下方,单击“Configuration Wizard”,会发现刚才的文本配置转换成了图形配置的形式,如下图所示。
从上图中可以看到,在每一项内容里面都可以通过鼠标点击来进行配置,而不必去代码中修改,非常方便。这里我们需要把系统振荡时钟更改为外部振荡(默认为内部RC振荡),用鼠单击上图中第5行“System PLL Clock Soure Oscillator Frequency Range”一项后面的“IRC Oscillator”,会形成一个下拉列表框,在其中选择“Crystal Oscillator(SYSOSC)”一项即可。选择了外部振荡SYSOSC以后,连接外部晶振的两个引脚P0_8、P_9引脚也同时被配置为了XTALIN、XTALOUT引脚,所以在程序代码中就不再需要Ext_Osc函数了,非常方便。如果发现上图界面中的选项不允许修改(为灰色),这是因为文件system_LPC82x.c为只读属性造成的,只要把它的只读属性去掉就可以了(具有只读属性的文件在MDK中的图标上会显示有一把钥匙)。另外,同样可以使用图形化方法进行配置的文件还有系统启动文件“startup_LPC82x.s”。
由于LED交替闪烁的程序比较简单,所以在MDK-ARM中并没有对文件进行分类。其实在规范的程序开发中,都会给程序文件通过目录的形式来分一下类,这有利于结构化,条理明晰,在开发大型项目时尤为适用。至于需要分配多少个目录,没有统一的规定,可根据需要自行决定。
把上述使用头文件精简过的代码放到MDK环境中去编译,发现会报一个错,说找不到文件core_cm0plus.h,这时要把该文件所在的路径通知MDK。打开配置对话框,选择C/C++标签页,在“Include Paths”后面的方框中填入core_cm0plus.h文件所在的路径,如下图所示。
以后在开发项目过程中如果还有自己定义的头文件,也在此处把路径一并包含进来,这样就能顺利通过编译了。把编译后的目标文件下载到LPC824中,可发现运行效果跟前面的实例是一模一样的。