文章目录[隐藏]
前言:引入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的调用
#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 <cstdlib>
在新的标准下,最好使用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不是同时闪亮,主要原因就是任务的优先级设置的问题。
因为优先级高的任务可以打断优先级低的任务,这就导致了优先级低的LED灯闪亮的时间不是自己设置的,更改的方法很简单。
7. IIC驱动液晶屏
在编写程序之前需要将源程序添加到工程里面。
版权声明:本文为CSDN博主「米杰的声音」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Roger_717/article/details/121540219
暂无评论