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

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

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

1 基础准备:一块电路板

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

【STM32】在Keil上使用C++编程_第1张图片

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

【STM32】在Keil上使用C++编程_第2张图片

2.1 CobeMX项目设置

ADC采用多通道和DMA传输:

扫描转换模式开启

【STM32】在Keil上使用C++编程_第3张图片

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

2.2 定时器TIM2的PWM输出设置

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

【STM32】在Keil上使用C++编程_第4张图片

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

2.3 按键外部中断设置

检测低电平按下

【STM32】在Keil上使用C++编程_第5张图片

 2.4 添加FREERTOS

【STM32】在Keil上使用C++编程_第6张图片【STM32】在Keil上使用C++编程_第7张图片

3 Keil环境配置

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

【STM32】在Keil上使用C++编程_第8张图片

【STM32】在Keil上使用C++编程_第9张图片

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类的这个文件。

并将其导入工程中。

【STM32】在Keil上使用C++编程_第10张图片

【STM32】在Keil上使用C++编程_第11张图片

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的调用

#ifndef __OSINTERFACE_H__
#define __OSINTERFACE_H__

#ifdef __cplusplus
extern "C"{
#endif



#ifdef __cplusplus
};
#endif

#endif/* __OSINTERFACE_H__ */

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

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

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

#ifndef __OSINTERFACE_H__
#define __OSINTERFACE_H__

#ifdef __cplusplus
extern "C"{
#endif
    void StartDefaultTask(void const * argument);
    void StartTask02(void const * argument);
    void StartTask03(void const * argument);
    void StartTask04(void const * argument);
    void StartTask05(void const * argument);
    void StartTask06(void const * argument);

#ifdef __cplusplus
};
#endif

#endif  /* __OSINTERFACE_H__ */

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

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

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

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

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

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

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

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

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

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

class TurnTable
{
private:
    static TurnTable *_instance; //私有静态对象属性

    TurnTable() {};禁止外部实例化对象,应当私有化这个类的构造函数

public:公有静态方法实例化对象
    static TurnTable *Instance();

}

TurnTable *TurnTable::_instance = nullptr;

TurnTable *TurnTable::Instance()
{
    if (nullptr == _instance)
    {
        _instance = new TurnTable;//如果时空创建这个对象
    }
    return _instance;//如果不为空,返回这个对象
}

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

5.1 C++中NULL和nullptr的区别

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

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

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

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

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

5.2 懒汉式单例模式

//懒汉式:
pbulic class SinglentonDemo{

     private static SingletonDemo instance;

     private SingletonDemo(){ } //构造函数

     public static SingletonDemo getinstance(){
        if(instance == nullprt){
        instance = new SingletonDemo();
        return instance;
        }
    }
}

//线程安全加锁,增加synchronized关键字,效率低
pbulic class SinglentonDemo{

     private static SingletonDemo instance;

     private SingletonDemo(){ } //构造函数

     public static synchronized SingletonDemo getinstance(){
        if(instance == nullprt){
        instance = new SingletonDemo();
        return instance;
        }
    }
}

//饿汉式:直接初始化
pbulic class SinglentonDemo{

     private static SingletonDemo instance = new SingletonDemo;

     private SingletonDemo(){ } //构造函数

     public static synchronized SingletonDemo getinstance(){
        if(instance == nullprt){
        instance = new SingletonDemo();
        return instance;
        }
    }
}

6 创建IO操作函数;

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

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

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

MCUGPIO LED1(LED1_GPIO_Port,LED1_Pin);
MCUGPIO LED2(LED2_GPIO_Port,LED2_Pin);
MCUGPIO LED3(LED3_GPIO_Port,LED3_Pin);
MCUGPIO LED4(LED4_GPIO_Port,LED4_Pin);

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

extern MCUGPIO LED1;
extern MCUGPIO LED2;
extern MCUGPIO LED3;
extern MCUGPIO LED4;

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

/**
 * @description: 延时函数
 * @param {uint32_t} ms
 * @return {*}
 */
void delay_ms(uint32_t nms)
{
#if (INCLUDE_xTaskGetSchedulerState  == 1 )	
	if(xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)//如果OS已经在跑了,并且不是在中断里面(中断里面不能任务调度)	    
	{		 
		osDelay(nms);	//OS延时   
	}
	else delay_us((uint32_t)(nms*1000));				//普通方式延时
#else
	HAL_Delay(nms);
#endif
} 

/**
 * @description: 微秒级延时
 * @param {__IO uint32_t} delay
 * @return {*}
 */
void delay_us(uint32_t us)
{
    __IO uint32_t currentTicks = SysTick->VAL;
    /* Number of ticks per millisecond */
    const uint32_t tickPerMs = SysTick->LOAD + 1;
    /* Number of ticks to count */
    const uint32_t nbTicks = ((us - ((us > 0) ? 1 : 0)) * tickPerMs) / 1000;
    /* Number of elapsed ticks */
    uint32_t elapsedTicks = 0;
    __IO uint32_t oldTicks = currentTicks;
    do
    {
        currentTicks = SysTick->VAL;
        elapsedTicks += (oldTicks < currentTicks) ? tickPerMs + oldTicks - currentTicks : oldTicks - currentTicks;
        oldTicks = currentTicks;
    } while (nbTicks > elapsedTicks);
}

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

void StartTask04(void const * argument)
{
  for(;;)
  {
    LED4.setpin();
    delay_ms(100); 
    LED4.resetpin();
    delay_ms(100); 
    osDelay(1);
  }
}

效果如下:

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

【STM32】在Keil上使用C++编程_第12张图片

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

【STM32】在Keil上使用C++编程_第13张图片

 

7. IIC驱动液晶屏

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

你可能感兴趣的:(STM32CobeMX,硬件工程)