这次项目的讲解分为4各部分,分别是简介(1/4)、基础知识(2/4)、程序开发(3/4)和联合调试(4/4),这一次内容属于程序开发(3/4),可以对应文章标题(↑)快速定位目前处于哪一讲解环节。
程序我们分两个部分,一个是上位机基于Qt的软件界面开发,一个是下位机基于Keil的嵌入式开发。
一、Qt开发的上位机
1. 软件界面
由于这次项目是针对自己的工作内容,所以就没有做什么设计,并且也是用来测试的,直接摆了一堆按钮能用就行,如图1.1.1:
图 1.1.1 Qt设计的界面
我们根据界面的内容来逐步讲解有些什么功能需要实现:
1. 首先就是通信方式,由于上下位机通讯用的USB CDC协议,根据STM32官方例程的说明,采用的是模拟串口通信的方式,因此我们在Qt中使用QSerialPort即可。同时电脑上可能会有多个串口源,这里添加了一个QComboBox的下拉选择列表用于选择我们需要的串口;
2. 由于电机比较多,把每个电机的控制单独列一个显得不太优雅,于是我把他们放在了一个列表里进行选择;
3. 可以看到这里只提供了两个简单的前进、后退和1千步、1万步的选择,因此自定义输入步长也很重要,这里用了个QLineEdit来输入数据,配合QIntValidator使用户只能输入数字;
4. 步进和后退就不用说了,定点是基于导轨上的限位反馈来实现的;
5. 探测是6条导轨上的限位反馈,没有限位时为绿色o图片,限位时为红色x图片,这里用的QLabel加载的图片;
6. 进度为电机运动的百分比,用了官方提供的QProgressBar控件;
7. 紧急停止按钮顾名思义,用于停止电机运转。
根据我的设想,上下位机之间之用于状态和控制信息,电机的各类计算和驱动全由下位机完成,未来如果有需要也可以添加SPI或I2C协议的液晶或墨水屏实现脱离上位机的离线控制。
2. 代码分析
代码部分我根据不同组件分开说,每个组件官方文档和案例内都有较好的说明和举例,这里我就只说我自己的实现部分:
(1)QserialPort
先说最重要的串口通信QSerialPort,串口通信有一些固定的参数,一般来说如果没有特别严格数据正确性需求 并且 短距离(小于50cm) 以及 没额外电磁干扰的情况下可以用最简单的设置方式:
QSerialPort* m_serialPort;
m_serialPort = new QSerialPort(this);
m_serialPort->setPortName(cbb_selectCom->currentText()); //当前选择的串口名字
m_serialPort->setBaudRate(10000000,QSerialPort::AllDirections); //设置波特率和读写方向
m_serialPort->setDataBits(QSerialPort::Data8); //数据位为8位
m_serialPort->setParity(QSerialPort::NoParity); //无校验位
m_serialPort->setStopBits(QSerialPort::OneStop); //一位停止位
m_serialPort->setFlowControl(QSerialPort::NoFlowControl); //无流控制
这里有一个 cbb_selectCom->currentText() 这个参数,初始化来源于下面的代码:
QComboBox* cbb_selectCom;
cbb_selectCom = ui->cbb_selectCom;
cbb_selectCom->clear();
QStringList m_portNameList = getPortNameList();
cbb_selectCom->addItems(m_portNameList);
其中 ui->cbb_selectCom 指代的是在.ui文件中的控件,通过 getPortNameList() 函数来获取端口名称列表,返回参数类型为QStringList,函数如下:
QStringList MainWindow::getPortNameList()
{
QStringList m_serialPortName;
foreach(const QSerialPortInfo &info,QSerialPortInfo::availablePorts())
{
m_serialPortName << info.portName();
qDebug()<<"serialPortName:"<<info.portName();
}
return m_serialPortName;
}
这里用了 QStringList 自带的<<符号重定义(重载), m_serialPortName << info.portName() 用于快速添加新的端口名称到 m_serialPortName 这个 QStringList 中,通过 foreach 的方式来遍历现在可用的端口,最终返回参数到 m_portNameList 并加载到 cbb_selectCom 用于界面显示和选择,选择好后就可以打开端口,方式如下:
bool MainWindow::openSerialPort()
{
if(m_serialPort->isOpen())
{
m_serialPort->clear();
m_serialPort->close();
}
m_serialPort->setPortName(cbb_selectCom->currentText()); //当前选择的串口名字
m_serialPort->setBaudRate(10000000,QSerialPort::AllDirections); //设置波特率和读写方向
m_serialPort->setDataBits(QSerialPort::Data8); //数据位为8位
m_serialPort->setParity(QSerialPort::NoParity); //无校验位
m_serialPort->setStopBits(QSerialPort::OneStop); //一位停止位
m_serialPort->setFlowControl(QSerialPort::NoFlowControl); //无流控制
if(m_serialPort->open(QIODevice::ReadWrite))//用ReadWrite 的模式尝试打开串口
{
connect(m_serialPort, &QSerialPort::readyRead, this, &MainWindow::readData);
connect(m_serialPort, &QSerialPort::errorOccurred, this, &MainWindow::handleError);
enablePushButton();
showStatusMessage(tr("Connect to %1").arg(cbb_selectCom->currentText()));
}
else
{
QMessageBox::critical(this, tr("错误"), m_serialPort->errorString());
showStatusMessage(tr("Open COM failed"));
return false;
}
return true;
}
代码最开始先是判断端口是否已打开,如果打开了就先关闭再配置参数,随后获取当前可用的端口并通过 ReadWrite 模式进行端口初始化:成功就进行信号槽链接,并使能界面所有按钮并显示信息到用户界面的状态栏,最后返回成功参数;失败就弹出错误提示窗口并返回失败参数。这里也码一下相关的函数:
void MainWindow::writeData(const QByteArray &data)
{
// 串口开启检测
if(defSwitch_Off == isConnect) return;
// 发送数据
m_serialPort->write(data);
}
void MainWindow::readData()
{
// 读取数据
const QByteArray data = m_serialPort->readAll();
// 数据完整性检测
if(0 == data.count())
return ;
// 打印原生数据
//qDebug() << "read Line: " << data;
// 打印16进制数据
QString ret;
for(int i = 0; i < data.count(); ++i)
ret.append( QObject::tr("%1,").arg((quint8)data.at(i),2,16,QLatin1Char('0')));
//qDebug() << "read data: " << ret;
// 其他业务逻辑
}
void MainWindow::handleError(QSerialPort::SerialPortError error)
{
if (error == QSerialPort::ResourceError) {
//QMessageBox::critical(this, tr("通信错误"), m_serialPort->errorString());
qDebug() << "Serial port fault: " << m_serialPort->errorString();
showStatusMessage(tr("Serial port critical fault"));
closeSerialPort();
}
}
void MainWindow::enablePushButton()
{
pb_inputRun_back->setEnabled(true);
pb_inputRun_fore->setEnabled(true);
// 其他按钮同理
}
void MainWindow::disablePushButton()
{
pb_inputRun_back->setEnabled(false);
pb_inputRun_fore->setEnabled(false);
// 其他按钮同理
}
// 提示信息
void MainWindow::showStatusMessage(const QString &message)
{
m_statusBar->showMessage(message, 3000); // 单位为 ms
qDebug() << message;
//QApplication::processEvents(); //用于处理信息
}
上面的所有控件都是经过初始化的,相比直接调用ui,这种方式更容易寻找和定位以及修改。
(2)界面初始化
再说说界面上的按钮和对应的信号槽初始化
void MainWindow::uiInit()
{
// 窗体设计
setWindowTitle(tr("电机控制程序 Powered by OolongLemon (Ver 1.0)"));
//setFixedSize( this->width (),this->height ()); // 固定窗口大小
//setWindowFlags(Qt::WindowCloseButtonHint | Qt::MSWindowsFixedSizeDialogHint);//只保留关闭按钮、窗口大小锁定
setWindowFlags(Qt::Dialog); // 窗体没有最大化最小化按钮
setWindowIcon(QIcon(":/new/logo/logo.ico")); // 设置任务栏和窗口图标
// 状态栏初始化
m_statusBar = ui->statusBar;
Label_statusBar = new QLabel(this);
m_statusBar->addPermanentWidget(Label_statusBar);
m_statusBar->setStyleSheet(QString("QStatusBar::item{border: 0px}"));
// 串口选择
cbb_selectCom = ui->cbb_selectCom;
pb_freshCom = ui->pb_freshCom;
pb_connectCom = ui->pb_connectCom;
//电机选择
cbb_selectMot = ui->cbb_selectMot;
pb_selectMot = ui->pb_selectMot;
// 输入运动
le_inputRun = ui->le_inputRun;
pb_inputRun_back = ui->pb_inputRun_back;
pb_inputRun_fore = ui->pb_inputRun_fore;
// 步进
pb_runFore_1 = ui->pb_runFore_1;
pb_runFore_2 = ui->pb_runFore_2;
// 后退
pb_runBack_1 = ui->pb_runBack_1;
pb_runBack_2 = ui->pb_runBack_2;
// 定点
pb_setPoint_1 = ui->pb_setPoint_1;
pb_setPoint_2 = ui->pb_setPoint_2;
// 探测
label_pic_L1d = ui->label_pic_L1d;
// 其他同理
// 进度
probar_1 = ui->probar_1;
// 其他同理
// 紧急停止
pb_emerStop = ui->pb_emerStop;
disablePushButton();
}
void MainWindow::uiConnect()
{
connect(ui->pb_freshCom, SIGNAL(clicked()), this, SLOT(slots_pb_freshCom()));
connect(ui->pb_connectCom, SIGNAL(clicked()), this, SLOT(slots_pb_connectCom()));
connect(ui->pb_selectMot, SIGNAL(clicked()), this, SLOT(slots_pb_selectMot()));
connect(ui->pb_inputRun_back, SIGNAL(clicked()), this, SLOT(slots_pb_inputRun_back()));
connect(ui->pb_inputRun_fore, SIGNAL(clicked()), this, SLOT(slots_pb_inputRun_fore()));
connect(ui->pb_runFore_1, SIGNAL(clicked()), this, SLOT(slots_pb_runFore_1()));
connect(ui->pb_runFore_2, SIGNAL(clicked()), this, SLOT(slots_pb_runFore_2()));
connect(ui->pb_runBack_1, SIGNAL(clicked()), this, SLOT(slots_pb_runBack_1()));
connect(ui->pb_runBack_2, SIGNAL(clicked()), this, SLOT(slots_pb_runBack_2()));
connect(ui->pb_setPoint_1, SIGNAL(clicked()), this, SLOT(slots_pb_setPoint_1()));
connect(ui->pb_setPoint_2, SIGNAL(clicked()), this, SLOT(slots_pb_setPoint_2()));
connect(ui->pb_emerStop, SIGNAL(clicked()), this, SLOT(slots_pb_emerStop()));
}
void MainWindow::moduInit()
{
// 设置输入框
QIntValidator *pIntVld = new QIntValidator(this);
le_inputRun->setValidator(pIntVld); // 输入过滤器,只接受整形(int)数字
le_inputRun->setText("50"); // 初始化显示 50
// 建立串口
m_serialPort = new QSerialPort(this);
// 添加显示
cbb_selectCom->clear();
m_portNameList = getPortNameList();
cbb_selectCom->addItems(m_portNameList);
// 选择电机
cbb_selectMot->clear();
m_motorNameList << "Motor 0" << "Motor 1" << "Motor 2" << \