在前面的示例中,给出了预定义部分的内容,但没有进行解释。这里就先来讨论一下在第一个演示示例中预定义部分的内容。
先看第一个部分,代码如下:
#define __IO volatile
#define __O volatile
#define __I volatile const
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
#pragma anon_unions
__I:定义输入口。既然是输入,那么寄存器的值就随时会被外部修改,所以不能对它进行优化,每次都必须从寄存器中读取。也不能写(即只读),否则就不是输入而是输出了。
__O:定义输出口,也不能对它进行优化,不然端口连续两次输出相同的值,编译器就会认为没有变化,而忽略后那一次输出,假如外部在两次输出中间修改了值,那就会影响输出的正确性。可写,否则就不能称为输出了。
__IO:定义输入输出口,也不能对它进行优化,原因同上。可读可写。
第三至五行是类型的声明,把无符号的字符型、短整型、整型分别用uint8_t、uint16_t、uint32_t来表示,以突出它们所占用的字节数,方便查看。
在最后一行中,pragma是一个关键字,它的使用较为复杂,有兴趣的读者可自行上网查阅。这里只需要记住,在使用到带union的结构体定义时,在预定义部分一定要有“#pragma anon_unions”这样一句,否则编译通不过。在第一个演示示例中,由于在后面定义了一个带union的结构体,所以在这里必须要写这一句。
接下来看第二个部分,这部分全部使用结构体来对寄存器进行描述。先来看对SYSCON结构体的定义:
typedef struct
{
__IO uint32_t SYSMEMREMAP; /*!< Offset: 0x000 (R/W) System memory remap Register */
__IO uint32_t PRESETCTRL; /*!< Offset: 0x004 (R/W) Peripheral reset control Register */
__IO uint32_t SYSPLLCTRL; /*!< Offset: 0x008 (R/W) System PLL control Register */
__I uint32_t SYSPLLSTAT; /*!< Offset: 0x00C (R/ ) System PLL status Register */
uint32_t RESERVED0[4];
__IO uint32_t SYSOSCCTRL; /*!< Offset: 0x020 (R/W) System oscillator control Register */
__IO uint32_t WDTOSCCTRL; /*!< Offset: 0x024 (R/W) Watchdog oscillator control Register */
__IO uint32_t IRCCTRL; /*!< Offset: 0x028 (R/W) IRC control Register */
uint32_t RESERVED1[1];
__I uint32_t SYSRSTSTAT; /*!< Offset: 0x030 (R/ ) System reset status Register */
uint32_t RESERVED2[3];
__IO uint32_t SYSPLLCLKSEL; /*!< Offset: 0x040 (R/W) System PLL clock source select Register */
__IO uint32_t SYSPLLCLKUEN; /*!< Offset: 0x044 (R/W) System PLL clock source update enable Register */
uint32_t RESERVED3[10];
__IO uint32_t MAINCLKSEL; /*!< Offset: 0x070 (R/W) Main clock source select Register */
__IO uint32_t MAINCLKUEN; /*!< Offset: 0x074 (R/W) Main clock source update enable Register */
__IO uint32_t SYSAHBCLKDIV; /*!< Offset: 0x078 (R/W) System AHB clock divider Register */
uint32_t RESERVED4[1];
__IO uint32_t SYSAHBCLKCTRL; /*!< Offset: 0x080 (R/W) System AHB clock control Register */
uint32_t RESERVED5[4];
__IO uint32_t SSP0CLKDIV; /*!< Offset: 0x094 (R/W) SSP0 clock divider Register */
__IO uint32_t UARTCLKDIV; /*!< Offset: 0x098 (R/W) UART clock divider Register */
__IO uint32_t SSP1CLKDIV; /*!< Offset: 0x09C (R/W) SSP1 clock divider Register */
uint32_t RESERVED6[1];
uint32_t RESERVED7[11];
__IO uint32_t WDTCLKSEL; /*!< Offset: 0x0D0 (R/W) WDT clock source select Register */
__IO uint32_t WDTCLKUEN; /*!< Offset: 0x0D4 (R/W) WDT clock source update enable Register */
__IO uint32_t WDTCLKDIV; /*!< Offset: 0x0D8 (R/W) WDT clock divider Register */
uint32_t RESERVED9[1];
__IO uint32_t CLKOUTCLKSEL; /*!< Offset: 0x0E0 (R/W) CLKOUT clock source select Register */
__IO uint32_t CLKOUTUEN; /*!< Offset: 0x0E4 (R/W) CLKOUT clock source update enable Register */
__IO uint32_t CLKOUTDIV; /*!< Offset: 0x0E8 (R/W) CLKOUT clock divider Register */
uint32_t RESERVED10[5];
__I uint32_t PIOPORCAP0; /*!< Offset: 0x100 (R/ ) POR captured PIO status 0 Register */
__I uint32_t PIOPORCAP1; /*!< Offset: 0x104 (R/ ) POR captured PIO status 1 Register */
uint32_t RESERVED11[11];
uint32_t RESERVED12[7];
__IO uint32_t BODCTRL; /*!< Offset: 0x150 (R/W) BOD control Register */
__IO uint32_t SYSTCKCAL; /*!< Offset: 0x154 (R/W) System tick counter calibration Register */
uint32_t RESERVED13[1];
uint32_t RESERVED14[5];
uint32_t RESERVED15[2];
uint32_t RESERVED16[34];
__IO uint32_t STARTAPRP0; /*!< Offset: 0x200 (R/W) Start logic edge control Register 0 */
__IO uint32_t STARTERP0; /*!< Offset: 0x204 (R/W) Start logic signal enable Register 0 */
__O uint32_t STARTRSRP0CLR; /*!< Offset: 0x208 ( /W) Start logic reset Register 0 */
__I uint32_t STARTSRP0; /*!< Offset: 0x20C (R/ ) Start logic status Register 0 */
__IO uint32_t STARTAPRP1; /*!< Offset: 0x210 (R/W) Start logic edge control Register 1 (LPC11UXX only) */
__IO uint32_t STARTERP1; /*!< Offset: 0x214 (R/W) Start logic signal enable Register 1 (LPC11UXX only) */
__O uint32_t STARTRSRP1CLR; /*!< Offset: 0x218 ( /W) Start logic reset Register 1 (LPC11UXX only) */
__I uint32_t STARTSRP1; /*!< Offset: 0x21C (R/ ) Start logic status Register 1 (LPC11UXX only) */
uint32_t RESERVED17[4];
__IO uint32_t PDSLEEPCFG; /*!< Offset: 0x230 (R/W) Power-down states in Deep-sleep mode Register */
__IO uint32_t PDAWAKECFG; /*!< Offset: 0x234 (R/W) Power-down states after wake-up from Deep-sleep mode Register*/
__IO uint32_t PDRUNCFG; /*!< Offset: 0x238 (R/W) Power-down configuration Register*/
uint32_t RESERVED18[110];
__I uint32_t DEVICE_ID; /*!< Offset: 0x3F4 (R/ ) Device ID Register */
} LPC_SYSCON_TypeDef;
从中可以看出,大部分语句都加上了“__IO”的前缀,这是由于这部分寄存器单元访问的特殊性决定的。“uint32_t”则反映了定义的变量会占用4个字节的地址空间,因为在前面的宏定义中已经知道,uint32_t就是“unsigned int”型。同时要特别注意一点,在这个结构体中定义的各个变量的顺序不能改变,也就是说各个变量在结构体中的位置是固定的。这是因为在结构体内定义的各个变量之间存在着严格的地址偏移量关系,这点从每一句后面的注解中也可以很清楚地看到。例如第一个变量定义的是“SYSMEMREMAP”,由于它被定义为“unsigned int”型的,所以占用4个字节的地址空间;而下一个定义的变量“PRESETCTRL”的地址,则是前面的变量“SYSMEMREMAP”地址再向后偏移4个字节。同理,第三个定义的变量“SYSPLLCTRL”的地址是第二个变量“SYSMEMREMAP”地址再向后偏移4个字节(因为第二个变量仍定义为“unsigned int”型),或者是第一个变量“SYSMEMREMAP”地址向后偏移8个字节。所以,如果不按照顺序来定义,其对应的地址将会出错。比如,如果把第二个变量“SYSMEMREMAP”删除,由于地址偏移量不变,则原来的第三个变量“SYSPLLCTRL”的地址将会被对应到原来第二个变量的地址(相对第一个变量偏移4字节而不是8字节),这将导致出错!这是因为在CPU中各个寄存器之间的地址是固定不变的,这一点目前可能会有些难理解,在后面讨论了结构体的指针以后就会明白的。
下面先来看一下,刚才定义在结构体“SYSCON”中的各成员变量,是如何与LPC1114内部的寄存器进行一一对映的。为了方便讨论,先看一下LPC1114内部的Memory Map(内存地图)是怎么分配的,如下图所示。
从上表中可以看出,因为system control模块的起始地址是0x40048000,所以它的基址就是0x40048000。这样它内部各寄存器的地址就可以以基址为参考点,用相对于基址的偏移量来进行描述。比如,在前面讨论时钟配置时用到的寄存器SYSPLLCLKSEL、MAINCLKSEL等,它们相对于基址的偏移地址就是0x040和0x070(查上表中的Address offset一列),而其绝对地址则是0x40048040和0x40048070(分别加上基址的值)。
我们知道,要访问CPU内部的硬件,最终只能通过它的地址进行访问,而我们对其进行的命名(如刚才的SYSPLLCLKSEL、MAINCLKSEL等)都要通过一种对映关系把它们联系起来。因为CPU不知道SYSPLLCLKSEL、MAINCLKSEL是什么,但它知道地址0x40048040、0x40048070的单元。而在高级语言中,直接使用地址不仅不直观,开发者还要费力去记住每个地址的寄存器功能,很不合适。为了适应高级语言的特点,我们就通过这种给寄存器命名,并让该名称对映到寄存器的实际地址的方式来处理。经过这样处理后,开发者就可以在程序中直接引用寄存器名称了,大大提高了程序的可读性,方便了开发。
从内存地图中可以看出,由于各设备的编址是分类的,所以使用高级语言中的“结构体”来处理这种名称与地址的对映关系是十分合适的。每一个结构体可对应一个分类,而分类中的寄存器就可以定义为这个结构体内的成员变量,各成员变量又严格对映到寄存器的实际地址。在前面示例部分的程序中,已经在头文件部分引入了这种结构体并进行了地址对映。
接下来再回到刚才定义的结构体“SYSCON”的讨论上来。从该结构体定义中可以看到,它内部定义的成员变量其实就是system control模块内的所有寄存器(见上表)。但是还没完,因为定义了“SYSCON”这个结构体只相当于对system control模块进行了“封装”(即进行了寄存器的按顺序命名),还没有对它进行地址的对映。
下面给出预定义部分中第三部分的内容,代码如下:
#define LPC_APB0_BASE (0x40000000UL)
#define LPC_AHB_BASE (0x50000000UL)
#define LPC_IOCON_BASE (LPC_APB0_BASE + 0x44000)
#define LPC_SYSCON_BASE (LPC_APB0_BASE + 0x48000)
#define SCS_BASE (0xE000E000UL)
#define LPC_SYSCON ((LPC_SYSCON_TypeDef *) LPC_SYSCON_BASE)
#define LPC_IOCON ((LPC_IOCON_TypeDef *) LPC_IOCON_BASE )
#define LPC_GPIO0_BASE (LPC_AHB_BASE + 0x00000)
#define LPC_GPIO1_BASE (LPC_AHB_BASE + 0x10000)
#define LPC_GPIO2_BASE (LPC_AHB_BASE + 0x20000)
#define LPC_GPIO3_BASE (LPC_AHB_BASE + 0x30000)
#define SysTick_BASE (SCS_BASE + 0x0010UL)
#define LPC_GPIO0 ((LPC_GPIO_TypeDef *) LPC_GPIO0_BASE )
#define LPC_GPIO1 ((LPC_GPIO_TypeDef *) LPC_GPIO1_BASE )
#define LPC_GPIO2 ((LPC_GPIO_TypeDef *) LPC_GPIO2_BASE )
#define LPC_GPIO3 ((LPC_GPIO_TypeDef *) LPC_GPIO3_BASE )
#define SysTick ((SysTick_Type *) SysTick_BASE )
可见,这部分又全是用define进行的宏定义。因这里只讨论与“SYSCON”结构体相关的内容,所以只需看第一、四、六行,其它部分暂时不作说明。前面说过,system control模块位于APB设备区,所以第一行先进行了APB设备区的基址定义,第四行又进行了SYSCON(即system control模块区)的基址定义。而真正进行结构体与地址对映的,是第六行的语句,现单独把它剔出来进行讨论。该语句如下:
#define LPC_SYSCON ((LPC_SYSCON_TypeDef *) LPC_SYSCON_BASE)
首先来看,(LPC_SYSCON_TypeDef *) LPC_SYSCON_BASE是把LPC_SYSCON_BASE(即SYSCON的基址)强行转换为一个LPC_SYSCON_TypeDef结构体的指针类型。根据前面的定义,LPC_SYSCON_BASE的值是0x40048000(LPC_APB0_BASE + 0x48000),而强行把它转换为一个LPC_SYSCON_TypeDef结构体的指针类型,则这个结构体的首地址就是LPC_SYSCON_BASE的基址(0x40048000)。这样一来,结构体LPC_SYSCON_TypeDef内部各成员变量的地址,就是以这个基址(0x40048000)为参考点的偏移地址了。
首地址对映了,那偏移量怎么实现呢?这就与结构体中成员变量定义的数据类型有关了。回顾一下上面的SYSCON这个结构体中,成员变量都用的是“unsigned int”型来定义,占用4个字节的空间,而观察上面的“system control模块内所有寄存器的分布情况表”可以看出,它的每个寄存器之间正好是4个字节(或是4的正数倍)的地址偏移,所以只要用“unsigned int”型来定义成员变量,寄存器的偏移地址就自动适应了。如果遇到保留地址,则可以通过定义“unsigned int”型的空数组来避开,以保证后面成员变量的地址偏移正确。另外,由于LPC1114是32的结构,所以它的寄存器也是32位的,刚好是4个字节,这也是为何每个寄存器之间是4个字节地址偏移量的原因。在表中还可以看出寄存器的读写属性,这与前面结构体定义中的“__I”、“__IO”、“__O”等就可以联系起来了。
接下来,通过define语句来把刚才的结构体指针取个“别名”,即LPC_SYSCON。这时LPC_SYSCON就是这个结构体指针类型了,通过“LPC_SYSCON->”的方式就可以来引用它内部的成员变量(即system control模块内的各个寄存器)了。这样一来,就可以把底层的地址用高级语言的名称来表示,非常直观,比如前面例子中的 要让PLL输入选择外部晶体振荡,执行语句“ LPC_SYSCON->SYSPLLCLKSEL = 0x00000001;”就可以了,但如果没有这种地址对映,就必须写成“MOV 0x40048040,#0x00000001”,这当然就非常不直观了,不查手册还不知道地址0x40048040是什么寄存器。
上述只是通过SYSCON这个结构体来进行讨论的,其它的结构体定义没有讨论。但它们所采用的方法是一样的,读者可参考上面对SYSCON结构体的分析方法来自行研究,这里就不再赘述了。
上一篇:LPC1114时钟配置
下一篇:STM32 ADC电压值的计算
推荐阅读最新更新时间:2024-03-16 15:06