slhal 硬件无关层(HAL)设计与实现
本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内容。
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/