在嵌入式领域,MATLAB非常适合进行算法原型开发和验证,C代码适合部署到最终目标产品平台。符合一定要求的MATLAB代码也可以通过MATLAB Coder生成C代码。
在实际研发过程中,一般同时存在MATLAB代码和C代码。为了提升整体研发效率,往往会使用MATLAB和C代码混合编程。本文介绍使用MATLAB自带的calllib等接口调用C代码的方式。
一般来说,首先使用MATLAB进行原型开发。
相对成熟,且对性能有一定要求的模块,使用C代码实现。其来源可能是手写代码,也可能是MATLAB Coder或Simulink Coder或Embedded Coder生成的C代码。
这些代码按照实际运行平台的特点,封装为相应的库。例如,Windows平台的DLL库,Linux平台的so库等。
MATLAB仿真时,可以调用这些C代码的库。
被调C代码为计算一个数组的均值和标准差,如下所示:
/* 头文件 */ typedef struct S_DEMO_IN { double n; double *arr; } DemoIn; typedef struct S_DEMO_RESULT { double mean; double std; } DemoRes; void demo_mean_std(DemoRes *pRes, double *arr, DemoIn *pIn);
实现文件
/* 实现文件 */ extern "C" _declspec(dllexport) void demo_mean_std(DemoRes *pRes, double *arr, DemoIn *pIn) { int N_std; pRes->mean = 0.0; pRes->std = 0.0; N_std = 0; for (int i = 0; i < pIn->n; i++) { N_std = N_std + 1; pRes->std = pRes->std + (N_std - 1) * (pIn->arr[i] - pRes->mean) * (pIn->arr[i] - pRes->mean) / N_std; pRes->mean = pRes->mean + (pIn->arr[i] - pRes->mean) / N_std; } if (N_std > 1) { pRes->std = sqrt(pRes->std / (N_std - 1)); } else { pRes->std = 0.0; } for (int i = 0; i < pIn->n; i++) { if (pIn->arr[i] > pRes->mean + 3.0 * pRes->std) { arr[i] = pRes->mean + 3.0 * pRes->std; } else if (pIn->arr[i] < pRes->mean - 3.0 * pRes->std) { arr[i] = pRes->mean - 3.0 * pRes->std; } else { arr[i] = pIn->arr[i]; } } return; }
将上述代码编译为动态链接库,设库文件名为“Dll_demo.dll”,头文件名为“Dll_demo.h”。
打开MATLAB(本文使用的是MATLAB 2021a),将Dll_demo.dll和Dll_demo.h所在路径,或将这两个文件拷贝到工作路径。
编写MATLAB指令,首先,定义所需的库名、头文件名、函数名及涉及的类型名:
slib_name = 'Dll_demo'; shead_name = 'Dll_demo.h'; stype_in_name = 'S_DEMO_IN'; stype_res_name = 'S_DEMO_RESULT'; sfunc_name = 'demo_mean_std';
使用loadlibrary接口,载入库函数。为了避免重复载入,需要先检查是否已被载入。
if not(libisloaded(slib_name)) [m1, m2] = loadlibrary(slib_name, shead_name); disp(m1); disp(m2); end
可以使用下列命令查看载入的库的函数接口:
libfunctions ( slib_name , '-full' );
对于本文的例子,MATLAB的输出如下:
C函数demo_mean_std的三个参数均为指针型,MATLAB语言本身无法直接支持指针类型,因此,它将其视为特殊的xxxPtr类型。
C使用指针类型可以实现输入参数同时作为输出参数,而MATLAB不支持输入参数同时作为输出参数。因此,在MATLAB视角下,该函数有三个返回值。
接下里,需要使用MATLAB指令构造输入参数。
para_out.mean = 0; para_out.std = 0; s_para_res = libstruct(stype_res_name, para_out);
arr = [17, 124, 1, 8, 15, 23, 5, 7, 14, 16, 4, 6, 13, 20, 22, 10, 12, 19, 21, 3, 11, 18, 25, 2, 9]; ptr_arr = libpointer('doublePtr', arr); para_in.n = length(arr); para_in.arr = ptr_arr; s_para_in = libstruct(stype_in_name, para_in);
arr2 = zeros(length(arr), 1); ptr_arr2 = libpointer('doublePtr', arr2);
使用calllib接口调用C函数:
[out_ret, out_arr, ~] = calllib(slib_name, sfunc_name, s_para_res, ptr_arr2, s_para_in); out_mean = out_ret.mean; out_std = out_ret.std;
C代码中结构体指针型的输出参数pRes直接被解析为MATLAB结构体,double指针型输出参数则被解析为MATLAB数组。
注意,在C语言层面,out_arr和ptr_arr2其实是同一个参数,因此,out_arr的维数和ptr_arr2完全一致。
使用clear指令清零空间,使用unloadlibrary指令释放库:
clear s_para_in; clear s_para_res; clear ptr_arr; clear ptr_arr2; unloadlibrary(slib_name);
注意,所有使用libpointer、libstruct指令定义的MATLAB变量都应当使用clear释放。
为了方便使用,可以将上述脚本封装为MATLAB函数
function [out_mean, out_std, out_arr, m1, m2] = call_c_demo(arr) slib_name = 'Dll_demo'; shead_name = 'Dll_demo.h'; stype_in_name = 'S_DEMO_IN'; stype_res_name = 'S_DEMO_RESULT'; sfunc_name = 'demo_mean_std'; % 载入库文件 if not(libisloaded(slib_name)) [m1, m2] = loadlibrary(slib_name, shead_name); else m1 = cell(0,0); m2 = 'already loaded ...'; end % 构造参数 ptr_arr = libpointer('doublePtr', arr); para_in.n = length(arr); para_in.arr = ptr_arr; s_para_in = libstruct(stype_in_name, para_in); arr2 = zeros(length(arr), 1); ptr_arr2 = libpointer('doublePtr', arr2); para_out.mean = 0; para_out.std = 0; s_para_res = libstruct(stype_res_name, para_out); % 调用库中的函数 [out_ret, out_arr, ~] = calllib(slib_name, sfunc_name, s_para_res, ptr_arr2, s_para_in); out_mean = out_ret.mean; out_std = out_ret.std; % 释放空间 clear s_para_in; clear s_para_res; clear ptr_arr; clear ptr_arr2; end
注意,封装为函数之后,没有调用unloadlibrary接口。这是为了提升运行效率,多次调用时,避免反复加载/释放带来的开销。MATLAB开发者可以在确认不会再使用该库时,再使用unloadlibrary接口释放C库。
调用示例如下:
arr = [17, 124, 1, 8, 15, 23, 5, 7, 14, 16, 4, 6]; [out_mean, out_std, out_arr, m1, m2] = call_c_demo(arr); disp(out_mean); disp(out_std); disp(out_arr); disp(m1); disp(m2);
由图中标红和标蓝的信息可知,调用结果正确。
MATLAB提供了单元测试框架matlab.unittest.TestCase,使用该框架,可以更规范、更方便地对封装的MATLAB函数进行测试。
classdef TestDemoC < matlab.unittest.TestCase properties slib_name = 'Dll_demo'; shead_name = 'Dll_demo.h'; end methods(TestMethodSetup) function loadDemoLib(test) if not(libisloaded(test.slib_name)) [m1, m2] = loadlibrary(test.slib_name, test.shead_name); disp(m1); disp(m2); end end end methods(TestMethodTeardown) function releaseDemoLib(test) if libisloaded(test.slib_name) disp('release library'); unloadlibrary(test.slib_name); end end end % 测试用例 methods (Test) % 用例1: 边界情况, 仅一个元素 function tes01(test) arr = 17; [out_mean, out_std, out_arr, ~, ~] = call_c_demo(arr); test.verifyEqual(out_mean, arr); test.verifyEqual(out_std, 0.0); test.verifyEqual(out_arr, arr); end % 用例2: 一般情况 function tes02(test) arr = [17, 24, 1, 8, 15, 23, 5, 7, 14, 16, 4, 6]; [out_mean, out_std, out_arr, ~, ~] = call_c_demo(arr); test.verifyEqual(out_mean, mean(arr)); test.verifyEqual(out_std, std(arr)); test.verifyEqual(out_arr, arr); end % 用例3: 一般情况 - 存在异常值, 被限幅 function tes03(test) arr = [17, 124, 1, 8, 15, 23, 5, 7, 14, 16, 4, 6]; arr2 = [17, 120.1626, 1, 8, 15, 23, 5, 7, 14, 16, 4, 6]; [out_mean, out_std, out_arr, ~, ~] = call_c_demo(arr); test.verifyEqual(out_mean, mean(arr)); test.verifyEqual(out_std, std(arr)); test.verifyEqual(out_arr, arr2, 'AbsTol', 1.0e-5); end end end