19. SCI UART——串口通信¶
本章配套视频介绍:
《27-SCI UART–串口通信(第1节)——串口通信协议简介(上)》
https://www.bilibili.com/video/BV1YN4y1x7qz/
《28-SCI UART–串口通信(第2节)——串口通信协议简介(下)》
https://www.bilibili.com/video/BV1Ge411i7Je/
《29-SCI UART–串口通信(第3节)——SCI的结构框图分析》
https://www.bilibili.com/video/BV1qK411x7P5/
《30-SCI UART–串口通信(第4节)——UART波特率计算》
https://www.bilibili.com/video/BV1mG411B7Sr/
《31-SCI UART–串口通信(第5节)——UART收发回显》
https://www.bilibili.com/video/BV1V94y1T7js/
《32-SCI UART–串口通信(第6节)——UART指令控制RGB灯》
https://www.bilibili.com/video/BV1i5411e7qz/
《33-SCI UART–串口通信(第7节)——环形队列详解》
https://www.bilibili.com/video/BV1W64y1N7yk/
《34-SCI UART–串口通信(第8节)——基于环形队列的UART收发回显》
https://www.bilibili.com/video/BV1re41117au/
19.1. 串口通信协议简介¶
串口通讯(Serial Communication)是一种设备间非常常用的串行通讯方式,因为它简单便捷,因此大部分电子设备都支持该通讯方式,电子工程师在调试设备时也经常使用该通讯方式输出调试信息。 在计算机科学里,大部分复杂的问题都可以通过分层来简化。如芯片被分为内核层和片上外设。对于通讯协议, 我们也以分层的方式来理解,最基本的是把它分为物理层和协议层。物理层规定通讯系统中具有机械、电子功能部分的特性,确保原始数据在物理媒体的传输。协议层主要规定通讯逻辑,统一收发双方的数据打包、解包标准。简单来说物理层规定我们用嘴巴还是用肢体来交流,协议层则规定我们用中文还是英文来交流。 下面我们分别对串口通讯协议的物理层及协议层进行讲解。
19.1.1. 物理层¶
串口通讯的物理层有很多标准及变种,我们主要讲解RS-232标准 ,RS-232标准主要规定了信号的用途、通讯接口以及信号的电平标准。
使用RS-232标准的串口设备间常见的通讯结构见 图19_1。
图 19-1 串口通讯结构图
在上面的通讯方式中,两个通讯设备的“DB9接口”之间通过串口信号线建立起连接,串口信号线中使用“RS-232标准”传输数据信号。由于RS-232电平标准的信号不能直接被控制器直接识别,所以这些信号会经过一个“电平转换芯片”转换成控制器能识别的“TTL标准”的电平信号,才能实现通讯。
19.1.1.1. 电平标准¶
根据通讯使用的电平标准不同,串口通讯可分为TTL标准及RS-232标准,见表19‑1。
表 19‑1 TTL电平标准与RS232电平标准
通讯标准 |
电平标准(发送端) |
5V TTL |
逻辑1:2.4V-5V |
逻辑0:0~0.5V |
|
RS-232 |
逻辑1:-15V~-3V |
逻辑0:+3V~+15V |
我们知道常见的电子电路中常使用TTL的电平标准,理想状态下,使用5V表示二进制逻辑1,使用0V表示逻辑0; 而为了增加串口通讯的远距离传输及抗干扰能力, RS-232使用-15V表示逻辑1,+15V表示逻辑0。使用RS232与TTL电平校准表示同一个信号时的对比见 图19_2。
图 19-2 RS-232与TTL电平标准下表示同一个信号
因为控制器一般使用TTL电平标准,所以常常会使用MA3232芯片对TTL及RS-232电平的信号进行互相转换。
19.1.1.2. RS-232信号线¶
在最初的应用中,RS-232串口标准常用于计算机、路由与调制调解器(MODEN,俗称“猫”)之间的通讯 ,在这种通讯系统中,设备被分为数据终端设备DTE(计算机、路由)和数据通讯设备DCE(调制调解器)。 我们以这种通讯模型讲解它们的信号线连接方式及各个信号线的作用。
在旧式的台式计算机中一般会有RS-232标准的COM口(也称DB9接口),见 图19_3。
图 19‑3 电脑主板上的COM口及串口线
其中接线口以针式引出信号线的称为公头,以孔式引出信号线的称为母头。 在计算机中一般引出公头接口,而在调制调解器设备中引出的一般为母头,使用上图中的串口线即可把它与计算机连接起来。 通讯时,串口线中传输的信号就是使用前面讲解的RS-232标准调制的。
在这种应用场合下,DB9接口中的公头及母头的各个引脚的标准信号线接法见 图19_4 及 表19_2。
图 19-4 DB9标准的公头及母头接法
表 19-2 DB9信号线说明(公头,为方便理解,可把DTE理解为计算机,DCE理解为调制调解器)
序号 |
名称 |
符号 |
数据方向 |
说明 |
1 |
载波检测 |
DCD |
DTEDCE |
Data Carrier Detect,数据载波检测,用于DTE告知对方,本机是否收到对方的载波信号 |
2 |
接收数据 |
RXD |
DTEDCE |
Receive Data,数据接收信号,即输入 。 |
3 |
发送数据 |
TXD |
DTEDCE |
Transmit Data,数据发送信号,即输出。两个设备之间的TXD与RXD应交叉相连 |
4 |
数据终端 (DTE) 就绪 |
DTR |
DTEDCE |
Data Terminal Ready,数据终端就绪,用于DTE向对方告知本机是否已准备好 |
5 |
信号地 |
GND |
地线,两个通讯设备之间的地电位可能不一样,这会影响收发双方的电平信号,所以两个串口设备之间必须要使用地线连接,即共地。 |
|
6 |
数据设备(DCE)就绪 |
DSR |
DTEDCE |
Data Set Ready,数据发送就绪,用于DCE告知对方本机是否处于待命状态 |
7 |
请求发送 |
RTS |
DTEDCE |
Request To Send,请求发送, DTE 请求 DCE 本设备向DCE端发送数据 |
8 |
允许发送 |
CTS |
DTEDCE |
Clear To Send,允许发送,DCE回应对方的RTS发送请求,告知对方是否可以发送数据 |
9 |
响铃指示 |
RI |
DTEDCE |
Ring Indicator,响铃指示,表示DCE端与线路已接通 |
上表中的是计算机端的DB9公头标准接法,由于两个通讯设备之间的收发信号(RXD与TXD)应交叉相连, 所以调制调解器端的DB9母头的收发信号接法一般与公头的相反,两个设备之间连接时, 只要使用“直通型”的串口线连接起来即可,见 图19_5。
图 19-5 计算机与调制调解器的信号线连接
串口线中的RTS、CTS、DSR、DTR及DCD信号,使用逻辑 1表示信号有效,逻辑0表示信号无效。例如,当计算机端控制DTR信号线表示为逻辑1时,它是为了告知远端的调制调解器,本机已准备好接收数据,0则表示还没准备就绪。
在目前的其它工业控制使用的串口通讯中,一般只使用RXD、TXD以及GND三条信号线,直接传输数据信号,而RTS、CTS、DSR、DTR及DCD信号都被裁剪掉了。
19.1.2. 协议层¶
串口通讯的数据包由发送设备通过自身的TXD接口传输到接收设备的RXD接口。在串口通讯的协议层中,规定了数据包的内容, 它由启始位、主体数据、校验位以及停止位组成,通讯双方的数据包格式要约定一致才能正常收发数据,其组成见 图19_6。
图 19-6 串口数据包的基本组成
19.1.2.1. 波特率¶
本章中主要讲解的是串口异步通讯,异步通讯中由于没有时钟信号(如前面讲解的DB9接口中是没有时钟信号的), 所以两个通讯设备之间需要约定好波特率,即每个码元的长度,以便对信号进行解码, 图19_6 中用虚线分开的每一格就是代表一个码元。常见的波特率为4800、9600、115200等。
19.1.2.2. 通讯的起始和停止信号¶
串口通讯的一个数据包从起始信号开始,直到停止信号结束。 数据包的起始信号由一个逻辑0的数据位表示,而数据包的停止信号可由0.5、1、1.5或2个逻辑1的数据位表示,只要双方约定一致即可。
19.1.2.3. 有效数据¶
在数据包的起始位之后紧接着的就是要传输的主体数据内容,也称为有效数据,有效数据的长度常被约定为5、6、7或8位长。
19.1.2.4. 数据校验¶
在有效数据之后,有一个可选的数据校验位。 由于数据通信相对更容易受到外部干扰导致传输数据出现偏差,可以在传输过程加上校验位来解决这个问题。 校验方法有奇校验(odd)、偶校验(even)、0校验(space)、1校验(mark)以及无校验(noparity)。
奇校验要求有效数据和校验位中“1”的个数为奇数, 比如一个8位长的有效数据为:01101001,此时总共有4个“1”, 为达到奇校验效果,校验位为“1”,最后传输的数据将是8位的有效数据加上1位的校验位总共9位。
偶校验与奇校验要求刚好相反,要求帧数据和校验位中“1”的个数为偶数, 比如数据帧:11001010,此时数据帧“1”的个数为4个,所以偶校验位为“0”。
0校验是不管有效数据中的内容是什么,校验位总为“0”,1校验是校验位总为“1”。
19.2. SCI 简介¶
SCI(Serial Communications Interface),意为串行通信接口, 是相对与并行通信的概念,是串行通信技术的一种总称,包括了UART,SPI等串行通信技术。 RA6M5 的SCI模块是一个有10个通道的异步/同步串行接口,RA4M2 有6个通道,RA2L1 有5个通道。
SCI 模块包含如下功能(根据具体型号有所不同):
UART
8位时钟同步接口
简易IIC(只能用作主机)
简易SPI
智能卡接口(符合ISO/IEC 7816-3国际标准)
曼彻斯特接口
增强的串行接口
另外,RA6M5 的 SCI0、SCI3~SCI9 有独立的FIFO缓冲区; RA4M2 的 SCI0、SCI3、SCI4、SCI9 有独立的FIFO缓冲区; RA2L1 仅 SCI0 有独立的FIFO缓冲区。
19.3. SCI的结构框图¶
以 RA6M5 为例,SCI的结构框图如下图所示。接下来我们大致地研究一下它的结构和功能。
19.3.1. 功能引脚¶
见图中标注 ① 处。
RXDn/SCLn/MISOn:
RXDn:UART接收数据输入。
SCLn:I2C时钟信号输入或输出。
MISOn:SPI主机信号输入,从机信号输出。
TXDn/SDAn/MOSIn:
TXDn:UART发送数据输出。
SDAn:I2C数据输入或输出。
MOSIn:SPI从机信号输入,主机信号输出。
SSn/CTSn_RTSn:
SSn:片选信号输入,低电平有效。
CTSn_RTSn:清除以发送(Clear to Send)或请求以发送(Request to Send)。 低电平有效。如果使能RTS流控制,当UART接收器准备好接收新数据时就会将RTS变成低电平;当接收寄存器已满时,RTS将被设置为高电平。 如果使能CTS流控制,发送器在发送下一帧数据之前会检测CTS引脚,如果为低电平,表示可以发送数据,如果为高电平则在发送完当前数据帧之后停止发送, 该引脚只适用于硬件流控制。
CTSn (n = 0, 3 to 9):清除以发送(Clear to Send),适用于硬件流控制。
SCKn:时钟输出或输入引脚,适用于同步通信。
19.3.3. 数据寄存器和移位寄存器¶
见图中标注 ③ 处。
RSR接收移位寄存器将RXDn引脚接收到的串行数据转为并行数据。当接收到一帧数据,数据会被自动传入RDR/RDRHL或FRDRHL寄存器(在FIFO模式下), 并允许RSR继续接收更多数据,CPU无法直接访问RSR。
TSR传输移位寄存器用于传输串行数据。要进行数据传输,SCI首先自动将数据从TDR/TDRHL或FTDRHL寄存器(在FIFO模式下)传入到TSR中, 然后将数据发送到TXDn引脚,CPU无法直接访问TSR。
19.3.4. 波特率发生器¶
见图中标注 ④ 处。
用于控制波特率,具体由BRR(Bit Rate Rigister),MDDR(Modulation Duty Register)和SMR(Serial Mode Register)寄存器控制。 SMR在这里对波特率生成器的输入时钟进行选择,可以选择PCLK,PCLK/4,PCLK/16,PCLK/64,即PCLK/4n(n=0-3)。
19.4. UART波特率计算¶
决定串口波特率的寄存器有BRR(Bite Rate Rigister),SEMR(Serial Extended Mode Rigister)和MDDR(Modulation Duty Register)。 波特率与寄存器的值的公式如 图19_8 所示。N表示BRR寄存器的值,B是波特率,PCLK是外设时钟的频率(单位:MHz)。 BGDM(Baud Rate Generator Double-Speed Mode Select) 在RA6M5中, SCI挂载在PHBIU(Peripheral High Speed Bus Interface Unit)总线上,使用时钟PCLKA,该时钟默认频率为100MHz。
图 19-8 波特率和误差的计算公式
参数 |
描述 |
---|---|
N |
BRR寄存器的值。 |
B |
波特率。 |
PCLK |
外设时钟的频率(单位:MHz),SCI挂载在PHBIU (Peripheral High Speed Bus Interface Unit)总线上, 使用时钟PCLKA,该时钟默认频率为100MHz。 |
BGDM bit |
Baud Rate Generator Double-Speed Mode Select, 写1则将波特率生成器输出的时钟进行双倍频,写0则不进行倍频。 |
ABCS bit |
Asynchronous Mode Base Clock Select, 写1则设置bit时间为16个时钟周期,写0则是8个时钟周期。 |
ABCSE bit |
Asynchronous Mode Extended Base Clock Select, 写1则固定bit时间为6个时钟周期,写0则由ABCS和BGDM决定bit时间。 |
n |
SMR寄存器的CKS(Clock Select)[1:0]的值,波特率生成器的输入时钟选择, 时钟输入为PCLK/4 n。 |
Error(%) |
申请波特率与实际波特率之间的误差。 |
由于N存放的是整数,故只能将公式计算出来的值进行四舍五入,这样肯定会存在误差(Error)。 例如,我们要配置当前波特率为115200bps。当PCLK=100MHz,BGDM=0,ABCS=0,ABCSE=0,n=1,按照 图19_8 公式,当前N为:
则当前误差Error为:
由于N是四舍五入取得的,故Error有可能是负数,因此这里取绝对值。
大多数时候会有多组参数符合波特率要求,由于误差越大,收发出错的可能性越大,虽然可以用奇偶校验避免这个问题, 但奇偶校验无法解决丢包问题,因此我们一般会选择误差最小的那组参数。例如: 同样是要求波特率为115200bps,但此时BGDM=1,ABCS=0,ABCSE=0,n=0,则此时的N为:
当前误差Error为:
很明显这组参数的误差0.47%要小于上一组参数的3.12%,因此我们会选择这一组参数。
因为可选参数有很多组,所以我们需要遍历所有可选的参数组合来寻找误差小的那组参数,毫无疑问的对用户来说是相当繁琐的。 瑞萨官方当然也想到了这一点,因此读者在实际开发的过程中,可以直接使用FSP库的R_SCI_UART_BaudCalculate函数来计算出适当的参数组。 在通过Smart Configuration配置串口的时候,Smart Configuration也会将适当的参数组计算出来,存放到hal_data.c当中,用户可以直接调用。
如果我们需要更小的误差,则需要在SEMR的BRME位(Bit Rate Modulation Enable)写1,使能比特率调制功能。 此时波特率和寄存器之间的公式见 图19_9。
图 19-9 使用比特率调制功能时的波特率和误差的计算公式
该公式与未使用比特率调制功能时的公式大同小异,不管是BRR setting还是Error,都只是在分母添加了个系数(256/M), M是8位寄存器MDDR的值,取值范围(128~256),这极大的降低了波特率的误差。例如:
同样是要求波特率为115200bps,但此时BGDM=1,ABCS=0,ABCSE=0,n=0,M=184,则此时N为:
当前误差Error为:
对比未使用波特率调制器时误差为0.47%,使用后为0.014%,误差降低到了原来的3%,效果可以说是相当的好,但是寻找效果最佳的M的值也是很繁琐的。 读者只需调用FSP库的R_SCI_UART_BaudCalculate函数,就能找到最适合的参数组。这个函数的源码在r_sci_uart.c,如果读者对其实现过程感兴趣, 可以翻看这个函数的源码。
19.5. 实验1:UART收发回显¶
UART只需两根信号线即可完成双向通信,对硬件要求低,使得很多模块都预留UART接口来实现与其他模块或者控制器进行数据传输, 比如GSM模块,WIFI模块、蓝牙模块等等。在硬件设计时,注意还需要一根“共地线”。
我们经常使用UART来实现控制器与电脑之间的数据传输。这使得我们调试程序非常方便,比如我们可以把一些变量的值、 函数的返回值、寄存器标志位等等通过UART发送到串口调试助手,这样我们可以非常清楚程序的运行状态, 当我们正式发布程序时再把这些调试信息去除即可。
我们不仅仅可以将数据发送到串口调试助手,我们还可以在串口调试助手发送数据给控制器,控制器程序根据接收到的数据进行下一步工作。
首先,我们来编写一个程序实现开发板与电脑通信,在开发板上电时通过UART发送一串字符串给电脑,然后开发板进入中断接收等待状态, 如果电脑有发送数据过来,开发板就会产生中断,我们在中断服务函数接收数据,并马上把数据返回发送给电脑。
19.5.1. 硬件设计¶
为利用 UART 实现开发板与电脑通信,需要用到一个USB转串口(UART)的芯片:CH340G。 CH340G 是一个USB总线的转接芯片,实现USB转UART、USB转lrDA红外或者USB转打印机接口,我们使用其USB转UART功能。 具体电路设计见 图19_10a、图19_10b、图19_10c。
在下面的三块开发板的电路图中,CH340G的TXD引脚与MCU芯片 UART 的RXD引脚连接, CH340G的RXD引脚与MCU芯片 UART 的TXD引脚连接。CH340G芯片集成在开发板上,其地线(GND)已与控制器的GND连通。
图 19-10a 启明6M5开发板:USB转串口硬件设计
图 19-10b 启明4M2开发板:USB转串口硬件设计
图 19-10c 启明2L1开发板:USB转串口硬件设计
开发板 |
调试串口引脚 |
---|---|
启明6M5 |
|
启明4M2 |
|
启明2L1 |
|
19.5.2. 软件设计¶
19.5.2.1. 新建工程¶
因为本章节的 UART 相关实验例程需要用到板子上的 LED 功能,因此我们可以直接以前面的“11_GPIO_LED”工程为基础进行修改。
- 对于 e2 studio 开发环境:
拷贝一份我们之前的 e2s 工程模板 “11_GPIO_LED”, 然后将工程文件夹重命名为 “19_UART_Receive_Send”,最后再将它导入到我们的 e2 studio 工作空间中。
- 对于 Keil 开发环境:
拷贝一份我们之前的 Keil 工程模板 “11_GPIO_LED”, 然后将工程文件夹重命名为 “19_UART_Receive_Send”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。
工程新建好之后,在工程根目录的 “src” 文件夹下面新建 “debug_uart” 文件夹, 再进入 “debug_uart” 文件夹里面新建源文件和头文件:“bsp_debug_uart.c” 和 “bsp_debug_uart.h”。 工程文件结构如下。
19_UART_Receive_Send
├─ ......
└─ src
├─ led
│ ├─ bsp_led.c
│ └─ bsp_led.h
├─ debug_uart
│ ├─ bsp_debug_uart.c
│ └─ bsp_debug_uart.h
└─ hal_entry.c
19.5.2.2. FSP配置¶
下面以启明6M5开发板为例,进行FSP配置,另外两块开发板的FSP配置过程与启明6M5开发板的配置过程基本上是一样的。
首先打开 “19_UART_Receive_Send” 项目的 FSP 配置界面进行配置。
在 FSP 配置界面里面点开 “Pins”-> “Peripherals”-> “Connectivity:SCI”-> “SCI4” 来配置SCI模块, 配置为 “Asynchronous UART” 模式,并选择开发板所使用的串口引脚,如 图19_11。
图 19-11 配置引脚
在配置界面底部点击 “Stack”,如 图19_12 步骤加入串口UART:
图 19-12 加入串口
如 图19_13 点击刚刚加入的窗口,在左下角的“属性”窗口中配置 名字(name)、通道(Channel)、回调函数(Callback)名字即可, 引脚(Pins)、波特率(Baud Rate)等其他的属性按照默认的配置即可。
图 19-13 配置串口属性
属性 |
描述 |
---|---|
Name |
名字,根据读者需求设置即可。 |
Channel |
通道,根据SCI号设置即可,例如实验使用SCI4,则这里配置为通道4。 |
Data Bits |
每个字(word)的比特(bit)数,默认为8bits |
Parity |
校验模式,可选择“Odd”奇校验,“Even”偶校验或“None”无校验。 |
Stop Bits |
停止位,可选1或2bit。 |
Baud Rate |
波特率 |
Baud Rate Modulation |
波特率调制,通过调整时钟周期,以减少申请波特率与实际波特率之间的误差。 |
Max Error(%) |
计算波特率时允许的最大百分比误差。 |
Callback |
回调函数的名字,根据读者需求设置即可。 |
Receive Interrupt Priority |
接收中断优先级 |
Transmit Data Empty Interrupt Priority |
发送数据空中断优先级 |
Transmit End Interrupt Priority |
发送完成中断优先级 |
Error Interrupt Priority |
错误中断优先级 |
使用 printf 函数时,需要使用到堆,默认情况下堆的大小为0,因此我们需要修改堆的大小。 可以在 FSP 配置界面中的“BSP”属性栏的“RA Common”中通过修改“Heap size”来设置堆区大小。 这里需要设置为 8 的整数倍,对于RA6M5推荐至少为4K(0x1000),如 图19_14。
图 19-14 修改堆的大小
最后点右上角的 “Generate Project Content” 按钮,让软件自动生成配置代码。
19.5.2.3. 串口初始化函数¶
FSP 配置并生成代码之后,首先需要使用 R_SCI_UART_Open 函数打开 SCI4 UART 模块, 我们把这层调用封装为一个 Debug_UART4_Init 函数,如下所示。
/* 调试串口 UART4 初始化 */
void Debug_UART4_Init(void)
{
fsp_err_t err = FSP_SUCCESS;
err = R_SCI_UART_Open (&g_uart4_ctrl, &g_uart4_cfg);
assert(FSP_SUCCESS == err);
}
19.5.2.4. R_SCI_UART_Write函数¶
串口初始化完成之后,我们可以直接使用 R_SCI_UART_Write 函数来将字符串写入到串口输出,该函数的原型如下。
fsp_err_t R_SCI_UART_Write (uart_ctrl_t * const p_api_ctrl, uint8_t const * const p_src, uint32_t const bytes)
参数 p_src 指向要写入的字符串首地址
参数 bytes 为传入的要写入的字符的数目
- 我们在使用 R_SCI_UART_Write 函数需要注意的一些事项:
若使用了 R_SCI_UART_Write() 来发送数据, 在数据发送完成之后会导致 uart_send_complete_flag 这个标志位被置位, 因此程序在调用 R_SCI_UART_Write 函数之后需要等待 uart_send_complete_flag 标志位被置位, 然后将该标志位清零。否则当连续调用 R_SCI_UART_Write 函数时可能导致发送数据丢失。 建议使用后文所述的 printf 函数将数据发送到串口。
19.5.2.5. 串口中断回调函数¶
在前面的 FSP 配置步骤的时候,我们设置了串口中断回调函数的名字为: debug_uart4_callback。 设置这么一个函数的原因是:每当串口发送或者接收完成一个字符时,都会默认触发串口的中断, 而在串口中断中会调用函数 debug_uart4_callback,在函数里我们需要根据不同的中断情况进行相应的处理。
因此,我们也需要同时在我们的代码里面定义并实现这么函数 debug_uart4_callback。 我们把这个函数放到文件“bsp_debug_uart.c”中,该函数代码如下所示。
其中,需要定义一个额外的标志变量 uart_send_complete_flag 来表示串口发送数据已完成。 变量 uart_send_complete_flag 必须加上volatile,否则可能被编译器优化。
/* 发送完成标志 */
volatile bool uart_send_complete_flag = false;
/* 串口中断回调 */
void debug_uart4_callback (uart_callback_args_t * p_args)
{
switch (p_args->event)
{
case UART_EVENT_RX_CHAR:
{
/* 把串口接收到的数据发送回去 */
R_SCI_UART_Write(&g_uart4_ctrl, (uint8_t *)&(p_args->data), 1);
break;
}
case UART_EVENT_TX_COMPLETE:
{
uart_send_complete_flag = true;
break;
}
default:
break;
}
}
19.5.2.6. 重定向printf输出到串口¶
虽然我们可以直接使用 R_SCI_UART_Write 函数来将字符串输出到串口, 但是这个函数在很多情况下没有 printf 函数那样方便。所以我们需要添加一段代码来将 printf 输出重定向到串口(UART4)。
我们将以下的代码添加到源文件“bsp_debug_uart.c”里面。 由于不同C库的 printf 函数的底层实现不同,这里使用条件编译选择我们需要重写的函数。
/* 重定向 printf 输出 */
#if defined __GNUC__ && !defined __clang__
int _write(int fd, char *pBuffer, int size); //防止编译警告
int _write(int fd, char *pBuffer, int size)
{
(void)fd;
R_SCI_UART_Write(&g_uart4_ctrl, (uint8_t *)pBuffer, (uint32_t)size);
while(uart_send_complete_flag == false);
uart_send_complete_flag = false;
return size;
}
#else
int fputc(int ch, FILE *f)
{
(void)f;
R_SCI_UART_Write(&g_uart4_ctrl, (uint8_t *)&ch, 1);
while(uart_send_complete_flag == false);
uart_send_complete_flag = false;
return ch;
}
#endif
在使用 e2 stdio 的时候,需要修改C语言项目设置,按下图步骤修改即可。
图 19-15 打开C/C++项目设置串口
图 19-16 修改项目工具设置
19.5.2.7. hal_entry入口函数¶
C语言程序的入口函数 main 函数调用了 hal_entry 函数。 我们在 hal_entry 函数里面编写我们的应用代码。
void hal_entry(void)
{
/* TODO: add your own code here */
LED_Init(); // LED 初始化
Debug_UART4_Init(); // SCI4 UART 调试串口初始化
printf("这是一个串口收发回显例程\r\n");
printf("打开串口助手发送数据,接收窗口会回显所发送的数据\r\n");
while(1)
{
LED1_ON;
R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_SECONDS);
LED1_OFF;
R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_SECONDS);
}
#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}
首先我们调用 LED_Init 函数初始化板子上的 LED 灯,然后调用 Debug_UART4_Init 函数初始化 SCI4 UART 作为调试串口来使用。 之后就可以使用 printf 函数了,我们调用 printf 输出提示信息到串口。 接着在 while 循环里是一段让 LED1 每隔一秒钟闪烁的程序。
19.6. 实验2:UART指令控制RGB灯¶
正如之前所讲,可以在串口调试助手输入指令,让开发板根据这些指令执行一些任务,现在我们编写程序让开发板接收UART数据,然后根据数据内容控制RGB彩灯的颜色。
19.6.1. 硬件设计¶
本实验使用到开发板的串口和LED灯,其原理图在本章实验1和前面的点亮LED灯章节有介绍,这里不进行赘述。
19.6.2. 软件设计¶
19.6.2.1. 新建工程¶
因为本节的 UART 实验例程与上一个实验例程的 FSP 配置以及 UART 相关的一些代码基本一致, 因此我们可以直接以前面的“19_UART_Receive_Send”工程为基础进行修改。
- 对于 e2 studio 开发环境:
拷贝一份我们之前的 e2s 工程模板 “19_UART_Receive_Send”, 然后将工程文件夹重命名为 “19_UART_LED_Control”,最后再将它导入到我们的 e2 studio 工作空间中。
- 对于 Keil 开发环境:
拷贝一份我们之前的 Keil 工程模板 “19_UART_Receive_Send”, 然后将工程文件夹重命名为 “19_UART_LED_Control”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。
FSP 配置完全一致,因此我们省略掉这部分。
19.6.2.2. 串口中断回调函数¶
我们需要在串口中断回调函数,也就是 debug_uart4_callback 函数里判断接收到的字符, 并根据所接收到的不同字符做出不同的操作。 修改 debug_uart4_callback 函数的代码,如下所示。
/* 串口中断回调 */
void debug_uart4_callback (uart_callback_args_t * p_args)
{
switch (p_args->event)
{
case UART_EVENT_RX_CHAR:
{
/* 根据字符指令控制RGB彩灯颜色 */
switch (p_args->data)
{
case '1':
LED1_ON;
break;
case '2':
LED2_ON;
break;
case '3':
LED3_ON;
break;
case '4':
LED1_OFF;
break;
case '5':
LED2_OFF;
break;
case '6':
LED3_OFF;
break;
case '7':
LED1_ON; LED2_ON; LED3_ON;
break;
case '8':
LED1_OFF; LED2_OFF; LED3_OFF;
break;
default:
break;
}
break;
}
case UART_EVENT_TX_COMPLETE:
{
uart_send_complete_flag = true;
break;
}
default:
break;
}
}
19.6.2.3. hal_entry入口函数¶
在 hal_entry 函数里面我们进行硬件初始化之后,首先打印提示信息,提醒用户从串口输入数字字符。 然后默认关闭所有 LED 灯,在 while 循环里什么都不做,等待用户的输入。
void hal_entry(void)
{
/* TODO: add your own code here */
LED_Init(); // LED 初始化
Debug_UART4_Init(); // SCI4 UART 调试串口初始化
printf("这是一个串口控制 LED 例程\r\n");
printf("打开串口助手发送以下指令,控制 LED 的状态\r\n");
printf ("\t指令 ------ 状态\r\n ");
printf ("\t 1 ------ LED1_ON\r\n ");
printf ("\t 2 ------ LED2_ON\r\n ");
printf ("\t 3 ------ LED3_ON\r\n ");
printf ("\t 4 ------ LED1_OFF\r\n ");
printf ("\t 5 ------ LED2_OFF\r\n ");
printf ("\t 6 ------ LED3_OFF\r\n ");
printf ("\t 7 ------ LED 全亮\r\n ");
printf ("\t 8 ------ LED 全灭\r\n ");
LED1_OFF; LED2_OFF; LED3_OFF; //默认关闭所有 LED 灯
while(1)
{
}
#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}
19.6.3. 下载验证¶
保证开发板相关硬件连接正确,用Type-C线连接开发板“USB TO UART”接口跟电脑, 在电脑端打开串口调试助手,把编译好的程序下载到开发板,此时串口调试助手即可收到开发板发过来的数据。 我们在串口调试助手发送区域输入一个特定字符,点击发送按钮,LED 灯状态随之改变。
19.7. 实验3:基于环形队列的UART收发回显¶
在实际项目开发中,由于有些串口不具备FIFO(如SCI1和SCI2)或FIFO的buffer比较小, 这可能会在数据处理速度小于数据接收速度的时候,导致数据的丢失。因此我们可以设计一个队列来避免这一问题。 在本实验中,我们使用环形队列来实现实验1的串口收发回显,将串口接收到的数据暂存在队列中, 待完成一次接收后再将队列中的数据全部发出去。
队列是一种特殊的线性表,只允许在队列头(head)删除元素,在队列尾(tail)添加元素。 当队列添加一个元素,队列尾向后移动,当队列删除一个元素,同样,删除一个元素,队列头向后移动,如 图19_18。
图 19-18 队列图示
由于存储空间是有限的,如果使用线性队列,删除元素后就会空出一段存储空间,这会造成很大的浪费。 因此实际上我们更多使用环形队列。并不是说这段存储空间是环形的,而是头指针和尾指针到达存储空间末尾后会回到存储空间起点。 因此在逻辑上这是循环的,如 图19_19。
图 19-19 环形队列
19.7.1. 硬件设计¶
本实验使用到开发板的串口和LED灯,其原理图在本章实验1和前面的点亮LED灯章节有介绍,这里不进行赘述。
19.7.2. 软件设计¶
19.7.2.1. 新建工程¶
因为本节的 UART 实验例程与上一个实验例程的 FSP 配置以及 UART 相关的一些代码基本一致, 因此我们可以直接以前面的“19_UART_Receive_Send”工程为基础进行修改。
- 对于 e2 studio 开发环境:
拷贝一份我们之前的 e2s 工程模板 “19_UART_Receive_Send”, 然后将工程文件夹重命名为 “19_UART_Circular_Queue”,最后再将它导入到我们的 e2 studio 工作空间中。
- 对于 Keil 开发环境:
拷贝一份我们之前的 Keil 工程模板 “19_UART_Receive_Send”, 然后将工程文件夹重命名为 “19_UART_Circular_Queue”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。
FSP 配置的步骤完全一致,因此我们省略掉这部分。
19.7.2.2. 环形队列的实现¶
#define DATA_LEN 300 //队列缓存大小
typedef struct
{
uint16_t head; //头指针
uint16_t tail; //尾指针
uint8_t data[DATA_LEN]; //队列数据
} Circular_queue_t;
extern Circular_queue_t Circular_queue; //环形队列全局变量
bool Queue_Init(Circular_queue_t *circular_queue); //初始化队列
bool Queue_isEmpty(Circular_queue_t *circular_queue); //判断队列是否为空
bool Queue_isFull(Circular_queue_t *circular_queue); //判断队列是否已满
bool Queue_Wirte(Circular_queue_t *circular_queue, uint8_t *string, uint16_t len); //写数据
bool Queue_Read(Circular_queue_t *circular_queue, uint8_t *string, uint16_t len); //读数据
uint16_t Queue_HadUse(Circular_queue_t *circular_queue); //返回队列中数据的长度
uint16_t Queue_NoUse(Circular_queue_t *circular_queue); //返回未使用数据的长度
环形队列相关函数的具体定义可以在例程的 circular_queue.c 中查看,这里就不赘述了。
19.7.2.3. 串口中断回调函数¶
debug_uart4_callback 串口中断回调函数的内容修改成如下所示。
/* 串口中断回调 */
void debug_uart4_callback (uart_callback_args_t * p_args)
{
switch (p_args->event)
{
case UART_EVENT_RX_CHAR:
{
/* 接收到数据后马上写入队列中 */
Queue_Wirte(&Circular_queue, (uint8_t*) &p_args->data, 1);
break;
}
case UART_EVENT_TX_COMPLETE:
{
uart_send_complete_flag = true;
break;
}
default:
break;
}
}
19.7.2.4. hal_entry入口函数¶
hal_entry 入口函数的内容修改成如下所示。
void hal_entry(void)
{
/* TODO: add your own code here */
uint8_t Read_Buffer[DATA_LEN];
uint16_t Read_Length;
LED_Init(); // LED 初始化
Debug_UART4_Init(); // SCI4 UART 调试串口初始化
Queue_Init((Circular_queue_t*)&Circular_queue); //环形队列初始化
printf("这是一个串口环形队列例程\r\n");
printf("打开串口助手发送数据 5 个及以上的数据,接收窗口会打印所发送的数据\r\n");
while(1)
{
if (Queue_isEmpty(&Circular_queue) == false) //判断队列中的数据不为空
{
Read_Length = Queue_HadUse(&Circular_queue);
if( Read_Length >= 5) // 如果队列中的数据大于等于5个,开始打印队列中的所有数据
{
printf("Read_Length=%d: ", Read_Length);
memset(Read_Buffer, 0, DATA_LEN);
/* 读出 Read_Length 个数据 */
Queue_Read(&Circular_queue, Read_Buffer, Read_Length);
printf("%s\r\n", Read_Buffer);
}
}
R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_MILLISECONDS);
}
#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}
19.7.3. 下载验证¶
保证开发板相关硬件连接正确,用Type-C USB线连接开发板“USB TO UART”接口跟电脑。本次实验需要使用到串口调试助手, 配置好串口参数并打开串口后,在调试助手的发送区域输入超过5个的任意字符并点击发送,即可在接收区看见返回字符, 不一定马上全部返回全部的已发送的字符,可以多发送几次数据观察,数据并没有丢失。
19.8. UART在5.2.0版本E2S中的重定向¶
19.8.1. 重定向失效原因¶
随着FSP库版本更新到5.2.0,在开发过程中就会发现4.0.0版本的串口重定义并不适用于最新版本。继续使用原来的重定向代码时,编译器就会报错。 根据报错的信息发现,缺少了几个函数的定义,这里补全定义即可。
图 19-20 串口重定义编译器报错
19.8.2. 重定向代码¶
Uart重定向函数函数的内容修改成如下所示。
#if defined __GNUC__ && !defined __clang__
int _write(int fd, char *pBuffer, int size); //防止编译警告
int _read(int fd, char *pBuffer, int size);
/* 重定向 printf 输出 */
int _write(int fd, char *pBuffer, int size)
{
(void) fd;
R_SCI_UART_Write (&g_uart0_ctrl, (uint8_t*) pBuffer, (uint32_t) size);
while (uart_send_complete_flag == false)
;
uart_send_complete_flag = false;
return size;
}
/* 重定向scanf函数 */
int _read(int fd, char *pBuffer, int size)
{
(void) fd;
R_SCI_UART_Read (&g_uart0_ctrl, (uint8_t*) pBuffer, (uint32_t) size);
while (uart_receive_complete_flag == false)
;
uart_receive_complete_flag = false;
/* 回显 */
R_SCI_UART_Write (&g_uart0_ctrl, (uint8_t*) pBuffer, (uint32_t) size);
return size;
}
#else
int fputc(int ch, FILE *f)
{
(void)f;
R_SCI_UART_Write(&g_uart0_ctrl, (uint8_t *)&ch, 1);
while(uart_send_complete_flag == false);
uart_send_complete_flag = false;
return ch;
}
#endif
Uart重定向头文件修改如下所示。
#ifndef __BSP_DEBUG_UART_H
#define __BSP_DEBUG_UART_H
#include "hal_data.h"
#include "stdio.h"
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
void Debug_UART0_Init(void);
#endif
补全缺失函数如下所示。
/* 函数声明 防止编译器警告 */
int _isatty(int fd);
int _close(int fd);
int _lseek(int fd, int ptr, int dir);
int _fstat(int fd, struct stat *st);
__attribute__((weak)) int _isatty(int fd)
{
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
return 1;
errno = EBADF;
return 0;
}
__attribute__((weak)) int _close(int fd)
{
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
return 0;
errno = EBADF;
return -1;
}
__attribute__((weak)) int _lseek(int fd, int ptr, int dir)
{
(void) fd;
(void) ptr;
(void) dir;
errno = EBADF;
return -1;
}
__attribute__((weak)) int _fstat(int fd, struct stat *st)
{
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
{
st->st_mode = S_IFCHR;
return 0;
}
errno = EBADF;
return 0;
}