《i.MX6ULL LED 裸机开发实战:从寄存器到点亮》
后面学习驱动开发时,我们会发现其实有不少内容和裸机阶段学过的知识是相通的。
为了帮大家更好地理解,我参考了《正点原子驱动开发手册》,结合自己的理解写下这篇文章,希望能对刚入门驱动开发的同学有所帮助。
本文主要通过一个 LED 裸机实验,带大家熟悉最基本的寄存器操作流程,为后续的 Linux 驱动开发打下基础。
1.GPIO
我们在编写STM32
的GPIO
功能时,我们会编写这一段代码
GPIO_InitTypeDef GPIO_InitStructure;//结构体声明RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);//时钟使能GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;//推挽输出
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_13;//13口
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;//频率
GPIO_Init(GPIOC,&GPIO_InitStructure);//结构体初始化GPIO_SetBits(GPIOC,GPIO_Pin_13);//设置高电平
在 STM32 裸机开发中,我们通常会先打开时钟,再通过寄存器配置 GPIO 的输入输出模式,比如推挽输出、上拉输入等。
而在 Linux 开发板中,同样也存在时钟控制和 IO 功能,只不过它们的配置方式更抽象,由内核和设备树共同完成。
对于每一个引脚(Pin),Linux 内核需要知道它具体的复用功能(MUX)和 管脚属性(PAD):
- MUX(Multiplexing) 决定了这个引脚究竟用作哪种外设功能(如 GPIO、UART、I2C、SPI 等)。
- PAD 则进一步定义了该引脚的电气特性,比如上下拉、驱动能力、输入保持等。
这就意味着,虽然我们在 Linux 下“点亮一个 LED”看似简单,但底层其实经历了引脚复用、PAD 属性配置、GPIO 控制等多个环节。理解这些内容,有助于我们更好地读懂设备树、分析驱动的底层行为。
总结
名称 | 类比 | 功能 |
---|---|---|
MUX | STM32 的 “Alternate Function (AF)” | 决定这个引脚干什么 |
PAD | STM32 的 “GPIO_InitTypeDef 参数(Pull/Speed/Mode)” | 决定这个引脚的电气特性 |
2.汇编展示
这里给大家展示一段汇编代码,能更清楚直观的感受到,GPIO
的配置
.global _start /* 全局标号,程序入口 */_start:/* =========================* 1、使能所有外设时钟(CCGR0~CCGR6)* ========================= */
ldr r0, =0x020C4068 /* CCGR0 地址 */
ldr r1, =0xFFFFFFFF
str r1, [r0]ldr r0, =0x020C406C /* CCGR1 */
str r1, [r0]ldr r0, =0x020C4070 /* CCGR2 */
str r1, [r0]ldr r0, =0x020C4074 /* CCGR3 */
str r1, [r0]ldr r0, =0x020C4078 /* CCGR4 */
str r1, [r0]ldr r0, =0x020C407C /* CCGR5 */
str r1, [r0]ldr r0, =0x020C4080 /* CCGR6 */
str r1, [r0]
//str r1, [r0]:把 r1 的值写入 r0 指向的内存地址(即写寄存器)。重点是地址!!/* =========================* 2、配置 GPIO1_IO03 复用为 GPIO 功能* ========================= */
ldr r0, =0x020E0068 /* SW_MUX_GPIO1_IO03 */
ldr r1, =0x5 /* MUX_MODE=5 -> GPIO */
str r1, [r0]/* =========================* 3、设置 GPIO1_IO03 IO 属性(PAD 配置)* ========================= */
/* SW_PAD_GPIO1_IO03 寄存器:* bit16 : HYS = 0 关闭* bit15-14: PUS = 00 默认下拉* bit13 : PUE = 0 keeper功能* bit12 : PKE = 1 pull/keeper使能* bit11 : ODE = 0 关闭开路输出* bit7-6 : SPEED = 10 100MHz* bit5-3 : DSE = 110 R0/6 驱动能力* bit0 : SRE = 0 低转换率*/
ldr r0, =0x020E02F4 /* SW_PAD_GPIO1_IO03 */
ldr r1, =0x10B0 /* 对应位配置 BIN:0001 0000 1011 0000 */
str r1, [r0]/* =========================* 4、设置 GPIO1_IO03 为输出* ========================= */
ldr r0, =0x0209C004 /* GPIO1_GDIR */
ldr r1, =0x0000008 /* 第3位 = 1 表示输出 */
str r1, [r0]/* =========================* 5、控制 LED 点亮* ========================= */
/* GPIO1_IO03 输出低电平点亮 LED */
ldr r0, =0x0209C000 /* GPIO1_DR */
ldr r1, =0x0 /* 输出 0,LED亮 */
str r1, [r0]/* =========================* 6、死循环,防止程序跑出* ========================= */
loop:b loop
- 首先就是找到时钟对应的地址,通过修改地址对应的值,以开启时钟
- 修改io引脚的值,得到想要复用的功能
- 根据手册查看需要修改位数
01
,编写属性,最后转十六进制写入- 最后把引脚对应地址值修改
01
,输出电平
3.C语言展示
虽然汇编能直接操作寄存器,但一旦代码多起来,满屏的数字和指令看得人头都大
了。
想想看,这么多看不懂的地址和数据,要是能换成有意义的英文单词该多好?
于是就有了 C 语言的宏定义
。它能把那些固定的寄存器地址“翻译”成可读的符号,看起来也舒服多
了。
所以,接下来我就用 C 语言来写一版,看看如何通过更友好的方式实现同样的功能。
imx6ul.h
#define CCM_BASE (0X020C4000)
#define CCM_ANALOG_BASE (0X020C8000)
#define IOMUX_SW_MUX_BASE (0X020E0014)
#define IOMUX_SW_PAD_BASE (0X020E0204)
#define GPIO1_BASE (0x0209C000)
#define GPIO2_BASE (0x020A0000)
#define GPIO3_BASE (0x020A4000)
#define GPIO4_BASE (0x020A8000)
#define GPIO5_BASE (0x020AC000)/* * CCM寄存器结构体定义,分为CCM和CCM_ANALOG */
typedef struct
{volatile unsigned int CCR;volatile unsigned int CCDR;volatile unsigned int CSR;'''volatile unsigned int CCGR5;volatile unsigned int CCGR6;volatile unsigned int RESERVED_3[1];volatile unsigned int CMEOR;
} CCM_Type; typedef struct
{volatile unsigned int PLL_ARM;volatile unsigned int PLL_ARM_SET;volatile unsigned int PLL_ARM_CLR;'''volatile unsigned int MISC1_TOG;volatile unsigned int MISC2;volatile unsigned int MISC2_SET;volatile unsigned int MISC2_CLR;volatile unsigned int MISC2_TOG;
} CCM_ANALOG_Type; /* * IOMUX寄存器组*/
typedef struct
{volatile unsigned int BOOT_MODE0;volatile unsigned int BOOT_MODE1;'''volatile unsigned int CSI_DATA00;volatile unsigned int CSI_DATA01;volatile unsigned int CSI_DATA02;volatile unsigned int CSI_DATA03;volatile unsigned int CSI_DATA04;volatile unsigned int CSI_DATA05;volatile unsigned int CSI_DATA06;volatile unsigned int CSI_DATA07;
}IOMUX_SW_MUX_Type;typedef struct
{volatile unsigned int DRAM_ADDR00;volatile unsigned int DRAM_ADDR01;volatile unsigned int DRAM_ADDR02;'''volatile unsigned int GRP_DDRHYS;volatile unsigned int GRP_DDRPKE;volatile unsigned int GRP_DDRMODE;volatile unsigned int GRP_DDR_TYPE;
}IOMUX_SW_PAD_Type;/* * GPIO寄存器结构体*/
typedef struct
{volatile unsigned int DR; volatile unsigned int GDIR; volatile unsigned int PSR; volatile unsigned int ICR1; volatile unsigned int ICR2; volatile unsigned int IMR; volatile unsigned int ISR; volatile unsigned int EDGE_SEL;
}GPIO_Type;/* * 外设指针 */
#define CCM ((CCM_Type *)CCM_BASE)
#define CCM_ANALOG ((CCM_ANALOG_Type *)CCM_ANALOG_BASE)
#define IOMUX_SW_MUX ((IOMUX_SW_MUX_Type *)IOMUX_SW_MUX_BASE)
#define IOMUX_SW_PAD ((IOMUX_SW_PAD_Type *)IOMUX_SW_PAD_BASE)
#define GPIO1 ((GPIO_Type *)GPIO1_BASE)
#define GPIO2 ((GPIO_Type *)GPIO2_BASE)
#define GPIO3 ((GPIO_Type *)GPIO3_BASE)
#define GPIO4 ((GPIO_Type *)GPIO4_BASE)
#define GPIO5 ((GPIO_Type *)GPIO5_BASE)
解释:
在裸机或驱动开发中,我们会根据芯片手册给出的外设寄存器地址,使用宏定义(#define
)定义每个外设的基地址(Base Address)。
然后将这些地址强制转换为对应外设寄存器结构体类型的指针(例如GPIO_Type *
),这样程序就能通过结构体成员的方式访问各个寄存器。每个结构体成员在内存中的偏移量是固定的,因此当我们访问
GPIO->GDIR
时,实际上就是从基地址开始加上一个特定偏移量去访问真实的硬件寄存器地址。通过修改这些寄存器的值,就能控制硬件外设的功能,实现点亮 LED、配置时钟、设置引脚复用等底层操作。
至于那些
reversed
我觉得应该是去对芯片手册一列列看下来吧
有了结构体,我们修改数值就更为方便,观感也更加直观
#include "imx6ul.h"void clk_enable(void)
{CCM->CCGR0 = 0xFFFFFFFF;CCM->CCGR1 = 0xFFFFFFFF;CCM->CCGR2 = 0xFFFFFFFF;CCM->CCGR3 = 0xFFFFFFFF;CCM->CCGR4 = 0xFFFFFFFF;CCM->CCGR5 = 0xFFFFFFFF;CCM->CCGR6 = 0xFFFFFFFF;
}void led_init(void)
{/* 1. 初始化 IO 复用 */IOMUX_SW_MUX->GPIO1_IO03 = 0x5; /* 复用为 GPIO1_IO03 *//** 2. 配置 GPIO1_IO03 的 IO 属性*/IOMUX_SW_PAD->GPIO1_IO03 = 0x10B0;//详细可以看汇编代码那一部分,有写的/* 3. 初始化 GPIO */GPIO1->GDIR = 0x00000008; /* GPIO1_IO03 设置为输出 *//* 4. 设置 GPIO1_IO03 输出低电平,点亮 LED0 */GPIO1->DR &= ~(1 << 3);
}/** @description : 打开 LED 灯* @param : 无* @return : 无*/void led_on(void)
{/* 将 GPIO1_DR 的 bit3 清零 */GPIO1->DR &= ~(1 << 3);
}/** @description : 关闭 LED 灯* @param : 无* @return : 无*/void led_off(void)
{/* 将 GPIO1_DR 的 bit3 置 1 */GPIO1->DR |= (1 << 3);
}/** @description : 短时间延时函数* @param - n : 延时循环次数(空操作循环次数)* @return : 无*/
void delay_short(volatile unsigned int n)
{while (n--) {}
}/** @description : 延时函数,在 396MHz 主频下* 延时时间大约为 1ms* @param - n : 要延时的 ms 数* @return : 无*/
void delay(volatile unsigned int n)
{while (n--){delay_short(0x7FF);}
}int main(void)
{clk_enable(); /* 使能所有时钟 */led_init(); /* 初始化 LED */while (1) /* 死循环 */{led_off(); /* 关闭 LED */delay(500); /* 延时 500ms */led_on(); /* 打开 LED */delay(500); /* 延时 500ms */}return 0;
}
4. 总结
本篇的
LED 裸机开发
实验就到这里啦!
通过这一篇内容,我们了解了从汇编到 C 语言的开发方式,初步建立了对底层寄存器和外设控制框架的认识。在下一篇中,我们将正式踏入
LED 驱动开发
的世界,去感受一下 物理地址 与 虚拟地址 之间的奇妙“摩擦”,看看同样的点灯实验在 Linux 驱动中是如何实现的