一、通信的基础概念
(1)通信的发展历史
通信:狼烟 ---> 信件 ---> 电子通信(电报、电话、网络信号)。
通信中最重要的两个方面:信息表示、解析方法 + 信息的传输方法。
通信双方事先需要约定好信息的表示方法和解析方法,做到一致,否则信息不能有效传递。
通信的传输方法是指经过编码后的通信信息如果在传输介质上传输的过程。
总结:
通信过程分为3个步骤:首先,发送方按照信息编码方式对有效信息进行编码(编成可以在通信线路上传输的信号形态);然后,编码后的信息在传输介质上进行传输,输送给接收方;最后,接收方接到编码信息后进行解码,解码后得到可以理解的有效信息。
(2)电子通信概念
1》同步通信和异步通信
发送方和接收方按照同一个时钟节拍工作就叫同步。发送方和接收方没有统一的时钟节拍、而各自按照自己的节拍工作就叫异步。
同步通信中,通信双方按照统一节拍工作,所以配合很好;一般需要发送方给接收方发送信息同时发送时钟信号,接收方根据发送方给它的时钟信号来安排自己的节奏。同步通信用在通信双方信息交换频率固定,或者经常通信时。
异步通信,又叫异步通知。在双方通信的频率不固定时(有时3ms收发一次,有时3天才收发一次),不适合使用同步通信,而适合异步通信。异步通信时接收方不必一直在意发送方,发送方需要发送信息时会首先给接收方一个信息开始的起始信号,接收方接收到起始信号后就认为后面紧跟着的就是有效信息,才会开始注意接收信息,直到收到发送方发过来的结束标志。
2》电平信号和差分信号
电平信号和差分信号是用来描述通信线路传输方式的。也就是说如何在通信线路上表示1和0。
电平信号的传输线中有一个参考电平线(一般是GND),然后信号线上的信号值是由信号线电平和参考电平线的电压差决定。
差分信号的传输线中没有参考电平线,所有都是信号线,然后1和0的表达靠信号线之间的电压差。
电平信号的2根通信线之间的电平差异容易受到干扰,传输容易失败;差分信号不容易受到干扰,因此传输质量比较稳定。现代通信一般都使用差分信号,电平信号几乎没有了。
在相同根数的通信线下,差分信号比电平信号要快,因为差分信号抗干扰能力强,因此1个发送周期更短。
3》并行接口和串行接口
串行、并行主要是考虑通信线的根数,就是发送方和接收方同时可以传递的信息量的多少。
在电平信号下,”1根参考电平线+1根信号线“可以传递1位二进制;”1根参考电平线+2根信号线“可以同时发送2位二进制;如果想同时发送8位二进制就需要9根线。
在差分信号下,2根线(彼此差分)可以同时发送1位二进制;如果需要同时发送8位二进制就需要16根线。
串行接口使用的更加广泛,因为更省信号线,而且对传输线的要求更低、成本更低;而且串行时可以通过提高通信速度来提高总体通信性能,不一定非得要并行。
4》总结
经过这么多年的发展,最终胜出的是:异步、串行、差分,譬如USB和网络通信。
二、串口通信的基本概念
(1)串口通信特点
1》异步
串口通信的发送方和接收方之间没有统一的时钟信号。
2》电平信号
串口通信出现的时间较早、速率较低、传输的距离较近,所有干扰不太明显,因此当时使用了电平信号传输。后期出现的传输协议都改成了差分信号传输了。
3》串行通信
串口通信每次同时只能传输1个二进制位。
(2)RS232电平和TTL电平
电平信号是用信号线电平减去参考线电平得到的电压差,这个电压差决定了传输值是1还是0。
在电平信号中多少V代表1,多少V代表0是不固定的,取决于电平标准。譬如,RS232电平中-15V ~ -3V表示1,+3V ~ +15V表示0;TTL电平中+5V表示1,0V表示0。
不管哪种电平都是为了在传输线上表示1和0,区别在于适用的环境和条件不同。RS232电平适合干扰大、距离远的情况,一般传输距离小于15米,用于工业上;TTL电平适合距离近且干扰小的情况,一般用在电路板内部的两个芯片之间。
对于编程来说,RS232电平传输和TTL电平传输是没有差异的。所以电平标准对硬件工程更有意义,而软件工程师只要略懂即可(TTL电平和RS232电平混接是不可以的)。
(3)波特率
波特率,指的是串口通信的速率,也就是串口通信时每秒钟可以传输多少个二进制位。譬如,每秒钟可以传输9600个二进制位(传输一个二进制位需要的时间是1/9600秒,也就是104us),波特率就是9600。
串口通信的波特率不能随意设定,而应该是在一些值中去选择。一般最常见的波特率是9600或115200(低端单片机如51常用9600,高端单片机和嵌入式SOC常用115200)。
波特率不能随意指定,主要是因为:通信双方必须事先设定相同的波特率才能成功通信,如果发送方和接收方按照不同的波特率通信则根本收不到,因此波特率最好是大家熟知的而不是随意指定的;常用的波特率经过长久发展,就形成了共识,大家常用的就是9600或者115200。
(4)起始位、数据位、奇偶校验位、停止位
串口通信时,收发是一个周期一个周期进行的,每个周期传输n个二进制位。这一个周期就叫做一个通信单元,一个通信单元由:起始位+数据位+奇偶校验位+停止位组成的。
起始位:表示发送方要开始发送一个通信单元,起始位的定义是串口通信标准事先指定的,是由通信线上的电平变化来反映的。
数据位:是一个通信单元中发送的有效信息位,是本次通信真正要发送的有效数据,串口通信一次发送多少位有效数据是可以设定的(可选的有6、7、8、9,一般都是选择8位数据位,因为一般通过串口发送的文字信息都是ASCII码编码,而ASCII码中一个字符刚好编码为8位)。
校验位:是用来校验数据位,以防止数据位出错的。
停止位:是发送方用来表示本通信单元结束标志的,停止位的定义是串口通信标准事先指定的,是由通信线上的电平变化来反映的。常见的有1位停止位、1.5位停止位、2位停止位等,一般使用的是1位停止位。
总结:
串口通信时因为是异步通信,所以通信双方必须事先约定好通信参数,这些通信参数包括:波特率、数据位、校验位、停止位(串口通信中起始位定义是唯一的,所以一般不用选择)。
(5)单工、半双工和全双工
单工:单方向收发数据,譬如,只能A发送数据,B接收数据。
半双工:双方分时收发数据,譬如,“A发送数据,B接收数据”或者“A接收数据,B发送数据”,两个方向不能同时进行。
全双工:双方同时收发数据,譬如,“A发送数据,B接收数据”同时“A接收数据,B发送数据”,两个方向同时进行。
三、串口通信的基本原理
(1)三根通信线:TX、RX、GND
任何通信都要有信息传输载体,或者是有线的或者是无线的。
串口通信是有线通信,是通过串口线来通信的。
串口线最少需要两根(GND和信号线),可以实现单工通信;也可以使用3根通信线(TX、RX、GND),来实现全双工通信。
一般开发板都会引出SOC上串口引脚直接输出的TTL电平的接口,用的是插针式插座,每个串口引出的都有3根通信线(TX、RX、GND),可以用这些插座直接连接外部的TTL电平的串口设备。
(2)收发双方事先规定好通信参数(波特率、数据位、奇偶校验位、停止位等)
串口通信属于基层基本性的通信规约,它自己本身不会去协商通信参数,需要通信前通信双方事先约定好通信参数(一般4个最重要的)
串口通信的任何一个关键参数设置错误,都会导致通信失败。譬如波特率调错了,发送方发送没问题,接收方也能接收,但是接收到全是乱码···
(3)信息以二进制流的方式在信道上传输
串口通信的发送方每隔一定时间(时间固定为1/波特率,单位是秒)将有效信息(1或者0)放到通信线上去,逐个二进制位的进行发送。
接收方通过定时(起始时间由读到起始位标志开始,间隔时间由波特率决定)读取通信线上的电平高低来区分发送给我的是1还是0。依次读取数据位、奇偶校验位、停止位,停止位就表示这一个通信单元(帧)结束,然后中间是不定长短的非通信时间(发送方有可能紧接着就发送第二帧,也可能半天都不发第二帧,这就叫异步通信),下来就是第二帧·····
总结:波特率非常重要,波特率错了整个通信就乱套了;数据位、奇偶校验位、停止位也很重要,否则可能认不清数据。通过串口不管发数字、还是文本还是命令还是什么,都要先对发送内容进行编码,编码成二进制再进行逐个位的发送。
串口发送的一般都是字符,一般都是ASCII码编码后的字符,所以一般设置数据位都是8,方便刚好一帧发送1个字符。
(4)DB9接口介绍
DB9接口是串口通信早期比较常用的一种规范化接口。
串行通信在早期是计算机与外界通信的主要手段,那时候的计算机都有标准配置的串口以实现和外部通信。那时候就定义了一套标准的串口规约,DB9接口就是标准接口。
DB9接口中有9根通信线,其中3根很重要,为GND、Tx、Rx,必不可少;剩余6根都是和流控有关的,现代我们使用串口都是用来做调试一般都禁用流控,所以这6根没用。
现在一般使用串口时要记得把流控禁止掉,不然可能发生意想不到的问题。
四、S5PV210串行通信接口详解
(1)串口的名称
S5PV210的串口控制器在数据手册的section 8.1章节。
串口的官方名称是:UNIVERSAL ASYNCHRONOUS RECEIVER AND TRANSMITTER,即通用异步收发器,英文缩写是uart,中文简称串口。
(2)串口控制器工作原理框图
整个串口控制器包含transmitter和receiver两部分,这两部分功能彼此独立,transmitter负责210向外部发送信息,receiver负责从外部接收信息到210。
总线角度来讲,串口控制器是挂载在APB总线上的,对编程有影响的是:将来计算串口控制器的源时钟时是以APB总线来计算的。
transmitter由发送缓冲区和发送移位寄存器构成。我们要发送信息时,首先将信息进行编码(一般用ASCII码)成二进制流,然后将一帧数据(一般是8位)写入发送缓冲区(从这里以后程序就不用管了,剩下的发送部分由硬件自动完成),最后发送移位寄存器会自动从发送缓冲区中读取一帧数据,然后自动移位(移位的目的是将一帧数据的各个位分别拿出来)将其发送到Tx通信线上。
receiver由接收缓冲区和接收移位寄存器构成。当有人通过串口线向我发送信息时,信息通过Rx通信线进入我的接收移位寄存器中,然后接收移位寄存器自动移位并将该二进制位保存入我的接收缓冲区,接收完一帧数据后receiver会产生一个中断给CPU,CPU收到中断后即可知道receiver接收满了一帧数据,就会来读取这帧数据。
总结:
发送缓冲区和接收缓冲区是关键,发送移位寄存器和接收移位寄存器的工作都是自动的,不用编程控制的,所以我们写串口代码就是:首先初始化串口控制器(包括发送控制器和接收控制器),然后要发送信息时直接写入发送缓冲区,要接收信息时直接去接收缓冲区中读取即可。
软件工程师对串口操作的接口就是发送/接收缓冲区(实际就是寄存器,操作方式就是读写内存)。
串口控制器中有一个波特率发生器,其作用是产生串口发送/接收的时钟节拍。波特率发生器其实就是个时钟分频器,它的工作需要源时钟(从APB总线来),然后内部将源时钟进行分频(需要软件设置寄存器来配置)得到目标时钟,然后再用这个目标时钟产生波特率(由硬件自动完成的)。
(3)FIFO模式及其作用
典型的串口设计,发送/接收缓冲区只有1字节,每次发送/接收只能处理1帧数据。这样在单片机中没什么问题,但是到复杂SOC中(一般有操作系统的)就会有问题,会导致效率低下,因为CPU需要不断切换上下文。
解决方案就是想办法扩展串口控制器的发送/接收缓冲区,譬如将发送/接收缓冲区设置为64字节,CPU一次过来直接给发送缓冲区64字节的待发送数据,然后transmitter慢慢发送,发送完再找CPU再要64字节数据。但是串口控制器本来的发送/接收缓冲区是固定的1字节大小的,所有做了个变相的扩展,就是FIFO。CPU先将64字节的数据放到FIFO中,然后启动FIFO模式,FIFO每次会自动往发送缓冲区中添加1字节数据,最后进行移位操作传输数据。
FIFO,就是first in first out,先进先出。FIFO其实是一种数据结构,这里这个大的缓冲区叫FIFO是因为这个缓冲区的工作方式类似于FIFO这种数据结构。
(4)DMA模式及其作用
DMA,就是direct memory access,直接内存访问。DMA本来是DSP中的一种技术,DMA技术的核心就是在交换数据时不需要CPU参与,模块可以自己完成。
DMA模式要解决的问题和上面FIFO模式是同一个问题,就是串口发送/接收要频繁的折腾CPU造成CPU反复切换上下文导致系统效率低下。
传统的串口工作方式(无FIFO无DMA)效率是最低的,适合低端单片机;高端单片机上CPU事务繁忙所以都需要串口能够自己完成大量数据发送/接收,这时候就需要FIFO或者DMA模式。FIFO模式是一种轻量级的解决方案(只能从一定程度上解决问题,譬如几十字节的数据),DMA模式适合大量数据迸发式的发送/接收(可以从根本上解决问题,可处理大量的数据)。
(5)串口通信与中断的关系
串口通信分为发送/接收两部分,发送一般不需要中断即可完成发送,接收一般需要使用中断来接收。
发送方可以选择使用中断,也可以选择不使用中断。使用中断的工作情景是:发送方先设置好中断并绑定一个中断处理程序,然后发送方丢一帧数据给transmitter,transmitter耗费一段时间来发送这一帧数据,这段时间内发送方CPU可以去做别的事情,等transmitter发送完成后会产生一个TXD中断,该中断会导致事先绑定的中断处理程序执行,在中断处理程序中CPU会切换回来继续给transmitter放一帧数据,然后CPU切换离开;不使用中断的工作情景是:发送方事先禁止TXD中断(当然也不需要给相应的中断处理程序了),发送方CPU给一帧数据到transmitter,然后transmitter耗费一段时间来发送这帧数据,这段时间CPU在这等着(CPU没有切换去做别的事情),待发送方发送完成后CPU再给它一帧数据继续发送直到所有数据发完。CPU是怎么知道transmitter已经发送完了?原来是有个状态寄存器,状态寄存器中有一个位叫发送缓冲区空标志,transmitter发送完成(发送缓冲区空了)就会给这个标志位置1,CPU就是通过不断查询这个标志位为1还是0来知道发送是否已经完成的。
接收方可以选择使用中断,也可以选择不使用中断。使用中断的工作情景是:接收方先设置好中断并绑定一个中断处理程序,然后接收方会耗费一段时间从receiver中来接收一帧数据,这段时间内接收方CPU可以去做别的事情,等receiver接收数据完成后会产生一个RXD中断,该中断会导致事先绑定的中断处理程序执行,在中断处理程序中CPU会切换回来从receiver中读取数据,然后CPU切换离开;不使用中断的工作情景是:接收方事先禁止RXD中断(当然也不需要给相应的中断处理程序了),接收方会耗费一段时间从receiver中接收一帧数据,这段时间CPU在这等着(CPU没有切换去做别的事情),待接收方接收完成后CPU会继续等着直到所有数据接收完。CPU是怎么知道receiver已经接收完了?原来是有个状态寄存器,状态寄存器中有一个位叫接收缓冲区满标志,receiver接收完成(接收缓冲区满了)就会给这个标志位置1,CPU就是通过不断查询这个标志位为1还是0来知道接收是否已经完成的。
因为串口通信是异步的,异步的意思就是说发送方占主导权。也就是说发送方随时想发就能发,但是接收方只有时刻等待才不会丢失数据。所以这个差异就导致发送方可以不用中断,而接收方不得不使用中断模式。
(6)串行通信接口的时钟设计
串口通信为什么需要时钟?因为串口通信需要一个固定的波特率,所以transmitter和receiver都需要一个时钟信号。
时钟信号从哪里来?源时钟信号是外部APB总线(PCLK_PSYS,66MHz)提供给串口模块的(这就是为什么我们说串口是挂在APB总线上的),然后进到串口控制器内部后给波特率发生器(实质上是一个分频器),在波特率发生器中进行分频,分频后得到一个低频时钟,这个时钟就是给transmitter和receiver使用的。
串口通信中时钟的设置主要看寄存器设置。重点的有:寄存器源设置(为串口控制器选择源时钟,一般选择为PCLK_PSYS,也可以是SCLK_UART),还有波特率发生器的2个寄存器。
波特率发生器有2个重要寄存器:UBRDIVn和UDIVSLOTn,其中UBRDIVn是主要的设置波特率的寄存器,UDIVSLOTn是用来辅助设置的,目的是为了校准波特率的。
五、原理图
六、主要的寄存器
(1)初始化串口的Tx和Rx引脚所对应的GPIO
(2)设置数据位、校验位和停止位
(3)发送和接收都选择轮询(polling)模式
(4)禁止modem、afc
(5)禁止FIFO模式
(6)和波特率相关的寄存器
(7)状态相关的寄存器
(8)发送/接收相关的寄存器
七、串口程序
start.S文件
#define WTCON 0xE2700000
#define SVC_STACK 0xd0037d80
.global _start
_start:
//第一步:关闭看门狗
ldr r0,=WTCON
ldr r1,=0x0
str r1,[r0]
//第二步:初始化时钟
bl clock_init
//第三步:设置SVC栈
ldr sp,=SVC_STACK
//第四步:开启iCache
mrc p15,0,r0,c1,c0,0
orr r0,r0,#(1<<12)
mcr p15,0,r0,c1,c0,0
//第五步:从这里开始调用C程序
bl main
b .
uart.h文件
#ifndef __UART_H__
#define __UART_H__
#define GPA0CON *((unsigned int *)0xE0200000)
#define ULCON0 *((unsigned int *)0xE2900000)
#define UCON0 *((unsigned int *)0xE2900004)
#define UFCON0 *((unsigned int *)0xE2900008)
#define UMCON0 *((unsigned int *)0xE290000C)
#define UBRDIV0 *((unsigned int *)0xE2900028)
#define UDIVSLOT0 *((unsigned int *)0xE290002C)
#define UTRSTAT0 *((unsigned int *)0xE2900010)
#define UTXH0 *((unsigned int *)0xE2900020)
#define URXH0 *((unsigned int *)0xE2900024)
extern void uart_init(void);
extern void uart_sendOneByte(char c);
extern char uart_recvOneByte();
#endif
uart.c文件
#include "uart.h"
//串口初始化程序
void uart_init(void)
{
//初始化Tx、Rx对应的GPIO引脚
GPA0CON &= ~(0xff); //把寄存器的bit0~7全部清零
GPA0CON |= (1<<2) | (1<<6); //0010 0010
//几个关键寄存器设置
ULCON0 = 0x3;
UCON0 = 0x5;
UFCON0 = 0x0;
UMCON0 = 0x0;
//波特率设置:DIV_VAL = (PCLK / (bps * 16))-1 = (66.7MHz / (115200 * 16)) - 1 = 35.18
//所以,UBRDIV0 = 35 ; UDIVSLOT0 = 0.18 * 16 = 2.88 = 3,所以有3个1,查表可得UDIVSLOT0的值应为0x0888
UBRDIV0 = 35;
UDIVSLOT0 = 0x0888;
}
//串口发送程序,发送一个字节
void uart_sendOneByte(char c)
{
//判断发送缓冲区是否为空(因为串口控制器发送1个字节的速度远低于CPU的速度,所以CPU发送1个字节前必须确认串口控制器当前缓冲区是空的)
while(!(UTRSTAT0 & (1<<1)));
//发送数据
UTXH0 = c;
}
//串口接收程序,轮训接收一个字节
char uart_recvOneByte()
{
//等待接收缓冲区中有数据
while(!(UTRSTAT0 & (1<<0)));
//CPU读取接收缓冲区中的数据
return (URXH0 & 0x0f);
}
clock.c文件
// 时钟控制器基地址
#define ELFIN_CLOCK_POWER_BASE 0xE0100000
// 时钟相关的寄存器相对时钟控制器基地址的偏移值
#define APLL_LOCK_OFFSET 0x00
#define MPLL_LOCK_OFFSET 0x08
#define APLL_CON0_OFFSET 0x100
#define APLL_CON1_OFFSET 0x104
#define MPLL_CON_OFFSET 0x108
#define CLK_SRC0_OFFSET 0x200
#define CLK_SRC1_OFFSET 0x204
#define CLK_SRC2_OFFSET 0x208
#define CLK_SRC3_OFFSET 0x20c
#define CLK_SRC4_OFFSET 0x210
#define CLK_SRC5_OFFSET 0x214
#define CLK_SRC6_OFFSET 0x218
#define CLK_SRC_MASK0_OFFSET 0x280
#define CLK_SRC_MASK1_OFFSET 0x284
#define CLK_DIV0_OFFSET 0x300
#define CLK_DIV1_OFFSET 0x304
#define CLK_DIV2_OFFSET 0x308
#define CLK_DIV3_OFFSET 0x30c
#define CLK_DIV4_OFFSET 0x310
#define CLK_DIV5_OFFSET 0x314
#define CLK_DIV6_OFFSET 0x318
#define CLK_DIV7_OFFSET 0x31c
#define CLK_DIV0_MASK 0x7fffffff
// 这些M、P、S的配置值都是查数据手册中典型时钟配置值的推荐配置得来的。
// 这些配置值是三星推荐的,因此工作最稳定。如果是自己随便瞎拼凑出来的那就要
// 经过严格测试,才能保证一定对。
#define APLL_MDIV 0x7d // 125
#define APLL_PDIV 0x3
#define APLL_SDIV 0x1
#define MPLL_MDIV 0x29b // 667
#define MPLL_PDIV 0xc
#define MPLL_SDIV 0x1
#define set_pll(mdiv, pdiv, sdiv) (1<<31 | mdiv<<16 | pdiv<<8 | sdiv)
#define APLL_VAL set_pll(APLL_MDIV,APLL_PDIV,APLL_SDIV)
#define MPLL_VAL set_pll(MPLL_MDIV,MPLL_PDIV,MPLL_SDIV)
#define REG_CLK_SRC0 (ELFIN_CLOCK_POWER_BASE + CLK_SRC0_OFFSET)
#define REG_APLL_LOCK (ELFIN_CLOCK_POWER_BASE + APLL_LOCK_OFFSET)
#define REG_MPLL_LOCK (ELFIN_CLOCK_POWER_BASE + MPLL_LOCK_OFFSET)
#define REG_CLK_DIV0 (ELFIN_CLOCK_POWER_BASE + CLK_DIV0_OFFSET)
#define REG_APLL_CON0 (ELFIN_CLOCK_POWER_BASE + APLL_CON0_OFFSET)
#define REG_MPLL_CON (ELFIN_CLOCK_POWER_BASE + MPLL_CON_OFFSET)
#define rREG_CLK_SRC0 (*(volatile unsigned int *)REG_CLK_SRC0)
#define rREG_APLL_LOCK (*(volatile unsigned int *)REG_APLL_LOCK)
#define rREG_MPLL_LOCK (*(volatile unsigned int *)REG_MPLL_LOCK)
#define rREG_CLK_DIV0 (*(volatile unsigned int *)REG_CLK_DIV0)
#define rREG_APLL_CON0 (*(volatile unsigned int *)REG_APLL_CON0)
#define rREG_MPLL_CON (*(volatile unsigned int *)REG_MPLL_CON)
void clock_init(void)
{
// 1 设置各种时钟开关,暂时不使用PLL
rREG_CLK_SRC0 = 0x0;
// 2 设置锁定时间,使用默认值即可
// 设置PLL后,时钟从Fin提升到目标频率时,需要一定的时间,即锁定时间
rREG_APLL_LOCK = 0x0000ffff;
rREG_MPLL_LOCK = 0x0000ffff;
// 3 设置分频
// 清bit[0~31]
rREG_CLK_DIV0 = 0x14131440;
// 4 设置PLL
// FOUT = MDIV*FIN/(PDIV*2^(SDIV-1))=0x7d*24/(0x3*2^(1-1))=1000 MHz
rREG_APLL_CON0 = APLL_VAL;
// FOUT = MDIV*FIN/(PDIV*2^SDIV)=0x29b*24/(0xc*2^1)= 667 MHz
rREG_MPLL_CON = MPLL_VAL;
// 5 设置各种时钟开关,使用PLL
rREG_CLK_SRC0 = 0x10001111;
}
main.c文件
#include "uart.h"
void delay(void);
void main(void)
{
uart_init();
while(1)
{
uart_sendOneByte('C');
delay();
}
}
void delay(void)
{
int i = 0;
for(i=0;i<1000000;i++);
}
Makefile文件
uart.bin:start.o uart.o main.o clock.o
arm-linux-ld -Ttext 0x0 -o uart.elf $^
arm-linux-objcopy -O binary uart.elf uart.bin
arm-linux-objdump -D uart.elf > uart_elf.dis
%.o:%.S
arm-linux-gcc -o $@ $< -c -nostdlib
%.o:%.c
arm-linux-gcc -o $@ $< -c -nostdlib
clean:
rm *.o *.elf *.bin *.dis -f
八、移植printf
printf函数工作时内部实际调用了2个关键函数:一个是vsprintf函数(主要功能是格式化打印信息,最终得到纯字符串格式的打印信息等待输出),另一个就是真正的输出函数putc(操控标准输出的硬件,将信息发送出去)。而移植printf函数时,putc实际调用的应该是串口中输出一个字符的函数,我们需要修改的也是这个函数。
将“别人移植好的printf”目录下的include和lib两个目录拷贝到03uart_printf目录中,然后对uart程序进行修改,需要写给的文件有:uart.c文件、main.c文件、Makefile文件。
uart.c文件
#include "uart.h"
//串口初始化程序(不需要修改)
void uart_init(void)
{
//初始化Tx、Rx对应的GPIO引脚
GPA0CON &= ~(0xff); //把寄存器的bit0~7全部清零
GPA0CON |= (1<<2) | (1<<6); //0010 0010
//几个关键寄存器设置
ULCON0 = 0x3;
UCON0 = 0x5;
UFCON0 = 0x0;
UMCON0 = 0x0;
//波特率设置:DIV_VAL = (PCLK / (bps * 16))-1 = (66.7MHz / (115200 * 16)) - 1 = 35.18
//所以,UBRDIV0 = 35 ; UDIVSLOT0 = 0.18 * 16 = 2.88 = 3,所以有3个1,查表可得UDIVSLOT0的值应为0x0888
UBRDIV0 = 35;
UDIVSLOT0 = 0x0888;
}
//串口发送程序,发送一个字节(将该函数名称改为putc)
//void uart_sendOneByte(char c)
void putc(char c) //在lib目录下的printf函数中调用的putc函数实际就是这里的这个函数,这个函数
//是与硬件操作相关,所以需要我们自己写。
{
//判断发送缓冲区是否为空(因为串口控制器发送1个字节的速度远低于CPU的速度,所以CPU发送1个字节前必须确认串口控制器当前缓冲区是空的)
while(!(UTRSTAT0 & (1<<1)));
//发送数据
UTXH0 = c;
}
//串口接收程序,轮训接收一个字节(将该函数名称改为getc)
//char uart_recvOneByte()
char getc() //在lib目录下的scanf函数中调用的getc函数实际就是这里的这个函数,这个函数是与硬件
//相关的,所以需要我们自己写。
{
//等待接收缓冲区中有数据
while(!(UTRSTAT0 & (1<<0)));
//CPU读取接收缓冲区中的数据
return (URXH0 & 0x0f);
}
main.c文件
#include "uart.h"
#include "stdio.h"
void delay(void);
void main(void)
{
uart_init();
int a = 1234;
printf("test for printf,a = %d\n",a);
//while(1)
//{
// uart_sendOneByte('C');
// delay();
//}
}
void delay(void)
{
int i = 0;
for(i=0;i<1000000;i++);
}
Makefile文件
CC = arm-linux-gcc
LD = arm-linux-ld
OBJCOPY = arm-linux-objcopy
OBJDUMP = arm-linux-objdump
AR = arm-linux-ar
INCDIR := $(shell pwd)
#C预处理器的flag,flag就是编译器可选的选项
CPPFLAGS := -nostdlib -nostdinc -I$(INCDIR)/include
#C编译器的flag
CFLAGS := -Wall -O2 -fno-builtin
#导出这些变量到全局,其实就是给子文件夹下面的Makefile使用
export CC LD OBJCOPY OBJDUMP AR CPPFLAGS CFLAGS
objs := start.o uart.o main.o clock.o
objs += lib/libc.a
uart.bin:$(objs)
$(LD) -Tlink.lds -o uart.elf $^
$(OBJCOPY) -O binary uart.elf uart.bin
$(OBJDUMP) -D uart.elf > uart_elf.dis
lib/libc.a:
cd lib; make; cd ..
%.o:%.S
$(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< -c
%.o:%.c
$(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< -c
clean:
rm *.o *.elf *.bin *.dis -f
cd lib; make clean; cd ..