前言
在上一篇文章中,我们完成了汇编语言的LED点灯实验。今天我们将进一步优化,用C语言重写LED驱动,引入SDK头文件,编写蜂鸣器驱动,并使用BSP工程管理方式组织代码,最后通过链接脚本控制程序布局。
本文涉及的知识点较多,建议按顺序阅读:
- volatile关键字与寄存器地址定义
- C语言点灯实现
- Makefile优化
- 寄存器访问方式优化(结构体封装)
- SDK头文件的使用
- 蜂鸣器驱动编写
- BSP工程管理
- 链接脚本编写
一、volatile关键字
在嵌入式开发中,volatile是一个非常重要的关键字。它告诉编译器:
这个变量的值可能会被程序之外的因素改变,每次访问都必须从内存中重新读取,不能优化。
对于寄存器操作,必须使用volatile修饰,否则编译器可能会将多次读写优化掉,导致程序行为异常。
c
#define P2 *((volatile unsigned char*)0x80)
二、C语言点灯实现
2.1 定义寄存器地址
方式一:宏定义直接映射
c
#define CCM_CCGR0 *((unsigned int *)0x020C4068)
#define CCM_CCGR1 *((unsigned int *)0x020C406C)
#define CCM_CCGR2 *((unsigned int *)0x020C4070)
#define CCM_CCGR3 *((unsigned int *)0x020C4074)
#define CCM_CCGR4 *((unsigned int *)0x020C4078)
#define CCM_CCGR5 *((unsigned int *)0x020C407C)
#define CCM_CCGR6 *((unsigned int *)0x020C4080)
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 *((unsigned int *)0x020E0068)
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 *((unsigned int *)0x020E02F4)
#define GPIO1_DR *((unsigned int *)0x0209C000)
#define GPIO1_GDIR *((unsigned int *)0x0209C004)
2.2 编写驱动函数
c
void clock_init(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)
{
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = 0x05;
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 led_delay(unsigned int time)
{
while (time--);
}
三、优化Makefile
makefile
COMPLITER = arm-linux-gnueabihf-
CC = $(COMPLITER)gcc
LD = $(COMPLITER)ld
OBJCOPY = $(COMPLITER)objcopy
OBJDUMP = $(COMPLITER)objdump
OBJS = start.o main.o
TARGET = led
%.o : %.S
$(CC) -c $^ -o $@ -g
%.o : %.c
$(CC) -c $^ -o $@ -g
$(TARGET).bin : $(OBJS)
$(LD) -Ttext 0x87800000 $^ -o $(TARGET).elf
$(OBJCOPY) -O binary -S -g $(TARGET).elf $@
$(OBJDUMP) -D $(TARGET).elf > $(TARGET).dis
clean:
rm $(OBJS) $(TARGET).elf $(TARGET).bin $(TARGET).dis -f
load:
./imxdownload $(TARGET).bin /dev/sdb
注意:原文件中
COMPLITER拼写有误,正确应为COMPILER,这里保留原样以便对照,实际使用建议修正。
四、优化寄存器访问方式
当寄存器数量较多时,使用宏定义会显得冗长。我们可以用结构体封装一组连续的寄存器:
c
struct GPIO_t
{
unsigned int DR;
unsigned int DGIR;
unsigned int PSR;
unsigned int ICR1;
unsigned int ICR2;
unsigned int IMR;
unsigned int ISR;
unsigned int EDGE_SEL;
};
#define GPIO1 (*((struct GPIO_t *)0x0209C000))
这样访问寄存器就像访问结构体成员一样自然:GPIO1->DR、GPIO1->GDIR。
小提示:结构体成员
DGIR应为GDIR(General Direction Register),原笔记中拼写有误,实际开发中需以参考手册为准。
五、使用SDK头文件
5.1 SDK简介
NXP官方提供了i.MX6ULL的SDK包,路径为IMAX6ULL/SDK/。虽然完整的SDK是一个IDE(类似Keil),需要额外的下载器、仿真器,但在裸机开发中,我们只需要它的头文件即可。
5.2 构建新工程
- 新建文件夹
led_sdk - 拷贝
led_c中的start.S、main.c、Makefile - 拷贝SDK目录下所有头文件到新工程
- 熟悉关键头文件,如
GPIO_Type
5.3 利用SDK编写点灯程序
c
void clock_init(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)
{
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0);
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0x10B0);
GPIO1->GDIR |= (1 << 3);
}
void led_on(void)
{
GPIO1->DR &= ~(1 << 3);
}
void led_off(void)
{
GPIO1->DR |= (1 << 3);
}
void led_flicker(void)
{
GPIO1->DR ^= (1 << 3);
}
使用SDK后,代码可读性和可维护性大幅提升。
六、蜂鸣器驱动编写
蜂鸣器电路使用S8550 PNP型三极管 作为开关,基极低电平导通。
c
/* beep.c */
void beep_init(void)
{
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0); /* 根据实际引脚修改 */
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0x10B0);
GPIO1->GDIR |= (1 << 3);
beep_off();
}
void beep_on(void)
{
GPIO1->DR &= ~(1 << 3); /* PNP:低电平导通 */
}
void beep_off(void)
{
GPIO1->DR |= (1 << 3);
}
七、BSP工程管理
随着外设增多,代码需要模块化管理。推荐如下目录结构:
project/ 存放必要程序
main.c
start.S
imx6ull/ 存放NXP提供的i.MX6ULL头文件
cc.h
core_ca7.h
fsl_common.h
fsl_iomuxc.h
MCIMX6Y2.h
bsp/ 存放硬件外设相关功能模块
led.c led.h
beep.c beep.h
Makefile 需要遍历目录
Makefile需要支持多目录编译,可通过wildcard、VPATH等实现。
八、链接脚本
链接脚本imx6ull.lds在链接阶段为连接器提供"蓝图"。关键点:启动代码必须在进入C语言第一条指令前,将.bss和.COMMON段清零。
ld
SECTIONS
{
. = 0x87800000;
.text :
{
obj/start.o
*(.text)
}
.rodata ALIGN(4) : {*(.rodata*)}
.data ALIGN(4) : {*(.data)}
__bss_start = .;
.bss ALIGN(4) : {*(.bss) *(.COMMON)}
__bss_end = .;
}
对应在start.S中需要添加清零.bss段的代码:
asm
ldr r0, =__bss_start
ldr r1, =__bss_end
mov r2, #0
bss_loop:
cmp r0, r1
strlt r2, [r0], #4
blt bss_loop
九、总结
本文从汇编点灯进阶到C语言驱动,核心要点:
| 知识点 | 说明 |
|---|---|
| volatile | 防止编译器优化寄存器访问 |
| 寄存器定义 | 宏定义或结构体封装 |
| SDK头文件 | 只用头文件,不用完整IDE |
| BSP管理 | 按project/imx6ull/bsp分层 |
| 链接脚本 | 指定入口、段布局、bss清零 |
下一步可以尝试:
- 按键输入驱动
- 中断控制器配置
- 时钟树详细配置
如果本文对你有帮助,欢迎点赞、收藏、评论三连!有任何问题也可以在评论区交流。
参考
- NXP i.MX6ULL参考手册
- 正点原子/野火i.MX6ULL裸机教程