当前位置:   article > 正文

【STM32】在Keil上使用C++编程_keil c++

keil c++

前言:引入C++面向对象的编程方式会让写单片机程序看起来更加板正。祝我顺利!

人如果要进步,就要用于去接受新鲜事物,新鲜方法,新鲜思想。这种新鲜对你来说是新鲜的,可是在客观的世界,它却比你想象的更加成熟,这就是你的无知,善于向优秀的人去学习,善于向不同领域的人学习,倾听他们的思想,可能当时你觉得没用,那是因为你们还不在一个LEVEL。回到单片机,仅仅以实现功能为目的,那可能单片机你能走的路也就这么长了,甚至一年以后,五年以后,你还是这个水平,写的代码拼凑,没有层次,没有美感。这不是我想要的,所以从0到1,每天努力一点,不要永远做底层的那帮人。——米杰的声音

1 基础准备:一块电路板

程序主要是在这个板子上跑,硬件电路板如下(原理图就不贴了,主要学习思想):

2 基础准备:CobeMX基础功能配置 

2.1 CobeMX项目设置

ADC采用多通道和DMA传输:

扫描转换模式开启

再把ADC中断加上,再加上DMA。

2.2 定时器TIM2的PWM输出设置

产生了1ms周期可调的占空比信号

加入中断,在中断里面调整占空比:

2.3 按键外部中断设置

检测低电平按下

 2.4 添加FREERTOS

3 Keil环境配置

我们的文件夹按照这个格式配置看起来相对工整一些,之后只需要将自己配置的文放在对应的文件夹里面就可以了。

Port用于存放连接硬件和应用层的底层驱动。 今后上述文件夹中的Scr和Inc再去调用GPIO的时候,不必直接去调用HAL库文件,而是直接调用Port中的GPIO类,因此给新建的GPIO类起一个好听的名字就显得格外重要。

4 GPIO类创建

开发环境IDE基于Keil5

类的基本思想是数据的抽象和封装,数据抽象是一个依赖于接口实现分离的编程。

封装实现了类的接口和实现的分离,要想实现数据抽象和封装,首先需要定义一个抽象的数据类型。

比如GPIO这个功能,我们想要调用一个端口比如GPIOA的PN1,GPIOA是端口的基地址,也就是:

#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOA_BASE            (D3_AHB1PERIPH_BASE + 0x0000UL)

引脚号是基于端口基地址的偏移量:

#define GPIO_PIN_1                 ((uint16_t)0x0002)  /* Pin 1 selected    */

因此我们操作具体某个引脚的时候,这两个输入实参就会作为数据的输入:

那么输入知道了,就需要对这个功能进行封装了。

我们常用的功能有输出、输入、反转;

我们建立便携GPIO类的这个文件。

并将其导入工程中。

4.1 创建MCUGPIO类——构造函数

这个类主要是实现GPIO的一些操作,比如:

HAL_GPIO_WritePin的GPIO_PIN_SET和GPIO_PIN_RESET

输入量是GPIO_TypeDef *GPIOx和uint16_t GPIO_Pin

那么首先要进行构造函数来初始化这两个输入值

首先类要有成员定义:

    void *_GPIOx;
    uint16_t _GPIO_Pin;

构造函数没有返回值且函数名和类名相同,同时构造函数也可以重载;

    MCUGPIO(void *GPIOx, uint16_t GPIO_Pin);
    MCUGPIO();

那么如何调用构造函数?

构造函数和普通函数不同,一般不显式的去调用;但在创建一个对象时构造函数被自动调用;

也就是说我定义了一个对象:

在生成这个对象的过程中:

MCUGPIO::MCUGPIO(void *GPIOx, uint16_t GPIO_Pin)
{
    _GPIOx = GPIOx;
    _GPIO_Pin = GPIO_Pin;
}

在.cpp中定义的构造函数的具体实现会被编译器自动调用。

在C中引用C++语言中的函数和变量时,C++的函数或变量要声明在extern "C"{}里,

在main.h中定义了引脚的宏定义

/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef __MAIN_H
#define __MAIN_H

/* Includes ------------------------------------------------------------------*/
#include "stm32f1xx_hal.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#ifdef __cplusplus
extern "C" {
#endif

#define SCL_Pin GPIO_PIN_6
#define SCL_GPIO_Port GPIOB
#define SDA_Pin GPIO_PIN_7
#define SDA_GPIO_Port GPIOB

/* USER CODE BEGIN Private defines */
#ifdef __cplusplus
}
#endif

/* USER CODE END Private defines */

#endif /* __MAIN_H */

4.1.1 析构函数

构造函数用来完成对象的初始化

析构函数用来清理

注意:析构函数和构造函数没有来行说明符,程序不能直接调用,创建和撤销由系统调用自动完成。

4.2 FREE RTOS任务函数创建——this指针

GPIO类创建完成之后

this指针:

1 this指针指向的是类的对象;答:this指针不占用类的大小,是编译器帮助传递的。

2 this指针是一个地址,地址里面存放的是什么?答:this实际存放对象的首地址。

3.类的成员函数有静态static和非静态,为什么this指针不能操作静态成员函数?

答:因为静态成员函数是先于对象存在的,是对于所有对象共享的。如果没有对象this是不能实例化对象首地址的。因此也就解释了为什么在static静态成员函数中不能直接调用this指针。

4 为什么要设计this指针?c++对象模型。

4.2.1 创建osInterface文件放在视图层

用于对下面的模型信息进行操作,任务的调度。

头文件的框架如下:

便于C的调用

  1. #ifndef __OSINTERFACE_H__
  2. #define __OSINTERFACE_H__
  3. #ifdef __cplusplus
  4. extern "C"{
  5. #endif
  6. #ifdef __cplusplus
  7. };
  8. #endif
  9. #endif/* __OSINTERFACE_H__ */

我们在OS文件中主要将CUBEMX生成的任务虚函数拿过来。

这些虚函数在freertos.c/.h文件里面

我们将它提取到视图文件里;

  1. #ifndef __OSINTERFACE_H__
  2. #define __OSINTERFACE_H__
  3. #ifdef __cplusplus
  4. extern "C"{
  5. #endif
  6. void StartDefaultTask(void const * argument);
  7. void StartTask02(void const * argument);
  8. void StartTask03(void const * argument);
  9. void StartTask04(void const * argument);
  10. void StartTask05(void const * argument);
  11. void StartTask06(void const * argument);
  12. #ifdef __cplusplus
  13. };
  14. #endif
  15. #endif /* __OSINTERFACE_H__ */

5 创建IO对象——单例模式(23种设计模式之一)

单例模式:确保一个类有且只有一个实例,且自行实例化并向整个系提供这个实例。

注定了它的构造方法不能是public,而是private。

且这个实例是当前类的成员变量,即静态变量,即用static修饰。

故单例模式要求构造方法是private,并且拥有当前类的静态成员变量。还需要提供一个静态方法,向外界提供当前类的实例。

单例模式的作用是确保一个类只有一个实例存在;

特点是:类构造器私有;持有自己类型的属性;对外界提供获取实例的静态方法;

注意事项:构造函数是私有的;析构函数是共有的;提供获取实例的函数;自己类型的属性需要在外部初始化;最后的垃圾回收。

实现顺序是:定义类——定义构造函数——添加自身属性——添加获取实例函数——使用

首先判断这个对象是否为空,不为空就返回这个对象,为空就创建一个新的对象;为这个对象开辟内存。

  1. class TurnTable
  2. {
  3. private:
  4. static TurnTable *_instance; //私有静态对象属性
  5. TurnTable() {};禁止外部实例化对象,应当私有化这个类的构造函数
  6. public:公有静态方法实例化对象
  7. static TurnTable *Instance();
  8. }
  9. TurnTable *TurnTable::_instance = nullptr;
  10. TurnTable *TurnTable::Instance()
  11. {
  12. if (nullptr == _instance)
  13. {
  14. _instance = new TurnTable;//如果时空创建这个对象
  15. }
  16. return _instance;//如果不为空,返回这个对象
  17. }

这就引出一个问题C++的空如何表示?

5.1 C++中NULL和nullptr的区别

空指针不指向任何对象,在试图使用一个指针之前可以首先检查它是否为空。

得到一个空指针最直接的办法就是直接使用nullprt初始化指针,这种类型的字面值可以被转化成任意其他的指针类型,也可以通过将指针初始化为0来生成空指针。

在C++中使用NULL来初始化指针需要引入头函数#include <cstdlib>

在新的标准下,最好使用nullpr,避免使用NULL。

因此应当初始化所有的指针,且在定义了对象之后再定义指向它的指针,如果不清楚指针的具体位置,可以使用0或者nullprt进行初始化

5.2 懒汉式单例模式

  1. //懒汉式:
  2. pbulic class SinglentonDemo{
  3. private static SingletonDemo instance;
  4. private SingletonDemo(){ } //构造函数
  5. public static SingletonDemo getinstance(){
  6. if(instance == nullprt){
  7. instance = new SingletonDemo();
  8. return instance;
  9. }
  10. }
  11. }
  12. //线程安全加锁,增加synchronized关键字,效率低
  13. pbulic class SinglentonDemo{
  14. private static SingletonDemo instance;
  15. private SingletonDemo(){ } //构造函数
  16. public static synchronized SingletonDemo getinstance(){
  17. if(instance == nullprt){
  18. instance = new SingletonDemo();
  19. return instance;
  20. }
  21. }
  22. }
  23. //饿汉式:直接初始化
  24. pbulic class SinglentonDemo{
  25. private static SingletonDemo instance = new SingletonDemo;
  26. private SingletonDemo(){ } //构造函数
  27. public static synchronized SingletonDemo getinstance(){
  28. if(instance == nullprt){
  29. instance = new SingletonDemo();
  30. return instance;
  31. }
  32. }
  33. }

6 创建IO操作函数;

对于这个项目,IO口主要实现的功能主要包括LED小灯的点亮工作。

我们建立一个deviceconfigure.cpp函数用于实例MCUGPIO类。

在这里我们创建了实例化构造函数:

  1. MCUGPIO LED1(LED1_GPIO_Port,LED1_Pin);
  2. MCUGPIO LED2(LED2_GPIO_Port,LED2_Pin);
  3. MCUGPIO LED3(LED3_GPIO_Port,LED3_Pin);
  4. MCUGPIO LED4(LED4_GPIO_Port,LED4_Pin);

并在头文件中声明了其调用性。

  1. extern MCUGPIO LED1;
  2. extern MCUGPIO LED2;
  3. extern MCUGPIO LED3;
  4. extern MCUGPIO LED4;

同时为了延时的实现,又声明了延时函数用于系统调用。

  1. /**
  2. * @description: 延时函数
  3. * @param {uint32_t} ms
  4. * @return {*}
  5. */
  6. void delay_ms(uint32_t nms)
  7. {
  8. #if (INCLUDE_xTaskGetSchedulerState == 1 )
  9. if(xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)//如果OS已经在跑了,并且不是在中断里面(中断里面不能任务调度)
  10. {
  11. osDelay(nms); //OS延时
  12. }
  13. else delay_us((uint32_t)(nms*1000)); //普通方式延时
  14. #else
  15. HAL_Delay(nms);
  16. #endif
  17. }
  18. /**
  19. * @description: 微秒级延时
  20. * @param {__IO uint32_t} delay
  21. * @return {*}
  22. */
  23. void delay_us(uint32_t us)
  24. {
  25. __IO uint32_t currentTicks = SysTick->VAL;
  26. /* Number of ticks per millisecond */
  27. const uint32_t tickPerMs = SysTick->LOAD + 1;
  28. /* Number of ticks to count */
  29. const uint32_t nbTicks = ((us - ((us > 0) ? 1 : 0)) * tickPerMs) / 1000;
  30. /* Number of elapsed ticks */
  31. uint32_t elapsedTicks = 0;
  32. __IO uint32_t oldTicks = currentTicks;
  33. do
  34. {
  35. currentTicks = SysTick->VAL;
  36. elapsedTicks += (oldTicks < currentTicks) ? tickPerMs + oldTicks - currentTicks : oldTicks - currentTicks;
  37. oldTicks = currentTicks;
  38. } while (nbTicks > elapsedTicks);
  39. }

然后再在视图控制器中去进行调用,也就是让其再任务中执行。例如:

  1. void StartTask04(void const * argument)
  2. {
  3. for(;;)
  4. {
  5. LED4.setpin();
  6. delay_ms(100);
  7. LED4.resetpin();
  8. delay_ms(100);
  9. osDelay(1);
  10. }
  11. }

效果如下:

我们可以看到LED不是同时闪亮,主要原因就是任务的优先级设置的问题。

因为优先级高的任务可以打断优先级低的任务,这就导致了优先级低的LED灯闪亮的时间不是自己设置的,更改的方法很简单。

 

7. IIC驱动液晶屏

在编写程序之前需要将源程序添加到工程里面。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小舞很执着/article/detail/1004324
推荐阅读
相关标签
  

闽ICP备14008679号