在第一节中已经介绍过,LPC1114处理器是一个32位结构的处理器,但它的GPIO端口没有把32根引脚都接出来,而是每组只接出来12根引脚(注意,第四组只接出来6根引脚),共有4组,一共42根引脚。它们都具有如下特点:
1.可通过软件配置GPIO引脚为输入或输出
2.每个独立的端口引脚均可作为外部中断的输入引脚(边沿或电平触发)
3.边沿触发中断可配置为上升沿触发、下降沿触发以及双边沿触发
4.电平触发中断引脚可以配置为高电平或低电平触发
5.所有GPIO引脚默认情况下均为输入
6.从端口读取和写入数据操作可以通过地址位13:2屏蔽
端口的具体使用配置会在后面一一进行讨论,这里先来看一下“通用输入/输出端口GPIO”的结构体是如何定义的,代码如下。
typedef struct
{
union {
__IO uint32_t MASKED_ACCESS[4096]; /*!< Offset: 0x0000 to 0x3FFC Port data Register for pins PIOn_0 to PIOn_11 (R/W) */
struct {
uint32_t RESERVED0[4095];
__IO uint32_t DATA; /*!< Offset: 0x3FFC Port data Register (R/W) */
};
};
uint32_t RESERVED1[4096];
__IO uint32_t DIR; /*!< Offset: 0x8000 Data direction Register (R/W) */
__IO uint32_t IS; /*!< Offset: 0x8004 Interrupt sense Register (R/W) */
__IO uint32_t IBE; /*!< Offset: 0x8008 Interrupt both edges Register (R/W) */
__IO uint32_t IEV; /*!< Offset: 0x800C Interrupt event Register (R/W) */
__IO uint32_t IE; /*!< Offset: 0x8010 Interrupt mask Register (R/W) */
__I uint32_t RIS; /*!< Offset: 0x8014 Raw interrupt status Register (R/ ) */
__I uint32_t MIS; /*!< Offset: 0x8018 Masked interrupt status Register (R/ ) */
__O uint32_t IC; /*!< Offset: 0x801C Interrupt clear Register (R/W) */
} LPC_GPIO_TypeDef;
上述代码对LPC1114的GPIO端口的进行了结构体定义,由于GPIO位于内存地图中的AHB部分,所以从代码中可以看出,同前面讨论的结构体一样,它定义的成员变量利用偏移地址与AHB中的GPIO寄存器进行了对映。特殊的地方在于多了一个关于union的定义,要弄清这个定义,还必须回到AHB模块部分的寄存器描述中去。下图就给出了AHB模块内GPIO部分的寄存器分布情况。
GPIODATA寄存器是GPIO的数据寄存器,它存放的数据直接被输出到GPIO的引脚上,引脚输入的数据也会被放到该寄存器中。所以要对端口进行电平控制(无论是输入还是输出),就要对GPIODATA寄存器进行操作。同样,GPIODATA寄存器也是一个32位的结构,其地址占用4个字节。而GPIO端口只有12个引脚,因此GPIODATA寄存器只用了低12位来对映GPIO端口引脚。
从上图中还可以看出,GPIODATA寄存器的偏移地址是从0x0000~0x3FFC。最高地址是0x3FFC是因为,每个GPIODATA寄存器单元占用4 字节,所以共有0x3FFC/4=0xFFF个(即4095个)GPIODATA寄存器单元,因为地址是从第0个单元算起的,所以总共有4096个单元。而2的12次方刚好就等4096,所以它刚好可以表征12个引脚的容量。由此可以看出,每个引脚电平的值(无论是输入还是输出)都可以在GPIODATA寄存器中找到一个对映的单元(因为有4096个引脚状态就有4096个GPIODATA地址单元)。这样做是为什么呢?为什么不把端口引脚统一用一个端口寄存器来描述(姑且称为“方法一”),写(或读)这个端口寄存器不就行了?其实,其它大部分单片机也确实是这样做的。而这里LPC1114却要花费4096个地址单元来把引脚状态全部描述一遍(姑且称为“方法二”),也肯定是有它的理由的。其实,应该说LPC1114是两种方法都包含的(因为严格来说,“方法一”包含于“方法二”)。至于“方法二”的优点,其实就是可以在不改变其它引脚状态下,单独改变某一引脚上的电平。按理说,通过“与或”的逻辑操作方式,也可以让“方法一”实现这一功能(其它单片机也是这样做的),但它必须通过“读——修改——写”的步骤来实现。比如要实现仅对P0端口的第7个引脚输出高电平,程序可写为“P0 |= 1<<7”,其实就是“P0 =P0| 0b10000000”,这就可以看出,它先要把P0的值读出来,然后进行修改(和0b10000000相与),最后再把结果写回P0去。而“方法二”就简单多了,直接访问地址为0b10000000的单元就行了。下面详细来讨论方法二是如何实现位操作的。
为了实现“方法二”的位操作,在LPC1114中引入了一个新的概念——屏蔽(MASK)。它利用一个14位的结构来描述屏蔽操作,只有在屏蔽结构中值为1的位所对应的端口引脚值才会生效。例如要改变端口第7个引脚的电平,那么在这个端口所对应的屏蔽结构中,与第7个引脚对应的屏蔽位的值必须为1才行。同时要注意,屏蔽位并不是与端口位置一一对应的,而是屏蔽结构整体“左移”了两位再来对应!由于端口引脚数量为12,屏蔽结构又整体“左移”两位,所以它才需要用一个14位的结构来描述。据此,那么刚才第7个引脚所对应的屏蔽位应该是第9位。要改变第7个引脚的电平,屏蔽结构中的第9位的值要是1才行。此过程可用下图来描述,至于为何屏蔽结构要“左移”两位,后面会进行详细讨论。
从上图还可以看出,GPIODATA的地址是GPIO基址+屏蔽地址(偏移地址)。在LPC1114中共有4组GPIO端口,它们的基址分别是:port0为0x50000000;port1为0x50010000;port2为0x50020000;port3为0x50030000。而每组端口都有自己的屏蔽地址,4组GPIO基址加上各自的屏蔽地址后就是:port0为0x50000000~0x50003FFC;port1为0x50010000~0x50013FFC;port2为0x50020000~0x50023FFC;port3为0x50030000~0x50033FFC。每组都有4096个32位的单元。
接下来讨论为何屏蔽结构要“左移”两位再来对应。关于这一点,即便在管方文档中也没有给出解释。但通过观察可以看出,虽然屏蔽结构只用了14位来描述,但由于处理器本身是32位结构的,所以其实每个屏蔽结构本身的长度还是32位的,只不过它只用了低14位就足够了。换句话说,一个屏蔽结构要占用4个字节的地址。而全部(4096个)屏蔽结构就要占用4×4096个地址,因为地址是从0算起的,所以整个屏蔽结构所占用的地址是4×4096-1=16383个,也即十六进制的0x3FFC个。对应上面的“GPIO寄存器分布情况表”会发现,这个值正好是GPIODATA寄存器地址偏移的最大值。那这两个之间会有什么关系呢?其实只能证明一点,在引入了屏蔽结构的概念后,屏蔽结构就是GPIODATA寄存器!更确切的说是地址为0x000~0x3FF8这段的GPIODATA寄存器(因为最后一个地址为0x3FFC的屏蔽结构是全部不屏蔽(值全为1),可以认为它无屏蔽作用,所以一般把它当作端口寄存器用(即“方法一”))。
这时再回到“GPIO寄存器分布情况表”上来看,第一行的GPIODATA的地址为0x000~0x3FF8,它是实际的4095个屏蔽结构,每个占用4个字节地址,使用时对应“方法二”;第二行的GPIODATA的地址为0x3FFC,它相当于端口寄存器,共占用4个字节地址,使用时对应“方法一”。但两者只能选其一,不能两种方法都同时用!
由于C语言中的共用体(或称联合体)就具有地址空间复用的特点,所以利用共用体来定义GPIODATA是最为合适的。下面单独把这部分定义剔出来讨论,代码如下。
union {
__IO uint32_t MASKED_ACCESS[4096]; /*!< Offset: 0x0000 to 0x3FFC Port data Register for pins PIOn_0 to PIOn_11 (R/W) */
struct {
uint32_t RESERVED0[4095];
__IO uint32_t DATA; /*!< Offset: 0x3FFC Port data Register (R/W) */
};
};
可以看出,在共用体中定义了两个部分的复用内容。第一个部分是一个“uint32_t”型的MASKED_ACCESS(屏蔽)数组,一共定义了4096个元素空间。每个数组元素占用4个字节地址,4096个MASKED_ACCESS数组共占用0x3FFC的地址空间,每个单元都具有可读可写的属性(__IO)。从地址分配可以看出,这个数组包括了从屏蔽所有位(地址0x0000)到不屏蔽任何位(地址0x3FFC)的所有屏蔽结构部分。MASKED_ACCESS[0]对应地址0x0000,屏蔽所有位;MASKED_ACCESS[1]对应地址是0x0004(每个占用4字节),二进制数是0b00000000000100(14位,左移两位来对映),即不屏蔽端口第0位引脚;MASKED_ACCESS[2]对应地址是0x0008,二进制数是0b00000000001000(14位,左移两位来对映),即不屏蔽端口第1位引脚;MASKED_ACCESS[3]对应地址是0x000C,二进制数是0b00000000001100(14位,左移两位来对映),即不屏蔽端口第0位和第1位引脚;MASKED_ACCESS[4]对应地址是0x0010,二进制数是0b00000000010000(14位,左移两位来对映),即不屏蔽端口第2位引脚;如此等等;最后一个数组元素是MASKED_ACCESS[4095],对应地址是0x3FFC,二进制数是0b11111111111100(14位,左移两位来对映),即不屏蔽任何端口引脚。
到此,就应该可以来回答刚才的问题“为何屏蔽结构要用12位左移2位来表示了”。这是由于,每个MASKED_ACCESS数组元素之间差了4个字节,为了让每个屏蔽结构都可以对应到各自对映的数组元素,必须对每个屏蔽结构乘以4。然而在位运算中,左移2位就相当于乘以4,所以用左移了2位的14位屏蔽结构,相当于给每个屏蔽结构都乘以4,这就免去了对4096个屏蔽结构都乘以4的操作指令!这是屏蔽结构要左移两位真正原因所在!
共用体中的第二个部分是一个“uint32_t”型的变量DATA。由于其前面定义了4095个“uint32_t”型空数组,避开了屏蔽结构中的前4095个单元。所以最后的变量DATA的起始地址就是0x3FFC,也就是最后一个屏蔽结构的地址。前面说过,最后一个屏蔽结构的值是全1,即不屏蔽。而这里用变量DATA来代替最后一个屏蔽结构,做法非常巧妙。相当于对DATA写什么值,在端口的引脚上就可以得到相应的电平。此时可认为它就是端口寄存器,而不是屏蔽结构。DATA也必须具有可读可写的属性(__IO)。
下面用一个例子来说明一下整个过程。例如,要让第0组GPIO的第0、3、10位输出1,其它位保持不变,需要如何操作。
首先看,要这三位输出1,先要给这三位对应的屏蔽位写1,则它们对应的14位屏蔽结构应该是“0b01000000100100”,换算成十六进制是“0x1024”。这个“0x1024”就是在0x0000~0x3FFC地址之间的一个单元,也即通过共用体对映到了4096个MASKED_ACCESS数组元素中的其中一个。但它到底是哪个MASKED_ACCESS数组元素呢?由于它们之间是4倍的关系,所以0x1024/4=0x409,十进制为1033,即MASKED_ACCESS[1033]单元。而写给端口的数据则是不左移的12位,即“0b010000001001”,换算成十六进制刚好就是“0x409”,十进制为1033,即该数组元素的编号。因此,通过执行MASKED_ACCESS[1033]=0x409,就可以实现对第0、3、10位输出1。而实际上,执行MASKED_ACCESS[1033]=0xFFF效果也是一样的,因为除了第0、3、10位为1以外,其它位可为任何值。同理,要对第0、3、10位输出0,执行MASKED_ACCESS[1033]=0x000和执行MASKED_ACCESS[1033]=0xBF6是一样的效果。
所以综上所述,MASKED_ACCESS要引用的数组单元,就是要输出到引脚上12位值的十进制数。当然,要实际引用,还要在程序预定义部分进行地址对映,代码如下。
#define LPC_AHB_BASE (0x50000000UL)
#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 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 )
有了上述预定义,刚才的例子就可以执行LPC_GPIO0->MASKED_ACCESS[1033]=0x409来实现了。
再来回顾一下前面第一个演示示例中对端口2的操作,主要代码如下。
while(1)
{
LPC_GPIO2->DATA = 0xAAA;
delay_ms(500);
LPC_GPIO2->DATA = 0x555;
delay_ms(500);
}
从中可以看出,它是通过“方法一”,即直接写DATA变量来实现的。因为它的全部12位都在变化,所以采用了这种方式。
剩于共用体定义中的其它部分,由于与SYSCON分析中的一样,可自行参考前面的内容来分析,这里就不再赘述了。最后不要忘记一点,由于在程序中引入了共用体union,所以在预定义部分要加下一句“#pragma anon_unions”,这在前面章节已经阐述过了。如果用包含头文件的方式的话,该定义存在于头文件LPC11xx.h中。
上一篇:LPC1114通用输入/输出端口(GPIO)续
下一篇:时钟配置的仿真
推荐阅读最新更新时间:2024-03-16 15:06