这次项目的讲解分为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" << \
"Motor 3" << "Motor 4" << "Motor 5"<< \
"Motor 6" << "Motor 7" ;
cbb_selectMot->addItems(m_motorNameList);
// 全局电机选择初始化
global_motor = 0;
cbb_selectMot->setCurrentIndex(global_motor);
}
这里的 信号槽我用了老版本的信号槽函数声明:(建议用新版本,我是懒得改了)
connect(ui->pb_freshCom, SIGNAL(clicked()), this, SLOT(slots_pb_freshCom()));
新版本为:
connect(ui->pb_freshCom, &QPushButton::clicked, this, &MainWindow::slots_pb_freshCom);
窗体的控制函数还挺多,建议可以看看其他博文多了解一下,其中:
setWindowIcon(QIcon(":/new/logo/logo.ico")); // 设置任务栏和窗口图标
这个函数用于设置软件运行时窗体的图标,QIcon的路径是自行添加的,方法如下:
首先要有一个.ico或图像的文件将其放在工程目录下任意位置,然后:
在项目列表根节点这里单击右键,选择" Add New... "
随后打开下面的界面选择 Qt -> Qt Resource File 然后确定
随后填写好资源文件的名字后一路确定,可以看到主目录下多了个 Resources 文件夹以及刚才新建的.qrc文件
如果文件关闭了,就在.qrc文件上右键选择 " Open in Editor "
打开后就是如下的界面
这里有两个文件夹是我先前创建的,创建新的文件夹就点 " Add Prefix ",随后点 " Add Files " 浏览本地文件并选择图标文件,随后在新添加的文件处单击右键选择 " 复制资源路径到剪贴板 ",可以快速创建文件的引用链接,随后将链接粘在下面的双引号里就可以了。
setWindowIcon(QIcon("粘在这里"));
这个 resource 文件存在的目的:1是为了方便管理资源分类,2是用于将资源本地目录替换为qt内部目录。所有的资源都是引用的本地资源而不是复制本地资源到 Qt 内,本地资源删了 Qt 无法引用是会报错的。当然这个 resource 文件还能拿来引用文本、图片、音频、视频、网页等内容,有需要的可以自己去探索一下使用方法。
其他代码里面还有一个比较重要的内容:
m_statusBar->setStyleSheet(QString("QStatusBar::item{border: 0px}"));
这里用到了代码版本的ui界面控件初始化,也可以直接在ui界面内对控件右键选择 " 改变样式表... " 来进行一些特殊配置。
除了界面,还有按钮的信号槽函数,这里主要贴三个按钮的实现:
void MainWindow::slots_pb_freshCom()
{
cbb_selectCom->clear();
m_portNameList = getPortNameList();
cbb_selectCom->addItems(m_portNameList);
}
void MainWindow::slots_pb_connectCom()
{
if(false == isConnect) // 还未连接
{
if(true == openSerialPort()) // 是否连接成功
{
isConnect = defSwitch_On;
pb_connectCom->setText(tr("断开"));
t_askSTM->start(); // 启动闻讯定时器
}
}
else // true == isConnect 已连接
{
closeSerialPort();
}
}
void MainWindow::closeSerialPort()
{
disablePushButton();
if (m_serialPort->isOpen())
m_serialPort->close();
pb_connectCom->setText(tr("连接"));
isConnect = defSwitch_Off;
// 其他业务逻辑
showStatusMessage("Disconnected");
}
void MainWindow::slots_pb_selectMot()
{
QString mot_select = cbb_selectMot->currentText();
for(uint8_t i = 0; i < 8; i++)
{
if(mot_select == m_motorNameList[i])
global_motor = i;
}
showStatusMessage( tr("Selecte %1").arg(mot_select) );
qDebug() << "selected: " << mot_select << " with motor_" << QString::number(global_motor);
}
这里下面这句用处比较多
pb_connectCom->setText(tr("断开"));
Qt很多控件都可以显示文字,用代码设置文字的方式就是 控件->setText(QString text),tr 为Qt自带的多语言自动机器翻译函数。
(3)上下位机通信及协议
为了简单方便的传递信息和获得信息反馈,这个版本自拟了一个通信协议(后续的版本我已经改成G代码和M代码的格式只做控制不做反馈)。
首先是上位机发给下位机的协议:
编号 | 代码 | 作用 |
0 | 0x13 | 信息起始位 |
1 | 0x?? | 电机状态改变选择位 |
2 | 0x?? | 运动方向位 |
3 | 0x?? | 端点前进位 |
4 | 0xAB or 0xFF | 紧急停止位 |
5 | (1|3)&(2|4) | 校验位 |
6 | 0xbc | 暂时还没啥用,留着 |
7 | 0x14 | 信息中断位 |
编号0和编号7为固定的参数(编号7的位置不固定);编号4为紧急停止位,紧急停止为0xFF,其余时候为0xAB;编号5的校验位为1、2、3、4编号位的或和与操作;编号1、2和3都是 8 bit 的char型数据,每个 bit 刚好对应1个电机,当上位机没有指令需要发给下位机时,编号1为0x00,仅作为心跳包用于检测下位机是否离线;当需要改变某个电机的状态时(比如前进后退、前往端点等),对应电机的bit位就置1,同时填写编号2对应 bit 位用于确定运动方向(例如:0前进,1后退);如果是前往端点,则同时填写1、2和3的对应 bit 位,如果是前进后退,则在编号6后另附4个参数(信息中断位顺移),3个参数用于组成24bit的步长,1个参数用于校验这3个参数,多个电机同时运行时根据电机编号排布参数。
下位机发给上位机的协议,类似但不完全一样:
编号 | 代码 | 作用 |
0 | 0x11 | 信息起始位 |
1 | 0x?? | 电机运转位 |
2 | 0x?? | 运动方向位 |
3 | 0x?? | 电机选择位 |
4 | 0x?? | 行程极限位 |
5 | (1|3)&(2|4) | 校验位 |
6 | 0x12 | 信息中断位 |
编号0和编号6为固定的参数(编号6的位置不固定);编号5的校验位为1、2、3、4编号位的或和与操作;编号1和2组成电机运转状态及方向的信息反馈参数,如果编号1有运转位为1(电机在运行),则在编号5后会添加电机运转参数,从0~100代表电机运转的完成百分比,这个参数同步更新到QProgressBar;编号3和4组成导轨行程限位反馈参数,如果有限位信息触发,则将 label_pic_L1d 这些ui组件的图片更换,显示限位情况。
界面设置代码如下:
void MainWindow::setPercentage(uint8_t proID, uint8_t num)
{
if(0 == proID) probar_1->setValue(num);
else if(1 == proID) probar_2->setValue(num);
else if(2 == proID) probar_3->setValue(num);
else if(3 == proID) probar_4->setValue(num);
else if(4 == proID) probar_5->setValue(num);
else if(5 == proID) probar_6->setValue(num);
}
void MainWindow::setPicture(uint8_t picID, bool state)
{
// 重复检测
if(state == motor_isDetectStateChange[picID])
return ;
else
motor_isDetectStateChange[picID] = state;
QString image_path;
if(motor_detect_warning == state)
image_path = ":/new/label/warning_32.png";
else
image_path = ":/new/label/ok_32.png";
if(motor_L1y == picID)
play_image(label_pic_L1y, image_path);
else if(motor_L1d == picID)
play_image(label_pic_L1d, image_path);
else if(motor_L2y == picID)
play_image(label_pic_L2y, image_path);
else if(motor_L2d == picID)
play_image(label_pic_L2d, image_path);
else if(motor_L3y == picID)
play_image(label_pic_L3y, image_path);
else if(motor_L3d == picID)
play_image(label_pic_L3d, image_path);
else if(motor_L4y == picID)
play_image(label_pic_L4y, image_path);
else if(motor_L4d == picID)
play_image(label_pic_L4d, image_path);
else if(motor_L5y == picID)
play_image(label_pic_L5y, image_path);
else if(motor_L5d == picID)
play_image(label_pic_L5d, image_path);
else if(motor_L6y == picID)
play_image(label_pic_L6y, image_path);
else if(motor_L6d == picID)
play_image(label_pic_L6d, image_path);
}
// 用于显示照片
void MainWindow::play_image(QLabel *lab, QString image_path)
{
QPixmap *pixmap = new QPixmap(image_path);
pixmap->scaled(lab->size(), Qt::KeepAspectRatio);
//lab->setScaledContents(true); // 已在ui中设置
lab->setPixmap(*pixmap);
lab->show();
}
上位机发给下位机的界面控制和信息编码代码:
void MainWindow::slots_pb_inputRun_back()
{
uint32_t step = abs(le_inputRun->text().toInt());
QByteArray inst = run_setup(global_motor,motor_backward, \
step,0,0,0);
writeData(inst);
}
void MainWindow::slots_pb_inputRun_fore()
{
uint32_t step = abs(le_inputRun->text().toInt());
QByteArray inst = run_setup(global_motor,motor_foreward, \
step,0,0,0);
writeData(inst);
}
void MainWindow::slots_pb_runFore_1()
{
uint32_t step = 1;
QByteArray inst = run_setup(global_motor,motor_foreward, \
step,0,0,0);
writeData(inst);
}
void MainWindow::slots_pb_runFore_2()
{
uint32_t step = 10;
QByteArray inst = run_setup(global_motor,motor_foreward, \
step,0,0,0);
writeData(inst);
}
void MainWindow::slots_pb_runBack_1()
{
uint32_t step = 1;
QByteArray inst = run_setup(global_motor,motor_backward, \
step,0,0,0);
writeData(inst);
}
void MainWindow::slots_pb_runBack_2()
{
uint32_t step = 10;
QByteArray inst = run_setup(global_motor,motor_backward, \
step,0,0,0);
writeData(inst);
}
void MainWindow::slots_pb_setPoint_1()
{
QByteArray inst = run_setup(global_motor,motor_backward, \
0,true,0,0);
writeData(inst);
}
void MainWindow::slots_pb_setPoint_2()
{
QByteArray inst = run_setup(global_motor,motor_foreward, \
0,true,0,0);
writeData(inst);
}
void MainWindow::slots_pb_emerStop()
{
QByteArray inst = run_setup(0,0,0,0,true,0);
writeData(inst);
}
void MainWindow::control_run(unsigned int mt_id, bool dir, \
unsigned int step, bool point, \
bool emer_stop)
{
QByteArray inst = run_setup(mt_id,dir,step,point,emer_stop,0);
writeData(inst); // 发送信息
}
QByteArray MainWindow::run_setup(unsigned int mt_id, bool dir, \
unsigned int step, bool point, \
bool emer_stop, bool tim)
{
QByteArray inst;
// 轮询
if(true == tim)
{
inst.resize(7);
inst[0] = 0x13;
inst[1] = 0x00;
inst[2] = 0x00;
inst[3] = 0x00;
inst[4] = (char)0xab;
inst[5] = (inst[1]|inst[3])&(inst[2]|inst[4]);
inst[6] = 0x14;
}
// 紧急停止
else if(true == emer_stop)
{
inst.resize(7);
inst[0] = 0x13;
inst[1] = 0x00;
inst[2] = 0x00;
inst[3] = 0x00;
inst[4] = (char)0xff;
inst[5] = (inst[1]|inst[3])&(inst[2]|inst[4]);
inst[6] = 0x14;
}
// 前往端点
else if(true == point)
{
inst.resize(7);
inst[0] = 0x13;
inst[1] = 0x01<<mt_id;
if(motor_foreward == dir)
inst[2] = 0x01<<mt_id;
else
inst[2] = 0x00;
inst[3] = 0x01<<mt_id;
inst[4] = (char)0xab;
inst[5] = (inst[1]|inst[3])&(inst[2]|inst[4]);
inst[6] = 0x14;
}
// 普通运动
else
{
inst.resize(12);
// 前标志位
inst[0] = 0x13;
// 是否运动
//EnterCriticalSection(&m_hConMotorMux[mt_id]); // 进入线程锁
motor_isRun[mt_id] = 1;
inst[1] = 0x01<<mt_id;
// 运动方向
if(motor_foreward == dir)
inst[2] = 0x01<<mt_id;
else
inst[2] = 0x00;
// 是否前往端点
inst[3] = 0x00;
// 紧急停止0xff 其余0xab
inst[4] = (char)0xab;
// 校验位
inst[5] = (inst[1]|inst[3])&(inst[2]|inst[4]);
// 待定0xbc (以后用于设定速度加速度)
inst[6] = (char)0xbc;
// 步进长度
inst[7] = ((step&0xFF0000)>>16)&0xFF;
inst[8] = ((step&0x00FF00)>>8 )&0xFF;
inst[9] = ((step&0x0000FF) )&0xFF;
// 校验位
inst[10] = (inst[6]|inst[8])&(inst[7]|inst[9]);
// 后标志位
inst[11] = 0x14;
}
return inst;
}
这里只是简单的实现了单信息控制1个电机驱动的信息发送,有兴趣的可以自己扩充为单信息多电机驱动。
下位机发给上位机的信息解码和界面控制代码:
void MainWindow::readData()
{
const QByteArray data = m_serialPort->readAll();
if(0 == data.count())
return ;
// 校验起始和结束位 不正确则打印句子
if(0x11 != data.at(0) || 0x12 != data.at(data.count()-1))
{
// 打印完整句子
qDebug() << "";
qDebug() << "read Line: " << data;
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;
motor_pct_cnt = 0;
if( ( (data.at(1)|data.at(3)) & (data.at(2)|data.at(4)) ) != data.at(5) ) // 校验数据位
{
qDebug() << "readDate *** Wrong data request pos 5";
return ;
}
// 清空断联计时器
discnt = 0;
for (unsigned int i = 0; i < 8; i++)
{
// 优先检查电机限位
if(1 == ((data.at(4)>>i)&0x01))
{
if(1 == ((data.at(3)>>i)&0x01))
setPicture(i*2+2, motor_detect_warning); //端点触发
else
setPicture(i*2+1, motor_detect_warning); //原点触发
}
// 电机未限位
else if(0 == ((data.at(4)>>i)&0x01))
{
setPicture(i*2+2, motor_detect_ok);
setPicture(i*2+1, motor_detect_ok);
}
// 电机正在运转
if(1 == ((data.at(1)>>i)&0x01))
{
if(0 == motor_isRunShow[i])
{
const QString s = "Motor " + QString::number(i) + " is running";
showStatusMessage(s);
motor_isRunShow[i] = 1;
}
//motor_isRun[i] = 1;
// 运行百分比
if(data.at(6 + motor_pct_cnt)<0 || data.at(6 + motor_pct_cnt)>100)
{
qDebug() << "readDate *** Wrong data " + QString::number(6+motor_pct_cnt) + " percentage";
continue;
}
// 显示数据
setPercentage(i,data.at(6 + motor_pct_cnt));
motor_pct_cnt++;
}
// 电机停止运转
else if(0 == ((data.at(1)>>i)&0x01) && 1 == motor_isRun[i])
{
const QString s = "Motor " + QString::number(i) + " runs done";
showStatusMessage(s);
motor_isRun[i] = 0;
motor_isRunShow[i] = 0;
// 运行完成设置 100
setPercentage(i,100);
}
}
}
(4)心跳包的定时器
为了上下位机能及时地获取信息,我设置了一个定时器用于触发发送心跳包,触发间隔为8ms(好像和初衷有点出入,所以为啥当时不用HID协议呢???)。
// 设置心跳包定时器
timer_intv = 8;
t_askSTM = new QTimer(this);
t_askSTM->setInterval(timer_intv); //设置定时周期,单位:毫秒
connect(t_askSTM, SIGNAL(timeout()), this, SLOT(slots_t_askSTM()));
void MainWindow::slots_t_askSTM()
{
// 100ms 下位机无消息关闭面板
discnt++;
if(discnt * timer_intv > 100)
{
if(true == isConnect)
closeSerialPort();
discnt = 0;
}
// 轮询专用配料表
QByteArray inst = run_setup(0,0,0,0,0,true);
writeData(inst);
}
基于Qt的上位机开发就大致如此,重点是下面基于Keil的下位机开发。
二、Keil开发的下位机
1. 实现方式
多电机驱动的实现主要依托STM32的硬件,控制电机步进步数的参数是脉冲数量,控制电机步进速度的参数是脉冲间隔,由于需要精确控制驱动步进数量,故不能简单的依靠输出脉冲的时间来确定,经过大量的方案查找,最终确定了使用Timer的SlaveMode来实现脉冲数量计数,使用Timer输出PWM来实现脉冲间隔控制(其实我甚至都考虑过使用DAC或SPI来替代,毕竟脉冲本质上就是01电平变化)。
先看一张图:
(1) 2个32位 Timer 计数器 + 2个16位 Timer 输出器
#include "tim32t.h"
// *****
// PWM 发生定时器 初始化
// *****
//TIM8 PWM部分初始化
//PWM输出初始化
//arr:自动重装值
//psc:时钟预分频数
void tim8_pwm_init(uint16_t arr,uint16_t psc)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM8, ENABLE); // TIM8时钟使能
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE); // 使能PORTC时钟
TIM_DeInit(TIM8);
GPIO_PinAFConfig(GPIOC, GPIO_PinSource6, GPIO_AF_TIM8); // GPIOC6复用为定时器 8
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; // 复用功能
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; // 速度100MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // 推挽复用输出
GPIO_InitStructure.GPIO_PuPd = m_TIM_GPIO_PuPd; // 上下拉
GPIO_Init(GPIOC, &GPIO_InitStructure); // 初始化PC6
TIM_TimeBaseStructure.TIM_Prescaler = psc; // 定时器分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
TIM_TimeBaseStructure.TIM_Period = arr; // 自动重装载值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟不分频
TIM_TimeBaseInit(TIM8, &TIM_TimeBaseStructure); // 初始化定时器 8
//初始化TIM8 PWM模式
TIM_OCInitStructure.TIM_OCMode = m_TIM_OCMode; // 选择定时器模式:TIM脉冲宽度调制模式
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 比较输出使能
TIM_OCInitStructure.TIM_OCPolarity = m_TIM_OCPolarity; // 输出极性:TIM输出比较极性
TIM_OCInitStructure.TIM_Pulse = 0; // arr/2->占空比 50% | 0->低电平
TIM_OC1Init(TIM8, &TIM_OCInitStructure); // 根据TIM指定的参数初始化外设TIM8 OC1
TIM_OC1PreloadConfig(TIM8, TIM_OCPreload_Enable); // 使能TIM8在CCR1上的预装载寄存器
TIM_ARRPreloadConfig(TIM8, ENABLE); // ARPE使能
TIM_Cmd(TIM8, ENABLE); // 使能TIM8
TIM_CtrlPWMOutputs(TIM8, ENABLE); // 使能TIM8主输出
TIM_SelectOutputTrigger(TIM8,TIM_TRGOSource_OC1Ref); // 内部输出传送来源 OC1
TIM_SelectMasterSlaveMode(TIM8,TIM_MasterSlaveMode_Enable);
TIM_Cmd(TIM8, DISABLE); // 失能TIM8
//TIM_SetCompare1(TIM8, arr / 2); // 改变TIM 8 CH 1 占空比
}
//TIM4 PWM部分初始化
//PWM输出初始化
//arr:自动重装值
//psc:时钟预分频数
void tim4_pwm_init(uint16_t arr,uint16_t psc)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); // TIM4时钟使能
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE); // 使能PORTD时钟
TIM_DeInit(TIM4);
GPIO_PinAFConfig(GPIOD, GPIO_PinSource15, GPIO_AF_TIM4); // GPIOD15复用为定时器 4
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; // 复用功能
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; // 速度100MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // 推挽复用输出
GPIO_InitStructure.GPIO_PuPd = m_TIM_GPIO_PuPd; // 上下拉
GPIO_Init(GPIOD, &GPIO_InitStructure); // 初始化PD15
TIM_TimeBaseStructure.TIM_Prescaler = psc; // 定时器分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
TIM_TimeBaseStructure.TIM_Period = arr; // 自动重装载值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟不分频
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); // 初始化定时器 4
//初始化TIM4 PWM模式
TIM_OCInitStructure.TIM_OCMode = m_TIM_OCMode; // 选择定时器模式:TIM脉冲宽度调制模式
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 比较输出使能
TIM_OCInitStructure.TIM_OCPolarity = m_TIM_OCPolarity; // 输出极性:TIM输出比较极性
TIM_OCInitStructure.TIM_Pulse = 0; // arr/2->占空比 50% | 0->低电平
TIM_OC4Init(TIM4, &TIM_OCInitStructure); // 根据TIM指定的参数初始化外设TIM4 OC4
TIM_OC4PreloadConfig(TIM4, TIM_OCPreload_Enable); // 使能TIM4在CCR4上的预装载寄存器
TIM_ARRPreloadConfig(TIM4, ENABLE); // ARPE使能
TIM_Cmd(TIM4, ENABLE); // 使能TIM4
TIM_SelectOutputTrigger(TIM4,TIM_TRGOSource_OC4Ref); // 内部输出传送来源 OC4
TIM_SelectMasterSlaveMode(TIM4,TIM_MasterSlaveMode_Enable);
TIM_Cmd(TIM4, DISABLE); // 失能TIM4
//TIM_SetCompare4(TIM4, arr / 2); // 改变 TIM 4 CH 4 占空比
}
// *****
// PWM 计数从设备 初始化
// *****
// 初始化 计数从设备 TIM 2 对应主设备TIM 8
void tim2_cnt_init(uint32_t cntValue)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitTypeStruct;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_TimeBaseStructure.TIM_Prescaler = 0; // 定时器不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
TIM_TimeBaseStructure.TIM_Period = cntValue; // 自动重装载值(计数个数)
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟不分频
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); // 初始化定时器 2
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 清除更新中断标志位
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 开启更新中断
NVIC_InitTypeStruct.NVIC_IRQChannel = TIM2_IRQn; // 中断处理函数
NVIC_InitTypeStruct.NVIC_IRQChannelCmd = ENABLE; // 中断使能
NVIC_InitTypeStruct.NVIC_IRQChannelPreemptionPriority = 2; // 主优先级
NVIC_InitTypeStruct.NVIC_IRQChannelSubPriority = 1; // 次优先级
NVIC_Init(&NVIC_InitTypeStruct); // 初始化中断
TIM_SelectInputTrigger(TIM2, TIM_TS_ITR1); // 查表对应 主设备TIM 8
TIM_SelectSlaveMode(TIM2, TIM_SlaveMode_External1);
TIM_SetCounter(TIM2, 0); // 清空计数器
//TIM_Cmd(TIM2,ENABLE);
//TIM_SetAutoreload(TIM2, cntValue) // 设置计数个数
//uint32_t cnt = TIM_GetCounter(TIM2); // 获取完成情况
}
// 初始化 计数从设备 TIM 5 对应主设备 TIM 4
void tim5_cnt_init(uint32_t cntValue)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitTypeStruct;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE);
TIM_TimeBaseStructure.TIM_Prescaler = 0; // 定时器不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
TIM_TimeBaseStructure.TIM_Period = cntValue; // 自动重装载值(计数个数)
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟不分频
TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); // 初始化定时器 5
TIM_ClearITPendingBit(TIM5, TIM_IT_Update); // 清除更新中断标志位
TIM_ITConfig(TIM5, TIM_IT_Update, ENABLE); // 开启更新中断
NVIC_InitTypeStruct.NVIC_IRQChannel = TIM5_IRQn; // 中断处理函数
NVIC_InitTypeStruct.NVIC_IRQChannelCmd = ENABLE; // 中断使能
NVIC_InitTypeStruct.NVIC_IRQChannelPreemptionPriority = 2; // 主优先级
NVIC_InitTypeStruct.NVIC_IRQChannelSubPriority = 1; // 次优先级
NVIC_Init(&NVIC_InitTypeStruct); // 初始化中断
TIM_SelectInputTrigger(TIM5, TIM_TS_ITR2); // 查表对应 主设备 TIM 4
TIM_SelectSlaveMode(TIM5, TIM_SlaveMode_External1);
//TIM_SelectMasterSlaveMode(TIM5, TIM_MasterSlaveMode_Enable);
TIM_SetCounter(TIM5, 0); // 清空计数器
//TIM_Cmd(TIM5,ENABLE);
//TIM_SetAutoreload(TIM5, cntValue) // 设置计数个数
//uint32_t cnt = TIM_GetCounter(TIM5); // 获取完成情况
}
// *****
// PWM 计数从设备 中断处理函数
// *****
// 从 TIM2 主 TIM8 对应 Motor 0 中断处理函数
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
TIM_SetCompare1(TIM8, 0); //TIM 8 CH 1 停止产生PWM
TIM_Cmd(TIM2, DISABLE); // 失能 计数 定时器
// 处理逻辑
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
// 从 TIM5 主 TIM4 对应 Motor 3 中断处理函数
void TIM5_IRQHandler(void)
{
if (TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET)
{
TIM_SetCompare4(TIM4, 0); // TIM 4 CH 4 停止产生PWM
TIM_Cmd(TIM5, DISABLE); // 失能 计数 定时器
// 处理逻辑
TIM_ClearITPendingBit(TIM5, TIM_IT_Update);
}
}
// *****
// PWM 计数从设备 开始函数
// *****
// 从 TIM2 主 TIM8 对应 Motor 0 开始运行
void tim2_tim8_mot0_start(void)
{
// 设置 PWM 和 CNT 定时器
TIM_SetAutoreload(TIM8, /*设置速度*/); // 更改 pwm 重装载值
TIM_SetAutoreload(TIM2, /*设置步数*/); // 更改 计数 重装载值
TIM_SetCounter(TIM2, 0); // 清空计数器
TIM_SetCounter(TIM8, 0); // 清空计数器
// 处理逻辑
TIM_SetCompare1(TIM8, /*高电平时长*/); // CH 1 继续 pwm 占空比
TIM_Cmd(TIM2, ENABLE); // 使能 计数 定时器
TIM_Cmd(TIM8, ENABLE); // 使能 pwm 定时器
}
// 从 TIM5 主 TIM4 对应 Motor 3 开始运行
void tim5_tim4_mot3_start(void)
{
// 设置 PWM 和 CNT 定时器
TIM_SetAutoreload(TIM4, /*设置速度*/); // 更改 pwm 重装载值
TIM_SetAutoreload(TIM5, /*设置步数*/); // 更改 计数 重装载值
TIM_SetCounter(TIM5, 0); // 清空计数器
TIM_SetCounter(TIM4, 0); // 清空计数器
// 处理逻辑
TIM_SetCompare4(TIM4, /*高电平时长*/); // CH 4 继续 pwm 占空比
TIM_Cmd(TIM5, ENABLE); // 使能 计数 定时器
TIM_Cmd(TIM4, ENABLE); // 使能 pwm 定时器
}
// *****
// PWM 计数从设备 停止函数
// *****
// 从 TIM2 主 TIM8 对应 Motor 0 停止运行
void tim2_tim8_mot0_stop(void)
{
TIM_SetCompare1(TIM8, 0); // 改变TIM 8 CH 1 占空比
TIM_Cmd(TIM2, DISABLE); // 失能 计数 定时器
TIM_GenerateEvent(TIM8,TIM_EventSource_Update); // 触发更新事件,用于确定关闭定时器后输出低电平
TIM_Cmd(TIM8, DISABLE); // 失能 pwm 定时器
TIM_SetCounter(TIM2, 0); // 清空计数器
TIM_SetCounter(TIM8, 0); // 输出低电平
}
// 从 TIM5 主 TIM4 对应 Motor 3 停止运行
void tim5_tim4_mot3_stop(void)
{
TIM_SetCompare4(TIM4, 0); // 改变TIM 4 CH 4 占空比
TIM_Cmd(TIM5, DISABLE); // 失能 计数 定时器
TIM_GenerateEvent(TIM4,TIM_EventSource_Update); // 触发更新事件,用于确定关闭定时器后输出低电平
TIM_Cmd(TIM4, DISABLE); // 失能 pwm 定时器
TIM_SetCounter(TIM5, 0); // 清空计数器
TIM_SetCounter(TIM4, 0); // 输出低电平
}
我们再来讲讲如何实现的,根据STM32的官方文档,如下: 从图中我们可以知道TIM2、3、4、5、9和12可以作为从定时器(时钟触发来源可以更改),它的时钟来源根据 ITRx 的控制可以连通到不同的主定时器。工作时,主定时器负责输出PWM脉冲信号,同时将脉冲信号输出到从定时器,从定时器对主定时器传来的脉冲进行计数,当脉冲数量达到某一预先设置好的阈值时,从定时器会溢出并触发中断,从而进入中断函数对主定时器进行控制,达到对脉冲数量的控制。
这里你可能会问,那怎么控制脉冲速度?这里我想来想去确实没想到比较好的纯硬件办法(因为提速需要改变脉冲频率,脉冲频率的改变需要改变定时器的分频系数或溢出值,根据官方文档的描述,这需要 STOP 定时器重新设置,停止计数器重新设置就会带来不可控的时延导致速度不准确,所以到头来只能考虑用单脉冲模式来处理,但是单脉冲模式在只适合在低速情况下使用,速度快了中断会处理不过来),我目前的办法通过软件的方式将加速脉冲分成几段逐步提高脉冲频率,间接实现看上去不那么连贯的加速效果。
接下来你可能会问,如果我计数的脉冲数量超过了32位怎么办?其实这问题在16位的定时器会更严重,目前也是没想到比较更好的纯硬件办法,还是老老实实的通过软件分段的方式来处理的。
下面的16位计数器同32位初始化方式基本一样,贴一下代码就不做讲解了。
(2) 2个16位 Timer 计数器 + 2个16位 Timer 输出器
#include "tim16t.h"
// *****
// PWM 发生定时器 初始化
// *****
//TIM10 PWM部分初始化
//PWM输出初始化
//arr:自动重装值
//psc:时钟预分频数
void tim10_pwm_init(uint16_t arr, uint16_t psc)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM10, ENABLE); // TIM10时钟使能
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE); // 使能PORT时钟
TIM_DeInit(TIM10);
GPIO_PinAFConfig(GPIOF, GPIO_PinSource6, GPIO_AF_TIM10); // GPIOC6复用为定时器 10
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; // 复用功能
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; // 速度100MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // 推挽复用输出
GPIO_InitStructure.GPIO_PuPd = m_TIM_GPIO_PuPd; // 上下拉
GPIO_Init(GPIOF, &GPIO_InitStructure); // 初始化PF6
TIM_TimeBaseStructure.TIM_Prescaler = psc; // 定时器分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
TIM_TimeBaseStructure.TIM_Period = arr; // 自动重装载值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟不分频
TIM_TimeBaseInit(TIM10, &TIM_TimeBaseStructure); // 初始化定时器 10
//初始化TIM10 PWM模式
TIM_OCInitStructure.TIM_OCMode = m_TIM_OCMode; // 选择定时器模式:TIM脉冲宽度调制模式
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 比较输出使能
TIM_OCInitStructure.TIM_OCPolarity = m_TIM_OCPolarity; // 输出极性:TIM输出比较极性
TIM_OCInitStructure.TIM_Pulse = 0; // arr/2->占空比 50% | 0->低电平
TIM_OC1Init(TIM10, &TIM_OCInitStructure); // 根据TIM指定的参数初始化外设TIM10 OC1
TIM_OC1PreloadConfig(TIM10, TIM_OCPreload_Enable); // 使能TIM10在CCR1上的预装载寄存器
TIM_ARRPreloadConfig(TIM10, ENABLE); // ARPE使能
TIM_Cmd(TIM10, ENABLE); // 使能TIM10
TIM_SelectOutputTrigger(TIM10,TIM_TRGOSource_OC1Ref); // 内部输出传送来源 OC1
TIM_SelectMasterSlaveMode(TIM10,TIM_MasterSlaveMode_Enable);
/*
(#) Configure the Master Timers using the following functions:
(++) void TIM_SelectOutputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_TRGOSource);
(++) void TIM_SelectMasterSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_MasterSlaveMode);
(#) Configure the Slave Timers using the following functions:
(++) void TIM_SelectInputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
(++) void TIM_SelectSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_SlaveMode);
*/
TIM_Cmd(TIM10, DISABLE); // 失能TIM10
//TIM_SetCompare1(TIM10, arr / 2); // 改变 TIM 10 CH 1 占空比
}
//TIM13 PWM部分初始化
//PWM输出初始化
//arr:自动重装值
//psc:时钟预分频数
void tim13_pwm_init(uint16_t arr, uint16_t psc)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM13, ENABLE); // TIM13时钟使能
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); // 使能PORT时钟
TIM_DeInit(TIM13);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_TIM13); // GPIOA6复用为定时器 13
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; // 复用功能
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; // 速度100MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // 推挽复用输出
GPIO_InitStructure.GPIO_PuPd = m_TIM_GPIO_PuPd; // 上下拉
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化PA6
TIM_TimeBaseStructure.TIM_Prescaler = psc; // 定时器分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
TIM_TimeBaseStructure.TIM_Period = arr; // 自动重装载值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟不分频
TIM_TimeBaseInit(TIM13, &TIM_TimeBaseStructure); // 初始化定时器 13
//初始化TIM13 PWM模式
TIM_OCInitStructure.TIM_OCMode = m_TIM_OCMode; // 选择定时器模式:TIM脉冲宽度调制模式
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 比较输出使能
TIM_OCInitStructure.TIM_OCPolarity = m_TIM_OCPolarity; // 输出极性:TIM输出比较极性
TIM_OCInitStructure.TIM_Pulse = 0; // arr/2->占空比 50% | 0->低电平
TIM_OC1Init(TIM13, &TIM_OCInitStructure); // 根据TIM指定的参数初始化外设TIM13 OC1
TIM_OC1PreloadConfig(TIM13, TIM_OCPreload_Enable); // 使能TIM13在CCR1上的预装载寄存器
TIM_ARRPreloadConfig(TIM13, ENABLE); // ARPE使能
TIM_Cmd(TIM13, ENABLE); // 使能TIM13
TIM_SelectOutputTrigger(TIM13,TIM_TRGOSource_OC1Ref); // 内部输出传送来源 OC1
TIM_SelectMasterSlaveMode(TIM13,TIM_MasterSlaveMode_Enable);
/*
(#) Configure the Master Timers using the following functions:
(++) void TIM_SelectOutputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_TRGOSource);
(++) void TIM_SelectMasterSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_MasterSlaveMode);
(#) Configure the Slave Timers using the following functions:
(++) void TIM_SelectInputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
(++) void TIM_SelectSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_SlaveMode);
*/
TIM_Cmd(TIM13, DISABLE); // 失能TIM13
//TIM_SetCompare1(TIM13, arr / 2); // 改变 TIM 13 CH 1 占空比
}
// *****
// PWM 计数从设备 初始化
// *****
// 初始化 计数从设备 TIM 9 对应主设备 TIM 10
void tim9_cnt_init(uint16_t cntValue)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitTypeStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM9, ENABLE);
TIM_TimeBaseStructure.TIM_Prescaler = 0; // 定时器不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
TIM_TimeBaseStructure.TIM_Period = cntValue; // 自动重装载值(计数个数)
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟不分频
TIM_TimeBaseInit(TIM9, &TIM_TimeBaseStructure); // 初始化定时器 2
TIM_ClearITPendingBit(TIM9, TIM_IT_Update); // 清除更新中断标志位
TIM_ITConfig(TIM9, TIM_IT_Update, ENABLE); // 开启更新中断
NVIC_InitTypeStruct.NVIC_IRQChannel = TIM1_BRK_TIM9_IRQn; // 中断处理函数
NVIC_InitTypeStruct.NVIC_IRQChannelCmd = ENABLE; // 中断使能
NVIC_InitTypeStruct.NVIC_IRQChannelPreemptionPriority = 2; // 主优先级
NVIC_InitTypeStruct.NVIC_IRQChannelSubPriority = 2; // 次优先级
NVIC_Init(&NVIC_InitTypeStruct); // 初始化中断
TIM_SelectInputTrigger(TIM9, TIM_TS_ITR2); // 查表对应 主设备 TIM 10
TIM_SelectSlaveMode(TIM9, TIM_SlaveMode_External1);
/*
(#) Configure the Master Timers using the following functions:
(++) void TIM_SelectOutputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_TRGOSource);
(++) void TIM_SelectMasterSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_MasterSlaveMode);
(#) Configure the Slave Timers using the following functions:
(++) void TIM_SelectInputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
(++) void TIM_SelectSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_SlaveMode);
*/
TIM_SetCounter(TIM9, 0); // 清空计数器
//TIM_Cmd(TIM9,ENABLE);
//TIM_SetAutoreload(TIM9, cntValue) // 设置计数个数
//uint32_t cnt = TIM_GetCounter(TIM9); // 获取完成情况
}
// 初始化 计数从设备 TIM 13 对应主设备 TIM 12
void tim12_cnt_init(uint16_t cntValue)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitTypeStruct;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM12, ENABLE);
TIM_TimeBaseStructure.TIM_Prescaler = 0; // 定时器不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
TIM_TimeBaseStructure.TIM_Period = cntValue; // 自动重装载值(计数个数)
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟不分频
TIM_TimeBaseInit(TIM12, &TIM_TimeBaseStructure); // 初始化定时器 5
TIM_ClearITPendingBit(TIM12, TIM_IT_Update); // 清除更新中断标志位
TIM_ITConfig(TIM12, TIM_IT_Update, ENABLE); // 开启更新中断
NVIC_InitTypeStruct.NVIC_IRQChannel = TIM8_BRK_TIM12_IRQn; // 中断处理函数
NVIC_InitTypeStruct.NVIC_IRQChannelCmd = ENABLE; // 中断使能
NVIC_InitTypeStruct.NVIC_IRQChannelPreemptionPriority = 2; // 主优先级
NVIC_InitTypeStruct.NVIC_IRQChannelSubPriority = 2; // 次优先级
NVIC_Init(&NVIC_InitTypeStruct); // 初始化中断
TIM_SelectInputTrigger(TIM12, TIM_TS_ITR2); // 查表对应 主设备 TIM12
TIM_SelectSlaveMode(TIM12, TIM_SlaveMode_External1);
//TIM_SelectMasterSlaveMode(TIM12, TIM_MasterSlaveMode_Enable);
TIM_SetCounter(TIM12, 0); // 清空计数器
//TIM_Cmd(TIM12,ENABLE);
//TIM_SetAutoreload(TIM12, cntValue) // 设置计数个数
//uint32_t cnt = TIM_GetCounter(TIM12); // 获取完成情况
}
// *****
// PWM 计数从设备 中断处理函数
// *****
// 从 TIM9 主 TIM10 对应 Motor 2 中断处理函数
void TIM1_BRK_TIM9_IRQHandler(void)
{
if (TIM_GetITStatus(TIM9, TIM_IT_Update) != RESET)
{
TIM_SetCompare1(TIM10, 0); // TIM 10 CH 1 停止产生PWM
TIM_Cmd(TIM9, DISABLE); // 失能 计数 定时器
// 处理逻辑
TIM_ClearITPendingBit(TIM9, TIM_IT_Update);
}
}
// 从 TIM12 主 TIM13 对应 Motor 5 中断处理函数
void TIM8_BRK_TIM12_IRQHandler(void)
{
if (TIM_GetITStatus(TIM12, TIM_IT_Update) != RESET)
{
TIM_SetCompare1(TIM13, 0); // TIM13 CH 1 停止产生PWM
TIM_Cmd(TIM12, DISABLE); // 失能 计数 定时器
// 处理逻辑
TIM_ClearITPendingBit(TIM12, TIM_IT_Update);
}
}
// *****
// PWM 计数从设备 开始函数
// *****
// 从 TIM9 主 TIM10 对应 Motor 2 开始运行
void tim9_tim10_mot2_start(void)
{
// 设置 PWM 和 CNT 定时器
TIM_SetAutoreload(TIM10, /*设置速度*/); // 更改 pwm 重装载值
TIM_SetAutoreload(TIM9, /*设置步数*/); // 更改 计数 重装载值
TIM_SetCounter(TIM9, 0); // 清空计数器
TIM_SetCounter(TIM10, 0); // 清空计数器
// 处理逻辑
TIM_SetCompare1(TIM10, /*高电平时长*/); // CH 1 继续 pwm 占空比
TIM_Cmd(TIM9, ENABLE); // 使能 计数 定时器
TIM_Cmd(TIM10, ENABLE); // 使能 pwm 定时器
}
// 从 TIM12 主 TIM13 对应 Motor 5 开始运行
void tim12_tim13_mot5_start(void)
{
// 设置 PWM 和 CNT 定时器
TIM_SetAutoreload(TIM13, /*设置速度*/); // 更改 pwm 重装载值
TIM_SetAutoreload(TIM12, /*设置步数*/); // 更改 计数 重装载值
TIM_SetCounter(TIM12, 0); // 清空计数器
TIM_SetCounter(TIM13, 0); // 清空计数器
// 处理逻辑
TIM_SetCompare1(TIM13, /*高电平时长*/); // CH 1 继续 pwm 占空比
TIM_Cmd(TIM12, ENABLE); // 使能 计数 定时器
TIM_Cmd(TIM13, ENABLE); // 使能 pwm 定时器
}
// *****
// PWM 计数从设备 停止函数
// *****
// 从 TIM9 主 TIM10 对应 Motor 2 停止运行
void tim9_tim10_mot2_stop(void)
{
TIM_SetCompare1(TIM10, 0); // 改变TIM 10 CH 1 占空比
TIM_Cmd(TIM9, DISABLE); // 失能 计数 定时器
TIM_GenerateEvent(TIM10,TIM_EventSource_Update); // 触发更新事件,用于确定关闭定时器后输出低电平
TIM_Cmd(TIM10, DISABLE); // 失能 pwm 定时器
TIM_SetCounter(TIM9, 0); // 清空计数器
TIM_SetCounter(TIM10, 0); // 输出低电平
}
// 从 TIM12 主 TIM13 对应 Motor 5 停止运行
void tim12_tim13_mot5_stop(void)
{
TIM_SetCompare1(TIM13, 0); // 改变TIM 13 CH 1 占空比
TIM_Cmd(TIM12, DISABLE); // 失能 计数 定时器
TIM_GenerateEvent(TIM13,TIM_EventSource_Update); // 触发更新事件,用于确定关闭定时器后输出低电平
TIM_Cmd(TIM13, DISABLE); // 失能 pwm 定时器
TIM_SetCounter(TIM12, 0); // 清空计数器
TIM_SetCounter(TIM13, 0); // 输出低电平
}
(3) DMA + 2个16位 Timer 输出器
(4) 中断计数 + 2个16位 Timer 输出器
(5) 上、下位机通信
三、其他版本算法讲解
1. 直线
2. 圆弧
下一节联合调试(4/4)我会将讲解我是如何解决在开发和调试中解决各部分遇到的问题。
相关连接:
去简介(1/4)、去基础知识(2/4)、去程序开发(3/4)、去联合调试(4/4)
好用的工具网站推荐