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<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。
歡迎關注個人訂閱號,共勉!
這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章