Code covered by the BSD License  

Highlights from
CO2gui - lab control and automation

CO2gui - lab control and automation

by

 

06 Jan 2010 (Updated )

Software used for controlling and data logging lab equipment.

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 controller
% on 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

% checks the number of arguments
error(nargchk(4, 5, nargin))

% gets the serial object user data
userData = serialObject.UserData;

% gets the maximum words if they exist
if isfield(userData, 'maxWords')
    % saves it for convenience
    maxWords = userData.maxWords;
    
else
    % defines a default value
    maxWords = 32;
end

% 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 ~isvector(type) || ~any(strcmp(type, {'read', 'readfullres', 'write', 'writefullres'}))
    % can only have these types of reading and writing
    error('Type must be "read", "readfullres", "write" or "writefullres".')
    
elseif ~isserial(serialObject) || (~isfield(serialObject.UserData, 'realTermHandle') && ~isrunning(serialObject))
    % the serial object must be valid and open to send or receive data
    % (unless there is a realterm handle available)
    error('serialObject must be a valid open serial object')
    
elseif ~isserial(serialObject) || (isfield(serialObject.UserData, 'realTermHandle') && ~isrunning(serialObject.UserData.realTermHandle))
    % if the handle is there, the object must be running (PortOpen is 1)
    error('If the Realterm handle is present, the object must be connected via Realterm.')
    
elseif isfield(serialObject.UserData, 'realTermHandle') && (~isfield(serialObject.UserData, 'captureFileID') || ~isfid(serialObject.UserData.captureFileID))
    % if using realterm, the capture file ID must be there
    error('If using Realterm, the capture file handle must be in the serial object''s UserData.')
    
elseif ~isnumeric(deviceAddress) || ~isscalar(deviceAddress) || isnan(deviceAddress) || ~isreal(deviceAddress) || deviceAddress ~= round(deviceAddress) || deviceAddress < 0 || deviceAddress > 254 || (all(~strfind(type, 'write')) && deviceAddress < 1)
    % 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 ~isnumeric(parameterAddress) || ~isscalar(parameterAddress) || isnan(parameterAddress) || ~isreal(parameterAddress) || parameterAddress ~= round(parameterAddress) || deviceAddress < 1 || deviceAddress > 65535
    % parameter address must be 0 to 65535 (a 16-bit number)
    error('ParameterAddress must be an unsigned integer from 1 to 65535.')
    
elseif any(strfind(type, 'read')) && nargin >= 5 && (~isnumeric(value) || ~isscalar(value) || isnan(parameterAddress) || ~isreal(value) || value < 1)
    % checks these things some more
    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) || isnan(value) || ~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 any(strfind(type, 'read'))
    % defines the function code (used later)
    functionCode = 3;
    
    % depends on the specific command
    if strcmp(type, 'read')
        % forms command
        command = [deviceAddress, functionCode, split(parameterAddress), split(value)];
        
    elseif strcmp(type, 'readfullres')
        % 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)];
    end
    
elseif any(strfind(type, 'write'))
    % defines the function code (used later)
    functionCode = 16;
    
    % different formats
    if strcmp(type, 'write')
        % forms command differently
        command = [deviceAddress, functionCode, split(parameterAddress), split(numel(value)), numel(value) * 2, split(value)];
        
    elseif strcmp(type, 'writefullres')
        % 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
end

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

% flushes everything in the buffer
serialflush(serialObject);

% sends the command
serialwrite(serialObject, command, 'async')

% 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.

% all use 'serialread' which, depending on if the realterm handle is there
% or not, reads things back

% if it wasn't a broadcast...
if deviceAddress
    % Reads in the first part of the output
    output = serialread(serialObject, 5)';
    
    % DEBUG
    %output = fread(serialObject, 5)';

    % if the output 5 characters long...
    if numel(output) == 5
        % 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, serialread(serialObject, bytes)'];

                % Checks to see if the checksum is correct
                if all(output == appendcrc16(output(1:(end - 2))))
                    % checks the command type
                    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
                serialflush(serialObject);
                
                % 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, serialread(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 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, serialread(serialObject, serialObject.BytesAvailable)];

                % errors if an unknown format
                error('Unexpected message from Eurotherm: %d', output)
            end
            
        else
            % fetches remaining data
            output = [output, serialread(serialObject, 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 full response from Eurotherm to command: ', num2str(command), '; answer: ', num2str(output)])
    end
end

% if the realtermhandle isn't present
%if ~isfield(serialObject.UserData, 'realTermHandle')
    % implied terminator is a gap in transmission of 35 ms (at 9600 baud rate)
    % - HOWEVER for some bizarre reason this seems to translate into ~350 ms so
    % has been excluded ffs) - hopefully the slack in MATLAB will make this
    % large enough to not be an issue
    pause(0.0035)
%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