发新帖本帖赏金 50.00元(功能说明)我要提问
返回列表
打印
[STM32]

单片机程序还能这么写?小小技巧,一学就会

[复制链接]
1950|6
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主

[i=s] 本帖最后由 begseeder 于 2025-5-14 10:49 编辑 [/i]<br /> <br />

[i=s] 本帖最后由 begseeder 于 2025-5-13 08:30 编辑 [/i]

#申请原创# @21小跑堂

前言

这篇文章涉及到的单片机是stm32f系列这种层级的类型,所以资源有限,纯C开发,基于面向对象的方式对单片机的底层驱动进行接口化设计。在这里呢,先提前声明,这里并不是一个通用的、现成的接口库,而只是介绍一种设计思路,我原本也是想做出一个通用的、统一的接口库,但是达到这种程度的水平,必须是一个领识特别丰富的高手才能做到,我还任重道远啊,所以若是文章内容有任何问题,还请大佬们不吝赐教,小弟感激不尽。好了,话不多说,直接上正题。

技术要点

C实现面向对象

我们都知道C本身是一个面向过程的语言,我的理解就是把一个项目的全部功能先一一列出,这就是函数的原型,然后过程就是这些函数的排列组合。最基础的排列就是顺序结构了,按部就班的,每个功能片依次实现就完成了整个功能,复杂点的就比如异步结构,比如前后台方式。其实不论是面向对象还是面向过程,本质上是一种看待事物的角度不同,与语言本身没有关系,因为我照样能用 C++或者 python写出面向过程的样子(就是不会用面向对象,,),把一个大而全的功能拆解为许多小的功能这是一种很好的解决复杂问题的方法,但是怎么拆解,拆解的基础或标准是什么呢,这个当然因人而异,但至少可以有两种角度(因为我就只知道这两种)。比如下面这个去医院看病的例子:

单片机驱动就该走对象接口化的路子1.jpg

上图中就是看病的一般流程了,可以大致的看出就是由具体的过程逐步实现的,但是换一个角度的话,就是另一番情景了,如下图所示:

单片机驱动就该走对象接口化的路子2.jpg

上图就是对上述流程的面向对象辨识,当然这种辨识可以是多种方式,在这里我分析出了三个主要的对象,分别是自助服务系统、智慧导诊系统以及患者管理系统,这3个对象提供了预约服务、取号服务、信息管理等功能,可以很明显的看面向对象的特点就是将问题抽象为对象,过程就是对象间的交互逻辑。并且也可以发现,其实不论是面向过程还是面向对象,看病的过程都是一致的,但是面向过程是直接照搬,过程是啥代码就是啥,而面向对象则是抽象的,分析的,从动态的过程中提炼出静态的元素,这样才有了灵活性、可扩展性等优点。

那么C语言要怎么实现面向对象呢,不要以为面向对象只是语言的实现技巧,它更是一种解决问题的方式,首要的一点就是自己要有这个意识,先把问题分析一下,抽象出问题中所涉及的交互对象,然后才是开始代码设计,主要通过结构体+函数指针的方式实现对象的设计。

typedef struct{
  /* PrivateArea作为私有域,不可直接访问 */
  struct{
    char a;
    char b;
    void (*func1)(void);
    void (*func2)(void);
  }PrivateArea;
  /* 以下为公共域,可直接访问*/
  int c;
  void (*func3)(void *arg);
}SomeClass_t;

可以看到定义了一个SomeClass_t的类,通过变量表达类属性,通过函数指针表达类方法,其次里面通过嵌套一级结构体作为私有属性和方法,人为限制访问。

接口化设计

上面介绍了面向对象的方法之后,那么我们看待驱动就有了不一样的角度。比如说我以前看待 GPIO,那就是读引脚,或者置高置低的操作,现在学了面向对象再看,把 GPIO看成一个对象,因为对象是与现实世界一一映射的,所以,不再有所谓置高置低的可读性很差的操作,而是根据属性进行相应操作,比如说用 GPIO驱动LED灯,当然可以看一下原理图,哪个电平可以亮哪个电平可以灭,但是考虑到如果换一种驱动方式,那么控制 LED的逻辑电平就不一样了,所以这时候可以抽象出一个LED类,那么驱动方式就内建在实例化的对象自身中,举例如下:

/* 前向声明 */
struct _LED_CLASS_T;
/* 定义LED类 */
typedef struct _LED_CLASS_T{
    struct{
        /* 有效电平属性 */
        int ValidFlag;
        /* 引脚句柄 - 能直接控制引脚就行 */
        volatile unsigned long  *PinxOut;

    }PrivateArea;
    /* 对象方法 */
    void (*On)(struct _LED_CLASS_T *);
    void (*Off)(struct _LED_CLASS_T *);
    void (*Toggle)(struct _LED_CLASS_T *);
    /* 对象初始化 */
    void (*Init)(struct _LED_CLASS_T *,int valid_flag,volatile unsigned long  *pout);
}LedClass_t;

上面定义了LED类,类中定义了私有属性 ValidFlag表示有效电平,PinxOut表示LED的驱动引脚,其次是接口定义,包括点亮,熄灭和翻转,因为是接口,所以具体实现可以有多种可能和方式。最后是一个初始化方法,用于初始化对象的属性和接口绑定,这是开始使用对象之前必须要做的事情,具体样例如下所示:

/* file: myled.c */

static void LedOn(LedClass_t *ledx){
    *(ledx->PrivateArea.PinxOut) = ledx->PrivateArea.ValidFlag;
}

static void LedOff(LedClass_t *ledx){
    *(ledx->PrivateArea.PinxOut) = !(ledx->PrivateArea.ValidFlag);
}

static void LedToggle(LedClass_t *ledx){
    *(ledx->PrivateArea.PinxOut) = !(*(ledx->PrivateArea.PinxOut));
}

static void InitLed(LedClass_t *ledx,int valid_flag,volatile unsigned long *pout){
    ledx->PrivateArea.ValidFlag = valid_flag;
    ledx->PrivateArea.PinxOut = pout;

    ledx->Off = LedOff;
    ledx->On = LedOn;
    ledx->Toggle = LedToggle;
}
/* 实例化对象 - 指定初始化方法 */
LedClass_t Led1 = {.Init = InitLed};
LedClass_t Led2 = {.Init = InitLed};

上面的代码中,实现了`LED`类的接口定义,这里两个`LED`使用同样的方法,保证可重用就行,然后在初始化方法中进行属性配置和接口绑定,每个接口需要传入对象的自身的引用,用过 python的都知道self关键字,类似这种概念。接下来介绍一下对象的使用。

/* 对象初始化 */
Led1.Init(&Led1,BIT_SET,&PCout(1));
Led2.Init(&Led2,BIT_RESET,&PCout(2));
/* 对象方法调用 */
Led1.On(&Led1);
Led2.On(&Led2);

在上面的代码中,先是对象的初始化,指定有效电平和驱动引脚,这里使用到了位带操作,经过初始化后,对象的具体模样就成型了,可以看到虽然两个LED的有效电平不同,但是调用方式是一样的,这样就屏蔽了对象间的内在差异,实现了通用的、方便移植的统一接口,讲的通俗一点就是,我不管你这灯是怎么亮的,我只要能控制亮灭就行。

综合实践

经过上面的介绍,对面向对象和接口化设计有了一定的了解,就我的认识而言,在写驱动的时候,要完成接口化的设计,最先做的就是类定义,类是抽象的,是具体事物的一般化结果。举个例子,现在想要实现一个flash驱动,从软件层面来定义,flash是用来读写数据的,那么flash类必然包含读方法和写方法,其次flash是有限的,那么必然包含flash可用空间大小,上下边界这样的属性,这样就定义了一个简单的flash接口类,结构如下所示:

typedef struct{
    struct{
        uint32_t LowerLimitAddr;
        uint32_t TotalFlashSize;
    }PrivateArea;
    int (*Init)(void);
    int (*Read)(uint8_t *buf,uint32_t addr,uint32_t len);
    int (*Write)(uint8_t *buf,uint32_t addr,uint32_t len);
}FlashInterface_t;

PS: 在上面的代码中我并没有用到传入对象自身引用的操作,因为考虑到如果板子的flash数量只有一个的话,那么没必要做这一步操作。

那么接下来就做一个小的demo程序来体现接口化的编码方式,有串口、LED、flash、和RTC,其中RTC使用软件I2C接口来进行操作,如下图所示。

单片机驱动就该走对象接口化的路子3.jpg

各个对象的接口类图如下:

单片机驱动就该走对象接口化的路子4.jpg

每个类都有一个Init的初始化方法,其中RtcClass,使用了SoftI2Class的接口,下面依次给出接口代码结构。

/* UsartClass接口定义 */

struct _UART_CLASS_T;

typedef struct _UART_CLASS_T{
    struct{
        int BufCnt;
        UART_HandleTypeDef *UARTxHandler;
        int RecvStatus;
    }PrivateAttr;

    void (*Send)(struct _UART_CLASS_T *,uint8_t *buf,int bufsize);

    int (*Recv)(struct _UART_CLASS_T *,uint8_t *buf,int limitsize);

void (*Init)(   struct _UART_CLASS_T *,UART_HandleTypeDef *);

}UartClass_t;

/* 软件I2C的接口定义 */

struct _SOFT_IIC_COM_T;

typedef struct _SOFT_IIC_COM_T{
  struct{
    volatile unsigned long  *I2cSdaPin;
    volatile unsigned long  *I2cSclPin;
    int Delay;
    void (*ComDelay)(int delay);
   }PrivateArea;
  void (*Start)(struct _SOFT_IIC_COM_T *);
  void (*Stop)(struct _SOFT_IIC_COM_T *);
  void (*Ack)(struct _SOFT_IIC_COM_T *);
  void (*Nack)(struct _SOFT_IIC_COM_T *);
  unsigned char (*Wack)(struct _SOFT_IIC_COM_T *);
  void (*Send_Byte)(struct _SOFT_IIC_COM_T *,unsigned char byte);
  unsigned char (*Read_Byte)(struct _SOFT_IIC_COM_T *,unsigned char ack);
  void (*Init)(struct _SOFT_IIC_COM_T *,volatile unsigned long  *sdapin,volatile unsigned long  *sclpin,int period);
}Soft_IIC_Com_t;

其中实现了一般I2C通信的主要功能函数接口,例如 StartStopAck等,同时给出了延时函数的接口,用于调节I2C的通信速率。

/* RTC驱动的接口定义 */

typedef struct{
    struct{
        Soft_IIC_Com_t *SoftI2c;
    }PrivateArea;

    void (*GetIime)(Date_TypeDef *data);
    void (*SetIime)(Date_TypeDef *data);
    void (*Init)(void);

}RtcClass_t;

在RTC接口中,依赖一个软件I2C对象,需要在初始化时指定一个。

接下来就是使用这些接口的方式

int main(void)
{

int i = 0,j=0;
Date_TypeDef data = {2025,5,12,6,17,20,30};
  HAL_Init();

  SystemClock_Config();
  MX_GPIO_Init();
  MX_CAN_Init();
  MX_TIM1_Init();
  MX_USART1_UART_Init();
  MX_TIM2_Init();
/* 驱动对象的接口初始化 */
McuFlash.Init();
BspLedInit();
Uart1Obj.Init(&Uart1Obj,&huart1);
BspRtc_Obj.Init();
/* 设置RTC时间 */
BspRtc_Obj.SetIime(&data);

  while (1)
  {
    if (TIMEBASE_HOOK(TimeBaseScope, OPT_TIMEBASE_500MS)) {
    TIMEBASE_DROP(TimeBaseScope, OPT_TIMEBASE_500MS);
    /* led驱动 >>>>>>>> */
    Led1.Toggle(&Led1);
    /* led驱动 <<<<<<<< */

    }
    if (TIMEBASE_HOOK(TimeBaseScope, OPT_TIMEBASE_1S)) {
    TIMEBASE_DROP(TimeBaseScope, OPT_TIMEBASE_1S);

    Led2.Toggle(&Led2);

    /* flash驱动 >>>>>>>> */
    i++;
    McuFlash.Write((uint8_t *)&i,0x7800,sizeof(int));

    McuFlash.Read((uint8_t *)&j,0x7800,sizeof(int));
    /* flash驱动 <<<<<<<< */

    /* rtc驱动 >>>>>>>> */
    BspRtc_Obj.GetIime(&data);
    /* 串口发送当前时间 */
    Uart1Obj.Send(&Uart1Obj,(uint8_t *)&data,32);
    /* rtc驱动 <<<<<<<< */
}
    /* uart驱动 >>>>>>>> */
    if(Uart1Obj.Recv(&Uart1Obj,UartBuf,32)){
        Uart1Obj.Send(&Uart1Obj,UartBuf,32);

    }
    /* uart驱动 <<<<<<<< */
  }

}

以上就是这篇文章的全部内容,虽然看起来使用接口化的时候会多很多比较繁琐的步骤,但是一旦熟悉之后,便会使代码的灵活性、可移植性以及可维护性大大提高。最后,也欢迎朋友提出批评和建议,大家一起共同学习,进步。

最后附上示例代码:upload 附件:demo_app.zip

使用特权

评论回复

打赏榜单

21小跑堂 打赏了 50.00 元 2025-05-14
理由:恭喜通过原创审核!期待您更多的原创作品~~

评论
21小跑堂 2025-5-14 16:10 回复TA
以面向对象的思想植入到C语言编程中,增加代码的灵活性和可移植性,降低维护成本,提升编程效率。 

相关帖子

沙发
呐咯密密| | 2025-5-14 15:00 | 只看该作者
用面向对象的思想来搞C,感觉挺不错

使用特权

评论回复
板凳
begseeder|  楼主 | 2025-5-14 17:21 | 只看该作者
呐咯密密 发表于 2025-5-14 15:00
用面向对象的思想来搞C,感觉挺不错

是的呢,我学习过程中发现这个思路早就存在了,肯定比不了那些面向对象的语言的丰富特性,但觉得这个角度很好,方便分析和解决问题

使用特权

评论回复
地板
ddllxxrr| | 2025-5-15 11:13 | 只看该作者
不错是不错,但是还是属于顺序结构,没有操作系统的那个优势

使用特权

评论回复
5
qinlu123| | 2025-5-16 15:39 | 只看该作者
我一直面向对象

使用特权

评论回复
6
M169| | 2025-5-20 13:23 | 只看该作者
学到了

使用特权

评论回复
发新帖 本帖赏金 50.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

4

主题

30

帖子

0

粉丝