目录
- 一. 引言
- 二. 软件功能
- 2.1 原理
- 2.2 软件功能
- 2.3 运行环境
- 三. 软件操作使用
- 3.1 软件界面
- 3.2 软件功能使用详解
- 3.2.1 连接
- 3.2.2 数据监测(串口示波器)
- 3.2.3 数据修改
- 3.2.4 数据保存
- 3.3 软件的硬件连接
- 四. 通信协议——STM32移植篇
- 4.1 通信协议
- 4.2 STM32如何传输浮点数
- 4.3 简单移植(只进行简单数据监控)
- 4.4 完整移植
- 写在最后
一. 引言
当前,嵌入式系统的运用非常普遍。无论是智能家居,还是各类机器人控制系统,都离不开嵌入式硬件和软件的支撑。而在各类系统的开发、使用和维护过程中,数据可视化都是一项重要内容。通过可视化界面,达到“测”与“控”的目的。“测”即监测系统运行过程中的各类状态,以数字、仪表、曲线等形式进行展示。“控”即通过上位机进行指令下发,达到参数修改、远程指挥的目的。
然而,嵌入式系统(一般称为下位机)的开发和可视化界面(一般称为上位机)的开发都是有难度的。熟悉嵌入式系统开发的工程师、学生并不一定擅长上位机的开发。有时候做一些小项目,仅仅只需要监测一两个数据,专门去开发一个可视化软件耗时耗力、并不经济。本人长期从事基于单片机的机器人控制系统研发,略懂一点可视化软件开发,经常是一个嵌入式系统就要配套一个上位机,开发周期长、维护难度大。另外经常收到各类关于上位机开发的咨询,深知这方面需求巨大。
因此,基于快速开发的需求,本软件开发了一款通用的上位机软件。使用者只需按照本软件提供的通信协议进行工程移植,即可使用本软件进行嵌入式系统远程“测”“控”。
本软件非常适合对上位机开发不熟悉的工程师、学生使用,可以快速构建自己的远程数据采集系统,大大缩短开发周期。尤其对学生而言、可快速构建测控系统,方便采集数据,加速论文写作。
软件和配套STM32工程、说明文档的下载地址:嵌入式系统远程测控软件-串口示波器-远程修改参数
二. 软件功能
2.1 原理
如何实现上位机和下位机通信,熟悉单片机开发的对串口调试助手肯定不陌生。串口调试助手就是一个上位机,它可收可发,只是它只能以文本显示原始数据帧。当我们定义好通信协议,下位机和上位机都按照协议进行数据发与收,自然就能解析出我们想要的数据。本软件也是基于串口通信的,使用的是自定义通信协议。
2.2 软件功能
(1) 数据监测。以文本数据和曲线形式显示下位机上传的数据。
(2) 数据修改。对下位机的数据进行修改
(3) 历史数据保存。以日志文件形式保存部分监测的数据,供后期进行数据处理。
2.3 运行环境
电脑环境:本软件基于Qt5.14开发,发行版为绿色免安装版本,无需安装开发环境。电脑上安装CH340、CP2102等串口驱动即可。
单片机环境:任意单片机,只要能进行串口通信。
三. 软件操作使用
3.1 软件界面
直接双击exe文件即可打开主界面。启动后主界面如图所示,界面由多个区域组成。其中左侧两个区域,“端口”框内为设备连接操作部分,下方是快速设置寄存器值。右侧区域为标签页区域,有四个标签页,“数据曲线”标签页是进行数据显示(上行数据),“更改参数”标签页是进行数据修改(下发数据),“串口数据”标签页显示串口原始16进制数据。“ReadMe”标签页里面是一些版本信息和说明文字。
其中,“数据曲线”标签页又分成了多个标签页。“数据曲线00”是地址为0x00的数据,“数据曲线01”是地址为0x02的数据,依次类推。“自定义曲线1”和“自定义曲线2”两个标签页可以任意选择地址进行数据显示,可以分别最多显示4条曲线。
3.2 软件功能使用详解
3.2.1 连接
(1)通过下拉框设置串口号、波特率、数据位、停止位、校验位等串口参数,其中默认参数为波特率115200、数据位8、停止位1、校验位无。串口号在软件启动时自动搜索计算机当前可用串口并可以通过下拉框选取,当没有设备连接时下拉框为空,不可选择。
(2)点击“打开串口”按钮,即可与下位机连接。
(3)当需要断开连接时点击“关闭串口”按钮,即可与控制板断开连接。
(4)当软件运行过程中出现插拔通信线,会改变可用串口设备,点击“更新设备列表”按钮,即可在串口号下拉框中选取最新的可用串口设备。(目前暂时是这样,需要手动更新)
3.2.2 数据监测(串口示波器)
数据的传输和解析都是是按照地址——数据的形式进行的。目前设置了8个子标签页,每个子标签页包含一个图表用于数据曲线显示。其中“数据曲线00”是地址显示0x00的数据,“数据曲线01”显示地址为0x01的数据,依次类推,前6个图表都只显示一条曲线。“自定义曲线1”和“自定义曲线2”两个标签页分别可以显示四条曲线(即四个地址的数据),并且这四个地址是可以自己设置的。
下图显示的是“数据曲线00”标签页的数据,图表下方横坐标是时间,时间是动态滚动的,左侧纵坐标是数据值。图表下侧以文本框形式显示数据的具体值。图表上方有三个输入框。“设置显示点数”设置图表区域显示的数据个数,这个数值越大,显示的点数越多,横坐标时间跨度越长。“设置y坐标范围”可以设置y轴坐标的最小值和最大值,使曲线能够完整显示在图框中。“数据曲线01”、“数据曲线02”、“数据曲线03”、“数据曲线04”、“数据曲线05”使用方法一样。
下图显示的是“自定义曲线1”标签页的数据,图表下方横坐标是时间,时间是动态滚动的,左侧纵坐标是数据值。图表上方有两排输入框。第一排三个输入框:“设置显示点数”设置图表区域显示的数据个数,这个数值越大,显示的点数越多,横坐标时间跨度越长。“设置y坐标范围”可以设置y轴坐标的最小值和最大值,使曲线能够完整显示在图框中。第二排四个输入框,“数据1”设置数据1的数据地址,“数据2”设置数据2的数据地址,“数据3”设置数据3的数据地址,“数据4”设置数据4的数据地址。四个数据前都有一个复选框,当勾选相应的复选框时,图像中就会显示这个数据的曲线,当取消勾选相应的复选框时,图像中就会不显示这个数据的曲线。“自定义曲线2”标签页使用方法一样。
左侧有一个“设置显示点数”的编辑框,所有的曲线显示时长我都改成在这个地方统一设置了,相应标签页的“设置显示点数”没激活。如果下位机每隔20ms上传一帧数据,那么500个点就是500*20ms = 10s。
3.2.3 数据修改
数据修改也是是按照地址——数据的形式进行的,在“更改参数”标签页和左侧“快速设置寄存器值”都可以修改参数。“更改参数”标签页里,“参数修改”那一块,可以修改0x80~0x8F一共16个数据,地址固定。点“查询参数”就可以查看当前值,点“载入参数”就把当前值填充到设置值那里(这一步非必须,只是为了方便,不需要一个一个再敲进去),然后在设定值那一列进行修改即可,改成自己想要的参数。最后点“写入参数”即可将参数写入进去。
右侧“指令下发”那一块,可以指定起始地址和数据个数,这样就可以应对所有场合了。比如将起始地址改成0xA0,数据个数改成10,点击“写入指令”,就会将从0xA0开始的10个数据写到单片机了。(不能超过16个数据,因为只给了16个框框)。
主界面左方“快速设置寄存器值”那里也是可以灵活修改参数,每个地址框都是可以滚动或敲入修改的,从而修改地址。数据框里的值一旦滚动或敲入则会自动下发数据下去。这个地方一次只能修改一个地址的数据,适用于测试场合。
3.2.4 数据保存
除了数据测控外,软件的第三大核心功能是数据保存,这个保存过程是在后台自动进行的,当软件启动时就会开始记录,直到软件关闭时停止记录。日志文件的生成和命名也是自动的。格式为“data_year_month_date_hh_mm.csv”。其存放位置为.exe文件的同级目录下。如图所示。
文件打开之后的格式如图13所示。第一行为表头,从左至右前四列为小时、分钟、秒、毫秒,四位数据标记数据接收的时间,之后依次显示每个地址的数据:0x00地址的数据,0x01地址的数据,……,0x0F地址的数据。本版本软件暂时设置只保存到0x0F的数据,即最多保存17个数据,使用者在使用的过程中要注意,虽然曲线显示是没有地址限制的,但是超过0x0F地址的数据是不被保存的。所以要优先使用低地址的数据。
3.3 软件的硬件连接
最后,说明一下本软件的硬件连接方式。本软件与单片机的连接方式可为串口ttl、485、232任意一种,只要是UART通信协议即可。
电脑与单片机之间可以使用一根数据线进行连接。这种方式是有线方式。也可以使用无线数传模块进行连接。电脑和单片机各连接一个无线数传模块,即可实现远程无线通信。
四. 通信协议——STM32移植篇
4.1 通信协议
依照本软件所规定的通信协议进行数据上传,本软件才能正确解析数据,进行显示和保存。否则通信失败,软件无法正常使用。
本软件使用的自定义通信协议,数据帧长度为任意字节,可以一次发送所有数据,也可以分多个数据帧上传数据。当数据过多时,建议分多个数据帧发送,否则一帧长度过长,出错概率增大。
数据的保存和传输按照“地址-数据”形式,按照浮点数32位保存,每个数据占4个字节。每个数据都有自己的地址,地址从0开始编号。
有三种数据帧:
(1)单片机主动上传数据:
发送N个数据(32 bits)一共4N+6个帧字节。
(2)上位机下发更改数据:
发送N个数据(32bits)也是一共4N+6个帧字节。
(3)上位机下发查询数据:
查询从起始地址开始的N个数据,查询帧是6个字节。下位机收到数据按照上传数据格式上传。
4.2 STM32如何传输浮点数
数据的传输最简单的是传输整形数据,本软件的早期版本是传输uint16类型数据,每个数据占用两个字节。这种方法的缺点是:(1)传输浮点数时需要进行倍数处理。例如0.12,将其乘100变成整形的12,上位机收到后除100变成浮点型的0.12。这种方法较麻烦,哪些地址的数据需要进行倍数,需要下位机和上位机同时定义清楚。(2)有符号和无符号类型数据区分。uint16类型数据较简单,直接传输,直接解析,没问题。int16上位机解析时,就需要进行类型转换了。哪些地址的数据要进行(int16_t)类型转换,也要定义清楚。(3)表示的数据范围有限,16位整形无符号数只能到65535,有符号数除2减半。如果是浮点数,乘掉了倍数,表示范围直接缩水。如果是翻100倍,只能表示到655。
所以,最方便的就是直接传输浮点数,省去很多麻烦。当然浮点数的缺点就是,一个数据要占用4个字节。因此效率是传输整形数据的一半。
传输浮点数,需要定义一个联合体:
union float_data
{
float f_data;
uint8_t byte[4];
};
f_data
和byte[4]
共用4个字节的内存单元,成员f_data
是实际使用的数据,成员byte[4]
是通信时用的数据,各司其职。
使用方法:
#include <stdio.h>
#include <stdint.h>
union float_data
{
float f_data;
uint8_t byte[4];
};
int main()
{
union float_data me, you;
me.f_data = 0.12;
you.byte[3] = me.byte[3];
you.byte[2] = me.byte[2];
you.byte[1] = me.byte[1];
you.byte[0] = me.byte[0];
printf("you = %f", you.f_data);
return 0;
}
出来的结果是一样的,0.12。聪明的读者可以发现,me和you对应下位机和上位机。只要me和you按照这种方式进行传输,就可以传输浮点数。传输多个浮点数,me和you就可以定义为一个数组,例如me[100]
, you[100]
。
数组的索引就是地址,数组的索引就是地址,数组的索引就是地址。 请记住这个思想和方法。
4.3 简单移植(只进行简单数据监控)
如果你只是想监测几个数据,可以非常迅速地实现。
下面给出示例代码,可以直接写在main函数里。
void main()
{
// 省略
union float_data register[10];
uint8_t data[100];
while(1)
{
uint8_t data;
data[0] = 0xAA;
data[1] = 0xFF;
data[2] = 0;
data[3] = 0;
data[4] = 4;
data[5] = register[0].byte[3];
data[6] = register[0].byte[2];
data[7] = register[0].byte[1];
data[8] = register[0].byte[0];
data[9] = 0;
for (i = 0; i < 9; i++)
data[9] += data[i];
for (i = 0; i < 10; i++)
Uart1_send_char(data[i]);
delay_ms(20);
}
}
这段代码即按照通信协议进行“填空”,数据的起始地址是0x0000,有效数据长度是4,即一个数据,一帧数据总长为10个字节,最后一个字节是校验和。这段循环结构按照20ms的间隔定期上传数据。
void main()
{
// 省略
union float_data register[10];
uint8_t data[100];
while(1)
{
data[0] = 0xAA;
data[1] = 0xFF;
data[2] = 0;
data[3] = 0;
data[4] = 4;
data[5] = register[0].byte[3];
data[6] = register[0].byte[2];
data[7] = register[0].byte[1];
data[8] = register[0].byte[0];
data[9] = 0;
for (i = 0; i < 9; i++)
data[9] += data[i];
for (i = 0; i < 10; i++)
Uart1_send_char(data[i]);
data[0] = 0xAA;
data[1] = 0xFF;
data[2] = 0;
data[3] = 5;
data[4] = 8;
data[5] = register[0].byte[3];
data[6] = register[0].byte[2];
data[7] = register[0].byte[1];
data[8] = register[0].byte[0];
data[9] = register[0].byte[3];
data[10] = register[0].byte[2];
data[11] = register[0].byte[1];
data[12] = register[0].byte[0];
data[13] = 0;
for (i = 0; i < 13; i++)
data[13] += data[i];
for (i = 0; i < 14; i++)
Uart1_send_char(data[i]);
delay_ms(20);
}
这段代码分两帧数据进行了数据传输。第一帧数据从0x0000地址开始,传输了一个数据(对应地址0x0000)。第二帧从0x0005开始,传输了两个数据(对应地址0x0005,0x0006)。
4.4 完整移植
使用全部功能,按照步骤移植,也很简单。
(1)工程里加入两个文件:transmission.h
和transmission.c
#ifndef __TRANSMISSION_H
#define __TRANSMISSION_H
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
union float_data
{
float f_data;
uint8_t byte[4];
};
extern union float_data float_register[255];
extern u8 send_buf[100]; // DMA传输缓冲区
u8 ComSendData(u16 addr, u8 len);
u8 cksum(u8 *data,u8 len);
void analyseData(u8 *data,u8 len);
#endif
#include "transmission.h"
#include "string.h"
u8 send_buf[100] = {0};
union float_data float_register[255];
u8 ComSendData(u16 addr, u8 num)
{
u8 i;
u8 len = 4*num+6; //一帧字节的长度
send_buf[0] = 0XAA; //帧头
send_buf[1] = 0XFF; //帧头 上行
send_buf[2] = addr << 8; //数据地址高8位
send_buf[3] = addr; //数据地址低8位
send_buf[4] = 4*num; //数据长度,单位为字节
for (i = 0; i < num; i++)
{
send_buf[5 + 4*i] = float_register[addr + i].byte[3]; //数据高8位
send_buf[6 + 4*i] = float_register[addr + i].byte[2];
send_buf[7 + 4*i] = float_register[addr + i].byte[1];
send_buf[8 + 4*i] = float_register[addr + i].byte[0]; //数据低8位
}
send_buf[len-1] = cksum(send_buf,len-1); // 先清零,避免上一帧数据这个位置有数据
for (i = 0; i < len; i++)
{
Uart1SendChar(send_buf[i]);
}
return 0;
}
// 相加校验,轻量计算
// 经使用,会出错,很奇怪
// 出错原因,sum没有初始化为0,晕
u8 cksum(u8 *data,u8 len)
{
u8 sum = 0;
u8 i;
for (i = 0; i < len; i++)
sum += data[i]; //计算校验和
return sum;
}
void analyseData(u8 *data,u8 len)
{
u16 addr,data_num;
u8 i;
u8 sum,sum1;
if (data[0] != 0xAA) return; // 帧头不对
if (data[1] != 0xAA && data[1] != 0x55 ) return; // 帧头不对
if ((u8)cksum(data,len-1) != data[len-1]) return; // (u8)强制类型转换要加上,不然出错
if (data[1] == 0x55) // 写指令
{
addr = (u16)data[2] << 8 | (u16)data[3];
data_num = data[4]/4; // 要更改的寄存器数据数量
for (i = 0; i < data_num;i++) // 更改寄存器值
{
float_register[addr + i].byte[3] = data[5 + 4*i];
float_register[addr + i].byte[2] = data[6 + 4*i];
float_register[addr + i].byte[1] = data[7 + 4*i];
float_register[addr + i].byte[0] = data[8 + 4*i];
}
ComSendData(addr,data_num); // 上传控制参数 写完之后上传回去
}
if (data[1] == 0xAA) // 读指令
{
addr = (u16)data[2] << 8 | (u16)data[3];
data_num = data[4]/4; // 要查询的寄存器数据数量
ComSendData(addr,data_num); // 上传控制参数 控制参数上传是慢速的。
}
}
这里面干了三件事:(1)定义了一个内存空间:union float_data float_register[255]
,内存大小可以自己修改,这里给的255。(2)定义上传函数:u8 ComSendData(u16 addr, u8 len)
,从起始地址上传len个数据。(3)定义解析函数:void analyseData(u8 *data,u8 len)
,解析一帧数据,如果是写指令,就更新float_register中的值,并且更新完了把新的数据上传上去。如果是读指令,就按照给定的起始地址和数据量上传数据。
代码很简单,核心是其中的思想,请读者自行体会~
(2)定义串口中断函数和串口发送函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
while (huart->Instance == USART1) //如果是串口1 // 有两种情况,0xAA AA是读指令,数据帧长度6个字节; 0xAA 55是写指令,数据长度 2N + 6字节
{
USART1_RX_BUF[USART1_RX_STA] = aRxBuffer1[0];
if (USART1_RX_STA == 0 && USART1_RX_BUF[0] != 0xAA) break; //帧头不对,丢掉
if (USART1_RX_STA == 1)
{
if( USART1_RX_BUF[1] != 0xAA && USART1_RX_BUF[1] != 0x55 )
{
USART1_RX_STA = 0;
break; //帧头不对,丢掉
}
}
USART1_RX_STA++;
if (USART1_RX_STA > USART_REC_LEN) USART1_RX_STA = 0; ///接收数据错误,重新开始接收
if ( (USART1_RX_BUF[1] == 0xAA && USART1_RX_STA == 6) || (USART1_RX_BUF[1] == 0x55 && USART1_RX_STA == USART1_RX_BUF[4] + 6) )
{
//analyseData(&USART1_RX_BUF[0]);
analyseData(USART1_RX_BUF, USART1_RX_STA);
LED3_TOGGLE();
memset(USART4_RX_BUF, 0, USART1_RX_STA);
USART1_RX_STA = 0;
}
break;
}
}
串口中断函数随便怎么定义,我这里给的是字节中断,你可以按DMA空闲中断去写都行,总之就是要按照前面给的通信协议,把上位机下发下来的“修改数据”或“查询数据”的数据帧解析出来,然后调用transmission.c
中的void analyseData(u8 *data,u8 len)
函数进行解析数据。
前面设计一个串口发送单个字节的函数int Uart1SendChar(u8 ch)
:
int Uart1SendChar(u8 ch)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR=(u8)ch;
return ch;
}
(3)测试代码:
提供的STM32工程里面,使用地址0x90作为正弦波和方波的幅值,地址0x91作为正弦波和方波的频率。地址0x00是正弦波值,地址0x01是方波值,每20ms上传一次数据。上位机通过修改0x90和0x91地址的数据,就可以更改正弦波和方波的幅值和频率。代码为:
//sign函数
float Sign(float input)
{
float output=0;
if(input>0)
{
output=1;
}
else if(input==0)
{
output=0;
}
else
{
output=-1;
}
return output;
}
int main(void)
{
uint8_t len;
uint16_t times = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
delay_init(168); /* 延时初始化 */
MX_USART1_UART_Init(); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
u32 run_cnt = 0;
u8 dt = 20;
u8 i;
float amplitude=100, frequency=0.5;
float_register[0x90].f_data = 100;
float_register[0x91].f_data = 0.5;
for (i = 0; i < 16; i++)
float_register[0x80+i].f_data = i; // 数据赋值, 测试上位机读数据
while(1)
{
// 测试上位机写数据
amplitude = float_register[0x90].f_data;
frequency = float_register[0x91].f_data;
// 测试从机主动上传数据
float_register[0].f_data = amplitude * sin(2*3.14*run_cnt*frequency*dt*0.001);
float_register[1].f_data = amplitude * Sign(float_register[0].f_data);
ComSendData(0,2); // 0地址开始3个数据
delay_ms(dt); // 采样周期
if (run_cnt++ % 10 == 0) LED2_TOGGLE();
}
}
效果如图。开始时幅值为100,频率为0.5Hz,修改为1Hz、幅值200,波形变化了两次。
写在最后
(1)由于本软件是通用软件,所以都是以“地址0x80”之类的命名,而不是参数的实际意义,例如“PID参数”、“前进速度”等。这就需要聪明的你自己在脑袋里定义了。鱼和熊掌不可兼得。
(2)开发上位机有很多软件,如LabWindows、labview、Qt、C#等,包括万能的Matlab,各有利弊。就本人近几年的开发经验,Matlab的实时性是灾难,LabWindows界面太丑、Labview的非编程开发方式习惯不了。相对而言,Qt功能是最为强大的,很适合实时系统,大名鼎鼎的飞控地面站QGroundControl就是Qt开发的。想学习上位机编程的强烈建议Qt。
(3)本软件及其配套STM32示例工程下载需要一点点费用。一方面是作者的劳动成果,请养成知识付费的思想和行动自觉,本人平时也是经常性为知识付费,花几块几十块钱解决大问题是非常非常划算的。另一方面,付费的东西能够帮助你筛选掉一些无用资源。使用过程中有任何问题欢迎私信。本软件的下载地址:嵌入式系统远程测控软件-串口示波器-远程修改参数
(4) 上位机软件源码后续也会陆续开源(一定的知识付费),供有兴趣的伙伴学习并定义自己的界面。