UART串口这个东西,是嵌入式学习上避不开的,不仅在调试中经常用到,还有很多模块通过串口与SOC相连。这篇文章让你彻彻底底,搞明白串口程序的编写。
没有基础的先看:
嵌入式Linux学习系列全部文章:嵌入式Linux学习—从裸机到应用教程大全
目录
全称:通用异步收发传输器(Universal Asynchronous Receiver/Transmitter,简称UART)是一种串行异步收发协议。
用的最多的地方就是开发板的串口连接电脑发送信息了,我们先看看电脑端什么样的:
UART硬件连接比较简单,仅需要3条线,如下图所示:
TX:发送数据端,要接对面设备的RX
RX:接收数据端,要接对面设备的TX
GND:保证两设备共地,有统一的参考平面
和电脑连接用这个东西:
这个东西可以把RS232电平转换为TTL电平,为什么要转换电平,这里不赘述,随便搜一下就知道了。如果你的开发板集成了电平转换就不需要这个东西,直接用USB线连接到电脑。
首先我们要知道UART协议中数据是一位一位(0或1)发送的,并且连续的一串数据被分成了一帧一帧发送的,下图便是一帧数据(不包含空闲位)。
uart传输数据的顺序就是:刚开始传输一个起始位,接着传输数据位,接着传输校验位(可不需要此位),最后传输停止位。这样一帧的数据就传输完了。接下来接着像上述过程一直传送。
协议如下:
空闲位:
UART协议规定,当总线处于空闲状态时信号线的状态为‘1’即高电平
起始位:
开始进行数据传输时,发送方先发出一个低电平’0’,表示传输字符的开始。
数据位:
起始位之后就是要传输的数据,数据可以是5,6,7,8,9位,构成一个字符,一般都是8位。
传输方向:即数据是从高位(MSB)开始传输还是从低位(LSB)开始传输。比如传输“A”如果是MSB那么就是01000001,如果是LSB那么就是10000010
奇偶校验位:
数据位传送完成后,要进行奇偶校验,校验位其实是调整个数,串口校验分几种方式:
1.无校验(no parity)
2.奇校验(odd parity):如果数据位中’1’的数目是偶数,则校验位为’1’,如果’1’的数目是奇数,校验位为’0’。
3.偶校验(even parity):如果数据为中’1’的数目是偶数,则校验位为’0’,如果为奇数,校验位为’1’。
4.mark parity:校验位始终为1
5.space parity:校验位始终为0
以传输“A”(01000001)为例:
1、当为奇数校验:”A”字符的8个bit位中有两个1,那么奇偶校验位为1才能满足1的个数为奇数(奇校验)。
2、当为偶数校验:”A”字符的8个bit位中有两个1,那么奇偶校验位为0才能满足1的个数为偶数(偶校验)。
通过配置相应寄存器,此位可以去除,即不需要奇偶校验位。通常是不需要的。
停止位:
数据结束标志,可以是1位,1.5位,2位的高电平。
波特率:
数据传输速率使用波特率来表示,单位bps(bits per second),常见的波特率9600bps,115200bps等等,其他标准的波特率是1200,2400,4800,19200,38400,57600。
例如:串口波特率设置为9600bps,那么传输一个比特需要的时间是1/9600≈104.2us。
再例如:数据传送速率为120字符/秒,而每一个字符为10位(1个起始位,7个数据位,1个校验位,1个结束位),则其传送的波特率为10×120=1200位/秒=1200波特。
手册告诉我们S3C2440有三个UART,那么哪个能用呢?我们找开发板上那个转了USB的方便与电脑连接。你手上的可能不一样,随便用一个就行。
翻一翻开发板的原理图
找到了,我的开发板有串口转USB功能
接着看,RxD0和TxD0连到了S3C2440的哪个引脚,
搜索一下,找到了,GPH2和GPH3,我们就用他了。
翻开S3C2440的数据手册,找到IO那一章。
找到GPIOH的控制寄存器地址:0x56000070.
GPH3配置为TXD0M,就是把GPIOH第6、7为分别置为1和0,GPH2同理。
代码就出来了
- /* 设置引脚用于串口 */
- /* GPH2,3用于TxD0, RxD0 */
- volatile unsigned int *GPHCON=0x56000070;
- *GPHCON &= ~((3<<4) | (3<<6));
- *GPHCON |= ((2<<4) | (2<<6));
不懂volatile和位运算的可以看这篇:嵌入式C语言重点(const、static、voliatile、位运算)
别忘了,前面说过:
空闲位:
UART协议规定,当总线处于空闲状态时信号线的状态为‘1’即高电平。
因此还得把端口内部上拉电阻设置一下,让他在空闲时,输出高电平
找到寄存器GPHUP的地址:0x56000078.
把寄存器GPHUP第2、3位设置为0就行。
- volatile unsigned int *GPHUP=0x56000078;
- *GPHUP &= ~((1<<2) | (1<<3)); /* 使能内部上拉 */
根据第一部分内容,我们知道,要设置帧格式:校验位、停止位、数据长度、波特率
目标:校验位:无,停止位1,数据长度:8,波特率:115200
首先找到控制帧格式的寄存器:
ULCON0地址为0x50000000。
校验位:
校验位设置如上图,我们不需要校验位,刚好默认就是没有,不用设置了。
停止位:
停止位设置如上图,我们设置为1位停止位,刚好默认值也是1位,又不用设置了。
数据位:
我们想设置为8位长度。
这次不能用默认了,得把1、0位设置为1、1.
- volatile unsigned int *ULCON0=0x50000000;
- /* 设置数据格式 */
- *ULCON0 = 0x00000003; /*8个数据位 */
波特率
UART clock可以用PCLK、FCLK\n、UEXTCLK,我们就用PCLK
我们想让波特率buad rate=115200,
根据上面公式,计算一下
UBRDIVn = (int)( UART clock / ( buad rate x 16) ) –1
UART clock = 50M
UBRDIVn = (int)( 50000000 / ( 115200 x 16) ) –1 = 26
上图又表明UBRDIV0地址为0x50000028,代码就出来了。
- volatile unsigned int *UBRDIV0=0x50000028;
- *UBRDIV0 = 26;
UART模式
还得设置一下控制器,选择传送模式,UART支持DMA,但是我们不用。
包括上面提到的
UART clock可以用PCLK、FCLK\n、UEXTCLK,我们用PCLK也得设置一下
这两个设置都在UART控制寄存器。
找到UCON0地址0x50000004.
默认UART clock就是用PCLK,不用管了。
我们用这个中断或轮询模式。
- volatile unsigned int *UCON0=0x50000004;
- *UCON0 = 0x00000005; /* PCLK,中断/查询模式 */
综合上述,得到UART初始化代码
- volatile unsigned int *GPHCON=0x56000070;
- volatile unsigned int *GPHUP=0x56000078;
- volatile unsigned int *ULCON0=0x50000000;
- volatile unsigned int *UBRDIV0=0x50000028;
- volatile unsigned int *UCON0=0x50000004;
-
- /* 设置引脚用于串口 */
- /* GPH2,3用于TxD0, RxD0 */
- *GPHCON &= ~((3<<4) | (3<<6));
- *GPHCON |= ((2<<4) | (2<<6));
-
- *GPHUP &= ~((1<<2) | (1<<3)); /* 使能内部上拉 */
-
- /* 设置数据格式 */
- *ULCON0 = 0x00000003; /* 8n1: 8个数据位, 无较验位, 1个停止位 */
-
- /* 设置波特率 */
- /* UBRDIVn = (int)( UART clock / ( buad rate x 16) ) –1
- * UART clock = 50M
- * UBRDIVn = (int)( 50000000 / ( 115200 x 16) ) –1 = 26
- */
- *UBRDIV0 = 26;
- /* PCLK,中断/查询模式 */
- *UCON0 = 0x00000005;
UART发送和接收分别有寄存器来保存数据,同时又有相应的状态寄存器。可以读取状态寄存器的值来判断发送或者接收完数据没有。
这里就直接给出简单的发送接收代码,大家可以自己去芯片手册找到寄存器,要多读手册,才能提高水平。
- int putchar(int c)
- {
- /* UTRSTAT0 */
- /* UTXH0 */
-
- while (!(UTRSTAT0 & (1<<2)));
- UTXH0 = (unsigned char)c;
-
- }
-
- int getchar(void)
- {
- while (!(UTRSTAT0 & (1<<0)));
- return URXH0;
- }
-
- int puts(const char *s)
- {
- while (*s)
- {
- putchar(*s);
- s++;
- }
- }
启动代码和makefile先给出,不知道怎么来的,先看一下我之前的两篇文章:
1.嵌入式Linux入门-从启动代码开始,真正从0开始点个灯
2.嵌入式Linux入门-读数据手册,设置时钟,让代码跑得更快
启动代码:
- .text
- .global _start
-
- _start:
-
- /* 关闭看门狗 */
- ldr r0, =0x53000000
- ldr r1, =0
- str r1, [r0]
-
- /* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m */
- /* LOCKTIME(0x4C000000) = 0xFFFFFFFF */
- ldr r0, =0x4C000000
- ldr r1, =0xFFFFFFFF
- str r1, [r0]
-
- /* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8 */
- ldr r0, =0x4C000014
- ldr r1, =0x5
- str r1, [r0]
-
- /* 设置CPU工作于异步模式 */
- mrc p15,0,r0,c1,c0,0
- orr r0,r0,#0xc0000000 //R1_nF:OR:R1_iA
- mcr p15,0,r0,c1,c0,0
-
- /* 设置MPLLCON(0x4C000004) = (92<<12)|(1<<4)|(1<<0)
- * m = MDIV+8 = 92+8=100
- * p = PDIV+2 = 1+2 = 3
- * s = SDIV = 1
- * FCLK = 2*m*Fin/(p*2^s) = 2*100*12/(3*2^1)=400M
- */
- ldr r0, =0x4C000004
- ldr r1, =(92<<12)|(1<<4)|(1<<0)
- str r1, [r0]
-
- /* 一旦设置PLL, 就会锁定lock time直到PLL输出稳定
- * 然后CPU工作于新的频率FCLK
- */
-
- /* 设置内存: sp 栈 */
- ldr sp, =4096 /* nand启动 */
-
- bl main
-
- halt:
- b halt
Makefile:
- all:
- arm-linux-gcc -c -o uart.o uart.c
- arm-linux-gcc -c -o start.o start.S
- arm-linux-ld -Ttext 0 start.o uart.o -o uart.elf
- arm-linux-objcopy -O binary -S uart.elf uart.bin
- clean:
- rm *.bin *.o *.elf
c代码:
在main函数中向电脑发个“Hello World”,并且回送电脑发过来的数据
- #include
- int putchar(int c)
- {
- /* UTRSTAT0 */
- volatile unsigned int *UTRSTAT0=0x50000010;
- volatile unsigned int *UTXH0=0x50000020;
-
- /* UTXH0 */
- while (!(*UTRSTAT0 & (1<<2)));
- *UTXH0 = (unsigned char)c;
- }
-
- int getchar(void)
- {
- volatile unsigned int *UTRSTAT0=0x50000010;
- volatile unsigned int *URXH0=0x50000024;
- while (!(*UTRSTAT0 & (1<<0)));
- return *URXH0;
- }
-
- int puts(const char *s)
- {
- while (*s)
- {
- putchar(*s);
- s++;
- }
- }
- int uart0_init(void)
- {
- volatile unsigned int *GPHCON=0x56000070;
- volatile unsigned int *GPHUP=0x56000078;
- volatile unsigned int *ULCON0=0x50000000;
- volatile unsigned int *UBRDIV0=0x50000028;
- volatile unsigned int *UCON0=0x50000004;
-
- /* 设置引脚用于串口 */
- /* GPH2,3用于TxD0, RxD0 */
- *GPHCON &= ~((3<<4) | (3<<6));
- *GPHCON |= ((2<<4) | (2<<6));
-
- *GPHUP &= ~((1<<2) | (1<<3)); /* 使能内部上拉 */
-
- /* 设置数据格式 */
- *ULCON0 = 0x00000003; /* 8n1: 8个数据位, 无较验位, 1个停止位 */
-
- /* 设置波特率 */
- /* UBRDIVn = (int)( UART clock / ( buad rate x 16) ) –1
- * UART clock = 50M
- * UBRDIVn = (int)( 50000000 / ( 115200 x 16) ) –1 = 26
- */
- *UBRDIV0 = 26;
- /* PCLK,中断/查询模式 */
- *UCON0 = 0x00000005;
-
- }
- int main(void)
- {
- unsigned char c;
- uart0_init();
- puts("Hello, world!\n\r");
-
- while(1)
- {
- c = getchar();
- if (c == '\r')
- {
- putchar('\n');
- }
-
- if (c == '\n')
- {
- putchar('\r');
- }
-
- putchar(c);
- }
-
- return 0;
- }
make命令,得到二进制文件,烧写,结果:
Hello,world出现了,随便输入也能回显,完美。