UART,全称Universal Asynchronous Receiver/Transmitter,通用异步收发传输器,也称串口。本文出于在bootloader中要使用串口作为控制台的需求,特意编写串口驱动代码,和读者一起学习!
相信触过嵌入式行业的程序猿们都使用过串口作为系统的调试工具。在之前学习stm32的过程中,同学们都习惯使用库函数的方式直接调用或移植串口代码,很少有人真正的去分析串口的工作机理(我就是这样滴),也很少有人自己从头到尾去编写过串口的驱动代码。和上一篇编写NandFlash驱动程序的思路类似,本文首先简述串口的工作机理,再带领读者去编写串口的驱动代码,最后在bootloader平台上去验证程序的准确性。
1、UART介绍
参考:http://baike.sogou.com/v16237.htm?fromTitle=UART
2、UART驱动实现
1)串口初始化
首先配置引脚功能(查看原理图,可知发送和接收脚为GPH2和GPH3),再设置数据格式和工作模式(DMA、中断、轮询),最后设置波特率(115200)。引脚功能由GPHCON寄存器(Configures the pins of port H)设置,其为22位寄存器,每两位控制一个引脚,分别控制GPH0~GPH10,第[4:5]和[6:7]位设置为10时,分别表示UART0的TXD和RXD功能。
串口的数据格式由ULCON0寄存器(UART channel 0 line control register)来设置,设置为6个数据位,1个停止位,无校验位,所以在ULCON0中写入的数据为0b11。
设置串口工作在中断或轮询模式下,通过在UCON0寄存器(UART channel 0 control register)中写0b0101来实现。
设置串口波特率是通过UBRDIV0寄存器(Baud rate divisior register 0)来实现,根据如下公式:(查看2440的datasheet的时钟树–》串口时钟为PCLK)
在前面的时钟初始化中,设置系统的分频比为FCLK:HCLK:PCLK = 1:4:8,由于MPLL的时钟为400Mhz,则PCLK为MPLL时钟的1/8,等于50Mhz。代入公式即可求得写入UBRDIV0的数据。
以下为串口初始化代码:
#define GPHCON (*(volatile unsigned long*)0x56000070)
#define ULCON0 (*(volatile unsigned long*)0x50000000)
#define UCON0 (*(volatile unsigned long*)0x50000004)
#define UBRDIV0 (*(volatile unsigned long*)0x50000028)
void uart_init()
{
//1.配置引脚功能
GPHCON &= ~(0xf<<4);
GPHCON |= (0xa<<4);
//2.1 设置数据格式
ULCON0 = 0b11;
//2.2 设置工作模式
UCON0 = 0b0101;
//3. 设置波特率
UBRDIV0 =(int)(PCLK/(BAUD*16)-1);
}
2)数据发送
数据发送和接收很简单,串口发送数据时,会判断发送缓冲寄存器(通过检测UTRSTAT0寄存器(UART channel 0 Tx/Rx status register)的第2位)是否为空(如上图),若空则将发送的unsigned char 写入UTXH0寄存器(UART channel 0 transmit buffer register)。
代码如下:
#define UTRSTAT0 (*(volatile unsigned long*)0x50000010)
#define UTXH0 (*(volatile unsigned long*)0x50000020)
void putc(unsigned char ch)
{
while (!(UTRSTAT0 & (1<<2)));
UTXH0 = ch;
}
3)数据接收
和上面类似,检测接收缓冲寄存器是否为空(UTRSTAT0的第0位)。
代码如下:
#define URXH0 (*(volatile unsigned long*)0x50000024)
unsigned char getc(void)
{
unsigned char ret;
while (!(UTRSTAT0 & (1<<0)));
// 取数据
ret = URXH0;
return ret;
}
3、建立串口菜单型控制台
在bootloader中,当开启串口工具(SecureCRT)时,使用串口控制台完成其他功能,例如开启TFTP下载、下载linux到内核等。在main.c中编写以下代码:
while(1)
{
printf("\n***************************************\n\r");
printf("\n*****************GBOOT*****************\n\r");
printf("1:Download Linux Kernel from TFTP Server!\n\r");
printf("2:Boot Linux from RAM!\n\r");
printf("3:Boor Linux from Nand Flash!\n\r");
printf("\n Plese Select:");
scanf("%d",&num);
switch (num)
{
case 1: //case选项中的代码暂不实现,目的是搭好串口控制台
//tftp_load();
break;
case 2:
//boot_linux_ram();
break;
case 3:
//boot_linux_nand();
break;
default:
printf("Error: wrong selection!\n\r");
break;
}
}
对于上面的程序,最主要的是实现printf和scanf两个函数,前面已经写好了串口发送(putc)和接收字符(getc)的函数,在printf和scanf中要分别合理调用这两个收发函数。
先贴出printf的实现代码:
#include "vsprintf.h"
unsigned char outbuf[1024];
int printf(const char* fmt,...)
{
unsigned int i;
va_list args;
//1.将变参转化为字符串
va_start(args,fmt); //fmt转化为变参列表
vsprintf((char*)outbuf,fmt,args); // 变参列表转化为字符串
va_end(); //转化结束
//2.打印字符到串口
for(i=0;i<strlen((const char*)outbuf);i++)
{
putc(outbuf[i]);
}
return i;
}
可以在sheel里面查看printf的函数原型,命令:man 3 printf
对于 int printf(const char* fmt,…):其中…表示变参,fmt表示变参的格式。重点是理解va_start( )、vsprintf( )、va_end( )三个函数,这三个函数很复杂,可以直接从linux的内核源码中移植lib和include两个文件夹。
va_start( )、va_end( )两个函数在lib中vspprintf.h中实现的:
#define va_end(ap) (void) 0
#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
vsprintf( )在在lib中vsprintf.c文件中实现。
将编写的printf.c放在lib目录中,并在lib中的makefile中的目标依赖文件中加上printf.o:
objs := div64.o lib1funcs.o ctype.o muldi3.o printf.o string.o vsprintf.o
在lib中生成的最终文件为lib.o:
all : $(objs)
arm-linux-ld -r -o lib.o $^
scanf的实现代码:
unsigned char inbuf[1024];
int scanf(const char* fmt, ...)
{
unsigned char c;
int i = 0;
va_list args;
//1. 获取输入的字符串
while (1)
{
c = getc();
if ((c==0x0d) || (c==0x0a))
{
inbuf[i] = '\n';
break;
}
else
{
inbuf[i++] = c;
}
}
//2. 格式转化
va_start(args, fmt);
vsscanf((char *)inbuf,fmt,args);
va_end(args);
return i;
}
修改顶层makefile:
OBJS := start.o main.o dev/dev.o lib/lib.o
CFLAGS := -nostdinc -fno-builtin -I$(shell pwd)/include
export CFLAGS
gboot.bin : gboot.elf
arm-linux-objcopy -O binary gboot.elf gboot.bin
gboot.elf : $(OBJS)
arm-linux-ld -Tgboot.lds -o gboot.elf $^
%.o : %.S
arm-linux-gcc -g -c $^
%.o : %.c
arm-linux-gcc $(CFLAGS) -c $^
lib/lib.o :
make -C lib all
dev/dev.o :
make -C dev all
注意顶层makeflie和子目录中makefile的书写规则。
上面的参数CFLAGS作用:指定头文件(.h文件)的路径。如果没有指明路径,则include中的头文件可能不会被链接到。
对于有学习stm32经验的同学,如果要在Keil MDK中实现printf函数就相对简单,步骤如下:
1)在程序的顶部加上头文件#include”stdio.h”
2)然后在程序中加上以下函数:
int fputc(int ch,FILE *f)
{
USART_SendData(USART1,(u8) ch);
while(USART_GetFlagStatus(USART1,USART1,USART_FLAG_TC));
return ch;
}
3)在 Keil MDK中的option for Target,选中User MiicroLIB,然后点击OK即可使用函数printf。