STM32第五章-串口通讯详解


串口通信是串行通信里面的异步方式。串行通信是相对于并行通信来说的。串口是一个事实存在的东西,比如DB9接口。
串口通讯里面的波特率,实际上是比特率。如果这两点你还不是很清楚地话,好好往下看。

通信涉及的几个基础概念

同步通信和异步通信

(1)、同步和异步的区别:简单来说就是发送方和接收方按照同一个时钟节拍工作就叫同步,发送方和接收方没有统一的时钟节拍、而各自按照自己的节拍工作就叫异步。
(2)、同步通信中,通信双方按照统一节拍工作,所以配合很好;一般需要发送方给接收方发送信息同时发送时钟信号,接收方根据发送方给它的时钟信号来安排自己的节奏。同步通信用在通信双方信息交换频率固定,或者经常通信时。带时钟同步信号传输。如-SPI,IIC通信。
(3)、异步通信又叫异步通知。在双方通信的频率不固定时(有时 3ms 收发一次,有时 3 天才收发一次)不适合使用同步通信,而适合异步通信。异步通信时接收方不必一直在意发送方,发送方需要发送信息时会首先给接收方一个信息开始的起始信号,接收方接收到起始信号后就认为后面紧跟着的就是有效信息,才会开始注意接收信息,直到收到发送方发过来的结束标志。异步通信:不带时钟同步信号。如·UART(通用异步收发器),单总线。

电平信号和差分信号

(1)、电平信号和差分信号是用来描述通信线路传输方式的。也就是说如何在通信线路上表达 1 和 0.
(2)、电平信号的传输线中有一个参考电平线(一般是 GND),然后信号线上的信号值是由信号线电平和参考电平线的电压差决定。
(3)、差分信号的传输线中没有参考电平,所有都是信号线。然后 1 和 0 的表达靠信号线之间的电压差。
总结:电平信号的 2 根通信线之间的电平差异容易受到干扰,传输容易失败;差分信号不容易受到干扰因此传输质量比较稳定,现代通信一般都使用差分信号,电平信号几乎没有了。总结 2:看起来似乎相同根数的通信线下,电平信号要比差分信号要快;但是实际还是差分信号快,因为差分信号抗干扰能力强,因此 1 个发送周期更短。

并行接口和串行接口

(1)、串行、并行主要是考虑通信线的根数,就是发送方和接收方同时可以传递的信息量的多少
(2)、譬如在电平信号下,1 根参考电平线+1 根信号线可以传递 1 位二进制;如果我们有 3根线(2 根信号线+1 根参考线)就可以同时发送 2 位二进制;如果想同时发送 8 位二进制就需要 9 根线。
(3)、在差分信号下,2 根线(彼此差分)可以同时发送 1 位二进制;如果需要同时发送 8 位二进制,需要 16 根线。
总结:听起来似乎并行接口比串行接口要快(串行接口一次只能发送 1 位二进制,而并行接口一次可以发送多位二进制)要更优秀;但是实际上串行接口才是王道,用的比较广。因为更省信号线,而且对传输线的要求更低、成本更低;而且串行时可以通过提高通信速度来提高总体通信性能,不一定非得要并行。
总结:异步、串行、差分,譬如 USB 和网络通信更胜一筹。

串口通信涉及的基础概念

异步、电平信号、串行

(1)、异步:串口通信的发送方和接收方之间是没有统一的时钟信号的。
(2)、电平信号:串口通信出现的时间较早,速率较低,传输的距离较近,所以干扰还不太明显,因此当时使用了电平信号传输。后期出现的传输协议都改成差分信号传输了。
(3)、串行通信:串口通信每次同时只能传输 1 个二进制位

RS232 电平和 TTL 电平

(1)电平信号是用信号线电平减去参考线电平得到电压差,这个电压差决定了传输值是 1 还是 0.
(2)在电平信号时多少 V 代表 1,多少 V 代表 0 不是固定的,取决于电平标准。譬如 RS232电平中-3V~-15V 表示 1;+3~+15V 表示 0;TTL 电平则是+5V 表示 1,0V 表示 0.
(3)不管哪种电平都是为了在传输线上表示 1 和 0.区别在于适用的环境和条件不同。RS232的电平定义比较大,适合干扰大、距离远的情况;TTL 电平电压范围小,适合距离近且干扰小的情况。
(4)我们台式电脑后面的串口插座就是 RS232 接口的,在工业上用串口时都用这个,传输距离小于 15 米;TTL 电平一般用在电路板内部两个芯片之间。
(5)对编程来说,RS232 电平传输还是 TTL 电平是没有差异的。所以电平标准对硬件工程师更有意义,而软件工程师只要略懂即可。(把 TTL 电平和 RS232 电平混接是不可以的)

波特率

(1)衡量通讯性能的一个非常重要的参数就是通讯速率,通常以比特率(Bitrate)来表示,即每秒钟传输的二进制位数,单位为比特每秒(bit/s)。容易与比特率混淆的概念是“波特率”。(Baudrate),它表示每秒钟传输了多少个码元。而码元是通讯信号调制的概念,通讯中常用时间间隔相同的符号来表示一个二进制数字,这样的信号称为码元。如常见的通讯传输中,用 0V表示数字 0,5V 表示数字 1,那么一个码元可以表示两种状态 0 和 1,所以一个码元等于一个二进制比特位,此时波特率的大小与比特率一致;如果在通讯传输中,有 0V、2V、4V以及 6V分别表示二进制数 00、01、10、11,那么每个码元可以表示四种状态,即两个二进制比特位,所以码元数是二进制比特位数的一半,这个时候的波特率为比特率的一半。因为很多常见的通讯中一个码元都是表示两种状态,人们常常直接以波特率来表示比特率。譬如每秒种可以传输 9600 个二进制位(传输一个二进制位需要的时间是 1/9600秒,也就是 104us),比特率就是 9600.但因为一个码元都是表示两种状态,所以比特率=波特率。也通常说波特率就是 9600
(2)串口通信的波特率不能随意设定,而应该在一些值中去选择。一般最常见的波特率是 9600或者 115200.为什么波特率不可以随便指定?主要是因为:第一,通信双方必须事先设定相同的波特率这样才能成功通信,如果发送方和接收方按照不同的波特率通信则根本收不到,因此波特率最好是大家熟知的而不是随意指定的。第二,常用的波特率经过长久发展,就形成了共识,大家常用就是 9600 或者 115200.

起始位、数据位、奇偶校验位、停止位

(1)串口通信时,收发是一个周期一个周期进行的,没周期传输 n 个二进制位。这一个周期就叫做一个通信单元,一个通信单元是由:起始位+数据位+奇偶校验位+停止位组成的。
(2)起始位表示发送方要开始发送一个通信单元;数据位是一个通信单元中发送的有效信息位;奇偶校验位是用来校验数据位,以防止数据位出错的;停止位是发送方用来表示本通信单元结束标志的。
(3)起始位的定义是串口通信标准事先指定的,是由通信线上的电平变化来反映的。
(4)数据位是本次通信真正要发送的有效数据,串口通信一次发送多少位有效数据是可以设定的(一般可选的有 6、7、8、9,99%情况下我们都是选择 8 位数据位。因为我们一般通过串口发送的文字信息都是 ASCII 码编码的,而 ASCII 码中一个字符刚好编码为 8 位。)
(5)奇偶校验位是用来给数据位进行奇偶校验(把待校验的有效数据逐个位的加起来,总和为奇数奇偶校验位就为 1,总和为偶数奇偶校验位就为 0)的,可以在一定程度上防止位反转。

  • 奇校验要求有效数据和校验位中“1”的个数为奇数,比如一个 8 位长的有效数据为:01101001,此时总共有 4 个“1”,为达到奇校验效果,校验位为“1”,最后传输的数据将是 8 位的有效数据加上 1 位的校验位总共 9位。
  • 偶校验与奇校验要求刚好相反,要求帧数据和校验位中“1”的个数为偶数,比如数据帧:11001010,此时数据帧“1”的个数为 4 个,所以偶校验位为“0”。
  • 0 校验是不管有效数据中的内容是什么,校验位总为“0”,1 校验是校验位总为“1”。
  • 在无校验的情况下,数据包中不包含校验位。

(6)停止位的定义是串口通信标准事先指定的,是由通信线上的电平变化来反映的。常见的有 1 位停止位,1.5 位停止位,2 位停止位等。99%情况下都是用 1 位停止位。
总结:串口通信时因为是异步通信,所以通信双方必须事先约定好通信参数,这些通信参数包括:波特率、数据位、奇偶校验位、停止位(串口通信中起始位定义是唯一的,所以一般不用选择)

串口通信的基本原理

全双工、半双工及单工通讯

(1)单工就是单方向,双工就是双方同时收发,同时只能单方向但是方向可以改变叫半双工
(2)如果只能 A 发 B 收则单工,A 发 B 收或者 B 发 A 收(两个方向不能同时)叫半双工,A发 B 收同时 B 发 A 收叫全双工。

三根通信线:Rx Tx GND

(1)任何通信都要有信息传输载体,或者是有线的或者是无线的。
(2)串口通信是有线通信,是通过串口线来通信的。
(3)串口通信线最少需要 2 根(GND 和信号线),可以实现单工通信,也可以使用 3 根通信线(Tx、Rx、GND)来实现全双工。

收发双方事先规定好通信参数

(1)串口通信属于基层基本性的通信规约,它自己本身不会去协商通信参数,需要通信前通信双方事先约定好通信参数(波特率、数据位、奇偶校验位、停止位等)
(2)串口通信的任何一个关键参数设置错误,都会导致通信失败。譬如波特率调错了,发送方发送没问题,接收方也能接收,但是接收到全是乱码···

信息以二进制流的方式在信道上传输

(1)、串口通信的发送方每隔一定时间(时间固定为 1/波特率,单位是秒)将有效信息(1或者 0)放到通信线上去,逐个二进制位的进行发送。
(2)接收方通过定时(起始时间由读到起始位标志开始,间隔时间由波特率决定)读取通信线上的电平高低来区分发送给我的是 1 还是 0。依次读取数据位、奇偶校验位、停止位,停止位就表示这一个通信单元(帧)结束,然后中间是不定长短的非通信时间(发送方有可能紧接着就发送第二帧,也可能半天都不发第二帧,这就叫异步通信),下来就是第二帧·····
总结:第一,波特率非常重要,波特率错了整个通信就乱套了;数据位、奇偶校验位、停止位也很重要,否则可能认不清数据。
第三,通过串口不管发数字、还是文本还是命令还是什么,都要先对发送内容进行编码,编码成二进制再进行逐个位的发送。
(3)串口发送的一般都是字符,一般都是 ASCII 码编码后的字符,所以一般设置数据位都是 8,方便刚好一帧发送 1 个字节。

STM32串口通讯详解

串口通讯的物理层有很多标准及变种,主要讲解 RS-232 标准 ,RS-232标准主要规定了信号的用途、通讯接口以及信号的电平标准。因为我们常见的市面上的开发板在串口通讯那一讲都是关于RS-232标准的协议。
我们这里就不讲啥结构体了,这些直接去看数据手册就好了,讲一下配置过程步骤,做到胸有成竹、心中有数。

1.使能串口引脚GPIOA的时钟

RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);

2.使能串口的时钟,串口挂载在AHB2

RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

3.将串口的引脚复用到串口中断线上

/*连接 PA10 复用到 USART1_Rx*/
GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1);
/*连接 PA9 复用到 USART1__Tx*/
GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1);  

STM32有很多的内置外设,这些外设的外部引脚都是与GPIO复用的。也就是说,一个GPIO如果可以复用为内置外设的功能引脚,那么当这个GPIO作为内置外设使用的时候,就叫做复用。例如串口1的发送接收引脚是PA9,PA10,当我们把PA9,PA10不用作GPIO,而用做复用功能串口1的发送接收引脚的时候,叫端口复用。

4.常规操作初始化串口引脚的GPIO

/* GPIO初始化 */
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;  
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
/* 配置Tx引脚为复用功能  */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_9  ;  
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 配置Rx引脚为复用功能 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_10;
GPIO_Init(GPIOA, &GPIO_InitStructure);

5.串口参数初始化

串口初始化是通过 USART_Init()函数实现的。

/* 波特率设置:115200 */
USART_InitStructure.USART_BaudRate = 115200;
/* 字长(数据位+校验位):8 */
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
/* 停止位:1个停止位 */
USART_InitStructure.USART_StopBits = USART_StopBits_1;
/* 校验位选择:偶校验 */  
USART_InitStructure.USART_Parity = USART_Parity_No;
/* 硬件流控制:不使用硬件流 */
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
/* USART模式控制:同时使能接收和发送 */
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
/* 完成USART初始化配置 */
USART_Init(USART1, &USART_InitStructure);
/* 使能串口 */
USART_Cmd(USART1, ENABLE);
/*开启中断 接收到数据产生中断 进入中断服务函数 */
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

初始化需要设置的参数为:波特率,字长,停止位,奇偶校验位,硬件数据流控制,模式(收,发)。
这里面的USART_Cmd();函数很好理解,就是是能串口。USART_ITConfig();就是开启中断响应了,这里面的第二个入口参数我们一般写的是USART_IT_RXNE.也就是打开接收中断,即程序在发送数据结束的时候要产生中断,调到中断服务函数中。

6.设置串口中断优先级分组

我们前一章说了,只要你的程序里面用了中断,就必须配置串口优先级分组。但是我们我们有时候会发现我们在一些厂家的串口例程中没有配置串口中断优先级分组,这是因为他程序只有一个串口中断就没有所谓的优先级,配不配置,程序都只有个中断。

NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;       //子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

7.编写串口中断服务函数

这个函数很重要,既然有中断就要执行串口中断服务函数中去,执行相应的指令。

void USART1_IRQHandler(void)
{
    if(USART_GetITStatus( USART1, USART_IT_RXNE ) != RESET)//获取接收中断标志位(接收到的数据必须是0x0d 0x0a结尾)
    {
        Res = USART_ReceiveData( USART1 );
            if((USART_RX_STA&0x8000)==0)//接收未完成
            {
            if(USART_RX_STA&0x4000)//接收到了0x0d
                {
                if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
                else USART_RX_STA|=0x8000;//接收完成了
                }
            else //还没收到0X0D
                {
                if(Res==0x0d)USART_RX_STA|=0x4000;
                else
                    {
                    USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
                    USART_RX_STA++;
                    if(USART_RX_STA>( USART_Rec_Len-1))USART_RX_STA=0;//接收数据错误,重新开始接收
                    }
            }
    }
}

7.1 中断服务函数名不是随便起的,USART1_IRQHandler表示串口一个中断服务函数,函数名字在启动文件startup_stm32f10x_hd.s文件中可以找到。
7.2 if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) 见名知意,得到串口中断的标志位,判断是否发生串口1接收中断,如果是串口接收中断,则读取串口接受到的数据。
7.3 Res =USART_ReceiveData(USART1);将从串口1读取接收到的数据赋值给变量Res。
7.4 简单的 接 收 协 议。通 if语句 , 配 合 一 个 数 组USART_RX_BUF[],一个接收状态寄存器 USART_RX_STA(此寄存器其实就是一个全局变量,由读者自行添加。由于它起到类似寄存器的功能,这里暂且称之为寄存器)实现对串口数据的接收管理。USART_RX_BUF 的大小由 USART_Rec_Len 定义,也就是一次接收的数据最大不能超过 USART_Rec_Len 个字节。USART_RX_STA 是一个接收状态寄存器。

接收到从电脑串口调试助手发过来的数据,把接收到的数据保存在 USART_RX_BUF 中,同时在接收状态寄存器(USART_RX_STA)中计数接收到的有效数据个数,当收到回车(回车的表示由 2 个字节组成:回车符的ASCII码是0X0D 和换行符的ASCII码是 0X0A)的第一个字节 0X0D 时,计数器将不再增加,等待0X0A 的到来,而如果 0X0A 没有来到,则认为这次接收失败,重新开始下一次接收。如果顺利接收到 0X0A,则标记 USART_RX_STA 的第 15 位,这样完成一次接收,并等待该位被其他程序清除,从而开始下一次的接收,而如果迟迟没有收到 0X0D,那么在接收数据超过 USART_Rec_Len 的时候,则会丢弃前面的数据,重新接收。

8.编写自定义发送函数

这个自定义的发送函数是我们直接在程序中发送数据到串口调试助手,这个自定义的函数和中断服务函数无关,因为中断服务函数是用来接收数据的,就是我们通过串口调试助向单片机发送数据,需要用到中断服务函数。换句话说,如果你只想通过单片机将数据发送到串口调试助手的话,就不需要写中断服务函数了。但是这样做法没有意义,我们用串口是主要是接收数据的,单单发送一个数据意义不大。

/*****************  发送一个字符 **********************/
static void Uart_SendByte(uint8_t ch)
{
    /* 发送一个字节数据到USART1 */
    USART_SendData(USART1,ch);
    /* 等待发送完毕 */
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);//获取发送状态
}
/*****************  发送字符串 **********************/
void Uart_SendString(uint8_t *str)
{
    uint8_t k=0;
    do
    {
        Uart_SendByte(*(str + k) );
        k++;
    } while(*(str + k)!='\0');
}
/*****************  指定长度的发送字符串 **********************/
void Uart_SendStr_length(uint8_t *str,uint32_t strlen )
    {
    uint8_t k=0;
    do
    {
        Uart_SendByte( *(str + k) );
        k++;
    } while(k < strlen);
}

我们这里编写了三个函数主要是功能是:向单片机发送一个字符、向单片机发送一个字符串、向单片机发送指定长度的字符串。
代码很短,不多解释。在发送字节的时候用到了获取状态函数,获取发送数据的寄存器第7位的状态,当为1是,数据传送到移位寄存器。传送完了就可以发送了。

9.编写主函数

int main(void)
{
    char ch;
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    USART1_Init();
    Uart_SendString( (uint8_t *)"这条数据是来自单片机发送的数据\n" );
    Uart_SendString( (uint8_t *)"输入数据并以回车键结束\n" );
    while(1)
    {

    }
}

首先我们需要调用NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);完成中断优先级的分组设置。再调用USART1_Init函数完成 USART 初始化配置,包括 GPIO配置,USART配置,接收中断使用等等信息。接下来就可以调用字符发送函数把数据发送给串口调试助手了。最后主函数什么都不做,只是静静地等待 USART接收中断的产生,并在中断服务函数把数据回传。

10.下载验证

保证开发板相关硬件连接正确,用 USB 线连接开发板的串口调试usb接口跟电脑,在电脑端打开串口调试助手,配置到波特率、校验位、数据位、停止位。把编译好的程序下载到开发板,此时串口调试助手即可收到开发板发过来的数据。我们在串口调试助手发送区域输入任意字符,点击发送按钮,马上在串口调试助手接收区即可看到相同的字符。因为我在家里手上没有串口线,所以不能演示截图了。

11.串口调试助手发送数据给单片机

单片机及收到后,执行相应的指令。我们不仅仅可以将数据发送到串口调试助手,我们还可以在串口调试助手发送数据给控制器,控制器程序根据接收到的数据进行下一步工作。首先,我们来编写一个程序实现开发板与电脑通信,在开发板上电时通过 USART发送一串字符串给电脑,然后开发板进入中断接收等待状态,如果电脑有发送数据过来,开发板就会产生中断,我们在中断服务函数接收数据,并马上把数据返回发送给电脑。

//重定向c库函数printf到串口,重定向后可使用printf函数
int fputc(int ch, FILE *f)
{
    /* 发送一个字节数据到串口 */
    USART_SendData(USART1, (uint8_t) ch);
    /* 等待发送完毕 */
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    return (ch);
}

//重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数
int fgetc(FILE *f)
{
    /* 等待串口输入数据 */
    while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
    return (int)USART_ReceiveData(USART1);
}

在 C 语言标准库中,fputc函数是 printf 函数内部的一个函数,功能是将字符 ch写入到文件指针 f所指向文件的当前写指针位置,简单理解就是把字符写入到特定文件中。我们使用 USART 函数重新修改 fputc函数内容,达到类似“写入”的功能。fgetc 函数与 fputc 函数非常相似,实现字符读取功能。在使用 scanf函数时需要注意字符输入格式。
还有一点需要注意的,使用 fput和 fgetc函数达到重定向 C语言标准库输入输出函数必须在 MDK的工程选项把“Use MicroLIB”勾选上,MicoroLIB 是缺省 C库的备选库,它对标准 C 库进行了高度优化使代码更少,占用更少资源。为使用 printf、scanf 函数需要在文件中包含 stdio.h头文件。

11.修改主函数

int main(void)
{
    char ch;
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    LED_Init();
    USART1_Init();
    Uart_SendString( (uint8_t *)"这条数据是来自串口发送的\n" );
    Uart_SendString( (uint8_t *)"输入数据并以回车键结束\n" );
    while(1)
    {
        ch=getchar();
        printf("接收到字符:%c\n",ch);
        switch(ch)
        {
            case '1':
                printf("LED灯亮");
                GPIO_ResetBits(GPIOH,GPIO_Pin_12);//PH12接了一个LED灯
                break;
            default:
                break;
        }
    }
}

至此串口通讯的编程详解就结束了,现在你会了吗?


文章作者: 小师弟
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小师弟 !
评论
  目录