ARM裸机之串口通信

 

这里记录了通讯的几个基础概念、串口通信的原理、以及串口的编程实践

这里记录了通讯的几个基础概念、串口通信的原理、以及串口的编程实践

ARM裸机之串口通信

通信涉及的几个基础概念

通信的发展历史

  • 最早通信:烽火台、狼烟;信件;电子通信(电报、电话、网络信号)
  • 通信中最重要的两个方面:信息表示、解析方法 + 信息的传输方法
  • 通信双方事先需要约定好信息的表示方法和解析方法,做到一致,否则信息不能有效传递
  • 信号的传输方法是指经过编码后的通信信息如何在传输介质上传输的过程。

通信过程其实分为3个步骤:

  1. 首先发送方先按照信息编码方式对有效信息进行编码(编程成可以在通信线路上传输的信号形态),
  2. 然后编码后的信息在传输介质上进行传输,输送给接收方;
  3. 最后接收方接收到编码信息后进行解码,解码后得到可以理解的有效信息。

同步通信和异步通信

同步和异步的区别

简单来说就是发送方和接收方按照同一个时钟节拍工作就叫同步,发送方和接收方没有统一的时钟节拍、而各自按照自己的节拍工作就叫异步。

同步通信中,通信双方按照统一节拍工作,所以配合很好;一般需要发送方接收方发送信息同时发送时钟信号,接收方根据发送方给它的时钟信号来安排自己的节奏。同步通信用在通信双方信息交换频率固定,或者经常通信时。

异步通信又叫异步通知。在双方通信的频率不固定时(有时3ms收发一次,有时3天才收发一次)不适合使用同步通信,而适合异步通信。异步通信时接收方不必一直在意发送方,发送方需要发送信息时会首先给接收方一个信息开始的起始信号,接收方接收到起始信号后就认为后面紧跟着的就是有效信息,才会开始注意接收信息,直到收到发送方发过来的结束标志。

电平信号和差分信号

电平信号差分信号是用来描述通信线路传输方式的。也就是说如何在通信线路上表达1和0.

电平信号的传输线中有一个参考电平线(一般是GND),然后信号线上的信号值是由信号线电平和参考电平线的电压差决定。

差分信号的传输线中没有参考电平,所有都是信号线。然后1和0的表达靠信号线之间的电压差。

总结:

  • 电平信号的2根通信线之间的电平差异容易受到干扰,传输容易失败;差分信号不容易受到干扰因此传输质量比较稳定,现代通信一般都使用差分信号,电平信号几乎没有了。
  • 看起来似乎相同根数的通信线下,电平信号要比差分信号要快;但是实际还是差分信号快,因为差分信号抗干扰能力强,因此1个发送周期更短。

并行接口和串行接口

  • 串行、并行主要是考虑通信线的根数,就是发送方和接收方同时可以传递的信息量的多少
    • 电平信号下,1根参考电平线+1根信号线可以传递1位二进制;如果我们有3根线(2根信号线+1根参考线)就可以同时发送2位二进制;如果想同时发送8位二进制就需要9根线。
    • 差分信号下,2根线(彼此差分)可以同时发送1位二进制;如果需要同时发送8位二进制,需要16根线。
  • 串行接口用的比较广。因为更省信号线,而且对传输线的要求更低、成本更低;而且串行时可以通过提高通信速度来提高总体通信性能,不一定非得要并行。

串口通信的特点

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

RS232电平和TTL电平

电平信号是用信号线电平减去参考线电平得到电压差,这个电压差决定了传输值是1还是0。电平信号多少V代表1,多少V代表0不是固定的,取决于电平标准。譬如RS232电平中-3V~-15V表示1;+3~+15V表示0;TTL电平则是+5V表示1,0V表示0.

不管哪种电平都是为了在传输线上表示1和0.区别在于适用的环境和条件不同。RS232的电平定义比较大,适合干扰大、距离远的情况;TTL电平电压范围小,适合距离近且干扰小的情况。

我们台式电脑后面的串口插座就是RS232接口的,在工业上用串口时都用这个,传输距离小于15米;TTL电平一般用在电路板内部两个芯片之间。

波特率

波特率(bandrate),指的是串口通信的速率,也就是串口通信时每秒钟可以传输多少个二进制位。譬如每秒种可以传输9600个二进制位(传输一个二进制位需要的时间是1/9600秒,也就是104us),波特率就是9600.

串口通信的波特率不能随意设定,而应该在一些值中去选择。一般最常见的波特率是9600或者115200(低端单片机如51常用9600,高端单片机和嵌入式SoC一般用115200).

为什么波特率不可以随便指定?主要是因为:

  • 第一,通信双方必须事先设定相同的波特率这样才能成功通信,如果发送方和接收方按照不同的波特率通信则根本收不到,因此波特率最好是大家熟知的而不是随意指定的。
  • 第二,常用的波特率经过长久发展,就形成了共识,大家常用就是9600或者115200.

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

串口通信时,收发是一个周期一个周期进行的,每周期传输n个二进制位。这一个周期就叫做一个通信单元,一个通信单元是由:起始位+数据位+奇偶校验位+停止位组成的。

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

单工通信和双工通信

单工就是单方向,双工就是双方同时收发,同时只能但方向但是方向可以改变叫半双工

如果只能A发B收则单工,A发B收或者B发A收(两个方向不能同时)叫半双工,A发B收同时B发A收叫全双工。

三根通信线:Rx Tx GND

  • 任何通信都要有信息传输载体,或者是有线的或者是无线的。
  • 串口通信是有线通信,是通过串口线来通信的。
  • 串口通信线最少需要2根(GND和信号线),可以实现单工通信,也可以使用3根通信线(Tx、Rx、GND)来实现全双工。
  • 一般开发板都会引出SoC上串口引脚直接输出的TTL电平的串口(X210开发板没有),插座用插针式插座,每个串口引出的都有3个线(Tx、Rx、GND),可以用这些插座直接连接外部的TTL电平的串口设备。

串口通信的基本原理

串口通信的发送方每隔一定时间(时间固定为1/波特率,单位是秒)将有效信息(1或者0)放到通信线上去,逐个二进制位的进行发送。

接收方通过定时(起始时间由读到起始位标志开始,间隔时间由波特率决定)读取通信线上的电平高低来区分发送给我的是1还是0。依次读取数据位、奇偶校验位、停止位,停止位就表示这一个通信单元(帧)结束,然后中间是不定长短的非通信时间(发送方有可能紧接着就发送第二帧,也可能半天都不发第二帧,这就叫异步通信),下来就是第二帧・・・・・

串口发送的一般都是字符,一般都是ASCII码编码后的字符,所以一般设置数据位都是8,方便刚好一帧发送1个字符。

DB9接口介绍

DB9接口是串口通信早期比较常用的一种规范化接口。串行通信在早期是计算机与外界通信的主要手段,那时候的计算机都有标准配置的串口以实现和外部通信。那时候就定义了一套标准的串口规约,DB9接口就是标准接口。

DB9接口中有9根通信线,其中3根很重要,为GND、Tx、Rx,必不可少;剩余6根都是和流控有关的,现代我们使用串口都是用来做调试一般都禁用流控,所以这6根没用。

自动流控(AFC:Auto flow control)

流控的目的是让串口通信非常可靠,在发送方速率比接收方快的时候流控可以保证发送和接收不会漏掉东西。现在计算机之间有更好更高级(usb、internet)的通讯方式,串口已经基本被废弃了。现在串口的用途更多是SoC用来输出调试信息的。由于调试信息不是关键性信息、而且由于硬件发展串口本身速度已经相对慢的要死了,所以硬件都能协调发送和接收速率,因此流控已经失去意义了,所以现在基本都废弃了。

S5PV210串行通信接口详解

串口的名称

串口的官方名称叫:universal asynchronous receiver and transmitter,通用异步收发器。英文缩写是 uart,中文简称串口。

S5PV210的串口控制器工作原理框图

uart

  • 整个串口控制器包含transmitter和receiver两部分,两部分功能彼此独立,transmitter负责210向外部发送信息,receiver负责从外部接收信息到210内部。
  • 总线角度来讲,串口控制器是接在APB总线上的。对我们编程有影响的是:将来计算串口控制器的源时钟时是以APB总线来计算的。
  • transmitter由发送缓冲区发送移位器构成。我们要发送信息时,首先将信息进行编码(一般用ASCII码)成二进制流,然后将一帧数据(一般是8位)写入发送缓冲区(从这里以后程序就不用管了,剩下的发送部分是硬件自动的),发送移位器会自动从发送缓冲区中读取一帧数据,然后自动移位(移位的目的是将一帧数据的各个位分别拿出来)将其发送到Tx通信线上。
  • receiver由接收缓冲区接收移位器构成。当有人通过串口线向我发送信息时,信息通过Rx通信线进入我的接收移位器,然后接收移位器自动移位将该二进制位保存入我的接收缓冲区,接收完一帧数据后receiver会产生一个中断给CPU,CPU收到中断后即可知道receiver接收满了一帧数据,就会来读取这帧数据。
  • 串口控制器中有一个波特率发生器,作用是产生串口发送/接收的节拍时钟。波特率发生器其实就是个时钟分频器,它的工作需要源时钟(APB总线来),然后内部将源时钟进行分频(软件设置寄存器来配置)得到目标时钟,然后再用这个目标时钟产生波特率(硬件自动的)。

发送缓冲区和接收缓冲区是关键。发送移位器和接收移位器的工作都是自动的,不用编程控制的,所以我们写串口的代码就是:

  • 首先初始化(初始化的实质是读写寄存器)好串口控制器(包括发送控制器和接收控制器),
  • 然后要发送信息时直接写入发送缓冲区,要接收信息时直接去接收缓冲区读取即可。

串口底层的工作(譬如怎么移位的、譬如起始位怎么定义的、譬如TTL电平还是RS232电平等)对程序员是隐藏的,程序员不用去管。软件工程师对串口操作的接口就是发送/接收缓冲区(实质就是寄存器,操作方式就是读写内存)

FIFO模式及其作用

FIFO就是first in first out,先进先出。fifo其实是一种数据结构,这里这个大的缓冲区叫FIFO是因为这个缓冲区的工作方式类似于FIFO这种数据结构。

典型的串口设计,发送/接收缓冲区只有1字节,每次发送/接收只能处理1帧数据。这样在单片机中没什么问题,但是到复杂SoC中(一般有操作系统的)就会有问题,会导致效率低下,因为CPU需要不断切换上下文。

解决方案就是想办法扩展串口控制器的发送/接收缓冲区,譬如将发送/接收缓冲器设置为64字节,CPU一次过来直接给发送缓冲区64字节的待发送数据,然后transmitter慢慢发,发完再找CPU再要64字节。但是串口控制器本来的发送/接收缓冲区是固定的1字节长度的,所以做了个变相的扩展,就是FIFO。

DMA模式及其作用

DMA direct memory access直接内存访问。DMA本来是DSP中的一种技术,DMA技术的核心就是在交换数据时不需要CPU参与,模块可以自己完成。DMA模式要解决的问题就是串口发送/接收要频繁的折腾CPU造成CPU反复切换上下文导致系统效率低下。DMA模式适合大量数据迸发式的发送/接收时。

IrDA模式及其用法

红外通信的原理是发送方固定间隔时间向接收方发送红外信号(表示1或0)或者不发送红外信号(表示0或者1),接收方每隔固定时间去判断有无红外线信号来接收1和0

210的某个串口支持IrDA模式,开启红外模式后,我们只需要向串口写数据,这些数据就会以红外光的方式向外发射出去(当然是需要一些外部硬件支持的),然后接收方接收这些红外数据即可解码得到我们的发送信息。

串行通信与中断的关系

串口通信分为发送/接收2部分。发送方一般不需要(也可以使用)中断即可完成发送,接收方必须(一般来说必须,也可以轮询方式接收)使用中断来接收。

使用中断的工作情景是:发送方先设置好中断并绑定一个中断处理程序,然后发送方丢一帧数据给transmitter,transmitter发送耗费一段时间来发送这一帧数据,这段时间内发送方CPU可以去做别的事情,等transmitter发送完成后会产生一个TXD中断,该中断会导致事先绑定的中断处理程序执行,在中断处理程序中CPU会切换回来继续给transmitter放一帧数据,然后CPU切换离开;

不使用中断的工作情景是:发送方事先禁止TXD中断(当然也不需要给相应的中断处理程序了),发送方CPU给一帧数据到transmitter,然后transmitter耗费一段时间来发送这帧数据,这段时间CPU在这等着(CPU没有切换去做别的事情),待发送方发送完成后CPU再给它一帧数据继续发送直到所有数据发完。CPU是怎么知道transmitter已经发送完了?原来是有个状态寄存器,状态寄存器中有一个位叫发送缓冲区空标志,transmitter发送完成(发送缓冲区空了)就会给这个标志位置位,CPU就是通过不断查询这个标志位为1还是0来指导发送是否已经完成的。

210串行通信接口的时钟设计

因为串口通信需要一个固定的波特率,所以transmitter和receiver都需要一个时钟信号。源时钟信号是外部APB总线(PCLK_PSYS,66MHz)提供给串口模块的(这就是为什么我们说串口是挂在APB总线上的),然后进到串口控制器内部后给波特率发生器(实质上是一个分频器),在波特率发生器中进行分频,分频后得到一个低频时钟,这个时钟就是给transmitter和receiver使用的。

串口通信中时钟的设置主要看寄存器设置。重点的有:

  • 寄存器源设置(为串口控制器选择源时钟,一般选择为PCLK_PSYS,也可以是SCLK_UART),还有波特率发生器的2个寄存器。
  • 波特率发生器有2个重要寄存器:UBRDIVn和UDIVSLOTn,其中UBRDIVn是主要的设置波特率的寄存器,UDIVSLOTn是用来辅助设置的,目的是为了校准波特率的。

Uart 输入时钟框图

S5PV210 为 UART 提供了多个时钟

uart_time

UART I/O

信号 I/O 描述 面板 类型 GPIO
UART_0_RXD Input 为 UART0 接收数据 XuRXD[0] 复用 GPA0[0]
UART_0_TXD Output 为 UART0 发送数据 XuTXD[0] 复用 GPA0[1]
UART_0_CTSn Input 安全,可发送 Clear ToSend XuCTSn[0] 复用 GPA0[2]
UART_0_RTSn Output 请求发送 Requests toSend XuRTSn[0] 复用 GPA0[3]

GPIO 寄存器

寄存器 地址 R/W 描述 初始值
GPA0CON 0xE020_0000 R/W GPA0 配置寄存器 0x0
GPA0DAT 0xE020_0004 R/W GPA0 数据寄存器 0x0
GPA0DRV 0xE020_000C R/W GPA0 驱动强度寄存器 0x0

GPA0CON 寄存器

[31:16] [15:12] [11:8] [7:4] [3:0]
GPA0CON[7:4] GPA0CON[3] GPA0CON[2] GPA0CON[1] GPA0CON[0]
  0010 = UART_1_RXD 0010 = UART_0_RTSn 0010 = UART_0_TXD 0010 = UART_0_RXD

GPA0 驱动寄存器

GPA0DRV Bit 描述 初始值
GPA0DRV[n] [2n+1:2n] n = 0 ~ 7 10 = 2x 0x0

UART 寄存器地址

寄存器 地址 R/W 描述 初始值
ULCON0 0xE290_0000 R/W 线控寄存器 0x0
UCON0 0xE290_0004 R/W 控制寄存器 0x0
UFCON0 0xE290_0008 R/W FIFO 控制寄存器 0x0
UMCON0 0xE290_000C R/W Modem 控制寄存器 0x0
UTRSTAT0 0xE290_0010 R Tx/Rx 状态寄存器 0x6
UTXH0 0xE290_0020 W 发送缓冲寄存器 -
URXH0 0xE290_0024 R 接收缓冲寄存器 0x0
UBRDIV0 0xE290_0028 R/W 波特率整商寄存器 0x0
UDIVSLOT0 0xE290_002C R/W 波特率校准寄存器 0x0

线控寄存器 ULCONn

[31:7] [6] [5:3] [2] [1:0]
保留 红外模式(0/1) 奇偶校验 (101 = 偶校验) 停止位 (0 = 1位) 有效数据长度 (11 = 8bit)

控制寄存器 UCONn

[10] [3:2] [1:0]
时钟源 (0 = PCLK) 发送模式 01=中断/轮询 接收模式 01=中断/轮询

FIFO 寄存器 UFCONn

UFCONn[0] = 0:关闭 FIFO

Tx/Rx 状态寄存器

[2] [1] [0]
0=发送器(缓冲区和移位器)不空;1=空 0= 发送缓冲区不空;1 = 空 0=接收缓冲区空; 1=接收到数据

发送缓冲区 UTXHn

[31:8] URXHn[7:0]
保留 UART的发送器

接收缓冲区 URXHn

[31:8] URXHn[7:0]
保留 UART的接收器

波特率寄存器

[31:16] UBRDIVn/UDIVSLOTn [15:0]
保留 UBRDIVn:波特率整商值
保留 UDIVSLOTn:波特率小数值

DIV_VAL = (PCLK / (bps x 16)) − 1

如果波特率是 115200 bps,SCLK_UART = 40MHz:

DIV_VAL = (40000000 / (115200 x 16)) - 1 = 21.7 - 1 = 20.7

UBRDIVn = 20

0.7 x 16 = 11.2

UDIVSLOTn的二进制形式的数据中应该有11个1,所以 16'b1110_1110_1110_1010 或者16’b0111_0111_0111_0101 都可以。数据手册P880页有推荐的数值表

中断向量控制器 VIC

S5PV210中的中断控制器由四个中断向量控制器(VIC),ARM PrimeCell PL192和四个TrustZone中断控制器(TZIC),SP890组成。

当用户清除中断挂起时,用户必须向所有的VICADDRESS寄存器写入0 (VIC0ADDRESS、VIC1ADDRESS、VIC2ADDRESS和VIC3ADDRESS)

寄存器地址

寄存器 地址 R/W 功能 初始值
VIC0IRQSTATUS 0xF2000000 R IRQ状态寄存器 0x0
VIC0FIQSTATUS 0xF2000004 R FIQ状态寄存器 0x0
VIC0RAWINTR 0xF2000008 R 原始中断状态寄存器 0x0
VIC0INTSELECT 0xF200000C R/W 中断选择寄存器 0x0
VIC0INTENABLE 0xF2000010 R/W 中断使能寄存器 0x0
VIC0INTENCLEAR 0xF2000014 W 清除中断使能寄存器 -
VIC0SOFTINT 0xF200_0018 R/W 软中断寄存器 0x0
VIC0SOFTINTCLEAR 0xF200_001C W 清除软中断寄存器 -
VIC0PROTECTION 0xF200_0020 R/W 保护使能寄存器 0x0
VIC0SWPRIORITYMASK 0xF200_0024 R/W 软件优先掩码寄存器 0xFFFF
VIC0PRIORITYDAISY 0xF200_0028 R/W 菊花链的向量优先级寄存器 0xF
VIC0VECTADDR0 0xF200_0100 R/W 向量地址0寄存器 0x0
VIC0VECTADDR31 0xF200_017C R/W 向量地址31寄存器 0x0
VIC0VECPRIORITY0 0xF200_0200 R/W 向量优先级0寄存器 0xF
VIC0VECPRIORITY31 0xF200_027C R/W 向量优先级31寄存器 0xF
VIC0ADDRESS 0xF200_0F00 R/W 向量地址寄存器 0x0
VIC0PERIPHID0 0xF200_0FE0 R 外设识别寄存器 bit[7:0] 0x92
VIC0PERIPHID3 0xF200_0FEC R 外设识别寄存器 bit[31:24] 0x00
VIC0PCELLID0 0xF200_0FF0 R PrimeCell识别寄存器 bit[7:0] 0x0D
VIC0PCELLID3 0xF200_0FFC R PrimeCell识别寄存器 bit[31:24] 0xB1

S5PV210串行通信编程实战

我们采用的是从 USB 通过 DNW 软件下载到开发板的 0xd0020010 处,然后再将整个程序复制到 0xd0024000 处,最后重定位到 0xd0024000 处执行的方式。

Makefile

export TOOLS-PREFIX=/usr/local/arm/arm-2009q3/bin/arm-none-linux-gnueabi-
export CC=$(TOOLS-PREFIX)gcc
export AR=$(TOOLS-PREFIX)ar
export INCDIR=$(shell pwd)/include
export CPPFLAGS=-nostdlib -nostdinc -I$(INCDIR)
export CFLAGS=-Wall -O2 -fno-builtin

objs := start.o led.o sdram_init.o main.o uart.o clock.o
objs += lib/libc.a

uart_usb.bin:  $(objs)
	$(TOOLS-PREFIX)ld -Tlink.lds -o uart.elf $^
	$(TOOLS-PREFIX)objcopy -O binary uart.elf uart_usb.bin
	$(TOOLS-PREFIX)objdump -D uart.elf > uart_elf.dis
	gcc mkv210_image.c -o mkx210
	./mkx210 uart_usb.bin uart_sd.bin
	
lib/libc.a:
	cd lib; make; cd ..;

%.o : %.S
	$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $< -c 

%.o : %.c
	$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $< -c 

clean:
	cd lib; make clean; cd ..;
	rm *.o *.elf *.bin *.dis mkx210 -f

链接脚本 link.lds

SECTIONS
{
	. = 0xd0024000;
	
	.text : {
		start.o
		sdram_init.o
		* (.text)
	}
    		
	.data : {
		* (.data)
	}
	
	bss_start = .; 
	.bss : {
		* (.bss)
	}
	
	bss_end  = .;	
}

start.S

#define WTCON		0xE2700000
#define SVC_STACK	0xd0037d80

.global _start   // 把_start链接属性改为外部,这样其他文件就可以看见_start了
_start:
	// 第1步:关看门狗(向WTCON的bit5写入0即可)
	ldr r0, =WTCON
	ldr r1, =0x0
	str r1, [r0]

	// 第2步:初始化时钟
	bl clock_init	

	// 第3步:设置SVC栈
	ldr sp, =SVC_STACK
	
	// 第4步:开/关icache
	mrc p15,0,r0,c1,c0,0;	   // 读出cp15的c1到r0中
	//bic r0, r0, #(1<<12)	// bit12 置0  关icache
	orr r0, r0, #(1<<12)		// bit12 置1  开icache
	mcr p15,0,r0,c1,c0,0;

	// 第5步:初始化ddr
	bl sdram_asm_init
	
	// 第6步:重定位
	// adr指令用于加载_start当前运行地址
	adr r0, _start // adr加载时就叫短加载		
	// ldr指令用于加载_start的链接地址:0xd0024000
	ldr r1, =_start // ldr加载时如果目标寄存器是pc就叫长跳转,如果目标寄存器是r1等就叫长加载	
	// bss段的起始地址
	ldr r2, =bss_start // 就是我们重定位代码的结束地址,重定位只需重定位代码段和数据段即可
	cmp r0, r1 // 比较_start的运行时地址和链接地址是否相等
	beq clean_bss // 如果相等说明不需要重定位,所以跳过copy_loop,直接到clean_bss
		// 如果不相等说明需要重定位,那么直接执行下面的copy_loop进行重定位
		// 重定位完成后继续执行clean_bss。

// 用汇编来实现的一个while循环
copy_loop:
	ldr r3, [r0], #4 // 源
	str r3, [r1], #4 // 目的   这两句代码就完成了4个字节内容的拷贝
	cmp r1, r2 // r1和r2都是用ldr加载的,都是链接地址,所以r1不断+4总能等于r2
	bne copy_loop

	// 清bss段,其实就是在链接地址处把bss段全部清零
clean_bss:
	ldr r0, =bss_start					
	ldr r1, =bss_end
	cmp r0, r1 // 如果r0等于r1,说明bss段为空,直接下去
	beq run_on_dram // 清除bss完之后的地址
	mov r2, #0
clear_loop:
	str r2, [r0], #4 // 先将r2中的值放入r0所指向的内存地址(r0中的值作为内存地址),
	cmp r0, r1 // 然后r0 = r0 + 4
	bne clear_loop

run_on_dram:	
	// 长跳转到led_blink开始第二阶段
	ldr pc, =uart_main

// 汇编最后的这个死循环不能丢
	b .

Uart.c

#define GPA0CON		0xE0200000
#define UCON0 		0xE2900004
#define ULCON0 		0xE2900000
#define UMCON0 		0xE290000C
#define UFCON0 		0xE2900008
#define UBRDIV0 	0xE2900028
#define UDIVSLOT0	0xE290002C
#define UTRSTAT0	0xE2900010
#define UTXH0		0xE2900020	
#define URXH0		0xE2900024	

#define rGPA0CON	(*(volatile unsigned int *)GPA0CON)
#define rUCON0		(*(volatile unsigned int *)UCON0)
#define rULCON0		(*(volatile unsigned int *)ULCON0)
#define rUMCON0		(*(volatile unsigned int *)UMCON0)
#define rUFCON0		(*(volatile unsigned int *)UFCON0)
#define rUBRDIV0	(*(volatile unsigned int *)UBRDIV0)
#define rUDIVSLOT0	(*(volatile unsigned int *)UDIVSLOT0)
#define rUTRSTAT0		(*(volatile unsigned int *)UTRSTAT0)
#define rUTXH0		(*(volatile unsigned int *)UTXH0)
#define rURXH0		(*(volatile unsigned int *)URXH0)

// 串口初始化程序
void uart_init(void)
{
	// 初始化Tx Rx对应的GPIO引脚
	rGPA0CON &= ~(0xff<<0);			// 把寄存器的bit0~7全部清零
	rGPA0CON |= 0x00000022;			// 0b0010, Rx Tx
	
	// 几个关键寄存器的设置
	rULCON0 = 0x3;
	rUCON0 = 0x5;
	rUMCON0 = 0;
	rUFCON0 = 0;
	
	// 波特率设置	DIV_VAL = (PCLK / (bps x 16))-1
	// PCLK_PSYS用66MHz算		余数0.8
	//rUBRDIV0 = 34;	
	//rUDIVSLOT0 = 0xdfdd;
	
	// PCLK_PSYS用66.7MHz算		余数0.18
	// DIV_VAL = (66700000/(115200*16)-1) = 35.18
	rUBRDIV0 = 35;
	// (rUDIVSLOT中的1的个数)/16=上一步计算的余数=0.18
	// (rUDIVSLOT中的1的个数 = 16*0.18= 2.88 = 3
	rUDIVSLOT0 = 0x0888;		// 3个1,查官方推荐表得到这个数字
}


// 串口发送程序,发送一个字节
void putc(char c)
{                  	
	// 串口发送一个字符,其实就是把一个字节丢到发送缓冲区中去
	// 因为串口控制器发送1个字节的速度远远低于CPU的速度,所以CPU发送1个字节前必须
	// 确认串口控制器当前缓冲区是空的(意思就是串口已经发完了上一个字节)
	// 如果缓冲区非空则位为0,此时应该循环,直到位为1
	while (!(rUTRSTAT0 & (1<<1)));
	rUTXH0 = c;
}

// 串口接收程序,轮询方式,接收一个字节
char getc(void)
{
	while (!(rUTRSTAT0 & (1<<0)));
	return (rURXH0 & 0x0f);
}

main.c

void uart_main(void)
{
    uart_init();

    printf("123 %d\n\r", 456);
	printf("%d\n\r", 12345);

    while(1) {
        putc('d');
        delay();
    }
}

最终结果

uart_print

uart_cmd