前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >树莓派4裸机基础教程:从hello world开始

树莓派4裸机基础教程:从hello world开始

作者头像
bigmagic
发布2020-09-18 19:07:27
2.5K0
发布2020-09-18 19:07:27
举报
文章被收录于专栏:嵌入式iot

树莓派4裸机基础教程:从hello world开始

  • 1.前言
  • 2.项目工程介绍
    • 2.1 Makefile
    • 2.2 link.ld 链接文件
  • 3.从CPU的角度看代码的运行
    • 3.1 start.S文件
    • 3.2 main函数的功能
  • 4.树莓派4串口外设程序
    • 4.1 设置gpio的功能
    • 4.2 配置串口控制器
  • 5.总结

1.前言

当我们去研究一个系统的时候,首先需要从最简单的程序开始入手。前面文章的介绍已经描述了项目的环境搭建以及启动过程。

树莓派4裸机基础教程:环境搭建

树莓派4裸机基础教程:芯片启动到代码执行

本文主要从最简单的裸机代码开始分析,让板子的串口可以输出hello world信息。这篇文章会介绍工程的构建,程序的运行等等一些列的流程,以及树莓派4最后如何输出hello world。在嵌入式开发的过程中,往往都是万事开头难,只有看到了程序正在运行的那一刻,后面的工作也就迎刃而解了。

2.项目工程介绍

我们还是以第一个1.compilation_environment的工程作为研究对象。工程的地址在下面的链接中:

代码语言:javascript
复制
https://github.com/bigmagic123/raspi4-bare-metal.git

最后的工程文件如下所示:

2.1 Makefile

我们通过Makefile进行相关工程的构建,使用make生成kernel可执行程序文件。对于这种简单的工程,使用Makefile进行工程的构建是很简单的,对于复杂的工程,可以使用scons或者cmake等更加高级的工具,进行工程的构建。

首先来看一下Makefile中的内容:

代码语言:javascript
复制
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
CFLAGS = -march=armv8-a -mtune=cortex-a72 -Wall -O2 -ffreestanding -nostdinc -nostdlib -nostartfiles

all: clean kernel7.img

start.o: start.S
 arm-none-eabi-gcc $(CFLAGS) -c start.S -o start.o

%.o: %.c
 arm-none-eabi-gcc $(CFLAGS) -c $< -o $@

kernel7.img: start.o $(OBJS)
 arm-none-eabi-ld -nostdlib -nostartfiles start.o $(OBJS) -T link.ld -o kernel7.elf
 arm-none-eabi-objcopy -O binary kernel7.elf kernel7.img

clean:
 rm kernel7.elf kernel7.img *.o >/dev/null 2>/dev/null || true

分析一下这个文件的细节:

代码语言:javascript
复制
SRCS = $(wildcard *.c)

其中使用wildcard这个函数来获取当前文件夹中所有的.c文件的列表放在SRCS目录中。

代码语言:javascript
复制
OBJS = $(SRCS:.c=.o)

该句表示环境变量的替换,就是将SRCS列表中的所有的.c文件名替换成.o文件名。

代码语言:javascript
复制
all: clean kernel7.img

当使用make或者make all的时候,会执行cleankernel7.img对应的命令的指令。

代码语言:javascript
复制
start.o: start.S
 arm-none-eabi-gcc $(CFLAGS) -c start.S -o start.o

根据makefile的语法规则这个解释应该是

代码语言:javascript
复制
目标:源
   指令

由于前面的定义只定义了C语言的代码,所以这里也需要将汇编语言的编译加进去。

代码语言:javascript
复制
%.o: %.c
 arm-none-eabi-gcc $(CFLAGS) -c $< -o $@

其中$<表示第一个依赖文件的名词,$@表示目标文件的名词。

代码语言:javascript
复制
kernel7.img: start.o $(OBJS)
 arm-none-eabi-ld -nostdlib -nostartfiles start.o $(OBJS) -T link.ld -o kernel7.elf
 arm-none-eabi-objcopy -O binary kernel7.elf kernel7.img

通过arm-none-eabi-ld链接所以的.o文件。arm-none-eabi-objcopy用于生成在arm平台上运行的可执行程序,另外的作用就是去掉一些符号信息。

代码语言:javascript
复制
clean:
 rm kernel7.elf kernel7.img *.o >/dev/null 2>/dev/null || true

用于清理编译过程中的中间文件。

2.2 link.ld 链接文件

由于程序的编译之后,需要进行链接,link文件告诉了程序链接的规则。下面看一下链接文件的内容:

代码语言:javascript
复制
SECTIONS {
	/*
	* First and formost we need the .init section, containing the code to
        * be run first. We allow room for the ATAGs and stack and conform to
        * the bootloader's expectation by putting this code at 0x8000.
	*/
    . = 0x8000;
    .text : {
        KEEP(*(.text.boot))
        *(.text .text.* .gnu.linkonce.t*)
    }

	/*
	* Next we put the data.
	*/
	.data : {
		*(.data)
	}

  .bss : {
        . = ALIGN(16);
        __bss_start = .;
        *(.bss*)
        *(COMMON*)
        __bss_end = .;
    }
}
__bss_size = (__bss_end - __bss_start) >> 3;

程序分为代码段(.text),数据段(.data)以及bss段(.bss)。首先将代码段的地址. = 0x8000;指向0x8000的地址处,因为默认情况下,树莓派默认启动后,会从0x8000这个地址处开始加载程序并启动。KEEP(*(.text.boot))表示首先将.text.boot的内容放在第一个地址处,目前开始的地址是0x8000。需要注意的是.bss段包含的是初始化为零的数据,通过将这些数据放在一个单独的节中,编译器可以在elf文件中省略一些空间。所以需要记录bss_start与bss_end段。并且将这段空间对齐。如果不对齐,一些函数访问的时候,将会出现异常数据。

3.从CPU的角度看代码的运行

要想真正的理解CPU的执行代码的流程,必须将自己的当作CPU去执行代码的逻辑。

3.1 start.S文件

在start.S文件中,设置了CPU的一些状态,为后续的程序执行准备了环境。

代码语言:javascript
复制
.equ Mode_USR,        0x10
.equ Mode_FIQ,        0x11
.equ Mode_IRQ,        0x12
.equ Mode_SVC,        0x13
.equ Mode_ABT,        0x17
.equ Mode_UND,        0x1B
.equ Mode_SYS,        0x1F

.section ".text.boot"
/* entry */
.globl _start
_start:
/* Check for HYP mode */
    mrs r0, cpsr_all
    and r0, r0, #0x1F
    mov r8, #0x1A
    cmp r0, r8
    beq overHyped
    b continue

overHyped: /* Get out of HYP mode */
    adr r1, continue
    msr ELR_hyp, r1
    mrs r1, cpsr_all
    and r1, r1, #0x1f    ;@ CPSR_MODE_MASK
    orr r1, r1, #0x13    ;@ CPSR_MODE_SUPERVISOR
    msr SPSR_hyp, r1
    eret

continue:
    /* Suspend the other cpu cores */
    mrc p15, 0, r0, c0, c0, 5
    ands r0, #3
    bne _halt

    /* set the cpu to SVC32 mode and disable interrupt */
    cps #Mode_SVC

    /* disable the data alignment check */
    mrc p15, 0, r1, c1, c0, 0
    bic r1, #(1<<1)
    mcr p15, 0, r1, c1, c0, 0

    /* set stack before our code */
    ldr sp, =_start

    /* clear .bss */
    mov     r0,#0                   /* get a zero                       */
    ldr     r1,=__bss_start         /* bss start                        */
    ldr     r2,=__bss_end           /* bss end                          */

bss_loop:
    cmp     r1,r2                   /* check if data to clear           */
    strlo   r0,[r1],#4              /* clear 4 bytes                    */
    blo     bss_loop                /* loop until done                  */

    /* jump to C code, should not return */
    ldr     pc, _main
    b _halt

_main:
    .word main

_halt:
    wfe
    b _halt

分别来看一下这些代码具体的细节。

代码语言:javascript
复制
.section ".text.boot"

表示该段标志为.text.boot,这里表示该文件夹会在链接脚本中链接到开头的地址中。然后将_start指定到0x8000的地址处。

代码语言:javascript
复制
/* entry */
.globl _start
_start:
/* Check for HYP mode */
    mrs r0, cpsr_all
    and r0, r0, #0x1F
    mov r8, #0x1A
    cmp r0, r8
    beq overHyped
    b continue

overHyped: /* Get out of HYP mode */
    adr r1, continue
    msr ELR_hyp, r1
    mrs r1, cpsr_all
    and r1, r1, #0x1f    ;@ CPSR_MODE_MASK
    orr r1, r1, #0x13    ;@ CPSR_MODE_SUPERVISOR
    msr SPSR_hyp, r1
    eret

从树莓派启动第一行代码的时候,此时是处于虚拟化模式的,从cpsr_all寄存器中可以读到当前的状态。此时需要退出虚拟化模式。使其运行在Supervisor模式。用eret指令将模式进行切换。

代码语言:javascript
复制
/* Suspend the other cpu cores */
mrc p15, 0, r0, c0, c0, 5
ands r0, #3
bne _halt

因为刚开始的时候,树莓派4是支持4核的,由于当前并不需要这么多核的功能,所以可以让其他的核进入low-power standby低功耗模式WFE(Wait for event)。

代码语言:javascript
复制
/* set the cpu to SVC32 mode and disable interrupt */
cps #Mode_SVC

/* disable the data alignment check */
mrc p15, 0, r1, c1, c0, 0
bic r1, #(1<<1)
mcr p15, 0, r1, c1, c0, 0

接着关闭中断、关闭非对齐检查。为后续的代码运行准备环境。

代码语言:javascript
复制
/* set stack before our code */
ldr sp, =_start

接着设置sp的栈指针,ldr sp, =_start表示将栈指针设置到_start段的地址这里,由于布局的时候,将_start的代码段的地址设置为0x8000,又因为arm上sp栈指针是向低地址方向增长,sp指向的是栈顶。所以我们可以认为0x8000地址之前的空间都是未被使用的,可以作为C语言执行的栈空间使用。

代码语言:javascript
复制
    /* clear .bss */
    mov     r0,#0                   /* get a zero                       */
    ldr     r1,=__bss_start         /* bss start                        */
    ldr     r2,=__bss_end           /* bss end                          */

bss_loop:
    cmp     r1,r2                   /* check if data to clear           */
    strlo   r0,[r1],#4              /* clear 4 bytes                    */
    blo     bss_loop                /* loop until done                  */

接着清空BSS段,BSS段通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0。

代码语言:javascript
复制
/* jump to C code, should not return */
ldr     pc, _main

然后设置PC指针。使用ldr pc, _main指令,将_main函数的指针,指向pc。这样下次再执行PC程序的时候就直接执行main函数了。

3.2 main函数的功能

在前面的汇编代码中,为C语言代码执行提供了环境,包括关闭非对齐检查、设置了栈SP的地址、清零了BSS段。这些都是为C代码的执行做准备。在C语言中做了具体的业务。由于目前的裸机代码比较的简单,所以业务也比较容易。

代码语言:javascript
复制
#include "uart.h"

void main()
{
    // set up serial console
    uart_init();
    
    // say hello
    uart_puts("Hello World!\n");
    
    // echo everything back
    while(1) {
        uart_send(uart_getc());
    }
}

这个代码就是通过串口输出一个hello world!,然后在while中不断的读串口的输入。那么重点还是放在树莓派串口的初始化上。

4.树莓派4串口外设程序

在做嵌入式的时候,我们总是希望设备与自己是有交互的,比如点亮一个led,或者用串口输出一段字符等等。这都表示程序正常运行。所以会写简单的交互程序也非常的重要。一般比较简单的就是led的呼吸灯。这里用串口,可以做人机交互的信息可以更加的丰富。下面我们来分析一下串口的程序的实现。

在写外设的驱动程序之前,首先需要查看芯片的Peripherals manual。这里查看rpi_DATA_2711_1p0.pdf即可。根据外设空间分布的地址,可以查看如下:

这里由于使用32位的地址空间,根据数据手册,得到芯片的外设的地址的起始地址为0xFE000000

如果要使用串口,必须要有两个先决条件:

1.相关的gpio配置成串口复用功能

2.配置串口控制器参数

4.1 设置gpio的功能

对于树莓派的gpio,找到对应的地址后,还需要找到其对应的功能。

首先查看树莓派上对应的硬件引脚:

对应的功能如下所示:目前串口使用的硬件引脚为14号与15号引脚。

需要设置的复用功能为ALT5。

有了这些信息之后,就可以配置GPFSEL1的功能了。

代码语言:javascript
复制
/**
 * gpio14 RX gpio15 TX
 */
void uart_gpio_init()
{
    register unsigned int r;
    /* map UART1 to GPIO pins */
    r=*GPFSEL1;
    r&=~((7<<12)|(7<<15)); // gpio14, gpio15
    r|=(2<<12)|(2<<15);    // alt5
    *GPFSEL1 = r;
    *GPPUD = 0;            // enable pins 14 and 15
    r=150; while(r--) { asm volatile("nop"); }
    *GPPUDCLK0 = (1<<14)|(1<<15);
    r=150; while(r--) { asm volatile("nop"); }
    *GPPUDCLK0 = 0;        // flush GPIO setup
    *AUX_MU_CNTL = 3;      // enable Tx, Rx
}

在树莓派中,首先需要选择使能哪些引脚,然后配置成什么模式。对着手册查看,就知道设置这些寄存器位的具体含义了。

4.2 配置串口控制器

串口控制器是需要配置的,目前使用的是AUX的串口控制器,也就是使用的mini UART。所以需要配置串口的一些参数信息。比如串口的波特率、位宽、停止位等等。

代码语言:javascript
复制
 */
void uart_init()
{
    /* initialize UART1 */
    *AUX_ENABLE |=1;       // enable UART1, AUX mini uart
    *AUX_MU_CNTL = 0;
    *AUX_MU_LCR = 3;       // 8 bits
    *AUX_MU_MCR = 0;
    *AUX_MU_IER = 0;
    *AUX_MU_IIR = 0xc6;    // disable interrupts
    *AUX_MU_BAUD = 270;    // 115200 baud
    uart_gpio_init();
}

目前串口不需要使用中断,所以收发数据都直接从串口的fifo中进行获取。

发送数据

代码语言:javascript
复制
/**
 * Send a character
 */
void uart_send(unsigned int c) {
    /* wait until we can send */
    do{asm volatile("nop");}while(!(*AUX_MU_LSR&0x20));
    /* write the character to the buffer */
    *AUX_MU_IO=c;
}

判断当前fifo是否有数据,如果没有就发送到串口的fifo。

代码语言:javascript
复制
char uart_getc() {
    char r;
    /* wait until something is in the buffer */
    do{asm volatile("nop");}while(!(*AUX_MU_LSR&0x01));
    /* read it and return */
    r=(char)(*AUX_MU_IO);
    /* convert carrige return to newline */
    return r=='\r'?'\n':r;
}

从串口的fifo中读取字符。

5.总结

从树莓派4的hello world程序分析,详细的描述了串口的输出信息到控制台的过程。前期的c语言运行环境的准备阶段是很多同等系列的芯片都需要去做的事情,后面外设的初始化可能会和具体的硬件平台相关。但是从整体上来看,整个流程还是比较通用的。在不同的芯片与不同的架构上,都需要去做这些基本操作。

本文从最小系统的角度描述了系统启动过程,配置寄存器参数需要对着手册查看,这里也不进行过多的分析,总之多看手册才是学会使用一款芯片的必经之路,只有反复的看,反复的思考理解,才能使用得当。欧阳修《卖油翁》 里说到:无他,但手熟尔。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-09-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 嵌入式IoT 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 树莓派4裸机基础教程:从hello world开始
    • 1.前言
      • 2.项目工程介绍
        • 2.1 Makefile
        • 2.2 link.ld 链接文件
      • 3.从CPU的角度看代码的运行
        • 3.1 start.S文件
        • 3.2 main函数的功能
      • 4.树莓派4串口外设程序
        • 4.1 设置gpio的功能
        • 4.2 配置串口控制器
      • 5.总结
      相关产品与服务
      腾讯云小微
      腾讯云小微,是一套腾讯云的智能服务系统,也是一个智能服务开放平台,接入小微的硬件可以快速具备听觉和视觉感知能力,帮助智能硬件厂商实现语音人机互动和音视频服务能力。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档