utCOM
——DIY的一套简通信协议
系统之间的协调运行与通信的可靠性密不可分,现在有很多开源的通信协议源代码提供下载,比如FreeModBus,ucModBus等,这些协议的移植性非常好,但我可能更喜欢归根结底的去了解一些原理,因此自己用STM32编写了一个简单的通信协议,协议具有很好的可塑性和移植性,现将从头至尾讲解源代码,当然鄙人能力有限,必然会有错误和不合理的地方,请大家指正。
一切问题都是来源于实践,正在做的一套系统需要实现主板和控制板之间的互相通信,主板不仅需要读取控制板当前的各种参数,还要让控制板实现不同的功能,因此必须实现一套可靠的通信。
刚开始我考虑了以下几个问题:
1、 多个数据包应该怎么识别,当然我可以仿照一些通信方式在帧头加入数据位长度,但是这样就会降低STM32的效率,偶然看到ModBus协议的帧间隔概念,再加上STM32本身具有的串口接收空闲中断功能,这样就可以在发送端设置发送帧的时间间隔,用STM32的空闲中断就可以接收不定长的数据包了。
2、 怎样提高CPU的使用效率,STM32提供了DMA机制,因此设定好DMA配置后,接收过程就不需要CPU的参与,大幅度提高的CPU的效率。
3、 怎样设定数据位的意义,从简考虑,最后设定如下:
Buffer[0] "地址字节"
Buffer[1] "功能字节"
Buffer[2] "具体控制内容"
Buffer[3] "校验位"
Buffer[4] "结束字节"
地址字节指定了需要和谁通信,功能字节表明需要处理的内容,具体控制内容是用户自己设定的,校验位是为了提高通信的可靠性,我做了最简单的求和校验,结束字节标明一帧数据的结束。
一、UART+DMA不定长数据接收
STM32的USART具有空闲总线中断接收的功能,这种方法在网上比较多见,另外,使用DMA发送和接收数据替代使用查询法发送,其速度快了很多,尤其是在数据传输与发送的时候其优势更加明显。
首先是USART1的串口GPIO初始化
/* USART1 GPIO Init */
void USART_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 打开串口时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
/* 打开端口时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
/* TXD */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* RXD */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
USART DMA初始化
/* 串口DMA 配置
*UART配置成总线空闲中断
*当总线由忙到空闲时会产生一个总线空闲中断
*/
void USART_DMAInit(void)
{
DMA_InitTypeDef DMA_InitStructure;
#if 1
NVIC_InitTypeDef NVIC_InitStructure;
#endif
/* 启动DMA 时钟 */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
#if 1
/* 串口发DMA 配置 */
DMA_DeInit(DMA1_Channel4);
DMA_InitStructure.DMA_PeripheralBaseAddr = (INT32U)(&USART1->DR);
DMA_InitStructure.DMA_MemoryBaseAddr = (INT32U)Uart_Tx;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = 0;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel4,&DMA_InitStructure);
/* 使能中断 */
DMA_ITConfig(DMA1_Channel4,DMA_IT_TC,ENABLE);
DMA_Cmd(DMA1_Channel4, DISABLE);
#endif
/* 串口收DMA 配置*/
DMA_DeInit(DMA1_Channel5);
/* UART->DR = 0x40013804 */
DMA_InitStructure.DMA_PeripheralBaseAddr = (INT32U)(&USART1->DR);
/* 接收缓存地址 */
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Uart_Rx;
/* 单向传输 */
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
/* 接收缓存最大长度,数据接收不能超过最大长度 */
DMA_InitStructure.DMA_BufferSize = UART_RX_LEN;
/* 只有一个外设 地址不用递增 */
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
/* 内存地址递增 */
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
/* 外设数据字长 */
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
/* 内存数据字长 */
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
/* 设置DMA 的传输模式 */
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
/* 设置DMA 的优先级别 */
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
/* 设置DMA 的2个memory 中的变量的相互访问 */
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel5, &DMA_InitStructure);
/* 关闭DMA1_Channel中断
*这里不使用DMA 中断
*只使用UART 总线空闲中断
*/
DMA_ITConfig(DMA1_Channel5, DMA_IT_TC, DISABLE);
DMA_ITConfig(DMA1_Channel5, DMA_IT_TE, DISABLE);
/* 采用DMA 方式接受数据 */
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
/* 使能通道3 */
DMA_Cmd(DMA1_Channel5, ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
配置USART
/* UART1 初始化
*/
void USARTInit()
{
NVIC_InitTypeDef NVIC_InitStructure;
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate=BaudRate;
USART_InitStructure.USART_WordLength=USART_WordLength_8b;
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);
/* 只启用USART 空闲中断 */
USART_ITConfig(USART1, USART_IT_TC, DISABLE);
USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
/* 需清除发送发送完成标志位,否则第一次发送回出错 */
USART_ClearFlag(USART1, USART_FLAG_TC);
/* 配置UART中断*/
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* 启动串口 */
USART_Cmd(USART1,ENABLE);
}
重定向:*为了能够使用printf打印串口进行调试
/* 串口发送重定向 */
int fputc(int ch,FILE *f)
{
USART_SendData(USART1,(u8)ch);
/* 注意此处的USART_FLAG_TXE标志位 */
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
return ch;
}
USART的空闲中断处理
/* USART3 IDLE Interruption */
void USART1_IRQHandler(void)
{
#if 1
INT16U DATA;
INT16U i = 0;
#endif
/* Determine whether there is an interrupt */
if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)
{
#if 1
/* Close DMA prevent interruption again */
DMA_Cmd(DMA1_Channel5, DISABLE);
/* Get the Length of the received data */
UART_RxDataLen = UART_RX_LEN - DMA_GetCurrDataCounter(DMA1_Channel5);
/* Receive complete flag */
UART_RecOver = 1;
/* Reload DMA Counter */
DMA1_Channel5->CNDTR = UART_RX_LEN;
/* Re-enable DMA */
DMA_Cmd(DMA1_Channel5, ENABLE);
/* Clear Idle
*A read operation to SR and DR register can clear PE,RXNE,ORE
*,IDLE,NE,FE,PE etc.
*/
i = USART1->SR;
i = USART1->DR;
/* This function is excess, but is more straightforward */
#endif
USART_ClearITPendingBit(USART1, USART_IT_IDLE);
}
}
进入空闲中断后,说明接收到了一帧的数据,先失能DMA接收,获取接收数据的长度UART_RxDataLen,设定接收完成标志位(**任何复杂的操作都不能在中断中完成!),然后重新设置DMA接收缓存大小,并启动DMA接收,i = USART1->SR; i = USART1->DR;这两句话十分重要,可以清楚USART的所有标志位。
USART DMA发送完成中断
void DMA1_Channel4_IRQHandler(void)
{
UART_SendOver = 1;
DMA_ClearITPendingBit(DMA1_IT_TC4);
DMA_ClearITPendingBit(DMA1_IT_GL4);
DMA_Cmd(DMA1_Channel4, DISABLE);
}
当DMA发送完一帧数据后会进入DMA通道四中断,置位发送完成标志位UART_SendOver,并清楚中断标志位。
在主函数初始化中使用
/* 用于系统的串口初始化 */
void UsartInit(void)
{
USART_GPIO_Config();
USART_DMAInit();
USARTInit();
}
以上步骤,完成了USART的底层配置,已经可以顺利接受和发送数据了。
二、帧解码
系统使用时间片,每1ms检测一次UART_RecOver标志位,如果为了则说明有数据接收,并开始解码。
/* 接收到了数据 */
if(UART_RecOver)
RxDataProc();
接下来就是具体的解码函数了:
void RxDataProc(void)
{
INT8U i;
/* 清除接收完成标志 */
UART_RecOver = 0;
/* 备份到缓冲区,为下次读取做准备*/
if(UART_RxDataLen)
{
/* 从Uart_Rx备份到Uart_RxBuffer */
memcpy(Uart_RxBuffer,Uart_Rx,UART_RxDataLen);
}
/* 数据太长不做处理 */
if(UART_RxDataLen>155)
{
printf("Uart_RxDataLen is too long\n");
return;
}
/* 单个数据 */
if(UART_RxDataLen == 1)
{
printf("\nSingle cmd %x\n", Uart_RxBuffer[0]);
return;
}
/* 此处数据肯定大于1 且小于155 */
if(UART_RxDataLen)
{
/* 如果结束字符不是0xFF
则为错误信息输出后直接返回
*/
if(Uart_RxBuffer[UART_RxDataLen-1] != 0xff)
{
printf("\r\nThis is not a right command\r\n");
for(i=0; i<UART_RxDataLen; i++)
{
printf("%x ",Uart_RxBuffer);
}
printf("\n");
return;
}
/* 此处数据已经正确输出后开始解码 */
#if 1
else
{
printf("\r\nThis is a right command\r\n");
for(i=0; i<UART_RxDataLen; i++)
{
printf("%x ",Uart_RxBuffer);
}
printf("\n");
UART_RxDataLen --;
ParseData();
}
#endif
}
}
首先必须清楚接收完成标志位,以便下一次接收处理。然后需要把数据备份到缓冲器,接下来的处理都是针对缓冲区的,这样就可以腾出接收缓冲,让其接收下一帧数据。
/* 数据太长不做处理 */
if(UART_RxDataLen>155)
{
printf("Uart_RxDataLen is too long\n");
return;
}
这里判断接收数据的长度,可以自己设定,当前长度大于155个字节时打印数据过长信息,并直接退出。
/* 单个数据 */
if(UART_RxDataLen == 1)
{
printf("\nSingle cmd %x\n", Uart_RxBuffer[0]);
return;
}
如果接收到的是单个数据,打印信息后直接退出。
/* 此处数据肯定大于1 且小于155 */
if(UART_RxDataLen)
{
/* 如果结束字符不是0xFF
则为错误信息输出后直接返回
*/
if(Uart_RxBuffer[UART_RxDataLen-1] != 0xff)
{
printf("\r\nThis is not a right command\r\n");
for(i=0; i<UART_RxDataLen; i++)
{
printf("%x ",Uart_RxBuffer);
}
printf("\n");
return;
}
/* 此处数据已经正确输出后开始解码 */
#if 1
else
{
printf("\r\nThis is a right command\r\n");
for(i=0; i<UART_RxDataLen; i++)
{
printf("%x ",Uart_RxBuffer);
}
printf("\n");
UART_RxDataLen --;
ParseData();
}
#endif
}
}
能够处理此段函数则说明数据长度大于1且小于155,符合当前要求。首先判断结束字节,我设定结束字节为0xFF,当读到的结束字节不为0xFF时,打印错误信息,同时输出一帧数据值。当结束字节为0xFF时,字节长度减1,并开始解码。
void ParseData(void)
{
INT8U Check;
/* 起始字节必须是0x5A */
if(Uart_RxBuffer[COM_START] != 0x5A)
{
printf("\r\nStart Byte is Error\r\n");
return;
}
/* 起始字节时0x5A */
if(Uart_RxBuffer[COM_START] == 0x5A)
{
/* 求和校验 */
Check = Uart_RxBuffer[COM_CMD] + Uart_RxBuffer[COM_DATA];
if(Uart_RxBuffer[COM_CHECK] != Check)
{
/* 求和校验出错直接退出 */
printf("\r\nCheck is Error\r\n");
return;
}
/* 控制字判断 */
switch(Uart_RxBuffer[COM_CMD])
{
/* 状态控制 */
case 0x00:
if(MotorIsBusy() != 0)
{
/* 给主板返回电机忙碌信号 */
printf("\r\nMotor Is Busy\r\n");
/* 直接返回 */
return;
}
printf("\r\nStATUS_CTR\r\n");
/* 具体是要控制哪个状态 */
switch(Uart_RxBuffer[COM_DATA])
{
/* 待命 */
case CMD_WaitCommand:
ToWaitState = 1;
printf("\r\nCMD_WaitCommand\r\n");
break;
/* 预备清洗 */
case CMD_PreWash:
ToPreCleanState = 1;
printf("\r\nCMD_PreWash\r\n");
break;
/* 取样吸入空气 */
case CMD_InhaleAirFor:
ToInhaleAirForState = 1;
printf("\r\nCMD_InhaleAirFor\r\n");
break;
/* 取样 */
case CMD_Sample:
ToSampleState = 1;
printf("\r\nCMD_Sample\r\n");
break;
/* 取样吸入前空气段 */
case CMD_InhaleAirBack:
ToInhaleAirBackState = 1;
printf("\r\nCMD_InhaleAirBack\r\n");
break;
/* 排出空气前段和样品前段 */
case CMD_ExpreSample:
ToExculdeAirState = 1;
printf("\r\nCMD_ExpreSample\r\n");
break;
/* 上样 */
case CMD_LoadSample:
ToLoadeState = 1;
printf("\r\nCMD_LoadSample\r\n");
break;
/* 排出样品后段及空气后段 */
case CMD_ExlateSample:
ToExculdeSampleState = 1;
printf("\r\nCMD_ExlateSample\r\n");
break;
/* 用预备液清洗 */
case CMD_BackWash:
ToClearValveState = 1;
printf("\r\nCMD_BackWash\r\n");
break;
/* 清洗进样针内外壁 */
case CMD_WashNeedle:
ToClearSyringesState = 1;
printf("\r\nCMD_WashNeedle\r\n");
break;
default:
printf("\r\nERROR_DATA\r\n");
break;
}
break;
/* 分布控制 */
case 0x01:
printf("\r\nSTEP_CTR\r\n");
break;
/* 状态读取 */
case 0x02:
printf("\r\nStATUS_READ\r\n");
break;
default:
printf("\r\nCommand Byte is Error\r\n");
break;
}
}
}
/* 起始字节必须是0x5A */
if(Uart_RxBuffer[COM_START] != 0x5A)
{
printf("\r\nStart Byte is Error\r\n");
return;
}
首先判断起始字节,即当前控制板的地址,比如当前的地址为0x5A,如果起始字节部位0x5A,则直接退出。
这里使用了枚举COM_START,枚举如下:
enum
{
COM_START = 0,
COM_CMD = 1,
COM_DATA = 2,
COM_CHECK = 3
};
如果起始字节时正确的,应首先判断校验位的正确性。
/* 求和校验 */
Check = Uart_RxBuffer[COM_CMD] + Uart_RxBuffer[COM_DATA];
if(Uart_RxBuffer[COM_CHECK] != Check)
{
/* 求和校验出错直接退出 */
printf("\r\nCheck is Error\r\n");
return;
}
然后就是控制字的判断,由于系统本身的要求,所以我使用了以下的几个命令:
enum
{
STATUS_CTR = 0x00,
STEP_CTR = 0x01,
StATUS_READ = 0x02
};
STATUS_CTR:状态控制
STEP_CTR: 分布控制
StATUS_READ:控制板状态读取
/* 控制字判断 */
switch(Uart_RxBuffer[COM_CMD])
{
/* 状态控制 */
case 0x00:
if(MotorIsBusy() != 0)
{
/* 给主板返回电机忙碌信号 */
printf("\r\nMotor Is Busy\r\n");
/* 直接返回 */
return;
}
printf("\r\nStATUS_CTR\r\n");
/* 具体是要控制哪个状态 */
switch(Uart_RxBuffer[COM_DATA])
{
/* 待命 */
case CMD_WaitCommand:
ToWaitState = 1;
printf("\r\nCMD_WaitCommand\r\n");
break;
/* 预备清洗 */
case CMD_PreWash:
ToPreCleanState = 1;
printf("\r\nCMD_PreWash\r\n");
break;
/* 取样吸入空气 */
case CMD_InhaleAirFor:
ToInhaleAirForState = 1;
printf("\r\nCMD_InhaleAirFor\r\n");
break;
/* 取样 */
case CMD_Sample:
ToSampleState = 1;
printf("\r\nCMD_Sample\r\n");
break;
/* 取样吸入前空气段 */
case CMD_InhaleAirBack:
ToInhaleAirBackState = 1;
printf("\r\nCMD_InhaleAirBack\r\n");
break;
/* 排出空气前段和样品前段 */
case CMD_ExpreSample:
ToExculdeAirState = 1;
printf("\r\nCMD_ExpreSample\r\n");
break;
/* 上样 */
case CMD_LoadSample:
ToLoadeState = 1;
printf("\r\nCMD_LoadSample\r\n");
break;
/* 排出样品后段及空气后段 */
case CMD_ExlateSample:
ToExculdeSampleState = 1;
printf("\r\nCMD_ExlateSample\r\n");
break;
/* 用预备液清洗 */
case CMD_BackWash:
ToClearValveState = 1;
printf("\r\nCMD_BackWash\r\n");
break;
/* 清洗进样针内外壁 */
case CMD_WashNeedle:
ToClearSyringesState = 1;
printf("\r\nCMD_WashNeedle\r\n");
break;
default:
printf("\r\nERROR_DATA\r\n");
break;
}
break;
/* 分布控制 */
case 0x01:
printf("\r\nSTEP_CTR\r\n");
break;
/* 状态读取 */
case 0x02:
printf("\r\nStATUS_READ\r\n");
break;
default:
printf("\r\nCommand Byte is Error\r\n");
break;
}
先判断状态控制:
如果是STATUS_CTR,则再判断COM_DATA的值,这个值具体制指定了需要控制的状态,我的控制板有以下几个状态,不同的需求就需要做不同的修改。
enum
{
CMD_WaitCommand = 0x00, //待命
CMD_PreWash = 0x01, //预备清洗
CMD_InhaleAirFor = 0x02, //取样,吸入空气
CMD_Sample = 0x03, //取样,样品后段,进样量和样品前段
CMD_InhaleAirBack = 0x04, //取样,吸入前空气段
CMD_ExpreSample = 0x05, //排出空气前段及样品前段
CMD_LoadSample = 0x06, //上样
CMD_ExlateSample = 0x07, //排出样品后段及空气后段
CMD_BackWash = 0x08, //用预备液清洗
CMD_WashNeedle = 0x09 //清洗进样针内外壁
};
这里的分布控制和状态读取中还没有添加成分,可以根据具体需要做修改。
三、数据的发送
方便使用控制板的发送接收和主板的发送接收使用相同的协议。具体的就不再展开了,如果需要源代码可以发送内容到xjcui@shqinlu.com,如有疑问和建议也可提出。
|