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)
c
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 位的寄存器,其各个位的含义如下:
位 | 名称 | 描述 |
---|---|---|
31 | N (Negative) | 负标志,表示最近一次运算结果为负数。 |
30 | Z (Zero) | 零标志,表示最近一次运算结果为零。 |
29 | C (Carry) | 进位标志,表示最近一次运算结果产生了进位或借位。 |
28 | V (Overflow) | 溢出标志,表示最近一次运算结果产生了溢出。 |
27-8 | Reserved | 保留位,未使用。 |
7 | I (IRQ disable) | IRQ 中断禁用位,设置为 1 时禁用 IRQ 中断。 |
6 | F (FIQ disable) | FIQ 中断禁用位,设置为 1 时禁用 FIQ 中断。 |
5 | T (Thumb) | Thumb 状态位,设置为 1 时处理器处于 Thumb 状态。 |
4-0 | Mode | 模式位,表示当前处理器的工作模式。 |
模式位
模式位用于设置处理器的工作模式,常见的模式及其对应的值如下:
模式 | 值 | 描述 |
---|---|---|
User | 0b10000 | 用户模式,非特权模式。 |
FIQ | 0b10001 | 快速中断模式。 |
IRQ | 0b10010 | 一般中断模式。 |
Supervisor | 0b10011 | 超级管理员模式,特权模式。 |
Monitor | 0b10110 | 监视模式,用于安全扩展模式。 |
Abort | 0b10111 | 数据访问终止模式。 |
Hypervisor | 0b11010 | 超级监视模式,用于虚拟化扩展。 |
Undefined | 0b11011 | 未定义指令终止模式。 |
System | 0b11111 | 系统模式,用于运行特权级的操作系统任务。 |
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 寄存器中,以设置栈指针的位置。
示例
assembly
; 将栈指针设置为地址 0x8000
MOV SP, #0x8000
保存和恢复 SP 指针
在某些情况下,可能需要保存当前的 SP 指针值,并在稍后恢复它。可以使用通用寄存器来保存和恢复 SP 指针。
示例
assembly
; 保存当前的 SP 指针值到 R0
MOV R0, SP
; 执行一些操作,可能会改变 SP 指针
; 恢复 SP 指针值
MOV SP, R0
SP可以指向内部RAM,也可以指向DDR内存。 对于512M的DDR来说,内存范围是 0x80000000~0x9FFFFFFF,栈大小一般设置为2M,由于A7栈是向下增长的,可以将SP设置为 0x80200000。
4. 跳转到C语言
一般跳到 main 函数,下面是一个示例:
c
.global _start
.extern main
_start:
/* 设置栈指针 */
LDR SP, =0x8000
/* 调用 C 语言的 main 函数 */
BL main
/* 死循环,防止程序返回 */
1: B 1b
二、程序编写
1. 启动文件 start.S
c
.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 定义寄存器
c
//
// 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
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
c
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
为扩展名,主要有以下作用:
- 定义内存布局 :指定程序各个段(如
.text
、.data
、.bss
)在内存中的位置。 - 设置入口点:指定程序的入口点,即程序开始执行的地址。
- 分配段:将目标文件中的段分配到内存中的特定位置。
- 定义符号:可以定义一些符号,用于在程序中引用特定的内存地址。
以下是本示例使用的链接脚本文件 linker.ld
:
linkerscript
SECTIONS
{
. = 0x87800000; /* 起始地址 */
.text : {
*(.text)
}
.data : {
*(.data)
}
.bss : {
*(.bss)
}
. = ALIGN(8);
__stack_top = .; /* 定义栈顶 */
}
ENTRY(_start) /* 指定入口点为 _start */
本次实验使用的链接脚本 文件:
c
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数据段
c
.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在闪烁。
本文代码开源地址: