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.
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.
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"
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"
⋮
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 ""
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"
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.
set_param(mdlName, 'LoadExternalInput', 'on'); set_param(mdlName, 'ExternalInput', 'dsObj');
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');
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