对比赛代码框架的思考
前段时间稚晖君做出了一个迷你机械臂,非常强大,里面使用的示教器Peak的软件部分借鉴的是另一个开源项目X-TRACK,在好奇心的驱使下我去大致阅读了两份代码,除却LVGL图形设计部分外,整个工程的框架设计对我有写额外的启发。
稚晖君自己也对X-TRACK写过一个分析文档,他绘制的代码结构如下图所示。
其中的消息框架和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函数,这样底层和上层就耦合在一起了,而通过函数指针的方式,二者就基本相互独立。
新的框架
于是我对新框架有了一些想法,以下是我对新框架的构想。

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