找回密码
 立即注册
查看: 489|回复: 4

分享一个超级好用的GPIO驱动接口

[复制链接]
  • 打卡等级:初来乍到
  • 打卡总天数:1
  • 最近打卡:2024-09-22 02:32:15

3

主题

9

回帖

301

积分

中级会员

积分
301
发表于 2024-5-17 12:42:36 | 显示全部楼层 |阅读模式
本帖最后由 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_RS  IO(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_RS  IO(P3, High)”,则PORT(LCD_RS)等价于P3寄存器

6、脚位名感知
有时,为了编写更加智能和高效的代码,需要感知别名所在的脚位的名称,这时可以这样做:
BIT(LCD_RS)                      //等价于BIT2,即0x04
说明:如果LCD_RS绑定的是多个引脚,例如“#define LCD_RS  IO(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(关键代码)
#define  LCD_RS    IO(P5, 4)               //将LCD_RS绑定到单片机的P5.5引脚上(本示例不驱动液晶的RW引脚,请将该引脚固定接地)
#define  LCD_E      IO(P5, 5)                //将LCD_E绑定到单片机的P5.4引脚上
#define  LCD_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驱动接口,诸位是否喜欢呢?如果喜欢的人多,我就贴源码上来。



1 喜欢他/她就送朵鲜花吧,赠人玫瑰,手有余香!
回复

使用道具 举报 送花

  • 打卡等级:以坛为家II
  • 打卡总天数:435
  • 最近打卡:2025-04-30 08:43:23

33

主题

2351

回帖

4860

积分

论坛元老

积分
4860
发表于 2024-5-17 13:08:49 | 显示全部楼层
宏定义这里命名可以再规范一些,比如说SetBit,可以再加上个GPIO  变成GPIO_SetBit,,如果是写某个端口的全部引脚就不要使用SetBit很容易引起歧义。
参考例程并不是对技术参 考手册的补充,而是对技术参 考手册的解释。
技术参 考手册不应该需要参考例程作为补充,而是解释成了参考例程的样子
回复 支持 反对

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:1
  • 最近打卡:2024-09-22 02:32:15

3

主题

9

回帖

301

积分

中级会员

积分
301
发表于 2024-5-17 13:11:37 | 显示全部楼层
这个编辑器也太~~~~了吧,我写的示例代码90%都被吞了。
回复 支持 反对

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:1
  • 最近打卡:2024-09-22 02:32:15

3

主题

9

回帖

301

积分

中级会员

积分
301
发表于 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。

#define  LCD_RS  IO(P5, 4)
SetBit(LCD_RS);    //预处理后变成:P54 = 1;

#define  LCD_DB  IO(P3, Low)
SetBit(LCD_DB);    //预处理后变成:P3 |= 0x0F;

#define  LCD_DB  IO(P3, High)
SetBit(LCD_DB);    //预处理后变成:P3 |= 0xF0;

#define  LCD_DB  IO(P3, All)
SetBit(LCD_DB);    //预处理后变成:P3 = 0xFF;

综合考虑,最终可能会将宏名改为:GPIO_SetBits(pinName)
回复 支持 反对

使用道具 举报 送花

  • 打卡等级:偶尔看看II
  • 打卡总天数:21
  • 最近打卡:2025-04-04 22:11:37
已绑定手机

36

主题

287

回帖

828

积分

高级会员

积分
828
发表于 2024-5-18 19:35:44 来自手机 | 显示全部楼层
是不是可以搞一个能映射的GPIO操作函数?
就比如模拟IIC接口,SPI接口之类的,通过函数间接访问寄存器(虽然会慢一点,但慢这么一点应该没事),就能把引脚选择推迟到程序运行时,关于引脚的映射选择结果保存在EEPROM里
这一点在智能小车的通用接口上看起来很有用。识别一下接的是什么,就给它什么IO资源。
回复 支持 反对

使用道具 举报 送花

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|手机版|小黑屋|深圳国芯人工智能有限公司 ( 粤ICP备2022108929号-2 )

GMT+8, 2025-5-4 10:34 , Processed in 0.109596 second(s), 76 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表