【STMBL学习笔记1】HAL模块的内容、连接和调度

STMBL是一个针对CNC机床和机器人改造的开源伺服驱动器,采用C语言编写,支持高电压和大功率。其软件架构基于简化版的LinuxCNCHAL,使用PIN和ctx变量进行模块间通信,并通过任务调度实现实时性和高效性。项目利用Python脚本自动生成HAL模块的头文件,实现灵活的模块管理和扩展。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是STMBL

STMBL 是一款基于 STM32F4 系列的开源伺服驱动器,专为 CNC 机床和机器人改造而设计。它支持高达 320V 和 2kW 的工业交流和直流伺服。项目所用语言为C语言(少见的纯C语言项目)。
STMBL驱动器
该驱动器可以通过类似于LinuxCNC所使用的HAL(hardware abstraction layer)模块对各种命令和反馈类型进行配置。
我在这里不想谈其中的驱动算法,而是想说说这个项目的软件架构。

HAL(硬件抽象层)

这里所说的HAL并非STM32的HAL库,而是LinuxCNC中“子模块”的概念,类似于梯形图的功能块和Simulink中的框图。
LinuxCNC模块连接图
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;

最后会形成一个复杂连线的网络:
graphviz生成的HAL连线图

任务调度

共三个调度: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的知识,真的收获很大,也建议大家静下心来学习一下这个项目。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

苏打豆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值