Code covered by the BSD License  

Highlights from
Eurotherm Modbus RS232 Control

Eurotherm Modbus RS232 Control

by

 

11 Jul 2009 (Updated )

Reads and writes information to Eurotherm controllers via Modbus RTU protocols.

tempobjcomm.m
function response = tempobjcomm(type, serialObject, deviceAddress, parameterAddress, value)
% TEMPOBJCOMM Reads and writes data from Eurotherm temperature controllers
% tempobjcomm(type, serialObject, deviceAddress, parameterAddress, value)
% writes or reads information from Eurotherm series 2000 and 3000
% temperature controllers connected via RS232 serial ports. type is 'read',
% 'readfullres', 'write', or 'writefullres', serialObject is the serial
% port object, deviceAddress the address of the temperature controller
% (Eurotherms are set to 1 by default), which must be an integer from 0-247
% (0 is a broadcast, and no response will be received for a broadcast, this
% is also only possible for write operations), parameterAddress is the
% parameter to be read/written.  If type == 'read' or 'readfullres', the
% value is the number of items to be read from that parameterAddress, e.g.
% if parameterAddress is 1, then items = 2 reads parameters 1 and 2
% together (must be an integer from 1 to 125).  If type == 'write' or
% 'writefullres', values writes that number to the parameter (integer:
% 0-65535) (type = 'write'). If more than one value is supplied as a
% vector, then each one is written to the parameter address, e.g. [5, 10]
% would write 5 to address 2, and 10 to address 3. If reading a value, the
% function responds with the requested value or values in a horizontal
% vector if successfully carried out.  For both read and write operations,
% the function errors if there was no response (unless it was a broadcast
% write operation using deviceAddress = 0).  Note that the 2400 series
% temperature controllers use temperatures that are read and written as
% multiplied by 10 and no decimal place, i.e. 249 is 24.9 C (the 3000
% series do not).  In either case, it is advisable to use the readfullres
% and writefullres options to avoid this problem if the scaling factor is
% not known.

% If using the 'readfullres' and 'writefullres' options, there is no
% scaling involved for either negative numbers, or for decimal places,
% since these are returned as "single" precision (actually the response
% format is double, but the precision is single as it comes from the
% Eurotherm).  The fullres commands should only be used with commands with
% full floating point data, e.g. set temperatures, or program times.
% Although arguments may be double precision, they will be automatically
% rounded to single precision.

% The write commands use the Modbus function code 16 (write n words), and
% the read commands all use function code 3 (read n words), as per the
% recommended approach by Eurotherm.


% Normal usage:

% e.g. 1 tempobjcomm('read', serialObject, 1, 1, 2) - reads from device 
% 1 (Eurotherm default) - gives process variable (current
% temperature) and set point (as two parameters were requested starting
% from parameter 1) e.g. [243, 249].  For a 2400 series controller, this
% would represent 24.3 and 24.9 C respectively.

% e.g. 2 tempobjcomm('readfullres', serialObject, 1, 2, 1) reads the set
% point from the controller - but with full precision, e.g. 24.31294...

% e.g. 3 tempobjcomm('write', serialObject, 1, 2, 400) writes a set
% temperature of 40 C to the deviceAddress 1 (this is for Eurotherm 2400
% controllers only - you would use 40 instead as the input argument for
% other controllers).

% e.g. 4 tempobjcomm('writefullres', serialObject, 1, 2, 40.0569) writes a
% set temperature of 40.0569 C to the deviceAddress 1.

% e.g. 5 tempobjcomm('write', serialObject, 0, 2, 0) BROADCASTS to all
% devices on the attached port

% e.g. 6 tempobjcomm('write', serialObject, 0, 2, [0, 0]) writes 0 to
% parameters 2 AND 3.


% Range:

% type = 'read', 'readfullres', 'write', or 'writefullres'

% serialObject = valid serial object, usually generated by tempobj

% deviceAddress = unsigned integer: 0-254 (although the Modbus
% specification officially only allows 0-247)

% parameterAddress = unsigned integer: 1-65535

% value = if not supplied for read commands, defaults to 1, errors for
% write commands if not supplied.  For read commands, this must be a
% scalar, but for write commands it can be a vector.
%           2200 series: 'read' - 1-32, 'readfullres' - 1-16 2400 series:
%           'read' - 1-125, 'readfullres' - 1-62
% For both cases, 'write' only accepts 0-65535 integers, but 'writefullres'
% accepts any double precision real number (it is rounded to single
% precision internally).  If a vector of values is supplied for the write
% commands, they will be written to all the parameters contiguous to the
% one supplied, i.e. [0, 0] to parameterAddress 2 will write 0 to both
% address 2 AND 3 (see example 6 above).

% for more see... Communications protocols:
% http://lrs.eurotherm.com/Content/downloadABRV.aspx?url=http://www.eurothe
% rm.com/library/products/controllers/2000_series/manuals/s2kc_026230_2.pdf
% &i=Misc-Cnt

% 2416 manual
% http://lrs.eurotherm.com/webforms/softwaredownloadsABRV.asp?url=http://ww
% w.eurotherm.com/library/products/controllers/2000_series/2400/2416/2416_H
% A025041_10.pdf&i=2416

% 3216 manuals
% http://www.eurotherm.com/products/controllers/series_3000/3216_doc.htm


% defines the currently acceptable valid commands
validTypes = {'read', 'readfullres', 'write', 'writefullres'};

% Error handling of arguments as arguments must be a number and in the
% correct range. Text can still be classed as an acceptable uint8/16 object
% so both logical tests are necessary
if nargin < 4
    % must have supplied all of the arguments
    error('Insufficient input arguments.')
    
elseif ~isvector(type) || ~any(strcmp(type, validTypes))
    % can only have these types of reading and writing - isvector checks
    % for correct dimensions, and strcmp simulataneously checks for
    % suitability as a string
    error('Type must be "read", "readfullres", "write" or "writefullres".')
    
elseif ~isvalidtempobj(serialObject) || ~strcmp(get(serialObject, 'Status'), 'open')
    % serial object must be valid and open
    error('SerialObject must be a valid open serial object.')
    
elseif ~isvalidtempobjdeviceaddress(deviceAddress)
    % device address must be 0 (broadcast mode write only), or 1-254
    error('DeviceAddress must be an unsigned integer from 1 to 254 (0 is also allowed for write operations only).')
    
elseif all(~strfind(type, 'write')) && ~deviceAddress
    % if the type is not "write" or "writefullres", then the deviceAddress
    % cannot be 0
    error('DeviceAddress cannot be 0 if not writing data.')
    
elseif ~isvalidtempobjparameteraddress(parameterAddress)
    % parameter address must be 0 to 65535 (a number that can be expressed
    % in 16-bits)
    error('ParameterAddress must be an integer from 1 to 65535.')
    
elseif any(strfind(type, 'read')) && nargin == 5 && (~isnumeric(value) || ~isscalar(value) || ~isreal(value) || value < 1)
    
    if strcmp(type, 'read') && value > maxWords
        % limitations on the maximum number of items that you can read from
        % the controller
        error('For type == "read" value (if supplied) must be an unsigned integer from 1 to %d.', maxWords)
        
    elseif strcmp(type, 'readfullres') && value > fix(maxWords / 2)
        % limitations on the maximum number of items that you can read from
        % the controller - readfullres uses twice as many bytes, so fewer
        % values can be read
        error('For type == "read" value (if supplied) must be an unsigned integer from 1 to %d.', fix(maxWords / 2))
    end
    
elseif any(strfind(type, 'write')) && nargin == 4
    % if a write operation is specified, a value must be supplied
    error('For write operations, a value to be written must be specified.')
    
elseif any(strfind(type, 'write')) && (~isnumeric(value) || sum(size(value) > 1) > 1)
    
    % for writing, value must a scalar or vector of numbers
    if strcmp(type, 'write') && (numel(value) > maxWords || (65535 - parameterAddress + 1) < numel(value) || all(value ~= uint16(value)))
        % writable parameters must be 16-bit integers (scalars or vectors)
        % (also tests out for imaginary and NaN since both are not defined
        % in the uint16 data type). also tests that the address supplied
        % allows enough room for all the data to be written.
        error('For type == "write", value must be an unsigned integer from 0 to 65535, and not overflow the address space.')
    
    elseif strcmp(type, 'writefullres') && (numel(value) > fix(maxWords / 2) || (65535 - parameterAddress + 1) < (numel(value) * 2) || ~isreal(value))
        % writable parameters must be real numbers, and as each number in
        % writefullres is sent as two words, has more stringent tests on
        % the size of the command allowed
        error('For type == "writefullres", value must be a real number.')
    end
end

% turns value into a row vector if it is not already (for correct
% concatenation when forming the command)
if size(value, 1) > 1
    % tranposes
    value = value';
end

% Defines correct function code (3 == read n words, 16 == write n words)
if strcmp(type, 'read')
    % defines the function code (used later)
    functionCode = 3;
    
    % forms command
    command = [deviceAddress, functionCode, split(parameterAddress), split(value)];
    
elseif strcmp(type, 'readfullres')
    % defines the function code (used later)
    functionCode = 3;
    
    % forms command differently - need to access the correct address
    % region, and read twice as many values
    command = [deviceAddress, functionCode, split(parameterAddress * 2 + hex2dec('8000')), split(value * 2)];
    
elseif strcmp(type, 'write')
    % defines the function code (used later)
    functionCode = 16;
    
    % forms command differently
	command = [deviceAddress, functionCode, split(parameterAddress), split(numel(value)), numel(value) * 2, split(value)];
    
elseif strcmp(type, 'writefullres')
    % defines the function code (used later)
    functionCode = 16;
    
    % recasts numbers (don't need to use split as this function does it
    % instead) - note that we have to turn it into double again (the normal
    % way - i.e. not recasting or the CRC-16 check does not work
    newValues = double(fliplr(typecast(single(fliplr(value)), 'uint8')));
    
    % forms command differently again: deviceAddress, 16, parameterAddress
    % (shifted for full resolution), the number of words (values * 2), the
    % number of bytes (value * 4).
    command = [deviceAddress, functionCode, split(parameterAddress * 2 + hex2dec('8000')), split(numel(value) * 2), numel(value) * 4, newValues];
end

% appends the CRC-16 on the end (checksum)
command = appendcrc16(command);

% if there is some data remaining...
if serialObject.BytesAvailable
    % currently justs read out everything that is there but ignores it
    fread(serialObject, serialObject.BytesAvailable);
end

% fwrite is in a try-catch loop due to a known MATLAB bug where the command
% will be sent but will give an error anyway and halt the function - no
% identifier for the catch is included to ensure compatibility with
% previous versions of MATLAB
try
    % sends the command
    fwrite(serialObject, command, 'async')
    
catch
end

% Eurotherm implied end of transmission is an gap of 3.5 ms at this baud
% rate - but fread time out is currently set to 0.5 s so this should not be
% an issue

% Response is (5 + (2 * value)) bytes if a 'read' command is executed, 8
% bytes if a 'write' command is executed (simply copies the command without
% the data part and the number of bytes and sends it back), and 5 bytes if
% there was an error - so fread is split into 2 parts to empty buffer
% without reaching a timeout and displaying an error message

% Error response is either... [1, 131, 2, 192, 241] (invalid parameter
% address) or [1, 131, 3, 1, 49] (invalid parameter value). A successful
% 'read' response would be... [deviceAddress, 3, (bytes), (firstValueA),
% (firstValueB),...(crcA), (crcB)] i.e. 5 + bytes long. A successful
% 'write' response would be... [(deviceAddress), 16, (parameterAddressA),
% (parameterAddressB), (numberOfValuesA), (numberOfValuesB), (crcA),
% (crcB)], i.e. 8 bytes.

% if it wasn't a broadcast, i.e. anything other than 0
if deviceAddress ~= 0
    % Reads in the first part of the output
    output = fread(serialObject, 5)';

    % if the output isn't empty...
    if ~isempty(output)
        % If message is in the format expected for the first part of the
        % data to be sent, then reads the remainder of the data as defined
        % to the command
        if any(strfind(type, 'read')) && all(output(1:2) == [deviceAddress, 3])
            
            % checks the third byte
            if (strcmp(type, 'read') && output(3) == value * 2) || (strcmp(type, 'readfullres') && output(3) == value * 4)
                % Reads the correct number of bytes as described by the
                % previous Eurotherm response
                bytes = output(3);
                output = [output, fread(serialObject, bytes)'];

                % Checks to see if the checksum is correct
                if all(output == appendcrc16(output(1:(end - 2))))
                    if strcmp(type, 'read')
                        % Produces a row vector with all of the requested
                        % values in
                        response = mergebytes(output(4:(3 + bytes)));
                        
                    elseif strcmp(type, 'readfullres')
                        % transforms the bytes into single precision
                        % numbers
                        response = double(swapbytes(typecast(uint8(output(4:end - 2)), 'single')));
                    end
                    
                else
                    % errors with a message containing the data
                    error('Checksum does not match - possible error in communication - data: %d', output)
                end
                
            else
                % empties the data for debugging - this is commented out
                % for routine use
                %response = fread(serialObject,
                %serialObject.BytesAvailable);
                
                % errors
                error('Number of returned values does not match those supplied.')
            end
            
        elseif any(strfind(type, 'write')) && all(output == command(1:5))
            % Reads the rest of the message
            output = [output, fread(serialObject, 3)'];

            % if the response from the Eurotherm does not match the
            % command, displays a warning message containing the data (only
            % need to check one more byte as the others have already been
            % checked
            if output(6) ~= command(6)
                % errors
                error('Unexpected message from Eurotherm: %d', output)
                
            elseif any(output ~= appendcrc16(output(1:6)))
                % errors if checksum is not valid
                error('Invalid checksum in response from Eurotherm.')
            end
            
        elseif output(1) == deviceAddress && output(2) == (functionCode + 128)
            % if it was a proper error message...

            % if it was an invalid parameter address
            if output(3) == 2;
                % errors
                error('Invalid parameter address - command ignored')
                
            elseif output(3) == 3
                % errors
                error('Invalid value to set - command ignored')
                
            else
                % fetches remaining data
                output = [output, fread(serialObject, get(serialObject, 'BytesAvailable'))];

                % errors if an unknown format
                error(['Unexpected message from Eurotherm: ', output])
            end
            
        else
            % fetches remaining data
            output = [output, fread(serialObject, get(serialObject, 'BytesAvailable'))'];

            % errors if an unknown format
            error(['Unexpected message from Eurotherm: ', output])
        end
        
    else
        % errors if there was no response from the Eurotherm
        error(['No response from Eurotherm to command: ', num2str(command)])
    end
end

% implied terminator is a gap in transmission of 35 ms (at 9600 baud rate)
pause(0.0035)


function response = isvalidtempobjparameteraddress(parameterAddress)
% ISVALIDPARAMETERADDRESS returns a scalar true or false is the parameter
% is valid or not - abstracted for future use
response = isnumeric(parameterAddress) && isscalar(parameterAddress) && isreal(parameterAddress) && parameterAddress ~= round(parameterAddress);


function maxWords = gettempobjmaxwords(serialObject)
% gets the maximum words from the userdata if they exist, defaulting to 32
% if not

% gets the user data
userData = serialObject.UserData;

% if its a field in the structure
if isfield(userData, 'maxWords')
    % use that
    maxWords = userData.maxWords;
    
else
    % defines a default value
    maxWords = 32;
end


function splitValues = split(valueToSplit)
% SPLIT turns a sequnce of 16 bit integers into a sequence of 2x the
% sequence length in 8 bit bytes

% need to convert it back into double at the end otherwise the CRC-16 check
% does not work correctly
splitValues = double(fliplr(typecast(uint16(fliplr(valueToSplit)), 'uint8')));


function mergedValue = mergebytes(bytes)
% MERGEBYTES turns an even number of 8 bit bytes into a row vector array of
% 16 bit integers

% recasts into 16 bit numbers
mergedValue = double(swapbytes(typecast(uint8(bytes), 'uint16')));


function appendedMessage = appendcrc16(message)
% APPENDCRC16 Appends a CRC-16 checksum (Low byte, high byte) to message
% for modbus communication. message is an array of bytes.

% initialises variables
N = numel(message);
crc = hex2dec('ffff');
polynomial = hex2dec('a001');

% loops round, applying bitxors as per the CRC specification
for m = 1:N
    crc = bitxor(crc, message(m));
    
    for n = 1:8
        
        if bitand(crc, 1)
            crc = bitshift(crc, -1);
            crc = bitxor(crc, polynomial);
            
        else
            crc = bitshift(crc, -1);
        end
    end
end

% forms the appended message for returning it
appendedMessage = [message, bitand(crc, hex2dec('ff')), bitshift(bitand(crc, hex2dec('ff00')), -8)];

Contact us