一、STM32与面向对象编程
上一章中提到了,C++的核心之一就在于面向对象思想。相比C语言常用的面向过程编程,面向对象编程的优势在于继承、封装、多态的特性,利用这种思想更有助于我们的程序实现模块化、抽象化。C++是一种天然支持面向对象编程的语言,在C语言的基础上,C++不仅提供了class关键字和类与对象的概念,使开发者可以清晰方便的实现面向对象编程。C++还支持数据封装和数据隐藏,支持继承和函数重载,因此可以有效的提高程序代码的复用程度。
实际上,这种面向对象编程思想尤其适合单片机这类以操作外设为主的平台,因为单片机是为完成特定目的的功能而运行的,我们可以很轻易的将功能分解成为属性和方法。此外,每一个外设都是一个天然的“对象”,拥有的属性和方法都十分确定,因此十分便于抽象。因此,若以面向对象思想进行单片机编程,则大部分工作将会变得十分简洁直观,能让我们更专心注重于功能算法逻辑的实现。我们可以将代码写成这样;
电机类 左电机(&互补PWM1,&互补PWM2,&互补PWM3);
电机类 右电机(&互补PWM4,&互补PWM5,&互补PWM6);
void 小车右转()
{
左电机.设置转速(100);
右电机.设置转速(2);
}
...
比起按照过程流程来一步一步设置的代码而言,这样写是不是更加直观也更加方便调试了?
二、STM32与面向对象编程
下面就以一个GPIO管脚为例,来实际的展现一个操作STM32外设的类吧。
硬件:STM32F103RCT6 正点原子Mini板
配置环境:STM32CubeMX 6.3.0
固件库:STM32CubeF1 1.8.4
开发环境:MDK Keil 5.32
编译器:Arm Compiler 6
STM32CubeMX将时钟、GPIO复用等大部分配置工作已经为我们完成了,因此在这里我们可以将GPIO管脚抽象为一个简单的DEVGPIO对象,分别有置高、拉低、翻转、读取管脚电平四个方法,类图如下:
将类图转化为代码,则可得到类定义.h文件和类实现.cpp文件:
头文件
#ifndef DEVGPIO_H
#define DEVGPIO_H
#ifdef __cplusplus
#include <stdint.h>
class DEVGPIO
{
public:
void *_GPIOx;
uint16_t _GPIO_Pin;
bool pin_status;
public:
DEVGPIO(void *GPIOx, uint16_t GPIO_Pin);
DEVGPIO() {}
void set();
void reset();
void toggle();
bool readpin();
void operator=(bool bit);
};
#endif
#endif
类实现代码
#include "devgpio.h"
#include "main.h"
//构造函数
DEVGPIO::DEVGPIO(void *GPIOx, uint16_t GPIO_Pin)
{
_GPIOx = GPIOx;
_GPIO_Pin = GPIO_Pin;
_pin_state = false;
}
//置位
void DEVGPIO::set()
{
HAL_GPIO_WritePin((GPIO_TypeDef *)_GPIOx, _GPIO_Pin, GPIO_PIN_SET);
_pin_state = true;
}
// 复位
void DEVGPIO::reset()
{
HAL_GPIO_WritePin((GPIO_TypeDef *)_GPIOx, _GPIO_Pin, GPIO_PIN_RESET);
_pin_state = false;
}
//翻转
void DEVGPIO::toggle()
{
HAL_GPIO_TogglePin((GPIO_TypeDef *)_GPIOx, _GPIO_Pin);
_pin_state = !_pin_state;
}
//读取管脚当前状态
bool DEVGPIO::readpin()
{
_pin_state = (HAL_GPIO_ReadPin((GPIO_TypeDef *)_GPIOx, _GPIO_Pin) == GPIO_PIN_SET);
return _pin_state;
}
//设置管脚
void DEVGPIO::operator=(bool bit)
{
bit ? set() : reset();
}
利用上面定义的DEVGPIO类,我们在之前的start.cpp当中加上控制LED灯的代码:
#include "start.h"
#include "devgpio.h"
#include "main.h"
DEVGPIO led(LED1_PIN_GPIO_Port,LED1_PIN_Pin);
void startup()
{
while(1)
{
HAL_Delay(100);
led.toggle();
}
}
下载到板子上,我们可以看到小灯有序亮灭如下:
可能这时候开始想了,费劲封装一个类出来就为了点个灯,这不是没事找事吗?其实不然,使用C++类来操作外设的优势在于我们在STM32原有固件库的基础上,我们再封装了一层对象。这样做的好处在于有两点:
1、在写操作IO设备代码的时候可以在不改变其他代码的情况下脱离原有的平台进行编写,并在开发机上对时序和IO操作先行验证。等到充分验证时序逻辑和功能正确之后,再通过替换类的实现代码集成到工程中仿真验证,这样不仅节省了开发时间,也有助于查出时序逻辑的问题。
2、因为经过了一层或者多层的封装,所以在操作外设和实现逻辑算法的代码当中是不直接存在STM32的库函数的,从而封装过的代码可以更高效地实现在不同项目中甚至不同运行环境中复用。利用当我们的设备IO管脚发生变化的时候,只需在设备类实例化时绑定不同的IO管脚,即可实现迅速完成移植工作。当代码需要其他环境中运行时,只需要将底层封装类的.cpp中与STM32相关的代码替换即可。
接下来我们就来演示一下通过在宿主机上跑测试程序来验证我们的时序过程,同时也证明我们的程序只需针对devgpio.cpp稍加修改替换,便可将基于devgpio.cpp之上的设备驱动代码和应用代码直接用于其他平台的开发当中。
三、在PC机上验证STM32代码
在实际的项目开发当中,我们往往会遇到工期紧张的情况下硬件PCB迟迟贴装调试不完,从而留给程序开发的时间非常短的情况。在这时,若通过良好的封装定义,我们可以在拿到板子之前就能以纯软件的方式先行将逻辑时序等功能进行验证。本节我们就通过替换devgpio.cpp的实现来演示如何先行验证逻辑时序。
测试环境:Windows 11的WSL Ubuntu 21.10
编译器:g++ 11.2.0
在原来的目录下新建devgpio_test.cpp文件,将类DEVGPIO的实现改为
#include "devgpio.h"
#include <iostream>
using namespace std;
//构造函数
DEVGPIO::DEVGPIO(void *GPIOx, uint16_t GPIO_Pin)
{
_GPIOx = GPIOx;
_GPIO_Pin = GPIO_Pin;
_pin_state = false;
}
//置位
void DEVGPIO::set()
{
cout << string((char *)_GPIOx) << _GPIO_Pin << ": 1" << endl;
_pin_state = true;
}
//复位
void DEVGPIO::reset()
{
cout << string((char *)_GPIOx) << _GPIO_Pin << ": 0" << endl;
_pin_state = false;
}
//翻转
void DEVGPIO::toggle()
{
(_pin_state) ? cout << string((char *)_GPIOx) << _GPIO_Pin << ": 0" << endl : cout << string((char *)_GPIOx) << _GPIO_Pin << ": 1" << endl;
_pin_state = !_pin_state;
}
//读取管脚当前状态
bool DEVGPIO::readpin()
{
int x;
cout << "GPIO: readpin" << endl;
cin >> x;
if (x != 0)
return true;
else
return false;
}
//设置管脚
void DEVGPIO::operator=(bool bit)
{
bit ? set() : reset();
}
将start.cpp里与设备相关的代码做一些改造,并新建test.cpp作为存放main函数及调用startup()的入口
#include "start.h"
int main()
{
startup();
return 0;
}
使用终端打开文件夹,并进入WSL命令行界面,输入g++ -o test devgpio_test.cpp start.cpp test.cpp
编译测试程序
执行测试程序:
逻辑与在单片机上运行的LED闪烁一致。
四、结论
在STM32 HAL库中,操作外设的各种方法是通过句柄(handler)完成的,从初始化配置、开启关闭中断到各种实际的操作均为在同样的函数中调用对应的句柄完成,抽象程度相对固件库而言更高,因此也更方便在不同的STM32芯片之间进行移植。然而,代码过于依赖于固件库的问题就在于想要移植到其他的芯片上会比较困难。由于疫情和供需关系的影响,最近这大半年STM32价格均出现了巨大的涨幅,使得国内设备商纷纷转向国产MCU。由于寄存器和固件库的不同,给软件移植带来了比较大的麻烦。对于软件而言,若能在HAL库之上再以类和对象的形式封装一层中间代码,则当设备换芯片之后,只需将中间代码中和STM32相关的函数进行替换就可以移植了,这样可以提高了代码的延续性和可移植性,也减少了单片机开发中反复造轮子的尴尬问题。
本章就STM32开发中封装C++类的优点进行了演示和分析,下一章将就C++的访问控制权限在STM32开发当中的应用进行介绍,在单片机开发当中如何利用访问控制提高数据收发和参数设置的可靠性。