Mapping MDF Channels to Simulink Model Input Ports

This example shows you how to programmatically map MDF channels and consume their data via input ports of a Simulink model. It performs the gathering of input port names of a Simulink model and correlates them to the contents of a given MDF file. A linkage between them is then created which consumes channel data sourced from the MDF file when the model runs.

Acquire Model Details

Define the example model name and open it.

mdlName = 'ModelForMDFInput';
open_system(mdlName);

Use the createInputDataset function to obtain overall information about the model and its inputs.

dsObj = createInputDataset(mdlName)
dsObj = 
Simulink.SimulationData.Dataset '' with 2 elements

                             Name      BlockPath 
                             ________  _________ 
    1  [1x1 timeseries]      triangle  ''       
    2  [1x1 struct    ]      busInput  ''       

  - Use braces { } to access, modify, or add elements using index.

Obtain Model Input Port Names

This model has both a bus and an individual input port. The helperGetMdlInputNames function demonstrates how to get the name of all the model inputs regardless of how they are defined in the model.

mdlInputNames = helperGetMdlInputNames(mdlName)
mdlInputNames = 4×1 string array
    "triangle"
    "pwm"
    "pwm_level"
    "pwm_filtered"

Investigate the MDF File

Now that you have the input port names of the model, you can see what channels exist in the MDF file so you can attempt to match them. The channelList function of the MDF feature allows quick access to the available channels present in an MDF file.

mdfName = 'CANape.MF4';
mdfObj = mdf(mdfName);
mdfChannelInfo = channelList(mdfObj)
mdfChannelInfo=120×9 table
            ChannelName            ChannelGroupNumber    ChannelGroupNumSamples    ChannelGroupAcquisitionName    ChannelGroupComment    ChannelDisplayName    ChannelUnit    ChannelComment                    ChannelDescription                 
    ___________________________    __________________    ______________________    ___________________________    ___________________    __________________    ___________    ______________    ___________________________________________________

    "ampl"                                 2                       199                        100ms                      100ms                   ""               100ms           100ms         "Amplitude of channel 1-3"                         
    "channel1"                             2                       199                        100ms                      100ms                   ""               100ms           100ms         "FLOAT demo signal (sine wave)"                    
    "Counter_B4"                           1                      1993                        10 ms                      10 ms                   ""               10 ms           10 ms         "Single bit demo signal (bit from a byte shifting)"
    "Counter_B5"                           1                      1993                        10 ms                      10 ms                   ""               10 ms           10 ms         "Single bit demo signal (bit from a byte shifting)"
    "Counter_B6"                           1                      1993                        10 ms                      10 ms                   ""               10 ms           10 ms         "Single bit demo signal (bit from a byte shifting)"
    "Counter_B7"                           1                      1993                        10 ms                      10 ms                   ""               10 ms           10 ms         "Single bit demo signal (bit from a byte shifting)"
    "map1_8_8_uc_measure"                  1                      1993                        10 ms                      10 ms                   ""               10 ms           10 ms         "8*8 fixed axis,  permanently morphing"            
    "map1_8_8_uc_measure[0][0]"            1                      1993                        10 ms                      10 ms                   ""               10 ms           10 ms         "8*8 fixed axis,  permanently morphing"            
    "map1_8_8_uc_measure[0][1]"            1                      1993                        10 ms                      10 ms                   ""               10 ms           10 ms         "8*8 fixed axis,  permanently morphing"            
    "map1_8_8_uc_measure[0][2]"            1                      1993                        10 ms                      10 ms                   ""               10 ms           10 ms         "8*8 fixed axis,  permanently morphing"            
    "map1_8_8_uc_measure[0][3]"            1                      1993                        10 ms                      10 ms                   ""               10 ms           10 ms         "8*8 fixed axis,  permanently morphing"            
    "map1_8_8_uc_measure[0][4]"            1                      1993                        10 ms                      10 ms                   ""               10 ms           10 ms         "8*8 fixed axis,  permanently morphing"            
    "map1_8_8_uc_measure[0][5]"            1                      1993                        10 ms                      10 ms                   ""               10 ms           10 ms         "8*8 fixed axis,  permanently morphing"            
    "map1_8_8_uc_measure[0][6]"            1                      1993                        10 ms                      10 ms                   ""               10 ms           10 ms         "8*8 fixed axis,  permanently morphing"            
    "map1_8_8_uc_measure[0][7]"            1                      1993                        10 ms                      10 ms                   ""               10 ms           10 ms         "8*8 fixed axis,  permanently morphing"            
    "map1_8_8_uc_measure[1][0]"            1                      1993                        10 ms                      10 ms                   ""               10 ms           10 ms         "8*8 fixed axis,  permanently morphing"            
      ⋮

Construct a Table to Manage Items of Interest

Use a table to map the model input ports to MDF channels.

channelTable = table();
channelTable.PortNames = mdlInputNames;
n = size(channelTable.PortNames,1);
channelTable.ChGrpNum = NaN(n,1);
channelTable.ChNameActual = strings(n,1);
channelTable
channelTable=4×3 table
      PortNames       ChGrpNum    ChNameActual
    ______________    ________    ____________

    "triangle"          NaN            ""     
    "pwm"               NaN            ""     
    "pwm_level"         NaN            ""     
    "pwm_filtered"      NaN            ""     

Perform Input Port to Channel Matching

The helperReportChannelInfo function searches the MDF file for channel names that match the model input port names. When found, the details of the channel are recorded in the table. Specifically, the channel group number where the given channel is in the file and its actual defined name. Note that the actual channel names are not exact matches to the model port names. In this example, the channel name matching is performed case-insensitive and ignores the underscore characters. This algorithm can be adapted as needed based on application-specific matching criteria.

channelTable = helperReportChannelInfo(channelTable, mdfChannelInfo)
channelTable=4×3 table
      PortNames       ChGrpNum    ChNameActual 
    ______________    ________    _____________

    "triangle"           1        "Triangle"   
    "pwm"                1        "PWM"        
    "pwm_level"          1        "PWM_Level"  
    "pwm_filtered"       1        "PWMFiltered"

Populate the Simulink Dataset Object with MDF Channel Data

The dataset object created earlier contains both a single timeseries object and a structure of timeseries objects. This makes assigning data back to them somewhat challenging. Things to keep in mind include:

  • When specifying 'TimeSeries' as the return type from the MDF read function, you must call read separately for each channel.

  • Because the dataset object has dissimilar elements (a scalar timeseries and a scalar structure of timeseries objects), you need to manually manage the collection and make sure you are writing to the correct location.

for ii = 1:dsObj.numElements
    switch ii
        case {1} % [1x1 timeseries], triangle
            % Read the input port data from the MDF file one channel at a time.
            mdfData = read(mdfObj, channelTable.ChGrpNum(ii), channelTable.ChNameActual(ii), 'OutputFormat', 'TimeSeries');
            % Populate the dataset object.
            dsObj{ii} = mdfData;
            
        case {2} % [1x1 struct], busInput
            for jj = 1:numel(fieldnames(dsObj.getElement(ii)))
                % Read the input port data from the MDF file one channel at a time.
                mdfData = read(mdfObj, channelTable.ChGrpNum(jj+1), channelTable.ChNameActual(jj+1), 'OutputFormat', 'TimeSeries');
                % Populate the dataset object.
                dsObj{ii}.(channelTable.PortNames{jj+1}) = mdfData;                
            end
    end
end
dsObj
dsObj = 
Simulink.SimulationData.Dataset '' with 2 elements

                             Name      BlockPath 
                             ________  _________ 
    1  [1x1 timeseries]      Triangle  ''       
    2  [1x1 struct    ]      busInput  ''       

  - Use braces { } to access, modify, or add elements using index.

Enable the Dataset as Input to the Simulink Model

set_param(mdlName, 'LoadExternalInput', 'on');
set_param(mdlName, 'ExternalInput', 'dsObj');

Run the Model

Upon executing the model, note that the MDF channel data properly maps to the designated input ports and plots through Simulink as expected.

open_system(mdlName);
bp = find_system(mdlName, 'BlockType', 'Scope');
open_system(bp);
pause(1)
set_param(mdlName, 'SimulationCommand', 'start');

Helper Functions

function mdlInputNames = helperGetMdlInputNames(mdlName)
% helperGetMdlInputNames Find input port names of a Simulink model.
%
% This function takes in the name of a Simulink model and returns the names of each model input. This specific model has 
% both a bus and a stand-alone input port going into it. To drive an input port that expects a bus means you need to supply 
% the signals as timeseries objects in a struct that matches the structure of the bus object attached to the input port.

% Test to see if the model is currently loaded in memory.
isLoaded = bdIsLoaded(matlab.lang.makeValidName(mdlName));

% If the model is not open then load it.
if ~isLoaded
    load_system(mdlName);
end

dsObj = createInputDataset(mdlName);
numElements = dsObj.numElements;
isStruct = zeros(1:numElements);

% Check to see if any of the elements in the returned dataset object are
% structs. If they are, assume they are for an input port that accepts a bus.
for elementIdx = 1:numElements
    isStruct(elementIdx) = isa(dsObj.getElement(elementIdx),'struct');
end

% For a port that accepts a bus, the data to be loaded must be arranged in a struct
% that matches the structure of the bus object attached to the input port.
busInportIdx = 1;
for idx = 1:numElements
    if isStruct(idx)
        % Get names of signals from a bus input port.
        inPortsBus(busInportIdx, :) = string(fieldnames(dsObj.getElement(idx)));
    else
        % Get signal name from a non-bus input port.
        inPorts(idx) = string(dsObj.getElement(idx).Name);
    end
end

mdlInputNames = [inPorts, inPortsBus]';
end


function channelTableOut = helperReportChannelInfo(channelTableIn, mdfChannelInfo)
% channelTableOut Reports if a channel is present in a set of channel names.

% Assign the output data.
channelTableOut = channelTableIn;

% Remove underscores and make everything lowercase for matching.
inPortChannelNames = lower(erase(channelTableIn.PortNames,'_'));
mdfChannelNames = lower(erase(mdfChannelInfo.ChannelName,'_'));

% Match the input channel names to the channel names in the MDF file.
[~, inPortidx] = ismember(inPortChannelNames, mdfChannelNames);

% Assign the relevant information back to the channel table.
channelTableOut.ChGrpNum = mdfChannelInfo{(inPortidx), {'ChannelGroupNumber'}};
channelTableOut.ChNameActual = mdfChannelInfo{(inPortidx), {'ChannelName'}};
end