MATLAB S-functions – file source and sink blocks

We'll develop the simplest possible S-functions to enable our models to communicate with the application we described earlier: a file-source block and a file-sink block.

These S-functions will have only one port and be able to read/write a scalar real signal from/to a file. The file path will be passed as a parameter; and the files will have only one line containing the new signal value.

The sink block, called filesink_msfun, will receive the input and convert it to a string that will be written to the file. The file path is passed as parameter, no DWork vector is needed because we don't have to output a default value.

The source block, called filesource_msfun, will read one line from the file, attempt to convert it into a real number, and output it. When the file is not readable, the last line is empty, or an error occurs, the last valid value will be used. This means that we'll have to use one DWork vector, and one more parameter for the initial output.

In the following paragraphs we will walk through a detailed description of their implementation, beginning with the simpler one, the filesink_msfun block.

The filesink_msfun block

This block has the purpose of writing the input signal to a text file. Since the text file will be read by the external application, we can't open it once the simulation starts and close it when the simulation ends; otherwise, the application may have problems in reading it. Therefore, the file has to be opened and closed at each time step, in the Outputs callback.

Let's open an empty model and save it as msfun_test.slx.

We'll immediately place the Level-2 MATLAB S-Function block from the Simulink | User-Defined Functions blockset in our model:

The filesink_msfun block

Double-clicking on the block will open the usual parameters window, where we need to replace the S-function name parameter with filesink_msfun:

The filesink_msfun block

Let's click on the Edit button; since the filesink_msfun.m script file does not exist yet, Simulink will ask if we want to open the editor or locate the file. Choose the Open editor option to launch the MATLAB editor with an empty file, which we'll immediately save as filesink_msfun.m.

Tip

A quick note to Linux users: the editor is using Emacs-like shortcuts by default (for example, Ctrl + W/Ctrl + Y to copy/paste). If you're not using Emacs, you may want to switch to Windows-like shortcuts (for example, Ctrl + C/Ctrl + V to copy/paste) by opening the Preferences window and navigating to Keyboard | Shortcuts. The first item, Active settings, will let you choose between Emacs or Windows behavior.

The MATLAB code

First and foremost, we need to define the S-function entry point, that is, a function with the same name as the script file itself (minus the extension), calling the mandatory setup callback.

%% S-function entry point
function filesink_msfun(block)
setup(block);

When Simulink updates or simulates the model, it will call filesink_msfun, passing as argument one Simulink.MSFcnRunTimeBlock object containing every data about the block itself. This will, in turn, run the setup callback.

The second function we must implement is the setup callback:

%% First required callback: setup
function setup(block)

The first line in this snippet of code is a comment, followed by the function definition taking the block object as argument.

Let's set the simplest block properties first: the number of parameters, input ports, and output ports.

block.NumDialogPrms  = 1; % Number of parameters
block.NumInputPorts  = 1; % Number of inports
block.NumOutputPorts = 0; % Number of outports

In order to edit them, it's sufficient to assign the desired values to the block object's attributes. In this case, we specify that the S-function will accept only one parameter. Being a sink block, it has one input port and no output port.

We must now configure the input port:

% Set input port properties as inheritedblock.SetPreCompInpPortInfoToDynamic;

% Override some properties: scalar real input
block.InputPort(1).Dimensions = 1;
block.InputPort(1).DatatypeID = 0; % double
block.InputPort(1).Complexity = 'Real';
block.InputPort(1).SamplingMode = 'Inherited';
block.InputPort(1).DirectFeedthrough = 1;

We begin by initializing the first input port properties (dimensions, datatype, complexity, and sampling mode) to be inherited from the driving block. Remember that the elements in a vector are accessed with one-based indexes.

Then we explicitly set the properties to make the input port a one-dimensional double-precision real number. The sampling mode is set as inherited from the previous block, but (unless you have the DSP System ToolboxTM product installed) the only possible value is Sample. Finally we set the DirectFeedthrough property in order to execute this block after the driving one.

Tip

We could have used the instruction block.SetPreCompPortInfoToDefaults that would have configured the port to accept a real scalar sampled signal with a double datatype.

Since we don't have to configure any output port, we proceed by defining the sample times:

% Set the sample time and the offset time.
%  [0 offset]            : Continuous sample time
%  [positive_num offset] : Discrete sample time
%  [-1, 0]               : Inherited sample time
%  [-2, 0]               : Variable sample time
block.SampleTimes = [-1 0];

This parameter accepts a vector: the first element is the sampling time period and the second is the initial offset time (both are expressed in seconds).

The possible combinations are as follows:

  • [0 Y]: For a continuous sample time with Y offset, meaning that a new sample will be acquired every time step
  • [X Y]: For a discrete sampling period of X seconds with Y offset
  • [-1 0]: To inherit the sample time from the driving block
  • [-2 0]: To declare a variable sample time, useful only with variable-step solvers

Here we set the sample time as inherited from the driving block (-1). This block will be executed immediately after the driving one.

The next step is to define how this block should behave when the user tries to save or restore a simulation state:

% Specify the block simStateCompliance. The allowed values are:
%    'UnknownSimState', < The default setting; warn and assume DefaultSimState
%    'DefaultSimState', < Same sim state as a built-in block
%    'HasNoSimState',   < No sim state
%    'CustomSimState',  < Has GetSimState and SetSimState methods
%    'DisallowSimState' < Error out when saving or restoring the model sim state
block.SimStateCompliance = 'HasNoSimState';

Since this block, not having a DWork vector, has no simulation state to save or restore, we use the 'HasNoSimState' constant. Another valid option would have been 'DefaultSimState', which would end up doing nothing anyway.

Tip

To understand what a simulation state is, and when it could be useful, refer to this page in the Documentation center: Simulink | Simulation | Run Simulation | Interactive | Save and Restore Simulation State as SimState.

The default setting is 'UnknownSimState', which is equivalent to 'DefaultSimState', with a warning issued when the user attempts to save or restore the simulation state.

Now every block property has been defined and Simulink should be theoretically able to do a model update. But there's one information missing to complete the initialization phase. We have to register every other implemented callback at the end of the setup function.

Since we'll need the mandatory Outputs callback, we'll register it now:

block.RegBlockMethod('Outputs', @Outputs); % Required

The RegBlockMethod function tells Simulink that the required callback ('Outputs') is implemented by another function (@Outputs). We could have called that function Foo and used block.RegBlockMethod('Outputs', @Foo)to register it. It's not mandatory to use the same name as the callback, but it's always better to keep things simple.

The final step is to implement the Outputs function's required callback:

%% Second required callback: Outputs
function Outputs(block)
% open the file as write-only
fid = fopen(block.DialogPrm(1).Data, 'w'), 
% print input port value to file
fprintf(fid, '%f', block.InputPort(1).Data);
% close the file
fclose(fid);

We're retrieving the value of the first parameter from the DialogPrm vector: it contains the filename.

The file is then opened in write mode using fopen, which returns the file identifier stored in fid.

fprintf will print to the file pointed by fid one line, that is, the number in fixed-point notation. This number is the signal we got from the first input port (accessed the same way as the parameter).

Finally, we're closing the file with fclose.

Tip

File operations are found in the documentation center of the MATLAB | Data and File Management | Data Import and Export section.

The script is complete. Save the filesink_msfun.m script and close the S-function block properties dialog window. You'll notice that the block appearance has changed, the output port has disappeared, and the script filename is shown without the extension:

The MATLAB code

Let's develop the corresponding source block now.

The filesource_msfun block

The filesource_msfun block has the purpose of reading its output signal from a text file, passed as parameter. Like we discussed before, the file can't be left open since the external application will be using it too. Therefore, we have to open and close the file at each time step.

Moreover, we have to provide a fallback value if the file isn't readable. That fallback will be the last valid value stored in a DWork vector. An initial value needs to be set via another parameter.

Repeating the same steps seen above, we add another Level-2 MATLAB S-function to the msfun_test.slx model, and start developing the new script that will be called filesource_msfun.m.

The MATLAB code

The S-function entry point, as we saw before, must have the same name as the file and must execute the setup callback:

%% S-function entry point
function filesource_msfun(block)
setup(block);

The mandatory setup callback will define the block characteristics, telling Simulink there are two parameters and one scalar, real output:

%% First required callback: setup
function setup(block)
block.NumDialogPrms  = 2; % Number of parameters
block.NumInputPorts  = 0; % Number of inports
block.NumOutputPorts = 1; % Number of outports

% Set the default properties to all ports
block.SetPreCompPortInfoToDefaults;

This time we won't inherit the port characteristics from other blocks; we will just set the default options.

% Set the sample time and the offset time.
block.SampleTimes = [0 0];

The same applies to the sample time. We want the block to be run at each time step, so we're using the continuous sample time option with zero offset time.

% Specify the block simStateCompliance.
block.SimStateCompliance = 'DefaultSimState';

This block will have one DWork vector. By using 'DefaultSimState', we'll instruct Simulink to save it into the simulation state.

Since we're using a DWork vector, we must register the optional callbacks (PostPropagationSetup) to define it, set its initial value (Start), and perform its update (Update) after the outputs have been calculated:

block.RegBlockMethod('PostPropagationSetup',@PostPropagationSetup);
block.RegBlockMethod('Start', @Start);
block.RegBlockMethod('Outputs', @Outputs); % Required
block.RegBlockMethod('Update', @Update);

Now we have to implement the callbacks, using the same order Simulink will call them with.

The first callback is the one that sets the DWork vector properties:

%% First optional callback: PostPropagationSetup
function PostPropagationSetup(block)

We must define how many DWork vectors we need. Since the variable that we need to store is the last valid output and there's only one output port, we need only one DWork vector:

block.NumDworks = 1;
block.Dwork(1).Name = 'lastValue'; % required!
block.Dwork(1).Dimensions = 1;
block.Dwork(1).DatatypeID = 0; % double
block.Dwork(1).Complexity = 'Real';
block.Dwork(1).Usage = 'DWork';

That vector is accessed using a one-based index. We define the vector to be one-dimensional, able to store a double datatype representing a real number. We're not interested in logging facilities, so we use the generic usage type 'DWork' (that is the default, so we could safely omit it).

Tip

The DWork vector requires the Name property to be set!

It's common practice to declare one DWork vector for each port; this way the indexes don't change and it's easy to develop and maintain the code.

The DWork vector has to be initialized before the simulation loop; this is done with the Start callback:

%% Second optional callback: Start
function Start(block)
block.Dwork(1).Data = block.DialogPrm(2).Data;

We just store the second parameter, which holds the initial output, inside the first DWork vector data.

The other required callback, Outputs, implements the logic that reads the source file:

%% Second required callback: Outputs
function Outputs(block)
% open file as readonly
fid = fopen(block.DialogPrm(1).Data, 'r'), 
% read one line
tline = fgetl(fid);
if (tline == -1) % fail
    % output last value
    block.OutputPort(1).Data = block.Dwork(1).Data;
    return
end
% convert to double
output = str2double(tline);
% check that the output is a valid number
if (isnan(output)) % fail
    % output last value
    block.OutputPort(1).Data = block.Dwork(1).Data;
    return
end
% output the read value
block.OutputPort(1).Data = output;
fclose(fid);

The file, whose path is the first parameter of the S-function, is opened in read-only mode with fopen. The fgetl function will attempt to read a line from the file. If the line is valid, the str2double function will parse it, putting the result into the output variable. If that variable actually contains a valid number, it is sent to the first output port. Finally the file is closed with fclose.

If something fails, the number stored in the first DWork vector will be sent instead, and the file is closed.

The final Update callback, called immediately after the Outputs callback, is the one that will update the DWork vector with the last output value:

%% Third optional callback: Update
function Update(block)
block.Dwork(1).Data = block.OutputPort(1).Data;

We're now done. Save the filesource_msfun.m script and check that the S-function block changes, having only the output port and displaying the new S-function name.

A quick test

To check that everything is working, let's add to the msfun_test.slx model a Sine wave, a Mux, and a Scope. Connect the blocks as shown in the following screenshot:

A quick test

Set the filesource_msfun parameter to 'testfile' 0, and the filesink_msfun parameter to 'testfile' (don't forget the single quotation marks around the filename), then set the Fixed-Step Discrete solver with a time step of 0.1 seconds in the Model configuration parameters window (Ctrl + E) and run the simulation.

By double-clicking on the Scope block we should see the following result:

A quick test

Good! The S-functions are working as expected: the filesink_msfun block has created the file named testfile in the workspace and written the Sine wave signal output to it. Meanwhile, the filesource_msfun is reading the value from the same file. A comparison of the two waves, shown in the third graph, shows that they are coincident.

Note

If we changed the direct feedthrough setting of the filesink_msfun block input to 0, we would see the second wave being late by 0.1 seconds. This is because Simulink will not know that the Sine Wave block must be executed before the filesink_msfun block.

The block execution order can be viewed in the model window by selecting the Display | Blocks | Sorted Execution Order menu item.

We're almost ready for the real thing.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.0.85