MJKDZ PS2手柄控制OskarBot小车(三):无线串口模块接收数据并处理
【目录】
1、硬件与软件设计思路
- 1.1 硬件资源
- 1.2 STM32串口接收数据的方法
2、源代码详解
- 2.1 串口中断接收数据
- 2.2 PS2手柄处理函数
- 2.3 串口1发送数据
- 2.4 运行结果
- 2.5 主函数与中断冲突:代码优化
1、硬件与软件设计思路
1.1 硬件资源
主控:STM32F103RCT6,主频 72MHz;
串口1:通过CH340G芯片和USB口连接电脑,用于ISP方式采用FlyMCU下载程序,以及打印串口数据在电脑上显示。
串口2:连接MJKDZ的无线串口模块,接收MJKDZ PS2无线手柄传来的数据,传送给STM32芯片。
1.2 STM32串口接收数据的方法
串口通信基础知识:
【第20章 USART—串口通讯 - 野火_firege - 博客园】
参考: https://www.cnblogs.com/firege/p/9323114.html
STM32F1串口接收数据有多种方法,随着数据越来越复杂,数据越长,数据量越大,速度越快,接收函数越复杂:
(1)少量数据,直接接收
【STM32串口中断的4种接收数据的实现方式 - 小胖墩 - ****博客】
参考: https://blog.****.net/weibo1230123/article/details/80596220
【STM32串口发送数据和接收数据方式总结 - qq_35281599的博客 - ****博客】
参考: https://blog.****.net/qq_35281599/article/details/80299770#
(2)有一定格式的数据,按帧接收(选择此方式)
主要参考这篇文章【STM32串口中断接收一个完整的数据帧 - cuishumao的专栏 - ****博客】
参考: https://blog.****.net/cuishumao/article/details/43701789
【串口中怎样接收一个完整数据包的解析】
参考: https://blog.****.net/lpp0900320123/article/details/28239765
其中也有人提到了使用IDLE串口空闲中断来判断一帧读取完毕,对于不定长度的数组,可能有用。
对于我这次传输的8个字节的数组,没有效果,还不如自行判断接收完毕。
【STM32学习笔记之-串口中断接收不定数据buff - 一颗偏执的心 - ****博客】
参考: https://blog.****.net/sinat_23338865/article/details/76599239
讲得详细,但是少提了一句,初始化的时候,要开启IDLE空闲中断。
【教你使用stm32接收串口的一帧数据!】
参考: https://blog.****.net/qq_35341807/article/details/79157437
(3)消息队列 & 环形缓冲
【STM32——串口通信升级版(队列方式) - 血染风采2018 - ****博客】
已剪辑自: https://blog.****.net/wqx521/article/details/69191025
(4)DMA接收
【STM32之串口DMA接收不定长数据 - 知乎】
已剪辑自: https://zhuanlan.zhihu.com/p/50767564
2、源代码详解
2.1 串口中断接收数据
串口接收的数据格式为,数组,8个字节的8位数据,首字符0x73,2个按键值,4个摇杆值,尾字符0x5A。
中断处理减少if判断,仅校验首字符,存到第8个数就将数组位置清0,重新接收。
两级缓存,第一个数组用来接收,第二个数组给主函数处理。
u8 USART2_RX_BUF[USART2_BUF_LEN]={0};//缓存串口2接收数据,存入数组
u8 USART2_RX_BUF_BCK[USART2_BUF_LEN]={0};//缓存接收数据,用于主函数处理,传递给 psx_buf[8]
|
//接收状态 //bit0, 接收到0x73,首校验 //bit7, 接收到0x5A,尾校验 //bit1~6,接收到的有效字节数目
u8 U2RecSta = 0;//接收数据计数,接收到第几个数据了
int USART2_IRQHandler(void)
{
u8 Clear = Clear; //这种定义方法,用来消除编译器的*没有用到*提醒
u8 UartBuf = 0;//临时存储,先校验,再存入数据
static u8 U2RecCnt = 0;//接收数据计数
/*中断处理*/
if(USART_GetITStatus(USART2,USART_IT_RXNE) == SET) //中断+使能标志,用于中断函数内
{
USART_ClearFlag(USART2, USART_FLAG_RXNE); //接收数据寄存器非空标志位
USART_ClearITPendingBit(USART2, USART_IT_RXNE);//清中断接收标志
UartBuf = USART_ReceiveData(USART2);//读串口2接收缓存,临时存储
USART2_RX_BUF[U2RecCnt] = UartBuf;//所有数据均接收
if(U2RecCnt==0)//帧头0x73,只校验首字符
{
U2RecCnt = (0x73!=UartBuf)?0:U2RecCnt+1;//若为真(不等),则U2RecCnt =0;若相等,则计数+1
}
else if(U2RecCnt > 0)//值value,不是首字符,当数据处理
{
U2RecCnt++;
if(U2RecCnt == 8)//计数到第8个数,清零
{
U2RecCnt=0;
//uart1_send_nbyte(USART2_RX_BUF,8);//
memcpy(USART2_RX_BUF_BCK,USART2_RX_BUF,8);//二级缓存,第二个数组用于给主函数处理
U2RecSta=1;//标记状态,一帧数据接收完,主循环处理
}
}
}
return 0;
}
|
没什么用的溢出中断,空闲中断,以及各种清除异常中断
//清除各种异常中断
if(USART_GetFlagStatus(USART2, USART_FLAG_PE) == SET)//奇偶错误标志位
{
Clear = USART_ReceiveData(USART2); //接收到数据不处理,抛弃
USART_ClearFlag(USART2, USART_FLAG_PE);
}
if(USART_GetFlagStatus(USART2, USART_FLAG_ORE) == SET)//溢出错误标志位
{
Clear = USART_ReceiveData(USART2); //接收到数据不处理,抛弃
USART_ClearFlag(USART2, USART_FLAG_ORE);
}
if(USART_GetFlagStatus(USART2, USART_FLAG_NE) == SET)//噪声错误标志
{
Clear = USART_ReceiveData(USART2); //接收到数据不处理,抛弃
USART_ClearFlag(USART2, USART_FLAG_NE);
}
if(USART_GetFlagStatus(USART2, USART_FLAG_FE) == SET)//帧错误标志位
{
Clear = USART_ReceiveData(USART2);
}
if(USART_GetITStatus(USART2,USART_IT_IDLE) == SET)//空闲中线标志位,一帧数据
{
Clear = USART2->SR; //读SR寄存器
Clear = USART2->DR; //读DR寄存器(先读SR再读DR,就是为了清除IDLE中断)
}
|
2.2 PS2手柄处理函数
校验一帧接收完毕(数组长度8个字节),赋值给psx_buf[8].
u8 psx_buf[8]={0}; //存储手柄按键信息
//接收手柄按键数据 void handle_button(void)
{
if(U2RecSta ==1)//帧接收完毕标志
{
U2RecSta = 0;//接收状态计数,清零
//uart1_send_nbyte(USART2_RX_BUF_BCK,8);//8个字节数据
if(USART2_RX_BUF_BCK[0] == 0x73 && USART2_RX_BUF_BCK[7] == 0x5A)//首尾字符校验
{
memcpy(psx_buf,USART2_RX_BUF_BCK,8*sizeof(u8));//校验完成,赋值给数组psx_buf[8]
//Test,8位数据接收OK,LED点亮一次
LED_ON;
delay_ms(90);
LED_OFF;
}
}
static unsigned char psx_button_bak[2] = {0};
if((psx_button_bak[0] == psx_buf[1]) && (psx_button_bak[1] == psx_buf[2]))
{
}
else if((psx_buf[0] == 0)&& (psx_buf[1] == 0))//20ms自动查询一次,handle_ps2()函数,更新psx_buf[]数组为0
{
//uart1_send_nbyte(psx_buf,8);//串口1发送 按键数据(全0)
//uart1_send_str(psx_buf);
parse_psx_buf(psx_buf+1, psx_buf[0]);//按键的首地址psx_buf+1,按键模式类型psx_buf[0](数组顺序与PSX手柄不同)
psx_button_bak[0] = psx_buf[1];
psx_button_bak[1] = psx_buf[2];
}
else if((psx_buf[0] == 0x73)&& (psx_buf[7] == 0x5A))//串口2中断,无线串口模块,更新psx_buf[]数组为按键与摇杆数据
{
uart1_send_nbyte(psx_buf,8);//串口1发送,发送缓存数组,PSX按键值
//Test,按键响应OK
if(psx_buf[1] == 0x7F && psx_buf[2] == 0xFF)//第一组按键,左键按下,蜂鸣器响一声
{
BEEP_ON;
delay_ms(90);
BEEP_OFF;
}
else if(psx_buf[1] == 0xFF && psx_buf[2] == 0x7F)//第二组按键,方形键按下,蜂鸣器响两声
{
//Test
BEEP_ON;
delay_ms(90);
BEEP_OFF;
delay_ms(90);
BEEP_ON;
delay_ms(90);
BEEP_OFF;
}
parse_psx_buf(psx_buf+1, psx_buf[0]);
psx_button_bak[0] = psx_buf[1];
psx_button_bak[1] = psx_buf[2];
}
if(psx_buf[0] == PS2_LED_GRN)
{ // joy_left_pwm = 0; // joy_right_pwm = 0;
}
else if(psx_buf[1]
|
2.3 串口1发送数据
void tb_usart1_send_nbyte(u8 *Data, u16 size)
{
u16 i = 0;
//发送首个数据之前,先读取一下USART_SR,那么就不会出现首个数据丢失
for(i=0; i<size; i++)
{
//发送一串数据的逻辑必须按照先检测TC,再发送字符的顺序进行
//监测发送状态位用TC而尽量不要使用TXE,TC是监测的是否数据发送完,而TXE只是监测缓存区是否移位到移位寄存器而已
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
USART_SendData(USART1, Data[i]);
}
//提升代码健壮性,避免其他部分代码出现类似不检测发送的问题 while(USART_GetFlagStatus(USART1,USART_FLAG_TC) == RESET) ;
return;
}
|
2.4 运行结果
备注:(1)PS2处理函数中有些会调用串口1发送字符 uart1_send_str();
(2)优化:主函数与终端

2.5 主函数与中断冲突:代码优化
就放上来的这段代码,有个大问题。当主函数在处理数据时,中断可能还在发生。所以你处理的数据,可能一半是上一次发的,一半是这一次发的。
这种问题有两个方案:
1、做个全局互锁开关,通讯完毕了打开开关,只要开关开着,就不能再接收数据。主函数判断开关,如果开着就处理数据,处理完了就关掉开关。
2、做两个缓存数组,一个用于实时接收,接收完毕了用memcpy函数拷贝到另一个数组。主函数永远只能处理另一个数组的数据。如果缓存太大怕memcpy函数执行太久影响中断,就做指针切换也行。接收完了指针就切换过去,主函数永远通过这个指针去取数组。
补充说明一下,第二个方案只适合于主函数运行足够快,能够实时处理完每一帧数据的情况,即,在处理数据时,那边又开始接受数据,这没问题,但是在接收完成数据开始memcpy时,就得考虑主函数是否已经处理完了。如果这个无法保证,就必定会有冲突,只能使用第一个方案,放弃某些帧。如果主函数轮询太慢,可以考虑设置软中断,接收完一帧后触发软中断去处理数据,这样最节约时间。
|
来自 <http://www.openedv.com/thread-103476-1-1.html>