Map Channels from MDF Files to Simulink Model Input Ports
This example shows you how to programmatically map channels from MDF files 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 content 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
(Simulink) function to obtain overall information about the model and its inputs. Specify the DatasetSignalFormat
option as "timetable"
to configure dataset signal elements as timetables.
dsObj = createInputDataset(mdlName, "DatasetSignalFormat", "timetable")
dsObj = Simulink.SimulationData.Dataset '' with 2 elements Name BlockPath ________ _________ 1 [2x1 timetable] 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 = 4x1 string
"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 mdfChannelInfo
function allows quick access to the available channels present in an MDF file.
mdfName = "CANape.MF4";
channelInfo = mdfChannelInfo(mdfName)
channelInfo=42×13 table
Name GroupNumber GroupNumSamples GroupAcquisitionName GroupComment GroupSourceName GroupSourcePath DisplayName Unit Comment ExtendedNamePrefix SourceName SourcePath
______________________________________ ___________ _______________ ____________________ ____________ _______________ _______________ ___________ ___________ _________________________________________________ __________________ ___________ __________
"Counter_B4" 1 1993 10 ms 10 ms <undefined> XCPsim "" <undefined> Single bit demo signal (bit from a byte shifting) XCPsim <undefined> XCPsim
"Counter_B5" 1 1993 10 ms 10 ms <undefined> XCPsim "" <undefined> Single bit demo signal (bit from a byte shifting) XCPsim <undefined> XCPsim
"Counter_B6" 1 1993 10 ms 10 ms <undefined> XCPsim "" <undefined> Single bit demo signal (bit from a byte shifting) XCPsim <undefined> XCPsim
"Counter_B7" 1 1993 10 ms 10 ms <undefined> XCPsim "" <undefined> Single bit demo signal (bit from a byte shifting) XCPsim <undefined> XCPsim
"PWM" 1 1993 10 ms 10 ms <undefined> XCPsim "" <undefined> Pulse width signal from PWM_level and Triangle XCPsim <undefined> XCPsim
"PWMFiltered" 1 1993 10 ms 10 ms <undefined> XCPsim "" <undefined> Low pass filtered PWM signal XCPsim <undefined> XCPsim
"PWM_Level" 1 1993 10 ms 10 ms <undefined> XCPsim "" <undefined> <undefined> XCPsim <undefined> XCPsim
"Triangle" 1 1993 10 ms 10 ms <undefined> XCPsim "" <undefined> Triangle test signal used for PWM output PWM XCPsim <undefined> XCPsim
"ampl" 2 199 100ms 100ms <undefined> XCPsim "" <undefined> Amplitude of channel 1-3 XCPsim <undefined> XCPsim
"channel1" 2 199 100ms 100ms <undefined> XCPsim "" <undefined> FLOAT demo signal (sine wave) XCPsim <undefined> XCPsim
"map1_8_8_uc_measure" 1 1993 10 ms 10 ms <undefined> XCPsim "" <undefined> 8*8 fixed axis, permanently morphing XCPsim <undefined> XCPsim
"syncArrayStruct.mem_charArray[000]" 2 199 100ms 100ms <undefined> XCPsim "" <undefined> <undefined> XCPsim <undefined> XCPsim
"syncArrayStruct.mem_charArray[001]" 2 199 100ms 100ms <undefined> XCPsim "" <undefined> <undefined> XCPsim <undefined> XCPsim
"syncArrayStruct.mem_doubleArray[000]" 2 199 100ms 100ms <undefined> XCPsim "" <undefined> <undefined> XCPsim <undefined> XCPsim
"syncArrayStruct.mem_doubleArray[001]" 2 199 100ms 100ms <undefined> XCPsim "" <undefined> <undefined> XCPsim <undefined> XCPsim
"syncArrayStruct.mem_floatArray[000]" 2 199 100ms 100ms <undefined> XCPsim "" <undefined> <undefined> XCPsim <undefined> XCPsim
⋮
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.GroupNumber = NaN(n,1); channelTable.ChannelName = strings(n,1); channelTable
channelTable=4×3 table
PortNames GroupNumber ChannelName
______________ ___________ ___________
"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, channelInfo)
channelTable=4×3 table
PortNames GroupNumber ChannelName
______________ ___________ _____________
"triangle" 1 "Triangle"
"pwm" 1 "PWM"
"pwm_level" 1 "PWM_Level"
"pwm_filtered" 1 "PWMFiltered"
Populate the Simulink Dataset Object with Channel Data
The dataset object created earlier contains both a single timetable and a structure of timetables. This makes assigning data back to them somewhat challenging. Things to keep in mind include:
Because the dataset object has dissimilar elements (a timetable and a structure of timetables), you need to manually manage the collection and make sure you are writing to the correct location.
The
mdfRead
function reads data as physical values by default. However, one channel of interest "PMW" containsValueToText
conversion. To make sure the raw numeric values are read to populate the dataset object, theReadRaw
option should be set totrue
.
for ii = 1:dsObj.numElements switch class(dsObj.getElement(ii)) case 'timetable' % Read the input port data from the MDF file one channel at a time. mdfData = mdfRead(mdfName, Channel=channelTable.ChannelName(ii), ReadRaw=true); % Populate the dataset object. dsObj{ii} = mdfData{1}; % 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. case 'struct' names = fieldnames(dsObj.getElement(ii)); for jj = 1:numel(names) % Find row index of this signal name in the channel table. rowIdx = find(channelTable.PortNames == names(jj)); % Read the input port data from the MDF file one channel at a time. mdfData = mdfRead(mdfName, Channel=channelTable.ChannelName(rowIdx), ReadRaw=true); % Populate the dataset object. dsObj{ii}.(channelTable.PortNames{rowIdx}) = mdfData{1}; end end end dsObj
dsObj = Simulink.SimulationData.Dataset '' with 2 elements Name BlockPath ________ _________ 1 [1993x1 timetable] 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 channel data from the MDF file 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) sim(mdlName, "TimeOut", 10);
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 standalone input port going into it. To drive an input port that expects a bus means you need to supply % the signals as timetables 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 % Initialize an empty string array to hold names of all input ports. If % your model has a very large number of inputs, consider preallocating the % string array for performance. mdlInputNames = string.empty; for idx = 1:numElements if isStruct(idx) % Get names of signals from a bus input port. mdlInputNames = [mdlInputNames; string(fieldnames(dsObj.getElement(idx)))]; %#ok<AGROW> else % Get signal name from a non-bus input port. mdlInputNames = [mdlInputNames; string(dsObj.getElement(idx).Name)]; %#ok<AGROW> end end end
function channelTableOut = helperReportChannelInfo(channelTableIn, channelInfo) % 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(channelInfo.Name,"_")); % 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.GroupNumber = channelInfo.GroupNumber(inPortIdx); channelTableOut.ChannelName = channelInfo.Name(inPortIdx); end