i.MX6ULL嵌入式系统代码详解(从零开始)

一、系统启动与初始化(start.S)

1. 异常向量表

.global _start

_start:
    ldr pc, =_reset_handler    ; 复位异常(系统启动)
    ldr pc, =_undef_handler    ; 未定义指令异常
    ldr pc, =_software_handler ; 软件中断(SWI)
    ldr pc, =_prefect_handler  ; 预取指令异常
    ldr pc, =_data_abort_handler ; 数据访问异常
    nop                         ; 保留
    ldr pc, =_irq_handler      ; IRQ中断
    ldr pc, =_fiq_handler      ; FIQ中断

解释:这是ARM处理器的异常向量表,位于内存起始位置(0x87800000)。处理器发生异常时,会根据异常类型自动跳转到对应地址。每个向量占用4字节,第7个向量为IRQ中断入口。

2. 复位处理程序(系统初始化)

_reset_handler:
    cpsid i                     ; 禁用IRQ中断(保证初始化不被中断)
    
    ; 配置CP15系统控制寄存器
    mrc p15, 0, r0, c1, c0, 0   ; 读取控制寄存器
    bic r0, r0, #(1 <<13)       ; 清除V位,使用VBAR重映射异常向量表
    orr r0, r0, #(1 <<12)       ; 设置I位,启用指令缓存(ICache)
    mcr p15, 0, r0, c1, c0, 0   ; 写回控制寄存器
    
    ; 设置IRQ模式栈指针
    cps #0x12                   ; 切换到IRQ模式(模式编码0x12)
    ldr sp, =0x82000000         ; IRQ栈起始地址
    
    ; 设置系统模式栈指针
    cps #0x1F                   ; 切换到系统模式(0x1F)
    ldr sp, =0x84000000         ; 系统栈起始地址
    
    cpsie i                     ; 启用IRQ中断
    
    bl _bss_init                ; 初始化BSS段(清零)
    b main                      ; 跳转到C语言main函数

关键点

  • cpsid i/cpsie i:中断禁用/使能指令

  • 不同模式有不同的栈指针,避免模式切换时数据破坏

  • BSS段存放未初始化的全局/静态变量,启动时需要清零

3. IRQ中断处理程序

_irq_handler:
    sub lr, lr, #4              ; 调整返回地址(ARM中断返回特性)
    stmfd sp!, {r0-r12, lr}     ; 保存现场(寄存器入栈)
    
    ; 获取GIC中断控制器基地址
    mrc p15, 4, r1, c15, c0, 0  ; 从CP15读取GIC基地址
    add r1, r1, #0x2000         ; GIC CPU接口偏移
    ldr r0, [r1, #0xC]          ; 读取中断ID(ICC_IAR)
    
    ; 保存中断ID并调用C中断处理函数
    stmfd sp!, {r0, r1}
    cps #0x1F                   ; 切换到系统模式
    stmfd sp!, {lr}
    bl system_interrupt_handler ; C语言中断分发函数
    ldmfd sp!, {lr}
    cps #0x12                   ; 切换回IRQ模式
    
    ; 中断处理完成,发送EOI
    ldmfd sp!, {r0, r1}
    str r0, [r1, #0x10]         ; 写ICC_EOIR,中断结束
    
    ldmfd sp!, {r0-r12, pc}^    ; 恢复现场并返回(^表示恢复CPSR)

二、时钟系统初始化(clock.c)

1. 主要时钟源配置

void clock_init(void)
{
    // 1. ARM内核时钟配置
    CCM->CCSR &= ~(1 << 8);     // 清除pll1_sw_clk_sel位
    CCM->CCSR |= (1 << 2);      // step_clk选择24MHz晶振
    
    // 设置ARM时钟分频
    CCM->CACRR &= ~(7 << 0);    // 清除分频系数
    CCM->CACRR |= (1 << 0);     // 设置2分频(ARM_PODF=1)
    
    // 配置PLL1(ARM PLL)
    unsigned int t = CCM_ANALOG->PLL_ARM;
    t &= ~(3 << 14);            // 清除旁路控制位
    t |= (1 << 13);             // 使能PLL输出
    t &= ~(0x7F << 0);          // 清除倍频系数
    t |= (88 << 0);             // 设置倍频系数N=88
    CCM_ANALOG->PLL_ARM = t;    // 24MHz × 88 = 2112MHz
    
    CCM->CCSR &= ~(1 << 2);     // step_clk切换回PLL1
    
    // 2. 配置528 PLL的PFD通道
    t = CCM_ANALOG->PFD_528;
    t &= ~((0x3F << 0) | (0x3F << 8) | (0x3F << 16) | (0x3F << 24));
    t |= ((27 << 0) | (16 << 8) | (24 << 16) | (32 << 24));
    CCM_ANALOG->PFD_528 = t;
    /*
    PFD0: 528×18/27 = 352MHz
    PFD1: 528×18/16 = 594MHz  
    PFD2: 528×18/24 = 396MHz
    PFD3: 528×18/32 = 297MHz
    */
    
    // 3. 配置480 PLL的PFD通道
    t = CCM_ANALOG->PFD_480;
    t &= ~((0x3F << 0) | (0x3F << 8) | (0x3F << 16) | (0x3F << 24));
    t |= ((12 << 0) | (16 << 8) | (17 << 16) | (19 << 24));
    CCM_ANALOG->PFD_480 = t;
    /*
    PFD0: 480×18/12 = 720MHz
    PFD1: 480×18/16 = 540MHz
    PFD2: 480×18/17 ≈ 508MHz
    PFD3: 480×18/19 ≈ 454MHz
    */
    
    // 4. 配置AHB总线时钟(132MHz)
    t = CCM->CBCMR;
    t &= ~(3 << 18);            // 清除PRE_PERIPH_CLK_SEL
    t |= (1 << 18);             // 选择PLL2 PFD2(396MHz)
    CCM->CBCMR = t;
    
    t = CCM->CBCDR;
    t &= ~(1 << 25);            // PERIPH_CLK_SEL=0(选择PRE_PERIPH)
    t &= ~(7 << 10);            // 清除AHB_PODF
    t |= (2 << 10);             // AHB_PODF=2(3分频),396MHz÷3=132MHz
    
    // 5. 配置IPG时钟(66MHz)
    t &= ~(3 << 8);             // 清除IPG_PODF
    t |= (1 << 8);              // IPG_PODF=1(2分频),132MHz÷2=66MHz
    CCM->CBCDR = t;
    
    // 6. 配置PERCLK时钟(66MHz)
    t = CCM->CSCMR1;
    t &= ~(1 << 6);             // PERCLK_CLK_SEL=0(选择IPG时钟)
    t &= ~(0x3F << 0);          // PERCLK_PODF=0(1分频)
    CCM->CSCMR1 = t;
    
    clock_cg_init();            // 使能所有外设时钟
}

2. 外设时钟使能

void clock_cg_init(void)
{
    CCM->CCGR0 = 0XFFFFFFFF;  // 使能CCGR0控制的所有外设时钟
    CCM->CCGR1 = 0XFFFFFFFF;  // GPIO1-5等
    CCM->CCGR2 = 0XFFFFFFFF;  // GPT、EPIT等定时器
    CCM->CCGR3 = 0XFFFFFFFF;  // UART、I2C等
    CCM->CCGR4 = 0XFFFFFFFF;  // PWM、ADC等
    CCM->CCGR5 = 0XFFFFFFFF;  // 视频相关
    CCM->CCGR6 = 0XFFFFFFFF;  // 以太网等
}

时钟门控:每个CCGR寄存器控制一组外设的时钟,写入1使能时钟,0禁用时钟(低功耗)。

三、中断系统(interrupt.c)

1. 中断向量表

irq_handler_t Vector_table[160] = {NULL};
// 定义中断向量表,最多支持160个中断源
// irq_handler_t是函数指针类型:typedef void (*irq_handler_t)(void);

2. 中断系统初始化

void system_interrupt_init(void)
{
    __set_VBAR(0x87800000);    // 设置异常向量表基地址到0x87800000
    GIC_Init();                // 初始化通用中断控制器(GIC)
}

VBAR:Vector Base Address Register,ARMv7异常向量表基址寄存器。

3. 中断注册与管理

// 中断服务函数注册
int system_interrupt_register(IRQn_Type irq, irq_handler_t handler)
{
    if (irq > PMU_IRQ2_IRQn || irq < IOMUXC_IRQn)
        return -1;              // 中断号范围检查
    
    if (handler == NULL)
        return -2;              // 空指针检查
    
    Vector_table[irq] = handler; // 注册中断处理函数
    return 0;
}

// 中断分发函数(由汇编_irq_handler调用)
void system_interrupt_handler(IRQn_Type irq)
{
    if (Vector_table[irq] != NULL){
        Vector_table[irq]();    // 调用注册的中断服务函数
    }
}

四、GPIO外设驱动

1. LED驱动(led.c)

void led_init(void)
{
    // 1. 引脚复用配置
    IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0);
    // 将GPIO1_IO03配置为GPIO功能(ALT5模式)
    
    // 2. 电气特性配置
    IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0x10B0);
    // 0x10B0配置含义:
    // - 驱动强度:R0/6(普通驱动)
    // - 速度:100MHz
    // - 上拉电阻:47K上拉
    // - 滞回器:使能
    // - 开漏输出:禁用
    
    // 3. 方向配置(输出模式)
    GPIO1->GDIR |= (1 << 3);    // 设置GPIO1_IO03为输出
    
    led_off();                  // 初始状态关闭
}

void led_on(void)   { GPIO1->DR &= ~(1 << 3); }  // 低电平点亮
void led_off(void)  { GPIO1->DR |= (1 << 3); }   // 高电平熄灭
void led_nor(void)  { GPIO1->DR ^= (1 << 3); }   // 电平翻转

2. 蜂鸣器驱动(beep.c)

void beep_init(void)
{
    // SNVS域GPIO(低功耗域)
    IOMUXC_SetPinMux(IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01, 0);
    IOMUXC_SetPinConfig(IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01, 0x10B0);
    
    GPIO5->GDIR |= (1 << 1);    // GPIO5_IO01设为输出
    beep_off();
}

SNVS域:始终供电域,系统休眠时仍可工作。

3. 按键驱动(key.c)与中断配置

void key_irq_handler(void)
{
    // 检查GPIO1_IO18中断状态
    if ((GPIO1->ISR & (1 << 18)) != 0)
    {
        led_nor();              // 翻转LED
        beep_nor();             // 翻转蜂鸣器
        GPIO1->ISR |= (1 << 18); // 清除中断标志
    }
}

void key_init(void)
{
    // 1. 引脚配置
    IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0);
    IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xF0B0);
    GPIO1->GDIR &= ~(1 << 18);  // 设为输入模式
    
    // 2. 中断触发方式配置
    GPIO1->ICR2  |= (3 << 4);   // 设置GPIO1_IO18双边沿触发
    /*
    ICR2寄存器位说明:
    - bits[5:4]:ICR18配置位
    - 00:低电平触发  01:高电平触发
    - 10:上升沿触发  11:双边沿触发
    */
    
    // 3. 中断使能
    GPIO1->IMR |= (1 << 18);    // 使能GPIO1_IO18中断屏蔽
    
    // 4. GIC中断配置
    GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);  // 使能GIC中断
    GIC_SetPriority(GPIO1_Combined_16_31_IRQn, 0); // 设置最高优先级
    
    // 5. 注册中断处理函数
    system_interrupt_register(GPIO1_Combined_16_31_IRQn, key_irq_handler);
}

五、主程序(main.c)

int main(void)
{
    system_interrupt_init();    // 1. 中断系统初始化
    clock_init();               // 2. 时钟系统初始化
    led_init();                 // 3. LED初始化
    beep_init();                // 4. 蜂鸣器初始化
    key_init();                 // 5. 按键中断初始化
    
    while(1)                    // 主循环
    {
        led_nor();              // 翻转LED(非中断方式)
        g_delay(0x7FFFF);       // 软件延时
    }
    return 0;
}

// 简单软件延时函数
void g_delay(unsigned int t)
{
    while(t--);                 // 空循环消耗时间
}

六、系统工作流程总结

1. 启动流程

上电复位 → 执行_start(汇编) → 初始化CPU → 设置栈指针 
→ 初始化BSS段 → 跳转到main() → 外设初始化 → 进入主循环

2. 中断响应流程

按键按下 → GPIO检测边沿 → 置位中断标志 → GIC接收中断
→ CPU响应IRQ → 执行_irq_handler(汇编) → 保存现场
→ 调用system_interrupt_handler → 执行key_irq_handler
→ 处理中断(翻转LED/蜂鸣器) → 清除中断标志 → 恢复现场返回

3. 时钟树概要

24MHz晶振 → PLL1(2112MHz) → 2分频 → ARM内核(1056MHz)
         → PLL2(528MHz) → PFD2(396MHz) → 3分频 → AHB(132MHz) → 2分频 → IPG(66MHz)
         → PLL3(480MHz) → 各PFD通道 → 各外设时钟

4. 硬件连接关系

GPIO1_IO03 → LED(低电平点亮)
GPIO5_IO01 → 蜂鸣器(低电平发声)
GPIO1_IO18 → 按键(按下为低电平)

七、关键概念解析

1. 内存地址映射

0x87800000:异常向量表起始地址(由VBAR指定)
0x82000000:IRQ模式栈空间
0x84000000:系统模式栈空间
0x0209C000:GPIO1寄存器基地址
0x020A8000:CCM时钟控制器基地址

2. 中断优先级

  • GIC优先级:0为最高,数值越大优先级越低

  • 嵌套中断:高优先级中断可打断低优先级中断处理

  • 中断屏蔽:通过CPSID/CPSIE指令全局控制

3. GPIO寄存器关键位

  • GDIR:方向寄存器(1=输出,0=输入)

  • DR:数据寄存器(读写引脚电平)

  • ICR1/2:中断配置寄存器(触发方式)

  • IMR:中断屏蔽寄存器(1=使能中断)

  • ISR:中断状态寄存器(1=中断发生,写1清除)

4. 时钟配置安全原则

  1. 先降频后升频:配置PLL前切换到低频时钟

  2. 分频保护:设置倍频前先配置输出分频

  3. 等待锁定:PLL配置后需等待LOCK标志

  4. 顺序切换:时钟源切换按标准流程操作

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。