Open imuncle opened 2 years ago
前段时间稚晖君做出了一个迷你机械臂,非常强大,里面使用的示教器Peak的软件部分借鉴的是另一个开源项目X-TRACK,在好奇心的驱使下我去大致阅读了两份代码,除却LVGL图形设计部分外,整个工程的框架设计对我有写额外的启发。
稚晖君自己也对X-TRACK写过一个分析文档,他绘制的代码结构如下图所示。
其中的消息框架和HAL层对我有些启发。
消息框架是一个消息发布订阅机制,类似于ROS里面的话题功能,这个机制可以将各个模块相互独立起来,而且对于一对多的消息传递这种情况,消息框架是非常容易管理的。
因为源代码中使用C++编写消息框架,而单片机开发用的更多的还是C语言,于是我自己简单写了一个C语言版的,其中动态数组cvector的实现修改自这篇博客。
cvector
#ifndef CVECTOR_H #define CVECTOR_H
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 );
* cvector.c ```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; }
#ifndef PUB_SUB_H #define PUB_SUB_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);
* pub_sub.c ```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层的设计,通过回调函数指针的方式,将底层硬件的配置代码与上层的逻辑代码分离,也增加了代码的复用性。这里以X-TRACK中的Encorder为例介绍一下。
/*外部中断回调函数指针数组*/ static EXTI_CallbackFunction_t EXTI_Function[16] = {0};
/**
@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); }
do{\ if(EXTI_GetIntStatus(EXTI_Line##n) != RESET)\ {\ if(EXTI_Function[n]) EXTI_Function[n]();\ EXTI_ClearIntPendingBit(EXTI_Line##n);\ }\ }while(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](https://user-images.githubusercontent.com/35989223/138667871-f5a64e43-1de4-4ecc-bbe8-8cd62029682c.png) # 结语 可能是以往的固有思路限制了我的想象,虽然这个框架在纯软件开发的时候都能想得到,但是却没有想过单片机这种低功耗,少资源的设备上能不能使用,答案是也可以,只要适当的简化就行了。
前段时间稚晖君做出了一个迷你机械臂,非常强大,里面使用的示教器Peak的软件部分借鉴的是另一个开源项目X-TRACK,在好奇心的驱使下我去大致阅读了两份代码,除却LVGL图形设计部分外,整个工程的框架设计对我有写额外的启发。
稚晖君自己也对X-TRACK写过一个分析文档,他绘制的代码结构如下图所示。
其中的消息框架和HAL层对我有些启发。
消息框架
消息框架是一个消息发布订阅机制,类似于ROS里面的话题功能,这个机制可以将各个模块相互独立起来,而且对于一对多的消息传递这种情况,消息框架是非常容易管理的。
因为源代码中使用C++编写消息框架,而单片机开发用的更多的还是C语言,于是我自己简单写了一个C语言版的,其中动态数组
cvector
的实现修改自这篇博客。include
include
include
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
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
当然只是很粗糙是实现了基本功能,舍去了消息队列缓冲,直接暴力的采用了阻塞式消息发布,也没有简单是否重复订阅同一话题,是否重复发布同一话题。
HAL层
第二个点是HAL层的设计,通过回调函数指针的方式,将底层硬件的配置代码与上层的逻辑代码分离,也增加了代码的复用性。这里以X-TRACK中的Encorder为例介绍一下。
/**
@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); }
/**
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)
/**
HAL_Encorder.cpp
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);
}