Home » 朱老师嵌入式 » uart stdio的移植

uart stdio的移植

编 辑:Y ┊ 时 间:2022年04月14日 ┊ 访问: 15 次

uart stdio的移植1

什么是stdio

(1)#include <stdio.h>
(2)stdio:standard input output,标准输入输出
(3)标准输入输出就是操作系统定义的默认的输入和输出通道。一般在PC机的情况下,标准输入指的是键盘,标准输出指的是屏幕。
(4)printf函数和scanf函数可以和底层输入/输出函数绑定,然后这两个函数就可以和stdio绑定起来。也就是说我们直接调用printf函数输出,内容就会被从标准输出输出出去。
(5)在我们这里,标准输出当然不是屏幕了,而是串口。标准输出也不是键盘,而是串口。

printf函数的工作原理

(1)printf函数工作时内部实际调用了2个关键函数:一个是vsprintf函数(主要功能是格式化打印信息,最终得到纯字符串格式的打印信息等待输出),另一个就是真正的输出函数putc(操控标准输出的硬件,将信息发送出去)

移植printf函数的三种思路

(1)我们希望在我们的开发板上使用printf函数进行(串口)输出,使用scanf函数进行(串口)输入,就像在PC机上用键盘和屏幕进行输入输出一样。因此需要移植printf函数/scanf函数
(2)我们说的移植而不是编写,我们不希望自己完全从新编写而是想尽量借用也有的代码(叫移植)
(3)一般移植printf函数可以有3个途径获取printf的实现源码:最原始最原本的来源就是linux内核中的printk。难度较大、关键是麻烦;稍微简单些的方法是从uboot中移植printf;更简单的方法就是直接使用别人移植好的。

(4)我们课程中使用第三种方法,别人移植好的printf函数来自于友善之臂的Tiny210的裸机教程中提供的。

移植printf

将lib和include放到项目目录

相关的makefile文件:

objs := div64.o lib1funcs.o ctype.o muldi3.o printf.o string.o vsprintf.o

libc.a: $(objs)
    ${AR} -r -o $@ $^
    
%.o:%.c
    ${CC} $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

%.o:%.S
    ${CC} $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

clean:
    rm -f libc.a *.o        
    

libc.a是一个库文件

用AR工具变成库了

将我们之前的uart.c里面的函数名相应的改一下

现在就可以把main中包含stdio.h

#include "stdio.h"

现在就可以调用printf()了

现在开始修改我们以前的makefile

改造makefile文件

现在我没增加一个相应的变量

objs := start.o led.o clock.o uart.o main.o 

定义其他相关:

CC        = arm-linux-gcc
LD         = arm-linux-ld
OBJCOPY    = arm-linux-objcopy
OBJDUMP    = arm-linux-objdump
AR        = arm-linux-ar

现在就可以把下面的替代掉了

uart.bin: $(objs)
    $(LD) -Tlink.lds -o uart.elf $^
    $(OBJCOPY) -O binary uart.elf uart.bin
    $(OBJDUMP) -D uart.elf > uart_elf.dis

export

#导出这些变量到全局,其实就是给子文件夹下面的Makefile使用
export CC LD OBJCOPY OBJDUMP AR CPPFLAGS CFLAGS

CPPFLAGS / CFLAGS

# C预处理器的flag,flag就是编译器可选的选项
CPPFLAGS    := -nostdlib -nostdinc -I$(INCDIR)/include
# C编译器的flag
CFLAGS        := -Wall -O2 -fno-builtin

-nostdinc 不使用标准库

-Wall 显示警告信息

-O2 代码优化 2是优化等级

-fno-builtin 编译链接的时候只使用自己的东西

INCDIR    := $(shell pwd)

$(shell pwd) 获取当前目录路径

-I 指定include的目录

当前目录下的include目录:

    -I$(INCDIR)/include

%.o : %.S
    $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< -c

%.o : %.c
    $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< -c

lib目录下makefile里面修改对应的内容

%.o:%.c
    ${CC} $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

%.o:%.S
    ${CC} $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

现在make后,lib下的文件却没有反应,现在在objs下加入一行代码:

objs := start.o led.o clock.o uart.o main.o 
objs += lib/libc.a

要生成uart.bin必须依赖$(objs)里的内容

现在执行后会报找不到相关规则,但是我们后面没有lib/libc.a相关的规则

所以我没需要再添加一个规则:

lib/libc.a:
    cd lib;    make;    cd ..

这个是进入子目录自己进行编译后调用。

现在clean发现子目录吓得没有清理

现在增加一个规则:

clean:
    rm *.o *.elf *.bin *.dis mkx210 -f
    cd lib; make clean; cd ..

makefile:

CC        = arm-linux-gcc
LD         = arm-linux-ld
OBJCOPY    = arm-linux-objcopy
OBJDUMP    = arm-linux-objdump
AR        = arm-linux-ar

INCDIR    := $(shell pwd)
# C预处理器的flag,flag就是编译器可选的选项
CPPFLAGS    := -nostdlib -nostdinc -I$(INCDIR)/include
# C编译器的flag
CFLAGS        := -Wall -O2 -fno-builtin

#导出这些变量到全局,其实就是给子文件夹下面的Makefile使用
export CC LD OBJCOPY OBJDUMP AR CPPFLAGS CFLAGS


objs := start.o led.o clock.o uart.o main.o 
objs += lib/libc.a

uart.bin: $(objs)
    $(LD) -Tlink.lds -o uart.elf $^
    $(OBJCOPY) -O binary uart.elf uart.bin
    $(OBJDUMP) -D uart.elf > uart_elf.dis
    gcc mkv210_image.c -o mkx210
    ./mkx210 uart.bin 210.bin

lib/libc.a:
    cd lib;    make;    cd ..
    
%.o : %.S
    $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< -c

%.o : %.c
    $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< -c

clean:
    rm *.o *.elf *.bin *.dis mkx210 -f
    cd lib; make clean; cd ..

    
    

子目录下的makefile:

objs := div64.o lib1funcs.o ctype.o muldi3.o printf.o string.o vsprintf.o

libc.a: $(objs)
    ${AR} -r -o $@ $^
    
%.o:%.c
    ${CC} $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

%.o:%.S
    ${CC} $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

clean:
    rm -f libc.a *.o        
    

uart.c:

#define GPA0CON        0xE0200000
#define UCON0         0xE2900004
#define ULCON0         0xE2900000
#define UMCON0         0xE290000C
#define UFCON0         0xE2900008
#define UBRDIV0     0xE2900028
#define UDIVSLOT0    0xE290002C
#define UTRSTAT0    0xE2900010
#define UTXH0        0xE2900020    
#define URXH0        0xE2900024    

#define rGPA0CON    (*(volatile unsigned int *)GPA0CON)
#define rUCON0        (*(volatile unsigned int *)UCON0)
#define rULCON0        (*(volatile unsigned int *)ULCON0)
#define rUMCON0        (*(volatile unsigned int *)UMCON0)
#define rUFCON0        (*(volatile unsigned int *)UFCON0)
#define rUBRDIV0    (*(volatile unsigned int *)UBRDIV0)
#define rUDIVSLOT0    (*(volatile unsigned int *)UDIVSLOT0)
#define rUTRSTAT0        (*(volatile unsigned int *)UTRSTAT0)
#define rUTXH0        (*(volatile unsigned int *)UTXH0)
#define rURXH0        (*(volatile unsigned int *)URXH0)

// 串口初始化程序
void uart_init(void)
{
    // 初始化Tx Rx对应的GPIO引脚
    rGPA0CON &= ~(0xff<<0);            // 把寄存器的bit0~7全部清零
    rGPA0CON |= 0x00000022;            // 0b0010, Rx Tx
    
    // 几个关键寄存器的设置
    rULCON0 = 0x3;
    rUCON0 = 0x5;
    rUMCON0 = 0;
    rUFCON0 = 0;
    
    // 波特率设置    DIV_VAL = (PCLK / (bps x 16))-1
    // PCLK_PSYS用66MHz算        余数0.8
    //rUBRDIV0 = 34;    
    //rUDIVSLOT0 = 0xdfdd;
    
    // PCLK_PSYS用66.7MHz算        余数0.18
    // DIV_VAL = (66700000/(115200*16)-1) = 35.18
    rUBRDIV0 = 35;
    // (rUDIVSLOT中的1的个数)/16=上一步计算的余数=0.18
    // (rUDIVSLOT中的1的个数 = 16*0.18= 2.88 = 3
    rUDIVSLOT0 = 0x0888;        // 3个1,查官方推荐表得到这个数字
}


// 串口发送程序,发送一个字节
void uart_putc(char c)
{                      
    // 串口发送一个字符,其实就是把一个字节丢到发送缓冲区中去
    // 因为串口控制器发送1个字节的速度远远低于CPU的速度,所以CPU发送1个字节前必须
    // 确认串口控制器当前缓冲区是空的(意思就是串口已经发完了上一个字节)
    // 如果缓冲区非空则位为0,此时应该循环,直到位为1
    while (!(rUTRSTAT0 & (1<<1)));
    rUTXH0 = c;
}

// 串口接收程序,轮询方式,接收一个字节
char uart_getc(void)
{
    while (!(rUTRSTAT0 & (1<<0)));
    return (rURXH0 & 0x0f);
}

main.c:

#include "stdio.h"

void uart_init(void);

int main(void)
{
    uart_init();
    
    int a = 12345678;
    putc('a');
    putc('b');
    putc('c');
    while (1)
    {
        printf("test for printf, a = %d.\n", a);
    }
    
    /*
    while(1)
    {
        uart_putc('a');
        delay();
    }
    */
    return 0;
}

start.S

/*
 * 文件名:    start.S
 * 作者:    朱老师
 * 描述:    演示串口通信
 */

#define WTCON        0xE2700000

#define SVC_STACK    0xd0037d80

.global _start                    // 把_start链接属性改为外部,这样其他文件就可以看见_start了
_start:
    // 第1步:关看门狗(向WTCON的bit5写入0即可)
    ldr r0, =WTCON
    ldr r1, =0x0
    str r1, [r0]
    
    // 第2步:初始化时钟
    bl clock_init
    
    // 第3步:设置SVC栈
    ldr sp, =SVC_STACK
    
    // 第4步:开/关icache
    mrc p15,0,r0,c1,c0,0;            // 读出cp15的c1到r0中
    //bic r0, r0, #(1<<12)            // bit12 置0  关icache
    orr r0, r0, #(1<<12)            // bit12 置1  开icache
    mcr p15,0,r0,c1,c0,0;

    bl main
    
    // 从这里之后就可以开始调用C程序了
    //bl led_blink                    // led_blink是C语言实现的一个函数
    
// 汇编最后的这个死循环不能丢
    b .

link.lds:

SECTIONS
{
    . = 0xd0020010;
    
    .text : {
        start.o
        * (.text)
    }
            
    .data : {
        * (.data)
    }
    
    bss_start = .; 
    .bss : {
        * (.bss)
    }
    
    bss_end  = .;    
}

uart stdio的移植

  • 修改Makefile进行printf移植
  • Makefile及gcc的库文件介绍
  • 多文件夹裸机工程的结构解析
  • 编译运行及测试
  • 在移植后的uart stdio项目中添加link.lds链接脚本,指定连接地址到0xd0020010

gcc可变参数及va_arg介绍

  1. printf函数中首先使用了C语言的可变参数va_start/va_arg/va_end;
  2. 建议大家先去baidu“C语言可变参数”,然后按照别人的教程、博客实际写几个简单的变参的使用示例,先明白可变参数怎么工作,然后再来分析这里。

vsprintf函数详解

printf
vsprintf
vsnprintf
number
vsprintf函数的作用是按照我们的printf传进去的格式化标本,对变参进行处理,然后将之格式化后缓存在一个事先分配好的缓冲区中。
printf后半段调用putc函数将缓冲区中格式化好的字符串直接输出到标准输出。

串口实验烧录问题总结

usb下载的问题

(1)USB下载时在Win7 X64系统下,下载前面章节的小代码时没问题,下载串口通信的小代码时也没问题,下载uart stdio的移植就有问题了。有时候下载不动、有时候能下载但是不运行、有时候又正常下载运行。我已经试过下载其他的dnw或dnw驱动更新,都无法解决。

SD卡镜像烧录

(1)SD卡烧录镜像做裸机实验,在第四部分1.4.2节中有讲过。
(2)本次我们在Windows下烧录(linux下的烧录参考以前的)
(3)Windows下烧录镜像是使用九鼎提供的工具(X210光盘资料\A盘\tools\x210_Fusing_Tool.exe),注意运行时右键“以管理员身份运行”。

启动方式设置

(1)X210开发板的启动方式的选择,请参考1.2.11节。其实就是OM5的问题,OM5设置为VCC则从USB启动,OM5设置成GND,则从iNand/SD卡启动。
(2)开发板选择从iNand启动后,还要确保iNand中uboot是被擦除的。
(3)关于如何破坏uboot的问题,大家可以参考之前课程中讲的在linux/android系统中破坏uboot的方法。我之前讲过在uboot中破坏uboot的方法:movi write u-boot 0x30000000。很多同学反映擦除后错乱,进不了系统也从SD卡启动不了,只能通过USB刷机来解决。后来又分析,改为:mw 0x30000000 0x0 0x100000,然后再movi write u-boot 0x30000000
。但是反馈结果有人说可以了,有人说还是不行·······
(4)不管怎么擦除uboot,总之首先确保你的板子SD卡启动是成功的。怎么确保?先用SD卡烧录启动之前的LED闪烁的项目,确保看到现象就证明烧录SD卡方法和启动SD卡都成功了,再做本节课的实验。

链接脚本的影响
bin文件大于16KB怎么办?

通过USB下载最多也只能下载96KB大小的bin,如果bin大于96KB肯定SRAM放不下会出错。如果用SD卡启动,那么mkv210_image.c决定了bin文件最大不能超过16KB。
超过了怎么办?2种解法:
第一,在USB下载时,可以先下载一个x210_usb.bin,然后再将裸机程序连接到0x23E00000,然后再修改dnw中下载地址,将裸机代码下载到0x23E00000运行。(这时不需要重定位了)
第二,在SD卡启动时,将整个裸机工程分为2部分;第一部分大小16KB以内,第二部分放剩下的(放在SD卡的后面的某个扇区开始的位置,譬如放在第50个扇区开始的位置),然后在裸机代码中进行重定位(SD卡中重定位)。这个暂时没讲,以后如果有用到就讲。

别人移植好的printf.rar




Copyright © 2026 Y 版权所有.网站运行:13年238天21小时29分