资讯详情

《嵌入式 – GD32开发实战指南》第10章 串口通信

MDK:Keil 5.30 开发板:GD32F207I-EVAL MCU:GD32F207IK

10.1串口简介

USART(Universal Synchronous Asynchronous Receiver and Transmitter,通用同步-异步接收发射器)为工业标准的使用提供了灵活的方法NRZ外部设备之间的异步串行数据格式进行全双工数据交换。USART利用分数波特率发生器提供广泛的波特率选择。支持同步单向通信和半双工单线通信LIN(局部互联网)、智能卡协议和IrDA(红外数据组织)SIR ENDEC规范和调制解调器(CTS/RTS)操作。它还允许多处理器通信。配置多缓冲器DMA可实现高速数据通信。

虽然USART它可以同步或异步,但最常用的是使用功能的异步功能。如果它被用作异步通信UART(Universal Asynchronous Receiver and Transmitter),可以说,UART是USART子集,但同步通信比异步通信多了一个时钟同步信号线。

以下是同步和异步的简要介绍。

在同步通信中,收发设备双方将使用信号线来表示时钟信号。在时钟信号的驱动下,双方将协调并同步数据,见下图。在通信中,双方通常规定在时钟信号的上下沿采样数据线。

在这里插入图片描述

时钟信号不用于异步通信中的数据同步。它们直接穿插数据信号中的一些同步信号位置,或包装主数据,以数据帧的形式传输数据。如下图所示,一些通信还需要双方同意数据的传输速率,以便更好地同步。

在同步通信中,数据信号传输的大部分内容是有效数据,而异步通信将包含帧的各种识别符,因此同步通信更有效,但同步通信双方的时钟允许误差较小,而异步通信双方的时钟允许误差较大。

从以上介绍可以看出,USART同步通信需要时钟同步信号,但不需要额外的开始和停止,以实现更快的传输速度。USART控制起来比较复杂,所以本文主要讲解异步通信。

异步串行通信以字符为单位,即一个字符传输 。

串口外设的架构图看起来非常复杂。事实上,对于软件开发人员,我们只需要对串口发送过程有一个大致的了解。从下到上,我们可以看到串口外设主要由波特率控制、收发控制和数据存储转移三部分组成。

波特率,即每秒传输的二进制位数b/s(bps)波特率可以通过控制时钟来改变。在配置波特率时,我们向波特比率寄存器 USART_BAUD写入参数,修改串口时钟的分频值USARTDIV。USART_BAUD寄存器包括两部分,即INTDIV(USARTDIV )和FRADIV(USARTDIV 最后,计算公式是 USARTDIV= INTDIV (FRADIV/16)。

USARTDIV 串口外设外设的时钟源,USART0/5系统时钟为PCLK2, USART1/2和UART系统时钟为3/4/6/7PCLK1.串口的时钟源经过 USARTDIV 分频后分别输出作为发送器时钟和接收器时钟,控制发送和接收时间。在使能USART系统时钟必须在时钟控制单元中使用。

有许多寄存器围绕着发送器和接收器的控制部分 :STAT0、USART_CTL0、USART_CTL1、USART_CTL2和 STAT1,即USART 三个控制寄存器(Control Register)状态寄存器(Status Register)。写入寄存器 发送和接收的各种控制参数,如奇偶验证位置、停止位置等,也包括对USART 中断的控制;串口的状态在任何时候都可以从状态寄存器中查询得到。停止位的配置如下图所示。

1.在USART_CTL0寄存器中位UEN位,使能USART; 2.通过USART_CTL0寄存器的WL设置字长; 3.在USART_CTL1寄存器中写STB[1:0]设置停止位长度; 4.如果选择多级缓存通信,应该是USART_CTL在寄存器中使能DMA (DENT位); 5.在USART_BAUD寄存器中设置波特率; 6.在USART_CTL设置0寄存器TEN位; 7.等待TBE置位; 8.向USART_DATA寄存器写数据; 9.若DMA每个字节都需要重复步骤7-8; 10.等待TC=一、发送完成。

在禁用USART或者在进入低功耗状态之前,必须等待TC置位。先读USART_STAT0然后再写USART_DATA可将TC位清0。多级缓存通信模式(DENT=1)下,直接向TC写0,也能清TC。

1.写USART_CTL0寄存器的WL位置设置字长; 2.在USART_CTL1寄存器中写STB[1:0]设置停止位长度; 3.如果选择多级缓存通信,应该是USART_CTL在寄存器中使能DMA(DENR位); 4.在USART_BAUD波特率设置在寄存器中; 5.在USART_CTL0寄存器中位UEN位,使能USART; 6.在USART_CTL0中设置REN位。

如果接收器在使能后检测到有效的起始脉冲,则开始接收码流。在接收数据帧时,会检测噪声错误、奇偶校验错误、帧错误和过载错误。

当接收到数据帧时, USART_STAT0寄存器中的RBNE如果设置了位置USART_CTL0寄存器中相应的中断使能位RBNEIE,会产生中断USART_STAT接收状态标志可在0寄存器中观察。

通过阅读软件USART_DATA寄存器或者DMA获取接收到的数据。无论是直接读取寄存器还是通过DMA,只要是对USART_DATA可以清除寄存器的读取操作RBNE位。

在接收过程中,需要使能REN位置,否则当前数据帧将丢失。

以上简要介绍了串口通信。为了方便读者更好地理解,笔者将在这里介绍一个新的想法–系统分层思想。既然你对嵌入式感兴趣,你必须对整个系统的架构有一定的了解。对GD裸机开发可分为物理层、协议层和应用层三层。前面说了这么多,也分析了串口协议。物理层常用的串口通信标准有232和485。

【注】UART和USART的区别 USART(universal synchronous asynchronous receiver and transmitte): 通用同步异步收发器,USART是一种串行通信设备,可灵活地与外部设备进行全双工数据交换。

UART(universal asynchronous receiver and transmitter): 通用异步收发器,异步串行通信口(UART)这就是我们常说的嵌入式串口,它也是一种通用的数据通信议。从名字上可以看出,USART在UART同步功能在此基础上增加,即USART是UART的增强型。

当我们使用USART在异步通信中,它与UART没有区别,但当使用同步通信时,区别非常明显:我们都知道同步通信需要时钟来触发数据传输,也就是说,USART相对UART其中一个区别供主动时钟。GD32的USART可提供时钟支持ISO智能卡接口7816。

USART指单片机的端口模块,可根据需要配置为同步模式(SPI,I2C),也可以将其配置为异步模式,后者就是UART。所以说UART姑姑可以称之为一个和SPI,I2C对等的协议USART不是协议,而是应该理解为实体。与同步通信相比,UART不需要统一的时钟线,接线更方便。但是,为了正常解码信号,使用它UART双方必须事先约定波特率,即每个码元的长度。

对串口的深入了解,请参阅作者的文章:

对串口通信有深入的了解

10.2串口通信寄存器描述

常用的串口寄存器有状态寄存器(USART_STATx)、数据寄存器(USART_DATA)、波特比寄存器(USART_BAUD)、控制寄存器 (USART_CTLx)。

10.3串口硬件

串口接口通过三个引脚与其他设备连接。USART双向通信至少需要两只脚:接收数据输入(RX)并发送数据输出(TX)。

  • RX:串行接收数据输入。通过采样技术来区分数据和噪声,从而恢复数据。

  • TX :发送数据输出。当发送器被禁止时,输出引脚恢复到它I/O端口配置。当发送器被激活而不发送数据时,TX引脚处于高电平。在单线和智能卡模式下I/O 口被同时用于数据的发送和接收。

板材采用串口0,接口采用232,但对软件来说,都是一样的。

10.四串发送(重定向)printf)

10.4.实现串口发送

下面作者用标准库操作串口0。

  • 串口0时钟使能

串口1是挂载的 APB2 以下外设使能函为:

rcu_periph_clock_enable(RCU_USART0);

值得注意的是,不仅要打开串口的时钟,还需要打开相应GPIO的时钟,最终的代码如下:

rcu_periph_clock_enable(RCU_GPIOA);
  • 配置串口GPIO

这个比较简单,前面的章节已经讲过了,只需要注意的是,这里的GPIO不再是普通GPIO,要配置成复用功能,因此TX和RX分别配置成GPIO_MODE_AF_PP和GPIO_MODE_IN_FLOATING。

  • 串口复位

当外设出现异常的时候可以通过复位设置,实现该外设的复位,然后重新配置这个外设达到让其重新工作的目的。一般在系统刚开始配置外设的时候,都会先执行复位该外设的操作。复位的是在函数usart_deinit()中完成:

void usart_deinit(uint32_t usart_periph);

比如我们要复位串口0,方法为:

usart_deinit(USART0);
  • 串口参数初始化

串口初始化是以下函数设置:

void usart_baudrate_set(uint32_t usart_periph, uint32_t baudval); //设置波特率
void usart_word_length_set(uint32_t usart_periph, uint32_t wlen); //设置传输字长
void usart_stop_bit_set(uint32_t usart_periph, uint32_t stblen); //设置停止位
void usart_parity_config(uint32_t usart_periph, uint32_t paritycfg); //设置校验位
void usart_hardware_flow_rts_config(uint32_t usart_periph, uint32_t rtsconfig); //设置RTS流控
void usart_hardware_flow_cts_config(uint32_t usart_periph, uint32_t ctsconfig); //设置CTS流控
void usart_receive_config(uint32_t usart_periph, uint32_t rxconfig); //设置接收使能
void usart_transmit_config(uint32_t usart_periph, uint32_t txconfig); //设置发送使能

从上面的初始化格式可以看出初始化需要设置的参数为:波特率,字长,停止位,奇偶校验位,硬件数据流控制,模式(收,发)。 我们可以根据需要设置这些参数。

  • 串口使能

串口使能是通过函数usart_enable()来实现的,这个很容易理解,使用方法是:

usart_enable(USART0);

到此,串口初始化的基本配置就算完成了,完整初始化代码如下:

/* brief configure COM port param[in] com: COM on the board arg COM1: COM1 on the board param[out] none retval none */
void com_init(uint32_t com)
{ 
        
    uint32_t com_id = 0U;
    if(COM1 == com) 
		{ 
        
        com_id = 0U;
    }
    /* enable GPIO clock */
    rcu_periph_clock_enable(COM_GPIO_CLK[com_id]);

    /* enable USART clock */
    rcu_periph_clock_enable(COM_CLK[com_id]);

    /* connect port to USARTx_Tx */
    gpio_init(COM_GPIO_PORT[com_id], GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, COM_TX_PIN[com_id]);

    /* connect port to USARTx_Rx */
    gpio_init(COM_GPIO_PORT[com_id], GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, COM_RX_PIN[com_id]);

    /* USART configure */
    usart_deinit(com);
    usart_baudrate_set(com, 115200U);
    usart_word_length_set(com, USART_WL_8BIT);
    usart_stop_bit_set(com, USART_STB_1BIT);
    usart_parity_config(com, USART_PM_NONE);
    usart_hardware_flow_rts_config(com, USART_RTS_DISABLE);
    usart_hardware_flow_cts_config(com, USART_CTS_DISABLE);
    usart_receive_config(com, USART_RECEIVE_ENABLE);
    usart_transmit_config(com, USART_TRANSMIT_ENABLE);
    usart_enable(com);
}

GD32 的发送与接收是通过数据寄存器USART_DATA来实现的,这是一个双寄存器。当向该寄存器写数据的时候,串口就会自动发送,当收到数据的时候,也是存在该寄存器内。

GD32库函数操作USART_DATA寄存器发送数据的函数是:

void usart_data_transmit(uint32_t usart_periph, uint16_t data);

通过该函数向串口寄存器 USART_DR 写入一个数据。

GD32库函数操作USART_DATA寄存器读取串口接收到的数据的函数是:

uint16_t usart_data_receive(uint32_t usart_periph);

通过该函数可以读取串口接受到的数据。

串口的状态可以通过状态寄存器USART_STAT0读取。

状态寄存器的其他位我们这里就不做过多讲解,大家需要可以查看中文参考手册。

在我们固件库函数里面,读取串口状态的函数是:

FlagStatus usart_flag_get(uint32_t usart_periph, usart_flag_enum flag);

这个函数的第二个入口参数非常关键, 它是标示我们要查看串口的哪种状态, 比如上面讲解的TBE(读数据寄存器非空)以及 TC(发送完成)。例如我们要判断读寄存器是否非空(TBE), 操作库函数的方法是:

usart_flag_get (USART0, USART_FLAG_TBE);

我们要判断发送是否完成(TC),操作库函数的方法是:

usart_flag_get (USART0, USART_FLAG_TC);

这些标识号是通过枚举类型定义的:

/* USART flags */
typedef enum { 
        
    /* flags in STAT0 register */
    USART_FLAG_CTSF = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 9U),      /*!< CTS change flag */
    USART_FLAG_LBDF = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 8U),      /*!< LIN break detected flag */
    USART_FLAG_TBE = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 7U),       /*!< transmit data buffer empty */
    USART_FLAG_TC = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 6U),        /*!< transmission complete */
    USART_FLAG_RBNE = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 5U),      /*!< read data buffer not empty */
    USART_FLAG_IDLEF = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 4U),     /*!< IDLE frame detected flag */
    USART_FLAG_ORERR = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 3U),     /*!< overrun error */
    USART_FLAG_NERR = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 2U),      /*!< noise error flag */
    USART_FLAG_FERR = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 1U),      /*!< frame error flag */
    USART_FLAG_PERR = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 0U),      /*!< parity error flag */
    /* flags in STAT1 register */
    USART_FLAG_BSY = USART_REGIDX_BIT(USART_STAT1_REG_OFFSET, 16U),      /*!< busy flag */
    USART_FLAG_EB = USART_REGIDX_BIT(USART_STAT1_REG_OFFSET, 12U),       /*!< end of block flag */
    USART_FLAG_RT = USART_REGIDX_BIT(USART_STAT1_REG_OFFSET, 11U)        /*!< receiver timeout flag */
} usart_flag_enum;

另外,笔者在此给出输出格式的说明,请读者朋友参考。

格式 说明
%d 按照十进制整型数打印
%6d 按照十进制整型数打印,至少6个字符宽
%f 按照浮点数打印
%6f 按照浮点数打印,至少6个字符宽
%.2f 按照浮点数打印,小数点后有2位小数
%6.2f 按照浮点数打印,至少6个字符宽,小数点后有2位小数
%x 按照十六进制打印
%c 打印字符
%s 打印字符串

接下来就可以实现串口的发送了,这里对发送函数进行封装。

/** * @brief 串口发送一个字节数据 * @param ch:待发送字符 * @retval None */
void usart_send_byte(uint8_t ch)
{ 
        
  /* 发送一个字节数据到USART0 */
  usart_data_transmit(USART0,ch);

  /* 等待发送完毕 */
  while (usart_flag_get(USART0, USART_FLAG_TBE) == RESET);	
}

/** * @brief 串口发送指定长度的字符串 * @param str:待发送字符串缓冲器 * strlen:指定字符串长度 * @retval None */
void usart_sendStr_length(uint8_t *str,uint32_t strlen)
{ 
        
  unsigned int k=0;
  do 
  { 
        
    usart_send_byte(*(str + k));
    k++;
  } while(k < strlen);
}

/** * @brief 串口发送字符串,直到遇到字符串结束符 * @param str:待发送字符串缓冲器 * @retval None */
void usart_send_string(uint8_t *str)
{ 
        
	unsigned int k=0;
  do 
  { 
        
    usart_send_byte(*(str + k));
    k++;
  } while(*(str + k)!='\0');
}

这样就方便多了,然后再主函数中调用发送函数。

/* Includes*********************************************************************/
#include "gd32f2_systick.h"
#include "gd32f2_usart.h"
#include <stdio.h>

/* brief main function param[in] none param[out] none retval none */
int main(void)
{ 
        
    char str[20];
 
    //systick init
    sysTick_init();

    //usart init 115200 8-N-1
    com_init(COM1);

    usart_send_string((uint8_t*)"This is COM1\r\n");	

    /* sprintf函数把格式化的数据写入某个字符串 */  
    sprintf(str,"20%02d-%02d-%02d",22,05,15); 
	  
    usart_send_string((uint8_t*)str);

    while(1)
    { 
        

    }
}

下面笔者还要介绍一种常用的串口打印方式I/O重定向,也就是使用printf打印数据到终端,但是我们的裸机系统没有终端,因此如果想让printf / scanf向USART0发送、获取数据,需要通过代码指定C标准库输入/输出函数的控制终端设备,也就是使用功能I/O重定向。

在stdio.h有相应的接口。

   /* * dynamically allocates a buffer of the right size for the * formatted string, and returns it in (*strp). Formal return value * is the same as any other printf variant, except that it returns * -1 if the buffer could not be allocated. * * (The functions with __ARM_ prefixed names are identical to the * ones without, but are available in all compilation modes without * violating user namespace.) */
extern _ARMABI int fgetc(FILE * /*stream*/) __attribute__((__nonnull__(1)));
   /* * reads at most one less than the number of characters specified by n from * the stream pointed to by stream into the array pointed to by s. No * additional characters are read after a new-line character (which is * retained) or after end-of-file. A null character is written immediately * after the last character read into the array. * Returns: s if successful. If end-of-file is encountered and no characters * have been read into the array, the contents of the array remain * unchanged and a null pointer is returned. If a read error occurs * during the operation, the array contents are indeterminate and a * null pointer is returned. */
extern _ARMABI int fputc(int /*c*/, FILE * /*stream*/) __attribute__((__nonnull__(2)));

下面我们以实现printf打印数据到USART(即重定义fputc函数)的实现过程。

/** * @brief 重定向c库函数printf到USART1 * @param None * @retval */
int fputc(int ch, FILE *f)
{ 
        
	/*清除标志位*/
	usart_flag_clear(USART0,USART_FLAG_TC);
	
	/* 发送一个字节数据到USART0 */
	usart_data_transmit(USART0, (uint8_t) ch);
			
	/* 等待发送完毕 */
	while (usart_flag_get(USART0, USART_FLAG_TC) == RESET);		
	
	return (ch);
}
scanf同理。
/** * @brief 重定向c库函数scanf到USART0 * @param None * @retval None */
int fgetc(FILE *f)
{ 
        
	/* 等待串口0输入数据 */
	while (usart_flag_get(USART0, USART_FLAG_RBNE) == RESET);

	return (int)usart_data_receive(USART0);
}

接下来就可使用printf和scanf函数了。

/* Includes*********************************************************************/
#include "gd32f2_systick.h"
#include "gd32f2_usart.h"
#include <stdio.h>

/* brief main function param[in] none param[out] none retval none */
int main(void)
{ 
        
    char str[20];
 
    //systick init
    sysTick_init();

    //usart init 115200 8-N-1
    com_init(COM1);

    printf("This is COM1\r\n");	

    /* sprintf函数把格式化的数据写入某个字符串 */  
    sprintf(str,"20%02d-%02d-%02d",22,05,15); 
	  
    printf("%s",str);

    while(1)
    { 
        

    }
}

完整代码请查看配套程序,另外还需添加微库以便支持printf。具体设置参看本节后文的小贴士部分。

我们来总结下串口发送的流程:

1.初始化硬件,时钟; 2.USART 的GPIO初始化,USART参数初始化; 3.重定向printf 4.打印输出

10.4.2实验现象

将程序编译好下载到板子中,打开串口助手,按下图设置相应参数,按下板子的复位按键,在接收区可以看到如下信息。

10.5串口接收数据(中断方式)

10.5.1串口接收实现

中断方式相对于与普通方式,还需要开启中断并且初始化 NVIC以及中断服务函数。

  • 开启中断

在接收到数据的时候(RBNE读数据寄存器非空),我们要产生中断,那么我们开启中断的方法是:

usart_interrupt_enable(USART0, USART_INT_RBNE); /* 使能串口0接收中断 */

在发送数据结束的时候( TC, 发送完成) 要产生中断,那么方法是:

usart_interrupt_enable(USART0, USART_INT_TBE);

开启NVIC中断以及优先级。

nvic_irq_enable(USART0_IRQn, 0, 0);
  • 中断服务函数
/*! \brief this function handles USART0 exception \param[in] none \param[out] none \retval none */
void USART0_IRQHandler(void)
{ 
        
    uint8_t ch;
    if(RESET != usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)) { 
        
        /* read one byte from the receive data register */
        ch = (uint8_t)usart_data_receive(USART0);

        printf( "%c", ch );    //将接受到的数据直接返回打印
    }
}

在中断服务程序中,接收到数据后立即输出。

主函数代码如下:

/* Includes*********************************************************************/
#include "gd32f2_systick.h"
#include "gd32f2_usart.h"
#include <stdio.h>

/* brief main function param[in] none param[out] none retval none */
int main(void)
{ 
        
    char str[20];
 
    //systick init
    sysTick_init();

    //usart init 115200 8-N-1
    com_init(COM1);

    printf("This is COM1\r\n");	

    /* sprintf函数把格式化的数据写入某个字符串 */  
    sprintf(str,"20%02d-%02d-%02d",22,05,15); 
	  
    printf("%s\r\n",str);

    while(1)
    { 
        

    }
}

总结下串口接收的编程流程:

1.硬件初始化,时钟初始化; 2.串口GPIO初始化,串口参数配置; 3.在main()函数中使能中断接收; 4.编写中断回调函数,处理接收的数据,

【注】中断接收函数只能触发一次接收中断,所以我们需要在中断回调函数中再次调用中断接收函数。这里可以对比下标准库的流程。

10.5.2实验现象

将程序编译好下载到板子中,打开串口助手,按下图设置相应参数,按下板子的复位按键,在接收区可以看到如下信息。


小贴士:

printf 函数重定向

要想 printf() 函数工作的话,我们需要把 printf() 重新定向到串口中。重定向是指用户可以自己重写 C 的库函数,当连接器检查到用户编写了与 C 库函数相同名字的函数时,优先采用用户编写的函数,这样用户就可以实现对库的修改了。

为了实现重定向 printf() 函数,我们需要重写 fputc() 这个 C 标准库函数,因为 printf()在 C 标准库函数中实质是一个宏,最终是调用了 fputc() 这个函数。

重定向的这部分工作,由gd32f2_usart.c 文件中的 fputc(int ch, FILE *f)这个函数来完成。重定向时,我们把 fputc( ) 的形参 ch,作为串口将要发送的数据,也就是说,当使用 printf( ) 时,它先调用这个 fputc( ) 函数,然后使用GD库的串口发送函数 usart_data_transmit (),把数据转移到发送数据寄存器 TDR,触发我们的串口向 PC 发送一个相应的数据。调用完 usart_data_transmit( )后,要使用while (usart_flag_get(USART0, USART_FLAG_TC) == RESET) 语句不停地检查串口发送是否完成的标志位TC,一直检测到标志为“完成”,才进入下一步的操作,避免出错。在这段 while 循环检测的延时中,串口外设已经由发送控制器以及根 据我们的配置把数据从移位寄存器一位一位地通过串口线 Tx 发送出去了。

【注意】printf函数在“stdio.h”头文件里,使用该函数必须引用“stdio.h”库, 还 要在编译器中设置一个选项 Use MicroLIB (使用微库)。设置方式如下: 单击Project,选择option选项,再选择Target 勾选Use MicroLIB 即可。


欢迎访问我的网站

BruceOu的哔哩哔哩 BruceOu的主页 BruceOu的博客 BruceOu的CSDN博客 BruceOu的简书 BruceOu的知乎


资源获取方式

1.关注公众号[] 2.在公众号回复关键词[]获取资料提取码

标签: 8pin电子终端连接器至上连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台