01 前言
串口通訊對于任何開發(fā)板都是非常重要的,也是必學知識之一,通過串口通信可以實現(xiàn)上位機與下位機之間、開發(fā)板之間的通訊,可以讓我們實時掌握機器人的各個關機的運動狀態(tài)和傳感器的信息。
現(xiàn)在的通信協(xié)議有很多,比如:UART、USART、CAN、SPI等等,它們功能不同,適用于不同的場合,USART作為單片機之間、下位機與上位機之間最常用的通訊方式之一,它對于數(shù)據(jù)的收發(fā)十分方便,應用日益廣泛。
02 USART通信原理
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)中文名叫:全雙工通用同步/異步串行收發(fā)模塊。
先對這個名字的各個部分進行解釋:
在平時的工程項目中,我們常用的是全雙工串行異步通信方式,雖然串行通信數(shù)據(jù)是一位一位的發(fā)送,但隨著近幾年科技的高速發(fā)展,串行通訊的速度已經(jīng)逐漸趕超并行通訊了,而且串行通訊方式適用于遠距離通訊,比較常用。
USART通訊的數(shù)據(jù)格式大致是這樣:
起始位(0)+串行數(shù)據(jù)幀(從低位到高位傳輸)+停止位(1)
串行數(shù)據(jù)幀可以人為設置為8位或者9位,9位是8位數(shù)據(jù)加上1位校驗位(奇偶校驗)。
另外一個比較重要的概念是波特率,在任何通訊開始之前,通訊雙方都要約定好波特率,波特率是每秒發(fā)送有效數(shù)據(jù)的位數(shù)(bit/s),雙方如果沒有約定好一致的波特率,在傳輸過程中則會出現(xiàn)亂碼的情況。
在STM32中,有專門的數(shù)據(jù)寄存器和特定的引腳負責USART通訊,并配合有相應的標志位,用于幫我們判斷數(shù)據(jù)是否發(fā)送/接收完畢,并且也有相關的庫函數(shù)幫助我們對串口進行配置。
相關寄存器有:發(fā)送數(shù)據(jù)寄存器(TDR)、發(fā)送移位寄存器、接收數(shù)據(jù)寄存器(RDR)、接受移位寄存器。
相關標志位有:
- TEX標志位:1:數(shù)據(jù)寄存器無數(shù)據(jù);0:數(shù)據(jù)寄存器有數(shù)據(jù)
- TX標志位:1:發(fā)送完成;0:發(fā)送未完成
- RXNE標志位:1:數(shù)據(jù)接收完成,可以讀出;0:數(shù)據(jù)未收到
具體知識在中文參考手冊P517,大家可以詳細查看
03 STM32與PC通訊
STM32與PC通訊需要進行一些配置,這里實現(xiàn)由PC端向STM32發(fā)送一個數(shù)據(jù),STM32接收到后再發(fā)回到PC端,該實驗需要用到串口調試助手。
STM32可以作為串口通訊的引腳大家可以通過數(shù)據(jù)手冊進行查看,比如PA9(TX)和PA10(RX)
串口通訊一般都配合中斷進行使用,下面講解串口配置過程:
1.串口時鐘、GPIO時鐘使能 RCC_APB2PeriphClockCmd
2.GPIO端口模式配置 GPIO_Init
3.串口參數(shù)初始化 USART_Init
4.開啟終端并初始化NVIC USART_ITConfig、NVIC_Init
5.使能串口 USART_Cmd 6.編寫中斷服務函數(shù) USART1_IRQHandler
my_usart.c 代碼如下:
void My_Usart1_Init(int bound)
{
/*1.串口時鐘、GPIOA使能*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA,ENABLE);
/*2.GPIO端口模式設置*/
GPIO_InitTypeDef GPIO_InitStruct;
/*TX*/
GPIO_InitStruct.GPIO_Pin = Usart1_TX;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
/*RX*/
GPIO_InitStruct.GPIO_Pin = Usart1_RX;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
//GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
/*3.串口參數(shù)初始化*/
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = bound;//波特率
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//硬件流控制
USART_InitStruct.USART_Mode = USART_Mode_Rx|USART_Mode_Tx;//USART模式
USART_InitStruct.USART_Parity = USART_Parity_No;//校驗位
USART_InitStruct.USART_StopBits = USART_StopBits_1;//終止位
USART_InitStruct.USART_WordLength = USART_WordLength_8b;//數(shù)據(jù)長度,如果有奇偶校驗就設置為9位
USART_Init(USART1, &USART_InitStruct);
/*4.開啟中斷并初始化NVIC*/
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//開中斷,開啟接受中斷后,接收到數(shù)據(jù)則會進入中斷服務函數(shù)
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 3;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 3;
NVIC_Init(&NVIC_InitStruct);//初始化NVIC
/*5.使能串口*/
USART_Cmd(USART1, ENABLE);
/*6.編寫中斷服務函數(shù)*/
}
進行如上配置后,當STM32接收到PC發(fā)送的信息就會進入中斷服務函數(shù),中斷服務函數(shù)接收到數(shù)據(jù)后再進行發(fā)送。
中斷服務函數(shù)代碼:
void USART1_IRQHandler(void)
{
u16 RX_From_PC;
if(USART_GetITStatus(USART1,USART_IT_RXNE) != RESET)
{
USART_ClearITPendingBit(USART1,USART_IT_RXNE);//清空中斷標志位
RX_From_PC = USART_ReceiveByte(USART1);//接收PC端發(fā)來的消息
USART_SendByte(USART1,RX_From_PC);//將數(shù)據(jù)發(fā)回到PC端
//usartReceiveOneData(&TargetVelocity,&TargetVelocity,&RX_Cmd_Form_Ros);//接收ROS發(fā)來的消息
}
}
在這里我們沒有使用官方固件庫中提供的收發(fā)函數(shù) USART_ReceiveData 和 USART_SendData,而是使用了我自己對他們進行重寫的函數(shù),在其中加入了相關標志位的判斷,這樣可以保證收發(fā)過程中不會發(fā)生數(shù)據(jù)覆蓋。
重寫后的函數(shù):
void USART_SendByte(USART_TypeDef* USARTx, uint16_t Data)
{
/* Check the parameters */
assert_param(IS_USART_ALL_PERIPH(USARTx));
assert_param(IS_USART_DATA(Data));
/* Transmit Data */
USARTx- >DR = (Data & (uint16_t)0x01FF);
while(USART_GetFlagStatus(USARTx,USART_FLAG_TXE)==RESET);
}
void USART_SendString(USART_TypeDef* USARTx,char* str)
{
while((*str) != '\\0')
{
USART_SendByte(USARTx, *str);
str++;
}
while(USART_GetFlagStatus(USARTx,USART_FLAG_TC)==RESET);
}
uint16_t USART_ReceiveByte(USART_TypeDef* USARTx)
{
/* Check the parameters */
assert_param(IS_USART_ALL_PERIPH(USARTx));
while(USART_GetFlagStatus(USARTx,USART_FLAG_RXNE)==RESET);
/* Receive Data */
return (uint16_t)(USARTx- >DR & (uint16_t)0x01FF);
}
在主函數(shù)中進行相應的初始化,我們就可以通過串口助手與STM32進行通訊了。
另外,Keil5是沒有終端的,但我們可以通過一些設置也使用printf函數(shù),讓數(shù)據(jù)收發(fā)更加方便,我們需要對fputs和fgets這兩個函數(shù)進行重定向(其實只重定向fputs即可),需要在程序中加入如下代碼:
/*重定向這兩個函數(shù)*/
int fputc(int ch,FILE *f)
{
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
USART_SendData(USART1,(uint8_t) ch);
return ch;
}
int fgetc(FILE *f)
{
while(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == RESET);
return (int)USART_ReceiveData(USART1);
}
printf函數(shù)使用的是半主機工作模式,需要使用微控制器,需要進行如下配置:
這玩意功能少一些,如果不想用,則需要在程序中再加入以下代碼,這是printf函數(shù)的底層程序:
#pragma import(__use_no_semihosting)
struct __FILE
{
int handle;
};
FILE __stdout;
_sys_exit(int x)
{
x = x;
}
編譯后,我們就可以與PC端進行愉快的通訊了!
04 STM32與HC-06通訊
STM32與HC-06的通訊與PC端的通訊類似,知識接線方式的不同而已,只需要進行如下正確的接線,即可完成順利地通訊。
這里要注意的是,給HC-06供電時,需要提供3.6V-6V的電壓;另外,要注意看一下STM32的引腳輸出電壓是多少,HC-06的輸入輸出電壓都是3.3V,比如像Arduino的輸出電壓為5V,這時單片機的TX在接HC-06的RX時需要分壓!
05 STM32與ROS通訊
STM32與ROS通訊時,需要定義一個協(xié)議,以保證數(shù)據(jù)傳輸?shù)目煽啃裕@個協(xié)議是STM32與ROS通訊時最廣泛使用的協(xié)議,協(xié)議格式如下:
數(shù)據(jù)頭55aa + 控制命令 + 數(shù)據(jù)字節(jié)數(shù)size + 數(shù)據(jù) + 校驗crc8 + 數(shù)據(jù)尾0d0a
在通訊時,我們只要對接收到的信息進行解碼,即可獲得有效數(shù)據(jù)信息,比如:設定的速度值、航向角等,保證了數(shù)據(jù)收發(fā)的可靠性。
先定義相關的變量:
//數(shù)據(jù)接收暫存區(qū)
unsigned char receiveBuff[Message_Length-4] = {0};
//通信協(xié)議常量
const unsigned char header[2] = {0x55, 0xaa};
const unsigned char ender[2] = {0x0d, 0x0a};
//發(fā)送數(shù)據(jù)(左輪速、右輪速、角度)共用體(-32767 - +32768)
union sendData
{
float d;
char data[4];
}leftVelNow,rightVelNow,angleNow;
//左右輪速控制速度共用體
union receiveData
{
float d;
char data[4];
}leftVelSet,rightVelSet;
接收信息函數(shù):
int usartReceiveOneData(float *p_leftSpeedSet,float *p_rightSpeedSet,unsigned char *p_crtlFlag)
{
unsigned char USART_Receiver = 0; //接收數(shù)據(jù)
static unsigned char checkSum = 0;
static unsigned char USARTBufferIndex = 0;
static short j=0,k=0;
static unsigned char USARTReceiverFront = 0;
static unsigned char Start_Flag = START; //一幀數(shù)據(jù)傳送開始標志位
static short dataLength = 0;
USART_Receiver = USART_ReceiveByte(USART1);
//接收消息頭
if(Start_Flag == START)
{
if(USART_Receiver == 0xaa) //buf[1]
{
if(USARTReceiverFront == 0x55) //數(shù)據(jù)頭兩位 //buf[0]
{
Start_Flag = !START; //收到數(shù)據(jù)頭,開始接收數(shù)據(jù)
//printf("header ok\\n");
receiveBuff[0]=header[0]; //buf[0]
receiveBuff[1]=header[1]; //buf[1]
USARTBufferIndex = 0; //緩沖區(qū)初始化
checkSum = 0x00; //校驗和初始化
}
}
else
{
USARTReceiverFront = USART_Receiver;
}
}
else
{
switch(USARTBufferIndex)
{
case 0://接收控制指令
receiveBuff[2] = USART_Receiver;
*p_crtlFlag = receiveBuff[2]; //buf[2]
USARTBufferIndex++;
case 1://接收左右輪速度數(shù)據(jù)的長度
receiveBuff[3] = USART_Receiver;
dataLength = receiveBuff[3]; //buf[3]
USARTBufferIndex++;
break;
case 2://接收所有數(shù)據(jù),并賦值處理
receiveBuff[j + 4] = USART_Receiver; //buf[4] - buf[11]
j++;
if(j >= dataLength)
{
j = 0;
USARTBufferIndex++;
}
break;
case 3://接收校驗值信息
receiveBuff[4 + dataLength] = USART_Receiver;
checkSum = getCrc8(receiveBuff, 4 + dataLength);
// 檢查信息校驗值
if (checkSum != receiveBuff[4 + dataLength]) //buf[12]
{
printf("Received data check sum error!");
return 0;
}
USARTBufferIndex++;
break;
case 4://接收信息尾
if(k==0)
{
//數(shù)據(jù)0d buf[13] 無需判斷
k++;
}
else if (k==1)
{
//數(shù)據(jù)0a buf[14] 無需判斷
//進行速度賦值操作
for(k = 0; k < 4; k++)
{
leftVelSet.data[k] = receiveBuff[k + 4]; //buf[4] - buf[7]
rightVelSet.data[k] = receiveBuff[k + 8]; //buf[8] - buf[11]
}
//速度賦值操作
sscanf(leftVelSet.data,"%f",&(*p_leftSpeedSet));
sscanf(leftVelSet.data,"%f",&(*p_rightSpeedSet));
//-----------------------------------------------------------------
//完成一個數(shù)據(jù)包的接收,相關變量清零,等待下一字節(jié)數(shù)據(jù)
USARTBufferIndex = 0;
USARTReceiverFront = 0;
Start_Flag = START;
checkSum = 0