在进行PID参数整定的工作过程中,我需要将电机的转速、扭矩、母线电压、母线电流、相电压、相电流等参数通过串口传递到电脑上用Matlab来进行BP神经网络算法的PID整定计算,再将计算得到的PID参数传给电机的驱动板,来控制电机运行。在整个过程中采用了Matlab来进行串口数据的读写。目前整个流程已经走通,在此将实现过程中遇到的问题,解决方法,以及尚未解决的疑问进行总结。
首先声明本人是一个初学Matlab的新手,因此下面大部分都是自己摸索的东西,可能有肤浅或者不简洁的地方。这里写出来主要还是对自己工作的一个总结,以便未来的使用和提高。
Matlab串口数据接收一般可以通过两种方法来实现,一种是直接调用tmtool工具,找到相应的串口,配置串口参数,生成代码;另一种方法是通过一系列与串口有关的函数来对串口进行操作。
通过tmtool进行串口设计比较简单,如图所示,在Communiate项目栏中可以对串口进行读写操作并输出,这里可以设置数据种类(Data type)为ASCII码,二进制、二进制数据块(这里笔者的理解是类似一个数据帧,拥有帧头、传输数据个数位等)。通过数据格式(Data format)来设置接收数据的格式,例如ASCII码的格式有字符、字符串、带换行符的字符串等;二进制有位数等。还可以设置写入和读取是否按照HEX(16进制)进行表示。
在Configure项目栏中,可以对串口的参数进行配置,如图所示。
BauRate是对串口波特率的设置,只有上下位机采用相同的波特率时串口才能够进行正常通讯。
DataBits : 是数据位,有8位和9位两种,一般选8位。
DataTerminalReady(DTR) : 数据终端就绪,表明机器已经准备好可以接受数据的一个标志位,一般用在RS232的串口场合。
FlowControl : 流控制位,当数据读写速度存在差异时,可以采用流控制,来开启和关闭数据流,有硬件和软件控制两种方式。
InputBufferSize/OutputBufferSize : 是输入缓存大小和输出缓存大小,单位是字节。
Parity : 奇偶校验位,这里应该与下位机格式相匹配。
RequestToSend(RTS): 请求结束位,也是在RS232中常用。
StopBits: 结束位,这里应该与下位机格式相匹配。
Terminator: 中断标志,这里是以某个字符作为中断函数入口的标志,检测到这个字符存在后即可进入回调函数。
Timeout: 溢出时间,当开始接收后超过这个时间还没有数据还没有接收完成,即可认为数据接收失败,这个时间可按照实际传输周期来设定,一般越小越好。
ByreOrder: 是数据的传输模式,有大端模式和小端模式两种。小端模式是指数据的高字节保存在内存的高地址中,大端模式是指数据的高字节保存在内存的低地址中。以unsigned int value = 0x12345678为例,该数分为四个字节 12 34 56 78:
小端模式情况下,字节存储顺序是
Buffer[3]=0x12 ------高位
Buffer[2]=0x34
Buffer[1]=0x56
Buffer[0]=0x78 ------低位
大端模式下,字节存储顺序是
Buffer[3]=0x78------高位
Buffer[2]=0x56
Buffer[1]=0x34
Buffer[0]=0x12 ------低位
大端模式和小端模式只在传输的一个数据是个多字节数据时,才需要区分。一般来说下位机ARM是按照小端模式来存储的,这一现象我会在另一篇下位机串口通讯时将共用体时举出例子。因为我这里传输的数据每次就是一个字节,因此无所谓大小端。
Session Log项目栏可以将之前的各种操作用函数语言来表示。
采用与串口有关的函数也能实现对串口的操作。作者主要是通过这种方法来实现整个串口的运行。
首先了解几个主要函数:
delete(instrfindall) : 删除所有串口信息,在程序结束后没有删除串口信息,可能会导致串口被占用,下一次程序无法使用串口或者其他软件无法使用串口。因此在不再使用串口的时候,应将串口释放。为了Matlab运行的正确性,也可以将运行中的所有工作变量删除,这里可以采用clera all语句。
obj1=serial('com2'): 定义某串口为变量obj1,也就是相当于为串口com2起了一个名字叫obj1,后面对obj1操作就相当于对com2操作。
fclose(obj1)/fopen(obj1): 关闭串口和开启串口,有些参数设定需要先关闭串口,因此在进行串口参数设定前先将串口关闭,设定完成后再打开。
obj1:直接在命令行窗口输入定义好的串口变量,可以看到一个串口obj1的参数列表。
set(obj1, 'BaudRate', 115200): 通过set函数可以对obj1进行参数设定,格式是set(串口变量名,'参数名称',参数),这里是设定波特率是115200。
set(obj1, 'InputBufferSize', 100): 输入缓存大小 100 字节。
set(obj1, 'OutputBufferSize', 100): 输出缓存大小 100 字节。
set(obj1, 'Timeout', 1.0): 溢出时间 1.0 s。
set(obj1,'BytesAvailableFcnMode','terminator'): 回调函数中断的模式,有两种,一种是terminator模式,关键词中断,当检测到某一关键字时进行中断。一种是byte模式,当二进制达到某个字节数时中断。
set(obj1,'terminator','D'): 中断关键字是ASCII的D
set(obj1,'BytesAvailableFcn',@my_callback): 回调函数的设定,当发生上述中断时,进入回调函数my_callback,在回调函数里可以对数据进行读写等操作。
(上面串口中断模式还有byte模式,这里给出函数示例: set(obj1,'BytesAvailableFcnMode','terminator')
set(obj1,'byte',24) )
在回调函数my_callback里面,可以进行串口数据的读取和发送等操作,下面先介绍几个串口接收和发送相关的函数:
data=fread(obj1, 24):向串口obj1读取24个数据,因为我这里数据是一个数据一个字节,因此也就相当于24个字节,这里默认情况下是按照一个数据8位,也可以加上数据格式,告诉电脑一个每个数据是什么格式,如data= fread(obj1, 24,'short')。
fwrite(obj2, sendbuff, 'uint8'):向串口obj2发送数据,被发送的数据存储在笔者自定的一个向量sendbuff中,这里会将sendbuff中的所有数据按照unit8的的格式依次发送。
这里我将整个串口设置和回调函数的代码给出:
代码1:主函数
因为项目需要,这里有一个串口接收数据,一个串口发送数据。这里我接收的数据是一个数据帧24个字节,发送的数据是8个字节。在接收的字节中帧头是'D',所以采用关键字中断模式,中断标志是'D'。
%主函数 进行串口设置和开启
delete(instrfindall)
clear obj1
clear obj2
clear all
global obj1;
global obj2;
global data;
global sendbuff;
global data1;
global data2;
global data3;
global data4;
global data5;
global data6;
global n;
n=0;
data=zeros(24,1)
sendbuff=zeros(1,8);
sendbuff(7)= hex2dec('0D');
sendbuff(8)= hex2dec('0A');
data1=0;
data2=0;
data3=0;
data4=0;
data5=0;
data6=0;
obj1=serial('com9');
fclose(obj1);
set(obj1, 'InputBufferSize', 100);
set(obj1, 'OutputBufferSize', 100);
set(obj1, 'BaudRate', 112500);
set(obj1, 'Timeout', 1.0);
set(obj1,'BytesAvailableFcnMode','terminator');
set(obj1,'terminator','D');
set(obj1,'BytesAvailableFcn',@my_callback1);
fopen(obj1);
obj2=serial('com8');
fclose(obj2);
set(obj2, 'InputBufferSize', 24);
set(obj2, 'OutputBufferSize', 24);
set(obj2, 'BaudRate', 115200);
set(obj2, 'Timeout', 1.0);
fopen(obj2);
代码2:回调函数
进入回调函数后对数据进行读取和处理,然后发送给下位机
接收数据帧的格式是:
字节1:'D',10进制表示是68
字节2:0x05,10进制表示是5,1-2是帧头,然后是数据位
字节3-4:表示电机转速,16位数据
字节5-6:表示负载扭矩,16位数据
字节7-10:表示母线电压,32位数据
字节11-14:表示母线电流,32位数据
字节15-18:表示相电压,32位数据
字节19-22:表示相电流,32位数据
字节23:0X0D
字节24:0X0A
发送数据帧的格式:
字节1-2:表示Kp,16位数据
字节3-4:表示Ki,16位数据
字节5-6:表示Kd,16位数据
字节7:0X0D
字节8:0X0A
function my_callback1(obj1,event)
global data;
global obj2;
global sendbuff;
global data1;
global data2;
global data3;
global data4;
global data5;
global data6;
global n;
n=n+1;
data= fread(obj1, 24)
if lenth(data)==24
if data(1) == 68
if data(2) == 5
data1 = (data(3)*256+data(4))/1000;
data2 = (data(5)*256+data(6))/1000;
data3 = (data(7)*16777216+data(8)*65536+data(9)*256+data(10))/100;
data4 = (data(11)*16777216+data(12)*65536+data(13)*256+data(14))/100;
data5 = (data(15)*16777216+data(16)*65536+data(17)*256+data(18))/100;
data6 = (data(19)*16777216+data(20)*65536+data(21)*256+data(22))/100;
end
end
if data(1) == 5
data1 = (data(2)*256+data(3))/1000;
data2 = (data(4)*256+data(5))/1000;
data3 = (data(6)*16777216+data(7)*65536+data(8)*256+data(9))/100;
data4 = (data(10)*16777216+data(11)*65536+data(12)*256+data(13))/100;
data5 = (data(14)*16777216+data(15)*65536+data(16)*256+data(17))/100;
data6 = (data(18)*16777216+data(19)*65536+data(20)*256+data(21))/100;
end
sendbuff(1)=floor((data1+data4)/256);
sendbuff(2)=floor((data1+data4));
sendbuff(3)=floor((data2+data5)/256);
sendbuff(4)=floor((data2+data5));
sendbuff(5)=floor((data3+data6)/256);
sendbuff(6)=floor((data4+data6));
fwrite(obj2, sendbuff, 'uint8');
%my_fwrite(obj2, sendbuff, 'uint8');
end
end
关于串口速度的讨论
在进行PID参数整定时,由于整个PID的调整周期较短,因此希望整个串口读取、处理、发送的时间最好在2ms以内完成。这个速度对于计算机来说其实是不容易实现的,因此如何提高整个回调函数的运算速度成为编写代码的关键。这里我发现了一些提高运行速度的方法:
对于串口读写速度,主要是串口发送函数 fwrite(obj2, sendbuff, 'uint8')消耗了太多的时间,运行时这里耗费的时间有20ms+,这对于整个项目时不可接受的,笔者在思考和尝试的时候发现了一个方法可以让这个时间缩小到0.7ms左右。那就是用一个新的my_fwrite.m文件代替原来的内部函数fwrite.m。并将上述的代码改为my_fwrite(obj2, sendbuff, 'uint8')。
新的文件如下:
function my_fwrite(obj, varargin)
%FWRITE Write binary data to instrument.
%
% FWRITE(OBJ, A) writes the data, A, to the instrument connected to
% interface object, OBJ.
%
% The interface object must be connected to the instrument with the
% FOPEN function before any data can be written to the instrument
% otherwise an error will be returned. A connected interface object
% has a Status property value of open.
%
% FWRITE(OBJ,A,'PRECISION') writes binary data translating MATLAB
% values to the specified precision, PRECISION. The supported
% PRECISION strings are defined below. By default the 'uchar'
% PRECISION is used.
%
% MATLAB Description
% 'uchar' unsigned character, 8 bits.
% 'schar' signed character, 8 bits.
% 'int8' integer, 8 bits.
% 'int16' integer, 16 bits.
% 'int32' integer, 32 bits.
% 'uint8' unsigned integer, 8 bits.
% 'uint16' unsigned integer, 16 bits.
% 'uint32' unsigned integer, 32 bits.
% 'single' floating point, 32 bits.
% 'float32' floating point, 32 bits.
% 'double' floating point, 64 bits.
% 'float64' floating point, 64 bits.
% 'char' character, 8 bits (signed or unsigned).
% 'short' integer, 16 bits.
% 'int' integer, 32 bits.
% 'long' integer, 32 or 64 bits.
% 'ushort' unsigned integer, 16 bits.
% 'uint' unsigned integer, 32 bits.
% 'ulong' unsigned integer, 32 bits or 64 bits.
% 'float' floating point, 32 bits.
%
% FWRITE(OBJ, A, 'MODE')
% FWRITE(OBJ, A, 'PRECISION', 'MODE') writes data asynchronously
% to the instrument when MODE is 'async' and writes data synchronously
% to the instrument when MODE is 'sync'. By default, the data is
% written with the 'sync' MODE, meaning control is returned to
% the MATLAB command line after the specified data has been written
% to the instrument or a timeout occurs. When the 'async' MODE is
% used, control is returned to the MATLAB command line immediately
% after executing the FWRITE function.
%
% The byte order of the instrument can be specified with OBJ's
% ByteOrder property.
%
% OBJ's ValuesSent property will be updated by the number of values
% written to the instrument.
%
% If OBJ's RecordStatus property is configured to on with the RECORD
% function, the data written to the instrument will be recorded in
% the file specified by OBJ's RecordName property value.
%
% Example:
% s = visa('ni', 'ASRL2::INSTR');
% fopen(s);
% fwrite(s, [0 5 5 0 5 5 0]);
% fclose(s);
%
% See also ICINTERFACE/FOPEN, ICINTERFACE/FPRINTF, ICINTERFACE/RECORD,
% ICINTERFACE/PROPINFO, INSTRHELP.
%
% MP 7-13-99
% Copyright 1999-2012 The MathWorks, Inc.
% Error checking.
if ~isa(obj, 'icinterface')
error(message('instrument:fwrite:invalidOBJInterface'));
end
if length(obj)>1
error(message('instrument:fwrite:invalidOBJDim'));
end
% Parse the input.
switch nargin
case 1
error(message('instrument:fwrite:invalidSyntaxA'));
case 2
cmd = varargin{1};
precision = 'uchar';
mode = 0;
case 3
% Original assumption: fwrite(obj, cmd, precision);
[cmd, precision] = deal(varargin{1:2});
mode = 0;
if ~(isa(precision, 'char') || isa(precision, 'double'))
error(message('instrument:fwrite:invalidArg'));
end
if strcmpi(precision, 'sync')
% Actual: fwrite(obj, cmd, mode);
mode = 0;
precision = 'uchar';
elseif strcmpi(precision, 'async')
% Actual: fwrite(obj, cmd, mode);
mode = 1;
precision = 'uchar';
end
case 4
% Ex. fprintf(obj, format, cmd, mode);
[cmd, precision, mode] = deal(varargin{1:3});
if ~ischar(mode)
error(message('instrument:fwrite:invalidMODE'));
end
if strcmpi(mode, 'sync')
mode = 0;
elseif strcmpi(mode, 'async')
mode = 1;
else
error(message('instrument:fwrite:invalidMODE'));
end
otherwise
error(message('instrument:fwrite:invalidSyntaxArgv'));
end
% % % % Error checking.
% % % if ~isa(precision, 'char')
% % % error(message('instrument:fwrite:invalidPRECISIONstring'));
% % % end
% % % if ~(isnumeric(cmd) || ischar(cmd))
% % % error(message('instrument:fwrite:invalidA'));
% % % end
% Convert the data to the requested precision.
switch (precision)
case {'uchar', 'char'}
cmd = uint8(cmd);
type = 5;
signed = 0;
case {'schar'}
cmd = int8(cmd);
type = 5;
signed = 1;
case {'int8'}
cmd = int8(cmd);
type = 0;
signed = 1;
case {'int16', 'short'}
cmd = int16(cmd);
type = 1;
signed = 1;
case {'int32', 'int', 'long'}
cmd = int32(cmd);
type = 2;
signed = 1;
case {'uint8'}
cmd = uint8(cmd);
type = 0;
signed = 0;
case {'uint16', 'ushort'}
cmd = uint16(cmd);
type = 1;
signed = 0;
case {'uint32', 'uint', 'ulong'}
cmd = uint32(cmd);
type = 2;
signed = 0;
import java.lang.Long
for iLoop = 1:length(cmd)
tmp(iLoop) = Long(cmd(iLoop)); %#ok
end
cmd=tmp;
case {'single', 'float32', 'float'}
cmd = single(cmd);
type = 3;
signed = 1;
case {'double' ,'float64'}
cmd = double(cmd);
type = 4;
signed = 1;
otherwise
error(message('instrument:fwrite:invalidPRECISION'));
end
% i2c does not support async mode
if mode == 1
error(message('instrument:fwrite:i2cAyncNotSupported'));
end
% Call the write java method.
try
fwrite(igetfield(obj, 'jobject'), cmd, length(cmd), type, mode, signed);
catch aException
newExc = MException('instrument:fwrite:opfailed', aException.message);
throw(newExc);
end
除此之外,在进行数据的处理的时候,应尽量对数据进行乘除取整取余等运算而不是调用一些强制转换的函数或者移位函数。笔者考虑可能是在调用matlab系统内部函数花费了较多的时间。
同时,这个运算速度和matlab版本以及电脑的CPU性能有关,同样的程序i5跑起来是2ms,i7跑起来0.8ms。
通过以上方法将原来数十微妙的运行速度降到了运行一次在一微妙以内。所以还是有些效果的。