打印
[应用相关]

GPIO流水灯改进实现(结构化)(寄存器版)

[复制链接]
78|0
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
tpgf|  楼主 | 2024-11-28 11:48 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
一、引言
大家好!继之前关于流水灯实验的讲解,但很明显我们的代码结构有一个很大的问题:所有逻辑堆在了一个main.c中,比较繁杂,不易看清代码主体结构。因此,接下来我们将继续对流水灯实验进行改进和进一步拓展,使之更加结构化、清晰化 ------- 分文件处理。

二、思路描述
本次流水灯改进实验是基于上一次流水灯实验(基于寄存器)进行的,故如果没看过之前流水灯实现的朋友可以点击下面博客链接先去了解一下,也可以直接食用。

GPIO_流水灯案例(寄存器)-CSDN博客
https://blog.csdn.net/2301_79475 ... 1001.2014.3001.5501
首先我们框架是采用程序设计之分文件处理的思路对代码进行结构化,现在说说此次改进实验的核心思路:

0、准备好存放LED相关代码和延时相关的目录和空文件;

       创建一个存放硬件外设的目录,LED的头文件、LED的源文件;在User中添加Delay延时的头文件和源文件。(文件名称自己定义即可)

1、将main.c中的LED相关配置代码以函数封装,作为LED初始化函数;

       做过点灯实验的朋友应该知道,要点亮LED,首先要配置好LED相关端口位,包含 开启GPIO时钟、配置GPIO端口工作模式,且在流水灯实验中,我们会先给一个默认的led状态(熄灭),所以这三部分代码可作为LED的初始化部分,我们将其封装成LED_Init()函数,后面直接调用,大大节省主调函数代码长度。

2、将LED对应端口名用易懂的宏定义替代,提高编码效率;

       做过点灯实验的朋友也知道,我们的LED通常是通过GPIO端口控制的,因此会用到GPIO端口位的宏定义来给端口状态,但是宏定义名称较长(如GPIO_ODR_ODR1等),写起来比较麻烦,因此我们想到使用LED1、LED2这种简短易懂的宏定义名称将其简化,提高编码效率。

3、将main.c中点亮和熄灭LED的功能代码用点灯/关灯函数封装作为LED开灯/关灯函数;

       与第二个步骤一样,点亮和关闭LED的代码较长,我们将其封装成函数,直接用短点的函数名代替,提高开发效率,也更加模块化。

4、将main.c中的延时功能函数放到延时文件中,LED部分也放到对应文件中。

由于我们流水灯实验使用的延时功能就是用函数封装的,因此这里直接原封不动的拖到延时文件中即可,LED的相关代码亦是同样操作。具体做法是函数声明和相关宏定义放在对应头文件中、函数实现体放入对应源文件中。

5、在main.c中包含延时文件和lLED的头文件;

与我们在main.c头上包含的stm32的宏定义头文件原因相同,相当于给程序一个关于我们使用的函数或者宏定义的路径。

为了保证main.c中能够找到我们放在其他文件中的函数,我们需要在main.c前面包含LED和Delay的头文件,这样程序就知道我们代码中的函数在哪,进而顺利调用到。

6、更改main.c主体代码,用包装的函数进行替换调用。

修改main.c之前的流水灯代码,将LED的配置部分换成初始化函数调用代码、将开灯和关灯部分改成点灯和关灯函数的调用代码、延时函数的直接调用即可。

三、工程创建
这次工程创建和之前关于流水灯创建方式一样:复制上一次流水灯实验的项目工程 -> 剔出之前配置产生的文件 -> 根据需求适当添加新文件

前面描述思路的时候也提到需要增加一些目录以及文件,因此,这里我在以图示的方式展示一下:

1、复制之前的流水灯项目工程



2、修正副本项目名称



3、删除之前的配置文件

(别把之前创建的启动目录和主目录删了)



删除后如下图所示



4、修正工程文件名



5、准备新目录文件

创建硬件外设去掉存放的目录Hardware



进入新建的hardware目录,继续创建存放LED相关驱动的目录



继续进入LED目录,准备好LED的源文件和头文件



然后进入User目录,准备好延时功能的源文件和头文件



四、工程配置
工程创建好后,接下来进入keil进行工程相关配置



进入Keil后,点击如下图图标,将准备好的新文件加载到该工作空间下



先添加准备的LED文件,其在Hardware的LED目录中,所以先创建目录并添加,如下图所示



目录添加好了



然后选中Hardware/LED目录,点击【Add Files】添加LED的文件



这里大家发现我没有添加LED.h文件,实际上是因为keil添加头文件不那么方便,况且我们之后是去VSCode中进行编写代码,而在VSCode里面是可以自动识别到的,所以不用担心看不见头文件



继续选中User,添加延时相关的头文件和源文件进来



添加完毕后如下图状态,然后千万记得要保存!!!



然后点击魔法棒,进去配置一些东西(如先前关于驱动方面的修改以及查看我要用到的文件路径是否被包含了)



进入后,点击【Debug】,改成正确的烧录驱动



接着进入Settings



点击进入【Flash Download】,勾上【Reset and Run】



点击进入【Pack】,取消勾选【Enable】,然后关于烧录的修改就改完了,一定要点击确定在退出!!!



然后点击下图页面的【C/C++】,在【Include Path】一栏最右侧,点击【...】添加我们新加载的Hardware目录的路径,防止keil仍找不到





点击新建后,如下图顺序进行操作即可



最后一定要注意确认保存所有修改!!!!!



至此,关于项目工程配置也完成了!

五、代码改进
工程创建配置好了,接下来就是核心了,对代码进行改进完善

首先,进入VSCode,打开咱项目工程,前面我已经讲过几次方法了,这里就不再赘述。

不记得的话可以去前面关于keil联合VSCode开发的文章中温习一下【加载Keil工程】部分。

Keil+VSCode优化开发体验-CSDN博客
https://blog.csdn.net/2301_79475 ... 1001.2014.3001.5501
在VSCode中打开工程后,我们可以看见这些原来流水灯实验的代码成分:

LED配置



LED开关灯的和延时的使用



延时功能实现体



根据前面大致思路的描述,我们现在就来进行实际的修改:

1、LED部分的完善
(1)LED.h 修改如下图所示



红框部分:首先,我们要添加三行防止头文件重复定义的语句;

黄框部分:其次,由于头文件相当于其源文件的声明,所以会在相应源文件中调用,又因为源文件中是头文件的实现部分,所以可能会用到stm32相关的宏定义,因此我们在头文件中直接包含一下stm32的宏定义头文件,免得在源文件中还要包含;

绿框部分:这里编写我们对可能使用的LED对应端口位的简洁宏定义。前面说思路也说了,我们创建的宏定义会放在其头文件中,如眼前所见;

橙框部分:此处写的是我们能够想到的可能要封装的函数声明。这部分我们先编写好我们要封装的函数名称声明,在我们一定会封装的LED初始化、LED开灯、LED关灯函数外,还扩展了几个LED控制的函数,都比较简单。

(2)LED.c 部分的完善



如上图所示,对于LED源文件的补充相对来说会麻烦一点,我们先包含其头文件,确保能在这个文件里面找到下面函数的声明;

然后我们直接把前面头文件中编写函数声明都复制过来,然后依次实现:

首先是LED初始化函数的实现:

前面我们说了,LED初始化就是完成端口控制LED前的配置项以及我们给予的默认LED状态,和在main.c中对LED的配置部分完全一致,故直接复制过来;同时,因为我们对控制LED的端口位进行了宏定义,因此这里我们还需要用我们的宏定义进行替换



其次,开关灯函数的实现:

开关灯函数非常简单,因为其逻辑就是给端口位一个高低电平状态即可。但是其代码较长,所以我们使用函数封装的初衷也是为了简化调用代码



接着就是我们拓展了一组函数,同时开关一组灯的函数实现:

这里,我们想实现一组LED的控制,也就是需要依次给几个端口高低电平,然后死循环保持状态即可,换句话说,这里就是打开或关闭多个灯。涉及到多个相同操作时我们可以想到借助循环控制,变化的只有控制LED的端口那么我们可以使用数组去存放这些端口,然后遍历使用即可实现。

因此,这里我们函数传参是LED数组以及数组长度,内部采用循环开关灯,其中LED就是传进来的数组元素。代码如下图所示



当然,我们这个时候可以直接使用前面定义的开关灯函数,不使用其较长的原始代码,也能显得更简洁易懂



这里还拓展了一个翻转LED状态的函数,如下图所示



翻转,就是将LED的状态变为与当前相反的状态。上图这个代码具体是如何实现的呢?

       首先,我们要变成相反状态是不是要获取到当前LED所处状态,然后进行判断去改成与之相反的状态。那么如何获取当前LED状态?

       之前咱是不是讲过GPIO端口输入数据寄存器啊(GPIOx_IDR),而且输入数据和输出数据寄存器中关于GPIO端口位还是一一对应的,可以很方便的获取对应位。通过这个寄存器我们就可以读取到当前控制LED端口位的状态了。

       但是此时又面临一个问题:我们获取到的是整个寄存器当前的数据,含16个端口,如何才能单独获取到这一个端口位的值呢?

       诶,有种运算叫位与&运算,可能可以,就是两组数分别进行与运算(相异为0,相同为1),如果我们让寄存器数据(如1010 1010 1011 0000)和某一端口位数据(如第八位0000 0001 0000 0000)进行与运算(作位与,则变成0000 0001 0000 0000),是不是就可以了(其他位会置0,端口位如果是0,则与完为0,非0则与完还是非0,就能获取到此时该端口的状态了)

       此时,输入数据寄存器的数据可以用GPIOA->IDR获取到,某端口位数据就是控制相应LED的端口,即我们宏定义过的那些端口位(那些名称前面讲过,表示的就是对应位的高电平,如第一个端口位就是0000 0000 0000 0001),然后作与运算就可以。

当然我们这个时候还会有一个问题,就是与运算得到的值我们如何用于判断LED状态?

       因为LED对应端口不在0位时,那个端口位高电平时对应十六进制的值不是1,我们没法用if(... == 1)来判断该LED是熄灭状态,这个时候大家应该能想到,那要使端口为处于低电平的话,那必然与运算后是0,那是不是就很好去判断此时LED时亮的呢?

因此,我们可以去判断此时获取到的LED对应端口位是否是低电平,如果是,则说明他此刻时点亮的,故我要翻转就执行关灯代码,反之亦然。

2、Delay部分的完善
(1)Delay.h的完善

前面LED部分已经详细说明,这里和LED除内容外步骤完全一致,就不在过多赘述。

添加防止头文件重复定义代码,以及stm323宏定义头文件



添加延时函数声明



(2)Delay.c的完善

添加延时函数的实现体(和main.c中对延时函数的实现完全一致,直接复制即可)



3、main.c的修改
将LED和Delay部分都补充完成后,接下来就是在main.c中调用这两个文件中的内容了

1. 包含LED.h和Delay.h头文件,防止执行main函数中相关函数和宏定义时找不到;

2. 将LED的相关配置和默认状态用封装好的函数LED_Init()直接调用;

3. 开灯关灯和延时同样都使用调用对应函数的方式实现。

具体代码如下图所示



此时,我们观察到while循环中流水灯逻辑,实际上就是对三个灯进行完全相同的亮-延时-灭操作,所以可以想到直接借助循环来处理,只需像前面开关一组灯那样创建一个LED数组依次存放好相应的端口即可。这样就能简化代码量了。

修改后的代码如下图所示



至此,代码就修改完了 ,最终测试效果没有问题。如果想获取源码可以私信我。

六、结语
          本次叙述了在编写代码时涉及到的一种非常重要的思想 ------ 模块化思想,同时也学到了如何去通过分文件处理来改善代码结构,使代码层次更加清晰,更易快速了解代码整体结构。
————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/2301_79475128/article/details/143879525

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

1955

主题

15685

帖子

12

粉丝