imuncle.github.io icon indicating copy to clipboard operation
imuncle.github.io copied to clipboard

对比赛代码框架的思考

Open imuncle opened this issue 4 years ago • 0 comments

前段时间稚晖君做出了一个迷你机械臂,非常强大,里面使用的示教器Peak的软件部分借鉴的是另一个开源项目X-TRACK,在好奇心的驱使下我去大致阅读了两份代码,除却LVGL图形设计部分外,整个工程的框架设计对我有写额外的启发。

稚晖君自己也对X-TRACK写过一个分析文档,他绘制的代码结构如下图所示。

img

其中的消息框架HAL层对我有些启发。

消息框架

消息框架是一个消息发布订阅机制,类似于ROS里面的话题功能,这个机制可以将各个模块相互独立起来,而且对于一对多的消息传递这种情况,消息框架是非常容易管理的。

因为源代码中使用C++编写消息框架,而单片机开发用的更多的还是C语言,于是我自己简单写了一个C语言版的,其中动态数组cvector的实现修改自这篇博客

  • cvector.h
#ifndef CVECTOR_H
#define CVECTOR_H

# include <stdio.h>  
# include <stdlib.h>  
# include <string.h>  

# define MIN_LEN 5
# define EXPANED_VAL 1

struct _cvector
{
	void *cv_pdata;
	size_t cv_len, cv_tot_len, cv_size;
};

typedef struct _cvector *cvector;

cvector   cvector_create   (const size_t size               );
void      cvector_destroy  (const cvector cv                );
size_t    cvector_length   (const cvector cv                );
void*     cvector_pushback (const cvector cv, void *memb    );
void*     cvector_val_at   (const cvector cv, size_t index  );

#endif
  • cvector.c
#include "cvector.h"

// size: 数组成员的大小
cvector cvector_create(const size_t size)
{
    cvector cv = (cvector)malloc(sizeof (struct _cvector));

    if (!cv) return NULL;

    cv->cv_pdata = malloc(MIN_LEN * size);

    if (!cv->cv_pdata)
    {
        free(cv);
        return NULL;
    }

    cv->cv_size = size;
    cv->cv_tot_len = MIN_LEN;
    cv->cv_len = 0;

    return cv;
}

void cvector_destroy(const cvector cv)
{
    free(cv->cv_pdata);
    free(cv);
    return;
}

size_t cvector_length(const cvector cv)
{
    return cv->cv_len;
}

void* cvector_pushback(const cvector cv, void *memb)
{
    if (cv->cv_len >= cv->cv_tot_len)
    {
        void *pd_sav = cv->cv_pdata;
		// 以cv_tot_len为最小单位进行扩张,避免反复realloc
        cv->cv_tot_len <<= EXPANED_VAL;
        cv->cv_pdata = realloc(cv->cv_pdata, cv->cv_tot_len * cv->cv_size);
    }
 
    memcpy((char *)cv->cv_pdata + cv->cv_len * cv->cv_size, memb, cv->cv_size);
    cv->cv_len++;

    return cv->cv_pdata + (cv->cv_len-1) * cv->cv_size;
}

void* cvector_val_at(const cvector cv, size_t index)
{
	return cv->cv_pdata + index * cv->cv_size;
}
  • pub_sub.h
#ifndef PUB_SUB_H
#define PUB_SUB_H

#include "cvector.h"

typedef unsigned char uint8_t;

typedef void(*sub_callback)(uint8_t* data, uint8_t len);

typedef struct publisher_t
{
    char* pub_topic;
    cvector subs;
    void(*publish)(struct publisher_t* pub, uint8_t* data, uint8_t len);
} Publisher;

typedef struct subscriber_t
{
    char *sub_topic;
    sub_callback callback;
} Subscriber;

void pub_commit(Publisher* pub, uint8_t *data, uint8_t len);
void pub_sub_init();
Publisher* create_publisher(char* topic);
void create_subscriber(char* topic, sub_callback bind_callback);

#endif
  • pub_sub.c
#include "cvector.h"
#include "pub_sub.h"
#include <stdio.h>

cvector pub_lists;
cvector sub_lists;

void pub_sub_init()
{
    pub_lists = cvector_create(sizeof(Publisher));
    sub_lists = cvector_create(sizeof(Subscriber));
}

void pub_commit(Publisher* pub, uint8_t *data, uint8_t len)
{
    int sub_len = cvector_length(pub->subs);
    for(int i = 0; i < sub_len; i++)
    {
        void* val = cvector_val_at(pub->subs, i);
        sub_callback callback = *(sub_callback*) val;
        callback(data, len);
    }
}

Publisher* create_publisher(char* topic)
{
    Publisher p;
    p.pub_topic = topic;
    p.subs = cvector_create(sizeof(sub_callback));
    int sub_len = cvector_length(sub_lists);
    for(int i = 0; i < sub_len; i++)
    {
        void* val = cvector_val_at(sub_lists, i);
        Subscriber *sub = (Subscriber*) val;
        if(!strcmp(topic, sub->sub_topic))
        {
            cvector_pushback(p.subs, &sub->callback);
        }
    }
    p.publish = pub_commit;
    void* pub = cvector_pushback(pub_lists, &p);
    return (Publisher*) pub;
}

void create_subscriber(char* topic, sub_callback bind_callback)
{
    Subscriber s;
    s.sub_topic = topic;
    s.callback = bind_callback;
    int pub_len = cvector_length(pub_lists);
    for(int i = 0; i < pub_len; i++)
    {
        void* val = cvector_val_at(pub_lists, i);
        Publisher *pub = (Publisher*) val;
        if(!strcmp(topic, pub->pub_topic))
        {
            cvector_pushback(pub->subs, &bind_callback);
        }
    }
    cvector_pushback(sub_lists, &s);
}

当然只是很粗糙是实现了基本功能,舍去了消息队列缓冲,直接暴力的采用了阻塞式消息发布,也没有简单是否重复订阅同一话题,是否重复发布同一话题。

HAL层

第二个点是HAL层的设计,通过回调函数指针的方式,将底层硬件的配置代码与上层的逻辑代码分离,也增加了代码的复用性。这里以X-TRACK中的Encorder为例介绍一下。

  • exit.c
/*外部中断回调函数指针数组*/
static EXTI_CallbackFunction_t EXTI_Function[16] = {0};

/**
  * @brief  外部中断初始化
  * @param  Pin: 引脚编号
  * @param  function: 回调函数
  * @param  Trigger_Mode: 触发方式
  * @param  PreemptionPriority: 抢占优先级
  * @param  SubPriority: 子优先级
  * @retval 无
  */
void EXTIx_Init(uint8_t Pin, EXTI_CallbackFunction_t function, EXTITrigger_Type Trigger_Mode, uint8_t PreemptionPriority, uint8_t SubPriority)
{
    EXTI_InitType EXTI_InitStructure;
    NVIC_InitType NVIC_InitStructure;
    uint8_t Pinx;

    if(!IS_PIN(Pin))
        return;

    Pinx = GPIO_GetPinNum(Pin);

    if(Pinx > 15)
        return;
    
    EXTI_Function[Pinx] = function;

    //GPIO中断线以及中断初始化配置
    RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_AFIO, ENABLE);
    GPIO_EXTILineConfig(GPIO_GetPortNum(Pin), Pinx);

    EXTI_InitStructure.EXTI_Line = EXTI_GetLinex(Pin);//设置中断线
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;//设置触发模式,中断触发(事件触发)
    EXTI_InitStructure.EXTI_Trigger = Trigger_Mode;//设置触发方式
    EXTI_InitStructure.EXTI_LineEnable = ENABLE;
    EXTI_Init(&EXTI_InitStructure);     //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器

    NVIC_InitStructure.NVIC_IRQChannel = EXTI_GetIRQn(Pin);                    //使能所在的外部中断通道
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = PreemptionPriority;      //抢占优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = SubPriority;                //子优先级
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;                     //使能外部中断通道
    NVIC_Init(&NVIC_InitStructure);
}

/**
  * @brief  外部中断初始化 (Arduino)
  * @param  Pin: 引脚编号
  * @param  function: 回调函数
  * @param  Trigger_Mode: 触发方式
  * @retval 无
  */
void attachInterrupt(uint8_t Pin, EXTI_CallbackFunction_t function, EXTITrigger_Type Trigger_Mode)
{
    EXTIx_Init(Pin, function, Trigger_Mode, EXTI_PreemptionPriority_Default, EXTI_SubPriority_Default);
}

#define EXTIx_IRQHANDLER(n) \
do{\
    if(EXTI_GetIntStatus(EXTI_Line##n) != RESET)\
    {\
        if(EXTI_Function[n]) EXTI_Function[n]();\
        EXTI_ClearIntPendingBit(EXTI_Line##n);\
    }\
}while(0)

/**
  * @brief  外部中断入口,通道0
  * @param  无
  * @retval 无
  */
void EXTI0_IRQHandler(void)
{
    EXTIx_IRQHANDLER(0);
}
  • HAL_Encorder.cpp
static void Encoder_EventHandler()
{
    if(!EncoderEnable || EncoderDiffDisable)
    {
        return;
    }

    int dir = (digitalRead(CONFIG_ENCODER_B_PIN) == LOW ? -1 : +1);
    EncoderDiff += dir;
    Buzz_Handler(dir);
}

void HAL::Encoder_Init()
{
    pinMode(CONFIG_ENCODER_A_PIN, INPUT_PULLUP);
    pinMode(CONFIG_ENCODER_B_PIN, INPUT_PULLUP);
    pinMode(CONFIG_ENCODER_PUSH_PIN, INPUT_PULLUP);

    attachInterrupt(CONFIG_ENCODER_A_PIN, Encoder_EventHandler, FALLING);

    EncoderPush.EventAttach(Encoder_PushHandler);
}

可以看到,通过attachInterrupt函数接口,将回调函数传给exti,在外部中断触发的时候,就会直接调用Encoder_EventHandler函数了。

大家最常用的方法是直接在EXTI0_IRQHandler函数里调用Encoder_EventHandler函数,这样底层和上层就耦合在一起了,而通过函数指针的方式,二者就基本相互独立。

新的框架

于是我对新框架有了一些想法,以下是我对新框架的构想。

QQ图片20211025170805

结语

可能是以往的固有思路限制了我的想象,虽然这个框架在纯软件开发的时候都能想得到,但是却没有想过单片机这种低功耗,少资源的设备上能不能使用,答案是也可以,只要适当的简化就行了。

imuncle avatar Oct 25 '21 09:10 imuncle