UART驱动程序设计

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



可以在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。 
 

你可能感兴趣的:(#,字符驱动)