自学内容网 自学内容网

I.MX6U 裸机开发5.准备C环境并用C语言控制LED

在这里插入图片描述

一、C运行环境

1. 设置处理器模式

在Cortex-A 的架构中一共有9种处理模式,如下表所示:

模式描述
USR (User Mode)用户模式,非特权模式,大部分程序运行的时候就处于此模式。
FIQ (Fast Interrupt Request Mode)快速中断模式,进入 FIQ 中断异常。
IRQ (Interrupt Request Mode)一般中断模式。
SVC (Supervisor Mode)超级管理员模式,特权模式,供操作系统使用。
MON (Monitor Mode)监视模式,用于安全扩展模式。
ABT (Abort Mode)数据访问终止模式,用于虚拟存储以及存储保护。
HYP (Hypervisor Mode)超级监视模式,用于虚拟化扩展。
UND (Undefined Mode)未定义指令终止模式。
SYS (System Mode)系统模式,用于运行特权级的操作系统任务。

I.MX6ULL 处理器中,可以通过修改 CPSR(Current Program Status Register)寄存器的模式位来设置处理器模式。以下是一些常见模式的设置方法:
进入用户模式 (User Mode)

MRS R0, CPSR        ; 读取当前 CPSR 寄存器值到 R0
BIC R0, R0, #0x1F   ; 清除模式位
ORR R0, R0, #0x16   ; 设置模式位为 Monitor Mode (0b10110)
MSR CPSR_c, R0      ; 将修改后的值写回 CPSR 寄存器

2. CPSR 寄存器

CPSR(Current Program Status Register)用于存储当前程序的状态信息。它包含了处理器模式、条件标志、中断屏蔽位等信息。

CPSR 寄存器结构

CPSR 是一个 32 位的寄存器,其各个位的含义如下:

名称描述
31N (Negative)负标志,表示最近一次运算结果为负数。
30Z (Zero)零标志,表示最近一次运算结果为零。
29C (Carry)进位标志,表示最近一次运算结果产生了进位或借位。
28V (Overflow)溢出标志,表示最近一次运算结果产生了溢出。
27-8Reserved保留位,未使用。
7I (IRQ disable)IRQ 中断禁用位,设置为 1 时禁用 IRQ 中断。
6F (FIQ disable)FIQ 中断禁用位,设置为 1 时禁用 FIQ 中断。
5T (Thumb)Thumb 状态位,设置为 1 时处理器处于 Thumb 状态。
4-0Mode模式位,表示当前处理器的工作模式。

模式位

模式位用于设置处理器的工作模式,常见的模式及其对应的值如下:

模式描述
User0b10000用户模式,非特权模式。
FIQ0b10001快速中断模式。
IRQ0b10010一般中断模式。
Supervisor0b10011超级管理员模式,特权模式。
Monitor0b10110监视模式,用于安全扩展模式。
Abort0b10111数据访问终止模式。
Hypervisor0b11010超级监视模式,用于虚拟化扩展。
Undefined0b11011未定义指令终止模式。
System0b11111系统模式,用于运行特权级的操作系统任务。

MRS 指令

MRS(Move Register from Special register)指令用于将特殊寄存器(如 CPSR 或 SPSR)的值移动到通用寄存器中。常用于读取当前程序状态寄存器(CPSR)或保存程序状态寄存器(SPSR)的值。

MSR 指令

MSR(Move Special register from Register)指令用于将通用寄存器的值移动到特殊寄存器中。常用于修改 CPSR 或 SPSR 的值。

3. 设置SP指针

在 ARM 处理器中,SP(Stack Pointer)指针用于指向当前栈的顶部。设置 SP 指针通常用于初始化栈或切换栈。以下是一些常见的设置 SP 指针的方法:

设置 SP 指针

可以使用 MOV 指令将一个值加载到 SP 寄存器中,以设置栈指针的位置。

示例
; 将栈指针设置为地址 0x8000
MOV SP, #0x8000

保存和恢复 SP 指针

在某些情况下,可能需要保存当前的 SP 指针值,并在稍后恢复它。可以使用通用寄存器来保存和恢复 SP 指针。

示例
; 保存当前的 SP 指针值到 R0
MOV R0, SP

; 执行一些操作,可能会改变 SP 指针

; 恢复 SP 指针值
MOV SP, R0

SP可以指向内部RAM,也可以指向DDR内存。 对于512M的DDR来说,内存范围是 0x80000000~0x9FFFFFFF,栈大小一般设置为2M,由于A7栈是向下增长的,可以将SP设置为 0x80200000。

4. 跳转到C语言

一般跳到 main 函数,下面是一个示例:

.global _start
.extern main

_start:
    /* 设置栈指针 */
    LDR SP, =0x8000

    /* 调用 C 语言的 main 函数 */
    BL main

    /* 死循环,防止程序返回 */
1:  B 1b

二、程序编写

1. 启动文件 start.S

.global __start

__start:
    /** 设置处理进入 SVC 模式 */
    MRS R0, CPSR        /** 读取当前的 CPSR */
    BIC R0, R0, #0x1F   /** 清除 CPSR 的低 5 位 */
    ORR R0, R0, #0x13   /** 设置 CPSR 为 SVC 模式 */
    MSR CPSR, R0        /** 设置 CPSR */

    /** 设置堆栈 */
    LDR R0, =0x80200000
    B main

2. main.h 定义寄存器

//
// Created by Xundh on 2024/11/9.
//

#ifndef LEARN_I_MX6U_MAIN_H
#define LEARN_I_MX6U_MAIN_H

/** 定义要使用的寄存器 **/
#define CCM_CCGR0 (*(volatile unsigned int *)0x020C4068)
#define CCM_CCGR1 (*(volatile unsigned int *)0x020C406C)
#define CCM_CCGR2 (*(volatile unsigned int *)0x020C4070)
#define CCM_CCGR3 (*(volatile unsigned int *)0x020C4074)
#define CCM_CCGR4 (*(volatile unsigned int *)0x020C4078)
#define CCM_CCGR5 (*(volatile unsigned int *)0x020C407C)
#define CCM_CCGR6 (*(volatile unsigned int *)0x020C4080)

/** IOMUXC 寄存器地址 **/
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 (*(volatile unsigned int *)0x020E0068)
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 (*(volatile unsigned int *)0x020E02F4)

/** GPIO1 寄存器地址 **/
#define GPIO1_DR   (*(volatile unsigned int *)0x0209C000)
#define GPIO1_GDIR (*(volatile unsigned int *)0x0209C004)
#define GPIO1_PSR  (*(volatile unsigned int *)0x0209C008)

#endif //LEARN_I_MX6U_MAIN_H


3. 主程序main.c

//
// Created by Xundh on 2024/11/9.
//
#include "main.h"
/**
 * 使能时钟
 * @return
 */
int enable_clock(void){
    CCM_CCGR1 = 0xffffffff;
    CCM_CCGR2 = 0xffffffff;
    CCM_CCGR3 = 0xffffffff;
    CCM_CCGR4 = 0xffffffff;
    CCM_CCGR5 = 0xffffffff;
    CCM_CCGR6 = 0xffffffff;

    return 0;
}
/**
 * 初始化LED
 * @return
 */
 void init_led(void){
    IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = 0x5; // 设置复用为GPIO1_IO03
    IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 = 0x10B0;  // 设置电气属性
    GPIO1_GDIR |= (1<<3);   // 设置为输出
}
void led_on(void){
    GPIO1_DR &= ~(1<<3);
}
void led_off(void){
    GPIO1_DR |= (1<<3);
}
void delay(volatile unsigned int n){
    while(n--);
}
int main(void){
    /** 初始化LED **/
    enable_clock();

    // 初始化LED
    init_led();

    while(1){
        led_on();
        delay(500);
        led_off();
        delay(500);
    }

    return 0;
}

4. Makefile

objs = start.o main.o

ledc.bin : $(objs)
# 把.o文件链接成.elf文件, 其中的 $^ 代表所有的.o文件
arm-linux-gnueabihf-ld -T linker.ld $^ -o ledc.elf
# 把.elf文件转换成.bin文件
arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@
# 把.elf文件转换成.dis文件
arm-linux-gnueabihf-objdump -D ledc.elf > ledc.dis

%.o : %.c
# 把.c文件编译成.o文件, 其中的 $@ 代表目标文件, $< 代表第一个依赖文件
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<

%.o : %.S
# 把.S文件编译成.o文件
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<

clean:
rm -f *.bin *.o *.dis *.elf

download:
../tools/imxdownload ledc.bin /dev/sdb

5. 链接文件 linker.ld

上面的 Makefile 直接编译的话,会出现 0x87800000 地址放的不是 _start 函数,而是 main 函数。为了确保 0x87800000,需要使用链接脚本 。

链接脚本(linker script)是用于控制链接器(如 ld)如何将目标文件(object files)链接成可执行文件的脚本,链接脚本通常以 .ld 为扩展名,主要有以下作用:

  1. 定义内存布局:指定程序各个段(如 .text.data.bss)在内存中的位置。
  2. 设置入口点:指定程序的入口点,即程序开始执行的地址。
  3. 分配段:将目标文件中的段分配到内存中的特定位置。
  4. 定义符号:可以定义一些符号,用于在程序中引用特定的内存地址。

以下是本示例使用的链接脚本文件 linker.ld

SECTIONS
{
    . = 0x87800000; /* 起始地址 */

    .text : {
        *(.text)
    }

    .data : {
        *(.data)
    }

    .bss : {
        *(.bss)
    }

    . = ALIGN(8);
    __stack_top = .; /* 定义栈顶 */
}

ENTRY(_start) /* 指定入口点为 _start */

本次实验使用的链接脚本 文件:

SECTIONS{
    . = 0x87800000;
    .text :
    {
        start.o
        *(.text)
    }
    .rodata ALIGN(4) : {*(.rodata*)}
    .data ALIGN(4) : {*(.data)}
    __bss_start=.;
    .bss ALIGN(4) : {*(.bss) *(COMMON)}
    __bss_end=.;
}

定位计数器

链接脚本中的定位计数器(location counter)是一个特殊的符号,用于跟踪当前分配的内存地址。它通常用 . 表示,并在链接脚本中用于定义段的起始地址和大小。上例中:
. = 0x87800000 , 前面的点就是定位计数器,设置定位计数器的初始值为 0x87800000,表示从这个地址开始分配内存。

.text

.text : {
    start.o
    *(.text)
}

定义 .text 段,包含 start.o 和所有 .text 段的内容。定位计数器会自动增加,以跟踪分配的内存地址。
*(.text)表示所有程序源代码编译出来的段。

.rodata 段

只读数据段,如字符串变量,使用const关键字定义的全局或静态变量。

.rodata ALIGN(4) : {
    *(.rodata*)
}

定义 .rodata 段,并将其对齐到 4 字节边界。定位计数器会更新为 .text 段结束后的地址,并对齐到 4 字节。
其中前面一个* 表示匹配所有的输入文件,
(.rodata*)表示匹配所有以.rodata 开头的段,如 .rodata,rodata1,.rodata2。

.data数据段

.data ALIGN(4) : {
    *(.data)
}

定义 .data 段,并将其对齐到 4 字节边界。定位计数器会更新为 .rodata 段结束后的地址,并对齐到 4 字节。

.bss段

__bss_start = .;
.bss ALIGN(4) : {
    *(.bss)
    *(COMMON)
}
__bss_end = .;

记录 .bss 段的起始地址为 __bss_start,然后定义 .bss 段,并将其对齐到 4 字节边界。定位计数器会更新为 .data 段结束后的地址,并对齐到 4 字节。最后,记录 .bss 段的结束地址为 __bss_end。

*(.bss):匹配所有输入文件中的 .bss 段,并将其内容放入输出文件的 .bss 段中。在输入文件中,未初始化的全局变量和静态变量会被放入 .bss 段。 这些变量在编译时会被放入 .bss 段,并在程序启动时被初始化为零。

6. 编译烧写测试

按第3课操作,将程序烧写到SD卡,可以看到LED1在闪烁。
在这里插入图片描述
本文代码开源地址:
https://gitee.com/xundh/learn_i.mx6u.git


原文地址:https://blog.csdn.net/xundh/article/details/143645485

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!