Creating a Custom Device Driver Block (Sink) - Carleton-SRCL/SPOT GitHub Wiki

A device driver is a special form of MATLAB System Block. This block generates custom C or C++ device driver code when it is deployed to the destination hardware board. These driver blocks allow users to easily develop code to access hardware board features. They allow you to directly write code in C++ or C and use existing APIs, which are then accessed by the MATLAB System Block. An added benefit of these types of blocks is that you can also write an equivalent MATLAB function that will run during any simulations when the C/C++ code can't be used.

Developing a device driver block falls into two categories:

  • Source blocks, which can read data from the destination hardware for further use in the Simulink diagram;
  • Sink blocks, which can take data from the Simulink diagram and pass it along to the destination hardware;

This guide will cover the development of a sink block, which is also referred to a "digital write" block by MathWorks. To demonstrate this process, we will develop a simple block that will send data to the Jetson:

Driver Folder Structure

The folder structure for a device driver block contains the following:

  • A System Object file;
  • A src folder;
  • An include folder;

These can be manually created by the user, or the user can run this simple command in the MALTAB command prompt to automatically create the folder structure, where the string 'my_driver' can be replaced with the name of your custom function. For our example, let's use:

codertarget.createDriverProject('showinput_file')

For this example, you can delete the "Source" file.

Writing Source Code

Next, create a new MATLAB file and write the following code, which will take one Simulink input (of data type double) and will print it to the terminal on the Jetson:

#include <iostream>
#include <iomanip>
#include <fstream>

#include "showinput_header.h"
using namespace std;

double display_input(double input_data)
{
   printf("Input data: %f\n",input_data);
}

Save the file as showinput_source.cpp, and move the file to the src folder.

Writing Header Code

Now, create another new MATLAB file and copy the following code. This will be the header file:

#include "rtwtypes.h"

double display_input();

Then, save the file as showinput_header.h and move it to the include folder.

Coding a System Object

We will now return to the top level of the folder structure and rename the Sink.m file. We will instead name it showinput.m; once it's been renamed, open the file. In this file, you'll need to rename everything that had "Sink" in it with the name of your file, which is now "showinput". The code should now look like this:

classdef showinput < matlab.System ...
        & coder.ExternalDependency ...
        & matlab.system.mixin.Propagates ...
        & matlab.system.mixin.CustomIcon
    %
    % System object template for a sink block.
    % 
    % This template includes most, but not all, possible properties,
    % attributes, and methods that you can implement for a System object in
    % Simulink.
    %
    % NOTE: When renaming the class name Sink, the file name and
    % constructor name must be updated to use the class name.
    %

    % Copyright 2016-2018 The MathWorks, Inc.
    %#codegen
    %#ok<*EMCA>

    properties
        % Public, tunable properties.
    end

    properties (Nontunable)
        % Public, non-tunable properties.
    end

    properties (Access = private)
        % Pre-computed constants.
    end

    methods
        % Constructor
        function obj = showinput(varargin)
            % Support name-value pair arguments when constructing the object.
            setProperties(obj,nargin,varargin{:});
        end
    end

    methods (Access=protected)
        function setupImpl(obj) %#ok<MANU>
            if isempty(coder.target)
                % Place simulation setup code here
            else
                % Call C-function implementing device initialization
                % coder.cinclude('sink.h');
                % coder.ceval('sink_init');
            end
        end

        function stepImpl(obj,u)  %#ok<INUSD>
            if isempty(coder.target)
                % Place simulation output code here 
            else
                % Call C-function implementing device output
                %coder.ceval('sink_output',u);
            end
        end

        function releaseImpl(obj) %#ok<MANU>
            if isempty(coder.target)
                % Place simulation termination code here
            else
                % Call C-function implementing device termination
                %coder.ceval('sink_terminate');
            end
        end
    end

    methods (Access=protected)
        %% Define input properties
        function num = getNumInputsImpl(~)
            num = 1;
        end

        function num = getNumOutputsImpl(~)
            num = 0;
        end

        function flag = isInputSizeLockedImpl(~,~)
            flag = true;
        end

        function varargout = isInputFixedSizeImpl(~,~)
            varargout{1} = true;
        end

        function flag = isInputComplexityLockedImpl(~,~)
            flag = true;
        end

        function validateInputsImpl(~, u)
            if isempty(coder.target)
                % Run input validation only in Simulation
                validateattributes(u,{'double'},{'scalar'},'','u');
            end
        end

        function icon = getIconImpl(~)
            % Define a string as the icon for the System block in Simulink.
            icon = 'showinput';
        end
    end

    methods (Static, Access=protected)
        function simMode = getSimulateUsingImpl(~)
            simMode = 'Interpreted execution';
        end

        function isVisible = showSimulateUsingImpl
            isVisible = false;
        end
    end

    methods (Static)
        function name = getDescriptiveName()
            name = 'showinput';
        end

        function b = isSupportedContext(context)
            b = context.isCodeGenTarget('rtw');
        end

        function updateBuildInfo(buildInfo, context)
            if context.isCodeGenTarget('rtw')
                % Update buildInfo
                srcDir = fullfile(fileparts(mfilename('fullpath')),'src'); %#ok<NASGU>
                includeDir = fullfile(fileparts(mfilename('fullpath')),'include');
                addIncludePaths(buildInfo,includeDir);
                % Use the following API's to add include files, sources and
                % linker flags
                %addIncludeFiles(buildInfo,'source.h',includeDir);
                %addSourceFiles(buildInfo,'source.c',srcDir);
                %addLinkFlags(buildInfo,{'-lSource'});
                %addLinkObjects(buildInfo,'sourcelib.a',srcDir);
                %addCompileFlags(buildInfo,{'-D_DEBUG=1'});
                %addDefines(buildInfo,'MY_DEFINE_1')
            end
        end
    end
end

The next step is to edit this object file. This can appear intimidating, but the structure is actually quite well laid out. These sections:

   properties
       % Public, tunable properties.
   end
   
   properties (Nontunable)
       % Public, non-tunable properties.
   end
   
   properties (Access = private)
       % Pre-computed constants.
   end

Allows you to create different types of constants that you can send to your generated code. The first block are parameters that will show up as "editable" when you open the block in Simulink. The second block is identical to the first one, but the parameters can only be viewed in Simulink, not edited. The last block contains any constants that the user should never see or change. These sections:

    methods (Access=protected)
        function setupImpl(obj) %#ok<MANU>
            if isempty(coder.target)
                % Place simulation setup code here
            else
                % Call C-function implementing device initialization
                % coder.cinclude('sink.h');
                % coder.ceval('sink_init');
            end
        end

        function stepImpl(obj,u)  %#ok<INUSD>
            if isempty(coder.target)
                % Place simulation output code here 
            else
                % Call C-function implementing device output
                %coder.ceval('sink_output',u);
            end
        end

        function releaseImpl(obj) %#ok<MANU>
            if isempty(coder.target)
                % Place simulation termination code here
            else
                % Call C-function implementing device termination
                %coder.ceval('sink_terminate');
            end
        end
    end

Contain the most critical functions: setupImpl, stepImpl, and releaseImpl. The first block will contain any functions that should only be run once during the initialization of the diagram. For example, this could be the function that opens a UDP communication port. The second block contains the functions that will be run once every time step. In our case, that will be the function we created that will print a double sent from Simulink to the Jetson via the terminal. The last block contains functions that should only be run once when the Simulink diagram is terminated. For example, this could be a function that closes a UDP communication port. In this example, we will not need any setup or release functions. To call the function we created, add the following code to the step block:

       function stepImpl(obj, u)   %#ok<INUSD>
           if isempty(coder.target)
               % Place simulation output code here
           else
               % Call C-function implementing device output
	   coder.cinclude('showinput_header.h');
	   coder.ceval('display_input',u);
           end
       end

Changing Block Inputs

You will also need to confirm that you have set the correct number of inputs and outputs for your function. In this case, we will only have one input for the block, but to add more inputs navigate to the following code block:

    methods (Access=protected)
        %% Define input properties
        function num = getNumInputsImpl(~)
            num = 1;
        end

        function num = getNumOutputsImpl(~)
            num = 0;
        end

        function flag = isInputSizeLockedImpl(~,~)
            flag = true;
        end

        function varargout = isInputFixedSizeImpl(~,~)
            varargout{1} = true;
        end

        function flag = isInputComplexityLockedImpl(~,~)
            flag = true;
        end

        function validateInputsImpl(~, u)
            if isempty(coder.target)
                % Run input validation only in Simulation
                validateattributes(u,{'double'},{'scalar'},'','u');
            end
        end

        function icon = getIconImpl(~)
            % Define a string as the icon for the System block in Simulink.
            icon = 'showinput';
        end
    end

To accept more then one input, the following changes would need to be made to the code:

methods (Access=protected)
    %% Define input properties
    function num = getNumInputsImpl(~)
        num = 3;
    end
    
    function num = getNumOutputsImpl(~)
        num = 0;
    end
    
    function flag = isInputSizeLockedImpl(~,~)
        flag = true;
    end
    
    function varargout = isInputFixedSizeImpl(~,~)
        varargout{1} = true;
    end
    
    function flag = isInputComplexityLockedImpl(~,~)
        flag = true;
    end
    
    function validateInputsImpl(~, u1, u2, u3)
        if isempty(coder.target)
            % Run input validation only in Simulation
            validateattributes(u1,{'double'},{'scalar'},'','u1');
            validateattributes(u2,{'double'},{'scalar'},'','u2');
            validateattributes(u3,{'double'},{'scalar'},'','u3');
        end
    end
    
    function icon = getIconImpl(~)
        % Define a string as the icon for the System block in Simulink.
        icon = 'MoveArm_Speed';
    end  
    
end

It's important to note here that you will also need to change the inputs to the step function, like so:

    function stepImpl(~, u1, u2, u3)  
        if isempty(coder.target)
            % Place simulation output code here 
        else
            % Call C-function implementing device output
            %coder.ceval('sink_output',u);
             coder.cinclude('dynamixel_sdk.h');
             coder.cinclude('dynamixel_functions.h');
             coder.ceval('command_dynamixel_speed',u1, u2, u3);
        end
    end

Editing the Build Information

The last thing you'll need to do is add the paths for the source and include directories, which is located in the last block of code:

    methods (Static)
        function name = getDescriptiveName()
            name = 'showinput';
        end

        function b = isSupportedContext(context)
            b = context.isCodeGenTarget('rtw');
        end

        function updateBuildInfo(buildInfo, context)
            if context.isCodeGenTarget('rtw')
                % Update buildInfo
                srcDir = fullfile(fileparts(mfilename('fullpath')),'src'); %#ok<NASGU>
                includeDir = fullfile(fileparts(mfilename('fullpath')),'include');
                addIncludePaths(buildInfo,includeDir);
                % Use the following API's to add include files, sources and
                % linker flags
                %addIncludeFiles(buildInfo,'source.h',includeDir);
                %addSourceFiles(buildInfo,'source.c',srcDir);
                %addLinkFlags(buildInfo,{'-lSource'});
                %addLinkObjects(buildInfo,'sourcelib.a',srcDir);
                %addCompileFlags(buildInfo,{'-D_DEBUG=1'});
                %addDefines(buildInfo,'MY_DEFINE_1')
            end
        end
    end

The default code should have the main include and src directories already defines, don't change this unless you have changed the structure of the device driver. This is also the code section where you would add any links for custom API; for example here is the build information for the Dynamixel actuators:

methods (Static)
    function name = getDescriptiveName()
        name = 'ReadArm_Position_Rates';
    end
    
    function b = isSupportedContext(context)
        b = context.isCodeGenTarget('rtw');
    end
    
    function updateBuildInfo(buildInfo, context)
        if context.isCodeGenTarget('rtw')
            
            % Update buildInfo
            srcDir = fullfile(fileparts(mfilename('fullpath')),'src');
            includeDir = fullfile(fileparts(mfilename('fullpath')),'include');
            addIncludePaths(buildInfo,includeDir);
            
            % Add all SOURCE files for compiling robotis software
            addSourceFiles(buildInfo,'protocol2_packet_handler.cpp', srcDir);
            addSourceFiles(buildInfo,'protocol1_packet_handler.cpp', srcDir);
            addSourceFiles(buildInfo,'port_handler_windows.cpp', srcDir);
            addSourceFiles(buildInfo,'port_handler_mac.cpp', srcDir);
            addSourceFiles(buildInfo,'port_handler_linux.cpp', srcDir);
            addSourceFiles(buildInfo,'port_handler_arduino.cpp', srcDir);
            addSourceFiles(buildInfo,'port_handler.cpp', srcDir);
            addSourceFiles(buildInfo,'packet_handler.cpp', srcDir);
            addSourceFiles(buildInfo,'group_sync_write.cpp', srcDir);
            addSourceFiles(buildInfo,'group_sync_read.cpp', srcDir);
            addSourceFiles(buildInfo,'group_bulk_write.cpp', srcDir);
            addSourceFiles(buildInfo,'group_bulk_read.cpp', srcDir);
            addSourceFiles(buildInfo,'dynamixel_functions.cpp', srcDir);
            
            % Add all INCLUDE files for compiling robotis software
            addIncludeFiles(buildInfo,'protocol2_packet_handler.h',includeDir);
            addIncludeFiles(buildInfo,'protocol1_packet_handler.h',includeDir);
            addIncludeFiles(buildInfo,'port_handler_windows.h',includeDir);
            addIncludeFiles(buildInfo,'port_handler_mac.h',includeDir);
            addIncludeFiles(buildInfo,'port_handler_linux.h',includeDir);
            addIncludeFiles(buildInfo,'port_handler.h',includeDir);
            addIncludeFiles(buildInfo,'packet_handler.h',includeDir);
            addIncludeFiles(buildInfo,'group_sync_write.h',includeDir);
            addIncludeFiles(buildInfo,'group_sync_read.h',includeDir);
            addIncludeFiles(buildInfo,'group_bulk_write.h',includeDir);
            addIncludeFiles(buildInfo,'group_bulk_read.h',includeDir);
            addIncludeFiles(buildInfo,'dynamixel_sdk.h',includeDir);
            addIncludeFiles(buildInfo,'port_handler_arduino.h',includeDir);
            addIncludeFiles(buildInfo,'dynamixel_functions.h',includeDir)
            
            %addLinkFlags(buildInfo,{'-lSource'});
            %addLinkObjects(buildInfo,'sourcelib.a',srcDir);
            %addCompileFlags(buildInfo,{'-D_DEBUG=1'});
            %addDefines(buildInfo,'MY_DEFINE_1')
        end
    end
end

At this point, you have created all the code required! The final step is to bring this code into MATLAB. Simply open a Simulink diagram, and drag in a MATLAB System block. Either drag and drop your System Object into the block, or click on the Simulink block and manually locate it.

⚠️ **GitHub.com Fallback** ⚠️