历史上的今天
返回首页

历史上的今天

今天是:2024年09月01日(星期日)

正在发生

2018年09月01日 | stm32使用两路串口及接收不定长数据的实现

2018-09-01 来源:eefocus

前言:前一段时间需要编写一个使用双路串口的程序采集传感器数据,由于自身能力有限所以遇到了很多坑,后来经过多方学习和调试基本完成了所需功能,现将自己的一些经(踩)验(过)方(的)法(坑),与大家分享。由于本人水平有限文章中有不足之处也欢迎大家指出改正! 

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中断函数中,分别来判断是否接收到1个字节,以及判断是否接收到1帧数据,这样不仅能接收不定长数据,同时usart3_in中也记录了RevBuff3的大小。此时再进行其他的控制就容易多了,以下是我自己控制部分的代码:


         //当串口3接收到一个完整数据帧,处理数据帧,将处理好结果发回串口1

         if (ReceiveStave == 1) {  

            ReceiveStave = 0;

            t = 0;


            //复制RevBuff3到tmp

            for (t = 0, tmpLen = 0; t < usart3_in; t++,tmpLen++) {

                tmp[tmpLen] = RevBuff3[t];

            }


            //得到处理后的数据

            p = ReadRealTimeData(tmp, tmpLen);



            //发送处理后的数据

            for (i = 0; i < strlen( p ); i++) {

                while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)!=SET);               

                USART_SendData(USART1, p[i]);

               // while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);

            }


             usart3_in = 0;

         }


3、串口发送数据的TC/TXE行为 

先来看看串口数据发送的过程是什么样的:


先写数据到DR寄存器->移位寄存器->TX管脚。当数据从DR寄存器移出到移位寄存器(即DR寄存器空)时TXE就置位,优点是能保持发送数据的连续性;而当一帧数据全部发送完成(”\0”结束符)则触发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[i]);  

  }


而TC=1代表该寄存器中的数据已全部发送完成,可以理解为每发送一次数据后,判断此次发送是否完成,代码如下:


for(t=0;t

    USART_SendData(USART1, USART_RX_BUF[t]);       

    while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);

}


4、注意硬件接线及标号是否正确!!! 

注意硬件接线及标号是否正确!!! 

注意硬件接线及标号是否正确!!! 

注意硬件接线及标号是否正确!!! 

解决了数据发送和接收的大问题,程序基本就应该可以完成了吧,想想还有点小激动呢!连接线路,RX接TX,TX接RX,一切看起来十分完美,然而出现了尴尬的情况,程序怎么也读不到数据……刚开始我觉得一定是程序写的有问题,找了好久后无果,请大神来看看,大神看完程序也说没问题,思考片刻后交换了一下接线,然后就好了!!!好了!!!(我还是太年轻,可是我也试过啊,怎么就不行呢?!被大神深深鄙视了一次…),最后发现是传感器上的标号标错了(RX,TX标反了!!!)。这大概是这个项目中最大的一个坑吧。所以再次提醒大家一定要注意硬件接线及标号是否正确!!!


总结


跌跌撞撞地总算是实现了基本的功能,由于自己也是第一次入手串口相关项目(都怪自己学的不扎实),所以遇到了许多大大小小的问题,项目至今也还有一些BUG,比如说,采集数据时还会出现丢包现象,用两种不同方式实现接收不定长数据是因为只用第一种就会出现数据丢失… 但是也算有所收获吧,写下来和大家分享交流。

推荐阅读

史海拾趣

Glenair公司的发展小趣事
如果电路的供电电源不稳定或电压不足,也可能影响电路的正常工作。
Catalyst公司的发展小趣事

Catalyst公司在电子行业的初创期,以其独特的创新理念和前瞻性的技术布局,奠定了坚实的基础。公司创始团队由一群对电子行业充满热情的工程师和科学家组成,他们共同致力于研发高效、稳定的电子产品。通过不断的技术研发和市场调研,Catalyst成功推出了一系列具有竞争力的产品,逐步在市场中崭露头角。

Hantronix公司的发展小趣事

面对电子行业的快速变化,HANBIT Electronics始终保持敏锐的市场洞察力。2025年,公司宣布与一家领先的物联网技术公司达成战略合作,共同开发智能家居解决方案。这一跨界合作不仅为HANBIT Electronics带来了新的增长点,也使其能够借助物联网技术的力量,进一步拓展在智能家居、智慧城市等新兴领域的应用。通过不断探索和尝试,HANBIT Electronics在保持传统业务优势的同时,也为公司的未来发展开辟了新的道路。

Future Designs, Inc. (FDI)公司的发展小趣事

在快速发展的同时,FDI也注重可持续发展。他们致力于环境保护和社会责任,积极采用环保材料和节能技术,减少生产过程中的资源消耗和污染排放。同时,FDI还关注员工的发展和福利,为员工提供良好的工作环境和培训机会,激发员工的创造力和创新精神。展望未来,FDI将继续秉承“创新、服务、质量、诚信”的核心理念,不断推动技术进步和产品升级,为电子行业的发展贡献更多的力量。

Eastman Kodak Company公司的发展小趣事

随着技术的不断进步,伊士曼柯达公司不断推出新的产品和服务,以满足市场的多样化需求。在20世纪初,柯达已成为全球最大的胶卷供应商,并占据了美国摄影市场的绝大部分份额。此外,公司还积极向国际市场扩张,通过设立分支机构和办事处,将产品销往全球各地。这一时期,柯达不仅凭借其优质的产品赢得了广泛的客户基础,还树立了良好的品牌形象。

G24 Innovations公司的发展小趣事

远阳(FLYOUNG)公司创立于21世纪初,正值中国电子行业快速发展的黄金时期。公司创始人李先生,凭借在电子行业多年的技术积累和敏锐的市场洞察力,决定创立一家专注于数据工程电缆研发与生产的公司。初期,远阳面临着资金短缺和技术瓶颈的双重挑战。但李先生带领团队夜以继日地研发,终于成功推出了首款高性能HDMI线,其卓越的品质迅速赢得了市场的认可。这一技术创新不仅为公司赢得了第一批忠实客户,也为远阳后续的技术发展奠定了坚实基础。

问答坊 | AI 解惑

一个简单的驱动程序与应用程序通信问题请教

com.sys的完整代码: #include #define NTSTRSAFE_LIB #include #define COM_DRIVER_NAME  L\"\\\\Device\\\\Serial0\"        //要绑定的设备名 //延时用的 #define  DELAY_ONE_MICROSECOND &nbs ...…

查看全部问答>

如何理解软件的字长?如何理解微机的字长?

如何理解软件的字长,如32位的软件或64位的软件,具体表示什么意义? 最好具体点…

查看全部问答>

求教各位大虾xilinx公司xc9572xl vq64 引脚参数

求教各位大虾xilinx公司可编程逻辑电路xc9572xl vq64 的引脚参数,有哪位高手知道的,求教,谢谢!…

查看全部问答>

LM358形成振荡器

这是我的电路图 [ 本帖最后由 zhangdaoyu 于 2011-7-20 18:41 编辑 ]…

查看全部问答>

关于DS18B20的问题

我用的是TX-1C实验板,自己写的程序为什么读取温度时是一个不正确的数,而且不会随外界温度改变????[attach]74559        这是我的程序和正确程序对比,我看不出什么大差别……[/attach] [ 本帖最后由 sphs ...…

查看全部问答>

MDK下2410简单例程

      MDK下2410简单例程,也是跑马灯的,采用查询式的定时器,外部晶振为12M,PLL后为120M; MDK版本为4。23   …

查看全部问答>

抛开电源线束缚的USB新供电规范 – USB Power Delivery

USB在一般现代人的日常生活中,可谓是最为广泛应用的传输界面之一,由于USB界面的方便易用、尺寸小且成本合理,如今在各个不同生活或工作场域都随处可见USB的踪迹。不论是家庭中的核心娱乐装置--电视、个人的笔记本电脑/智能型手机/平版装置、办 ...…

查看全部问答>

高薪诚聘:副总经理和高级电子工程师!

创荣发以设计、产品、规模、服务赢得了多家国际著名厂商的信赖,是行业中少有的通过多家国际著名厂商与品牌体系审核合格的企业,从而成为其研发与制造的战略合作伙伴,产品 70% 销往国外,服务于世界500强中70%的消费电子类企业。公司分别获得了 IS ...…

查看全部问答>

12位、300 kSPS、单电源、完全隔离式数据采集系统,用于±10 V输入

12位、300 kSPS、单电源、完全隔离式数据采集系统,用于±10 V输入 …

查看全部问答>

LM5117

请问LM5117是从哪里给PWM波的啊? …

查看全部问答>