分享一个超级好用的GPIO驱动接口
本帖最后由 baiyu 于 2024-5-17 14:01 编辑STC系列单片机的IO口由PX、PXM1、PXM0、PXPU、PXNCS、PXSR、PXDR、PXIE等八组IO寄存器共同控制,新的STC单片机还增加了PXPD寄存器。
如此多的寄存器,如何才能高效率且高性能地驾驭它们呢?
我建立了一种通用的IO驱动模型。通过这种模型,用户只需为目标IO引脚绑定一个别名,就可以通过这个别名和用户的控制意图,间接地操作相关的IO寄存器,再也无需直接与IO寄存器打交道了。
假设我们需要使用STC单片的P3.2引脚驱动LCD1602字符液晶的RS引脚,那么我们可以通过下面的宏定义,将P3.2引脚绑定到别名LCD_RS上:
#define LCD_RS IO(P3, 2) //将P3.2引脚绑定到别名LCD_RS上
//第一个参数可以是P0、P1、P2……
//第二个参数可以是0、1、2、3、4、5、6、7、Low(低四位)、High(高四位),或者All(全八位)
这样绑定之后,LCD_RS就成了P3.2引脚的别名,我们就可以通过LCD_RS这个别名,按照我们的意图,间接地操作与P3.2引脚相关的所有寄存器。
1、设置工作模式
STC单片机的IO引脚具有四种工作模式:弱上拉准双向口模式(PullUp模式)、推挽输出口模式(PushPull模式)、高阻输入口模式(HighZ模式),以及开漏双向口模式(OpenDrain模式)
ToPullUp(LCD_RS); //将LCD_RS引脚(即P3.2引脚)设置为弱上拉双向口模式(PullUp模式)
ToPushPull(LCD_RS); //将LCD_RS引脚(即P3.2引脚)设置为推挽输出口模式(PushPull模式)
ToHighZ(LCD_RS); //将LCD_RS引脚(即P3.2引脚)设置为高阻输入口模式(HighZ模式)
ToOpenDrain(LCD_RS); //将LCD_RS引脚(即P3.2引脚)设置为开漏双向口模式(OpenDrain模式)
2、设置驱动模式(需要事先Enable(XFR);)
STC单片机的IO引脚具有四种驱动模式:电平慢速翻转模式(Slow模式)、电平快速翻转模式(Fast模式)、电流弱驱动模式(Weak模式),以及电流强驱动能力模式(Strong模式)
ToSlow(LCD_RS); //将LCD_RS引脚(即P3.2引脚)设置为电平慢速翻转模式(Slow模式)
ToFast(LCD_RS); //将LCD_RS引脚(即P3.2引脚)设置为电平快速翻转模式(Fast模式)
ToWeak(LCD_RS); //将LCD_RS引脚(即P3.2引脚)设置为电流弱驱动模式(Weak模式)
ToStrong(LCD_RS); //将LCD_RS引脚(即P3.2引脚)设置为电流强驱动模式(Strong模式)
ToSlowWeak(LCD_RS); //将LCD_RS引脚(即P3.2引脚)设置为慢速弱驱动模式(SlowWeak模式)
ToSlowStrong(LCD_RS //将LCD_RS引脚(即P3.2引脚)设置为慢速强驱动转模式(SlowStrong模式)
ToFastWeak(LCD_RS); //将LCD_RS引脚(即P3.2引脚)设置为快速弱驱动模式(FastWeak模式)
ToFastStrong(LCD_RS); //将LCD_RS引脚(即P3.2引脚)设置为快速强驱动模式(FastStrong模式)
3、启禁附加功能(需要事先Enable(XFR);)
STC单片机的IO引脚有四种可以启禁的附加功能:内部4.1K上拉电阻(PUR)、数字信号输入(DIGIT)、施密特抑噪输入(SMT),部分单片机还有额外的内部10K下拉电阻(PDR)
EnablePUR(LCD_RS); //启用LCD_RS引脚(即P3.2引脚)的内部4.1K上拉电阻
EnablePDR(LCD_RS); //启用LCD_RS引脚(即P3.2引脚)的内部10K下拉电阻
EnableSMT(LCD_RS); //启用LCD_RS引脚(即P3.2引脚)的施密特抑噪输入功能
EnableDIGIT(LCD_RS); //启用LCD_RS引脚(即P3.2引脚)的数字信号输入功能
DisablePUR(LCD_RS); //禁用LCD_RS引脚(即P3.2引脚)的内部4.1K上拉电阻
DisablePDR(LCD_RS); //禁用LCD_RS引脚(即P3.2引脚)的内部10K下拉电阻
DisableSMT(LCD_RS); //禁用LCD_RS引脚(即P3.2引脚)的施密特抑噪输入功能
DisableDIGIT(LCD_RS); //禁用LCD_RS引脚(即P3.2引脚)的数字信号输入功能
4、数据操作
为了控制IO引脚的电平或从IO引脚读取电平信号,可以这样:
SetValue(LCD_RS, value); //将LCD_RS引脚(即P3.2引脚)赋值为value电平(相当于“P32 = value;”)
SetBit(LCD_RS); //将LCD_RS引脚(即P3.2引脚)设置为高电平 (相当于“P32 = 1;”)
ClrBit(LCD_RS); //将LCD_RS引脚(即P3.2引脚)设置为低电平 (相当于“P32 = 0;”)
TogBit(LCD_RS); //将LCD_RS引脚(即P3.2引脚)的电平高低翻转 (相当于“P32 = ~P32;”)
GetBit(LCD_RS); //读取LCD_RS引脚(即P3.2引脚)的电平 (相当于“读取P32”)
说明:如果LCD_RS绑定的是多个引脚,例如“#define LCD_RSIO(P3, High)”,则
(1)“SetValue(RS_RS, value);”仅将value的高四位赋值给P3寄存器的高四位,而忽略value的低四位,并且不会影响P3寄存器的低四位;
(2)“GetBit(LCD_RS);”相当于“P3 & 0xF0”,即仅读取P3寄存器的高四位,而不会影响P3寄存器的低四位。
5、端口名感知
有时,为了编写更加智能和高效的代码,需要感知别名所在的端口的名称,这时可以这样做:
PORT(LCD_RS) //等价于P32寄存器
说明:如果LCD_RS绑定的是多个引脚,例如“#define LCD_RSIO(P3, High)”,则PORT(LCD_RS)等价于P3寄存器
6、脚位名感知
有时,为了编写更加智能和高效的代码,需要感知别名所在的脚位的名称,这时可以这样做:
BIT(LCD_RS) //等价于BIT2,即0x04
说明:如果LCD_RS绑定的是多个引脚,例如“#define LCD_RSIO(P3, High)”,则PORT(LCD_RS)等价于BITHigh,即0xF0
7、使用位掩码
用户可以同时使用BITX风格或BIT(X)风格的位掩码而不会产生冲突
BIT0 //等价于BIT(0), 并且等价于0x01
BIT1 //等价于BIT(1), 并且等价于0x02
BIT2 //等价于BIT(2), 并且等价于0x04
BIT3 //等价于BIT(3), 并且等价于0x08
BIT4 //等价于BIT(4), 并且等价于0x10
BIT5 //等价于BIT(5), 并且等价于0x20
BIT6 //等价于BIT(6), 并且等价于0x40
BIT7 //等价于BIT(7), 并且等价于0x80
BIT8 //等价于BIT(8), 并且等价于0x0100
BIT9 //等价于BIT(9), 并且等价于0x0200
BIT10 //等价于BIT(10),并且等价于0x0400
BIT11 //等价于BIT(11),并且等价于0x0800
BIT12 //等价于BIT(12),并且等价于0x1000
BIT13 //等价于BIT(13),并且等价于0x2000
BIT14 //等价于BIT(14),并且等价于0x4000
BIT15 //等价于BIT(15),并且等价于0x8000
BIT16 //等价于BIT(16),并且等价于0x00010000UL
BIT17 //等价于BIT(17),并且等价于0x00020000UL
BIT18 //等价于BIT(18),并且等价于0x00040000UL
BIT19 //等价于BIT(19),并且等价于0x00080000UL
BIT20 //等价于BIT(20),并且等价于0x00100000UL
BIT21 //等价于BIT(21),并且等价于0x00200000UL
BIT22 //等价于BIT(22),并且等价于0x00400000UL
BIT23 //等价于BIT(23),并且等价于0x00800000UL
BIT24 //等价于BIT(24),并且等价于0x01000000UL
BIT25 //等价于BIT(25),并且等价于0x02000000UL
BIT26 //等价于BIT(26),并且等价于0x04000000UL
BIT27 //等价于BIT(27),并且等价于0x08000000UL
BIT28 //等价于BIT(28),并且等价于0x10000000UL
BIT29 //等价于BIT(29),并且等价于0x20000000UL
BIT30 //等价于BIT(30),并且等价于0x40000000UL
BIT31 //等价于BIT(31),并且等价于0x80000000UL
8、应用示例
//lcd1602.h(关键代码)
#defineLCD_RS IO(P5, 4) //将LCD_RS绑定到单片机的P5.5引脚上(本示例不驱动液晶的RW引脚,请将该引脚固定接地)
#defineLCD_E IO(P5, 5) //将LCD_E绑定到单片机的P5.4引脚上
#defineLCD_DB IO(P3, Low) //Low:将单片机的P3.0~P3.4引脚分别连接到LCD_DB的DB4~DB7引脚上
//High:将单片机的P3.0~P3.4引脚分别连接到LCD_DB的DB4~DB7引脚上
// All:将单片机的P3.0~P3.4引脚分别连接到LCD_DB的DB4~DB7引脚上
//lcd1602.c(关键代码)
void LcdWriteByte(unsigned char byte)
{
//延时60.0微秒确保液晶处于”不忙“状态
delay_us(60);
//如果是全八位通信模式,将value通过DB0~DB7写入液晶模块;如果是低四位或高四位通信模式,将byte的高四位通过DB4~DB7写入液晶模块
SetValue(LCD_DB, BIT(LCD_DB) == BITLow ? byte >> 4 : byte);
SetBit(LCD_E);
delay_us(1);
ClrBit(LCD_E);
//如果是全八位通信模式,什么也不用做;如果是低四位或高四位通信模式,将byte的低四位通过DB4~DB7写入液晶模块
#if BIT(LCD_DB) != BITAll
SetValue(LCD_DB, BIT(LCD_DB) == BITHigh ? byte << 4 : byte);
SetBit(LCD_E);
delay_us(1);
ClrBit(LCD_E);
#endif
}
void LcdInit(void)
{
//确保所有控制脚都是低电平
ClrBit(LCD_RS);
ClrBit(LCD_E);
//确保所有控制脚都是推挽输出态
ToPushPull(LCD_RS);
ToPushPull(LCD_E);
//确保液晶的DB引脚处于输入态,才将LCD_DB引脚置为输出态
ToPullUp(LCD_DB);
//等待液晶模块的电源稳定
delay_ms(100);
//兼容四线通信模式和八线通信模式
LcdWriteCommand(BIT(DB) == BITAll ? 0x38 : 0x28);
LcdWriteCommand(0x01);
delay_ms(2);
LcdWriteCommand(0x06);
LcdWriteCommand(0x0C);
}
从上面的示例可以非常清楚地看到这种通用GPIO驱动接口的优点:
(1)只需定义一次,就可以为IO引脚绑定别名
(2)绑定别名之后,可以直接按照意图间接操作IO寄存器,再也不必直接操作IO寄存器了
(3)基于别名机制编写出来的程序体现了将IO引脚对象化的编程思想,具有很强的可读性
(4)基于GPIO驱动库编写出来的程序兼容性强,例如LCD1602可以同时兼容低四位的四线通信模式、高四位的四线模式和全八位通信模式
(5)基于GPIO驱动库编写出来的程序十分健壮,例如当需要更改IO引脚的硬件连接方式时,只需去别名绑定的地方作单次更改就可以了,程序的其它地方完全无需改动。
(6)基于GPIO驱动库编写出来的程序十分高效和智能,例如,SetBit(LCD_RS)编译后是“P54 = 1;”,SetBit(LCD_DB)编译后是“P3 |= 0x0F;”,如果"#define LCD_DB IO(P3, All)",则SetBit(LCD_DB)编译后是“P3 = 0xFF;”。
这样的IO驱动接口,诸位是否喜欢呢?如果喜欢的人多,我就贴源码上来。
宏定义这里命名可以再规范一些,比如说SetBit,可以再加上个GPIO变成GPIO_SetBit,,如果是写某个端口的全部引脚就不要使用SetBit很容易引起歧义。{:4_197:} 这个编辑器也太~~~~了吧,我写的示例代码90%都被吞了。 本帖最后由 baiyu 于 2024-5-17 14:15 编辑
_奶咖君_ 发表于 2024-5-17 13:08
宏定义这里命名可以再规范一些,比如说SetBit,可以再加上个GPIO变成GPIO_SetBit,,如果是写某个端口的全 ...
确实应该加GPIO_前缀。
不过,SetBIt(或GPIO_SetBit)可以对IO端口的单个位、低四位、高四位和全八位执行置位操作。为了接口的统一,即使是对整个端口置位,也建议使用SetBit。
#defineLCD_RSIO(P5, 4)
SetBit(LCD_RS); //预处理后变成:P54 = 1;
#defineLCD_DBIO(P3, Low)
SetBit(LCD_DB); //预处理后变成:P3 |= 0x0F;
#defineLCD_DBIO(P3, High)
SetBit(LCD_DB); //预处理后变成:P3 |= 0xF0;
#defineLCD_DBIO(P3, All)
SetBit(LCD_DB); //预处理后变成:P3 = 0xFF;
综合考虑,最终可能会将宏名改为:GPIO_SetBits(pinName) 是不是可以搞一个能映射的GPIO操作函数?
就比如模拟IIC接口,SPI接口之类的,通过函数间接访问寄存器(虽然会慢一点,但慢这么一点应该没事),就能把引脚选择推迟到程序运行时,关于引脚的映射选择结果保存在EEPROM里
这一点在智能小车的通用接口上看起来很有用。识别一下接的是什么,就给它什么IO资源。
页:
[1]