文章目录
- 前言
- GPIO通用输出模式
- 初始化LED小灯的GPIO
- 原理图
- 初始化代码
- 初始化的效果
- 功能函数封装
- 直接分开宏定义两个
- 使用条件运算符
- 封装函数实现简单的功能
前言
上一篇中,介绍了GPIO相关的所有寄存器,并在最后简单实现了一个LED灯的控制,由于那个篇幅实在是太长了,编程那部分写的有些许潦草,本文再借着点亮剩下的LED小灯来做个稍微详细点的描述,会涉及一些开发环境使用中的常见BUG、以及部分位操作相关的C语言知识。文中如有不足之处欢迎大家批评指正,创作不易,需要转载或者引用的请注明出处。
GPIO通用输出模式
常见的使用通用输出模式的片外外设就是LED灯、有源蜂鸣器、继电器等等,在我们板子上最常见的就是LED灯,上一篇中,配置了GPIO的端口A的4号管脚为推挽输出模式,并输出了低电平点亮了LED灯,我们先回顾一下配置流程,
初始化LED小灯的GPIO
首先是初始化部分,这里还是借用一个伪代码,这样看起来更加直观:
//注意前面这一段注释一定要记得写上,一方面是为了方便自己能够看懂自己的代码,另一方面,当别人看你的代码的时候也能一目了然。
/*******************************
函数名:Led_Init
函数功能:LED灯IO口初始化配置
函数形参:void
函数返回值:void
备注:
LED1----->PA6 通用输出
LED2----->PA7 通用输出
********************************/
void Led_Init(void)//函数命名,一般是'模块——功能'其中模块名与功能名的首字母大写
{①打开PA的时钟②端口模式寄存器③输出类型寄存器④输出速度寄存器⑤上下拉寄存器
}
根据这个伪代码我们即可配置出对应端口对应管脚的模式为通用输出模式。
下面我们在昨天的基础上再来配置一次,这次将两个LED都一并初始化。
原理图
还是昨天的原理图,从这可以知道,需要配置的是PA6和PA7两个口。
初始化代码
/*******************************
函数名:Led_Init
函数功能:LED灯IO口初始化配置
函数形参:void
函数返回值:void
备注:
LED1----->PA6 通用输出
LED2----->PA7 通用输出
********************************/
void Led_Init(void)//函数命名,一般是'模块——功能'其中模块名与功能名的首字母大写
{
/*---------------------①打开PA的时钟------------------------------------------------------------------------*/RCC->AHB1ENR |= (1<<0);/*对应在编程手册的6.3.12节,有关于此寄存器的配置;GPIOA端口的时钟使能是有该寄存器的第0位进行控制的,要使能GPIOA的时钟就是对其第0位进行写1,根据前面提到的位操作,就是将1直接赋值给该寄存器即可。*/
/*----------------------②端口模式寄存器---------------------------------------------------------------------*//*端口模式清零*/GPIOA->MODER &= ~(0xf<<12);/*这一步是保证我们操作的寄存器在我们写入数据之前一定是00.避免出现被覆盖,而变成其他模式的问题。清零的思路:1.我们需要写入的是5和6两个端口,也就是这个寄存器的15 14 13 12 这四位为了将这四位清零,首先使用‘|’运算肯定是不行的,只能选择‘&0’的操作才能确保对应位清零。2.0是不能移位操作的,所以只能借助1移位后再进行取反来实现;3.于是得出清零语句GPIOA->MODER &= ~(0xf<<12);也就是0xf也就是二进制的1111向前移了12位变成了1111 0000 0000 0000然后取反变成0000 1111 1111 1111然后与原来寄存器内的数据相与,和0相与的位都被清成0了,和1相与的位保持不变,0000 XXXX XXXX XXXX这样既保证了对应位写入0又保证了其他位不被干扰。*//*端口通用输出模式*/GPIOA->MODER |= (0x5<<12);/*上一步已经清零了需要配置的位,接下来直接写入即可,写入过程:1.查询寄存器可以知道,要配置为通用输出模式,需要对这四位写入:01 01;2.具体操作可以通过|=来实现,GPIOA->MODER |= (0x5<<12);3.0101是十六进制的0x5,需要前移12位来到我们需要操作的数据位,由于里面的数据位都是0,所以|操作后这四位被覆盖成了0101*/
/*----------------------③输出类型寄存器-----------------------------------------------------------------------*//*端口输出推挽模式*/GPIOA->OTYPER &= ~(3<<6);/*由于我们需要控制小灯的亮灭,这就要求GPIO口具有独立输出高低电平的能力,所以我们需要配置为推挽模式,也就是需要将第六位以及第七位进行写零操作,参考上面的写零思路:只需要将二进制的11也就是十进制的3左移6位即可实现。*/
/*----------------------④输出速度寄存器-----------------------------------------------------------------------*//*端口输出速度25MHz就只是控制一个LED灯,对于引脚的高低电平翻转速度没有啥要求,配置一个中速即可,也就是需要将第15 14 13 12 四位先清零,然后写入0101,与上面操作一致*/GPIOA->OSPEEDR &= ~(0xf<<12);//清零OSPEEDRGPIOA->OSPEEDR |= (0x5<<12);//25MHZ中速
/*----------------------⑤上下拉寄存器--------------------------------------------------------------------------*/GPIOA->PUPDR &= ~(0xf<<12);//默认无上下拉/*由于是输出模式,我们不需要上下拉操作,直接对对应的15 14 13 12 这四位写零即可*/
}
初始化的效果
到这里,我们已经初始化完成了两个LED的GPIO口,此时不管我们先抛开ODR寄存器不管,直接编译烧录,就会发现,两个LED灯已经点亮了,这是因为ODR寄存器默认状态就是低电平输出,所以他会亮。
功能函数封装
实际做产品的过程中,很少有说初始化后就直接点亮或者开启的,都是需要有后续逻辑控制了后再开启的,所以我们需要对上面的代码进行加工,
按照之前的思路,应该是直接操作对应的·ODR寄存器,来实现开灯与关灯,但是这样不利于后期维护代码,可能过个一两周,你回来看自己的代码都看不明白了,所以我们采用宏定义来对这个开灯关灯操作做一个封装,我这里有两种方式,两个灯用了不同的方式,大家根据自己的喜好来就行。
直接分开宏定义两个
第一种方式就是直接分别宏定义一个LEDn_ON,与一个LEDn_OFF,具体写法如下:
// An highlighted block
#define LED_1_ON GPIOA->ODR &= ~(1<<6)//置零拉低对应端口,LED1灯亮
#define LED_1_OFF GPIOA->ODR |= (1<<6)//置1拉高对应端口,LED1灯灭
//这个比较好理解,直接对对应端口的控制位写零写一。
使用条件运算符
第二种方式,使用条件运算符来实现,宏定义的时候定义为LED_n(x) ;当x非0的时候执行(GPIOA->ODR &=~(1<<7);将GPIO对应的控制位置0,拉低IO口,小灯点亮;当x=0时,执行(GPIOA->ODR |=1<<7)将对应位拉高,小灯熄灭。
#define LED_2(x) (x)?(GPIOA->ODR &=~(1<<7):(GPIOA->ODR |=1<<7))
宏定义应该放在那个位置呢,这个我们上一篇中提到过哈,在我们写头文件的时候还专门区分了一个区域用来存放宏定义的。此时的宏定义就放到这。
然后将上面的两个封装好的宏进行调用进初始化,使LED默认熄灭。
编译烧录后,可以发现LED上电后默认是熄灭的。
封装函数实现简单的功能
经过上面的操作,已经将LED的开启与关闭做了封装,接下来,就是做对应功能的封装,类似跑马灯、流水灯,闪烁之类的操作。这类操作,一般都是采用一个功能函数进行封装而不是直接码在while(1)里面的。
这里实现一个流水灯吧。查一个小技巧,如果我们有一个函数代码写的很长了,往后翻比较麻烦,可以在代码任意位置右键------>选择1的位置,---------->点击一下2的位置。
就会出现左侧的折叠符号,点击就可以将整个函数进行折叠。
//第一步,写注释
/***********************************************
*函数名 :Led_Flow
*函数功能 :实现一个简单的流水灯(非阻塞)
*函数参数 :u8 delay
*函数返回值:void
*函数描述 :灯1亮 灯1灭 灯2亮 灯2灭灯1亮(非阻塞)
delay 用来控制流水灯的速度。
*********************************************/
//第二步写函数,由于是非阻塞的流水灯,所以是不能使用while(i--)的那种死等的延时的。
void Led_Flow(u8 delay)
{static u8 n=1;static u32 cnt=0;if(n==1){LED_1_ON;}if(n==2){LED_2(1);}cnt++; //延时切换灯if(cnt>50000*delay) //不精准延时{if(n==1){LED_1_OFF; }if(n==2){LED_2(0);}n++; //往后切灯if(n>2){n=1; }cnt=0;}
}
然后函数声明,在主函数调用;
主函数调用,由于设置了一个可以调速的delay形参,因此在主函数中需要摄者一个Led_Speed来传递参数。
然后编译下载,可以实现如下图效果: