赞
踩
众所周知,KEIL / MDK是支持C++编程方式的。但是就目前来说,使用C++开发嵌入式的程序员还是比较少,就我个人认为原因是一方面KEIL / MDK对C++程序的支持还不够全面,另一方面则是C++程序的体量相较于C程序过于庞大,对于小型的应用来说没有必要,而且嵌入式开发程序员通常C++功底并不高,C才是他们的拿手好戏。但我认为随着MCU性能的逐渐提升,嵌入式C++的开发潜力将会越来越多的被发掘。并且C++的标准还在更新,总体来说C++的活力是高于C的。当然,最重要的一点则是C++兼容了C,有人甚至说只要C不死,C++就不会死!我认为这是很有道理的。
作为一个嵌入式和智能终端都有一点接触的程序员来说,我当然是更希望我的代码能够更容易地移植。那么使用C++编程的方式无疑就要合理一些。最近两天刚刚忙完期末测评,突然兴起就写了这篇博文,那么接下来我就以自己在KEIL / MDK上建的一个STM32F1的C++模板工程为例,聊一聊C++编程的相关事项,如果有错误或者不足请各位勿怪,也欢迎各位留言和我交流。
准备C++模板工程:
首先我们要新建一个KEIL / MDK工程,如果没建过的话就花点时间百度教程吧,如果不想多费力气那也OK,直接把一个C工程的main.c替换为main.cpp即可。工程的配置均保持原样无需改变。但此时编译的这个工程应该会出一些错误,当文件后缀是C的时候IDE会使用C编译器进行编译,如果文件后缀是CPP则IDE使用C++编译器进行编译,工程包含的头文件是使用C++编译器进行编译的,不过头文件声明的还是C文件的符号,所以IDE会无法正确编译链接。此时我们应该将头文件所有声明C符号的部分用预编译宏加extern "C" { }的形式包含起来,告诉编译器该段要使用C编译器进行编译。具体形式如下:
- #ifdef __cplusplus
- extern "C" {
- #endif
-
- ... your C declared part ...
-
- #ifdef __cplusplus
- }
- #endif
当使用STM32标准库编写程序的时候会发现它所有的头文件都是这样处理过的。也就是说库函数完全兼容C++。这也是我们开发C++程序的强大后盾。在准备了基本工程后就可以在main.cpp文件中使用C++编程了,但还应该注意的是,C编译器不能直接引进CPP文件的符号,同理C++编译器也不能直接引进C文件的符号。当C文件要extern 一个CPP文件的符号时,这个CPP文件的符号定义应该用extern "C" { }进行修饰,否则不能通过编译链接。同样地,当CPP文件要引进一个C文件的符号时也要在CPP文件中使用extern "C" { }进行修饰。另外汇编启动文件的[WEAK]声明仅对C文件符号有效,所以我们编写外设中断服务方法时应该写在C文件中,或者在CPP文件中使用exetrn "C" { }修饰符。
建立的C++模板工程文件目录如下:
其中serial.c和retarget.c都是在KEIL / MDK的安装目录下复制过来的,这两个文件提供了对C标准库和C++标准库的部分支持。添加上述文件到工程后我们就能正常使用C++的std标准库进行编程了。关于serial.c和retarget.c我们需要知道以下一些知识:
KEIL / MDK在semihosting模式下,标准库的输入输出流并没有定向到串口,要正常使用std标准库就需要先关闭semihosting模式,即使用预编译指令如下:
#pragma import(__use_no_semihosting_swi)
关闭semihosting后需要对部分标准库方法进行重定向编写,既将输入输出流重定向至STM32的串口端。而retarget.c里面就实现了相关的方法。
在serial.c中定义了对串口的初始化以及发送和接收的方法。为了让串口能在运行系统入口方法前被初始化而不是在用户main.c中初始化,文件定义了如下方法进行对串口的提前初始化。
- /*----------------------------------------------------------------------------
- Superclass to initialize the serial interface
- *----------------------------------------------------------------------------*/
- /* 引进原始__rt_entry方法 */
- extern void $Super$$__rt_entry(void);
-
- /* 定义新__rt_entry方法 */
- void $Sub$$__rt_entry(void) {
- SER_Init();
- /* 调用原始__rt_entry方法; */
- $Super$$__rt_entry();
- }
通过$super
Heap_Size EQU 0x00001000 ; Extend for using C++ std lib
还有需要注意的是,KEIL / MDK 的C++编译器默认失能异常捕获机制,如果你的代码中用到了该机制,那么就需要在工程配置的"Options for Target - C/C++ - Misc controls"选项中添加'--exceptions'选项。添加如下图所示:
C++模板工程测试:
弄完上面的这些步骤之后,这个模板工程就可以随心所欲地进行C++程序编写了,下面我们编写一点简单代码进行测试标准库,代码如下:
- #include <string>
- #include <iostream>
- #include "ledx.h"
- #include "tick.h"
-
-
- int main(void) {
-
- LED_Init(); // 初始化LED
- Tick_Init(); // 初始化SYSTICK
-
- std::string str("the float number is ");
- float number = 0;
-
- while (1) {
- std::cout << str << number++ << std::endl;
-
- led1_on();
- led2_on();
- delay_ms(500);
- led1_off();
- led2_off();
- delay_ms(500);
- }
- }
烧录运行并输出到PC串口助手的内容如下:
可以看到运行结果是正确的。但实际情况却是几乎没人愿意这么用在嵌入式开发中,原因也很简单。
这是上面那段测试程序编译链接后的输出窗口,勤俭持家的嵌入式程序员看到这个估计要炸锅了吧!这点代码用纯C写最多占6KB FLASH和极少的RAM资源,但引入C++标准库后居然要占用52KB FLASH和27KB RAM,这不太科学啊!可能习惯编写终端的高富帅程序员看了会不以为然:这么点都不舍得花,怎么能过上有品质的生活啊!但我们的嵌入式程序员却表示:有钱也不能这样花啊,我们的每一分钱都要花得值当,再说我们的积蓄哪里有这么多!(这才是原因,哈哈尴尬!)
优化C++程序:
当然以上只是玩笑话,不过作为一个优秀的嵌入式开发程序员,代码优化永远都是重中之重。那么我们为了勤俭节约一把,自然是不能滥用标准库的,不过得益于C++的优越性,我们可以轻松地编写自己的功能类用于处理一般性的问题,就拿以上测试程序为例,不就是需要一个类似cout输出形式的类嘛!这还不容易!然后我们就开始简单地编写一个类用于实现上述功能。
下面是这个类的头文件部分:
- #include "stdio.h"
- #include "stdarg.h"
- #include "string.h"
-
- // NAME SPACE OUT DEFINE
- namespace cchar {
-
- // CONST TYPE VARIABLE
- const char tint = 'd';
- const char tuint = 'u';
- const char tchar = 'c';
- const char tftp = 'f';
-
- // CONST ENTER VARIABLE
- const char endl = '\n';
- }
- using namespace cchar;
-
- // STRING OUTPUT CLASS
- class ostring {
- public:
- ostring& operator << (const char* str);
- ostring& operator << (int num);
- ostring& operator << (unsigned int num);
- ostring& operator << (short num);
- ostring& operator << (unsigned short num);
- ostring& operator << (char num);
- ostring& operator << (unsigned char num);
- ostring& operator << (float num);
- ostring& operator << (double num);
- private:
- void put_string(const char* str, unsigned int cnt);
- void put_number(char type, ...);
- };
我们声明了一个ostring类,类里面啥数据成员都木有,只重载不同数据类型的运算符"<<"即可,私有的两个成员函数负责整合字符串并向串口发送。
下面是ostring类的实现文件:
- #include "ostr.hpp"
-
-
- extern "C" { extern int put_char(int c);}
- // OUT STRING CLASS BASE PRINT FUNCTION
- void ostring::put_string(const char* str, unsigned int cnt) {
- for (unsigned int index = 0; index < cnt; index++) {
- put_char(str[index]);
- }
- }
-
- // STRING OUTPUT CLASS PUT NUMBER FUNCTION
- void ostring::put_number(char type, ...) {
- va_list arg;
- char temp[20];
- unsigned char cnt = 0;
-
- va_start(arg, type);
- switch (type) {
- case tint:
- cnt = sprintf(temp, "%d", va_arg(arg, int));
- break;
- case tuint:
- cnt = sprintf(temp, "%d", va_arg(arg, unsigned int));
- break;
- case tchar:
- cnt = sprintf(temp, "%c", va_arg(arg, int));
- break;
- case tftp:
- cnt = sprintf(temp, "%f", va_arg(arg, double));
- break;
- }
- va_end(arg);
- put_string(temp, cnt);
- }
-
- // STRING OUTPUT CLASS OPERATOR FUNCTION
- ostring& ostring::operator << (const char* str) {
- put_string(str, strlen(str));
- return *this;
- }
- ostring& ostring::operator << (int num) {
- put_number(tint, num);
- return *this;
- }
- ostring& ostring::operator << (unsigned int num) {
- put_number(tuint, num);
- return *this;
- }
- ostring& ostring::operator << (short num) {
- put_number(tint, num);
- return *this;
- }
- ostring& ostring::operator << (unsigned short num) {
- put_number(tuint, num);
- return *this;
- }
- ostring& ostring::operator << (char num) {
- put_number(tchar, num);
- return *this;
- }
- ostring& ostring::operator << (unsigned char num) {
- put_number(tuint, num);
- return *this;
- }
- ostring& ostring::operator << (float num) {
- put_number(tftp, num);
- return *this;
- }
- ostring& ostring::operator << (double num) {
- put_number(tftp, num);
- return *this;
- }
这个CPP文件引进了发送单个字符到串口的方法put_char,put_string方法简单地将一个字符串依次发送出去,而put_number方法则是使用C标准库的可变参数对数据类型进行分类,再整合到字符串中再依次发出。这里为了节约开销,我没有用模板函数(试过的,开销显然比这个大)。把这两个文件添加进工程后再运行刚刚的测试程序,
首先将测试程序修改成如下:
- #include "ledx.h"
- #include "tick.h"
- #include "ostr.hpp"
-
-
- int main(void) {
-
- LED_Init(); // 初始化LED
- Tick_Init(); // 初始化SYSTICK
-
- ostring cout;
- float number = 0;
-
- while (1) {
- cout << "the float number is " << number++ << endl;
-
- led1_on();
- led2_on();
- delay_ms(500);
- led1_off();
- led2_off();
- delay_ms(500);
- }
- }
哈哈!现在我们也能像标准库那样输出了!再来看看这个程序的开销如何
可以看出来吧?仅仅用了7KB不到的FLASH和6KB RAM资源,而且我们还没有将划分给标准库的那部分RAM回收,所以RAM实际上消耗是很小的。最后运行一下这个程序,其输出到串口助手的截图如下:
结果依然是正确的,和标准输出唯一不同的是我没有控制输出精度。
C++模板工程对操作系统的支持:
在写这篇博文之前我也验证了在C++模板工程上运行RTOS的可行性。结果当然是可行的,而且不会出现任何问题。这正是得益于C++对C的兼容性。也就是说,C++具备在嵌入式开发的一切条件,真的是只欠东风(程序员)!另外,我还在C++模板工程上成功运行了自己这学期用C写的一个RTOS。我把它叫做REGINA,在上一篇博文里简短地介绍了一下,有兴趣的朋友不妨去下载下来使用看看,它是一个免费的自由软件,而它的体量已经被我精简地非常不错了。博文地址是http://blog.csdn.net/hlld__/article/details/78865260。不过如果有兴趣的你看到那篇博文后可能会感觉很奇怪,原谅我是用较官方性的语言来写的,这样能营造一些正式的气氛!哈哈!
通过这个简单的实例,我们还模仿了用于标准输出的类,并实现了相关的功能。在实际嵌入式开发中为了节约体量还会干很多这样的事情,但就像我前面说的那样,C++给予了程序员这样的便利,仅仅很小的工作量就能实现我们想要的功能。而且相较与C程序而言,C++程序的可移植性、代码的可扩展性显然更高。更为重要的是C++处理大批次数据的能力及实现复杂逻辑的困难程度都要优于C程序,我相信随着MCU的运算能力不断增强,使用C++开发的程序员将会越来越多,而嵌入式C++的开发潜力也将更多地体现出来。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。