基于STM32调用固件库实现点灯

2024-03-08  

相信学过单片机的同学,对于调库这个操作都不陌生,大多数人都是从调别人的库阶段过来的。


今天看到一个评论说,如果只会调库,到了公司后会发现自己啥都不是。其实这话说的一点也不假,如果只会调库的话,你的单片机水平还停留在C语言阶段,并不能称为真正的单片机开发。

36a1dc56-dfb7-11ed-bfe3-dac502259ad0.png


但我们要有这么一个概念:调库是自己编写的开始,如果上来就给你讲寄存器这些,我相信很多初学者都接收不了、理解不了这写寄存器到底在干啥。可是,如果从调别人库开始学习单片机,我们就会对单片机有个初始概念,对于后面的学习非常有帮助。


所以,今天我们就来看一下如何从调库工程师成为真正的开发工程师。


1. 什么是调库?

如果你通过机构的培训视频,比如野火的STM32单片机开发视频,相信你对于调库并不陌生,调库其实就是通过调用别人封装好的库函数,来实现自己的某些功能,不同的机构封装出来的库函数也有所不同,但基本操作都大同小异。

下面,我们就以STM32调用固件库实现点灯为例,给大家进行讲解。

首先来看一个我们非常熟悉的结构体:

void LED_GPIO_Config(void)//初始化相关的GPIO 第2个灯
{
 GPIO_InitTypeDef GPIO_InitStruct;
 /*第一步:打开外设的时钟(RCC寄存器控制)*/
 RCC_APB2PeriphClockCmd(LED1_GPIO_CLK|LED2_GPIO_CLK,ENABLE);

/*第二步:配置外设初始化结构体*/
GPIO_InitStruct.GPIO_Pin = LED1_GPIO_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出

GPIO_InitStruct.GPIO_Speed = GPIO_Speed_10MHz;


/*第三步:调用外设初始化函数,把配置好的结构体成员写到寄存器里面*/
GPIO_Init(LED1_GPIO_PORT,&GPIO_InitStruct);


GPIO_InitStruct.GPIO_Pin = LED2_GPIO_PIN;
GPIO_Init(LED2_GPIO_PORT,&GPIO_InitStruct);
}


相信对于学习过STM32单片机的同学,对于这个函数都不陌生。这个函数其实就是实现了对于一个GPIO的初始化,相信初学者并没有思考过我们为什么要这么初始化呢?这里面的一些函数都有什么作用呢?他们是在哪个地方被封装的呢?我们可不可以不按照这个函数的结构来写呢?

带着这些疑问,我们继续往更深的层次去探索一下这些东西都是什么意思:

这里面用到了很多的宏定义,我们可以使用右键-->go to来向前查询该宏定义在哪个地方进行定义的,例如我们对时钟的宏定义LED1_GPIO_CLK 具体如下:

#define LED1_GPIO_CLK         RCC_APB2Periph_GPIOC//时钟
#define LED1_GPIO_PORT        GPIOC               //端口
#define LED1_GPIO_PIN         GPIO_Pin_2//pin 引脚

#define LED2_GPIO_CLK RCC_APB2Periph_GPIOC//时钟
#define LED2_GPIO_PORT GPIOC //端口
#define LED2_GPIO_PIN GPIO_Pin_3//pin


我们可以看到一些宏定义,例如LED1_GPIO_CLK被宏定义为RCC_APB2Periph_GPIOC,这里的RCC_APB2Periph_GPIOC就是官方固件库中定义的时钟,如果你想继续研究RCC_APB2Periph_GPIOC代表什么意思,我们可以继续右键-->go to

36bc596e-dfb7-11ed-bfe3-dac502259ad0.png


我们发现依然是宏定义,这里将RCC_APB2Periph_GPIOC宏定义成了((uint32_t)0x00000010),如果你想继续了解((uint32_t)0x00000010)代表什么的话那就需要查看STM32的芯片手册了,我们这里做一下简单的讲解。

关于GPIO的需要用到的寄存器如下:

36ea68a4-dfb7-11ed-bfe3-dac502259ad0.png


我们将0x10转换为2进制为:1 0000我们可以看到第四位为1,其他位为0,查看芯片手册可以发现第四位解释如下:

36fc65b8-dfb7-11ed-bfe3-dac502259ad0.png


发现这句话其实就是在使能I/O端时钟C,和我们的使用是相同的。到这里我们就知道了从封装的库到底层寄存器中间经过了什么。当然,这只是一个简单的例子,实际会比此复杂很多。

2. 如何不调库点亮一个LED?

通过固件库我们可以看到如果想要控制一个GPIO大概需要以下几步操作:

  1. 打开GOIO端口的时钟

  2. .配置IO口为输出(控制CRL寄存器)

  3. 配置ODR寄存器

知道了需要进行的操作,下一步我们就可以开始通过寄存器操作来控制一个LED了。具体代码,这里直接贴出来了,大家可以自己进行分析。


#define rRCCAHB1CLKEN *((volatile unsigned long *)0x40023830)

#define rGPIOF_MODER *((volatile unsigned long *)0x40021400) #define rGPIOF_OTYPER *((volatile unsigned long *)0x40021404) #define rGPIOF_OSPEEDR *((volatile unsigned long *)0x40021408) #define rGPIOF_IDR *((volatile unsigned long *)0x40021410) #define rGPIOF_ODR *((volatile unsigned long *)0x40021414)

#define rGPIOE_MODER *((volatile unsigned long *)0x40021000) #define rGPIOE_OTYPER *((volatile unsigned long *)0x40021004) #define rGPIOE_OSPEEDR *((volatile unsigned long *)0x40021008) #define rGPIOE_IDR *((volatile unsigned long *)0x40021010) #define rGPIOE_ODR *((volatile unsigned long *)0x40021014)

#define rGPIOA_MODER *((volatile unsigned long *)0x40020000) #define rGPIOA_OTYPER *((volatile unsigned long *)0x40020004) #define rGPIOA_OSPEEDR *((volatile unsigned long *)0x40020008) #define rGPIOA_IDR *((volatile unsigned long *)0x40020010) #define rGPIOA_ODR *((volatile unsigned long *)0x40020014) void key_init() {

rRCCAHB1CLKEN |= 1 | (1 << 1);

rGPIOA_MODER&=~(1|(1<<1));

rGPIOF_OSPEEDR &= ~(1 | (1 << 1) );

rGPIOE_MODER&= ~(0x3f<<4);

rGPIOE_MODER &= ~(0x3f<<4); } void led_init() {

rRCCAHB1CLKEN |= (1 << 5) | (1 << 4);

rGPIOF_MODER &= ~((0x3 << 18) | (0x3 << 20)); rGPIOF_MODER |= (1 << 18) | (1 << 20);

rGPIOF_OTYPER &= ~( (1 << 9) | (1 << 10));

rGPIOF_OSPEEDR &= ~((0x3 << 18) | (0x3 << 20) );

rGPIOF_ODR |= (1 << 9 | 1 << 10) ;

rGPIOE_MODER &= ~((0X3 << 26) | (0X3 << 28) ); rGPIOE_MODER |= (1 << 26) | (1 << 28);

rGPIOE_OTYPER &= ~( (1 << 13) | (1 << 14));

rGPIOE_OSPEEDR &= ~((0x3 << 26) | (0x3 << 28) );

rGPIOE_ODR |= (1 << 13 | 1 << 14) ;

}

void delay(int i) { int v = i; while(v–); }

void led_on(int i) { if (i == 0) { rGPIOF_ODR &= ~(1 << 9); rGPIOF_ODR |= 1 << 10;

rGPIOE_ODR |= (1 << 13) | (1 << 14); } else if (i == 1) { rGPIOF_ODR |= (1 << 9); rGPIOF_ODR &= ~(1 << 10);

rGPIOE_ODR |= (1 << 13) | (1 << 14);

} else if (i == 2) { rGPIOF_ODR |= (1 << 9) | (1 << 10);

rGPIOE_ODR &= ~(1 << 13); rGPIOE_ODR |= 1 << 14; } else if (i == 3) { rGPIOF_ODR |= (1 << 9) | (1 << 10);

rGPIOE_ODR &= ~(1 << 14); rGPIOE_ODR |= 1 << 13; } }

int main() { int i = 0; led_init(); key_init(); while(1) {

if(!(rGPIOA_IDR&1)) { delay(50);//消抖 if(!(rGPIOA_IDR&1)) { led_on(0); } } else { rGPIOF_ODR |= 1 << 9;//µÆÃð } if(!(rGPIOE_IDR&(1<<2))) { delay(50); if(!(rGPIOE_IDR&(1<<2))) { led_on(1); } } else { rGPIOF_ODR |= 1 << 10; } if(!(rGPIOE_IDR&(1<<3))) { delay(50); if(!(rGPIOE_IDR&(1<<3))) { led_on(2); } } else { rGPIOE_ODR |= 1 << 13; } if(!(rGPIOE_IDR&(1<<4))) { delay(50); if(!(rGPIOE_IDR&(1<<4))) { led_on(3); } } else { rGPIOE_ODR |= 1 << 14; }

} }

上面的代码实现的功能是通过循环扫描判断按键是否被按下,如果按键被按下则对LED引脚输出低电平从而点亮LED灯,这里用了四个按键和四个LED,方便大家理解之间的不同,引脚的定义如下:

LED的引脚定义为:
LED0 ->PF9
LED1 -> PF10
LED2-> PE13
LED3 -> PE14

按键引脚定义为:
KEY0–> PA0
KEY1–> PE2
KEY2–> PE3
KEY3–> PE4

具体每个寄存器代表什么意思,大家可以查看STM32的官方手册,里面有详细的介绍。没有手册的话,可以看下面这篇文章,里面有常用的寄存器:
https://www.cnblogs.com/jzcn/p/15775328.html

3. 调库与不调库的区别

说到这两者的区别,也是我写这篇文章的主要意图,相信你打开这篇文章绝对不是来看不调库是如何开发的,而是来看调库开发和不调库开发具体有哪些区别,为什么有现成的库不用,非要自己去查寄存器,自己进行开发。

从应用角度讲,寄存器相对来说是属于更底层的,类似于驱动层,而固件库则类似通过将寄存器封装之后的应用层。相比之下,固件库更像是包装好给用户的产品一样,只需要我们使用就行了,让封装自己和寄存器打交道,而使用寄存器在使用时必须要清楚自己要操作那个一个寄存器,就很复杂,需要了解清楚寄存器的底层配置。

如果你学习过Linux的话,想必你对分层的思想是有所了解的。虽然在单片机中分层思想的应用和Linux中的分层不太一样,但也都是大同小异的。

STM32标准外设库之前的版本,也称固件函数库或简称固件库,是⼀个固件函数包,它由程序、数据结构和宏组成,包括了微控制器所有外设的性能特征。

该函数库还包括每⼀个外设的驱动描述和应用实例,为开发者访问底层硬件提供了⼀个中间API,通过使用固件函数库,无需深入掌握底层硬件细节,开发者就可以轻松应⽤每⼀个外设。

因此,使⽤固态函数库可以大大减少用户的程序编写时间,进而降低开发成本。每个外设驱动都由⼀组函数组成,这组函数覆盖了该外设所有功能。每个器件的开发都由⼀个通⽤API驱动,API对该驱动程序的结构,函数和参数名称都进⾏了标准化。

这样的操作既有好处又有坏处,对于毫无基础的人来说它可以使我们的控制更加简单,上手更容易,但是他也会造成我们接触不到单片机的底层操作,可能你使用单片机干过很多的事,做过很多的项目,但是对于单片机的运行逻辑依然不清楚。

从专业角度来讲,由于寄存器更底层,更需要用户了解基本构成以及底层配置,所以说操作寄存器相对于固件库显得更加专业,相比之下,直接操作固件库不需要了解那么多甚至不了解就可以直接开发,并不需要太多专业知识。

通过上面的分析,我们可以总结出他们的优缺点:

  • 固件库优点:可以直接应用,操作更方便,开发迅速,适合新手入门。

  • 固件库缺点:因为操作固件库,本质上也会对寄存器的操作,因为要通过封装这一中间商,所以执行速度要比直接操作寄存器更慢,但没有寄存器移植那么方便。

所以我们可以从固件库入门,之后再慢慢深入了解寄存器,了解相关知识,在我看来,了解更多底层的东西是有利无害的,更利于提升自己,可以懒,但是不能不会。

4. 为什么要操作寄存器?

回归我们的中心,讲了这么多我们到底该如何学习单片机呢?详细这个问题在互联网上都已经被谈烂了。对于初学者应该如何入门应该学习哪些东西今天这篇文章我就不再讨论了,今天要讨论的内容是如果你已经入门了,也已经通过操作固件库做了很多的东西,下一步你应该学习哪些东西。

如果你已经使用单片机做了很多的实验,比如什么ADC采集、PWM波输出这些操作你都用过了,并且感觉单片机你已经玩的炉火纯青了,那么下面的东西对你应该很有用。

还有一点需要强调一下,如果未来你并不打算做单片机相关的工作的话,那么下面的东西你可以量力而行,可以作为了解的内容,并不用深入的了解。

大家学习51单片机的时候,是不是经常进行一些寄存器操作,为什么我们在32中就很少见到这些直接对寄存器进行操作呢?那是因为32的寄存器相比于51单片机要复杂很多,比如一个GPIO的操作可能就和很多的寄存器有关,我们很难通过一句话就可以控制一个GPIO,当然不这么干不代表不能这么干。

如果你接触到单片机的高级开发(当然没有这么一说,你可以理解成用单片机做一些产品)那么你的开发就会遇到瓶颈,从而限制你的开发,这也是很多单片机开发要求你一定要会寄存器操作。

对于经过系统培训的开发者,单片机(MCU)或SoC的驱动开发,不管是使用各种库还是直接上寄存器,都不成问题。

HAL库函数或者固件库都是ST开发的,也是人写出来的代码,既然是代码,那就有存在BUG的可能,而且像这些经过ST调试过的代码,更可能隐藏深层问题,这些都需要通过修改寄存器配置来调试定位。

所以,这就可能在你的代码里埋下了更深的炸弹,而且这些炸弹是埋藏的非常深的(一般的小bug是不会有的,毕竟那么多人使用)而这些bug一旦复现,你就会不知所措,完全不知道从何查起。

而且一般公司的MCU都不是你平时学的这些单片机,而是一些工业级的MCU,所以你可以想一想如果你一直使用的都是STM32的固件库进行的开发,从来没接触过寄存器操作,或者根本都不知道怎么看芯片手册,怎么操作寄存器,那么你怎么保证 你们公司使用的MCU你就一定会操作呢?

所以,对于STM32或者51这类单片机的定位,你就把它当成学习使用的,你要通过这类简单的、有丰富资料的单片机去入门、去学习。当你的学习内容达到一定程度后,就一定会接触到寄存器这些操作。

说实话,寄存器操作也只是工作的基础,最重要的是举一反三,通过一个单片机学习到所有MCU操作的本质,这样才能更好的在工作中使用,而不受单片机(MCU)型号的限制。

5. 结语

对于单片机的学习我们可以使用单片机的固件库入门,初步了解单片机操作的步骤,可以先不接触寄存器,等到固件库使用的非常熟悉之后可以转战寄存器了。

对于寄存器操作绝不是点个小灯就完了,你需要做的是知道如何查看芯片手册,知道固件库里的每个宏定义或者函数这么写的依据是什么?如果让你来写一个固件库你会怎么写?

当你的水平能够达到对STM32的寄存器操作非常6的话,你可以尝试几款工业级的MCU,例如工业非常常用的TC397,这个MCU在车载行业用的非常多,可以尝试一下,不过治疗可能不太好找,如果遇到问题的话就需要自己琢磨了,这也是一种进步。


文章来源于:电子工程世界    原文链接
本站所有转载文章系出于传递更多信息之目的,且明确注明来源,不希望被转载的媒体或个人可与我们联系,我们将立即进行删除处理。