上一篇中,介绍了GPIO相关的所有寄存器,并在最后简单实现了一个LED灯的控制,由于那个篇幅实在是太长了,编程那部分写的有些许潦草,本文再借着点亮剩下的LED小灯来做个稍微详细点的描述,会涉及一些开发环境使用中的常见BUG、以及部分位操作相关的C语言知识。文中如有不足之处欢迎大家批评指正,创作不易,需要转载或者引用的请注明出处。
常见的使用通用输出模式的片外外设就是LED灯、有源蜂鸣器、继电器等等,在我们板子上最常见的就是LED灯,上一篇中,配置了GPIO的端口A的4号管脚为推挽输出模式,并输出了低电平点亮了LED灯,我们先回顾一下配置流程,
首先是初始化部分,这里还是借用一个伪代码,这样看起来更加直观:
//注意前面这一段注释一定要记得写上,一方面是为了方便自己能够看懂自己的代码,另一方面,当别人看你的代码的时候也能一目了然。
/*******************************
函数名: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);//清零OSPEEDR
GPIOA->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来传递参数。
然后编译下载,可以实现如下图效果:
如果出现代码右侧一直报小红叉,但是编译又没有问题的情况,如下图所示,这是因为KEIL默认开启了Dynamic Syntax Checking(动态语法检查的原因)看着十分恼火,这里我们手动关闭一下。
步骤:
1.点击下图所得扳手
2.在弹出的框中选择Text Completion ----------->然后把Dynamic Syntax Checking下方的复选框给取消勾选就可以了。
出现这类问题极有可能是自己的版本和其他人的版本不一致,KEIL的新版有可能连一些官方例程都没法编译通过。遇到这种问题一般双击下面的报错选项都会跳转到一些官方文件显示一些东西未定义
这是由于编译器版本太新造成的。
类似如下的报错:
…/CMSIS/Include\core_cmFunc.h(614): error: unknown register name ‘vfpcc’ in asm
__ASM volatile (“VMSR fpscr, %0” : : “r” (fpscr) : “vfpcc”);
解决方案:点击1位置的魔法棒,然后选择2的Target,再然后选择3位置的编译器版本为5,然后重新编译即可。
这个也是可以设置成让其自动帮我们完成复位操作的,也是点击魔法棒,然后选择Debug,注意要选中ST-LINK再点击settings,在弹出的框上将4号框的复选框选中即可。
Keil MDK5修改配色和字体以及字号的方法http://t.csdn.cn/MPqpH
第一类,…\USER\main.c(6): error: #65: expected a “;”
下方提示是小的errors,这种错误直接点击errors会跳转到对应位置,在跳转的位置仔细寻找自己的语法错误即可,有时候定位和报的错误类型不一定准,但一般都是在定位附近查找即可,如下图,报错是说在main.c的第六行是少了一个分号,实际是第六行的赋值运算符多写了一个=号。
第二类,.\Objects\TEST1.axf: Error: L6218E: Undefined symbol Led_Flow (referred from main.o).
报错是Errors,E是大写的,这种错误一般都是没有定义,或者重复定义,或者是没有将对应的.c文件添加到工程引起的报错。
他也会指出地方和报错的内容,下图的报错,说的就是,在main.o中没有找到Led_Flow这个函数,这里的main.o实际就是main.c;整个.C的编译过程如下图所示:
重新添加Led.c进入工程后即可解决报错。
1.嵌入式学习笔记——概述
2.嵌入式学习笔记——基于Cortex-M的单片机介绍
3.嵌入式学习笔记——STM32单片机开发前的准备
4.嵌入式学习笔记——STM32硬件基础知识
5.嵌入式学习笔记——认识STM32的 GPIO口
6.嵌入式学习笔记——使用寄存器编程操作GPIO
7.嵌入式学习笔记——寄存器实现控制LED小灯
8.嵌入式学习笔记——使用寄存器编程实现按键输入功能
9.嵌入式学习笔记——STM32的USART通信概述
10.嵌入式学习笔记——STM32的USART相关寄存器介绍及其配置
11.嵌入式学习笔记——STM32的USART收发字符串及串口中断
12.嵌入式学习笔记——STM32的中断控制体系
13.嵌入式学习笔记——STM32寄存器编程实现外部中断
14.嵌入式学习笔记——STM32的时钟树
15.嵌入式学习笔记——SysTick(系统滴答)
16.嵌入式学习笔记——M4的基本定时器
17.嵌入式学习笔记——通用定时器
18.嵌入式学习笔记——PWM与输入捕获(上)
19.嵌入式学习笔记——PWM与输入捕获(下)
20.嵌入式学习笔记——ADC模数转换器
21.嵌入式学习笔记——DMA
22.嵌入式学习笔记——SPI通信
23.嵌入式学习笔记——SPI通信的应用
24嵌入式学习笔记——IIC通信