一、硬软件平台
本次程序实现效果为对GPS信号穿送来的数据进行筛选,并将筛选后的信息通过上位机显示出来,所以此次设计所需硬件包括STM32F407、RS232转TTL、CH340USB转串口模块,注意该模块在使用前,对应的系统需要安装驱动,否则串口调试助手无法识别,另外还包括JLink下载器。
本次代码设计软件为KEIL5并结合F4固件包,上位机系统为WIN7,主机系统为WIN10。
二、算法总体思路设计
由于GPS通过RS232将数据传送给板子,因此使用两个串口资源(串口1和串口2),其中一个用来接收数据,另外一个用来将筛选后的数据发送给上位机。GPS在发送信号时,相邻两次数据的发送之间有明显的时间间隔,所以该间隔可作为判断当前接收的数据是否是一个完整数据的依据。为了保证接收数据的及时性,接收数据时采用串口接收中断,发送数据则作为主程序。因此可得到以下主程序流程图。
三、具体实现步骤
3.1 串口1初始化
本次程序设计通过串口1将筛选后的数据发送给上位机,对应的硬件资源为PA9(USART1-TX)、PA10(USART1-RX)。通过将这两个IO口连接CH340,实现数据传送。串口1波特率设置为38400。
3.2 串口2初始化
串口2用来接收GPS信号,对应硬件资源为PA2(UASRT2-TX)、PA3(USART2-RX),GPS设备接口为RS232,接入单片机时,需要转为TTL电平,注意,在连接板子和RS232转TTL模块的TTL输出端时,RX和TX用反接,否则会接收不到数据。由于GPS信号发送波特率为固定的38400,所以初始化时串口2的波特率也要设置为38400。
串口2使用中断来接收数据,因为初始化时也要配置中断。具体代码如下:
void uart2_init(u32 bound){
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIOA时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);//使能USART1时钟
//串口1对应引脚复用映射
GPIO_PinAFConfig(GPIOA,GPIO_PinSource2,GPIO_AF_USART2); //GPIOA2复用为USART2
GPIO_PinAFConfig(GPIOA,GPIO_PinSource3,GPIO_AF_USART2); //GPIOA3复用为USART2
//USART1端口配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3; //GPIOA9与GPIOA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化PA9,PA10
//USART1 初始化设置
USART_InitStructure.USART_BaudRate = bound;//波特率设置
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(USART2, &USART_InitStructure); //初始化串口2
USART_Cmd(USART2, ENABLE); //使能串口2
USART_ClearFlag(USART2, USART_FLAG_TC);
USART_ITConfig(USART2, USART_IT_RXNE,ENABLE);//开启相关中断
//Usart2 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;//串口2中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;//抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority =3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
TIM7_Int_Init(99,7199);
USART2_RX_STA=0;
TIM_Cmd(TIM7,DISABLE); //关闭定时器7
}
3.3 串口2接收中断函数
该中断函数是整个程序的重点所在,主要目的是将接收的字符存入数组内,这里数组名及大小定义为USART2_RX_BUF[NMEA_COUNT_MAX];其中将NMEA_COUNT_MAX设置为600,代表数组最大容量。当有数据发过来时,存入数组,并对该数组进行处理,若没有处理完毕,则不再接收其他数据,这里定义数据接收状态变量vu16 USART2_RX_STA。另外借助10ms定时器(TIM7)中断判断是不是一次连续的数据,如果接收连续2个字符之间的时间差不大于10ms则认为是,如果大于10ms则中断触发,强制标记数据接收完成。
void USART2_IRQHandler(void) //串口2中断服务程序
{
GPIO_ResetBits(GPIOA,GPIO_Pin_6); //LED0灯亮
char Buffer;
if(SET==USART_GetITStatus(USART2,USART_IT_RXNE))
{
USART_ClearFlag(USART2, USART_FLAG_RXNE);
Buffer = USART_ReceiveData(USART2);//接收数据
if((USART2_RX_STA&(1<<15))==0)//接收完的一批数据,还没有被处理,则不再接收其他数据
{
if(USART2_RX_STA<NMEA_COUNT_MAX)//还可以接收数据
{
TIM_SetCounter(TIM7,0);//计数器清空
if(USART2_RX_STA==0)
{
TIM_Cmd(TIM7,ENABLE);//使能定时器7
}
USART2_RX_BUF[USART2_RX_STA++]=Buffer;//记录接收到的值
}
else
{
USART2_RX_STA|=1<<15;//强制标记接收完成
}
}
}
GPIO_SetBits(GPIOA,GPIO_Pin_6);//LED0灯灭
}
/*定时器7中断服务函数*/
void TIM7_IRQHandler(void)
{
if (TIM_GetITStatus(TIM7, TIM_IT_Update) != RESET)//是更新中断
{
USART2_RX_STA|=1<<15; //标记接收完成
TIM_ClearITPendingBit(TIM7, TIM_IT_Update ); //清除TIM7更新中断标志
TIM_Cmd(TIM7, DISABLE); //关闭TIM7
}
}
3.4数据解析
每次接收的数据都存放在数组里面,每个信息都有属于自己的标识符,例如$GNRMC、!AIVDM等,所以要找到目标信息的位置,直接对数组进行字符串搜索即可。搜索算法如下图所示,返回字符串首字符在数组中的位置。
/*查找字符串*/
u16 FindStr(char *str,char *ptr)
{
u16 index=0;
char *STemp=NULL;
char *PTemp=NULL;
char *MTemp=NULL;
if(0==str||0==ptr)
return 0;
for(STemp=str;*STemp!='\0';STemp++) //依次查找字符串
{
index++; //当前偏移量加1
MTemp=STemp; //指向当前字符串
//比较
for(PTemp=ptr;*PTemp!='\0';PTemp++)
{
if(*PTemp!=*MTemp)
break;
MTemp++;
}
if(*PTemp=='\0') //出现了所要查找的字符,退出
break;
}
return index;
}
得到目标信息的位置后,就可以提取信息中的数据了,NMEA数据的特点是信息中的数据之间都是用逗号隔开,所以逗号的数量就代表了该条信息含有多少个数据,通过数逗号的方法就可以得到每个数据。
以提取!AIVDM信息为例,其他信息提取方法相同。代码中通过宏定义的方式来选择解析和发送哪条信息。
/*定义AIVDM数据结构体*/
typedef struct AIVDM_data
{
char first[2];
char second[2];
char third[2];
char four[5];
char five[50];
char six[12];
}AIVDM_data;
/*数据解析*/
#if PRINT_AIVDM //发送数据开关
u8 CommaNum_AIVDM=0;//逗号数量
u8 BufIndex_AIVDM=0;
char Sbuf_AIVDM;
char *Pstr_AIVDM;
u16 index_AIVDM=0;
memset(&AIVDM_data_ais,0x00,sizeof(AIVDM_data_ais));
index_AIVDM=FindStr(GPS_REC_data,"!AIVDM");
if(index_AIVDM)
{
CommaNum_AIVDM=0;
Pstr_AIVDM=GPS_REC_data+index_AIVDM+6;
do
{
Sbuf_AIVDM=*Pstr_AIVDM++ ;
switch(Sbuf_AIVDM)
{
case ',':CommaNum_AIVDM++;
BufIndex_AIVDM=0;
break;
default:
switch(CommaNum_AIVDM)
{
case 0:AIVDM_data_ais.first[BufIndex_AIVDM]=Sbuf_AIVDM;break;//逗号数量为0表示第一个数据
case 1:AIVDM_data_ais.second[BufIndex_AIVDM]=Sbuf_AIVDM;break;//逗号数量为1表示第二个数据
case 2:AIVDM_data_ais.third[BufIndex_AIVDM]=Sbuf_AIVDM;break;//逗号数量为2表示第三个数据
case 3:AIVDM_data_ais.four[BufIndex_AIVDM]=Sbuf_AIVDM;break;//逗号数量为3表示第四个数据
case 4:AIVDM_data_ais.five[BufIndex_AIVDM]=Sbuf_AIVDM;break;//逗号数量为4表示第五个数据
case 5:AIVDM_data_ais.six[BufIndex_AIVDM]=Sbuf_AIVDM;break;//逗号数量为5表示第六个数据
default:break;
}
BufIndex_AIVDM++;
break;
}
}while(Sbuf_AIVDM!='\r');//每条信息以'\r'字符结尾
}
//memset(GPS_Buffer,0,sizeof(GPS_Buffer));
#endif
3.5 信息发送
信息发送就用普通的printf函数,不过要使用串口1函数,所以要重写一个fputs()函数,并在头文件中stdlib.h头文件。在发送之前,需要对数据进行一些简单的过滤,比较最后一个数据是信息的校验码,校验码的第二位一定是符号*等等,如果不符合过滤的条件就不发送,保证数据的正确性。
/*数据过滤及打印*/
#if PRINT_AIVDM
if(AIVDM_data_ais.six[1]=='*'&&AIVDM_data_ais.five[0]!='0'&&AIVDM_data_ais.five[1]!='0'&&AIVDM_data_ais.five[2]!='0'&&AIVDM_data_ais.five[0]!='8'&&AIVDM_data_ais.five[1]!='8'&&AIVDM_data_ais.five[2]!='8')
{
printf("!AIVDM");
printf(",");
printf("%s",AIVDM_data_ais.first);
printf(",");
printf("%s",AIVDM_data_ais.second);
printf(",");
printf("%s",AIVDM_data_ais.third);
printf(",");
printf("%s",AIVDM_data_ais.four);
printf(",");
printf("%s",AIVDM_data_ais.five);
printf(",");
printf("%s\n",AIVDM_data_ais.six);
}
#endif
3.6 主函数
各个模块的功能已经实现了,接下来就可以根据图2.1编写主函数的程序,如下图所示。
/*主函数大循环*/
while(1)
{
delay_ms(1);
if(USART2_RX_STA&0X8000)//接收到一次数据了
{
rxlen=USART2_RX_STA&0X7FFF;//得到数据长度
for(i=0;i<rxlen;i++)
GPS_REC_data[i]=USART2_RX_BUF[i];//将缓冲数据写入数组中
USART2_RX_STA=0;//启动下一次接收
GPSParse();//解析字符串
send_data();//发送字符串
}
}
四、遇到的问题及解决方案
4.1 数据打印前一半打印正常,后一半没有数据或者是不正确的数据
原因:在前一次数据没有解析发送完,就来了第二次数据,由于接收是采用中断方式,所以会暂停解析发送转而去接收数据,这就导致了上一次的数据被新的数据冲刷掉了,冲刷后的结果无法预测,可能没有了,可能是别的。
解决方法:参考正点原子代码。定义接收状态标志变量USART3_RX_STA,将该变量看作一个16位的寄存器,其中0-14位代表串口接收数据的长度,第15位为1时代表不接收当前数据,为0代表接受当前数据,只有当定时器中断触发时即GPS一次连续的数据已经发送完毕时,手动给该位置1,此时不再接收下一次数据,当当前数据取出后即写给另外一个数组时该位再重新置1。具体代码见图3.2和3.7。
4.2 GPS接收没数据
原因:RS232转TTL的TTL输出端与PA2,PA3反接了。
解决方法:不用反接,RX接RX,TX接TX。
五、结果
六、工程下载连接
https://download.csdn.net/download/weixin_39954922/13218297
- 七、参考文献
- http://training.eeworld.com.cn/video/18411
- https://blog.csdn.net/jickjiang/article/details/79086202
- https://blog.csdn.net/qq_33559992/article/details/52051689
- 感谢正点原子例程~