Main Content

Custom MATLAB Function Code Generation with CRL

This example demonstrates the workflow for replacing a user's custom MATLAB functions with a custom Code Replacement Library (CRL) intended for Qualcomm Hexagon™ hardware during code generation. It uses coder.replace to facilitate the replacement of the custom MATLAB function.

In this example, the custom function is an IIRInterpolator, which combines up-sampling with Biquad filtering. The entire MATLAB implementation of this function is replaced with mw_qhdsp_iirinterp_af, serving as a wrapper for the Qualcomm Hexagon™ SDK function qhdsp_iirinterp_af. The CRL is specifically tailored for Hexagon DSP v73 architecture version.

This example is ideal for users who have developed a custom MATLAB function and want to completely replace it with a manually implemented C function using a CRL. Alternatively, coder.ceval can be used to generate C code for a specific function call within MATLAB code.

  1. Using coder.ceval without creating any CRLs is suitable when you need to directly call external C functions from MATLAB code without setting up a full CRL. This approach allows for straightforward integration of external code but lacks the automation and broader replacement capabilities offered by a CRL. For more information, see coder.ceval.

  2. Using coder.ceval with CRLs provides a balance between direct external integration and leveraging the advanced features of CRLs. For more information, see Replace coder.ceval Calls to External Functions.

Prerequisites

Setup the Qualcomm Hexagon SDK. For more information, see Launch Hardware Setup.

Custom MATLAB Function Definition

Here, upsample_iir.m is the custom MATLAB function, and its signature should be defined to match the custom C function it is intended to replace during code generation.

% // Requires the states buffers to be 8 byte aligned
% // length shouldn't be more than 512/r
% // coefs - IIR filter coefficients - arranged as b0, b1, a1, b2, a2
%            and cascade for all biquad filters
%
% int32_t mw_qhdsp_iirinterp_af(float* in_samples,
%                               float* coefs,
%                               float* states,
%                               uint32_t num_biquads,
%                               uint32_t length,
%                               uint32_t r,
%                               float* out_samples);

Therefore, define the upsample_fir function to accept six input arguments and return two output arguments: one for the processed output and another for the updated states. Explicitly specify any buffer requiring specific alignment using coder.dataAlignment. Set coder.replace in the function definition for replacement with the appropriate implementation.

type upsample_iir.m
function [y,zi] = upsample_iir(x,h,zi,biquadSections,~,r,y)
%#codegen
coder.replace();
coder.dataAlignment('zi',8);

a = ones(3,1,'like',h);
b = zeros(3,1,'like',h);

biquadLength = 5;
sectionStatesLength = 2;

% copy input to output as per the 'r'
y(1:r:end) = x;

for i = 1:biquadSections
    idx = (i-1)*biquadLength;
    idx2 = (i-1)*sectionStatesLength;

    % split h to b,a = [(b0,b1,a1,b2,a2),(b0,b1,a1,b2,a2)]
    b(1) = h(idx+1);
    b(2) = h(idx+2);
    a(2) = h(idx+3);
    b(3) = h(idx+4);
    a(3) = h(idx+5);

    [y, zi(idx2+1:idx2+2)] = filter(b,a,y,zi(idx2+1:idx2+2)');
end

end

Currently, because the output length varies based on the upscaling factor (r), the coder treats the output as dynamically sized. The Code Replacement Library (CRL) does not support replacements for dynamically sized matrices. To work around this limitation, create a MATLAB wrapper function (like iirinterpolator.m) that generates an output buffer and uses it as both an input and output argument for the upsample_iir function.

type iirinterpolator.m
function [y,zi] = iirinterpolator(x,h,zi,r)
%#codegen
coder.dataAlignment('zi',8);

inFrameSize = size(x,1);
outFrameSize = inFrameSize*r;
channels = size(x,2);
sections = length(h)/5;

% Output buffer should be twice the size of input buffer
y = zeros(outFrameSize,channels,'like',x);

[y,zi] = upsample_iir(x,h,zi,uint32(sections),uint32(inFrameSize),uint32(r),y);

end

Creating CRL

Create a file named crl_table_iirinterpolator.m to define the function entries for a code replacement table. This table entry should specify the source file (mw_iirinterpolator.c), header file (mw_iirinterpolator.h), and any additional dependent header files and libraries required. Ensure that the dependent files and libraries are accessible within the Qualcomm Hexagon SDK, whose folder path can be retrieved using the command matlabshared.toolchain.hexagonllvm.getSDKFolder() after setting up and installing the Embedded Coder Support Package for Qualcomm Hexagon Processors.

For more information on creating the CRL entry, refer to Custom Function Code Replacement. Alternatively, this CRL can also be generated using Code Replacement Tool, a GUI way.

function hLib = crl_table_iirinterpolator()

hLib = RTW.TflTable;

% Get the Hexagon SDK root path
hexagonSDKFolder = matlabshared.toolchain.hexagonllvm.getSDKFolder();

% Get the source files path
srcPath = fullfile(fileparts(mfilename('fullpath')),'source');

% Get the additional dependency header file's path
additionalIncPaths = {...
    fullfile(hexagonSDKFolder,'libs','qhl','inc','qhdsp'),...    
    fullfile(hexagonSDKFolder,'libs','qhl','inc','qhblas'),...
    fullfile(hexagonSDKFolder,'libs','qhl','inc','qhcomplex'),...
    fullfile(hexagonSDKFolder,'libs','qhl','inc','qhmath'),...
    fullfile(hexagonSDKFolder,'libs','qhl','src','internal')};

% Get the v73 compiled library paths from the SDK's prebuilt location 
libPath = fullfile(hexagonSDKFolder, ...
    'libs','qhl','prebuilt','hexagon_toolv87_v73');

%---------- entry: upsample_iir ----------- 
hEnt = RTW.TflCFunctionEntry;
setTflCFunctionEntryParameters(hEnt, ...
  'Key', 'upsample_iir', ...                                    % MATLAB function to be replace
  'Priority', 100, ...                                          % Priority for replacement
  'ArrayLayout', 'COLUMN_MAJOR', ...                            % Layout of the array
  'ImplementationName', 'mw_qhdsp_iirinterp_af', ...            % C equivalent function implementation of MATLAB function
  'ImplementationHeaderFile', 'mw_iirinterpolator.h', ...       % Implementation header file
  'ImplementationHeaderPath', srcPath,...                       % Path to implementation source
  'ImplementationSourceFile', 'mw_iirinterpolator.c', ...       % Implementation source file
  'ImplementationSourcePath', srcPath,...                       % Path to implementation header
  'AdditionalIncludePaths', additionalIncPaths,...              % Additional header files path
  'AdditionalLinkObjs', ...
  {'libqhdsp.a','libqhdsp.a','libqhmath.a','libqhcomplex.a'},...% Libraries and its dependencies required by the above source file
  'AdditionalLinkObjsPaths', ...
  {libPath,libPath,libPath,libPath});                           % Path to the libraries mentioned above

%-------- Create the conceptual representation -----
% Conceptual match checks if this entry matches to the arguments
% and types of the upsample_iir.m

% Maximum allowed output is 512, due to "mw_qhdsp_iirinterp_af"
hEnt.createAndAddConceptualArg('RTW.TflArgMatrix',...
    'Name','y1',...
    'IOType','RTW_IO_OUTPUT',...
    'BaseType','single',...
    'DimRange',[1 1; 512 1]);

hEnt.createAndAddConceptualArg('RTW.TflArgMatrix',...
    'Name','y2',...
    'IOType','RTW_IO_OUTPUT',...
    'BaseType','single',...
    'DimRange',[1 1; Inf 1]);

% Maximum allowed input is 512
hEnt.createAndAddConceptualArg('RTW.TflArgMatrix',...
    'Name','u1',... % for input data
    'IOType','RTW_IO_INPUT',...
    'BaseType','single',...
    'DimRange',[1 1; 512 1]);

hEnt.createAndAddConceptualArg('RTW.TflArgMatrix',...
    'Name','u2',... % for coefficients
    'IOType','RTW_IO_INPUT',...
    'BaseType','single',...
    'DimRange',[1 1; 1 Inf]);

hEnt.createAndAddConceptualArg('RTW.TflArgMatrix',...
    'Name','u3',... % for states
    'IOType','RTW_IO_INPUT',...
    'BaseType','single',...
    'DimRange',[1 1; Inf 1]);

arg = getTflArgFromString(hEnt, 'u4','uint32');
arg.IOType = 'RTW_IO_INPUT';
addConceptualArg(hEnt, arg);

arg = getTflArgFromString(hEnt, 'u5','uint32');
arg.IOType = 'RTW_IO_INPUT';
addConceptualArg(hEnt, arg);

arg = getTflArgFromString(hEnt, 'u6','uint32');
arg.IOType = 'RTW_IO_INPUT';
addConceptualArg(hEnt, arg);

hEnt.createAndAddConceptualArg('RTW.TflArgMatrix',...
    'Name','u7',... % for output buffer
    'IOType','RTW_IO_INPUT',...
    'BaseType','single',...
    'DimRange',[1 1; Inf 1]);

%-------- Create the implementation representation -----
arg = hLib.getTflArgFromString('unused', 'int32');
arg.IOType = 'RTW_IO_OUTPUT';
hEnt.Implementation.setReturn(arg);

arg = hLib.getTflArgFromString('u1',['single' '*']);
arg.IOType = 'RTW_IO_INPUT';
arg.Type.ReadOnly = true;
hEnt.Implementation.addArgument(arg);

arg = hLib.getTflArgFromString('u2',['single' '*']);
arg.IOType = 'RTW_IO_INPUT';
arg.Type.ReadOnly = true;
hEnt.Implementation.addArgument(arg);

% Add states to implementation along with alignment requirements
arg = hLib.getTflArgFromString('u3',['single' '*']);
arg.IOType = 'RTW_IO_INPUT';
alignmentDesc = RTW.ArgumentDescriptor;
alignmentDesc.AlignmentBoundary = 8;
arg.Descriptor = alignmentDesc;
hEnt.Implementation.addArgument(arg);

% Update number of elements in do_match
arg = hLib.getTflArgFromString('u4','uint32');
hEnt.Implementation.addArgument(arg);

arg = hLib.getTflArgFromString('u5','uint32');
hEnt.Implementation.addArgument(arg);

arg = hLib.getTflArgFromString('u6','uint32');
hEnt.Implementation.addArgument(arg);

arg = hLib.getTflArgFromString('y1',['single' '*']);
arg.IOType = 'RTW_IO_OUTPUT';
hEnt.Implementation.addArgument(arg);

hLib.addEntry(hEnt);

end

Create an rtwTargetInfo file to register the code replacement library. Registration creates a code replacement library by defining the library name, code replacement tables, and other information. Create a registration file by using these specifications:

  1. Data Alignment for Code Replacement.

  2. Hardware device type, which should be enabled for Qualcomm Hexagon hardware only.

  3. CRL library name, which is QHL v73 Functions.

function rtwTargetInfo(tr)
% rtwTargetInfo function to register a code
% replacement library (CRL) for using with codegen

% Register the CRL defined in local function locCrlRegFcn
tr.registerTargetInfo(@locCrlRegFcn);

end % End of RTWTARGETINFO

function thisCrl = locCrlRegFcn

% Instantiate a CRL registry entry
thisCrl = RTW.TflRegistry;

% create an alignment specification object
as = RTW.AlignmentSpecification;
as.AlignmentType = {'DATA_ALIGNMENT_LOCAL_VAR', ...
    'DATA_ALIGNMENT_GLOBAL_VAR', ...
    'DATA_ALIGNMENT_STRUCT_FIELD'};
as.AlignmentSyntaxTemplate = '__attribute__((aligned(%n)))';
as.SupportedLanguages={'c', 'c++'};

% add the alignment specification object
da = RTW.DataAlignment;
da.addAlignmentSpecification(as);
da.RequireAlignmentOfMatrixElements = false;
da.PreferDataCopyToFailAlignment = true;

% add the data alignment object to target characteristics
tc = RTW.TargetCharacteristics;
tc.DataAlignment = da;

% Define the CRL properties
thisCrl.Name = 'QHL v73 Functions';
thisCrl.Description = 'Demonstration of custom function replacement';
thisCrl.TableList = {'crl_table_iirinterpolator.m'};
thisCrl.TargetHWDeviceType = {'Qualcomm->Hexagon'};
thisCrl.TargetCharacteristics = tc;

end % End of LOCCRLREGFCN

Add the CRL to the MATLAB path.

addpath("crl_iir"); % Needs to be updated as per the CRL location

Refresh registration information. At the MATLAB command line, enter:

RTW.TargetRegistry.getInstance('reset'); 

Generating Code and PIL Verification

Before generating the code, get the reference output by run in MATLAB for the following inputs.

%% Test parameters
% Input random data
inputData = rand(48000,1,'single');

% IIR coefficients
h = single([
    0.238129, 0.476258, -0.267690, 0.238129, 0.281005, ...
    0.238129, 0.476258, -0.267690, 0.238129, 0.281005, ...
    0.238129, 0.476258, -0.267690, 0.238129, 0.281005, ...
]);

% Interpolation Factor
interpolationFactor = 2;

inputBlockLength = 240;
inputLength = length(inputData);
numBiquads = length(h)/5; % Each biquad section has 5 coefficients
outputBlockLength = inputBlockLength*interpolationFactor;
inputBlocks = floor(inputLength/inputBlockLength);

Fragment the entire input into frames of length 240 and loop across the function to get the reference.

% Test MATLAB implementation

% Create the states buffer
ziRef = zeros(numBiquads*2, 1,'like',inputData); % Each biquad has 2 states
% Create the reference output buffer
refData = zeros(length(inputData)*2,1,'like',inputData);

for i = 1:inputBlocks
    in = inputData((i-1)*inputBlockLength+1:(i-1)*inputBlockLength+inputBlockLength);

    % Reference
    [ref,ziRef] = iirinterpolator(in,h,ziRef,interpolationFactor);

    % Accumulate the reference for comparison
    refData((i-1)*outputBlockLength+1:(i-1)*outputBlockLength+outputBlockLength) = ref;
end

Configure the following properties in the coder.config to generate the code.

% code-generation configurations
cfg = coder.config('lib','ecoder',true);
cfg.Hardware = coder.Hardware('Qualcomm Hexagon Simulator');
cfg.Hardware.ProcessorVersion = 'V73';
cfg.GenerateCodeReplacementReport = true;
cfg.CodeReplacementLibrary = 'QHL v73 Functions';
cfg.VerificationMode = "PIL";

% Generate the code
codegen -config cfg iirinterpolator -args {rand(inputBlockLength,1,'single'), h, ziRef, coder.Constant(interpolationFactor)} -launchreport
### Connectivity configuration for function 'iirinterpolator': 'Hexagon Simulator'
Code generation successful: View report

Code replacements can be verified by viewing the Code Replacements Report.

Verify the generated code against the MATLAB implementation output.

% Test PIL

% Create the states buffer
zi = zeros(numBiquads*2, 1,'like',inputData); % Each biquad has 2 states
% Create the output buffer
outputData = zeros(length(inputData)*2,1,'like',inputData);

for i = 1:inputBlocks
    in = inputData((i-1)*inputBlockLength+1:(i-1)*inputBlockLength+inputBlockLength);

    % PIL simulation
    [out,zi] = iirinterpolator_pil(in,h,zi,interpolationFactor);

    % Accumulate the PIL output for comparison
    outputData((i-1)*outputBlockLength+1:(i-1)*outputBlockLength+outputBlockLength) = out;
end
### Starting application: 'codegen\lib\iirinterpolator\pil\iirinterpolator.elf'
    To terminate execution: clear iirinterpolator_pil

Inf-norm can be used to verify the numerical accuracy of PIL output with the MATLAB reference output.

fprintf("The inf-norm difference between the PIL output and reference output is %2.8f\n",norm(outputData-refData,inf));
The inf-norm difference between the PIL output and reference output is 0.00000012

Code Replacement of MATLAB Function in Simulink

The replacements can also be used in Simulink using MATLAB Function block. Where a mask can be created to define the tuning and non-tuning parameters of this function. For more information, see Use Data in Multiple MATLAB Function Blocks by Defining Parameter Variables.

The function block creates states using persistent variables and processes input data and coefficients through ports to compute the output. You can refer to the model implementation by opening the model using this command:

open_system('IIRInterpolationUsingCRL')

More About