volatile int *output=(volatile unsigned int *)0xff800000;//定义一个I/O端口
通信的过程:首先发送方先按照信息编码方式对有效信息进行编码(编程成可以在通信线路上传输的信号形态),编码后的信息在传输介质上进行传输,输送给接收方;最后接收方接收到编码信息后进行解码,解码后得到可以理解的有效信息。
(1)同步通信:通信双方按照统一节拍工作,一般需要发送方给接收方发送信息同时发送时钟信号,接收方根据发送方给它的时钟信号来安排自己的节拍。(同步通信用在通信双方信息交换频率固定,或者经常通信时)
(2)异步通信:又叫异步通知。异步通信时接收方不必一直等待发送方,发送方需要发送信息时会首先给接收方一个信息开始的起始信号,接收方接收到起始信号后就认为后面紧跟着的就是有效信息,才会开始注意接收信息,直到收到发送方发过来的结束标志。(异步通信用在双方通信的频率不固定时)
(3)同步和异步的区别:发送方和接收方按照同一个时钟节拍工作就叫同步,发送方和接收方没有统一的时钟节拍、而各自按照自己的节拍工作就叫异步,同步一般带有一根时钟线,负责传输时钟,控制接收方的节拍(大部分情况下的判断依据)。
(1)电平信号和差分信号是用来描述通信线路传输方式的,即通过什么方式在通信线路上来表达逻辑0或1。
(2)电平信号的传输线中有一个参考电平线(一般是GND),通过信号线上的信号电平和参考电平线的电压差决定信息表达是0或1。这种方式的传播一般容易受到外界干扰。
(3)差分信号的传输线中没有参考电平,所有都是信号线。通过信号线之间的电压差来表达逻辑0或1。由于两条信号线受到干扰环境的影响基本相同,因此两者之间受到的干扰可以在通过电压差计算时相互抵消。
(1)串行、并行主要是考虑通信线的根数,就是发送方和接收方同时可以传递的信息量的多少。例如同时发送8位二进制数,对于电平信号,需要9根线(8根信号线+1根参考线);对于差分信号,需要16根线(2根组成1对,一共8对)。
(2)在实际使用时,串行接口应用更广泛,因为更省信号线,而且对传输线的要求更低、成本更低;而且串行时可以通过提高通信速度来提高总体通信性能,不一定非得要并行。2条线的串行通信方式每次只能传输1个二进制位。
最常用的搭配方式为:异步、串行、差分,譬如USB和网络通信。
指的是串口通信的速率,也就是串口通信时每秒钟可以传输多少个二进制位。譬如每秒种可以传输9600个二进制位(传输一个二进制位需要的时间是1/9600秒,也就是104us),波特率就是9600。一般最常见的波特率是9600或者115200(低端单片机如51常用9600,高端单片机和嵌入式SoC一般用115200)。通信双方必须事先设定相同的波特率这样才能成功通信,如果发送方和接收方按照不同的波特率通信则根本收不到。
(1)串口通信时,收发是一个周期一个周期进行的,每周期传输n个二进制位。这一个周期就叫做一个通信单元,一个通信单元是由:起始位+数据位+奇偶校验位+停止位组成的(又叫做一帧)。
(2)起始位表示发送方要开始发送一个通信单元,起始位是串口通信标准事先指定的,是由通信线上的电平变化来反映的,也就是时序。数据位是一个通信单元中发送的有效信息位,也就是本次通信真正要发送的有效数据,大部分情况下数据位是8位,因为通过串口发送的文字信息都是ASCII码编码的,而ASCII码中一个字符刚好编码为8位。奇偶校验位是用来校验数据位,把待校验的有效数据逐个位的加起来,总和为奇数奇偶校验位就为1,总和为偶数奇偶校验位就为0。停止位是发送方用来表示本通信单元结束标志的,一般为1位。
总结:串口通信时因为是异步通信,所以通信双方必须事先约定好通信参数,这些通信参数包括:波特率、数据位、奇偶校验位、停止位(串口通信中起始位定义是唯一的,所以一般不用选择)。
(1)单工就是单方向传输,表示只能A发B收。
(2)双工就是双方同时收发,A发B收的同时也能B发A收。
(3)半双工就是只能单方向但是方向可以改变,A发B收或者B发A收(两个方向不能同时)。
本次课程中选用的FS4412开发板,使用了如下三种串行通讯:
双线: uart4 全双工 异步 TXD------RXD
RXD-----TXD
双线: i2c8 半双工 同步 SCL------SCL
SDA------SDA
三线: SPI*3 全双工 同步 SCL------SCL
MISO------MOSI
MOSI------MISO
供电范围在0~5V。
对输出:大于2.4V是高电平;小于0.4V是低电平。
对输入:大于2V是高电平;小于0.8V是低电平。
对输出:输出“1”时的电平应在-3~-15 V之间,输出“0”时的电平应在+3~+15 V之间。
对输入:输入电平在-3~-15 V之间被认为“1”,在+3~+15 V之间被认为“0”。
当线路上不传送数据(空闲)时,发送器输出为“1”。
双向传输,全双工通信,最高传输速率20kbps。
对输出:逻辑"1"以两线间的电压差为+2v ~ +6表示;逻辑"0"以两线间的电压差为-2V ~ -6V 表示。
对输入:A比B高200mV以上即认为是逻辑"1",A 比B 低200mV 以上即认为是逻辑"0"。
双向差分传输,半双工通信,最高传输速率10Mbps。
与RS485的电平标准相同,发送口与接收口不同,如若将其并连就变成了RS485。
相当于两个半双工的RS485构成了一个全双工通信,最高速率10Mbps。
(1)任何通信都要有信息传输载体,或者是有线的或者是无线的。串口通信是有线通信,是通过串口线来通信的。
(2)串口通信线最少需要2根(GND和信号线),可以实现单工通信,也可以使用3根通信线(Tx、Rx、GND)来实现全双工。一般开发板都会引出SoC上串口引脚直接输出的TTL电平的串口(X210开发板没有),插座用插针式插座,每个串口引出的都有3个线(Tx、Rx、GND),可以用这些插座直接连接外部的TTL电平的串口设备。
串口通信的发送方每隔一定时间(时间固定为1/波特率,单位是秒)将有效信息(1或者0)放到通信线上去,逐个二进制位的进行发送。
接收方通过定时(起始时间由读到起始位标志开始,间隔时间由波特率决定)读取通信线上的电平高低来区分发送给我的是1还是0。依次读取数据位、奇偶校验位、停止位,停止位就表示这一个通信单元(帧)结束,然后中间是不定长短的非通信时间(发送方有可能紧接着就发送第二帧,也可能半天都不发第二帧,这就叫异步通信),接着发送第二帧·····
对于典型的串口设计,发送/接收缓冲区只有1字节,因此每次发送/接收时只能处理1帧数据。在复杂的SoC中CPU的时钟远高于串口发送端,会导致CPU需要不断切换上下文(每发完1帧数据就需要重新切换回发送端),大大降低了速率。
如何像icache一样提供一个解决两者速率差异较大的方法?解决方案就是想办法扩展串口控制器的发送/接收缓冲区,例如将发送/接收缓冲器设置为64字节,CPU一次过来直接给发送缓冲区64字节的待发送数据,然后transmitter慢慢发,发完再找CPU再要64字节。但是串口控制器本来的发送/接收缓冲区是固定的1字节长度的,所以做了个变相的扩展,就是FIFO(first in first out),先进入缓冲区的先出来,从而不影响顺序。
CPU来一次直接给FIFO了64字节的内容,然后FIFO一个字节一个字节的给发送缓冲区,此时就不需要CPU的参与,大大提高了效率。
DMA direct memory access,直接内存访问。 DMA本来是DSP中的一种技术,DMA技术的核心就是在交换数据时不需要CPU参与,模块可以自己完成。DMA模式要解决的问题和上面FIFO模式是同一个问题,就是串口发送/接收要频繁的折腾CPU造成CPU反复切换上下文导致系统效率低下。
传统的串口工作方式(无FIFO无DMA)效率是最低的,适合低端单片机;高端单片机上CPU事物繁忙所以都需要串口能够自己完成大量数据发送/接收。这时候就需要FIFO或者DMA模式。FIFO模式是一种轻量级的解决方案,DMA模式适合大量数据迸发式的发送/接收时。
首先说明一下为什么串口叫UART,universal asynchronous reciver and transmitter,通用异步收发器,即可知UART的通信方式是异步的。
(1)从图中可以看出,整个串口控制器包含transmitter和receiver两部分,两部分功能彼此独立,transmitter负责4412向外部发送信息,receiver负责从外部接收信息到4412内部。
(2)从上面的的时钟部分可知,串口控制器是接在PERIL总线上的。对我们编程有影响的是:将来计算串口控制器的源时钟时是以PERIL总线来计算的。
(3)transmitter由发送缓冲区和发送移位器构成。 在发送信息时,首先将信息进行编码成二进制流,然后将一帧数据写入发送缓冲区,发送移位器会自动从发送缓冲区中读取一帧数据,然后自动移位(移位的目的是将一帧数据的各个位分别拿出来)将其发送到TX(发送端)通信线上。
(4)receiver由接收缓冲区和接收移位器构成。 当有人通过串口线向我发送信息时,信息通过RX(接收端)通信线进入我的接收移位器,然后接收移位器自动移位将该二进制位保存到我的接收缓冲区,接收完一帧数据后receiver会产生一个中断给CPU,CPU收到中断后即可知道receiver接收满了一帧数据,就会来读取这帧数据。
总结:发送缓冲区和接收缓冲区是关键。发送移位器和接收移位器的工作都是自动的,不用编程控制的,所以我们写串口的代码就是:首先初始化串口控制器(初始化的实质是读写寄存器,包括发送控制器和接收控制器),然后将要发送信息时直接写入发送缓冲区,要接收信息时直接去接收缓冲区读取即可。
串口通信分为发送/接收2部分。发送方一般不需要(也可以使用)中断即可完成发送,接收方必须(一般来说必须,也可以轮询方式接收)使用中断来接收。本实验采用轮询的方式接收,通过状态寄存器中有一个位叫发送缓冲区空标志,transmitter发送完成(发送缓冲区空了)就会给这个标志位置位,CPU就是通过不断查询这个标志位为1还是0来知道发送是否已经完成的。
从图中可以看出波特率的产生需要时钟的提供(Clock Source),所以transmitter和receiver都需要一个时钟信号。
由上述可得,源时钟信号是外部总线(PERIL,100MHz)提供给串口模块的,然后进到串口控制器内部后给波特率发生器(实质上是一个分频器),在波特率发生器中进行分频,分频后得到一个低频时钟,这个时钟就是给transmitter和receiver使用的。
初始化
管脚设置为UART模式
串口协议设置(奇偶校验位,数据位等)
串口波特率设置
发送字符
发送状态判断
发送
接收字符后环回
接收状态判断
接收
.global delay1s @.C文件要调用delay1s函数,因此要设置成全局函数
.text
.global _start @
_start:
b reset @0x00
ldr pc,_undefined_instruction @0x04
ldr pc,_software_interrupt
ldr pc,_prefetch_abort
ldr pc,_data_abort
ldr pc,_not_used
ldr pc,_irq
ldr pc,_fiq
_undefined_instruction: .word _undefined_instruction
_software_interrupt: .word _software_interrupt
_prefetch_abort: .word _prefetch_abort
_data_abort: .word _data_abort
_not_used: .word _not_used
_irq: .word _irq
_fiq: .word _fiq
reset:
ldr r0,=0x40008000 @设置异常向量表的起始地址为 0x40008000
mcr p15,0,r0,c12,c0,0 @ Vector Base Address Register
init_stack:
ldr r0,stacktop /*get stack top pointer*/
/********svc mode stack********/@设置各种模式的堆栈
mov sp,r0
sub r0,#128*4 /*512 byte for irq mode of stack*/
/****irq mode stack**/
msr cpsr,#0xd2 /* 初始化阶段要禁止IRQ,FIQ中断,I位=1,r位=1,mode位=10010 集合到一起就是d2 */
mov sp,r0
sub r0,#128*4 /*512 byte for irq mode of stack*/
/***fiq mode stack***/
msr cpsr,#0xd1
mov sp,r0
sub r0,#0
/***abort mode stack***/
msr cpsr,#0xd7
mov sp,r0
sub r0,#0
/***undefine mode stack***/
msr cpsr,#0xdb
mov sp,r0
sub r0,#0
/*** sys mode and usr mode stack ***/
msr cpsr,#0x10
mov sp,r0 /*1024 byte for user mode of stack*/
b main
delay1s: /* 延时1s函数 */
ldr r4,=0x1ffffff
delay1s_loop:
sub r4,r4,#1
cmp r4,#0
bne delay1s_loop
mov pc,lr
.align 4 /* 4字节对齐 */
/**** swi_interrupt handler ****/
stacktop: .word stack+4*512
.data
stack:
.space 4*512
.end
#define GPA1CON 0x11400020 #define ULCON2 0x13820000 #define UCON2 0x13820004 #define UBRDIV2 0x13820028 #define UFRACVAL2 0x1382002c #define UTXH2 0x13820020 #define UTRSTAT2 0x13820010 #define rGPA1CON (*(volatile unsigned int*)GPA1CON) #define rULCON2 (*(volatile unsigned int*)ULCON2) #define rUCON2 (*(volatile unsigned int*)UCON2) #define rUBRDIV2 (*(volatile unsigned int*)UBRDIV2) #define rUFRACVAL2 (*(volatile unsigned int*)UFRACVAL2) #define rUTXH2 (*(volatile unsigned int*)UTXH2) #define rUTRSTAT2 (*(volatile unsigned int*)UTRSTAT2) void uart_putc(char c); int main(int argc, const char *argv[]) { /* 设置GPA1控制器为UART模式 */ rGPA1CON &= ~(0xff<<0); //把寄存器的bit0~7全部清零 rGPA1CON |= 0x22<<0; //Rx,Tx /* 设置串口协议 */ rULCON2 = 0x03; //0校验位 ,8数据位,1停止位 rUCON2 = 0x05; //轮询模式 /* * 设置波特率: *UART时钟信号源的值为: *100Mhz= 100 000khz = 100 000 000hz *本实验波特率值位115200,DIV_VAL = 100000000/(115200*16) -1 = 54.25 -1 = 53.25 *UBRDIVn = 53 *UFRACVALn/16 = 0.25 ----> UFRACVALn = 4 */ rUBRDIV2 = 53; rUFRACVAL2 = 4; /* 发送状态判断 */ while(1) { uart_putc('c'); delay1s(); } return 0; } void uart_putc(char c) { while(!(rUTRSTAT2&0x02)); rUTXH2 = c; return; }
找寻到 UART_AUDIO_TXD 和 UART_AUDIO_RXD
对应的芯片端接口 GPA1_1 和 GPA1_0,查找4412芯片手册,找到GPA1接口的设定GPA1CON
Base Address: 0x1140_0000
Address = Base Address + 0x0020, Reset Value = 0x0000_0000
管脚设置为UART模式:0x11400 0020 = 0x0000 0022
接下来设置串口协议(奇偶校验位,数据位等),查看4412芯片手册目录,找到Universal Asynchronous Receiver and Transmitter(即UART):
查看UART模块介绍:
找到串口协议设置相关寄存器:
将字长设置为8其余全部默认值即可:0x1382 0000 = 0x0000 0003
将传输模式改为polling模式,接收模式也改为polling模式。0x1382_0004 = 0x0000 0005
接下来进行波特率的设置:
第三第四个寄存器值的算法:
查4412手册,查看UART的时钟源频率:
UART时钟信号源的值为:
100Mhz= 100 000khz = 100 000 000hz
本实验波特率值位115200,DIV_VAL = 100000000/(115200*16) -1 = 54.25 -1 = 53.25
UBRDIVn = 53
UFRACVALn/16 = 0.25 ----> UFRACVALn = 4
本次实验使用的链接文件 map.lds:
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") /* 指定输出可执行文件是elf格式,32位ARM指令,小端 */
OUTPUT_ARCH(arm) /* 设置输出文件的架构体系为arm架构 */
ENTRY(_start) /* 设置入口点为_start */
SECTIONS /* 段落 */
{
. = 0x40008000; /* 初始地址 */
. = ALIGN(4); /* 四字节对齐 */
.text : /* 代码段 */
{
start.o(.text) /* 从start.o 文件的.text段落开始执行 首先给它分配地址空间*/
*(.text) /* 其余链接文件的.text自动分配地址 */
}
. = ALIGN(4); /* 4字节对齐 */
.data : /* 数据段 */
{ *(.data) } /* 所有链接文件中的数据段均自动分配地址 */
. = ALIGN(4); /* 4字节对齐 */
.bss : /* 未初始化的数据段 */
{ *(.bss) } /* 所有链接文件中的未初始化的数据段均自动分配地址 */
}
.lds文件分析:
对于.lds文件,它定义了整个程序编译之后的连接过程,决定了一个可执行程序的各个段的存储位置。虽然现在我还没怎么用它,但感觉还是挺重要的,有必要了解一下。
先看一下 GNU官方网站上:http://www.gnu.org/
对.lds文件形式的完整描述:
SECTIONS {
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
}
secname和contents是必须的,其他的都是可选的。下面挑几个常用的看看:
1、secname:段名
2、contents:决定哪些内容放在本段,可以是整个目标文件,也可以是目标文件中的某段(代码段、数据段等)
3、start:本段连接(运行)的地址,如果没有使用AT(ldadr),本段存储的地址也是start。GNU网站上说start可以用任意一种描述地址的符号来描述。
4、AT(ldadr):定义本段存储(加载)的地址。
看一个简单的例子:(摘自《2410完全开发》)
/* nand.lds */
SECTIONS {
firtst 0x00000000 : { head.o init.o }
second 0x30000000 : AT(4096) { main.o }
}
以上,head.o放在0x00000000地址开始处,init.o放在head.o后面,他们的运行地址也是0x00000000,即连接和存储地址相同(没有AT指定);main.o放在4096(0x1000,是AT指定的,存储地址)开始处,但是它的运行地址在0x30000000,运行之前需要从0x1000(加载处)复制到0x30000000(运行处),此过程也就用到了读取Nand flash。
这就是存储地址和连接(运行)地址的不同,称为加载时域和运行时域,可以在.lds连接脚本文件中分别指定。
编写好的.lds文件,在用arm-linux-ld连接命令时带-Tfilename来调用执行,如
arm-linux-ld –Tnand.lds x.o y.o –o xy.o。也用-Ttext参数直接指定连接地址,如
arm-linux-ld –Ttext 0x30000000 x.o y.o –o xy.o。
既然程序有了两种地址,就涉及到一些跳转指令的区别,这里正好写下来,以后万一忘记了也可查看,以前不少东西没记下来现在忘得差不多了。。。
ARM汇编中,常有两种跳转方法:b跳转指令、ldr指令向PC赋值。
我自己经过归纳如下:
(1)
b step1 :b跳转指令是相对跳转,依赖当前PC的值,偏移量是通过该指令本身的bit[23:0]算出来的,这使得使用b指令的程序不依赖于要跳到的代码的位置,只看指令本身。
(2)
ldr pc, =step1 :该指令是从内存中的某个位置(step1)读出数据并赋给PC,同样依赖当前PC的值,但是偏移量是那个位置(step1)的连接地址(运行时的地址),所以可以用它实现从Flash到RAM的程序跳转。
(3)
此外,有必要回味一下adr伪指令,U-boot中那段relocate代码就是通过adr实现当前程序是在RAM中还是flash中。仍然用我当时的注释:
relocate: /* 把U-Boot重新定位到RAM /
adr r0, _start / r0是代码的当前位置 /
/ adr伪指令,汇编器自动通过当前PC的值算出 如果执行到_start时PC的值,放到r0中:
当此段在flash中执行时r0 = _start = 0;当此段在RAM中执行时_start =_TEXT_BASE(在board/smdk2410/config.mk中指定的值为0x33F80000,即u-boot在把代码拷贝到RAM中去执行的代码段的开始) /
ldr r1, _TEXT_BASE / 测试判断是从Flash启动,还是RAM /
/ 此句执行的结果r1始终是0x33FF80000,因为此值是又编译器指定的(ads中设置,或-D设置编译器参数) /
cmp r0, r1 / 比较r0和r1,调试的时候不要执行重定位 */
下面,结合u-boot.lds看看一个正式的连接脚本文件。
OUTPUT_FORMAT("elf32littlearm","elf32littlearm","elf32littlearm")
;指定输出可执行文件是elf格式,32位ARM指令,小端
OUTPUT_ARCH(arm)
;指定输出可执行文件的平台为ARM
ENTRY(_start)
;指定输出可执行文件的起始代码段为_start.
SECTIONS
{
. = 0x00000000 ; 从0x0位置开始
. = ALIGN(4) ; 代码以4字节对齐
.text : ;指定代码段
{
cpu/arm920t/start.o (.text) ; 代码的第一个代码部分
*(.text) ;其它代码部分
}
. = ALIGN(4)
.rodata : { *(.rodata) } ;指定只读数据段
. = ALIGN(4);
.data : { *(.data) } ;指定读/写数据段
. = ALIGN(4);
.got : { *(.got) } ;指定got段, got段式是uboot自定义的一个段, 非标准段
__u_boot_cmd_start = . ;把__u_boot_cmd_start赋值为当前位置, 即起始位置
.u_boot_cmd : { *(.u_boot_cmd) } ;指定u_boot_cmd段, uboot把所有的uboot命令放在该段.
__u_boot_cmd_end = .;把__u_boot_cmd_end赋值为当前位置,即结束位置
. = ALIGN(4);
__bss_start = .; 把__bss_start赋值为当前位置,即bss段的开始位置
.bss : { *(.bss) }; 指定bss段
_end = .; 把_end赋值为当前位置,即bss段的结束位置
}
all:
arm-none-linux-gnueabi-gcc -fno-builtin -nostdinc -c -o start.o start.S //生成.o汇编文件
arm-none-linux-gnueabi-gcc -fno-builtin -nostdinc -c -o main.o main.c //生成.o汇编文件
arm-none-linux-gnueabi-ld start.o main.o -Tmap.lds -o uart.elf //两个.o汇编文件基于map.lds配置文件合并 生成.elf可GDB调试文件
arm-none-linux-gnueabi-objcopy -O binary uart.elf uart.bin //.elf文件生成.bin二进制文件 用于烧录到开发板
arm-none-linux-gnueabi-objdump -D uart.elf > uart.dis //.elf文件生成.dis反汇编文件方便追踪调试
clean:
rm -rf *.bak start.o main.o uart.elf uart.bin uart.dis
一般情况,一个程序本质上都是由 bss段、data段、text段三个段组成——这是计算机程序设计中重要的基本概念。而且在嵌入式系统的设计中也非常重要,牵涉到嵌入式系统运行时的内存大小分配,存储单元占用空间大小的问题。
在采用段式内存管理的架构中(比如intel的80x86系统),bss段(Block Started by Symbol segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,一般在初始化时bss 段部分将会清零(bss段属于静态内存分配,即程序一开始就将其清零了)。
比如,在C语言程序编译完成之后,已初始化的全局变量保存在.data 段中,未初始化的全局变量保存在.bss 段中。
text段: 用于存放程序代码的区域, 编译时确定, 只读。更进一步讲是存放处理器的机器指令,当各个源文件单独编译之后生成目标文件,经连接器链接各个目标文件并解决各个源文件之间函数的引用,与此同时,还得将所有目标文件中的.text段合在一起,但不是简单的将它们“堆”在一起就完事,还需要处理各个段之间的函数引用问题。
在嵌入式系统中,如果处理器是带MMU(MemoryManagement Unit,内存管理单元),那么当我们的可执行程序被加载到内存以后,通常都会将.text段所在的内存空间设置为只读,以保护.text中的代码不会被意外的改写(比如在程序出错时)。当然,如果没有MMU就无法获得这种代码保护功能。
data段 :用于存放在编译阶段(而非运行时)就能确定的数据,可读可写。也是通常所说的静态存储区,赋了初值的全局变量、常量和静态变量都存放在这个域。
而bss段不在可执行文件中,由系统初始化。
bss段(未手动初始化的数据)并不给该段的数据分配空间,只是记录数据所需空间的大小。
data段(已手动初始化的数据)为数据分配空间,数据保存在目标文件中。
data段包含经过初始化的全局变量以及它们的值。
BSS段的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段后面。当这个内存区进入程序的地址空间后全部清零,包含data和bss段的整个区段此时通常称为数据区。