前言:
前一段时间需要编写一个使用双路串口的程序采集传感器数据,由于自身能力有限所以遇到了很多坑,后来经过多方学习和调试基本完成了所需功能,现将自己的一些经(踩)验(过)方(的)法(坑),与大家分享。由于本人水平有限文章中有不足之处也欢迎大家指出改正!
1、串口配置
本人采用的是stm32F407的串口1和串口3(串口2因为硬件问题让我给烧坏了…尴尬, 在此也提醒大家一定要确保硬件连接无误),配置串口的过程不再赘述,直接上代码
//初始化IO 串口1
//bound:波特率
void uart_init(u32 bound){
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//使能USART1时钟
//串口1对应引脚复用映射
GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1); //GPIOA9复用为USART1
GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1); //GPIOA10复用为USART1
//USART1端口配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; //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(USART1, &USART_InitStructure); //初始化串口1
USART_Cmd(USART1, ENABLE); //使能串口1
//USART_ClearFlag(USART1, USART_FLAG_TC);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启相关中断
// USART_ITConfig(USART1, USART_IT_TC, ENABLE);
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//串口1中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority =3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器、
}
void uart3_init(u32 bound)
{
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE); //使能GPIOA时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3,ENABLE);//使能USART1时钟
//串口3对应引脚复用映射
GPIO_PinAFConfig(GPIOB,GPIO_PinSource10,GPIO_AF_USART3); //GPIOA9复用为USART1
GPIO_PinAFConfig(GPIOB,GPIO_PinSource11,GPIO_AF_USART3); //GPIOA10复用为USART1
//USART3端口配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; //GPIOB10与GPIOB11
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(GPIOB,&GPIO_InitStructure); //初始化PA9,PA10
//USART3 初始化设置
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(USART3, &USART_InitStructure); //初始化串口3
USART_Cmd(USART3, ENABLE); //使能串口3
USART_ITConfig(USART3, USART_IT_RXNE, ENABLE); //开启接收中断
USART_ITConfig(USART3, USART_IT_IDLE, ENABLE); //开启空闲中断
//Usart3 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;//串口1中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority =3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器、
}
这里需要说明的强调的是,在配置串口3的时候开启了空闲中断,这里是为了实现接收不定长数据,会在下文详细说明。
2、接收不定长数据的实现
首先说明一下个人的思路,如下图。
首先是由上位机通过串口1向stm32发送一条相关指令,stm32再将指令转发给传感器端,传感器接收到相关指令后通过串口3发送相关数据给stm32,最后再由stm32将数据进行相应解析后通过串口1回传给上位机。
但是无论是上位机指令还是传感器回传的数据,其数据长度都不是固定的,以下给出两种方法分别实现对串口1和串口3的数据接收。
2.1、使用循环队列的方法接收串口1的数据
如上图所示,RevBuff1为串口1的接收缓存区;usart1_in为队头,每次接收到一个字节的数据后usart1_in++,用来记录当前入队的位置;usart1_out为队尾,每次出队后队尾记录上次入队的位置。串口1中断函数如下:
uint32_t usart1_in = 0;
uint32_t usart1_out = 0;
u8 RevBuff1[BUFFSIZE] = {0};
u8 flag = 0;
void USART1_IRQHandler(void) //串口1中断服务程序
{
u8 res1 = 0;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)//接收到数据
{
res1 = USART_ReceiveData(USART1);
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
RevBuff1[usart1_in++] = res1;
if (usart1_in >= BUFFSIZE) {
usart1_in = 0;
}
flag = 1;
}
}
按照这个思路,要想将串口1的接收到的数据发送给串口3时,只需要判断队头和队尾的位置,然后发送队尾至队头之间的数据就可以实现不定长数据的发送了,具体代码实现如下:
//把串口1接收到的数据透传发送给串口3
if (flag == 1) {
if (usart1_out != usart1_in) { //在队头队尾不相等的情况下,即有数据存入RecBuff1
if (usart1_out < usart1_in) { //若队尾小于队头,直接发送队尾至队头之间的数据
for (t = usart1_out; t < usart1_in; t++) {
while(USART_GetFlagStatus(USART3,USART_FLAG_TXE)!=SET);
USART_SendData(USART3, RevBuff1[t]);
}
usart1_out = usart1_in; //发送完成后队尾移至当前队头的位置
flag = 0;
}
else {
//若队尾大于队头,先发送队尾至RecBuff1[BUFFSIZE]之间的数据
//然后发送RecBuff1[0]至队头之间的元素
for (t = usart1_out; t < BUFFSIZE; t++) {
while(USART_GetFlagStatus(USART3,USART_FLAG_TXE)!=SET);
USART_SendData(USART3, RevBuff1[t]);
}
for (t = 0; t < usart1_in; t++) {
while(USART_GetFlagStatus(USART3,USART_FLAG_TXE)!=SET);
USART_SendData(USART3, RevBuff1[t]);
}
usart1_out = 0; //发送完成后队尾移至RecBuff1[0]的位置
flag = 0;
}
}
}
2.2、利用串口的RXNE和IDLE中断接收串口3的数据
首先说明,IDLE中断就是串口收到一帧数据后,发生的中断;所谓一帧数据是指给单片机一次发来1个字节,或者一次发来若干个字节,这些一次发来的数据,就称为一帧数据。 而当接收到1个字节,就会产生RXNE中断。
上图为状态寄存器描述图,当串口接收到数据时,bit5就会自动变成1,当接收完一帧数据后,bit4就会变成1。
需要注意的是,在中断函数里面,需要把对应的位清0,否则会影响下一次数据的接收。比如RXNE接收数据中断,只要把接收到的一个字节读出来,就会清除这个中断。IDLE中断,如果是F0系列的单片机,需要用ICR寄存器来清除,如果是F1系列的单片机,清除方法是“先读SR寄存器,再读DR寄存器”,经测试,F4系列与F1系列的方法相同。在使用IDLE中断之前先要在串口3配置时开启相应中断(上文已经提到过),如下图:
接着来看串口3的中断程序:
void USART3_IRQHandler(void) //串口3中断服务程序
{
u8 res3 = 0;
u8 Clear = 0;
//如果接收到一个字节
if (USART_GetITStatus(USART3, USART_IT_RXNE) != RESET) {
res3 = USART_ReceiveData(USART3);
RevBuff3[usart3_in++] = res3;
if (usart3_in >= BUFFSIZE) {
usart3_in = 0;
}
flag3 = 1;
}
//如果接收到一帧数据
else if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) {
Clear = USART3->SR; //读SR寄存器
Clear = USART3->DR; //读DR寄存器(先读SR再度DR,可以清除IDLE中断)
ReceiveStave = 1; //标记接收到1帧的数据
}
}
3、串口发送数据的TC/TXE行为
先来看看串口数据发送的过程是什么样的:
先写数据到DR寄存器->移位寄存器->TX管脚。当数据从DR寄存器移出到移位寄存器(即DR寄存器空)时TXE就置位,优点是能保持发送数据的连续性;而当一帧数据全部发送完成(” ”结束符)则触发TC中断,优点是可确定发送完成的时间多用于数据的流控。
也就是说,TXE=1代表发送数据寄存器空,可以往里存放数据 ;TC=1代表该寄存器中的数据已全部发送完成。借用openedv论坛中一句形象的描述就是:
你写数据到串口时,是装入弹仓,硬件会将数据移到枪膛,这时,TXE为1,TC为0,STM32硬件的TX脚正在发送数据,但你还可以装入数据到弹仓,装入后,TXE为0,TC为0.
TX发送完一个数据后,立即将数据从弹仓移入枪膛,这时,TXE为1,TC为0.
最后TX发送完数据,你又没有装入新数据,这时。TXE为1,TC为1.
那么了解这两个有什么具体的用处呢?
常见的使用方式有以下两种,在向串口写数据时需要知道上一次数据是否发送完成,既然TXE=1代表发送数据寄存器空,可以往里存放数据,那么我们在写数据之前可以先进行TXE中断判断,代码如下:
//发送处理后的数据 for (i = 0; i < strlen( p ); i++) { while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)!=SET); USART_SendData(USART1, p); }而TC=1代表该寄存器中的数据已全部发送完成,可以理解为每发送一次数据后,判断此次发送是否完成,代码如下:
for(t=0;t
4、注意硬件接线及标号是否正确!!!
注意硬件接线及标号是否正确!!!
注意硬件接线及标号是否正确!!!
注意硬件接线及标号是否正确!!!
解决了数据发送和接收的大问题,程序基本就应该可以完成了吧,想想还有点小激动呢!连接线路,RX接TX,TX接RX,一切看起来十分完美,然而出现了尴尬的情况,程序怎么也读不到数据……刚开始我觉得一定是程序写的有问题,找了好久后无果,请大神来看看,大神看完程序也说没问题,思考片刻后交换了一下接线,然后就好了!!!好了!!!(我还是太年轻,可是我也试过啊,怎么就不行呢?!被大神深深鄙视了一次…),最后发现是传感器上的标号标错了(RX,TX标反了!!!)。这大概是这个项目中最大的一个坑吧。所以再次提醒大家一定要注意硬件接线及标号是否正确!!!
总结
跌跌撞撞地总算是实现了基本的功能,由于自己也是第一次入手串口相关项目(都怪自己学的不扎实),所以遇到了许多大大小小的问题,项目至今也还有一些BUG,比如说,采集数据时还会出现丢包现象,用两种不同方式实现接收不定长数据是因为只用第一种就会出现数据丢失… 但是也算有所收获吧,写下来和大家分享交流。