baiyu 发表于 2024-5-17 12:42:36

分享一个超级好用的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驱动接口,诸位是否喜欢呢?如果喜欢的人多,我就贴源码上来。



_奶咖君_ 发表于 2024-5-17 13:08:49

宏定义这里命名可以再规范一些,比如说SetBit,可以再加上个GPIO变成GPIO_SetBit,,如果是写某个端口的全部引脚就不要使用SetBit很容易引起歧义。{:4_197:}

baiyu 发表于 2024-5-17 13:11:37

这个编辑器也太~~~~了吧,我写的示例代码90%都被吞了。

baiyu 发表于 2024-5-17 14:14:10

本帖最后由 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)

_NCY_ 发表于 2024-5-18 19:35:44

是不是可以搞一个能映射的GPIO操作函数?
就比如模拟IIC接口,SPI接口之类的,通过函数间接访问寄存器(虽然会慢一点,但慢这么一点应该没事),就能把引脚选择推迟到程序运行时,关于引脚的映射选择结果保存在EEPROM里
这一点在智能小车的通用接口上看起来很有用。识别一下接的是什么,就给它什么IO资源。
页: [1]
查看完整版本: 分享一个超级好用的GPIO驱动接口