UART驱动程序设计

mac2025-10-29  18

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。   

最新回复(0)