本文我们通过一个简单的上位机发送指令,通过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);
    }
}