UART驅動程式設計
UART,全稱Universal Asynchronous Receiver/Transmitter,通用非同步收發傳輸器,也稱串列埠。本文出於在bootloader中要使用串列埠作為控制檯的需求,特意編寫串列埠驅動程式碼,和讀者一起學習!
相信觸過嵌入式行業的程式猿們都使用過串列埠作為系統的除錯工具。在之前學習stm32的過程中,同學們都習慣使用庫函式的方式直接呼叫或移植串列埠程式碼,很少有人真正的去分析串列埠的工作機理(我就是這樣滴),也很少有人自己從頭到尾去編寫過串列埠的驅動程式碼。和上一篇編寫NandFlash驅動程式的思路類似,本文首先簡述串列埠的工作機理,再帶領讀者去編寫串列埠的驅動程式碼,最後在bootloader平臺上去驗證程式的準確性。
1、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。
歡迎關注個人訂閱號,共勉!