1. 串口通信基本概念
1)通信基础概念
A. 电平信号 & 差分信号
电平信号 & 差分信号用于说明信号线上如何表示数据0和1
① 电平信号
电平信号中有一根电平参考线(一般为GND),信号线上的数据值由信号线和参考线上的电压差决定
② 差分信号
差分信号中没有参考电平,都是信号线,只是2路信号振幅相同/相位相同/极性相反。信号线上的数据值由信号线之间的电压差决定。
说明:电平信号的2根通信线之间的电平差值容易受到干扰;差分信号抗干扰能力较强(尤其是共模干扰),因此传输质量比较稳定。
B. 串行通信 & 并行通信
① 串行通信
计算机与IO设备之间数据传输的
各位按顺序依次传送
,通常数据在一根数据线或一对差分线上传输。
一般特点:速度慢/成本低/适合远距离传输
② 并行通信
计算机与IO设备之间通过
多条传输线
交换数据,
数据各位同时传送
。
一般特点:速度快/成本高/适合近距离传输
注意1:一些差分串行通信总线(e.g. RS485/RS422/USB)抗干扰能力强,传输距离远,速度也比较快~~
注意2:CPU内部均是采用并行传输(数据总线)
C. 异步串行通信 & 同步串行通信
① 异步串行通信
特点:
a.
以字符为单位
传送信息
b. 相邻2个字符之间的间隔为任意长
c.
因为一个字符的比特位长度有限,所以接收时钟和发送时钟只要相近即可
(即发送方和接收方可以各自维护时钟)
d.
字符间异步,字符内同步
数据格式:
a. 1位起始位,规定为低电平(空闲时为高电平,将信号线拉低表示起始位)
b. 5 ~ 8位数据位,即要传送的有效信息,一般设置为8位。从LSB开始传输,靠时钟定位。
c. 1位奇偶校验位,可以构成奇校验(使“1”的位数为奇数)或偶校验(使“1”的位数为偶数)
d. 1 ~ 2位停止位,规定为高电平
② 同步串行通信
特点:
a. 以数据块为单位
b. 在一个数据块(信息帧)内,字符与字符之间无间隔
c.
因为一次传输的数据块中数据较多,所以接收时钟和发送时钟必须严格同
步,通常要有同步时钟。
d. 字符内部与字符之间均同步
说明:经过多年发展,最终胜出的是
异步/串行/差分
通信,如USB和网络通信
2)串口通信格式
串口通信的基本特点:异步/串行/电平信号
A. RS232电平和TTL电平
① RS232电平:
逻辑1:-3 ~ -15V
逻辑0:+3 ~ +15V
② TTL电平:
逻辑1:+3.5 ~ +5V
逻辑0:< 0.4V
③ 使用场景:
RS232电平幅值范围较大,适合干扰大/距离远的情况。原先PC机以及工业应用中,均使用RS232串口
TTL电平赋值范围较小,适合干扰小/距离近的情况。TTL电平一般用在电路板内部2个芯片之间
④ 电平转换
X210开发板(包括OK210开发板)使用管脚引出的串口均为TTL电平;同时也使用了TTL转RS232芯片,并使用DB9接口引出。
注意:串口电路中仅使用了DB9接口中的Tx/Rx接口(同时GND接地),其余接口并未使用(也就是说,流控及其他功能均无法使用)
说明:编程时并不考虑电平标准
B. 波特率
① 波特率和比特率
波特率:信号变化的次数(即每秒采样的次数),也称为码元速率(即得到一个信号的频率)
比特率:数据传输速率,单位为bps(bit per second)
② 波特率和比特率的关系
如果信号分为两级:0、1,那么一次信号变化可表示1bit,所以比特率 = 波特率
如果信号分为八级:0 ~ 7,那么一次信号变化可表示3bit,所以比特率 = 3 * 波特率
因此,如果信号分为v级,则比特率 = log
2
v
* 波特率
上图中两个信号的波特率相同(即信号到达的速率相同,竖虚线等宽),但下面的信号每次携带2 bit(信号分为四级)信息,所以比特率是上面信号的2倍
③ 串口波特率
由于在串口中信号只分为2级,所以比特率 = 波特率,也就是串口通信时每秒可以传输多少个二进制位。
由于串口通信属于异步传输,所以收发双方各自维护波特率(也就是串口通信的时钟)
C. 数据帧构成
串口通信按帧传输,每个周期由起始位 + 数据位 + 奇偶校验位 + 停止位构成。具体格式见上文异步串行通信部分。
2. S5PV210 UART模块解析
1)概述
① UART模块负责CPU和UART之间的数据传输(包含了串并转换和时序控制)
② S5PV210共有4路UART接口,且均可基于中断/DMA工作
③ 最高支持3Mbps速率
④ 每条UART通道包含2条FIFO(分别用于收/发),其中ch0 - 256B/ch1 - 64B/ch2 - 16B/ch3 - 16B
⑤ ch0 - ch3支持红外模式
⑥ ch0 - ch2支持自动流控(AFC)
2)构成部件
A. Buad-rate Generator
波特率发生器使用PCLK或SCLK_UART分频生成所需的波特率
在我们的示例中,使用PCLK_PSYS作为UART模块的时钟源
B. Trsansmitter
发送部件由
发送FIFO
和
发送移位器
组成,在非FIFO模式下,相当于只用FIFO的一个字节。
程序将要发送的数据写入Tx FIFO后,将自动拷贝到Tx shifter并逐位传输到TxD引脚。
C. Receiver
接收部件由
接收FIFO
和
接收移位器
组成,在非FIFO模式下,相当于只用FIFO的一个字节。
从RxD引脚接收到的数据逐位移入Rx shifter,然后自动拷贝到Rx FIFO中。
说明:通过FIFO和shifter的配合,实现了UART的串并转换(CPU内部数据传输均为并行)
D. Control unit
控制整个UART收发流程
3)自动流控(Auto Flow Control,AFC)
A. 概述
① 流控的目的是让串口通信可靠,在发送方速率比接收方处理能力强时,流控可以确保不丢帧。
当前不使用流控,是因为串口主要用于输出调试信息,作为接收方的PC,处理能力远比SoC强。
② S5PV210的UART0和UART1支持AFC;如果将TxD3/RxD3引脚设置为nRTS2/nCTS2功能,则UART2也支持AFC(当然,此时UART3无法使用~~)
B. 实现
S5PV210为实现AFC,需要用到RTS/CTS引脚,其中
RTS为output
,
CTS为input
① 发送数据
在AFC中,nCTS信号表示
对端UART的接收FIFO已准备好接收数据
。所以当发送端nCTS信号生效时,可以将数据写入自身发送FIFO(传输则由Control Unit控制完成)
② 接收数据
在AFC中,nRTS信号表示
自身接收FIFO准备好接收数据
。所以在接收端接收数据前,需要使能nRTS信号
说明:根据上文分析,通信两端的CTS/RTS信号交叉连接,整个流控过程实际是由接收端控制(因为只有RTS信号是输出信号)。
当接收端接收FIFO无法继续接收数据时,使得RTS信号失效,则发送端无法收到CTS信号,所以不再继续发送。
当接收端接收FIFO出现空闲,可以继续接收时,将RTS信号使能,则发送端收到CTS信号,将继续发送数据。
在S5PV210中,RTS和CTS均是低电平有效,所以拉低RTS使能该信号时,对端连接的CTS也将生效。(注意寄存器值与高低电平的关系~~)
注意:如果将S5PV210的UART与Modem连接,需要禁止UMCONn寄存器的AFC位,并且通过软件控制nRTS信号
4)中断与DMA请求
A. UART的7种状态
在S5PV210中,UART共有7种状态,分别记录在UTRSTAT和UERSTAT寄存器种。可以分为3类:
① Tx
Tx buffer empty / Tx shifter empty
② Rx
Rx buffer data ready
③ Error
Overrun error:缓冲区溢出错误,即新数据已经覆盖旧的未读数据
Parity error:奇偶校验错误
Frame error:帧错误,即收到的数据没有有效的停止位
Break condition:UART发送端可以产生break信号,将串口输出状态强制为logic 0 state,并保持一个数据帧的长度(后文可知,发出break信号有寄存器可供设置)。如果break信号超过一个数据帧的长度,接收端将检测到break condition。
B. 中断产生列表
Type
|
FIFO Mode
|
Non-FIFO Mode
|
Rx interrupt
|
① Rx FIFO数据量
>=
Rx FIFO阈值
② Rx FIFO数据量 < Rx FIFO阈值,同时
3个字长时间没有收到数据(
处理尾字节
)
|
Rx holding register中有数据
|
Tx interrupt
|
Tx FIFO数据量
<=
Tx FIFO阈值
|
Tx holding register中无数据
|
Error interrupt
|
frame error/parity error/break condition/
overrun error触发。且当多个错误同时发生
时,只触发一次中断
|
同FIFO Mode
|
注意事项:
① 产生Tx/Rx中断的配置
UART工作在中断或轮询模式 && 中断mask寄存器对应位打开
② 产生Error中断的配置
使能Rx Error中断 && 中断mask寄存器对应位打开
③ 中断的共享与识别
S5PV210中,每个UART只有一个中断源,所以如果同时使能UART的多种中断,需要在ISR中进行识别。
④ Tx中断注意点
根据Tx中断的触发条件,只要Tx FIFO数据量 <= Tx FIFO阈值就会触发,因此
建议先向Tx FIFO中写入数据再使能Tx中断
。
⑤ 由于S5PV210的中断控制器为电平触发,因此UART的中断信号类型只能设置为电平模式
C. UART DMA请求
① 如果设置UART工作在DMA模式,那么在上文中产生Tx/Rx中断的条件下就不会产生中断,而是产生DMA请求
② DMA模式一般配合FIFO使用(否则意义不大~~)
③ DMA burst size可设置为1B或4B,
建议FIFO阈值与DMA burse size匹配
④ DMA传输会一直持续到满足FIFO阈值
5)错误状态FIFO
A. 概述
① 错误状态FIFO用于标识Rx FIFO中哪个数据是错误的及其错误类型
② 每个错误状态FIFO记录一种错误,并与Rx FIFO的位置对应
B. 注意事项
① 如果使能了Error中断,只有当错误数据被读取时(通过读取URXHn寄存器)才会触发中断
② 为清除错误FIFO中的状态,需要同时读取URXHn和UERSTATn寄存器
6)红外模式
① UART的红外模式需要在串口之外结合红外编解码器实现
② 红外模式的通信的原理是发送方在固定时间间隔向接收方发送红外信号(表示1)或者不发送红外信号(表示0)
当串口启用IrDA模式,我们向串口写数据时,这些数据就以红外光的方式向外发射出去。
3. S5PV210 UART程序设计
1)核心寄存器解析
A. ULCONn
ULCONn(UART Line Control)寄存器主要用于设置数据帧格式,一般设置为:
8位数据位 + 无奇偶校验 + 1位停止位
B. UCONn
UCONn(UART Control)寄存器主要用于设置UART的工作模式,需要注意以下字段:
① Tx/Rx/Error中断使能设置(注意,Tx/Rx的中断类型只能是电平)
② 此处选择PCLK_PSYS作为UART的时钟源
③ 注意Rx time-out中断,该中断就是用于处理尾字节,类型为Rx中断
C. UFCONn & UFSTATn
UFCONn(UART FIFO Control)寄存器主要用于使能FIFO及设置Tx/Rx FIFO触发中断的阈值。
注意不同channel的UART FIFO深度不同~~
UFSTATn(UART FIFO Status)寄存器表征了当前FIFO的状态,主要关注Tx/Rx FIFO中的数据量,在使能FIFO和中断的情况下,需要在ISR中根据该寄存器读写相应数量的数据。
D. UTRSTATn & UERSTATn
上文中说明的UART的7种状态就保存在这2个寄存器中
UTRSTATn(UART Tx/Rx Status)寄存器标识Tx/Rx相关状态,需要注意以下字段:
① Transmitter empty是指Tx buffer和Tx shifter均为空
② Transmit buffer empty仅指Tx buffer为空,在Non-FIFO模式中,此时就触发中断或DMA请求
③
当使能FIFO时,应该检查UFSTAT寄存器而不是该寄存器
UERSTATn(UART Error Status)寄存器标识Rx过程中发生的各种错误。
注意:读取UERSTATn寄存器就可将相关标志位自动清零
E. UTXHn & URXHn
UTXHn(UART Transmit Buffer[Holding Register & FIFO register])和URXHn(UART Receive Buffer Register[Holding Register & FIFO register])是读写串口数据时实际操作的寄存器。
在Non-FIFO模式下,就是Holding Register;在FIFO模式下,则是FIFO register
F. UBRDIVn & UDIVSLOTn
UBRDIVn(UART Channel Baud Rate Division)和UDIVSLOTn(UART Channel Dividing Slot)寄存器共同实现波特率的设置。其中UBRDIVn负责主分频,UDIVSLOTn用于辅助校准,以提高波特率的精度。
计算公式如下:
UBRDIVn = (PCLK / (bps * 16)) - 1的整数部分
UDIVSLOTn = (PCLK / (bps * 16)) - 1的小数部分 * 16 的整数部分构造而来
在本示例中,PCLK = 66.7MHz,波特率设置为115200,因此
UBRDIVn = (66700000 / (115200 * 16)) - 1 = 35.18的整数部分,所以UBRDIVn寄存器值为35
UDIVSLOTn = 0.18 * 16 = 2.88 ≈ 3,即UDIVSLOTn寄存器的值中必须有3个1,为确保串口工作的稳定性,可以根据手册查表得到。
注意事项:
① 当选择PCLK_PSYS为时钟源时,UBRDIVn的值必须 >= 0
② 当选择SCLK_UART为时钟源时,计算方法与上文相同,但是UBRDIVn的值可以为0(即在时钟模块已经分频完毕)。当UBRDIVn的值为0时,波特率不受UBRDIVn和UDIVSLOTn寄存器的影响
G. UINTMn & UINTSPn & UINTPn
UINTMn(UART Interrupt Mask)寄存器用于
屏蔽指定的中断
,当某位置1时即使对应的中断情况发生,也不会触发中断。
UINTSPn(UART Interrupt Source Pending)寄存器记录了中断源请求的情况,即使在UINTMn寄存器中屏蔽了某个中断,如果该中断发生,依然会将UINTSPn中的对应位置1。
注意:UINTSPn寄存器只是反映中断源的原始状态,不需要清pend
UINTPn(UART Interrupt Pending)寄存器是真正的中断源请求寄存器,在UART的ISR中
需要清pend;清pend的方式是向对应位写1
。
说明:UINTMn/UINTSPn/UINTPn寄存器的关系如下图所示
2)串口初始化&收发数据代码解析
A. 串口初始化代码解析
串口初始化代码需要完成以下步骤:
① 设置UART对应的GPIO管脚模式
UART0对应的GPIO管脚由GPA0 group管理
② 设置UART模块相关寄存器
ULCON:8位数据位 + 无奇偶校验 + 1位停止位
UCON:收发采用中断或轮询模式
UMCON:禁用modem和AFC相关(使用默认值即可)
UFCON:暂时不用FIFO(使用默认值即可)
UFCON/UDIVSLOT:根据波特率计算
B. 串口收发函数解析
使用轮询方式实现数据收发,就是不断查询UTRSTATn寄存器对应位的值,并据此执行相关操作。
说明:
① 使用轮询方式实现收发能很好地和printf/scanf函数配合(尤其是在当前没有操作系统的环境下)。
② 此处getc函数中没有进行回显,是因为回显实现在了scanf函数中,可参见下文相关内容。
3)基于中断的Rx代码解析
A. 中断初始化
中断的整个流程涉及中断源 + VIC + CPU三个部分,
① 中断源
在中断源端使能中断
② VIC
注册ISR + 在VIC端使能中断
③ CPU
为IRQ流程准备环境,包括将CPSR中的I/F位清零、设置IRQ栈、中断源的识别与ISR的调用等
B. ISR
ISR需要完成2个任务,
① 实际的中断业务流程
② 在中断源端清pend(VIC端清pend由总的中断处理程序完成)
4. printf移植解析
1)什么是stdio
① stdio(standard input output),即标准输入输出
② 标准输入输出是操作系统定义的默认的输入和输出通道。一般在PC中,标准输入是键盘,标准输出是屏幕。在操作系统中可以定义标准输入/输出/错误流。
在我们的程序中,标准输入/输出就是串口。
③ printf/scanf函数可以和底层的输入/输出函数绑定,然后这两个函数就可以和stdio绑定起来。
2)C语言对可变参函数的支持
所需头文件:
头文件提供的工具可以使我们能够编写可变数量参数的函数,下面结合示例加以说明。
运行结果:
提供了
1种类型和4个宏
用于处理可变参数列表,
va_list类型
:用于处理可变参数列表的类型,在实现中一般就是char *类型
va_start宏
:指出参数列表中可变长度部分开始的位置。带有可变数量参数的函数必须至少有一个“正常的”形式参数,省略号总是出现在形式参数列表的末尾,在最后一个“正常”参数之后。
实现中就是将参数列表中可变长度部分的起始地址赋值给va_list类型的变量,同时注意4B对齐,具体原因后文会有论述。
va_end宏
:用于对可变参数列表的“清理”,实现中可以什么都不做~~
va_start和va_end宏必须成对调用
va_arg宏
:用于遍历可变参数列表,可以根据指定的类型取出对应的参数。
其中_bnd宏就是用于实现4B对齐(其实是按int型大小对齐)
特别注意:每从可变参数列表中取出一个参数,ap会指向下一个参数
va_copy宏
:va_copy(va_list dest, va_list src)在C99中提出,用于拷贝可变参数列表
va_copy也必须和va_end成对使用
说明:在可变参数函数中,需要根据“正常”参数解析可变参数列表(e.g. printf函数中的格式字符串)
特别注意:关于参数类型提升
当调用带有可变参数列表的函数时,
编译器会在省略号对应的所有参数上执行默认的实际参数提升
。e.g. char类型和short类型会被提升为int类型,float类型会被提升为double类型。因此把char、short或float类型作为参数传递给va_arg是没有意义的,提升后的参数不可能具有这些类型。
从实现中分析,从可变参数列表中取出参数时均要求4B对齐,因此在示例中处理字符时也是按int类型取出参数,然后强制类型转换为char类型。
3)移植printf/scanf函数
A. 移植代码来源
① 最原始来源:Linux内核中的printk函数
② 简化方法:从uboot中移植(也是从linux中得来)
③ 最简单方法:直接使用别人移植好的~~(源自友善之臂裸机代码)
B. printf/scanf工程的组织和编译
将printf/scanf工程相关文件单独编译成静态库供整个工程链接使用,需要注意在CFLAGS变量中添加如下编译选项:
-nostdinc:不使用标准头文件
-fno-builtin:不使用内建函数
在C语言标准中,有些通用函数被定义为built-in function(内建函数),像printf/strchr/memset等等,这些函数不需要包含头文件中的声明,就可以编译链接该函数。增加-fno-builtin选项后,就可以自己实现这些函数,而不会导致冲突。
-nostdlib:不连接系统标准启动文件和标准库文件,只把指定的文件传递给连接器。这个选项常用于编译内核、bootloader等程序,它们不需要启动文件、标准库文件。
4)printf/scanf函数工作原理
A. printf函数工作原理
① 定义发送缓存区g_pcOutBuf
② 调用va_start宏取出可变参数列表
③ 调用vsprintf函数,根据格式字符串fmt解析可变参数列表,并将解析结果输出到发送缓冲区
④ 调用putc函数(此处为串口输出函数)将发送缓冲区中的内容输出到串口
说明:printf函数调用链
printf --> vsprintf --> vsnprintf
在vsnprintf函数中会根据格式字符串解析可变参数列表,并调用va_arg宏逐个取出参数并打印到发送缓存区中。
B. scanf函数工作原理
① 定义接收缓冲区
② 从串口接收数据直到遇到换行(0x0a)或回车(0x0d),此时将接收缓冲区截零(且换行和回车符并不放入接收缓冲区)
③ 调用va_start宏取出可变参数列表
④ 调用vsscanf函数,根据格式字符串解析可变参数列表,并从g_pcInBuf中取出要赋给变量的值,然后向设定的地址赋值。
需要注意的是,从g_pcInBuf中取出的“值”都是字符串形式,需要转换后才能赋值。
5. 自建bootloader超过BL1长度限制的解决方案
S5PV210在启动过程中对BL1的长度有16KB的限制,但是随着片内外设使用的增加,尤其是相关库的引入(e.g. printf/scanf),目前自建的bootloader已经超过16KB的限制。
为实现程序的正确下载/烧写与运行,有如下2种解决方案:
1)dnw下载
先用dnw下载
x210_usb.bin
,
该文件可以完成内存初始化等操作
,这样就可以将自建bootloader下载到内存运行。
注意:经上机验证,通过dnw下载到iRAM中运行的代码可以超过16KB。根据iRAM的内存布局,Reserved部分其实可供自建bootloader下载运行。
特别注意:重复初始化内存问题
问题现象:先将x210_usb.bin下载到0xd0020010,再将key_interrupt.bin(自建bootloader)下载到0x20000000,程序没有正确运行。
原因分析:x210_usb.bin会初始化内存,而自建bootloader中也会初始化一次内存,第二次初始化会使得下载到其中的bootloader失效,所以无法运行。
解决方案1:直接禁用reset流程中的内存初始化
经过测试,reset流程中不再调用sdram_asm_init函数初始化内存后,可以将自建bootloader下载到内存中运行(当然,此前要先下载x210_usb.bin)。
需要注意的是,如果下载的内存地址与链接地址(目前为0x20000000)不符,仍需要做重定位。但是要注意其中的一个小限制,为避免异常错误,如果下载地址与链接地址不符,则需要间隔一个bootloader镜像的长度(即需要避免拷贝目标地址与源地址区域的重叠)。
举个例子,如果自建bootloader链接地址为0x20000000,文件大小为20KB。如果下载地址为0x20000010,会导致异常错误。因为此时重定位相当于把文件整体向前移动16B,而目标地址与源地址区域的重叠会导致拷贝过程中程序就跑飞~~
解决方案2:将内存初始化加入code_relocate子过程
其实这个方案是有逻辑小漏洞的~~需要注意的问题有2个,
①
重定位的条件是运行地址和链接地址不匹配;调用sdram_asm_init的条件是内存尚未初始化。
其中的逻辑漏洞在于,如果此时自建bootloader就是运行在内存中,但是运行地址和链接地址不匹配,此时仍会进行内存初始化,还是会导致问题。也就是说,并不是需要重定位的情况都需要内存初始化。
解决方法就是,确保自建bootloader的下载地址与链接地址一致
② 不能在code_relocate中简单调用bl sdram_asm_init,因为code_relocate为非叶子函数,再次调用bl时,必须将当前LR的内容入栈,否则无法返回code_relocate的上级函数。
个人:对于自建bootloader大于16KB的情况,建议仍使用dnw下载(SD卡烧写插拔太尼玛麻烦~~)。对于下载x210_usb.bin后下载自建bootloader,建议直接禁用重复的内存初始化即可~~
2)SD卡烧写
由于mkv210_image.c代码中对BL1镜像限制在16KB,因此可以
将自建bootloader工程拆分为2部分
。第一部分在16KB以内,第二部分放置剩余部分(存储在SD卡后面的某个扇区中,比如第45个扇区)。然后在第一部分中进行重定位(从SD卡中重定位到内存中,使用S5PV210提供的存储设备操作函数可以完成)。
BL1的一般任务:
① 环境初始化,尤其是内存初始化。
BL1是由CPU在启动过程中自动从SD卡加载到iRAM中运行,需要为BL2的运行准备环境。
其实BL2有2个运行场所,如果BL2 < 80KB则可以加载到iRAM中运行;如果BL2 > 80KB则只能加载到内存中运行。
虽然S5PV210手册中推荐使用iRAM运行BL2,但是Uboot在实现过程中一般直接使用内存运行BL2,我们也采取这种方式。
② 将BL2从SD卡加载到内存中并启动运行
考虑到地址相关操作的影响,BL2的链接地址需要和加载的地址匹配(当然不匹配也可以,就是再在内存中进行一次重定位,但是这么做就木有什么意义了~~)。