头文件解析

在前面的第一个演示示例中可以看出,光实现LED的闪烁,其代码量似乎比51单片的多了很多。但仔细观察后会发现,其实除了多了时钟配置以外,就数预定义部分的代码数量最多,而且这部分大多是以结构体的形式出现的。其实在正规的开发过程中,这部分预定义的内容被是被放在头文件中并包含进来的,前面的代码只是为了编译方便,所以把全部代码都给了出来,不太规范。下面就来讨论一下在ARM-MDK环境中开发LPC824的头文件配置。
在前面的示例中,给出了预定义部分的内容,但没有进行解释。这里就先来讨论一下在第一个演示示例中预定义部分的内容。
先看第一个部分,代码如下:

#define   __I     volatile const
#define   __O     volatile      
#define   __IO    volatile            
typedef unsigned           char uint8_t;
typedef unsigned short     int uint16_t;
typedef unsigned           int uint32_t;

第一、二、三行是三个宏定义,通过define语句把__IO等效为volatile,把__O等效为volatile,把__I等效为volatile const。一般来说宏定义都用大写形式,但因为这里用的字母比较少(只有I或O或IO),所以在其前面添加双下划线来进行区分,这样做可以有效避免命名冲突问题。volatile本身是一个关键字,表示其后面定义的变量不让编译器进行优化,即每次读取或者修改值的时候,都必须重新从内存或者寄存器中读取或者修改。例如在单片机开发中,经常会用到软件延时,但若想让软件延时不被编译器优化掉,就必须在变量定义前加上关键字volatile,如“for(volatile unsigned int k=0;k<60000;k++);”。volatile const则表示其后面定义的变量为只读,比如用它来定义一个只读的状态寄存器。定义为volatile是因为它的值可能会被硬件意想不到地改变,而定义为const则是因为程序不应该试图去修改它的值。通俗的说,就是它定义的是一个“只读变量”而不是常量,它的值是由硬件来改变的,不能通过程序写入来改变。
归纳一下:
__I:定义输入口。既然是输入,那么寄存器的值就随时会被外部修改,所以不能对它进行优化,每次都必须从寄存器中读取。也不能写(只读),否则就不是输入而是输出了。
__O:定义输出口,也不能对它进行优化,不然端口连续两次输出相同的值,编译器就会认为没有变化,而忽略后那一次输出,假如外部在两次输出中间修改了值,那就会影响输出的正确性。可写,否则就不能称为输出了。
__IO:定义输入输出口,也不能对它进行优化,原因同上。可读可写。
第三至五行是类型声明,把无符号的字符型、短整型、整型分别用uint8_t、uint16_t、uint32_t来表示,以突出它们所占用的字节数,方便查看。
接下来看第二部分,这部分全部使用结构体来对寄存器进行描述。下面给出的是系统控制部分SYSCON的结构体定义,这部分内容是直接从MDK-ARM开发环境的头文件中拷贝过来,所以每句都带有详细的英文注解。

typedef struct {                  /*!< (@ 0x40048000) SYSCON Structure  */
  __IO uint32_t  SYSMEMREMAP;     /*!< (@ 0x40048000) System memory remap */
  __IO uint32_t  PRESETCTRL;      /*!< (@ 0x40048004) Peripheral reset control  */
  __IO uint32_t  SYSPLLCTRL;      /*!< (@ 0x40048008) System PLL control  */
  __I  uint32_t  SYSPLLSTAT;      /*!< (@ 0x4004800C) System PLL status   */
  __I  uint32_t  RESERVED0[4];
  __IO uint32_t  SYSOSCCTRL;      /*!< (@ 0x40048020) System oscillator control  */
  __IO uint32_t  WDTOSCCTRL;      /*!< (@ 0x40048024) Watchdog oscillator control  */
  __IO uint32_t  IRCCTRL;         /*!< (@ 0x40048028) IRC control  */
  __I  uint32_t  RESERVED1;
  __IO uint32_t  SYSRSTSTAT;      /*!< (@ 0x40048030) System reset status register  */
  __I  uint32_t  RESERVED2[3];
  __IO uint32_t  SYSPLLCLKSEL;    /*!< (@ 0x40048040) System PLL clock source select   */
  __IO uint32_t  SYSPLLCLKUEN;    /*!< (@ 0x40048044) System PLL clock source update enable */
  __I  uint32_t  RESERVED3[10];
  __IO uint32_t  MAINCLKSEL;      /*!< (@ 0x40048070) Main clock source select   */
  __IO uint32_t  MAINCLKUEN;      /*!< (@ 0x40048074) Main clock source update enable  */
  __IO uint32_t  SYSAHBCLKDIV;    /*!< (@ 0x40048078) System clock divider  */
  __I  uint32_t  RESERVED4;
  __IO uint32_t  SYSAHBCLKCTRL;   /*!< (@ 0x40048080) System clock control  */
  __I  uint32_t  RESERVED5[4];
  __IO uint32_t  UARTCLKDIV;      /*!< (@ 0x40048094) USART clock divider */
  __I  uint32_t  RESERVED6[18];
  __IO uint32_t  CLKOUTSEL;       /*!< (@ 0x400480E0) CLKOUT clock source select   */
  __IO uint32_t  CLKOUTUEN;       /*!< (@ 0x400480E4) CLKOUT clock source update enable  */
  __IO uint32_t  CLKOUTDIV;       /*!< (@ 0x400480E8) CLKOUT clock divider  */
  __I  uint32_t  RESERVED7;
  __IO uint32_t  UARTFRGDIV;      /*!< (@ 0x400480F0) USART1 to USART4 common fractional generator
                                                         divider value  */
  __IO uint32_t  UARTFRGMULT;   /*!< (@ 0x400480F4) USART1 to USART4 common fractional generator
                                                          multiplier value   */
  __I  uint32_t  RESERVED8;
  __IO uint32_t  EXTTRACECMD;     /*!< (@ 0x400480FC) External trace buffer command register  */
  __I  uint32_t  PIOPORCAP0;      /*!< (@ 0x40048100) POR captured PIO status 0  */
  __I  uint32_t  RESERVED9[12];
  __IO uint32_t  IOCONCLKDIV6;   /*!< (@ 0x40048134) Peripheral clock 6 to the IOCON block for
                                                          programmable glitch filter   */
  __I  uint32_t  RESERVED10[6];
  __IO uint32_t  BODCTRL;         /*!< (@ 0x40048150) Brown-Out Detect  */
  __IO uint32_t  SYSTCKCAL;       /*!< (@ 0x40048154) System tick counter calibration  */
  __I  uint32_t  RESERVED11[6];
  __IO uint32_t  IRQLATENCY;      /*!< (@ 0x40048170) IQR delay. Allows trade-off between interrupt
                                                        latency and determinism. */
  __IO uint32_t  NMISRC;          /*!< (@ 0x40048174) NMI Source Control */
  __IO uint32_t  PINTSEL0;        /*!< (@ 0x40048178) GPIO Pin Interrupt Select register 0 */
  __IO uint32_t  PINTSEL1;        /*!< (@ 0x4004817C) GPIO Pin Interrupt Select register 0 */
  __IO uint32_t  PINTSEL2;        /*!< (@ 0x40048180) GPIO Pin Interrupt Select register 0 */
  __IO uint32_t  PINTSEL3;        /*!< (@ 0x40048184) GPIO Pin Interrupt Select register 0 */
  __IO uint32_t  PINTSEL4;        /*!< (@ 0x40048188) GPIO Pin Interrupt Select register 0 */
  __IO uint32_t  PINTSEL5;        /*!< (@ 0x4004818C) GPIO Pin Interrupt Select register 0 */
  __IO uint32_t  PINTSEL6;        /*!< (@ 0x40048190) GPIO Pin Interrupt Select register 0 */
  __IO uint32_t  PINTSEL7;        /*!< (@ 0x40048194) GPIO Pin Interrupt Select register 0 */
  __I  uint32_t  RESERVED12[27];
  __IO uint32_t  STARTERP0;       /*!< (@ 0x40048204) Start logic 0 pin wake-up enable register*/
  __I  uint32_t  RESERVED13[3];
  __IO uint32_t  STARTERP1;       /*!< (@ 0x40048214) Start logic 1 interrupt wake-up enable register*/
  __I  uint32_t  RESERVED14[6];
  __IO uint32_t  PDSLEEPCFG;      /*!< (@ 0x40048230) Power-down states in deep-sleep mode*/
  __IO uint32_t  PDAWAKECFG;   /*!< (@ 0x40048234) Power-down states for wake-up from deep-sleep*/
  __IO uint32_t  PDRUNCFG;        /*!< (@ 0x40048238) Power configuration register   */
  __I  uint32_t  RESERVED15[111];
  __I  uint32_t  DEVICE_ID;       /*!< (@ 0x400483F8) Device ID   */
} LPC_SYSCON_Type;

从上述结构体中可以看出,大部分语句都加上了“__IO”的前缀,这是由于这部分寄存器单元访问的特殊性决定的。“uint32_t”则反映定义的变量会占用4个字节的地址空间,因为在前面的宏定义中已经知道,uint32_t就是“unsigned int”型。同时要特别注意,在这个结构体中定义的各个变量的顺序不能改变,也就是说各个变量在结构体中的位置是固定的。这是因为在结构体内定义的各个变量之间存在着严格的地址偏移关系,这点从每一句后面的英文注解中也可以很清楚地看到。例如第一个变量定义的是“SYSMEMREMAP”,由于它被定义为“unsigned int”型的,所以占用4个字节的地址空间,而下一个定义的变量“PRESETCTRL”的地址,则是前面变量“SYSMEMREMAP”的地址再向后偏移4个字节。同理,第三个定义的变量“SYSPLLCTRL”的地址是第二个变量“PRESETCTRL”地址再向后偏移4个字节(因为第二个变量仍定义为“unsigned int”型),或者是第一个变量“SYSMEMREMAP”地址向后偏移8个字节。所以,如果不按照顺序来定义,则对应的地址将会出错。比如,如果把第二个变量“PRESETCTRL”删除,由于地址偏移量不变,则原来的第三个变量“SYSPLLCTRL”的地址将会被对应到原来第二个变量的地址(相对第一个变量偏移4字节而不是8字节),这将导致出错!因为在微控制器中各个寄存器之间的地址是固定不变的,大家如果对这一点难以理解没有关系,在后面详细讨论结构体的指针时就会弄明白。

刚才定义在结构体“SYSCON”中的各成员变量,是如何与LPC824内部寄存器进行一一映射的呢?为了方便讨论,先看一下LPC824内部的Memory Map(内存地图)是怎么分配的,如下图所示。

从内存地图中可以看到,由于LPC824是32位结构,所以其寻址空间达到了4GB(0x00000000~0xFFFFFFFF),且无论什么模块,都统一编址在其中,而不像51单片机那样程序存储和数据存储是各自独立编址的。通过观察内存地图会发现,地址分配是分类的,即:0x00000000~0x10012000的区域是内存区(包含了FlashROM和SRAM);0x14000000~0x14001000的区域是MTB寄存器区;0x1FFF0000~0x1FFF3000的区域是引导区(即BootROM区);0x40000000~0x40080000的区域是APB设备区,它包含了除IO端口以外的所有外围设备资源;0x50000000~0x50004000的区域是CRC校验区;0x50004000~0x50008000的区域是SCTimer/PWM区(即状态可配置定时器区);0x50008000~0x5000C000的区域是DMA区;0xA0000000~0xA0004000的区域是GPIO端口区,它包含了所有的IO端口资源;0xA0004000~0xA0008000的区域是GPIO PINT区(即引脚中断匹配区);0xE0000000~0xE0100000区域是私有外围设备总线区;其他剩余区域为保留区,便于未来升级扩展。
前面定义的结构体“SYSCON”所对映的设备,就是在内存地图中位于APB区内的system control模块部分(地址为0x40048000~0x4004C000)。为了便于讨论,下面把这部分的内容单独剔出来进行说明。
下表给出了system control模块内所有寄存器的分布情况。 

 

从表中可以看出,因为system control模块的起始地址是0x40048000,所以它的基址就是0x40048000。这样它内部各寄存器的地址就可以以基址作为参考点,用相对于基址的偏移量来进行描述。比如,在前面讨论时钟配置时用到的寄存器SYSPLLCLKSEL、MAINCLKSEL等,它们相对于基址的偏移地址就为0x040和0x070(查表中的Offset一列),而其绝对地址则是0x40048040和0x40048070(分别加上基址的值)。
我们知道,要访问CPU的内部模块,最终只能通过访问它们的地址来进行,而对这些模块的命名(如SYSPLLCLKSEL、MAINCLKSEL等)需要通过一种对应关系来把它们与其地址关联起来。因为CPU不知道SYSPLLCLKSEL、MAINCLKSEL是什么,但它知道0x40048040、0x40048070的地址单元。
在高级语言中,直接使用地址不仅不直观,开发者还要费力去记住每个地址的寄存器功能,很不切合实际。所以为了适应高级语言的特点,可以通过给寄存器命名,并采用把名称与寄存器实际物理地址对应起来的方式进行处理。经过这样的处理后,开发者就可以在程序中直接引用寄存器的名称,而不必去引用地址,大大提高了程序的可读性,方便了开发。
从内存地图中还可以看到,由于各设备的编址是分类的,所以使用高级语言中的“结构体”来处理这种名称与地址的对应关系是十分合适的。每一个结构体可对应一个分类,而分类中的寄存器则可以定义成这个结构体内的成员变量,各成员变量又严格对应到寄存器的实际物理地址。在以前的例子程序中,已经在开头部分引入了这种结构体并进行了地址映射。
下面再回到刚才定义的结构体“SYSCON”的讨论上来。从该结构体的定义中可以看到,它内部定义的成员变量其实就是system control模块内的所有寄存器(见上表)。但是还没完,因为定义了“SYSCON”这个结构体只相当于对system control模块进行了“封装”(即进行了寄存器的按顺序命名),还没有对它进行地址的映射。
下面给出预定义部分中第三部分的内容:

#define SCS_BASE            (0xE000E000UL)                            /*!< System Control Space Base Address */
#define LPC_SYSCON_BASE                 0x40048000UL
#define LPC_IOCON_BASE                  0x40044000UL
#define LPC_SWM_BASE                    0x4000C000UL
#define LPC_GPIO_PORT_BASE              0xA0000000UL
#define SysTick_BASE        (SCS_BASE +  0x0010UL)                    /*!< SysTick Base Address */
#define LPC_SYSCON                      ((LPC_SYSCON_Type         *) LPC_SYSCON_BASE)
#define LPC_IOCON                       ((LPC_IOCON_Type          *) LPC_IOCON_BASE)
#define LPC_SWM                         ((LPC_SWM_Type            *) LPC_SWM_BASE)
#define LPC_GPIO_PORT                   ((LPC_GPIO_PORT_Type      *) LPC_GPIO_PORT_BASE)
#define SysTick             ((SysTick_Type   *)     SysTick_BASE  )   /*!< SysTick configuration struct */ 

可见,这部分全部是用define进行的宏定义。因这里只讨论与“SYSCON”结构体相关的内容,所以只须看第二、七行即可,其他部分暂时不做说明。前面说过,system control模块位于APB设备区,其基址为0x40048000,所以第二行是进行了system control模块的基址定义。第七行则是把前面定义的SYSCON结构体与system control模块进行地址映射,现单独把它剔出来进行讨论。该语句如下:
#define LPC_SYSCON    ((LPC_SYSCON_Type *) LPC_SYSCON_BASE )
首先看,(LPC_SYSCON_Type *) LPC_SYSCON_BASE把LPC_SYSCON_BASE(即SYSCON的基址)强行转换为一个LPC_SYSCON_Type结构体的指针类型。根据前面的定义,LPC_SYSCON_BASE的值是0x40048000,而强行把它转换为一个LPC_SYSCON_Type结构体的指针类型,则这个结构体的首地址就是LPC_SYSCON_BASE的基址(0x40048000)。这样一来,结构体LPC_SYSCON_Type内部各成员变量的地址,就是以这个基址(0x40048000)为参考点的偏移地址了。
首地址对应了,那偏移量怎么实现呢?这就与结构体中成员变量定义的数据类型有关了。回顾一下前面的SYSCON这个结构体,其成员变量都是用“unsigned int”型来定义的,占用4个字节的空间,观察上表可以看出,它每个寄存器之间正好是4个字节(或是4的正数倍)的地址偏移,所以只要使用“unsigned int”型进行成员变量定义,寄存器的偏移地址就会自动适应。如果遇到保留地址,则可以通过定义“unsigned int”型的空数组来避开,以确保后续成员变量的地址偏移是正确的。另外,由于LPC824是32位结构,所以它的寄存器也是32位的,刚好4个字节,这也是为何每个寄存器之间是4个字节地址偏移量的原因。在上表中还可以看出寄存器的读写属性,这与前面结构体定义中的“__I”、“__IO”、“__O”等就可以联系起来了。
接下来再通过define语句来给刚才的结构体指针取个“别名”(即LPC_SYSCON),这样LPC_SYSCON就成为了这个结构体指针类型,通过“LPC_SYSCON->”这样的方式就可以引用它的内部成员变量(即system control模块内的各个寄存器)了。这样一来,就把芯片底层的地址用高级语言名称的形式来表示,非常直观,比如前面例子中,想让PLL输入选择外部晶体振荡,执行语句“ LPC_SYSCON->SYSPLLCLKSEL = 0x00000001;”就可以了,但如果没有这种地址映射,这句可能就要写成“MOVS 0x40048040,#0x00000001”这样的形式,这当然就很不直观了,不查手册还不知道地址0x40048040是什么寄存器。
上面只针对SYSCON这个模块的结构体进行了讨论,没有提及其他模块的结构体,但它们所采用的方法是一样的,读者可参考上面对SYSCON结构体的分析方法来研究其他模块结构体的定义,这里就不再赘述了。

posted @ 2020-04-29 15:24  fxzq  阅读(686)  评论(0编辑  收藏  举报