一、问题背景
利用STM32CubeMX建立的Keil工程中,默认是使用AC5(Arm Compiler 5)编译器的,该编译器仅支持C99标准和C++98标准,无法支持现代C++(C++11之后由于添加了大量现代编程语言特性而被称作现代C++),且AC5编译速度的慢也是有目共睹的。诸多因素之下,我们选用更新更强大的AC6(Arm Compiler 6)来给我们的开发(折腾)上一个新的台阶。毕竟C++11之后出现的大量新特性还是很香的,不在STM32这个平台上大显身手就可惜了。
比如强大的auto关键字:
auto ff = "hello";
比如lambda表达式:
auto fun =[=](int x,int y)->bool{return x%10<y%10;};
再比如基于范围的(range-based)for循环:
map<int,string> index_map{{1,"hello"},{2,"world"},{3,"!"}};
for(const auto &e : index_map)
{
HAL_UART_Transmit(&huart1, (uint8_t *)e.second.data() , e.second.length() , 0xff);
}
是不是感觉打开新世界的大门了呢?稍稍配置,STM32与现代C++的结合,体验一种新的开发方式。现在我们就来从STM32进入现代C++的世界吧。
二、开启步骤
1、点击进入工程配置对话框,
将ARM编译器改为编译器6,如图所示
这时C/C++选项卡会变成C/C++(AC6)
与AC5不同的是,这里的语言选项支持到C11和C++17,因此现代C++的语言特性大部分都可以在上面得到比较好的支持。如果在cubemx中添加了FreeRTOS支持的同学,到这里应该会收到数百个错误的提示。这时只需要将…Middlewares\Third_Party\FreeRTOS\Source\portable里的文件替换为cubemx的firmware库中GCC版本即可正常编译。
到这里开启工程支持就结束了,很简单是不是?
But!
仅仅这样配置完是运行不起来的,或者说运行起来会有各种问题。首先出现的就是一个大家熟知也困扰已久的问题:ARM的半主机模式。
三、解决半主机模式
STM32玩的比较熟的都知道,如果在工程选项中没有勾选上microlib支持又使用到一些和stdio相关的函数时,就会陷入一个很尴尬的问题,怎么debug一上来就停在一个地方进行不下去了?
BKPT 0xAB
当出现这个的时候,就说明你的CPU就陷入了半主机模式。具体半主机模式是什么,很多地方解释了我也就不再赘述。在一般的C程序中,我们可以用两种方式解决这个问题:
一、使用microlib库
二、重定向stdio.h里的方法,并在工程里加上如下代码:
#pragma import(__use_no_semihosting) // 确保没有从 C 库链接使用半主机的函数
_sys_exit(int x) //定义 _sys_exit() 以避免使用半主机模式
{
x = x;
}
struct __FILE // 标准库需要的支持函数
{
int handle;
};
/* FILE is typedef ’ d in stdio.h. */
FILE __stdout;
在网上的各种资料以及正点原子的例程中,我们都可以看到有这一段代码用来解决半主机模式的问题。在传统的C程序当中,这两招是公认的终极解决办法,然而这两招在AC6与C++上就不好使了,会报各种各样的错:
比如引用microlib后报的错:
再比如加入重定向代码后出的错(不用试图改错,改好了还会有更多的报错等着):
因此,就只能使用另一种办法来解决该问题:使用keil补丁。
Keil补丁
估计是ARM开发团队也意识到这个问题了吧,在后来的版本中可以自动添加进官方版本的重定向代码,在补丁中可以选定是进入中断、进入错误回调函数还是使用用户自定义实现版本,于是就有了下面的方法:
一、点击
按钮进入运行时环境配置界面:
二、做些配置
依次进入Compiler->I/O,将里面的STDERR、STDIN、STDOUT勾选上,并将variant列依次选择如下图所示:
选择完毕后点保存,编译。这样就能避免陷入半主机模式的中断当中去了。
四、解决动态分配内存问题
再执行完上面的操作之后,再进行编译,则可以编译通过,运行程序,不使用io相关函数的情况下也不会进入半主机模式,若需要使用io相关函数则对fput等函数进行重定向即可使用(直接使用会导致程序进入Error_Handler)。然而新的问题又会挡在我们面前:动态分配内存问题(malloc和free)。
我们知道,C++中new运算符的底层实现原理如下:
1、调用malloc函数开辟内存空间
2、调用构造函数生成对象
而delete函数实现原理与之相反:
1、调用析构函数做各种释放处理
2、执行free函数释放内存空间
因此在C++中,动态分配内存决定了一个对象是否能被靠谱的new出来。
然而经过了前面的配置后,由于我们不再使用microlib,因此开发环境在编译时将会连接标准C库的malloc函数和free函数,受限于单片机小的可怜的RAM空间,且没有MMU的帮助下,标准C库的内存分配函数对于单片机来说比较庞大,若是进行new和delete对象,很容易出现状态异常,进入Error_Handler。所以我们需要重定向new和delete操作符。
通常情况下,有两种方法去实现malloc和free:
1、自己设计实现内存池,并实现malloc和free函数(例如正点原子例程里的mymalloc和myfree)
2、用操作系统自带的内存管理功能实现
在这里为了方便介绍实现重定向动态分配过程,以使用操作系统自带的malloc方式来介绍。
我们使用freertos自带的pvPortMalloc()
和vPortFree()
来替换C标准库的malloc()和free()函数,这样做的好处是当内存被释放的时候会由操作系统来进行回收,具体的内存位置在操作系统申请的HEAP空间中(configTOTAL_HEAP_SIZE ),分配和回收方式由加载到工程中的heap_x.c确定。
在stm32cubemx中,我们将configTOTAL_HEAP_SIZE根据各芯片的RAM空间设置为合适的大小,并利用pvPortMalloc()
和vPortFree()
来重载new和delete运算符,代码如下:
#if USE_OS
#include <new>
void *operator new(size_t size) throw(std::bad_alloc)
{
return pvPortMalloc(size);
}
void operator delete(void *pointer) throw()
{
vPortFree(pointer);
}
#endif
其中,头文件包含了运算符new和delete的函数声明,我们在自己的程序中利用将new和delete运算符进行重载。至此,在程序中我们可以new和delete对象了。