GPIO端口应用
通过前面的分析,我们知道在LPC824中,对GPIO端口的操作一共涉及到68个寄存器,那究竟该使用哪些寄存器,特别是对于具有相同功能的寄存器,应该如何选择,下面就来进行具体讨论。
虽然一共有68个寄存器,但其中的端口字节引脚寄存器B和端口字引脚寄存器W,由于每个对应一根引脚,所以就分别占用了29个寄存器,一共占用了58个,因此可以把B寄存器和W寄存器算成两个(两类),这样的话一共就只有12个寄存器了。在LPC824中,要实现对GPIO的同一个控制,操作的寄存器可有多种选择。例如:要控制某个引脚输出高电平,可以选择的寄存器就有PIN0、W、B、SET0及MPIN0等四个。那究竟应该如何选择,在LPC824的官方手册中给出了如下建议:
(1)对于复位或重新初始化后的初始设置,写PORT寄存器(即PIN0寄存器)。
(2)如需更改某个引脚的状态,写字节引脚或字引脚寄存器。
(3)如需一次性更改多个引脚的状态,写SET和(或)CLR寄存器。
(4)如需在严格控制的环境(如软件状态机)中更改多个引脚的状态,可考虑使用NOT寄存器,这比SET和CLR需要的写操作更少。
(5)如需读取某个引脚的状态,读字节引脚或字引脚寄存器。
(6)如需根据多个引脚作出决定,读取并屏蔽PORT寄存器(即MPIN0寄存器)。
一般来说,当要控制的GPIO引脚是“一次性”操作的话,就操作PIN0寄存器,因为它会一次性把29位电平全部输出到对应的端口引脚上,或一次性把29根端口引脚上的电平全部读入到寄存器中,效率较高,比较适合芯片初始化时的引脚电平赋值、全端口电平取反等操作。而对于只控制单一的某一根引脚状态(读或写)时,可操作W或B寄存器,因为它只更改端口的其中一位的值而不会影响到其他位,不必像传统使用“与或”操作的方式那样麻烦。对于一次要控制多根引脚状态时,可根据情况来操作SET0和CLR0寄存器,其中SET0寄存器为指定的引脚输出高电平,而其他引脚的电平保持不变,CLR0则相反,为指定的引脚输出低电平,其他引脚的电平保持不变。但使用SET0和CLR0寄存器时要特别注意,它们中值为0的位所对应的引脚上的电平是保持原有不变,而不是输出低电平。当要对某些引脚进行电平取反时,可操作NOT0寄存器,值为1的位所对应的引脚电平取反,为0的保持原有电平不变。前面的操作均不受MASK0寄存器的影响,当要对端口的某些引脚确定其操作状态时,即只能针对某些引脚进行控制时,可操作MPIN0寄存器,而对其中能操作的引脚则通过MASK0寄存器来进行设置,MASK0中值为1的位所对应的引脚将被“固定”住其原来的状态不变,值为0的位所对应的引脚才能通过写MPIN0寄存器来更改。
同样的道理,对于方向寄存器DIR0、DIRSET0、DIRCLR0及DIRNOT0的使用也可参考上述原则进行。当要“一次性”确定所有引脚的方向时,就操作DIR0寄存器,原理就不再赘述了。而对于要一次更改多个(或单个)引脚的方向时,可根据情况来操作DIRSET0和DIRCLR0寄存器,其中DIRSET0寄存器为指定的引脚更改为输出方向,而其他引脚的方向保持不变,DIRCLR0则相反,为指定的引脚更改为输入方向,其他引脚的方向保持不变。同样,在使用DIRSET0和DIRCLR0寄存器时要注意,它们中值为0的位所对应的引脚方向是保持原来的不变,而不是变为输入。当只针对某些引脚的方向进行取反时,可操作DIRNOT0寄存器,值为1的位所对应的引脚方向取反,为0的保持原有方向不变。
通过上述分析,就可明确这12个端口寄存器的具体用法了。下面再通过修改前面第一个演示示例的例子,来看一下这12个端口寄存器可以怎样操作。为了简化,这里只罗列出需要修改的部分,其余部分照旧。下面给出的是原来的端口初始化函数。
void Port_init(void)
{
LPC_GPIO_PORT->DIR0 = 0x1FFFFFFF; //设置端口为输出方向
LPC_GPIO_PORT->PIN0 = 0x10090080; //输出相应电平交替点亮LED
}
由于是端口初始化,即“一次性”操作,所以这里访问的是DIR0和PIN0两个寄存器,用来设置引脚的输出方向和电平高低。下面是定时器中断服务函数,用来把引脚的输出电平取反。
void SysTick_Handler(void)
{
LPC_GPIO_PORT->PIN0 = ~LPC_GPIO_PORT->PIN0; //取反赋值
}
在原来的程序中,访问的依然是PIN0寄存器,因为这里的取反也属于对端口的“一次性”操作。然而,该语句也可以更改为下面的样子。
void SysTick_Handler(void)
{
LPC_GPIO_PORT->NOT0 = 0x1FFFFFFF; //端口引脚电平取反
}
由于在NOT0寄存器中,值为1的位对应的引脚电平取反,所以把PIO0~PIO28的共29个位全部设置为1,也可以实现LPC_GPIO_PORT->PIN0 = ~LPC_GPIO_PORT->PIN0的取反效果。
除此以外,还可以通过访问MPIN0和MASK0两个寄存器来实现同样的效果。两个函数更改如下。
void Port_init(void)
{
LPC_GPIO_PORT->DIR0 = 0x1FFFFFFF; //设置端口为输出方向
LPC_GPIO_PORT->MPIN0 = 0x10090080; //输出相应电平交替点亮LED
// LPC_GPIO_PORT->MASK0 = 0x1FFFFFFF; //屏蔽输出引脚
}
void SysTick_Handler(void)
{
LPC_GPIO_PORT->MPIN0 = ~LPC_GPIO_PORT->MPIN0; //取反赋值
}
按上述更改重新编译程序,会发现当注释掉对MASK0操作这一句的话,运行效果是与前面一致的。而当把注释掉的语句恢复后,则在运行时LED就不再闪烁了。原因是MASK0中值为1的位对应的引脚被屏蔽了,不会因MPIN0的赋值而改变。还可再进一步验证,把MASK0的赋值改成和MPIN0的值一样,即LPC_GPIO_PORT->MASK0 = 0x10090080,然后编译运行看看效果。可看到,LED变成了隔空闪烁而不是交替闪烁,这是为什么呢?仔细来分析一下,为了方便我们不防假设只有4个LED,而且是紧挨着的,依然是低电平点亮(即值为0时亮)。初始状态假设为“1010”,即MPIN0和MASK0的值都为“1010”。MASK0中值为1的位保持原来状态不变,即LED状态应为“灭X灭X”,其中X的状态由MPIN0的值确定。初始时X对应的值都为0(即低电平),所以初始状态的LED为“灭亮灭亮”。当MPIN0取反后值变为“0101”,X的值都由0变成了1(即高电平),所以X对应的LED状态为灭,因此加上MASK0中的屏蔽位后,LED的状态为“灭灭灭灭”,即全灭。而当MPIN0再次取反后,X的值又变为了0,所以加上MASK0中的屏蔽位后,LED的状态又变为了“灭亮灭亮”。可见,最终LED的效果就是隔空闪烁。
同理还可再做其他尝试,比如保持上述的端口初始化不变,而把中断服务函数改成下面的样子。
void SysTick_Handler(void)
{
LPC_GPIO_PORT->PIN0 = ~LPC_GPIO_PORT->PIN0; //取反赋值
// LPC_GPIO_PORT->NOT0 = 0x1FFFFFFF; //端口引脚电平取反
}
编译后运行程序,会发现无论是操作PIN0取反,还是操作NOT0取反,都不影响LED的交替闪烁,这就证明了PIN0和NOT0两个寄存器确实不受MASK0寄存器的影响。
同样,为了验证使用SET0和CLR0两个寄存器的效果,还可以把中断服务函数改成下面的样子。
void SysTick_Handler(void)
{
uint32_t temp;
temp = LPC_GPIO_PORT->PIN0;
LPC_GPIO_PORT->SET0 = ~temp;
LPC_GPIO_PORT->CLR0 = temp;
}
可以看到,使用SET0和CLR0两个寄存器来实现同样的效果,就稍为麻烦一些。为了便于理解,我们仍然使用前面假设的4个LED。假设初始状态仍为“1010”,则LED状为“灭亮灭亮”,取反后的值为“0101”,赋值给SET0后,值为0的位对应的引脚电平保持不变,而值为1的位对应的引脚输出高电平,所以LED的状态变为了“灭灭灭灭”,即全灭。而给CLR0的赋值仍为“1010”,值为0的位对应的引脚电平保持不变,而值为1的位对应的引脚输出低电平,所以LED的状态又变为了“亮灭亮灭”,这样就实现了LED的交替闪烁。同时,为了便于操作端口,还定义了一个局部变量来存取上一次的端口值。需要强调的是,这里仅仅是为了学习而演示,此种方法在实际工程中是不建议采用的,不是因为复杂,而是因为它不可靠,细究会发现在端口上有一瞬间是输出了全1的(即LED全灭),虽然时间非常非常短,但仍然存在控制上的隐患,所以仅适用于学习。
在上述验证中,还可以把temp = LPC_GPIO_PORT->PIN0改成temp = LPC_GPIO_PORT->MPIN0,并把端口初始化中的MASK0进行启用或禁用,逐步来验证效果,以提高对端口屏蔽的认识,具体操作在此就不赘述了。
下面来看一个键控灯的小例子,电路如下图所示。
要求通过按键KEY来控制LED的亮灭,即按下按键LED亮,松开按键LED灭。程序代码如下。
#include <LPC82x.h>
//************************端口初始化***********************************
void Port_init(void)
{
LPC_GPIO_PORT->DIRSET0 = 0x80; //设置引脚PIO0_7为输出方向
LPC_GPIO_PORT->DIRCLR0 = 0x01; //设置引脚PIO0_1为输入方向
LPC_GPIO_PORT->SET0 = 0x80; //设置引脚PIO0_7输出高电平
}
//***************************主函数************************************
int main(void)
{
Port_init(); //调用端口初始化
while(1)
{
if(LPC_GPIO_PORT->B1)
LPC_GPIO_PORT->B7 = 1; //按键释放,LED灭
else
LPC_GPIO_PORT->B7 = 0; //按键按下,LED亮
}
}
在上述程序中,端口初始化函数并没有直接操作PIN0寄存器,而是分别操作了DIRSET0、DIRCLR0和SET0寄存器。 根据前面的电路图可知,按键和LED一共只接了两根引脚,所以只需要对这两根引脚进行操作就可以了,其他的引脚不要去改变它。DIRSET0寄存器中值为1的位对应的引脚被设置为输出方向,而值为0的位对应的引脚不变。所以执行了LPC_GPIO_PORT->DIRSET0=0x80这一句后,只有引脚PIO0_7被设置为输出方向,并不会去改变其他引脚的方向。同理,DIRCLR0寄存器中值为1的位对应的引脚被设置为输入方向,而值为0的位对应的引脚不变。所以执行了LPC_GPIO_PORT->DIRCLR0=0x01这一句后,只有引脚PIO0_1被设置为输入方向,也不会去改变其他引脚的方向。对应电路原理图,引脚PIO0_7接的是LED,为输出方向,引脚PIO0_1接的是按键,为输入方向,所以执行了端口初始化函数后,LED和按键的驱动方向也就设置好了。
接下来在主函数中,通过读取引脚PIO0_1上的电平,就可心判断按键是否被按下了,这里读取的是字节引脚寄存器B1(对应引脚PIO0_1),而没有读取PIN0寄存器。因为这里只读取一个位的值,不必全部位都读取,大大提高了操作效率。同理,在点亮(或熄灭)LED时,也是操作字节引脚寄存器B7(对应引脚PIO0_7),而没有操作PIN0寄存器,同样提高了操作效率。
把上述程序编译下载到LPC824中并执行,可看到初始时LED是不亮的,按下KEY键LED发光,释放后LED熄灭,达到了设计要求。此外,还可把操作的字节引脚寄存器B换成字引脚寄存器W,即B1换成W1,B7换成W7,重新编译执行,会发现效果不变。说明在进行位操作时,即可以访问字节引脚寄存器也可访问字引脚寄存器,效果是一样的。那究竟应该选择哪个引脚寄存器呢?一般地,如果引脚上只写(或读)0和1两个值的话,就用字节引脚寄存器B,如果写(或读)的是0和非0多个值的话,就用字引脚寄存器W。实际上W寄存器是为了兼容而设计的,一般访问B寄存器就可以了。
最后还有一个DIRNOT0寄存器,这个寄存器在值为1的位对应的引脚会进行方向的取反,即输入变输出或输出变输入。这为单引脚上需要经常改变方向的操作提供了极大的便利,比如在一些单总线的设备模块访问上,就经常需要改变方向,这时直接使用DIRNOT0寄存器就方便多了,这将在以后用到时再作讲解,现在暂时不讨论了。
最后再强调一下,在芯片复位后,默认所有的引脚均为输入方向,所以复位后B、W、PIN0、MPIN0等4个寄存器的值是由外部电路来决定的。
通过上述讨论可见,在LPC824中操作GPIO端口的寄存器有很多,这为开发提供了极大的便利,所以在进行程序设计的时候,就应该尽量避免对端口寄存器进行“与或”等操作了,这样可大大提高操作效率。