本文我们通过一个简单的上位机发送指令,通过STM32板卡驱动机械臂运动的例子说明STM32上位机通信的过程。
STM32控制板上的代码
控制板与上位机之间通过USART协议进行通信,首先启动Keil uVision,打开你已创建的项目,增加两个文件,一个是usart.h,一个是usart.c,主要负责与上位机通信的过程,当然,你也可以用其他名字作为文件名。
usart.c的代码如下:
#include "usart.h"
void uart1_init(u32 baud) {
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
USART_ClockInitTypeDef USART_ClockInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); //使能USART1, GPIOA时钟
USART_DeInit(USART1);
//USART1_TX, GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推免输出
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化GPIOA.9
//USART1_RX, GPIOA.10初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; //PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10
//USART1 NVIC设置
USART_ClockInitStructure.USART_Clock = USART_Clock_Disable;
USART_ClockInitStructure.USART_CPOL = USART_CPOL_Low;
USART_ClockInitStructure.USART_CPHA = USART_CPHA_2Edge;
USART_ClockInitStructure.USART_LastBit = USART_LastBit_Disable;
USART_ClockInit(USART1, &USART_ClockInitStructure);
//USART初始化设置
USART_InitStructure.USART_BaudRate = baud; //串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1; //一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No; //无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART1, &USART_InitStructure ); //初始化串口1
//USART1 NVIC设置
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //子优先级1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接受中断
USART_Cmd(USART1, ENABLE); //使能串口1
uart1_open(); //打开USART1中断
}
/***********************************************
函数名称: uart1_send_byte()
功能介绍: 串口1发送字节
函数参数: dat 发送的字节
返回值: 无
***********************************************/
void uart1_send_byte(u8 dat) {
USART_SendData(USART1, dat);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
return;
}
/***********************************************
函数名称: uart1_send_str()
功能介绍: 串口1发送字符串
函数参数: *s 发送的字符串
返回值: 无
***********************************************/
void uart1_send_str(u8 *s) {
uart1_close();
while (*s) {
uart1_send_byte(*s++);
}
uart1_open();
}
/***********************************************
函数名称: void USART1_IRQHandler(void)
功能介绍: 串口1中断函数
函数参数: 无
返回值: 无
***********************************************/
void USART1_IRQHandler(void) {
static u8 sbuf_bak;
if(USART_GetFlagStatus(USART1,USART_IT_RXNE)==SET) {
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
sbuf_bak = USART_ReceiveData(USART1);
/*******返回接收到的指令*******/
uart1_send_byte(sbuf_bak);
/*******若正在执行命令,则不存储命令*******/
if(uart_get_ok) return;
/*******检测命令起始*******/
if(sbuf_bak == '$') {
uart_receive_buf_index = 0;
}
/*******检测命令结尾*******/
else if(sbuf_bak == '!'){
uart_receive_buf[uart_receive_buf_index] = sbuf_bak;
uart1_send_str((u8 *)"\r\n");
uart_get_ok = 1;
return;
}
uart_receive_buf[uart_receive_buf_index++] = sbuf_bak;
/*******检测命令长度*******/
if(uart_receive_buf_index >= UART_RECEIVE_BUF_SIZE) {
uart_receive_buf_index = 0;
}
}
return;
}
上面的代码,简单解释一下,它包含了一个初始化函数uart1_init(u32 baud),一个中断函数USART1_IRQHandler(void),还有一些数据发送函数。
首先USART通信建立的时候,需要对接口作一些初始化的工作,这部分工作都是在 uart1_init(u32 baud) 函数中完成的。建立连接之后,就可以开始传输数据了。
其次STM32控制板接收数据的功能是在中断函数 USART1_IRQHandler(void) 中完成的,中断函数可以类比于PC机操作系统中的线程机制,它独立地异步接收数据,然后交给主线程处理。STM32控制板中有若干个USART中断函数,以USART后面带的数字来指代,例如 USART1_IRQHandler 指代USART1的中断函数。
最后是一些发送数据的函数,如果需要向上位机返回命令的执行结果,可以调用这些函数。
另外usart.h头文件代码大概如下:
#ifndef __UART_H__
#define __UART_H__
#include <string.h>
#include "stm32f10x.h"
#define uart1_open() USART_ITConfig(USART1, USART_IT_RXNE, ENABLE)
#define uart1_close() USART_ITConfig(USART1, USART_IT_RXNE, DISABLE)
void uart1_init(u32 baud);
void uart1_send_byte(u8 dat);
void uart1_send_str(u8 *s);
#endif
USART代码写好之后,直接在main函数中调用就可以了。
int main(void) {
//usart
uart1_init(115200);
uart1_open();
}
上位机上的C++代码
STM32控制板上的代码写好之后,就可以开始写上位机的代码了,这部分比较简单,跟其他串口通信代码没有什么区别。同时你也可以选择同步或者异步的方式来收发数据。
这里我们采用C++作为开发语言,为方便起见,我们使用QT作为开发工具,当然你也可以使用任何其他工具,本质上差别不大,只是一些字符串处理函数等需要替换一下。
首先创建一个C++项目,然后新建一个串口通信类,这里命名为serialcommunication.cpp,代码如下:
#include "serialcommunication.h"
#include <algorithm>
SerialCommunication::SerialCommunication()
{
hcom = NULL;
portName = "";
}
bool SerialCommunication::open(int baudrate, char parity, char databit, char stopbit)
{
return open(portName, baudrate, parity, databit, stopbit);
}
bool SerialCommunication::open(string portName, int baudrate, char parity, char databit, char stopbit)
{
hcom = CreateFileA(portName.c_str(), //串口名
GENERIC_READ | GENERIC_WRITE, //支持读写
0, //独占方式,串口不支持共享
NULL,//安全属性指针,默认值为NULL
OPEN_EXISTING, //打开现有的串口文件
0, //0:同步方式,FILE_FLAG_OVERLAPPED:异步方式
NULL);//用于复制文件句柄,默认值为NULL,对串口而言该参数必须置为NULL
if(hcom == INVALID_HANDLE_VALUE) {
return false;
}
if(!SetupComm(hcom, 1024, 1024)) {
return false;
}
DCB cfg;
GetCommState(hcom, &cfg);
cfg.BaudRate = baudrate;
cfg.Parity = parity;
cfg.ByteSize = databit;
cfg.StopBits = stopbit;
SetCommState(hcom, &cfg);
//超时处理,单位:毫秒
//总超时=时间系数×读或写的字符数+时间常量
COMMTIMEOUTS TimeOuts;
TimeOuts.ReadIntervalTimeout = 1000; //读间隔超时
TimeOuts.ReadTotalTimeoutMultiplier = 500; //读时间系数
TimeOuts.ReadTotalTimeoutConstant = 5000; //读时间常量
TimeOuts.WriteTotalTimeoutMultiplier = 500; // 写时间系数
TimeOuts.WriteTotalTimeoutConstant = 2000; //写时间常量
SetCommTimeouts(hcom, &TimeOuts);
PurgeComm(hcom, PURGE_TXCLEAR | PURGE_RXCLEAR);//清空串口缓冲区
return true;
}
void SerialCommunication::close()
{
if(!CloseHandle(hcom)) {
DWORD err = GetLastError();
printf("CloseHandle error: %d\r\n", (int)err);
}
}
int SerialCommunication::read(int byteToRead)
{
// OVERLAPPED overlap;
// overlap.Offset = 0;
// overlap.OffsetHigh = 0;
// overlap.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
DWORD dwCount = min(1024, byteToRead);
char data[1024];
bool bReadStat = ReadFile(hcom, data, dwCount, &dwCount, NULL);
if(bReadStat)
{
recvData = "";
int endSymbolCount = 0;
for(int i=0; i<1024; i++)
{
recvData += data[i];
if (data[i] == 33) {
endSymbolCount++;
}
if(endSymbolCount >= 2) {
break;
}
}
} else {
DWORD err = GetLastError();
printf("Readfile error: %d", (int)err);
}
return dwCount;
}
bool SerialCommunication::write(string data)
{
// OVERLAPPED overlap;
// overlap.Offset = 0;
// overlap.OffsetHigh = 0;
// overlap.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
// 同步方式
DWORD dwBytesWrite = data.length(); //成功写入的数据字节数
BOOL bWriteStat = WriteFile(hcom, //串口句柄
(char*)data.c_str(), //数据首地址
dwBytesWrite, //要发送的数据字节数
&dwBytesWrite, //DWORD*,用来接收返回成功发送的数据字节数
NULL); //NULL为同步发送,OVERLAPPED*为异步发送
return bWriteStat;
}
int SerialCommunication::getByteInCom()
{
DWORD dwError = 0; /** 错误码 */
COMSTAT comstat; /** COMSTAT结构体,记录通信设备的状态信息 */
memset(&comstat, 0, sizeof(COMSTAT));
UINT BytesInQue = 0;
/** 在调用ReadFile和WriteFile之前,通过本函数清除以前遗留的错误标志 */
if (ClearCommError(hcom, &dwError, &comstat))
{
BytesInQue = comstat.cbInQue; /** 获取在输入缓冲区中的字节数 */
}
return BytesInQue;
}
string SerialCommunication::getPortName() const
{
return portName;
}
void SerialCommunication::setPortName(const string &value)
{
portName = value;
}
string SerialCommunication::getRecvData() const
{
return recvData;
}
char* SerialCommunication::wideCharToMultiByte(wchar_t* pWCStrKey)
{
//第一次调用确认转换后单字节字符串的长度,用于开辟空间
int pSize = WideCharToMultiByte(CP_OEMCP, 0, pWCStrKey, wcslen(pWCStrKey), NULL, 0, NULL, NULL);
char* pCStrKey = new char[pSize + 1];
//第二次调用将双字节字符串转换成单字节字符串
WideCharToMultiByte(CP_OEMCP, 0, pWCStrKey, wcslen(pWCStrKey), pCStrKey, pSize, NULL, NULL);
pCStrKey[pSize] = '\0';
return pCStrKey;
//如果想要转换成string,直接赋值即可
//string pKey = pCStrKey;
}
vector<string> SerialCommunication::getComPorts()
{
HKEY hKey;
wchar_t portName[256], w_commName[256];
std::vector<std::string> comName;
//打开串口注册表对应的键值
if (ERROR_SUCCESS == ::RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"Hardware\\DeviceMap\\SerialComm", NULL, KEY_READ, &hKey))
{
int i = 0;
DWORD dwLong, dwSize;
while (TRUE)
{
dwLong = dwSize = sizeof(portName);
//枚举串口
if (ERROR_NO_MORE_ITEMS == ::RegEnumValue(hKey, i, portName, &dwLong, NULL, NULL, (PUCHAR)w_commName, &dwSize))
{
break;
}
char* commName = wideCharToMultiByte(w_commName);
comName.push_back(commName);
delete[] commName;
i++;
}
//关闭注册表
RegCloseKey(hKey);
}
else
{
//MessageBox(NULL, "您的计算机的注册表上没有HKEY_LOCAL_MACHINE:Hardware\\DeviceMap\\SerialComm项", "警告", MB_OK);
}
//返回串口号
return comName;
}
然后在测试界面的按钮函数中加入如下代码:
ui->textEdit_recvData->clear();
string sendData;
if(ui->checkBox_GenCmd->isChecked())
{
sendData = tr("#00%1P%2T%3!").arg(ui->spinBox_Axis->value()).arg(ui->spinBox_PWMValue->value())
.arg(ui->spinBox_Time->value()).toStdString();
ui->textEdit_sendData->setText(QString::fromStdString(sendData));
}
if(serialComm.open())
{
qDebug() << tr("successfully open %1").arg(QString::fromStdString(serialComm.getPortName()));
sendData = ui->textEdit_sendData->toPlainText().toLatin1().toStdString();
bool sendResult = serialComm.write(sendData);
if(sendResult) {
qDebug() << "Send OK.";
} else {
qDebug() << "send failed.";
}
recvComTimer->start(10);
connect(recvComTimer, SIGNAL(timeout()),this, SLOT(RecvBuffer()));
delay(1000);
recvComTimer->stop();
serialComm.close();
} else {
qDebug() << "failed to open serial port.";
}
发送的数据格式如下:#00%1P%2T%3!,其中%1代表舵机号,%2代表转动角度,%3代表用时(快慢),#是命令开始,!是命令结束符。
该代码调用serialComm.open()打开串口通信接口,serialComm.write(sendData)发送数据,然后用RecvBuffer()接收数据。RecvBuffer()代码如下:
void RobotPanel::RecvBuffer()
{
//qDebug() << "Receive data start...";
int byteNum = serialComm.getByteInCom();
if(serialComm.read(byteNum) > 0)
{
QString recvBuf = QString::fromStdString(serialComm.getRecvData());
ui->textEdit_recvData->append(recvBuf);
}
}