加入日志功能;添加窗口状态自动保存;单次读取字节数限制由上位机转到下位机;略降低采样速度以降低CPU占用率;版本号更新为V1.2.0

This commit is contained in:
skythinker 2022-02-18 22:36:23 +08:00
parent c00b423d7a
commit 3816ad6402
22 changed files with 580 additions and 63 deletions

View File

@ -11,7 +11,7 @@ CONFIG += c++11
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS \
APP_VERSION=\\\"1.1.0\\\"
APP_VERSION=\\\"1.2.0\\\"
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
@ -24,6 +24,7 @@ SOURCES += \
graphwindow.cpp \
helpwindow.cpp \
listwindow.cpp \
logwindow.cpp \
main.cpp \
mainwindow.cpp \
openocd.cpp \
@ -35,6 +36,7 @@ HEADERS += \
graphwindow.h \
helpwindow.h \
listwindow.h \
logwindow.h \
mainwindow.h \
openocd.h \
serialocd.h \
@ -45,6 +47,7 @@ FORMS += \
graphwindow.ui \
helpwindow.ui \
listwindow.ui \
logwindow.ui \
mainwindow.ui
RC_ICONS = icon.ico

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE QtCreatorProject>
<!-- Written by QtCreator 4.11.0, 2022-02-03T22:09:38. -->
<!-- Written by QtCreator 4.11.0, 2022-02-18T22:25:23. -->
<qtcreator>
<data>
<variable>EnvironmentId</variable>

View File

@ -12,7 +12,9 @@
* 变量值**波形实时绘制**
* 采样数据导出到CSV表格
* **采样数据导出**到CSV表格
* 格式化**日志**输出
* 调试器模式理论上支持OpenOCD允许GDB连接的各种调试器及硬件芯片如STLink、JLink、CMSIS-DAP等以及STM32全系列等
@ -30,9 +32,13 @@
## 使用方法
1. 若使用串口连接,需先将下位机程序移植到目标芯片中 [移植说明](lower/README.md)
1. 若使用串口连接,需先将串口下位机程序移植到目标芯片中 [串口移植说明](lower/serial/README.md)
2. 点击设置符号文件,添加变量
2. 若使用日志功能,需先将日志下位机程序移植到目标芯片中[日志移植说明](lower/log/README.md)
> 注:串口与日志功能不冲突,可以同时使用
3. 点击设置符号文件,添加变量
* 加载符号文件后即可在变量选择窗口添加需要查看的变量,可以直接添加到列表或添加到编辑框后进行手动编辑
@ -42,20 +48,22 @@
* 变量名不仅可以填入单个变量名还可以填入合法的C语言表达式GDB支持即可复合类型不能修改和绘图只能实时查看
3. 选择连接模式,连接芯片,连接后程序开始循环采样
4. 选择连接模式,连接芯片,连接后程序开始循环采样
* 调试器模式下,在下拉框中选择调试器和芯片类型,点击连接目标
* 串口模式下,点击刷新串口加载串口列表,选中所连接的串口,点击连接目标
4. 编辑`修改变量`列可以修改变量值,双击`图线颜色`列可以选择绘图颜色
5. 编辑`修改变量`列可以修改变量值,双击`图线颜色`列可以选择绘图颜色
5. 单击`变量名`列选中对应的变量,绘图窗口会加粗绘制波形,左下角会显示当前值和查看值(拖动鼠标进行查看)
6. 单击`变量名`列选中对应的变量,绘图窗口会加粗绘制波形,左下角会显示当前值和查看值(拖动鼠标进行查看)
6. 绘图界面说明可以在绘图窗口点击操作说明查看,滚轮配合`Ctrl`、`Shift`、`Alt`可以实现画面的缩放和移动
7. 绘图界面说明可以在绘图窗口点击操作说明查看,滚轮配合`Ctrl`、`Shift`、`Alt`可以实现画面的缩放和移动
![操作演示](imgs/oper-sample.gif)
![日志输出](imgs/log-sample.gif)
---
## 主要菜单项说明
@ -82,6 +90,8 @@
* 本程序不带下载功能,连接目标前请确认已为目标芯片下载过指定程序;若更换为不同类型的调试器,即使芯片程序没有变动,也应使用更换后的调试器再次下载程序
* 下位机程序应使用与上位机同时发行的版本,更新上位机软件后应同时更新下位机程序
---
## 已知问题及解决方法
@ -92,6 +102,18 @@
---
## 其他说明
* 关于采样速度
* 采样速度与CPU占用率、添加的变量数量、日志输出频率等因素相关程序会以尽可能高的速度进行采样
* 简介中介绍的采样速度是在`i5-8265U`CPU接近空载时添加单个变量并关闭日志的情况下测试得到的
* 调试器模式下获取单条日志用时约50ms串口模式约90ms获取过程中无法进行采样若日志数量较多则会对采样速度造成较大影响
---
## TODO
* 增加对不同文件中同名变量的区分
@ -114,6 +136,8 @@
* 变量选择窗口另开有一个GDB进程用于解析符号文件内部使用树形结构存储各级变量信息在用户每展开一级树形图时使用`info variables`、`whatis`、`ptype`指令从GDB获取下一级变量类型信息并用正则表达式解析
* 日志下位机程序会创建一个缓冲区队列下位机会将日志输出到缓冲区中程序会定时使用GDB的print指令将日志出队并解析显示出来
---
## 仓库文件说明

View File

@ -91,7 +91,7 @@
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;hr width=&quot;300&quot;/&gt;&lt;p align=&quot;center&quot;&gt;版本号V1.1.0&lt;/p&gt;&lt;p align=&quot;center&quot;&gt;更新时间2022/2/3&lt;/p&gt;&lt;hr width=&quot;300&quot;/&gt;&lt;p align=&quot;center&quot;&gt;Developed by Skythinker&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;hr width=&quot;300&quot;/&gt;&lt;p align=&quot;center&quot;&gt;版本号V1.2.0&lt;/p&gt;&lt;p align=&quot;center&quot;&gt;更新时间2022/2/18&lt;/p&gt;&lt;hr width=&quot;300&quot;/&gt;&lt;p align=&quot;center&quot;&gt;Developed by Skythinker&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>

View File

@ -17,7 +17,7 @@ QString GDBProcess::runCmd(const QString &cmd)
process->write(cmd.toStdString().c_str());
QString res="";
do{
process->waitForReadyRead(0);
process->waitForReadyRead(1);
res+=process->readAllStandardOutput();
}while(!res.endsWith("(gdb) "));
return res;
@ -112,6 +112,34 @@ bool GDBProcess::getDoubleFromDisplayValue(const QString &rawValue, double &resu
return true;
}
//将gdb所返回的无符号数组变量值转换为uint数组
QList<uint> GDBProcess::getUintArrayFromDisplay(const QString &rawDisplay)
{
QRegExp rx("\\{(.*)\\}");
rx.indexIn(rawDisplay);
QString raw=rx.cap(1);
raw=raw.replace("\r\n ","");
QStringList rawList=raw.split(", ");
QList<uint> numList;
for(int index=0;index<rawList.size();index++)
{
if(rawList[index].contains('<'))//查找并展开重复值
{
QRegExp repeatRx("(\\d+)\\s<repeats\\s(\\d+)\\stimes>");
repeatRx.indexIn(rawList[index]);
int num=repeatRx.cap(1).toUInt();
int times=repeatRx.cap(2).toUInt();
for(int i=0;i<times;i++)
numList<<num;
}
else
{
numList<<rawList[index].toUInt();
}
}
return numList;
}
//命令GDB设定变量值
void GDBProcess::setVarValue(const QString &varFullName, double value)
{

View File

@ -26,6 +26,7 @@ public:
void setDisplayList(QStringList &list);
QString captureValueFromDisplay(const QString &rawDisplay,const QString &name);
bool getDoubleFromDisplayValue(const QString &rawValue,double &result);
QList<uint> getUintArrayFromDisplay(const QString &rawDisplay);
void setVarValue(const QString &varFullName,double value);
bool checkExpandableType(const QString &varFullName);
QStringList getVarListFromRawOutput(const QString &rawVarList);

View File

@ -32,9 +32,9 @@
<property name="geometry">
<rect>
<x>0</x>
<y>-735</y>
<y>-390</y>
<width>523</width>
<height>1179</height>
<height>1308</height>
</rect>
</property>
<property name="autoFillBackground">
@ -53,7 +53,7 @@
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;LinkScope简介&lt;/span&gt;&lt;/p&gt;&lt;p&gt;本程序使用QT编写用于硬件设备的调试可直接驱动串口或各种调试器基于OpenOCD支持有以下几个主要功能&lt;/p&gt;&lt;p&gt;1.实时查看和修改变量值&lt;/p&gt;&lt;p&gt;2.变量值波形实时绘制&lt;/p&gt;&lt;p&gt;3.采样数据导出为CSV表格&lt;/p&gt;&lt;p&gt;调试器连接模式理论上支持OpenOCD允许GDB连接的各种调试器及硬件芯片如STLink、JLink、CMSIS-DAP等以及STM32全系列等&lt;/p&gt;&lt;hr/&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;使用方法&lt;/span&gt;&lt;/p&gt;&lt;p&gt;1.点击【设置符号文件】,添加变量&lt;/p&gt;&lt;p&gt;&amp;gt; 加载符号文件后即可在变量选择窗口添加需要查看的变量,可以直接添加到列表或添加到编辑框后进行手动编辑&lt;/p&gt;&lt;p&gt;&amp;gt; 在主窗口表格最后一行变量名(编辑框)处手动填写也可以添加变量&lt;/p&gt;&lt;p&gt;&amp;gt; 在主窗口中选中变量名按Del键可删除变量&lt;/p&gt;&lt;p&gt;&amp;gt; 变量名不仅可以填入单个变量名还可以填入合法的C语言表达式GDB支持即可复合类型不能修改和绘图只能实时查看&lt;/p&gt;&lt;p&gt;2.选择连接模式,连接芯片,连接后程序开始循环采样&lt;/p&gt;&lt;p&gt;&amp;gt; 调试器模式下,在下拉框中选择调试器和芯片类型,点击连接目标&lt;/p&gt;&lt;p&gt;&amp;gt; 串口模式下,点击刷新串口加载串口列表,选中所连接的串口,点击连接目标&lt;/p&gt;&lt;p&gt;3.编辑【修改变量】列可以修改变量值,双击【图线颜色】列可以选择绘图颜色 &lt;/p&gt;&lt;p&gt;4.单击【变量名】列选中对应的变量,可以在绘图窗口查看历史数据,并会加粗绘制 &lt;/p&gt;&lt;p&gt;5.绘图界面说明请到绘图窗口点击操作说明 &lt;/p&gt;&lt;p&gt;6.点击菜单中的保存/导入配置可以将当前配置保存到INI文件或从文件中恢复配置点击导出数据可以将获取到的采样数据导出到CSV表格文件&lt;/p&gt;&lt;hr/&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;注意事项 &lt;/span&gt;&lt;/p&gt;&lt;p&gt;1.修改符号文件路径后需要重新连接 &lt;/p&gt;&lt;p&gt;2.连接目标前请确认已使用该调试器为目标芯片下载过指定程序 &lt;/p&gt;&lt;p&gt;3.若程序闪退后发现下一次运行时无法连接目标请尝试手动结束openocd.exe进程 &lt;/p&gt;&lt;p&gt;4.连接配置文件位于openocd/share/openocd/scripts下的target和interface中用户可按照openocd语法编写配置脚本放入对应目录下后点击“刷新连接配置”菜单项&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;LinkScope简介&lt;/span&gt;&lt;/p&gt;&lt;p&gt;本程序使用QT编写用于硬件设备的调试可直接驱动串口或各种调试器基于OpenOCD支持有以下几个主要功能&lt;/p&gt;&lt;p&gt;1.实时查看和修改变量值&lt;/p&gt;&lt;p&gt;2.变量值波形实时绘制&lt;/p&gt;&lt;p&gt;3.采样数据导出为CSV表格&lt;/p&gt;&lt;p&gt;4.格式化日志输出&lt;/p&gt;&lt;p&gt;调试器连接模式理论上支持OpenOCD允许GDB连接的各种调试器及硬件芯片如STLink、JLink、CMSIS-DAP等以及STM32全系列等&lt;/p&gt;&lt;hr/&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;使用方法&lt;/span&gt;&lt;/p&gt;&lt;p&gt;1.点击【设置符号文件】选择编译器输出的符号文件然后添加变量到列表中&lt;/p&gt;&lt;p&gt;&amp;gt; 加载符号文件后即可在变量选择窗口添加需要查看的变量,可以直接添加到列表或添加到编辑框后进行手动编辑&lt;/p&gt;&lt;p&gt;&amp;gt; 在主窗口表格最后一行变量名(编辑框)处手动填写也可以添加变量&lt;/p&gt;&lt;p&gt;&amp;gt; 在主窗口中选中变量名按Del键可删除变量&lt;/p&gt;&lt;p&gt;&amp;gt; 变量名不仅可以填入单个变量名还可以填入合法的C语言表达式GDB支持即可复合类型不能修改和绘图只能实时查看&lt;/p&gt;&lt;p&gt;2.选择连接模式,连接芯片,连接后程序开始循环采样,勾选【监视日志】后即可将下位机输出的日志实时显示出来&lt;/p&gt;&lt;p&gt;&amp;gt; 调试器模式下,在下拉框中选择调试器和芯片类型,点击连接目标&lt;/p&gt;&lt;p&gt;&amp;gt; 串口模式下,点击刷新串口加载串口列表,选中所连接的串口,点击连接目标&lt;/p&gt;&lt;p&gt;&amp;gt; 串口功能和日志功能都需要修改下位机程序,可以在菜单中进入主页查看说明&lt;/p&gt;&lt;p&gt;3.编辑【修改变量】列可以修改变量值,双击【图线颜色】列可以选择绘图颜色 &lt;/p&gt;&lt;p&gt;4.单击【变量名】列选中对应的变量,可以在绘图窗口查看历史数据,并会加粗绘制 &lt;/p&gt;&lt;p&gt;5.绘图界面说明请到绘图窗口点击操作说明 &lt;/p&gt;&lt;p&gt;6.点击菜单中的保存/导入配置可以将当前配置保存到INI文件或从文件中恢复配置点击导出数据可以将获取到的采样数据导出到CSV表格文件&lt;/p&gt;&lt;hr/&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;注意事项 &lt;/span&gt;&lt;/p&gt;&lt;p&gt;1.修改符号文件路径后需要重新连接 &lt;/p&gt;&lt;p&gt;2.连接目标前请确认已使用该调试器为目标芯片下载过指定程序 &lt;/p&gt;&lt;p&gt;3.若程序闪退后发现下一次运行时无法连接目标请尝试手动结束openocd.exe进程 &lt;/p&gt;&lt;p&gt;4.连接配置文件位于openocd/share/openocd/scripts下的target和interface中用户可按照openocd语法编写配置脚本放入对应目录下后点击“刷新连接配置”菜单项&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::AutoText</enum>

BIN
imgs/log-sample.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 160 KiB

148
logwindow.cpp Normal file
View File

@ -0,0 +1,148 @@
#include "logwindow.h"
#include "ui_logwindow.h"
LogWindow::LogWindow(QWidget *parent) :
QDialog(parent),
ui(new Ui::LogWindow)
{
ui->setupUi(this);
setWindowTitle("LinkScope - Log");
tableModel=new QStandardItemModel(this);//创建表格数据
ui->tb_log->setModel(tableModel);//绑定表格数据
ui->tb_log->horizontalHeader()->setMinimumSectionSize(150);//设置最小列宽
ui->tb_log->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);//列宽可由用户调整
ui->tb_log->verticalHeader()->setSectionResizeMode(QHeaderView::Stretch);//行高自动拉伸
ui->tb_log->verticalHeader()->hide();//隐藏垂直表头,否则滚动语句执行时间较长
drawTableHeader();//设置表头
}
LogWindow::~LogWindow()
{
delete ui;
delete tableModel;
}
//添加一条日志
void LogWindow::addLog(char attr, const QString &tag, const QString &msg, qulonglong time, const QString &func)
{
//计算日志类型字符对应的颜色和属性名
QColor textColor=Qt::black;
QString attrText="Unknown";
switch(attr)
{
case 'i':
textColor=QColor("#218B47");
attrText="Info";
break;
case 'd':
textColor=QColor("#0046EB");
attrText="Debug";
break;
case 'w':
textColor=QColor("#FF8C00");
attrText="Warn";
break;
case 'e':
textColor=QColor("#EB005F");
attrText="Error";
break;
}
//创建各单元格数据
QList<QStandardItem*> items;
items<<new QStandardItem(attrText)
<<new QStandardItem(tag)
<<new QStandardItem(msg)
<<new QStandardItem(QString("%1:%2:%3.%4")
.arg(time/1000/3600,2,10,QChar('0'))
.arg(time/1000/60%60,2,10,QChar('0'))
.arg(time/1000%60,2,10,QChar('0'))
.arg(time%1000,3,10,QChar('0')))
<<new QStandardItem(func);
for(int i=0;i<5;i++)
{
items[i]->setForeground(QBrush(textColor));//设置文字颜色
items[i]->setFlags(Qt::ItemIsEnabled);//设置为不可编辑
}
//若当前表格滚动到底端则在插入新行后自动滚动到底端
bool scrollAtEnd=ui->tb_log->verticalScrollBar()->value()==ui->tb_log->verticalScrollBar()->maximum();
tableModel->insertRow(tableModel->rowCount(),items);//在表格末尾插入新行
if(scrollAtEnd)
ui->tb_log->scrollToBottom();
}
//清空所有日志
void LogWindow::clearLog()
{
QByteArray horiHeaderState=ui->tb_log->horizontalHeader()->saveState();//记录列宽
tableModel->clear();
drawTableHeader();
ui->tb_log->horizontalHeader()->restoreState(horiHeaderState);//恢复列宽
}
//绘制表头
void LogWindow::drawTableHeader()
{
tableModel->setColumnCount(5);
tableModel->setHeaderData(0,Qt::Horizontal,"类型");
tableModel->setHeaderData(1,Qt::Horizontal,"TAG");
tableModel->setHeaderData(2,Qt::Horizontal,"MSG");
tableModel->setHeaderData(3,Qt::Horizontal,"时间戳");
tableModel->setHeaderData(4,Qt::Horizontal,"函数名");
}
//将表格数据导出到CSV文件
bool LogWindow::exportCSV(const QString &filename)
{
QFile file(filename);
if(!file.open(QFile::WriteOnly))
return false;
QTextStream outStream(&file);//使用流方式输出
outStream<<"Type,TAG,MSG,Timestamp,Function\n";
for(int row=0;row<tableModel->rowCount();row++)
{
for(int col=0;col<tableModel->columnCount();col++)
outStream<<tableModel->item(row,col)->text()<<',';
outStream<<'\n';
}
return true;
}
//清空日志按钮点击
void LogWindow::on_bt_clear_clicked()
{
clearLog();
}
//导出日志按钮点击
void LogWindow::on_bt_export_clicked()
{
//弹出文件选择框
QFileDialog *fileDialog = new QFileDialog(this);
fileDialog->setWindowTitle(QStringLiteral("选中文件"));
fileDialog->setDirectory(".");
fileDialog->setNameFilter(tr("CSV File (*.csv)"));
fileDialog->setFileMode(QFileDialog::AnyFile);
fileDialog->setViewMode(QFileDialog::Detail);
if(fileDialog->exec())//等待用户选择文件
{
QStringList fileList=fileDialog->selectedFiles();
QString fileName=fileList.at(0);
if(!fileName.endsWith(".csv"))//若用户输入的文件名不以csv结尾则加上后缀
fileName.append(".csv");
QFileInfo file(fileName);//判断是否已经存在文件,若存在则弹出覆盖警告框
if(file.exists())
{
QMessageBox messageBox(QMessageBox::NoIcon,"警告", "文件已存在,是否覆盖?",QMessageBox::Yes | QMessageBox::No, NULL);
if(messageBox.exec()!=QMessageBox::Yes)
return;
}
if(!exportCSV(fileName))//将数据写入文件
{
QMessageBox::information(this,"错误","文件打开失败,请检查文件是否被占用");
}
}
}

38
logwindow.h Normal file
View File

@ -0,0 +1,38 @@
#ifndef LOGWINDOW_H
#define LOGWINDOW_H
#include <QDialog>
#include <qstandarditemmodel.h>
#include <qscrollbar.h>
#include <QTime>
#include <qdebug.h>
#include <qfile.h>
#include <qfiledialog.h>
#include <qmessagebox.h>
namespace Ui {
class LogWindow;
}
class LogWindow : public QDialog
{
Q_OBJECT
public:
explicit LogWindow(QWidget *parent = nullptr);
~LogWindow();
void addLog(char attr,const QString &tag,const QString &msg,qulonglong time,const QString &func);
void clearLog();
private slots:
void on_bt_clear_clicked();
void on_bt_export_clicked();
private:
Ui::LogWindow *ui;
QStandardItemModel *tableModel;//表格数据
void drawTableHeader();
bool exportCSV(const QString &filename);
};
#endif // LOGWINDOW_H

42
logwindow.ui Normal file
View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LogWindow</class>
<widget class="QDialog" name="LogWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>828</width>
<height>486</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTableView" name="tb_log"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="bt_clear">
<property name="text">
<string>清空日志</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="bt_export">
<property name="text">
<string>导出日志</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

71
lower/log/README.md Normal file
View File

@ -0,0 +1,71 @@
# 日志下位机程序说明
---
## 配置项
* **标准库函数配置项**
* `#define LOG_MEMCPY(dst,src,len)`:内存拷贝函数,默认使用`string.h`中的`memcpy`函数
* `#define LOG_SPRINTF(buf,fmt,...)`:格式化字符串打印函数,默认使用`stdio.h`中的`sprintf`函数
* `#define LOG_STRLEN(str)`:字符串长度计算函数,默认使用`string.h`中的`strlen`函数
> 注:标准库函数配置项是为了平台不支持标准库函数的情况设计的,一般无需修改
* **缓冲区大小配置项**
* `#define LOG_MAX_LEN`单条日志数据最大长度包含日志标签、日志内容、时间戳、函数名等数据建议不小于100
* `#define LOG_MAX_QUEUE_SIZE`缓冲区能存放的最大日志条数需根据日志打印频率确定默认值为10
> 注:缓冲区总占用内存大小为上述两项的乘积,若短时间写入多条日志可能导致缓冲区溢出,后输出的日志将被丢弃
* **其他配置项**
* `#define LOG_GET_MS()`:获取时间戳接口,需返回系统启动以来的毫秒数
* `#define LOG_GET_FUNC_NAME()`:获取宏展开位置的函数名,一般由编译器提供`__FUNCTION__`宏实现,若平台不支持可替换为空字符串`("")`
* `#define LOG_ENABLE`:日志输出使能项,若注释掉则所有输出语句将被替换为空语句
---
## 日志输出接口
* `#define LOG_INFO(tag,msg,...)`:输出信息日志,`tag`为日志标签,`msg`为日志内容,日志内容中可包含格式化占位符,与后面的可变参数一起进行格式化输出
> 例:`LOG_INFO("sys","code=%d",123);//输出的日志标签为"sys",日志内容为"code=123"`
* `#define LOG_DEBUG(tag,msg,...)`:输出调试日志,用法与信息日志相同
* `#define LOG_WARN(tag,msg,...)`:输出警告日志,用法与信息日志相同
* `#define LOG_ERROR(tag,msg,...)`:输出错误日志,用法与信息日志相同
---
## 移植说明
1. 将`log.h`和`log.c`添加到工程目录中
> 注:若不想新增`log.c`文件或编译器不支持多个源文件,可以将其中的内容放入任何一个源文件中
2. 根据平台修改配置项
* 一般情况下只需修改时间戳配置即可,其余参数可以保持默认
* 若下位机内存不够,或希望输出更长的日志,或希望一次性写入更多条日志,可修改日志缓冲区大小配置
* 若编译器不支持`__FUNCTION__`宏,需要将函数名宏定义替换为空字符串
* 若编译器不支持标准库函数,需要自行实现各函数并替换到对应宏定义中
3. 在需要使用日志的文件中引用`log.h`,然后调用接口进行日志输出即可,上位机会定时查看并移除日志缓冲区中的数据
---
## 注意事项
* 日志输出频率不能持续过高上位机读取速度约为10条/s若长时间高于该频率输出日志会使缓冲区溢出而导致日志丢失

6
lower/log/log.c Normal file
View File

@ -0,0 +1,6 @@
#include "log.h"
#ifdef LOG_ENABLE
//日志缓冲区定义
LogQueue logQueue={LOG_MAX_QUEUE_SIZE};
#endif

67
lower/log/log.h Normal file
View File

@ -0,0 +1,67 @@
#ifndef _LOG_H_
#define _LOG_H_
/**************↓配置区↓**************/
//标准库头文件
//若不希望使用对应函数可注释头文件后修改下方函数宏实现相应功能
#include <stdio.h> //使用sprintf
#include <string.h> //使用memcpy、strlen
//标准库函数移植宏
#define LOG_MEMCPY(dst,src,len) memcpy(dst,src,len) //内存拷贝
#define LOG_SPRINTF(buf,fmt,...) sprintf(buf,fmt,##__VA_ARGS__) //格式化字符串
#define LOG_STRLEN(str) strlen(str) //计算字符串长度
//日志缓冲区大小,总大小为下列两个值的乘积
#define LOG_MAX_LEN 100 //单条日志缓冲区大小包含日志内容、时间戳、函数名等信息建议不小于100
#define LOG_MAX_QUEUE_SIZE 10 //缓冲区可存日志条数
//获取时间戳接口(启动后经过的毫秒数)
#define LOG_GET_MS() HAL_GetTick() //本例使用STM32-HAL
//获取语句所在函数名的宏(一般由编译器提供)
#define LOG_GET_FUNC_NAME() (__FUNCTION__)
//是否开启日志输出(注释掉则所有输出语句会替换为空语句)
#define LOG_ENABLE
/**************↑配置区↑**************/
/**************↓日志输出接口↓**************/
//信息日志
#define LOG_INFO(tag,msg,...) LOG_ADD_FORMAT('i',(tag),(msg),##__VA_ARGS__)
//调试日志
#define LOG_DEBUG(tag,msg,...) LOG_ADD_FORMAT('d',(tag),(msg),##__VA_ARGS__)
//警告日志
#define LOG_WARN(tag,msg,...) LOG_ADD_FORMAT('w',(tag),(msg),##__VA_ARGS__)
//错误日志
#define LOG_ERROR(tag,msg,...) LOG_ADD_FORMAT('e',(tag),(msg),##__VA_ARGS__)
/**************↑日志输出接口↑**************/
//日志缓冲区类型,以队列方式存储
typedef struct{
int maxSize; //队列最大长度初始化时须赋值为LOG_MAX_QUEUE_SIZE
char buf[LOG_MAX_QUEUE_SIZE][LOG_MAX_LEN]; //实际存储空间
int lenBuf[LOG_MAX_QUEUE_SIZE]; //与buf一一对应表示存储的日志长度
int size,startPos;//当前队列长度和队头位置
}LogQueue;
#ifdef LOG_ENABLE
//日志输出集中处理,添加一条日志到缓冲区
#define LOG_ADD_FORMAT(attr,tag,msg,...) if(logQueue.size<LOG_MAX_QUEUE_SIZE){ \
int index=(logQueue.startPos+logQueue.size+1)%LOG_MAX_QUEUE_SIZE; \
char *bufStartAddr=logQueue.buf[index],*buf=bufStartAddr; int termLen=0; \
*buf=attr; buf+=1; \
termLen=LOG_STRLEN(tag)+1; LOG_MEMCPY(buf,tag,termLen); buf+=termLen; \
LOG_SPRINTF(buf,msg,##__VA_ARGS__); termLen=LOG_STRLEN(buf)+1; buf+=termLen; \
LOG_SPRINTF(buf,"%d",LOG_GET_MS()); termLen=LOG_STRLEN(buf)+1; buf+=termLen; \
termLen=LOG_STRLEN(LOG_GET_FUNC_NAME())+1; LOG_MEMCPY(buf,LOG_GET_FUNC_NAME(),termLen); buf+=termLen; buf+=termLen; \
logQueue.lenBuf[index]=buf-bufStartAddr; logQueue.size++; \
}
//声明缓冲区定义,实际定义可在任意一个源文件中
extern LogQueue logQueue;
#else
#define LOG_ADD_FORMAT(attr,tag,msg,...)
#endif
#endif

View File

@ -1,4 +1,4 @@
# 下位机程序说明
# 串口下位机程序说明
---

View File

@ -89,6 +89,8 @@ void Debug_ParseBuffer()
if(cmd==SerialCMD_ReadMem) //若要读取内存数据
{
uint8_t byteNum=DEBUG_QUEUE_AT(3); //要读取的字节数
if(byteNum>DEBUG_TXBUF_SIZE-3) //限制读取的字节数不能使帧长超过发送缓冲区大小
byteNum=DEBUG_TXBUF_SIZE-3;
uint32_t addr=0; //计算目标地址
for(uint8_t i=0;i<4;i++)
addr|=((uint32_t)DEBUG_QUEUE_AT(4+i))<<(i*8);

View File

@ -10,20 +10,26 @@ MainWindow::MainWindow(QWidget *parent)
setStylesheet();//设置全局样式表
graph=new GraphWindow();//创建并显示绘图窗口
graph=new GraphWindow();//创建绘图窗口
graph->setVarList(&varList);
graph->show();
connect(ui->action_show_graph,&QAction::toggled,this,[=](bool checked){graph->setHidden(!checked);});//绑定菜单项到窗口开关
connect(graph,&QDialog::rejected,this,[=]{ui->action_show_graph->setChecked(false);});//当窗口关闭时取消勾选菜单
listWindow=new ListWindow();
listWindow->show();
listWindow=new ListWindow();//创建选择窗口
connect(listWindow,SIGNAL(add2Edit(const QString &)),this,SLOT(slotOnVarAdd2Edit(const QString &)));
connect(listWindow,SIGNAL(add2List(const QString &)),this,SLOT(slotOnVarAdd2List(const QString &)));
connect(ui->action_show_selector,&QAction::toggled,this,[=](bool checked){listWindow->setHidden(!checked);});
connect(listWindow,&QDialog::rejected,this,[=]{ui->action_show_selector->setChecked(false);});
logWindow=new LogWindow();//创建日志窗口
connect(ui->action_show_log,&QAction::toggled,this,[=](bool checked){logWindow->setHidden(!checked);});
connect(logWindow,&QDialog::rejected,this,[=]{ui->action_show_log->setChecked(false);});
stampTimer=new QElapsedTimer();//创建并运行时间戳定时器
stampTimer->start();
watchTimer=new QTimer(this);//创建watch定时器
watchTimer->setInterval(10);
watchTimer->setInterval(0);
watchTimer->stop();
connect(watchTimer,SIGNAL(timeout()),this,SLOT(slotWatchTimerTrig()));
@ -32,13 +38,18 @@ MainWindow::MainWindow(QWidget *parent)
tableTimer->stop();
connect(tableTimer,SIGNAL(timeout()),this,SLOT(slotTableTimerTrig()));
logTimer=new QTimer(this);//创建日志监视定时器
logTimer->setInterval(100);
logTimer->stop();
connect(logTimer,SIGNAL(timeout()),this,SLOT(slotLogTimerTrig()));
tableModel=new QStandardItemModel(this);//创建并初始化表格
initTable();
openocd=new OpenOCD();
openocd=new OpenOCD();//创建OpenOCD对象并连接错误处理槽
connect(openocd,&OpenOCD::onErrorOccur,this,&MainWindow::slotOnConnErrorOccur,Qt::QueuedConnection);
serialocd=new SerialOCD();
serialocd=new SerialOCD();//创建SerialOCD对象并连接错误处理槽
connect(serialocd,&SerialOCD::onErrorOccur,this,&MainWindow::slotOnConnErrorOccur,Qt::QueuedConnection);
gdb=new GDBProcess();//创建并启动GDB
@ -46,6 +57,7 @@ MainWindow::MainWindow(QWidget *parent)
gdb->start();//启动gdb进程
loadConfFileList();//从openocd文件夹中读取配置文件列表
loadGlobalConf();//加载软件全局配置
}
MainWindow::~MainWindow()
@ -67,8 +79,10 @@ MainWindow::~MainWindow()
delete stampTimer;
delete watchTimer;
delete tableTimer;
delete logTimer;
delete graph;
delete listWindow;
delete logWindow;
}
//按键事件监听DEL键按下用于删除单个变量
@ -91,24 +105,21 @@ void MainWindow::keyPressEvent(QKeyEvent *event)
redrawTable();
if(connected)//若正在连接状态则向gdb发送新的变量列表
{
QStringList nameList;
for(int index=0;index<varList.size();index++)
nameList<<varList.at(index).name;
gdb->setDisplayList(nameList);
}
updateGDBList();
}
}
}
}
}
//窗口关闭事件,在主窗口关闭时自动关闭绘图窗口以退出程序
//窗口关闭事件,处理软件退出
void MainWindow::closeEvent(QCloseEvent *event)
{
Q_UNUSED(event);
graph->close();
saveGlobalConf();//保存软件全局设置
graph->close();//关闭各窗口,退出软件
listWindow->close();
logWindow->close();
}
//表格被编辑,添加变量或修改变量值
@ -130,12 +141,7 @@ void MainWindow::slotTableEdit(QModelIndex topleft, QModelIndex bottomright)
redrawTable();//重绘表格
if(connected)//若正在连接状态则向gdb发送新的变量列表
{
QStringList nameList;
for(int index=0;index<varList.size();index++)
nameList<<varList.at(index).name;
gdb->setDisplayList(nameList);
}
updateGDBList();
}
}
else if(topleft.column()==2 && topleft.row()!=varList.size())//若编辑的是第二列,表示需进行变量值修改
@ -191,6 +197,28 @@ void MainWindow::slotTableTimerTrig()
}
}
//日志监视定时器触发,获取并解析日志信息
void MainWindow::slotLogTimerTrig()
{
const QString cmd="print/u logQueue.size>0?(logQueue.size--,logQueue.startPos=(logQueue.startPos+1)%logQueue.maxSize,*logQueue.buf[logQueue.startPos]@logQueue.lenBuf[logQueue.startPos]):{0}\r\n";
QString raw=gdb->runCmd(cmd);//命令GDB从下位机出队一条日志
QList<uint> numList=gdb->getUintArrayFromDisplay(raw);//从日志中解析缓冲区数组内容
if(numList.size()>1)
{
char attr=numList.at(0);//日志属性字符
QStringList logList;//将缓冲区数组数据拆分成几个字符串
logList<<""<<""<<""<<"";
for(int pos=1,strNum=0; pos<numList.size() && strNum<logList.size(); pos++)
{
if(numList[pos]==0)
strNum++;
else
logList[strNum].append(numList[pos]);
}
logWindow->addLog(attr,logList[0],logList[1],logList[2].toULongLong(),logList[3]);//向日志窗口添加一条日志
}
}
//选择器窗口添加到编辑框槽函数
void MainWindow::slotOnVarAdd2Edit(const QString &name)
{
@ -265,10 +293,7 @@ void MainWindow::setConnState(bool connect)
{
gdb->loadSymbolFile(ui->txt_axf_path->text());//设置gdb符号文件
gdb->connectToRemote("localhost:3333");//连接gdb到ocd
QStringList nameList;
for(int index=0;index<varList.size();index++)//向gdb发送变量列表
nameList<<varList.at(index).name;
gdb->setDisplayList(nameList);
updateGDBList();//向gdb发送变量列表
for(int i=0;i<varList.size();i++)//清空变量列表历史采样点数据
varList[i].samples.clear();
@ -277,6 +302,10 @@ void MainWindow::setConnState(bool connect)
watchTimer->start();//开启定时查看变量值
tableTimer->start();//开启表格定时刷新
if(ui->cb_log->isChecked())//开启日志监视定时器
logTimer->start();
logWindow->clearLog();//清空日志列表
ui->bt_conn->setText("断开连接");
ui->bt_reset->setEnabled(true);//使能复位按钮
ui->rb_openocd->setEnabled(false);//失能连接方式选择
@ -290,6 +319,8 @@ void MainWindow::setConnState(bool connect)
{
watchTimer->stop();//停止定时查看和定时刷新表格
tableTimer->stop();
if(ui->cb_log->isChecked())//停止日志监视定时器
logTimer->stop();
gdb->disconnectFromRemote();//断开gdb
gdb->unloadSymbolFile();//卸载符号文件
if(ui->rb_openocd->isChecked())//根据连接方式选择结束ocd
@ -366,7 +397,7 @@ void MainWindow::redrawTable()
tableModel->setHeaderData(0,Qt::Horizontal,"变量名");//设置表头
tableModel->setHeaderData(1,Qt::Horizontal,"当前值");
tableModel->setHeaderData(2,Qt::Horizontal,"修改变量");
tableModel->setHeaderData(3,Qt::Horizontal,"使能绘图");
tableModel->setHeaderData(3,Qt::Horizontal,"显示图像");
tableModel->setHeaderData(4,Qt::Horizontal,"图线颜色");
for(int i=0;i<varList.size();i++)//依次添加各变量信息
@ -391,6 +422,15 @@ void MainWindow::redrawTable()
ui->tb_var->resizeColumnToContents(0);//根据第一列内容自动调整列宽
}
//更新GDB的display列表
void MainWindow::updateGDBList()
{
QStringList nameList;
for(int index=0;index<varList.size();index++)
nameList<<varList.at(index).name;
gdb->setDisplayList(nameList);
}
//保存配置到指定路径的文件
void MainWindow::saveToFile(const QString &filename)
{
@ -405,6 +445,7 @@ void MainWindow::saveToFile(const QString &filename)
settings.setValue("Target",ui->cb_target->currentText());
settings.setValue("AxfChosen",axfChosen);
settings.setValue("AxfPath",ui->txt_axf_path->text());
settings.setValue("LogEnabled",ui->cb_log->isChecked());
settings.setValue("VarNum",varList.size());
settings.endGroup();
@ -434,6 +475,7 @@ void MainWindow::loadFromFile(const QString &filename)
ui->cb_target->setCurrentText(settings.value("Target").toString());
axfChosen=settings.value("AxfChosen",true).toBool();
ui->txt_axf_path->setText(settings.value("AxfPath").toString());
ui->cb_log->setChecked(settings.value("LogEnabled",false).toBool());
int varNum=settings.value("VarNum").toInt();
settings.endGroup();
@ -488,6 +530,28 @@ bool MainWindow::exportCSV(const QString &filename)
return true;
}
//读取全局配置
void MainWindow::loadGlobalConf()
{
QSettings settings("conf.ini",QSettings::IniFormat);
settings.setIniCodec("GBK");
ui->action_show_graph->setChecked(settings.value("GraphWindowOpen",true).toBool());
ui->action_show_selector->setChecked(settings.value("ListWindowOpen",true).toBool());
ui->action_show_log->setChecked(settings.value("LogWindowOpen",true).toBool());
}
//保存全局配置
void MainWindow::saveGlobalConf()
{
QSettings settings("conf.ini",QSettings::IniFormat);
settings.setIniCodec("GBK");
settings.setValue("GraphWindowOpen",!graph->isHidden());
settings.setValue("ListWindowOpen",!listWindow->isHidden());
settings.setValue("LogWindowOpen",!logWindow->isHidden());
}
//复位按钮点击向gdb发送复位指令
void MainWindow::on_bt_reset_clicked()
{
@ -619,13 +683,6 @@ void MainWindow::on_action_help_triggered()
HelpWindow().exec();
}
//显示绘图窗口菜单点击
void MainWindow::on_action_show_graph_triggered()
{
graph->show();
graph->activateWindow();
}
//刷新连接配置菜单点击
void MainWindow::on_action_refresh_conf_triggered()
{
@ -646,13 +703,6 @@ void MainWindow::setStylesheet()
qApp->setStyleSheet(QLatin1String(qss.readAll()));
}
//显示选择窗口菜单点击
void MainWindow::on_action_show_selector_triggered()
{
listWindow->show();
listWindow->activateWindow();
}
//反馈菜单点击
void MainWindow::on_action_feedback_triggered()
{
@ -735,3 +785,15 @@ void MainWindow::on_bt_refresh_serial_clicked()
ui->cb_com->clear();
ui->cb_com->addItems(serialocd->getSerialList());
}
//日志使能状态切换
void MainWindow::on_cb_log_toggled(bool checked)
{
if(connected)
{
if(checked)
logTimer->start();
else
logTimer->stop();
}
}

View File

@ -27,6 +27,7 @@
#include <gdbprocess.h>
#include <openocd.h>
#include <serialocd.h>
#include <logwindow.h>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
@ -46,6 +47,7 @@ private slots:
void slotTableEdit(QModelIndex topleft, QModelIndex bottomright);
void slotWatchTimerTrig();
void slotTableTimerTrig();
void slotLogTimerTrig();
void slotOnVarAdd2Edit(const QString &name);
void slotOnVarAdd2List(const QString &name);
void slotOnConnErrorOccur(const QString &info);
@ -59,15 +61,14 @@ private slots:
void on_action_export_triggered();
void on_action_about_triggered();
void on_action_help_triggered();
void on_action_show_graph_triggered();
void on_action_refresh_conf_triggered();
void on_action_homepage_triggered();
void on_action_show_selector_triggered();
void on_action_feedback_triggered();
void on_action_checkupdate_triggered();
void on_rb_openocd_toggled(bool checked);
void on_rb_serialocd_toggled(bool checked);
void on_bt_refresh_serial_clicked();
void on_cb_log_toggled(bool checked);
private:
Ui::MainWindow *ui;
@ -78,12 +79,13 @@ private:
bool connected=false;//标记当前是否已连接
QStandardItemModel *tableModel;//表格数据
QList<VarInfo> varList;//变量列表
QTimer *watchTimer,*tableTimer;//定时器,用于查看变量值和刷新表格
QTimer *watchTimer,*tableTimer,*logTimer;//定时器,用于查看变量值、刷新表格和监视日志
QElapsedTimer *stampTimer;//时间戳定时器指针
GraphWindow *graph;//绘图窗口指针
bool isWatchProcessing=false;//标记当前是否正在处理变量值查看
bool axfChosen=false;//是否已经选择了axf文件
ListWindow *listWindow;
LogWindow *logWindow;
void checkUpdate();
void setStylesheet();
void setConnState(bool connect);
@ -91,8 +93,11 @@ private:
void loadConfFileList();
void initTable();
void redrawTable();
void updateGDBList();
void saveToFile(const QString &filename);
void loadFromFile(const QString &filename);
bool exportCSV(const QString &filename);
void loadGlobalConf();
void saveGlobalConf();
};
#endif // MAINWINDOW_H

View File

@ -159,6 +159,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cb_log">
<property name="text">
<string>监视日志</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -246,6 +253,7 @@
</property>
<addaction name="action_show_graph"/>
<addaction name="action_show_selector"/>
<addaction name="action_show_log"/>
</widget>
<addaction name="menu"/>
<addaction name="menu_3"/>
@ -278,8 +286,11 @@
</property>
</action>
<action name="action_show_graph">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>显示绘图窗口</string>
<string>绘图窗口</string>
</property>
</action>
<action name="action_refresh_conf">
@ -296,8 +307,11 @@
</property>
</action>
<action name="action_show_selector">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>显示选择窗口</string>
<string>选择窗口</string>
</property>
</action>
<action name="action_feedback">
@ -310,6 +324,14 @@
<string>检查更新</string>
</property>
</action>
<action name="action_show_log">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>日志窗口</string>
</property>
</action>
</widget>
<resources/>
<connections/>

View File

@ -137,7 +137,7 @@ QMenu{
QMenu:item{
background-color: white;
padding: 8px 15px;
padding: 8px 30px;
}
QMenu::item:selected{

View File

@ -236,8 +236,6 @@ void SerialOCD::sendSerialReadMem(int addr, int len)
{
if(port->isOpen())
{
if(len>8)//最多一次性读取8个字节
len=8;
QByteArray arr;
arr.resize(8);
arr[0]=DEBUG_FRAME_HEADER;