1. MATLAB App显示动态波形
IMU901上传上来的数据中包含四元数和加速度,每帧数据包按如下格式向上位机发送:
0.00 0.00 0.00 0.00 0.00 0.00 0.00\r\n
每帧数据中,相邻两个数据之间使用空格符隔开,末尾使用\r\n字符作为判断符。在器件运行的过程中,我们可能需要实时观察数据波形来判断设备是否正常运作。这一节,我将详细介绍如何在MATLAB的坐标区中显示动态波形。
1.1 在MATLAB脚本中显示波形
1.回到MATLAB Appdesigner实用技巧(二):MATLAB App建立TCP服务端和下位机通信1.3节中我们创建的MATLAB脚本文件。根据上位机发送上来的数据格式,我们可以以字符串“\r\n”为标准将每组数据分割开。使用如下函数:
regexp(data,'\r\n','split');
可以将数据从“\r\n”字符处分割,形成我们需要的数据组。将1.3中的代码改写如下(此处仅附上需要改写的部分,读者需要对比MATLAB Appdesigner实用技巧(二):MATLAB App建立TCP服务端和下位机通信1.3节中的代码进行修改):
clc; clear; close all
%% 全局变量
global data;
global splitedData;
%% 回调函数
function Receive_Callback(ts,~)
global data;
global splitedData;
TCPBytesAvailable = get(ts,'NumBytesAvailable');
if TCPBytesAvailable % 判断是否有数据可读,如果可读则进行下一步读出的操作
data = read(ts,TCPBytesAvailable,"char"); % 读出的数据类型为char
disp(data);
splitedData = regexp(data,'\r\n','split');
end
end
2.运行上述代码,在工作区找到并点开“data”和“splitedData”变量:
可以看到,回调函数中直接读出来的数据为char类型,这个长度实际包含了两组数据;而分割函数将数据分割成了一个1×3的元胞数组。那么,这时我们就需要对分割后的数据做处理。很显然,这个元胞数组的最后一个元素为空,这是我们不需要的,我们仅仅需要前面两个元胞数组存储的数据。那么接下来,我们需要做如下几件事:
- 首先,将分割出来的每一组数据从字符串类型转化为double类型。使用如下函数
str2num(splitedData{1});
该函数可以将字符串转化为double数组。
- 判断每一组数据的长度是否为7,如果是则保留,不是则去掉
- 新建若干变量,用以存储可用数据以及坐标区显示范围。
- 绘制图像
各个部分需要注意的地方均在下列代码中添加了注释。
完整代码如下:
clc; clear; close all
%% 全局变量
global data;
global splitedData;
global Accx Accy Accz; % 三轴加速度
Accx = 0; Accy = 0; Accz = 0;
% 上面的三轴加速度将作为y轴参与绘图;
% 因此我们需要一个x轴变量来绘图;
% 并且为了实现动态效果,我们需要一个变量来存储坐标区显示的起始值
global XAxis StartValue;
XAxis = 0; StartValue = 0;
% 自程序运行以来接收到的可用数据的组数
global NumAllData;
NumAllData = 1;
%% 创建TCPServer
address = '192.168.4.2';
port = 8080;
ts = tcpserver(address,port);
%% 加速度图窗
global AccFigure
AccFigure = figure('Name','Acc','NumberTitle','off');
AccFigure.Color = [1 1 1];
% 加速度Plot句柄。创建句柄后,使用set函数更新数据会提高绘图速度
global AccPlots
AccPlots = plot(XAxis,Accx,'r',...
XAxis,Accy,'g', ...
XAxis,Accz,'b');
legend(AccFigure.Children,{'AccX','AccY','AccZ'});
axis([XAxis XAxis+1000 -2 2]);
AccFigure.Position = [400 200 1200 700];
grid on
grid minor
%% 清空数据并打开回调函数
flush(ts);
configureTerminator(ts,"CR/LF");
configureCallback(ts,"terminator",@Receive_Callback);
% 这里pause的效果是,按下键盘上任意键,代码从这一行继续运行。
% 如果不加这一行,则程序将直接运行到configureCallback(ts,"off");即关闭回调函数
% 回调函数将不会运行
pause;
configureCallback(ts,"off");
%% 回调函数
function Receive_Callback(ts,~)
global data;
global splitedData;
global Accx Accy Accz;
global XAxis StartValue;
global NumAllData;
global AccPlots;
global AccFigure;
NumUsefullData = 0; % 每次回调函数读出的可用数据组数
TCPBytesAvailable = get(ts,'NumBytesAvailable');
if TCPBytesAvailable % 判断是否有数据可读,如果可读则进行下一步读出的操作
data = read(ts,TCPBytesAvailable,"char"); % 读出的数据类型为char
disp(data);
splitedData = regexp(data,'\r\n','split');
L = length(splitedData);
if isempty(splitedData)
return;
else
usefullData = cell(L,1);
for i = 1:L
% 如果数组长度为7,则认为此组为可用组,可用数据组数+1
if length(str2num(splitedData{i})) == 7
NumUsefullData = NumUsefullData + 1;
usefullData{NumUsefullData} = str2num(splitedData{i});
end
end
acx = zeros(NumUsefullData,1);
acy = acx; acz = acx;
for j = 1:NumUsefullData
acx(j) = usefullData{j}(5);
acy(j) = usefullData{j}(6);
acz(j) = usefullData{j}(7);
end
% 更新x轴的数据信息
XAxis = [XAxis;transpose(NumAllData:NumAllData+NumUsefullData-1)];
% 更新三轴加速度(y轴)的数据信息
Accx = [Accx;acx];
Accy = [Accy;acy];
Accz = [Accz;acz];
% 更新自运行以来所有接收到的可用数据组数
NumAllData = NumAllData + NumUsefullData;
% 设置坐标区x轴的显示范围始终是自StartValue开始的1000个数据
AccFigure.Children(2).XLim = [StartValue, StartValue+1000];
% 使用set函数更新在脚本开头创建的plot句柄的数据,
% 这样就不需要每次都使用plot画图,否则运行一段时间后程序巨卡无比
set(AccPlots(1),"XData",XAxis,"YData",Accx);
set(AccPlots(2),"XData",XAxis,"YData",Accy);
set(AccPlots(3),"XData",XAxis,"YData",Accz);
drawnow;
% 如果数据组大于1000个,则更改横坐标轴的起始值,
% 在原来的起始值基础上加上每次回调函数读出来的可用数据组数
% 这样用来实现动态效果
if XAxis(end) >= StartValue+1000
StartValue = StartValue + NumUsefullData;
end
end
end
end
运行上述代码,效果如下:
可以看到,在坐标区显示出了我们的波形,并且随着数据量的增加,坐标区的起始值发生了改变,也就做出了示波器的效果。
1.2 在App的坐标区中显示波形
有了上面的测试经验,我们将代码移植到App中即可。关于Appdesigner的基本使用。移植过程中需要注意以下几点:
- 调用公共属性时,需要在属性名前加app.。在App中,公共属性起到类似全局变量的作用,所以不需要写”global“。具体可参考上述文章的第8和9节。
- plot函数可以指定坐标区,在我们创建的坐标区控件中绘图
- 调用回调函数时,需要在回调函数名称前面添加“@app.“。在本例中,如果在App中调用回调函数,则语法格式如下:
configureCallback(app.ts,"terminator",@app.TCPReceive_Callback);
至此,我们就在App中实现了示波器效果。笔者已写好的App中,运行效果如下: