2020-03-24 16:03:07 +08:00
2020-03-17 14:55:41 +08:00
2020-03-18 09:52:09 +08:00
2020-03-18 09:52:09 +08:00
2020-03-18 09:52:09 +08:00

slhal 硬件无关层(HAL)设计与实现

pipeline status

本Simulink库对飞控连接的各种设备进行抽象,提出一套硬件无关层标准化接口,使得上层simulink飞控开发者无需关心具体硬件。

设计思想概述

使用S-function Builder协助生成接口模板

HAL的接口模块都是masked S-function。为了免去手工编写.c/.tlc代码引入错误,项目使用了S-function Builder协助生成接口模板。

每个HAL接口库文件都有对应的*_bld.slx库,其用于存放对应的s-function builder的模块。 所有s-function模块的的PW、输入、输出、参数等接口定义都由图形化定义生成,大大简化了编写s-function的过程,自动生成了标准的mex接口代码、TLC代码完全无需修改,只需要关注产生的包裹函数模板_wrapper.c。

硬件移植的一般过程

HAL接口是通过s-function实现与飞控硬件接口的调用。只需要将template目录复制到新目录下,并在*_wrapper.c模板文件的基础上根据硬件配置实现其具体功能,最终在飞控代码生成中指向新目录(addpath),即可生成包含此飞控系统接口代码的完整程序。

wrapper函数需要实现的功能

*_Start_wrapper()函数用于设备初始化、可配置参数的注册与绑定。

*_Outputs_wrapper()函数一般用于程序正常运行时的调用。还可以细分为三种被调用情况:

调用情况 说明
周期调用 通常情况,按固定周期被调用。常用于输入与输出。
在Function-Call中被调用 通常是被StateFlow状态机调用。常用于事件处理(Event)。
在initialize function中被调用 在所有*_Start_wrapper()后,也就是初始完成后,正式启动前被调用,因此也可称为后初始化(post_init)。
这种用法的特殊在于此时可以保证所有其他模块都初始化可用,另一个好处是该调用有输入inputs,而*_Start_wrapper()没有输入只有参数传入。

生成代码的执行流程见下图。

graph TD;
    inits --> period-call
    starts -.-> |call| call_start["*_Start_wrapper()"]
    inits -.-> |call| call_outputs["*_Outputs_wrapper()"]
    period-call -.-> |call| call_outputs
    func-call -.-> |call| call_outputs
    subgraph "initialize"
    starts["init"] --> inits["post-init"]
    end

    subgraph "step"
    func-call
    period-call --> period-call
    end

hal_api_uart库

hal_api_uart.slx实现了对串行流的抽象操作,分别是初始化配置(hal_uart_init)、读(hal_uart_in)与写(hal_uart_out)。

模块 功能 备注
hal_uart_init 初始化配置 可以配置串口波特率、读写FIFO长度等参数。
如果某FIFO长度为零,则表示这不是一个双向读写设备。
其内部是一个initialize function。 也就是说它的Outputs函数只会在整个系统Start后Step前调用。
hal_uart_in 读取串行流 从串行流中尽量读取数据到Data缓存,读取长度len不会超出Data的长度。
如果出错,ErrorCode返回非零值。
hal_uart_out 输出到串行流 Data是输出的缓存,可用长度为len。当seq号变化时表示有新数据需要从缓存输出。

模块依赖关系如下图。

    graph LR;
    hal_api_uart(hal_api_uart.slx) --> hal_uart_init
    hal_api_uart(hal_api_uart.slx) --> hal_uart_in
    hal_api_uart(hal_api_uart.slx) --> hal_uart_out
    hal_uart_init -- startup --> hal_uart_init_Start_wrapper --> initialize_uart_device
    hal_uart_init -- initialize --> hal_uart_init_Outputs_wrapper
    hal_uart_in -- startup --> hal_uart_in_Start_wrapper --> initialize_uart_device
    hal_uart_in -- step --> hal_uart_in_Outputs_wrapper
    hal_uart_out -- startup --> hal_uart_out_Start_wrapper --> initialize_uart_device
    hal_uart_out -- step --> hal_uart_out_Outputs_wrapper

hal_uart_init模块实现

hal_uart_init模块的功能在hal_uart_init_wrapper.c中实现。其中,hal_uart_init_Start_wrapper将在初始化时被调用,它会去调用一个单例函数initialize_uart_device。无论被调用几次(每个hal_api_uart库中模块初始化(start)都会去调用它),单例函数initialize_uart_device只应当初始化对应UART设备一次。

initialize_uart_device函数应该对设备初始化,但无需配置(进行默认配置也可以)。 它还应当实现对设备属性的绑定注册功能(利用slparm的param_mgr.h绑定设置/读取波特率、字长、中止位、校验位等的函数),这样稍后就可以让地面站配置其波特率;或者从NVRAM中读取参数配置其波特率等属性。

hal_uart_init_wrapper.c中的hal_uart_init_Outputs_wrapper函数被设计成在所有模块start后被初始化调用,这个函数应用于实现配置UART设备的默认波特率及FIFO空间大小。

hal_uart_in模块实现

hal_uart_in模块的功能应在hal_uart_in_wrapper.c中实现。 hal_uart_in_Start_wrapper用于调用单例初始化函数initialize_uart_device。

hal_uart_in_Outputs_wrapper用于实现从uart_id设备中读取数据到Data中,并返回本次读取的数据长度Data_len(读取数据长度最多不超过y_width)。 当未被正常初始化或其他读取异常状态时,返回非零值。

hal_uart_out模块实现

hal_uart_out模块的功能应在hal_uart_out_wrapper.c中实现。 hal_uart_out_Start_wrapper用于调用单例初始化函数initialize_uart_device。

hal_uart_out_Outputs_wrapper用于实现uart_id设备对应seq变化时,写入长度len(不超过u_width)数据到buff中。 当未被正常初始化或其他写入异常状态时,返回非零值。

hal_api_nvram库

hal_api_nvram.slx实现了对非易失存储设备(如FRAM/EEPRM/FLASH/SDMMC设备)的抽象操作,分别是读(hal_nvram_read)、写(hal_nvram_write)与取消(hal_nvram_cancel)。三模块都最终封装为function-call形式,因此应当使用StateFlow对其进行调用操作。

模块 功能 备注
hal_nvram_read 从nvram_id设备的StartAddress地址开始读取Length到Buffer中 ErrorCode返回负值为出错,返回正值为还有多少字节未读,返回零表示完成。
hal_nvram_write 从nvram_id设备的StartAddress地址开始从Buffer写Length字节 ErrorCode返回负值为出错,返回正值为还有多少字节未写,返回零表示完成。
hal_nvram_cancel 取消nvram_id设备的StartAddress地址开始的读写任务 必须nvram_id与StartAddress都匹配的作业才能取消

模块依赖关系如下图。

    graph LR;
    hal_api_nvram(hal_api_nvram.slx) --> hal_nvram_read
    hal_api_nvram --> hal_nvram_write
    hal_api_nvram --> hal_nvram_cancel
    hal_nvram_read -- startup --> hal_nvram_read_Start_wrapper --> initialize_nvram_device
    hal_nvram_read -- call --> hal_nvram_read_Outputs_wrapper
    hal_nvram_write -- startup --> hal_nvram_write_in_Start_wrapper --> initialize_nvram_device
    hal_nvram_write -- call --> hal_nvram_write_Outputs_wrapper
    hal_nvram_cancel -- startup --> hal_nvram_cancel_Start_wrapper --> initialize_nvram_device
    hal_nvram_cancel -- call --> hal_nvram_cancel_Outputs_wrapper

接口设计思路

其中initialize_nvram_device是单例函数,实现对nvram_id号的设备进行单次的初始化。 nvram_id号从0开始,可以表示单个FRAM/EEPRM/FLASH/SDMMC设备。 但更本质上,nvram_id号是某对NVRAM设备独立操作进程的序号。比如可以同时2个进程对SDMMC进行读写,那么即使只有1个SDMMC设备,也应该有两个nvram_id号,用于独立处理。

这组接口在实现上需要有一定技巧。 一般NVRAM的读写操作都需要一定时间,但该接口要求即时返回,不能长时间占用。 因此设计上应当是每个nvram_id号与一个NVRAM处理进程相关联,接口只用于触发读写任务、监控当前任务状态。 当前任务可以用nvram_id与start_address的组合进行标识。如果读写操作未完成,而新调用的start_address改变了,则应当认为返回-2(Ocuppied)拒绝新读写调用。

ErrorCode返回值 含义 NVRAM处理进程状态
-3 Canncelled 当前任务已经被取消,可以开始下次任务
-2 Ocuppied 其他任务正在执行中,无法开始任务
-1 Genral Err 一般性错误,未完成初始化等
0 Completed 当前任务已完成,可以开始下次任务
>0 Working 当前任务正在传输,请等待

此外,NVRAM设备一般是按块(页)读写的,而本接口设计上为了使用方便属于按段读写。因此应当在处理进程中设计缓存Buffer/Cache。 当写操作时,如果在同一块内写,则先写入块buffer中,到超时或开始写其他块时才真正开始写入。 当读操作时,先读取整个块到cache中,如果读取都在cache中则命中,否则再读对应块。

hal_nvram_read模块实现

hal_nvram_read模块的功能应在hal_nvram_read_wrapper.c中实现。 hal_nvram_read_Start_wrapper用于调用单例初始化函数initialize_nvram_device。

hal_nvram_read_Outputs_wrapper用于实现启动/监测nvram_id进程从start address开始读取长度len(不超过y_width)数据到buffer中。返回值是当前未读入字节数,零表示写入完成,小于零值表示错误。

hal_nvram_write模块实现

hal_nvram_write模块的功能应在hal_nvram_write_wrapper.c中实现。 hal_nvram_write_Start_wrapper用于调用单例初始化函数initialize_nvram_device。

hal_nvram_write_Outputs_wrapper用于实现启动/监测nvram_id进程从start address开始从buffer写长度len(不超过u_width)数据。返回值是当前未写入字节数,零表示写入完成,小于零值表示错误。

hal_nvram_cancel模块实现

hal_nvram_cancel模块的功能应在hal_nvram_cancel_wrapper.c中实现。 hal_nvram_cancel_Start_wrapper用于调用单例初始化函数initialize_nvram_device。

hal_nvram_cancel_Outputs_wrapper用于实现取消nvram_id进程从start address开始的读写作业。返回值是零表示完成取消,非零值表示错误。

sim模式

为了在仿真中验证使用NVRAM模块的逻辑是否正确,增加了sim模式。 系统根据VariantNVRAM变量选择是工作在sfun模式还是dummy(sim)模式。 在sim模式下,读写NVRAM变成了读写全局信号量nvram_sim。

使用generate_dummy_nvram函数可以在base workspace下生产Variant变量、全局信号量与初始值,控制仿真方式。 因此不同测试可以通过不同参数调用generate_dummy_nvram来选择不同的默认nvram内容。 此外,每次写操作时会记录当前的nvram,结果在nvram_sim_last变量中。

hal_api_ctrls库

hal_api_ctrls.slx实现了对输入输出设备的抽象操作。 模块简介如下:

模块 功能 备注
hal_pwm_out PWM输出 单位us,类型uint16
hal_pwm_in PWM输入 同上
hal_do 离散量输出 类型bool
hal_di 离散量输入 同上
hal_ao 离散量输出 最高16位DAC,类型uint16
hal_ai 离散量输入 最高16位ADC,类型uint16
hal_led_set 三色LED输出 类型Bus: LedColorMsg, rgb:0-255 uint8
hal_sbus_in Sbus接收机输入 类型Bus: SbusInMsg

模块依赖关系如下图。

    graph LR;
    hal_api_ctrls(hal_api_ctrls.slx) --> hal_pwm_out
    hal_api_ctrls --> hal_pwm_in
    hal_api_ctrls --> hal_do
    hal_api_ctrls --> hal_di
    hal_api_ctrls --> hal_ao
    hal_api_ctrls --> hal_ai
    hal_api_ctrls --> hal_led_set
    hal_api_ctrls --> hal_sbus_in
    hal_pwm_out --> hal_pwm_out_wrapper[hal_pwm_out_wrapper.c]
    hal_pwm_in --> hal_pwm_in_wrapper[hal_pwm_in_wrapper.c]
    hal_do --> hal_do_wrapper[hal_do_wrapper.c]
    hal_di --> hal_di_wrapper[hal_di_wrapper.c]
    hal_ao --> hal_ao_wrapper[hal_ao_wrapper.c]
    hal_ai --> hal_ai_wrapper[hal_ai_wrapper.c]
    hal_led_set --> hal_led_set_wrapper[hal_led_set_wrapper.c]
    hal_sbus_in --> hal_sbus_in_wrapper[hal_sbus_in_wrapper.c]
    hal_led_set_wrapper --> hal_led_set_bus[hal_led_set_bus.h]
    hal_sbus_in_wrapper --> hal_sbus_in_bus[hal_sbus_in_bus.h]
    hal_led_set_bus --> hal_api_busdef[hal_api_busdef.mat]
    hal_led_set_bus --> hal_api_busdef

hal_api_ctrls接口设计思路

对于PWM、DIO、AIO这些输入输出接口,不管是几个设备拼成的,每一类的通道需要统一编址。 如PWM输出可能一个时钟控制4个通道,另一个时钟控制2个通道,我们无需给每组编制一个设备号,而是统一从零开始编号(0、1、2、3、4、5、……)。 而访问这些通道的时候,可以从start_idx开始连续读写。 读写的长度取决于给定的输入输出缓存空间width与真实通道的长度(两者取短)。 如果真实能够的读写的长度小于缓存空间,应返回非零的ErrorCode。

hal_pwm_out模块实现

hal_pwm_out模块的功能应在hal_pwm_out_wrapper.c中实现。 hal_pwm_out_Start_wrapper用于调用单例初始化函数pwm_out_init。 pwm_out_init可以初始化PWM输出设置并绑定PWM周期设定与读取参数。

hal_pwm_out_Outputs_wrapper用于实现输出PWM指令到编号从start_idx到start_idx+u_width-1的通道。 ErrorCode返回值非零表示错误,一般是对应通道不存在。

hal_pwm_in模块实现

hal_pwm_in模块的功能应在hal_pwm_in_wrapper.c中实现。 hal_pwm_in_Start_wrapper用于调用单例初始化函数pwm_in_init。 pwm_in_init可以初始化PWM输入设置并绑定相关参数。

hal_pwm_in_Outputs_wrapper用于实现从编号start_idx到start_idx+u_width-1的通道读取PWM输入脉宽值。 ErrorCode返回值非零表示错误,一般是对应通道不存在。

hal_do模块实现

hal_do模块的功能应在hal_do_wrapper.c中实现。 hal_do_Start_wrapper用于调用单例初始化函数discrete_output_init。 discrete_output_init可以初始化离散量输出设置并绑定相关参数。

hal_do_Outputs_wrapper用于实现输出离散指令到编号从start_idx到start_idx+u_width-1的数字通道。 ErrorCode返回值非零表示错误,一般是对应通道不存在。

hal_di模块实现

hal_di_in模块的功能应在hal_di_wrapper.c中实现。 hal_di_Start_wrapper用于调用单例初始化函数discrete_input_init。 discrete_input_init可以初始化离散输入设置并绑定相关参数。

hal_di_Outputs_wrapper用于实现从编号start_idx到start_idx+u_width-1的通道读取离散量输入值。 ErrorCode返回值非零表示错误,一般是对应通道不存在。

hal_ao模块实现

hal_ao模块的功能应在hal_ao_wrapper.c中实现。 hal_ao_Start_wrapper用于调用单例初始化函数analog_output_init。 analog_output_init可以初始化DAC输出设置并绑定相关参数。

hal_ao_Outputs_wrapper用于实现输出模拟指令到编号从start_idx到start_idx+u_width-1的DAC通道。 ErrorCode返回值非零表示错误,一般是对应通道不存在,或者超量程。

hal_ai模块实现

hal_ai_in模块的功能应在hal_ai_wrapper.c中实现。 hal_ai_Start_wrapper用于调用单例初始化函数analog_input_init。 analog_input_init可以初始化ADC输入设置并绑定相关参数。

hal_ai_Outputs_wrapper用于实现从编号start_idx到start_idx+u_width-1的通道读取ADC采样值。 ErrorCode返回值非零表示错误,一般是对应通道不存在。

hal_sbus_in

hal_sbus_in模块的功能应在hal_sbus_in_wrapper.c中实现。 hal_sbus_in_Start_wrapper用于调用单例初始化函数sbus_input_init。 sbus_input_init可以初始化sbus输入设置并绑定相关参数。

hal_sbus_in_Outputs_wrapper用于实现根据id编号读取sbus信号。信号包括18通道的输入(uint16)、seq循环计数(收到新包就加一)、ErrorCode返回值非零表示未收到遥控器信号(对应遥控器接收机红灯)。

hal_led_set

hal_led_set是一个function-call模块,用于StateFlow设置三色LED 其功能应在hal_led_set_wrapper.c中实现。 hal_led_set_Start_wrapper用于调用单例初始化函数led_set_init。 led_set_init根据设备号id初始化相应设备并绑定参数。

hal_led_set_Outputs_wrapper用于根据id设置LED颜色。ErrorCode返回值非零表示错误,一般是对应设备不存在。

对于带pwm控制的三色LED,rgb通道取值范围在0-255;如果只是三色,则取值零与非零;如果是单色,则是any(rgb)。

LED长短闪烁颜色等功能可以在stateflow中实现。

hal_api_sens库

hal_api_sens.slx实现了对传感器设备(如IMU/INS/Baro/Radar)的抽象。抽象接口有两大类,一般性接口与通用接口。其中通用性接口设计借鉴了simulink中ROS接口设计思路。

一般设备接口设计与实现

每个模块根据其ID绑定一个具体设备,输出一个bus对象,通过ErrorCode返回读取状态。

传感器接口数据结构定义在hal_api.h中。对应的Bus对象定义在hal_api_busdef.m函数中,并通generate_hal_api_sldd.m脚本更新数据库hal_api.sldd。

在内部,*_Start_wrapper函数去调用初始化单例函数,保证多个模块读取相同设备时只初始一次。

现在实现的传感器读取模块见下表:

模块名称 对应设备
hal_6axis_imu 六轴IMU,包括三轴加速度计与三轴角速度陀螺
hal_3axis_magn 三轴磁力计
hal_pressure 气压计,包括差压与绝压
hal_gps GPS输入
hal_ins 组合惯导输入

hal_sen_read通用接口设计与实现

hal_sen_read模块用于根据信号名称获取值。 信号名称可以从交互界面中的下拉列表中查找。 而这个列表可以通过hal_sen_popup_update.m脚本进行更新。 信号可以是标量或向量,但当前的限制是类型必须是single。

hal_sen_read模块通过hal_sen_read_wrapper.c文件实现其功能。 hal_sen_read_Start_wrapper函数用于根据name名称建立关联信号量,并将关联信息储存在模块的pW中。

pW定义 类型 说明
pw[0] int 是否关联好。0-未关联;1-关联。
pw[1] PROP_TYPE 信号量类型
pw[2] int 信号量长度。
pw[3] void* 指向信号对应的地址指针或函数指针
pw[4] void* 传给函数指针对应函数第二个自定义参数

PROP_TYPE定义如下。ARRAY表示指向地址,FUNC表示指向函数。

PROP_TYPE enum序号
PROP_ARRAY_UINT8 1
PROP_ARRAY_INT8 2
PROP_ARRAY_UINT16 3
PROP_ARRAY_INT16 4
PROP_ARRAY_UINT32 5
PROP_ARRAY_INT32 6
PROP_ARRAY_REAL32 7
PROP_FUNC_UINT8 8
PROP_FUNC_INT8 9
PROP_FUNC_UINT16 10
PROP_FUNC_INT16 11
PROP_FUNC_UINT32 12
PROP_FUNC_INT32 13
PROP_FUNC_REAL32 14

pw[3]作为函数指针是类似float (*get_prop_f_func_ptr)(int index, void *paramter)形式,用于提取第index个信号分量。完整声明如下:

typedef int8_t (*get_prop_b_func_ptr)(int index, void *paramter);
typedef uint8_t (*get_prop_B_func_ptr)(int index, void *paramter);
typedef int16_t (*get_prop_h_func_ptr)(int index, void *paramter);
typedef uint16_t (*get_prop_H_func_ptr)(int index, void *paramter);
typedef int32_t (*get_prop_i_func_ptr)(int index, void *paramter);
typedef uint32_t (*get_prop_I_func_ptr)(int index, void *paramter);
typedef float (*get_prop_f_func_ptr)(int index, void *paramter);

pw[4]或者说是void *paramter是传入调用函数的自定义参数,可以表示为一个32位数字,也可以是函数、对象的指针。可类似于C++中的this指针使用。

此外,还可以在hal_sen_read_Start_wrapper中注册属性。 属性与信号都是名称与数值的关联,但用法上有明显的区别:

  • 属性表示的是设备的状态,而信号是设备源源不断产生的;
  • 属性是可以读可以写(设置)的,而信号量是只读的(可写的信号量是hal_api_ctrls里的输出模块);
  • 属性是标量(向量则变成标量组),而信号量可以是向量;
  • 属性量可以是整型或浮点类型(union),而信号量当前只能是浮点类型;

hal_sen_read_Outputs_wrapper函数用于提取信号向量,如果hal_sen_read_Start_wrapper中关联pW成功,则一般无需改动。其实现就是依次从地址或函数调用中提取信号诸元素并返回。

hal_api_general库

hal_api_general.slx实现了对最通用的接口抽象,其中包含了S-function hal_general_in/out。

hal_general_in/out模块用于根据名称调用注册的函数。 函数名称可以从交互界面中的下拉列表中查找。 而这个列表可以通过hal_general_popup_update.m脚本进行更新。

为了做到通用化,模块的输入输出都是不定长的uint8向量,其内容是打包的数据。 在simulink中,可以通过Pack模块将任意数据打包成uint8向量传入,通过Unpack模块将结果从uint8向量中解出。 当出现错误调用时,ErrorCode返回非零值。

理论上,HAL库中任意模块的功能都可以用hal_general_in/out为核心,加入pack输入、unpack输出,最后mask最外层的subsystem而成的模块代替。 但是特化的模块无需打包解包与函数嵌套,更为节省内存与CPU时间。 因此在实践上,如果设备属于HAL已经有的模块类别,应该去实现这些模块对应的模板文件,以提升效率与可移植性; 而一个平台特有的设备,则可以用此hal_general实现,然后通过pack/unpack/mask处理这个s-function形成该平台的特有HAL库。

hal_general模块通过hal_general_in/out_wrapper.c文件实现其功能。 hal_general_in/out_Start_wrapper函数用于根据name名称建立函数关联,并将关联信息储存在模块的pW中。 此外,还可以在关联前对设备进行初始化与参数绑定。

pW定义 类型 说明
pw[0] int 是否关联好。0-未关联;1-关联。
pw[1] genral_in/out_ptr 注册的函数
pw[2]以上 void * 函数参数。

hal_general_in/out_Outputs_wrapper函数根据pw[0]与pw[1]调用函数。作为一层壳,一般本函数无需修改。

测试

工程使用Gitlab-CI实现自动化测试。当前测试项目test/scriptTest.m是通过代码生成与编译hal_template.slx模型,检查接口设计正确性。


Copyright (C) 2020 GONG Zheng(matt@matthewgong.com) http://www.matthewgong.com/

S
Description
No description provided
Readme MIT 1.8 MiB
Languages
C 96.8%
MATLAB 3.1%