stm32f103——串口UART

在学习UART之前,我们先来了解一下单片机与外围设备之间的通信:

        单片机与外围设备之间的信息交换和传输我们称为通信。过去通信方式有两种:并行通信串行通信

并行通信:
定义:并行通信是指利用多条传输线将一个数据的各位同时传送。
传输方式:传输一个字节(8个位)的数据时,并口是将8个位一字排开,分别在8条连接线上同时传输
特点:传输速度块,适用于短距离通信。

缺点:虽然,并行通信传输速度快,但是由于,线与线之间存在电磁干扰,会导致数据错误。而且由于线比较多,PCB布线比较麻烦,所以并行通信不常用,而串行通信用得比较广泛。

stm32f103——串口UART_第1张图片

        

串行通信:
定义:串行通信是指利用一条传输线将数据一位位地顺序传送
传输方式:传输一个字节(8个位)的数据时,串口是将8个位排好队,逐个地在1条连接线上传输。
特点:通信线路简单,适用于远距离通信和上、下机通信。

缺点:传输数度比较慢。(因为它是一位一位的传输的)

stm32f103——串口UART_第2张图片

 串行通信根据传输数据方式来划分的话:有同步方式异步方式:

同步方式:即发送方和接收方的时钟频率要求一直,所以双方就需要在同步时钟线的牵引下,收、发数据需要同时进行。

stm32f103——串口UART_第3张图片

异步方式:双方在相同的波特率的情况下,发送器只管发送,接收器只管接收,收发互不干涉。

stm32f103——串口UART_第4张图片

串行通信根据收发数据方式来划分的话:有单工半双工、全双工:

单工通信:数据只能单方向传输。

stm32f103——串口UART_第5张图片

半双工通信:数据传输上支持双方向传输,但是不能同时进行双向传输,在同一时刻,某一端只能进行发送或者接收。

stm32f103——串口UART_第6张图片

全双工通信:双工是指数据同时在两个方向上传输,是两个单工通信的结合,要求发送设备和接收设备同时具有独立的接收和发送能力。

stm32f103——串口UART_第7张图片

 

波特率:

 stm32f103——串口UART_第8张图片

         所以,双方需要约定好波特率。

我们现在来学习UART 

          UART,是通用异步收发传输器(USART则是通用同步/异步收发传输器),既然是“器”,显然,它就是个设备而已,要完成一个特定的功能的硬件,它本身并不是协议。那么它要完成什么功能呢?它的最基本功能,是串行数据和并行数据之间的转换。注意:UART是TTL电平。注意:串口通信是一对一的通信,而不是像I^2C那样一对多的通信。

       注意:UART、RS232、RS485,它们实际上都是串口上面的属性,只不过它们的功能不一样。UART的功能是将串行转并行,而RS232、RS485则是一种电平标准。

双工:

单工:
stm32f103——串口UART_第9张图片

       

USART与UART的区别:
        1,USART(同步,异步可选择)(全双工,半双工可选择)
        2,UART(异步)(全双工,半双工可选择)

串口通信的作用:
        1,用于设备间的通信
        2,可以用于设备调试----printf的打印

CPU片上外设,UART模块分析:

stm32f103——串口UART_第10张图片

        发送数据寄存器:它接收CPU从数据总线上送来的并行数据。并加以保存。
        发送移位寄存器:它接收从发送数据寄存器送来的并行数据,并给数据加上起始位和停止位(即:帧头和帧尾),然后以发送时钟的速率把数据逐位移出,即将并行数据转换为串行数据输出。

        接收数据寄存器:它从输入移位寄存器中接收并行数据,由CPU(或者DMA)取走。
        接收移位寄存器:它以接收时钟的速率把出现在串行数据输入线上的数据逐位移入,当数据装满后,并行送往接收数据寄存器,即将串行数据转换成并行数据,然后去掉起始位和停止位,然后将数据传给接收数据寄存器

UART在发送数据时,是低位先行(即:低位先发送)还是高位先行?接收数据呢?

stm32f103——串口UART_第11张图片

         由图知:红线部分是从移位寄存器的尾部出发的,而移位寄存器的尾部是数据的低位,所以发送数据的时候是低位先行(即:先发送低位,再发送高位)

        stm32f103——串口UART_第12张图片

         接收数据同理:红线部分是接移位寄存器的头部,先将低位接收,然后低位在移位寄存器中逐步往后移动,所以接收数据的时候也是低位先行(即:先接收低位,再接收高位)

UART内部总结构:

stm32f103——串口UART_第13张图片

UART,一帧数据的格式:

stm32f103——串口UART_第14张图片

串口数据包:11bit
        1. 起始位:低电平 1bit
        2. 数据位:8个位,低位先行  8bit
        3. 奇偶校验位:1个bit
            奇校验:指的是数据位中1的个数。如果数据位中1的个数位偶数,那么该位为1。如果数据位中1的个数位奇数,那么该位为0。
            偶校验:指的是数据位中1的个数,如果数据位中1的个数位偶数,那么该位为0。如果数据位中1的个数位奇数,那么该位为1。
        4. 停止位:高电平  1bit
        
        如果串口没有奇偶校验位:一共10个bit(1+8+1 = 10)

包含奇偶效验位:数据长度为9位(不包含起始位和停止位)

stm32f103——串口UART_第15张图片

注意:正因为包含了奇偶效验位,所以数据为9位长度,所以可知,奇偶效验位属于数据位中的一员。

不包含奇偶效验位:数据长度为8位(不包含起始位和停止位)

stm32f103——串口UART_第16张图片

所以我们设置数据位长度的时候,如果需要使用奇偶效验位,那么就设置为9位长度,不使用则设置为8位。

奇效验就是,如果8位数据位的1为偶数,那么再加上奇偶效验位中的1,就可以将1的个数凑成奇数。如果8位数据位的1为奇数,那么再加上奇偶效验位中的0,就可以将1的个数凑成奇数

偶效验就是,如果8位数据位的1为偶数,那么再加上奇偶效验位中的0,就可以将1的个数凑成偶数。如果8位数据位的1为奇数,那么再加上奇偶效验位中的1,就可以将1的个数凑成偶数

由此可知:为什么奇校验中,如果数据位中1的个数位偶数,那么该位为1。如果数据位中1的个数位奇数,那么该位为0。因为要把奇偶效验位一并算在数据位上。

        但是,一般我们不用奇偶效验位。因为,每一次接收发送一帧数据,都要对数据中的1的个数进行判断,这样传输速度就会慢一些。通常我们的校验方式都会加在通信协议上面,每个字节的校验我们一般不用。

UART函数介绍:

stm32f103——串口UART_第17张图片

stm32f103——串口UART_第18张图片

         UART初始化函数:我们可以看到,初始化函数的结构体成员有很多,但是并不是我们都要去设置的,根据异步模式还是同步模式来设置,如果是异步模式,我们只需要设置前面6个成员就行了。如果是,同步模式,则全部成员都需要设置。

 第一个成员:设置波特率,它可以直接赋值。如:直接赋值为115200

 

 第二个成员:设置数据位长度。设置为9位长度,则是包含奇偶效验位。设置为8位长度,则是不包含奇偶效验位。

stm32f103——串口UART_第19张图片

 第三个成员:设置停止位

stm32f103——串口UART_第20张图片

 第四位成员:是否使用奇偶效验位。

stm32f103——串口UART_第21张图片

 第5个成员:是否使用硬件流控模式

 第6个成员:使能发送/接收模式

串口助手串口设置:

stm32f103——串口UART_第22张图片  

        这个是串口助手的串口设置,我们可以看到我们程序中需要对UART的设置与串口助手中对串口设置的对象是一模一样的。

stm32f103——串口UART_第23张图片

 UART外设使能函数。使能UART1还是使能UART2。

stm32f103——串口UART_第24张图片

 UART发送数据函数,注意:它一次只能发送一个字节,而不能多个字节一起发。因为数据寄存器只发送一个字节。

stm32f103——串口UART_第25张图片

 接收数据函数:它有一个返回值,返回最近接收到的一个字节数据。

stm32f103——串口UART_第26张图片

 stm32f103——串口UART_第27张图片

 获取某一个事件发生的标志位函数:通过标志位函数,检测到标志位,然后进行相应的操作。我们主要是用这4个标志位。发生则返回SET,没发生则返回RESET。注意:标志位需要自己手动去清空。

USART_FLAG_TXE:当发送数据寄存器中的数据已经取完了,该标志位就会被置1,从而引发该事件的中断。所以,其实USART_FLAG_TXE就是用来标志一个事件的,通过它的值可以知道该事件有没有发生(即发送数据寄存器中的数据有没有被取走)。当数据写完后,发送数据寄存器中的数据就会被移位寄存器取走,此时发送数据寄存器里面为空。
 

USART_FLAG_TC:当发送移位寄存器中的1个字节数据已经通过TX脚一位一位的移出去后,1个字节数据被发送完成,该标志位就会被置1,从而引发该事件的中断。所以,其实USART_FLAG_TC就是用来标志“发送移位寄存器中的数据有没有全部发送出去”这件事的。
 

USART_FLAG_RXNE:表示已经接收到了完整的数据。即接收到的数据已经从移位寄存器放到数据寄存器中,并被CPU(或者DMA)取走了。注意:此时接收数据寄存器里面的数据不会被清空,它需要保留该数据,因为可能后面需要去读这个数据。

编程步骤:

实现stm32与PC串口通信,编程步骤:
        1,打开时钟---GPIOA,串口1,AFIO 
        2,GPIO初始化
            ---GPIO_Pin_9(TX)
            ---复用推挽输出
            ---速度--2MHZ
            
            ---GPIO_Pin_10(RX)
            ---浮空输入(接收到的高低电平由片外外设来决定,所以用浮空输入模式)


        3,串口初始化
            ---波特率---115200
            ---数据位数---8bit
            ---停止位---1bit
            ---奇偶校验---无
            ---硬件流控---无
            ---模式---发送和接收使能
        4,使能串口

stm32f103——串口UART_第28张图片

注意:USART1、GPIOA、AFIO(复用功能)都在APB2总线上面。

编写程序:

功能:MCU从PC上面接收到的数据,又通过MCU发送到PC上面显示。

void USART1_Config(void)
{
    GPIO_InitTypeDef  GPIO_InitStruct;
    USART_InitTypeDef USART_InitStruct;
    // 1,打开时钟---GPIOA,串口1,AFIO 
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO|RCC_APB2Periph_USART1,ENABLE);

    // 2,GPIO初始化
    GPIO_InitStruct.GPIO_Pin    =GPIO_Pin_9;
    GPIO_InitStruct.GPIO_Mode   =GPIO_Mode_AF_PP;        //推挽输出
    GPIO_InitStruct.GPIO_Speed  =GPIO_Speed_2MHz;
    GPIO_Init(GPIOA,&GPIO_InitStruct);

    GPIO_InitStruct.GPIO_Pin    =GPIO_Pin_10;
    GPIO_InitStruct.GPIO_Mode   =GPIO_Mode_IN_FLOATING;        //浮空输入
    GPIO_Init(GPIOA,&GPIO_InitStruct);    

    // 3,串口初始化
    USART_InitStruct.USART_BaudRate      =115200;        //波特率
    USART_InitStruct.USART_HardwareFlowControl  =USART_HardwareFlowControl_None;        //硬件流控失能
    USART_InitStruct.USART_Mode            =USART_Mode_Rx|USART_Mode_Tx;     //注意:这里使能RX/TX是可以用或操的
    USART_InitStruct.USART_Parity               =USART_Parity_No;        //奇偶效验位
    USART_InitStruct.USART_StopBits             =USART_StopBits_1;        //停止位
    USART_InitStruct.USART_WordLength           =USART_WordLength_8b;        //数据位长度
    USART_Init(USART1,&USART_InitStruct);

/*清空标志位,因为很多标志位一开始都是默认置上的(即:置1),所以我们需要在初始化的时候,就将标志位清空。如果没有这一步清空标志位,那么printf第一个字母就会打印不出来,所以我们需要在传输数据之前,就清空标志位。如果不清空标志位,那么printf函数打印的第一个字节还没来得即发送给PC,就会被第二个字节给覆盖掉了。*/

USART_ClearFlag(USART1, USART_FLAG_TC|USART_FLAG_TXE); 

    // 4,使能串口
    USART_Cmd(USART1,ENABLE);
}

/*功能:MCU从PC上面接收到的数据,又通过MCU发送到PC上面显示。这样,我们就可以即可以验证接收功能是否成功,也可以验证发送功能是否成功*/

int main(void)
{
   
    Systick_Config(72);
    USART1_Config();
    printf("HELLO!\n");


    while(1){

/*由于接收数据需要等移位寄存器接收数据,然后将数据移到接收数据寄存器中去,这些操作需要花费时间,如果我们不等数据接收完就马上进行下一个数据的接收,那么就会导致后面的数据将前面的数据给覆盖掉,这样的话数据就出现了错误*/
        if(SET==USART_GetFlagStatus(USART1,USART_FLAG_RXNE)){
            USART_ClearFlag(USART1,USART_FLAG_RXNE);        //清空标志位
            USART_SendData(USART1,USART_ReceiveData(USART1));  //发送数据给电脑,注意:是一个字节一个字节的发送
        }
 
    }
}

注意:

1.烧录程序的时候,记得要把串口助手给关掉,否则串口被占用,无法烧录程序。

2.发送/接收都是一个字节一个字节的发送/接收的。

3.我们一开始就需要清空标志位。因为很多标志位一开始都是默认置上的(即:置1),所以我们需要在初始化的时候,就将标志位清空。如果一开始不清空标志位,那么第一个字节还没来得即发送,就会被第二个字节给覆盖掉了。

4.标志位,只是一个值,它是用来给我们写while阻塞或者中断的。接收/发生标志位置不置位,单片机都不会去自动阻塞,如果我们需要阻塞,则需要我们自己手动去写while()循环在main函数中进行手动阻塞(这也体现出了它的灵活性)。

printf重定向

我们写单片机上面写printf函数,并不能将数据打印出来,因为它没有屏幕,嘿嘿。

那么,将数据传输到PC上面,在PC上面显示不就行了吗?这是个好主意。但是,在PC上面要怎么才能打印出数据来?

我们可以借助上面学的串口UART,利用UART将数据传输到串口助手上面,将数据显示出来。

但是,又个问题。printf函数内部并没有UART函数的配置,那么我们怎么将printf与UART相结合呢?

        我们可以重写printf函数。实际上prinf函数是一个库函数,每次执行printf函数的时候,它就会调用内部的 fputc()函数。fputc()函数将数据打印在屏幕上面。所以,我们需要重新编写fputc()函数,将其内部改写成UART相关操作。

如果在单片机中需要使用printf,需要做两件事情
        1,需要重写fputc----重定向
            int fputc(int ch,FILE *f)
            {
                USART_SendData(USART1, ch);
                while(SET!=USART_GetFlagStatus(USART1, USART_FLAG_TC))

                        ;
                USART_ClearFlag(USART1, USART_FLAG_TC);
                return ch;
            }
        2,使用微库
            keil---->魔术棒---->target---->USE MicroLIB打勾
            
    12》printf第一个字母打印不出来----串口配置函数
        USART_ClearFlag(USART1, USART_FLAG_TC|USART_FLAG_TXE);    

编写程序:

#include "stdio.h"

int fputc(int ch,FILE *f)
{
    USART_SendData(USART1, ch);
    while(SET!=USART_GetFlagStatus(USART1, USART_FLAG_TC))    //如果一个字节数据没有发送完,那么就阻塞在这里

        ;
    USART_ClearFlag(USART1, USART_FLAG_TC);        //传输完数据后,手动清空标志位
    return ch;  
}

int main(){

        printf("HELLO!\n");        //此时串口助手上面就打印出来HELLO!了

        return 0;

}

注意:不能在中断服务函数里面中用printf函数,这样会把MCU搞晕的。

为什么可以重写printf()函数?

因为在启动文件中,printf()函数是WEAK属性

stm32f103——串口UART_第29张图片

         weak属性也就是,如果没有重写该函数,那么就执行库中提供的函数。如果重写了该函数的话,就执行重写后的函数。

你可能感兴趣的:(stm32,单片机,arm)