一、STM32及其他单片机开发现状
在目前绝大部分的单片机开发当中,C语言占据着主流的地位,但由于C语言本身是一种面向过程的语言,因此在当前利用面向对象思想构建可复用代码为主流的今天显得比较麻烦,很多人写单片机程序时都会遇到一个问题,明明写的是同一种外设或者同一个处理流程,可程序却经常会写出诸如
void PWM1_Init()
{
}
void PWM2_Init()
{
}
void PWM3_Init()
{
}
...
之类的代码出来。究其原因,还是对编程过程中做封装理解不够,同时又由于使用C语言完成面向对象编程比较麻烦,因此才会出现这样难以维护且移植困难的代码。 在STM32开发中,目前主流使用的HAL(硬件抽象层)库将诸多外设都抽象成一个个对象模型,配合CubeMX使用,开发者可以通过CubeMX生成的句柄(Handler)来操作单片机外设。然而为了在C语言环境下实现这套机制,HAL库可谓是煞费心思,以至于在HAL库中能频繁看到诸如
typedef struct __UART_HandleTypeDef
{
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
void (* TxHalfCpltCallback)(struct __UART_HandleTypeDef *huart);
void (* TxCpltCallback)(struct __UART_HandleTypeDef *huart);
void (* RxHalfCpltCallback)(struct __UART_HandleTypeDef *huart);
void (* RxCpltCallback)(struct __UART_HandleTypeDef *huart);
void (* ErrorCallback)(struct __UART_HandleTypeDef *huart);
void (* AbortCpltCallback)(struct __UART_HandleTypeDef *huart);
void (* AbortTransmitCpltCallback)(struct __UART_HandleTypeDef *huart);
void (* AbortReceiveCpltCallback)(struct __UART_HandleTypeDef *huart);
void (* WakeupCallback)(struct __UART_HandleTypeDef *huart);
void (* RxFifoFullCallback)(struct __UART_HandleTypeDef *huart);
void (* TxFifoEmptyCallback)(struct __UART_HandleTypeDef *huart);
void (* RxEventCallback)(struct __UART_HandleTypeDef *huart, uint16_t Pos);
void (* MspInitCallback)(struct __UART_HandleTypeDef *huart);
void (* MspDeInitCallback)(struct __UART_HandleTypeDef *huart);
#endif
} UART_HandleTypeDef;
之类的函数指针写法来“曲线救国”地实现面向对象编程的封装、继承、多态三大特性,一眼看过去实在是让人生畏。
二、单片机使用C++开发是否可行?
如果说STM32的寄存器编程是由于寄存器过多难以记忆而使人劝退,那么HAL库则是由于C语言的各种花式玩法而让人研究不下去。既然用C语言来实现面向对象编程需要如此大费周章,为什么不用C++来编程呢?事实上,C++作为C面向对象的超集,曾被视作是带类的C(当然目前C++各种眼花缭乱的特性更加使人劝退),且在现在主流的IDE(如Keil 5和STM32CubeIDE)中,编译器都支持C++和C语言混合编程,所以我们是可以使用C++来写单片机程序的。在keil5自带的Arm Compiler编译器中,AC5和AC6均支持C++编程,其中AC5只支持C++98标准,而目前最新的AC6支持C++14标准,从编译器的角度来看用C++开发Arm单片机程序是可行的。在Keil5中看编译器配置可知编译器支持的标准:
之前听很多人说,C++的效率低,所以单片机编程不能用C++。然而目前有一类单片机用上了C++编程,那就是大名鼎鼎的Arduino。Arduino的标准版单片机仅仅用的是8位avr单片机,却能愉快的用上C++。所以说,目前基于Cortex-M的性能肯定足够带的起C++程序的。事实上,我曾经在STM32F030F4P6这样空间小性能低的单片机上使用C++封装成对象的方式成功驱动起了W5500以太网芯片。因此只要不使用C++的诸如虚函数或者模板这类会使代码膨胀的特性,仅仅使用C++的面向对象特性和丰富的库函数,普通的F103及以上的应用开发想用上C++,芯片资源完全够用了。而且如果使用C++来开发单片机程序的话,则可以移植利用Arduino社区的各种设备驱动和代码库。 此外,虽然目前最新的CubeMX依旧是生成C语言的工程,但是每次生成工程之后弹出的对话框在最新的版本中多了一行:
是不是有点意思呢?
三、C/C++混合编程
在PC机编程上,因为编译器针对每个源文件不管是.c还是.cpp都编译成.o输出文件再进行链接,所以C语言是可以和C++混合编程的,但是由于C++支持函数重载的特性,使得C++的函数名在C中无法正确识别,因此要在.c中使用.cpp的函数,则必须在引用的头文件中将函数声明部分写成这样:
#ifdef __cplusplus
extern "C"
{
#endif
void startup(void);
void StartTask(void *argument);
void CommandParseTask(void *argument);
void UdpTask(void *argument);
void SampleTask(void *argument);
void UploadTask(void *argument);
void ATPTask(void *argument);
void DebugTask(void *argument);
#ifdef __cplusplus
}
#endif
在加上extern "C"声明之后,在C程序中就可以正确调用C++函数了。同样的道理,在单片机编程中,我们也可以改变原本程序的流向,使其从C程序的空间进入到C++程序的空间,从而我们就可以在.cpp中尽情的写单片机代码了。
四、来个例子
介绍了这么多,来个实际的例子。 在刚才我们利用CubeMX生成了一个工程,其中生成的main.c文件如下图所示: 注意,当程序从startup_xx.s里的 LDR R0, =__main 进入主函数main()之后,整个程序则从汇编进入了C语言的环境。因此,若我们想将程序从C语言环境中带入到C++环境,则可以采取同样的方法,利用一个引导函数使整个程序进入C++环境中运行。有的文章在介绍C++开发STM32时喜欢将整个工程的.c文件都改成.cpp后缀,或者在文件编译选项中将编译方式选择成为C++编译,个人觉得这样做不可取,因为如果要动CubeMX生成的文件的话,当需要改变某个外设的配置时就不能直接使用CubeMX重新生成了,因此我还是推荐使用引导函数的方式进入C++环境。
1.新建C++文件
在工程中新建一个.cpp文件和对应的.h文件,如图所示:
2.在.h文件和.cpp文件中加入代码
现在我们新建了start.cpp和start.h文件了,在start.h文件中加入如下代码:
#ifndef __START_H_
#define __START_H_
#ifdef __cplusplus
extern "C"
{
#endif
void startup(void);
#ifdef __cplusplus
}
#endif
#endif
在start.cpp中加入如下代码
#include "start.h"
#include "main.h"
#include "usart.h"
#include <string>
using namespace std;
void startup()
{
string str = "hello world!";
while(1)
{
HAL_Delay(100);
HAL_UART_Transmit(&huart1,(uint8_t *)str.data(),str.size(),0xff);
}
}
3.在main函数中调用C++的函数
之后,在main.c中加入头文件引用#include "start.h" 并在主函数中加入startup(); 如图所示(删掉部分自动生成的注释) 当程序运行起来之后,进入startup()函数后就进入了我们在上面写的while(1)循环之中,此时,整个程序即可直接使用C++的各种面向对象特性,以及强大的库函数,在我们的startup()程序中使用了C++的string库来生成字符串,并用到string对象的data()方法来获取C-style字符串并打印串口,串口连接电脑后如图所示:
此外,利用string对象的丰富特性,我们还能这样写(to_string()方法需要开启C++11支持): 可以看到运行得到的结果如下:
4.结论
STM32利用C++进行编程,不仅可以利用C++的面向对象特性简单的实现代码的封装复用,还能使用C++中经过千锤百炼完善的各种库对象和库方法,使我们能以更加简单的方式开发出更多功能。然而,本章的简单程序中并未涉及到C++的核心——类和对象。下一章我将通过不同外设封装的案例来展现单片机编程中使用面向对象思想的优势以及要注意的问题。
|