GD32开发实战指南(基础篇) 第10章 串口通信

开发环境:
mdk:keil 5.30
开发板:gd32f207i-eval
mcu:gd32f207ik
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和uart3/4/6/7的系统时钟为pclk1,串口的时钟源经过 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_ctl2寄存器中使能dma (dent位);
5.在usart_baud寄存器中设置波特率;
6.在usart_ctl0寄存器中设置ten位;
7.等待tbe置位;
8.向usart_data寄存器写数据;
9.若dma未使能,每发送一个字节都需重复步骤7-8;
10.等待tc=1,发送完成。
在禁用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_ctl2寄存器中使能dma(denr位);
4.在usart_baud寄存器中设置波特率;
5.在usart_ctl0寄存器中置位uen位,使能usart;
6.在usart_ctl0中设置ren位。
接收器在使能后若检测到一个有效的起始脉冲便开始接收码流。在接收一个数据帧的过程中会检测噪声错误,奇偶校验错误,帧错误和过载错误。
当接收到一个数据帧, usart_stat0寄存器中的rbne置位,如果设置了usart_ctl0寄存器中相应的中断使能位rbneie,将会产生中断。在usart_stat0寄存器中可以观察接收状态标志。
软件可以通过读usart_data寄存器或者dma方式获取接收到的数据。不管是直接读寄存器还是通过dma,只要是对usart_data寄存器的一个读操作都可以清除rbne位。
在接收过程中,需使能ren位,不然当前的数据帧将会丢失。
以上对串口通信进行了简单介绍,为了方便各位读者朋友更好的理解,在这里笔者将引入一个新的思想--系统分层思想。既然各位对着有意于嵌入式,那么必须得有对整个系统的架构要有一定的认知。对gd32裸机开发,我们可以将分为三层:物理层、协议层和应用层。前文讲了这么多也是对串口协议进行分析,常用的物理层的串口通信标准有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可以提供时钟支持iso7816的智能卡接口。
usart是指单片机的一个端口模块,可以根据需要配置成同步模式(spi,i2c),也可以将其配置为异步模式,后者就是uart。所以说uart姑且可以称之为一个与spi,i2c对等的“协议”,而usart则不是一个协议,而是更应该理解为一个实体。相比于同步通讯,uart不需要统一的时钟线,接线更加方便。但是,为了正常的对信号进行解码,使用uart通讯的双方必须事先约定好波特率,即每个码元的长度。
关于串口的深入理解,请参看笔者文章:
https://blog.bruceou.cn/2021/01/detailed-explanation-of-stm32-serial-communication/555/
2 串口通信的寄存器描述串口常用的寄存器有状态寄存器(usart_statx)、数据寄存器(usart_data)、波特比率寄存器(usart_baud)、控制寄存器 (usart_ctlx)。
3 串口硬件串口的接口通过三个引脚与其他设备连接在一起。任何usart双向通信至少需要两个脚:接收数据输入(rx)和发送数据输出(tx)。
rx:接收数据串行输入。通过采样技术来区别数据和噪音,从而恢复数据。tx :发送数据输出。当发送器被禁止时,输出引脚恢复到它的i/o端口配置。当发送器被激活,并且不发送数据时,tx引脚处于高电平。在单线和智能卡模式里,此i/o 口被同时用于数据的发送和接收。
板子使用串口0,接口用的232,但对于软件来说,都是一样的。
4 串口发送(重定向printf)4.1 串口发送实现下面笔者就用标准库来操作串口0。
1.串口配置
串口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_typedef_enum com_id, uint32_t baudval param[out] none retval none*/void com_init(com_typedef_enum com_id, uint32_t baudval){ /* 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[com_id]); usart_baudrate_set(com_usart[com_id], baudval); usart_word_length_set(com_usart[com_id], usart_wl_8bit); usart_stop_bit_set(com_usart[com_id], usart_stb_1bit); usart_parity_config(com_usart[com_id], usart_pm_none); usart_hardware_flow_rts_config(com_usart[com_id], usart_rts_disable); usart_hardware_flow_cts_config(com_usart[com_id], usart_cts_disable); usart_receive_config(com_usart[com_id], usart_receive_enable); usart_transmit_config(com_usart[com_id], usart_transmit_enable); usart_enable(com_usart[com_id]);}2.数据发送与接收
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);通过该函数可以读取串口接受到的数据。
3.串口状态
串口的状态可以通过状态寄存器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){ /* 发送一个字节数据到usart */ 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)!='');}这样就方便多了,然后再主函数中调用发送函数。
/* 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, 115200); usart_send_string((uint8_t*)this is com1); /* 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函数了。
/* 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, 115200); printf(this is com1); /* sprintf函数把格式化的数据写入某个字符串 */ sprintf(str,20%02d-%02d-%02d,22,05,15); printf(%s,str); while(1) { }}完整代码请查看配套程序,另外还需添加微库以便支持printf。具体设置参看本节后文的小贴士部分。
我们来总结下串口发送的流程:
1.初始化硬件,时钟;
2.usart 的gpio初始化,usart参数初始化;
3.重定向printf
4.打印输出
4.2 实验现象将程序编译好下载到板子中,打开串口助手,按下图设置相应参数,按下板子的复位按键,在接收区可以看到如下信息。
5 串口接收数据(中断方式)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);中断服务函数/*! rief this function handles usart0 exception param[in] none param[out] none etval 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 ); //将接受到的数据直接返回打印 }}在中断服务程序中,接收到数据后立即输出。
主函数代码如下:
/* 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, 115200, 0, 1); printf(this is com1); /* sprintf函数把格式化的数据写入某个字符串 */ sprintf(str,20%02d-%02d-%02d,22,05,15); printf(%s,str); while(1) { }}总结下串口接收的编程流程:
1.硬件初始化,时钟初始化;
2.串口gpio初始化,串口参数配置;
3.在main()函数中使能中断接收;
4.编写中断回调函数,处理接收的数据,
【注】中断接收函数只能触发一次接收中断,所以我们需要在中断回调函数中再次调用中断接收函数。这里可以对比下标准库的流程。
5.2 实验现象将程序编译好下载到板子中,打开串口助手,按下图设置相应参数,按下板子的复位按键,在接收区可以看到如下信息。

基于PSIM的L6562芯片建模仿真
关于Linux操作系统中LKM的优势与不足研究与应用浅析
物联网设备的安全问题深度分析
装修智能家居,首选电力载波有线方式无须布线
具有统计分析功能的聊天机器人Chatbase
GD32开发实战指南(基础篇) 第10章 串口通信
传感器数量暴增 汽车生态系统押注以太网
低压电能计量装置安装技术规范
商用机器人热潮涌动 商业化短板犹存破冰有难度
从财报看熊本地震对瑞萨/索尼影响多大
详解机器学习和深度学习常见的正则化
Windows 11牵着安卓的手如期而至
针对iPhone 6S关机投诉 苹果副总裁亲赴中消协说明情况
直流双臂电桥的工作原理
华为P10怎么样?华为P10在续航和价格上面分分钟完虐三星S8,看完后你会选谁?
电力专用纵向加密认证网关工作原理
什么是电子制造业ERP?电子制造业ERP系统有哪些
普通电机用于变频器供电会有什么影响
5G时代来了,助力手机3D复合盖板成为新宠
跨链方案面临怎样的问题和挑战