QT读取串口输出异常
Abnormal output when reading from serial port in QT
我正在使用 QT 和 QCustomPlot
创建一个实时绘图工具,绘图的数据是从 Arduino UNO 板读取的。我的应用程序在数据一团糟的情况下成功绘制。下面是我的代码(部分代码来自 QCustomPlot 网站):
void Dialog::realtimeDataSlot()
{
bool currentPortNameChanged = false;
QString currentPortName;
if (currentPortName != portName) {
currentPortName = portName;
currentPortNameChanged = true;
}
QString currentRequest = request;
QSerialPort serial;
if (currentPortNameChanged) {
serial.close();
serial.setPortName(currentPortName);
if (!serial.open(QIODevice::ReadOnly)) {
return;
}
}
static QTime time(QTime::currentTime());
// calculate two new data points:
double key = time.elapsed()/1000.0;
static double lastPointKey = 0;
if (key-lastPointKey > 0.002) // at most add point every 2 ms
{
// add data to lines:
if(serial.waitForReadyRead(-1)){
data = serial.readAll();
QTextStream(stdout) << "HERE:" << data.toDouble() << endl;
customPlot->graph(0)->addData(key, data.toDouble());
customPlot->graph(0)->rescaleValueAxis(); //rescale value (vertical) axis to fit the current data:
lastPointKey = key;
customPlot->xAxis->setRange(key, 8, Qt::AlignRight);
customPlot->replot();
static double lastFpsKey;
static int frameCount;
++frameCount;
if (key-lastFpsKey > 2) // average fps over 2 seconds
{
lastFpsKey = key;
frameCount = 0;
}
}
}
// calculate frames per second:
if (currentPortName != portName) {
currentPortName = portName;
currentPortNameChanged = true;
} else {
currentPortNameChanged = false;
}
}
当我试图打印出从串口读取的数据时,我发现如下:
HERE:1
HERE:15
HERE:150
HERE:149
HERE:149
HERE:149
HERE:150
HERE:150
HERE:15
HERE:150
HERE:149
HERE:49
HERE:150
HERE:150
HERE:1
HERE:150
150左右的值是正常的,0、1到其他的则不是。也不是以稳定的速度打印出来。不知道这是怎么回事,感谢各位大神,如果有更好的实现方法,我将不胜感激。
这里的问题是不能保证串口传输一下子全部收到。所以最好让序列号在别的地方处理,比如:
// in the class definition
QSerialPort serialPort;
private slots:
void handleReadyRead();
private:
QByteArray serialBuffer;
volatile double lastSerialValue;
// In the initialization part (not the realtimeDataSlot function)
lastSerialValue = qQNaN();
serialPort.setPortName(currentPortName);
connect(&serialPort, &QSerialPort::readyRead, this, &Dialog::handleReadyRead, Qt::UniqueConnection);
if (!serialPort.open(QIODevice::ReadOnly)) {
return;
}
serialBuffer.clear();
// Other functions:
void Dialog::realtimeDataSlot()
{
...
if (key-lastPointKey > 0.002) // at most add point every 2 ms
{
if (!qIsNaN(lastSerialData))
{
// use lastSerialValue as the data.toDouble() you had before, then, at the end
lastSerialValue = qQNaN();
}
...
}
void Dialog::handleReadyRead()
{
serialBuffer.append(serialPort.readAll());
int serPos;
while ((serPos = serialBuffer.indexOf('\n')) >= 0)
{
bool ok;
double tempValue = QString::fromLatin1(serialBuffer.left(serPos)).toDouble(&ok);
if (ok) lastSerialValue = tempValue;
serialBuffer = serialBuffer.mid(serPos+1);
}
}
说明:每当你从 arduino 收到一些东西时,字节就会附加到缓冲区中。然后解析字节数组以寻找终止符,如果找到则拆分并分析字节数组。当另一个函数需要数据时,它只是简单地拉取保存在变量中的最新数据。
注意1:我看到你使用了二进制传输。问题是您没有任何方法可以确定数据以这种方式开始和结束的位置。例如,如果您收到 0x01 0x02 0x03 0x04 并且您知道有 3 个字节,那么它们是 01..03 还是 02..04 或 03、04 以及缺少一个还是...?我实现的版本要求你发送带有 new-line 终止符的字符串格式的数据(最简单的版本,你只需要在 arduino 代码中写 Serial.println(doubleValue);
),但如果你需要我可以提供的二进制版本你一些提示
注意 2:我写的代码不是线程安全的。只有在同一个线程中调用 realtimeDataSlot 和 handleReadyRead 时,它才会起作用。请注意,如果它们属于同一对象并通过信号调用,则可以保证。
现在,这应该可以了。但我强烈建议您不要这样做。我不知道谁需要调用 realtimeDataSlot()
,但我认为最正确的版本是这样的:
// in the class definition
QSerialPort serialPort;
private slots:
void handleReadyRead();
void receivedData(double val);
private:
QByteArray serialBuffer;
signals:
void newData(double data);
// In the initialization part (not the realtimeDataSlot function)
serialPort.setPortName(currentPortName);
connect(&serialPort, &QSerialPort::readyRead, this, &Dialog::handleReadyRead, Qt::UniqueConnection);
connect(this, &Dialog::newData, this, &Dialog::receivedData, Qt::UniqueConnection);
if (!serialPort.open(QIODevice::ReadOnly)) {
return;
}
serialBuffer.clear();
// Other functions:
void Dialog::receivedData(double val)
{
double key = time.elapsed()/1000.0;
static double lastPointKey = 0;
if (key-lastPointKey > 0.002) // at most add point every 2 ms
{
QTextStream(stdout) << "HERE:" << data.toDouble() << endl;
customPlot->graph(0)->addData(key, data.toDouble());
customPlot->graph(0)->rescaleValueAxis();
...
}
}
void Dialog::handleReadyRead()
{
serialBuffer.append(serialPort.readAll());
int serPos;
while ((serPos = serialBuffer.indexOf('\n')) >= 0)
{
bool ok;
double tempValue = QString::fromLatin1(serialBuffer.left(serPos)).toDouble(&ok);
if (ok) emit newData(tempValue);
serialBuffer = serialBuffer.mid(serPos+1);
}
}
所以让图形响应事件(接收到新数据)而不是计时器。
还有一件事:我故意删除了端口更改。我建议你用另一种方式处理它:放置一个按钮来启动和停止串口,当串口启动时防止用户更改端口名称。这样,用户在需要更改端口时将明确需要将其关闭。但是,如果您想要您的版本,请不要将其包含在您的代码中,而是在您需要更改端口名称时自行创建一个插槽来调用:
void changeSerialPortName(QString newName)
{
if (newName != serialPort.portName()) {
if (serialPort.isOpen())
serialPort.close();
serialPort.setPortName(newName);
if (!serialPort.open(QIODevice::ReadOnly)) {
return;
}
}
}
我正在使用 QT 和 QCustomPlot
创建一个实时绘图工具,绘图的数据是从 Arduino UNO 板读取的。我的应用程序在数据一团糟的情况下成功绘制。下面是我的代码(部分代码来自 QCustomPlot 网站):
void Dialog::realtimeDataSlot()
{
bool currentPortNameChanged = false;
QString currentPortName;
if (currentPortName != portName) {
currentPortName = portName;
currentPortNameChanged = true;
}
QString currentRequest = request;
QSerialPort serial;
if (currentPortNameChanged) {
serial.close();
serial.setPortName(currentPortName);
if (!serial.open(QIODevice::ReadOnly)) {
return;
}
}
static QTime time(QTime::currentTime());
// calculate two new data points:
double key = time.elapsed()/1000.0;
static double lastPointKey = 0;
if (key-lastPointKey > 0.002) // at most add point every 2 ms
{
// add data to lines:
if(serial.waitForReadyRead(-1)){
data = serial.readAll();
QTextStream(stdout) << "HERE:" << data.toDouble() << endl;
customPlot->graph(0)->addData(key, data.toDouble());
customPlot->graph(0)->rescaleValueAxis(); //rescale value (vertical) axis to fit the current data:
lastPointKey = key;
customPlot->xAxis->setRange(key, 8, Qt::AlignRight);
customPlot->replot();
static double lastFpsKey;
static int frameCount;
++frameCount;
if (key-lastFpsKey > 2) // average fps over 2 seconds
{
lastFpsKey = key;
frameCount = 0;
}
}
}
// calculate frames per second:
if (currentPortName != portName) {
currentPortName = portName;
currentPortNameChanged = true;
} else {
currentPortNameChanged = false;
}
}
当我试图打印出从串口读取的数据时,我发现如下:
HERE:1
HERE:15
HERE:150
HERE:149
HERE:149
HERE:149
HERE:150
HERE:150
HERE:15
HERE:150
HERE:149
HERE:49
HERE:150
HERE:150
HERE:1
HERE:150
150左右的值是正常的,0、1到其他的则不是。也不是以稳定的速度打印出来。不知道这是怎么回事,感谢各位大神,如果有更好的实现方法,我将不胜感激。
这里的问题是不能保证串口传输一下子全部收到。所以最好让序列号在别的地方处理,比如:
// in the class definition
QSerialPort serialPort;
private slots:
void handleReadyRead();
private:
QByteArray serialBuffer;
volatile double lastSerialValue;
// In the initialization part (not the realtimeDataSlot function)
lastSerialValue = qQNaN();
serialPort.setPortName(currentPortName);
connect(&serialPort, &QSerialPort::readyRead, this, &Dialog::handleReadyRead, Qt::UniqueConnection);
if (!serialPort.open(QIODevice::ReadOnly)) {
return;
}
serialBuffer.clear();
// Other functions:
void Dialog::realtimeDataSlot()
{
...
if (key-lastPointKey > 0.002) // at most add point every 2 ms
{
if (!qIsNaN(lastSerialData))
{
// use lastSerialValue as the data.toDouble() you had before, then, at the end
lastSerialValue = qQNaN();
}
...
}
void Dialog::handleReadyRead()
{
serialBuffer.append(serialPort.readAll());
int serPos;
while ((serPos = serialBuffer.indexOf('\n')) >= 0)
{
bool ok;
double tempValue = QString::fromLatin1(serialBuffer.left(serPos)).toDouble(&ok);
if (ok) lastSerialValue = tempValue;
serialBuffer = serialBuffer.mid(serPos+1);
}
}
说明:每当你从 arduino 收到一些东西时,字节就会附加到缓冲区中。然后解析字节数组以寻找终止符,如果找到则拆分并分析字节数组。当另一个函数需要数据时,它只是简单地拉取保存在变量中的最新数据。
注意1:我看到你使用了二进制传输。问题是您没有任何方法可以确定数据以这种方式开始和结束的位置。例如,如果您收到 0x01 0x02 0x03 0x04 并且您知道有 3 个字节,那么它们是 01..03 还是 02..04 或 03、04 以及缺少一个还是...?我实现的版本要求你发送带有 new-line 终止符的字符串格式的数据(最简单的版本,你只需要在 arduino 代码中写 Serial.println(doubleValue);
),但如果你需要我可以提供的二进制版本你一些提示
注意 2:我写的代码不是线程安全的。只有在同一个线程中调用 realtimeDataSlot 和 handleReadyRead 时,它才会起作用。请注意,如果它们属于同一对象并通过信号调用,则可以保证。
现在,这应该可以了。但我强烈建议您不要这样做。我不知道谁需要调用 realtimeDataSlot()
,但我认为最正确的版本是这样的:
// in the class definition
QSerialPort serialPort;
private slots:
void handleReadyRead();
void receivedData(double val);
private:
QByteArray serialBuffer;
signals:
void newData(double data);
// In the initialization part (not the realtimeDataSlot function)
serialPort.setPortName(currentPortName);
connect(&serialPort, &QSerialPort::readyRead, this, &Dialog::handleReadyRead, Qt::UniqueConnection);
connect(this, &Dialog::newData, this, &Dialog::receivedData, Qt::UniqueConnection);
if (!serialPort.open(QIODevice::ReadOnly)) {
return;
}
serialBuffer.clear();
// Other functions:
void Dialog::receivedData(double val)
{
double key = time.elapsed()/1000.0;
static double lastPointKey = 0;
if (key-lastPointKey > 0.002) // at most add point every 2 ms
{
QTextStream(stdout) << "HERE:" << data.toDouble() << endl;
customPlot->graph(0)->addData(key, data.toDouble());
customPlot->graph(0)->rescaleValueAxis();
...
}
}
void Dialog::handleReadyRead()
{
serialBuffer.append(serialPort.readAll());
int serPos;
while ((serPos = serialBuffer.indexOf('\n')) >= 0)
{
bool ok;
double tempValue = QString::fromLatin1(serialBuffer.left(serPos)).toDouble(&ok);
if (ok) emit newData(tempValue);
serialBuffer = serialBuffer.mid(serPos+1);
}
}
所以让图形响应事件(接收到新数据)而不是计时器。
还有一件事:我故意删除了端口更改。我建议你用另一种方式处理它:放置一个按钮来启动和停止串口,当串口启动时防止用户更改端口名称。这样,用户在需要更改端口时将明确需要将其关闭。但是,如果您想要您的版本,请不要将其包含在您的代码中,而是在您需要更改端口名称时自行创建一个插槽来调用:
void changeSerialPortName(QString newName)
{
if (newName != serialPort.portName()) {
if (serialPort.isOpen())
serialPort.close();
serialPort.setPortName(newName);
if (!serialPort.open(QIODevice::ReadOnly)) {
return;
}
}
}