当前位置:   article > 正文

KEIL / MDK C++编程实例说明:发掘C++的嵌入式开发活力

mdk c++

众所周知,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编译器进行编译。具体形式如下:

  1. #ifdef __cplusplus
  2. extern "C" {
  3. #endif
  4. ... your C declared part ...
  5. #ifdef __cplusplus
  6. }
  7. #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中初始化,文件定义了如下方法进行对串口的提前初始化。

  1. /*----------------------------------------------------------------------------
  2. Superclass to initialize the serial interface
  3. *----------------------------------------------------------------------------*/
  4. /* 引进原始__rt_entry方法 */
  5. extern void $Super$$__rt_entry(void);
  6. /* 定义新__rt_entry方法 */
  7. void $Sub$$__rt_entry(void) {
  8. SER_Init();
  9. /* 调用原始__rt_entry方法; */
  10. $Super$$__rt_entry();
  11. }

通过$super

$sub
两个编译器指令的结合使用,将串口初始化方法“填”进了系统的入口方法中。其中SER_Init()方法初始化了USART1。我们也无需再在main.c中初始化串口。另外标准库需要用户提供堆内存进行支持。STM32的启动文件中已经定义了用户堆。所以我们无需再定义,但默认的堆容量对于标准库来说还是太过于小,要手动修改堆的尺寸如下:

Heap_Size       EQU     0x00001000	; Extend for using C++ std lib

还有需要注意的是,KEIL / MDK 的C++编译器默认失能异常捕获机制,如果你的代码中用到了该机制,那么就需要在工程配置的"Options for Target - C/C++ - Misc controls"选项中添加'--exceptions'选项。添加如下图所示:

C++模板工程测试:

弄完上面的这些步骤之后,这个模板工程就可以随心所欲地进行C++程序编写了,下面我们编写一点简单代码进行测试标准库,代码如下:

  1. #include <string>
  2. #include <iostream>
  3. #include "ledx.h"
  4. #include "tick.h"
  5. int main(void) {
  6. LED_Init(); // 初始化LED
  7. Tick_Init(); // 初始化SYSTICK
  8. std::string str("the float number is ");
  9. float number = 0;
  10. while (1) {
  11. std::cout << str << number++ << std::endl;
  12. led1_on();
  13. led2_on();
  14. delay_ms(500);
  15. led1_off();
  16. led2_off();
  17. delay_ms(500);
  18. }
  19. }

烧录运行并输出到PC串口助手的内容如下:

可以看到运行结果是正确的。但实际情况却是几乎没人愿意这么用在嵌入式开发中,原因也很简单。

这是上面那段测试程序编译链接后的输出窗口,勤俭持家的嵌入式程序员看到这个估计要炸锅了吧!这点代码用纯C写最多占6KB FLASH和极少的RAM资源,但引入C++标准库后居然要占用52KB FLASH和27KB RAM,这不太科学啊!可能习惯编写终端的高富帅程序员看了会不以为然:这么点都不舍得花,怎么能过上有品质的生活啊!但我们的嵌入式程序员却表示:有钱也不能这样花啊,我们的每一分钱都要花得值当,再说我们的积蓄哪里有这么多!(这才是原因,哈哈尴尬!)

优化C++程序:

当然以上只是玩笑话,不过作为一个优秀的嵌入式开发程序员,代码优化永远都是重中之重。那么我们为了勤俭节约一把,自然是不能滥用标准库的,不过得益于C++的优越性,我们可以轻松地编写自己的功能类用于处理一般性的问题,就拿以上测试程序为例,不就是需要一个类似cout输出形式的类嘛!这还不容易!然后我们就开始简单地编写一个类用于实现上述功能。

下面是这个类的头文件部分:

  1. #include "stdio.h"
  2. #include "stdarg.h"
  3. #include "string.h"
  4. // NAME SPACE OUT DEFINE
  5. namespace cchar {
  6. // CONST TYPE VARIABLE
  7. const char tint = 'd';
  8. const char tuint = 'u';
  9. const char tchar = 'c';
  10. const char tftp = 'f';
  11. // CONST ENTER VARIABLE
  12. const char endl = '\n';
  13. }
  14. using namespace cchar;
  15. // STRING OUTPUT CLASS
  16. class ostring {
  17. public:
  18. ostring& operator << (const char* str);
  19. ostring& operator << (int num);
  20. ostring& operator << (unsigned int num);
  21. ostring& operator << (short num);
  22. ostring& operator << (unsigned short num);
  23. ostring& operator << (char num);
  24. ostring& operator << (unsigned char num);
  25. ostring& operator << (float num);
  26. ostring& operator << (double num);
  27. private:
  28. void put_string(const char* str, unsigned int cnt);
  29. void put_number(char type, ...);
  30. };

我们声明了一个ostring类,类里面啥数据成员都木有,只重载不同数据类型的运算符"<<"即可,私有的两个成员函数负责整合字符串并向串口发送。

下面是ostring类的实现文件:

  1. #include "ostr.hpp"
  2. extern "C" { extern int put_char(int c);}
  3. // OUT STRING CLASS BASE PRINT FUNCTION
  4. void ostring::put_string(const char* str, unsigned int cnt) {
  5. for (unsigned int index = 0; index < cnt; index++) {
  6. put_char(str[index]);
  7. }
  8. }
  9. // STRING OUTPUT CLASS PUT NUMBER FUNCTION
  10. void ostring::put_number(char type, ...) {
  11. va_list arg;
  12. char temp[20];
  13. unsigned char cnt = 0;
  14. va_start(arg, type);
  15. switch (type) {
  16. case tint:
  17. cnt = sprintf(temp, "%d", va_arg(arg, int));
  18. break;
  19. case tuint:
  20. cnt = sprintf(temp, "%d", va_arg(arg, unsigned int));
  21. break;
  22. case tchar:
  23. cnt = sprintf(temp, "%c", va_arg(arg, int));
  24. break;
  25. case tftp:
  26. cnt = sprintf(temp, "%f", va_arg(arg, double));
  27. break;
  28. }
  29. va_end(arg);
  30. put_string(temp, cnt);
  31. }
  32. // STRING OUTPUT CLASS OPERATOR FUNCTION
  33. ostring& ostring::operator << (const char* str) {
  34. put_string(str, strlen(str));
  35. return *this;
  36. }
  37. ostring& ostring::operator << (int num) {
  38. put_number(tint, num);
  39. return *this;
  40. }
  41. ostring& ostring::operator << (unsigned int num) {
  42. put_number(tuint, num);
  43. return *this;
  44. }
  45. ostring& ostring::operator << (short num) {
  46. put_number(tint, num);
  47. return *this;
  48. }
  49. ostring& ostring::operator << (unsigned short num) {
  50. put_number(tuint, num);
  51. return *this;
  52. }
  53. ostring& ostring::operator << (char num) {
  54. put_number(tchar, num);
  55. return *this;
  56. }
  57. ostring& ostring::operator << (unsigned char num) {
  58. put_number(tuint, num);
  59. return *this;
  60. }
  61. ostring& ostring::operator << (float num) {
  62. put_number(tftp, num);
  63. return *this;
  64. }
  65. ostring& ostring::operator << (double num) {
  66. put_number(tftp, num);
  67. return *this;
  68. }

这个CPP文件引进了发送单个字符到串口的方法put_char,put_string方法简单地将一个字符串依次发送出去,而put_number方法则是使用C标准库的可变参数对数据类型进行分类,再整合到字符串中再依次发出。这里为了节约开销,我没有用模板函数(试过的,开销显然比这个大)。把这两个文件添加进工程后再运行刚刚的测试程序,

首先将测试程序修改成如下:

  1. #include "ledx.h"
  2. #include "tick.h"
  3. #include "ostr.hpp"
  4. int main(void) {
  5. LED_Init(); // 初始化LED
  6. Tick_Init(); // 初始化SYSTICK
  7. ostring cout;
  8. float number = 0;
  9. while (1) {
  10. cout << "the float number is " << number++ << endl;
  11. led1_on();
  12. led2_on();
  13. delay_ms(500);
  14. led1_off();
  15. led2_off();
  16. delay_ms(500);
  17. }
  18. }

哈哈!现在我们也能像标准库那样输出了!再来看看这个程序的开销如何

可以看出来吧?仅仅用了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++的开发潜力也将更多地体现出来。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/1004336
推荐阅读
相关标签
  

闽ICP备14008679号