什么是STMBL
STMBL 是一款基于 STM32F4 系列的开源伺服驱动器,专为 CNC 机床和机器人改造而设计。它支持高达 320V 和 2kW 的工业交流和直流伺服。项目所用语言为C语言(少见的纯C语言项目)。
该驱动器可以通过类似于LinuxCNC所使用的HAL(hardware abstraction layer)模块对各种命令和反馈类型进行配置。
我在这里不想谈其中的驱动算法,而是想说说这个项目的软件架构。
HAL(硬件抽象层)
这里所说的HAL并非STM32的HAL库,而是LinuxCNC中“子模块”的概念,类似于梯形图的功能块和Simulink中的框图。
STMBL的HAL对LinuxCNC的HAL进行了简化,变量(并不包含在HAL结构体中)包括PIN(输入输出变量)和ctx(内部变量),任务包括rt任务(5KHz中断调度任务)、frt任务(20KHz中断调度任务)、nrt(主循环调度任务),以及初始化函数和开始停止函数,hal_comp_t结构体如下:
typedef const struct {
NAME name;
void (*nrt)(void *ctx_ptr, hal_pin_inst_t *pin_ptr);
void (*rt)(float period, void *ctx_ptr, hal_pin_inst_t *pin_ptr);
void (*frt)(float period, void *ctx_ptr, hal_pin_inst_t *pin_ptr);
void (*nrt_init)(void *ctx_ptr, hal_pin_inst_t *pin_ptr);
void (*hw_init)(void *ctx_ptr, hal_pin_inst_t *pin_ptr);
void (*rt_start)(void *ctx_ptr, hal_pin_inst_t *pin_ptr);
void (*frt_start)(void *ctx_ptr, hal_pin_inst_t *pin_ptr);
void (*rt_stop)(void *ctx_ptr, hal_pin_inst_t *pin_ptr);
void (*frt_stop)(void *ctx_ptr, hal_pin_inst_t *pin_ptr);
uint32_t ctx_size;
uint32_t pin_count;
} hal_comp_t;
pin和ctx
HAL是对硬件的抽象,因此对外的数据接口无论输入和输出都叫做pin(引脚)。而ctx则是HAL模块内部使用的“局部变量”,所有变量均为float类型。
为了对所有模块的变量进行统一管理,并且没有分配堆空间,无法进行动态内存分配,因此需要将变量个数作为一个属性存起来,在实例化模块时统一分配空间。方式是存在一个全局数组中(依次为所有HAL模块的数组,所有PIN的数组以及所有ctx的数组):
struct hal_comp_inst_t comp_insts[HAL_MAX_COMPS];
struct hal_pin_inst_t pin_insts[HAL_MAX_PINS];
uint8_t ctxs[HAL_MAX_CTX];
HAL模块间采用的是PIN和PIN连接的方式,因此需要用类似链表表示连接关系:
typedef struct hal_pin_inst_t {
float value;
struct hal_pin_inst_t *source;
} hal_pin_inst_t;
最后会形成一个复杂连线的网络:
任务调度
共三个调度:rt(real time)、frt(fast real time)、nrt(non real time)
//20kHz
void TIM_SLAVE_HANDLER(void) {
TIM_ClearITPendingBit(TIM_SLAVE, TIM_IT_Update);
hal_run_frt();
if(TIM_GetITStatus(TIM_SLAVE, TIM_IT_Update) == SET) {
hal_stop();
hal.hal_state = FRT_TOO_LONG;
}
}
//5 kHz interrupt for hal. at this point all ADCs have been sampled,
//see setup_res() in setup.c if you are interested in the magic behind this.
void DMA2_Stream0_IRQHandler(void) {
DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TCIF0);
hal_run_rt();
if(DMA_GetITStatus(DMA2_Stream0, DMA_IT_TCIF0) == SET) {
hal_stop();
hal.hal_state = RT_TOO_LONG;
}
}
int main(void) {
setup();
hal_init(0.0002, 0.00005);
while(1) //run non realtime stuff
{
hal_run_nrt();
//cdc_poll();
Wait(1);
}
}
优先级
所有的HAL模块都会有两个pin:
struct pin_ctx_t{
hal_pin_inst_t rt_prio;
hal_pin_inst_t frt_prio;
};
分别表示rt任务和frt任务的优先级,在函数:
void sort_rt();
void sort_frt();
中将项目中所有HAL模块的real time调度函数分别按优先级排序,存在全局变量中:
struct hal_comp_inst_t *rt_comps[HAL_MAX_COMPS];
struct hal_comp_inst_t *frt_comps[HAL_MAX_COMPS];
调用时只需按数组顺序即可。
HAL.h文件自动生成
HAL模块由一对.c文件和.h文件组成,但实际增加新模块时,只需在.c文件中加入标记,通过python脚本,自动生成.h文件。标记如下(以term模块为例):
HAL_COMP(term);
HAL_PINA(wave, 8);
HAL_PINA(offset, 8);
HAL_PINA(gain, 8);
HAL_PIN(send_step);
HAL_PIN(con);
其实这些标记都是一些空的宏:
#define HAL_COMP(name) //HAL模块
#define HAL_PIN(name) //PIN
#define HAL_PINA(name, index) //PIN数组
证明这些标记并不起实际作用,但在create_comp_h.py文件中找到了踪迹:
comp = re.search('COMP\((\w*)\);', line)
pin = re.search('HAL_PIN\((\w*)\)', line)
pin = re.search('HAL_PINA\((\w*),\s*(\d*)\)', line)
最终在makefile中可以找到:
inc/comps/%_comp.h: src/comps/%.c
@echo Generating H: $<
@$(MKDIR) -p $(dir $@)
@$(PYTHON) tools/create_comp_h.py $@ $<
这一系列自动生成头文件的操作确实值得学习!
PIN变量的使用
为了使得PIN在使用起来像变量且有固定格式(以term模块为例):
static void nrt_init(void *ctx_ptr, hal_pin_inst_t *pin_ptr) {
// struct term_ctx_t * ctx = (struct sim_ctx_t *)ctx_ptr;
struct term_pin_ctx_t *pins = (struct term_pin_ctx_t *)pin_ptr;
PIN(send_step) = 50;
for(int i = 0; i < TERM_NUM_WAVES; i++) {
PINA(gain, i) = 10;
}
}
直接使用PIN和PINA开起来十分清爽,但背后是个宏定义:
#define PIN(p) (pins->p.source->value)
#define PINA(p, i) (pins->p[i].source->value)
赋值时不赋给自己的value,而赋给source的,那么如果有其他PIN将它作为source该怎么办?阅读代码,发现在连接好所有PIN后,有一个relink的操作:
void hal_relink_pins(char *ptr){
for(int i = 0; i < hal.comp_inst_count; i++) {
for(int j = 0; j < hal.comp_insts[i].comp->pin_count; j++) {
hal.comp_insts[i].pin_insts[j].source = hal.comp_insts[i].pin_insts[j].source->source;
}
}
}
也就是说如故a -> b, b -> c,那么通过relink之后,变成了a -> c, b -> c,这样在给对b赋值时,只赋值给c,a也一样能拿到正确结果。
总结
虽然嵌入式C语言没有堆内存,并且面向过程,但是STMBL这个项目在内存管理和模块对象的把握十分精彩,还有很多细节值得学习。
此外,项目使用python脚本自动生成文件的方式使得项目文件的结构更加灵活,另外还采用了配置文件的方式来实例化和组织HAL模块,使得整个项目的新增和删减都极为灵活,除了头文件,还有其他文件同样是自动生成的,这一块内容我将继续更新。
刚开始看这个项目我一头雾水,抛开具体内容不谈,整体缺失了很多文件,无法连贯地读代码。后面慢慢读makefile才发现了其中的奥妙,顺便复习了makefile和python的知识,真的收获很大,也建议大家静下心来学习一下这个项目。