今年在电赛的准备中,发现没有一个合适的波形显示器,显示电压电流的变化曲线。所有的调差,我都感觉我在瞎调,让我觉得着实不舒服。于是我花了两天的时间学了下Qt的基本使用,然后就开始撸我需要的这个工具—可进行PID调参的串口示波器。前前后后用了3天半的时间吧,前端的设计都很简单,没有什么说的。哦,这里吹一手QCustomPlot,这是一个绘图Widget,非常好用,支持风格设置,拖拽,放大缩小,很是适合做示波器。其次就是QSerialPort串口接收大量数据时,会造成数据分段。正是由于这种分段,导致了单次收到的数据是不完整的,因此需要定义解析数据的方式,增加帧头和帧尾。
先看效果
先看源码的同学,直接去我的github上面吧:https://github.com/Miller-em/UART_Oscillscope,要是觉得不错的话,欢迎fork和star😃。
QCustomPlot的使用
Qt的串口的收发,怎么用的,网上一大推,我这里就不重复说一个内容了。这一节,主要聊聊,我是怎么实现customPlot的背景设置,拖拽,缩放等功能的。QCustomPlot这个是继承自QWidget,所以在Qt设计师中,直接拖一个Widget就好了,然后选择提升为QCustomPlot。然后就可以去编辑区写自己的业务需要了。
下面就进行程序开始时的配置,设置背景色,拖拽,缩放,可选和坐标range。
void MainWindow::buildChart()
{
/*--------------------------------
* 构造示波器
*-------------------------------*/
ui->customPlot->setBackground(QBrush(QColor("#474848"))); //
ui->customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables); //可拖拽+可滚轮缩放
ui->customPlot->yAxis->setRange(0, 3.3);
}
接收串口数据并绘图
这个地方,卡了我差不多一天,因为串口的ReadAll()
那个API,读数据会断节,不是一条完整的数据,就不能完成绘图。然后我就找了很多解决方案,有人说用延时,我个人感觉不太靠谱(在我这种开发场景下)。另外一个就是加数据帧头和帧尾。然后我又觉得,网上定义那种复杂的数据解析格式,实在不好写,好吧,是我太菜了。思前想后,于是我想到了利用Json数据进行数据传输,而帧头和帧尾恰好是{
和}
,然后我更加偷懒,只检查{
的位置,然后对断节的信息,进行拼接。
下面是实现代码:
void MainWindow::receiveInfo()
{
/*--------------------------------
* 接受串口信息
*-------------------------------*/
QString data;
QByteArray info = global_port.readAll();
QStringList list;
if(!info.isEmpty())
{
if (info.contains('{')){
data = info;
// 下面是数据解析
list= data.split("{");
if (list.length() == 2){
if (frontData.isEmpty()){
frontData = list.at(1);
frontData.insert(0,'{');
return;
}
oneData = frontData + list.at(0);
frontData = list.at(1);
frontData.insert(0,'{');
plotCustom(oneData.toLocal8Bit()); //绘图
qDebug() << "One data: " << oneData;
}
if (list.length() == 3){
oneData = frontData + list.at(0);
plotCustom(oneData.toLocal8Bit()); //绘图
qDebug() << "One data: " << oneData;
oneData = list.at(1);
oneData.insert(0,'{');
plotCustom(oneData.toLocal8Bit()); //绘图
qDebug() << "One data: " << oneData;
frontData = list.at(2);
frontData.insert(0,'{');
}
}
}
}
这是绘图函数:
void MainWindow::plotCustom(QByteArray info)
{
//避免中文乱码
QTextCodec *tc = QTextCodec::codecForName("GBK");
QString tmpQStr = tc->toUnicode(info);
// 若示波器打开,开始解析数据
if (oscill_flag){
QStringList datakeys;
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(info, &jsonError);
if (jsonError.error != QJsonParseError::NoError)
{
qDebug() << "parse json object failed, " << jsonError.errorString();
ui->textBrowser->append("parse json object failed!");
return;
}
QJsonObject jsonObj = jsonDoc.object();
qDebug() << "parse json object Successfully";
datakeys = jsonObj.keys(); // 获取通道名称
qDebug() << datakeys;
if (Received_Keys != datakeys){
// 只能够设置一次标签
qDebug() << "update keys";
Received_Keys = datakeys;
this->index = 0;
this->Ptext.clear();
this->XData.clear();
this->YData.clear();
// 清除画布
ui->customPlot->clearGraphs();
ui->customPlot->legend->setVisible(false);
ui->customPlot->replot();
ui->customPlot->legend->setVisible(true); //右上角指示曲线的缩略框
ui->customPlot->legend->setBrush(QColor(100, 100, 100, 0));//设置图例背景颜色,可设置透明
ui->customPlot->legend->setTextColor(Qt::white);
for (int i=0; i < datakeys.size(); i++){
ui->customPlot->addGraph();
ui->customPlot->graph(i)->setPen(QPen(color[i]));
ui->customPlot->graph(i)->setName(datakeys[i]);
Ptext.push_back(datakeys[i]);
}
}
// 添加XData , YData 数据
XData.push_back(index);
QVector<double> Data;
YData.resize(datakeys.size());
for (int i = 0; i < Ptext.size(); ++i) {
Data.push_back(jsonObj.value(datakeys[i]).toDouble());
}
for (int i = 0; i < Ptext.size(); ++i) {
YData[i].push_back(Data[i]);
}
//向坐标值赋值
for (int i=0; i < datakeys.size(); ++i){
ui->customPlot->graph(i)->addData(XData[index], YData[i][index]);
}
this->index++;
// 更新坐标
ui->customPlot->xAxis->setRange((ui->customPlot->graph(0)->dataCount()>1000)?
(ui->customPlot->graph(0)->dataCount()-1000):
0,
ui->customPlot->graph(0)->dataCount());
ui->customPlot->replot(QCustomPlot::rpQueuedReplot); //实现重绘
}
// 向接收区打印
ui->textBrowser->append(tmpQStr);
qDebug() << "Received Data: " << tmpQStr;
}
不过,我这样解决问题有些取巧,对于那种很长的字符串,解析效果也是不好,会出现丢帧的情况。大概在50个字符以内基本还是不会出现丢数据的情况。后面如果想到更好的数据解析方式,我会做更改。
STM32 测试程序
由于我们的整个系统都是依靠的Json进行数据的传输,所以单片机那边发的数据就要是Json格式的数据,我这里使用的cJson的库,具体如何使用我也不准备说了,网上很多例子。
- 导入头文件
-
然后在main函数里面向上位机串口发送两个正选数据:
int main(void) { /* USER CODE BEGIN 1 */ cJSON *cJson_test = NULL; //创建头指针 char* str = NULL; cJson_test = cJSON_CreateObject(); //创建头结点,并将头指针指向头结点 /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_USART1_UART_Init(); /* USER CODE BEGIN 2 */ cJSON_AddNumberToObject(cJson_test, "ADC1", 0); cJSON_AddNumberToObject(cJson_test, "ADC2", 0); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { double target_val; HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // printf("{\"ADC1\":1.333,\"ADC2\":2.555}"); // printf("{\"ADC1\":1.333}"); for (int i = 0; i < N; ++i){ target_val = (int)((sin(2*PI*i/N))*1000+0.5)/1000.0; cJSON_ReplaceItemInObject(cJson_test, "ADC1", cJSON_CreateNumber(target_val)); target_val = (int)((cos(2*PI*i/N))*1000+0.5)/1000.0; cJSON_ReplaceItemInObject(cJson_test, "ADC2", cJSON_CreateNumber(target_val)); str = cJSON_PrintUnformatted(cJson_test); printf("%s", str); } /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }