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。
