目录
继上篇使用汇编完成LED点灯功能后,本篇将使用C语言的固件库进行后续的开发
首先对外设功能实现可概括为以下三步
1.先看底板原理图找到对应外设及引脚;
2.再去开发手册里找对应引脚的复用以及电气实现;
3.在VScode对相应的GPIOx寄存器进行赋值。
一.蜂鸣器
1.查看底板原理图


显然给低电平蜂鸣器工作。
2.查找开发手册
由于引脚以默认功能命名,故BEEP对应引脚名即SNVS TAMPER1,也即该引脚的默认功能,而由于蜂鸣器为GPIO功能,故该引脚一定可以复用为GPIO功能,则应去参考手册的复用功能的SNVS开头寻找

1.先看复用功能

由上图可知,MUX_MODE只有101一种值,故该引脚只有默认GPIO5_IO01的一个功能复用。
2.再看电气属性

由于蜂鸣器也是输出模式,与LED灯一样,对电气属性这些设置默认值,设为0x10B0,和LED灯一样即可(具体可参考上篇LED灯的电气属性)
3.查找GPIO功能


给GPIO5的bit1置0,BEEP对应引脚即为低电平工作

设置GDIR为输出模式
3.代码实现
1.先导入ARM固件库

2.调用库函数
void beep_init(void)
{
// 配置引脚复用功能
IOMUXC_SetPinMux(IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01, 0);
// 配置引脚电气属性
IOMUXC_SetPinConfig(IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01, 0x10B0);
// 配置引脚方向:输出方向
GPIO5->GDIR |= (1 << 1);
}
void beep_on(void)
{
GPIO5->DR &= ~(1 << 1);
}
void beep_off(void)
{
GPIO5->DR |= (1 << 1);
}
void beep_nor(void)
{
GPIO5->DR ^= (1 << 1);
}
// 使能外设时钟
void enable_clocks(void)
{
CCM->CCGR0 = 0xFFFFFFFF;
CCM->CCGR1 = 0xFFFFFFFF;
CCM->CCGR2 = 0xFFFFFFFF;
CCM->CCGR3 = 0xFFFFFFFF;
CCM->CCGR4 = 0xFFFFFFFF;
CCM->CCGR5 = 0xFFFFFFFF;
CCM->CCGR6 = 0xFFFFFFFF;
}
3.源码解读
1.引脚复用及电气函数实现
先是引脚的名字,再是引脚的具体复用功能,不同引脚具有不同的复用功能,如下图所示,上篇点亮LED灯对应的GPIO1_IO03引脚具有多个复用功能,而SNVS_TAMPER1则只有GPIO5_IO01的功能。


首先IOMUXC_SetPinMux函数传递两个参数,一个带参宏,一个int型数据,前者为定义一个宏传五个参数,如下

而根据函数定义,需要传递六个参数,故定义第一个参数宏为了方便传参,传一个宏等于传五个参数,如下图所示,六个参数分别代表引脚复用寄存器,引脚复用模式,选择输入寄存器,输入压摆率,电气属性配置寄存器地址,以及软件输入开启,对应SION位


IOMUXC_SetPinConfig函数则同理,用于设置电气属性,唯有第二个参数为要配置的电气属性的值,蜂鸣器与LED灯由于都是输出模式,故可采用相同的默认值,前文也已经提到。
2.GPIO功能实现


显然底层源码通过宏定义,指定了对应的GPIOx的基地址,将GPIOx的基地址强转为GPIO_Type *的指针,而GPIO_Type则为定义了GPIO里的8个寄存器的结构体,如此即可通过GPIOx指针访问对应结构体成员的寄存器进行赋值。


而__IO,__I,__O则被定义为volatile
//指针前加volatile,表示通过该指针访问的内存空间中的内容不会被优化
//volatile 避免编译器对该变量的任何操作进行优化
//部分SOC中写1清0,很奇怪,老师也不懂
//例如
int i = 0x01; //0x 0000 0001
i = i; //0x 0000 0001 = 0x 0000 0001,把自己写入,为1的bit则清0,即可将bit0清0
//若不加volatile,编译器则会认为i = i为冗余指令,自动优化
3.CCM功能实现


其中,RESERVED数组用来占位,用于内存对齐
在单片机的“寄存器映射”(Register Map)中,物理地址并不是每一个字节都有对应的寄存器。寄存器之间往往会留有空隙(保留区域)。
C 语言的结构体在内存中是连续分配的。如果不用数组占位,编译器会认为 CSCDR3 后面紧接着就是 CDHIPR,这会导致结构体成员的地址偏移与硬件实际物理地址完全错位。

显然CCM_Type结构体同GPIO一样,内部定义了对应的寄存器,与参考手册的寄存器一一对应。

而通过对CCM_BASE赋值得到基地址,再强转为CCM_Type类型,定义为CCM,此时即可通过CCM指针访问寄存器的值。
二.按键

ON/OF按键为控制低功耗模式,中间为复位按键,而用户能真正操作的按键只有这个黄色的按键KEY0,也就是本次要实现的功能。
1.查看底板原理图

按键没有被按下时,KEY0与上拉电阻连接,为3.3V高电平,按键按下,接通GND,则置为低电平

显然默认为串口1功能,而 按键为GPIO功能,所以仍要在引脚复用章节查看手册。
2.查找开发手册
1.复用实现

显然只有0101为GPIO功能,即为key的实现
2.电气属性

key为输入模式,则对于电气属性的配置应有些改变。
HYS默认即可
PUS为设置电阻,而硬件图里有一个10k的上拉电阻,此处应选择11,22k的上拉电阻,与10k的电阻并联,总电阻值应小于10k,现在市面上的芯片内部设计对内部电阻有一定要求,4.7k--10k之间
PUE置1,控制引脚使用上拉
PKE打开PUE的开关,为1表示PUE上拉保持功能开,置1
后面的默认值即可,0xF0B0
3.代码实现
void key_init(void)
{
// 引脚复用功能
IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0);
// 引脚电气属性
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xF0B0);
// 按键引脚初始化
GPIO_Type_t t =
{
.direction = gpio_input//输入功能,无需配置引脚电平
};
gpio_init(GPIO1, 18, &t);
}
int key_press(void)
{
if (gpio_get_value(GPIO1, 18) == 0)
{
return 1;
}
else
{
return 0;
}
}
三.清空bss段
由于在链接时,只写了text段的地址,对于其他如data段,rodata段,commom段,bss段也应给编译器地址
.text段:存放的代码、指令-Ttext878000000
bss段:存放初始化为0的全局变量和静态变量
.common段:存放未初始化的全局变量
.data段:存放的已经初始化的全局变量和静态变量
.rodata段:存放的是只读常量
1.创建imx6ull.lds文件
lds代表链接脚本文件。
SECTIONS
{
. = 0x87800000; //. 表示当前地址计数器,将 0x87800000赋给当前地址
.text : //在当前地址存放text段
{
obj/start.o //start.o作为启动代码应该放在最前面
*(.text) //*表示将所有文件的text段放在此处
}
.rodata ALIGN(4) : {*(.rodata*)} //ALIGN(4)表示按四字节对齐
//(.rodata*)表示包括所有rodata类型
.data ALIGN(4) : {*(.data)}
. = ALIGN(4);
__bss_start = .; //将当前地址作为起始地址
.bss ALIGN(4) : {*(.bss) *(COMMON)} //bss段与common段为未初始化的值,都需要清零
__bss_end = .; ////将当前地址作为结束地址
}
## 关于段名中的通配符说明
在链接脚本中,经常会看到类似`*(.rodata*)`这样的写法,这里包含两个`*`通配符,它们有不同的含义:
1. 第一个`*`:
- 表示收集所有输入文件中的相关段
- 如果不加这个`*`,就只会收集特定文件的段
2. 第二个`*`(如`rodata*`中的`*`):
- 实际编译时会产生多种rodata相关的段,例如:
- `.rodata`:基本的只读数据段
- `.rodata.str1.1`:长度为1字节对齐的字符串常量
- `.rodata.str1.4`:长度为4字节对齐的字符串常量
- `.rodata.cst8`:8字节常量(如double类型的常量)
- `.rodata.cst4`:4字节常量(如float类型的常量)
如果只写`*(.rodata)`而不是`*(.rodata*)`:
- 只会收集严格命名为`.rodata`的段
- 其他相关的只读数据段(如`.rodata.str1.1`等)会被遗漏
- 这些被遗漏的数据可能导致程序无法正常工作
同时makefile的链接,-T0x8700 0000地址换为 -Timx6ull.lds
arm-linux-gnueabihf-ld -Timx6ull.lds $(objs) -o $(target).elf
2.清空bss与common
在启动代码的复位函数中应增加对bss段清零的操作
_reset_handler:
// 设置irq模式
mrs r0, cpsr
bic r0, r0, #0x1F
orr r0, r0, #0x12
msr cpsr, r0
// 设置irq模式下的栈(32M)
ldr sp, =0x86000000
// 设置system模式
mrs r0, cpsr
bic r0, r0, #0x1F
orr r0, r0, #0x1F
msr cpsr, r0
// 设置system模式下的栈
ldr sp, =0x84000000
// 清空bss与common段
bl _bss_clear
b main
_bss_clear:
ldr r0, =__bss_start
ldr r1, =__bss_end
loop:
mov r2, #0
str r2, [r0]
add r0, r0, #4
cmp r0, r1
blt loop
bx lr
3.项目创建规范
最后,工程创建最好有一定的规范,如下

四.将GPIO功能封装
通过对前面外设功能的实现,我们发现对于这种实现流程相似的功能可以封装函数,创建GPIO.c与GPIO.h实现,这样代码将具有更好的复用性。
1.GPIO封装
GPIO.h
// GPIO引脚方向枚举
enum GPIO_Direction
{
gpio_input,
gpio_output
};
// GPIO信息结构体
typedef struct
{
enum GPIO_Direction direction; // 引脚方向
int default_value; // 引脚默认电平
}GPIO_Type_t;
GPIO.c
// gpio初始化
void gpio_init(GPIO_Type *base, int pin, GPIO_Type_t *gpio) //GPIOx(第几组), 几号引脚,定义的GPIO_Type结构体
{
if (gpio->direction == gpio_output)
{
base->GDIR |= (1 << pin);
if (gpio->default_value)
{
base->DR |= (1 << pin);
}
else
{
base->DR &= ~(1 << pin);
}
}
else if (gpio->direction == gpio_input)
{
base->GDIR &= ~(1 << pin);
}
}
// gpio写
void gpio_set_value(GPIO_Type *base, int pin, int value)
{
if (value)
{
base->DR |= (1 << pin);
}
else
{
base->DR &= ~(1 << pin);
}
}
// gpio读 1高电平 0低电平
int gpio_get_value(GPIO_Type *base, int pin)
{
return (base->DR & (1 << pin)) != 0;
}
2.LED 调用

3.BEEF 调用

2418



被折叠的 条评论
为什么被折叠?



