MDK 开发Stm32 上使用 C++ cout 对象的实现ffice ffice" />
--Pony279
1. Retargeting 和 non-semihosting
为了实现 cout,需要加入了一段代码,在这段代码里重新实现了 fputc, ferror, fseek, ftell 等底层函数。在 mdk 的帮助文档的 Libraries and Floating Point Support Guide: Redefining low-level library functions to enable direct use of high-level library functions 一节中,也有类似的代码,读者可以自行参考。
因为这些底层函数原来默认的实现使用了半主机模式(其实我不知道半主机模式是什么),如果在 STM32 上使用了半主机模式的相关指令,就会直接导致死机,为了保证程序没有使用半主机模式,我加入了一句
#pragma import(__use_no_semihosting_swi)
这样,只要程序里使用了任何半主机模式相关的指令,链接器都会产生错误了。这个在 MDK 的帮助文档里面也有相关说明,可以参考 Libraries and Floating Point Support Guide: Using the libraries in a nonsemihosting environment。
以下是需要加入的代码:
//////////////////////////////////////////////////////////////////
// Retargeting cout and printf
#if 1
#include <stdio.h>
#include <stdlib.h>
#pragma import(__use_no_semihosting_swi)
namespace std{
struct __FILE
{
int handle;
/* Whatever you require here. If the only file you are using is */
/* standard output using printf() for debugging, no file handling */
/* is required. */
};
FILE __stdout;
FILE __stdin;
FILE __stderr;
// You might also have to re-implement fopen() and related functions
// if you define your own version of __FILE
// 有兴趣的可以定义自己的 __FILE,实现自己的 fopen,fwrite, fread
// 因为库里面的 fopen 是弱符号,你的定义可以覆盖库的定义
FILE *fopen(const char * __restrict /*filename*/,
const char * __restrict /*mode*/)
{
usart1<<"\n\r fopen. \n\r";
return NULL;
}
//-----------------------------------------------------------
int fputc(int ch, std::FILE *f) {
return sendchar(ch);
}
//-----------------------------------------------------------
int fgetc(FILE *f) {
/* Your implementation of fgetc(). */
usart1<<"\n\r fgetc \n\r";
return 0;
}
/*
检查是流否有错误,如果没有错误,返回 0
*/
int ferror(FILE *stream)
{
/* Your implementation of ferror(). */
return 0;
}
long int ftell(FILE *stream){
/* Your implementation of ftell(). */
usart1<<"ftell\n\r";
return 0;
}
int fclose(FILE *f){
/* Your implementation of fclose(). */
usart1<<"\n\r fclose \n\r";
return 0;
}
int fseek(FILE *f, long nPos, int nMode){
/* Your implementation of fseek(). */
usart1<<"fseek\n\r";
return 0;
}
/*
对于输出流,把缓冲区中的内容全部发送出去
这里用的是自己定义的 FILE 结构体,没有缓冲区,
所以什么都不做就行了。
*/
int fflush(FILE *f){
/* Your implementation of fflush(). */
return 0;
}
/*
默认的 _sys_exit 使用了 semihosted calls
所以必须重新实现它。
declared in <rt_sys.h> */
ARMAPI void _sys_exit(int) {
/* declared in <stdlib.h> */
abort();
while(1);
}
/*
这个函数是C/C++标准库用来打印必要的调试信息的
最好重新实现它,把调试信息发送到串口。
this function is declared in <rt_sys.h> */
ARMAPI void _ttywrch(int ch) {
sendchar(ch);
return ;
}
}
#endif
//end
//////////////////////////////////////////////////////////////////
注:
1. 这里面的函数是在我自己的代码环境下的实现方式,如果你的是C语言环境或者你自己的函数库里没有 usart1<<... 这一类的调用,需要自己做一下修改
2. 有一些函数,如 ftell ,我并没有期望它会被调用,所以我直接在函数里面输出串口信息,以免被错误调用了我还被蒙在鼓里。
还有测试代码,相当简单哦 
#include <iostream>
using std::cout;
using std::endl;
int main(void) {
cout<<"hello world!"<<endl;
while(1);
}
注:测试代码虽然简单,但是这这些代码执行之前需要做一些基本的硬件初始化,如系统时钟,串口等。(如果你使用了 ST 公司提供的 STM32 启动代码 V3.5 版本)这部分初始化可以在 SystemInit 函数中完成,SystemInit 是 Stm32 刚启动时,在调用 __main 之前所调用的硬件初始化函数。
代码编译通过,但是在程序运行的时候打印出
SIGABRT: Abnormal termination
然后就死机了,(不过不要太悲伤,幸好自己重实现了 __ttywrch 这个错误信息输出函数!)我并不了解SIGABRT 是什么,但在 mdk 的帮助文档是找到相关的说明:
也许是堆的大小不够吧,原来的定义是 0x400,那么把大小改成了 0x800 试试看:
编译,然后仿真运行正常了!到这里,基本工作已经完成了。在接下来的两节将描述我遇到的特殊情况,感兴趣的同学可以继续看下去。
2.下载后不能正常运行的奇怪现象
我把代码下载到开发板上运行,结果还是死机,而且是和之前一样的信息!做到这里我不得不吐槽 ARM 写的C/C++底层库了!“SIGABRT: Abnormal termination”算是什么!尼玛谁会看不出来系统死机了!!!尼玛就不能输出点有用的信息么!
唉,还是再看看帮助文档吧,我找到了
现在的代码里暂时还没有使用到 Exception Handliing 这个诱人的特性,所以是否重新实现 abort() 都无所谓的。还是写一个吧,随便输出点调试信息也好。
extern "C"
__attribute((weak))
void abort(){
output<<"abort()"<<endl;
while(1);
}
这样做完之后没什么特别的事情发生,只不过原来输出的错误信息 “SIGABRT: Abnormal termination” 现在变成了自己写的 abort 函数输出的 “abort()”然后再死机而已。
我在继续编译下载调试的过程中,发现代码有那么一两次是可以正常运行的,不过后来我又发现,当我断电,再上电后,又不正常了!后来我一直看文档,把大部分底层函数都重实现了,结果还是不行。想上 MDK 论坛去发帖请教,结果大神都没有回复,唉,看来还是只能靠自己啊。
这个现象说明软件仿真和实际运行一定存在着一些区别,虽然目前的代码没有使用多少硬件资源。经过思考和猜测,最后终于发现,原来是因为,我在工程里设置了 noinit(以前手贱设置的= =),所以上电时在 SRAM 中的那些没有声明初始值的全局变量不会被清零(刚上电时 SRAM 中的数据都是不确定的)。而软件仿真时,模拟的 SRAM 中的所有内容都已经事先被软件清零了。(下图中IRAM1右边的 NoInit 对应的那个框框本来有个我打的勾勾的,现在我把它去掉了)
去掉 noinit 后 ,再编译下载,cout 成功执行了!!!也许这应该归为 MDK 使用的 C/C++ 库的 BUG 吧,为啥一定要初始化清零呢?何必呢?就算这个问题不归为 C/C++ 库的 BUG,那么就应该归为 MDK 软件仿真的 BUG 了。
3. 其他问题
其实到这里,主要问题都已经解决了。不过我还遇到了其他问题,想和大家分享一下经验技巧,继续看吧 
1)部分 flash 写坏了 L
我继续下载调试程序的时候发现编程软件提示我 flash 在 45k的时候写坏了(在调试过程中我把优化改到了最低,所以代码膨胀到 45K以上了,使用 C++ 的库对 STM32 来说还是挺臃肿的啊)。不过换芯片神马的会比较苦逼,最关键的是现在是凌晨 2 点 L,上哪找 STM32去。于是我找到了一个比较可行的办法,让链接器在放代码时避开那个位置,还好能用 Y^_^Y
2)隐藏基本的软硬件初始化
另外,作为一个工程模板,我希望一些基本的硬件初始化和软件初始化代码应该在用户的程序执行之前进行。这样做可以事先建立一个基本的执行环境,对用户隐藏一些不必要的细节,例如我的测试代码里面,主函数就一句输出 “hello world!”,没有任何硬件初始化的代码。这样就方便了用户代码的编写,也让程序的结构变得更加直观。但是这样做就需要保证 __main 中进行 C/C++ 运行时库初始化的时候不可以把我已经初始化过一些全局变量清零。由于又不能 noinit,所以我选择了在 _clock_init 里进行初始化。_clock_init 是 C语言标准库里的函数,它是在清零全局变量之后,初始化用户定义的全局变量之前进行的,而且这个函数允许用户对其进行重新实现。其实我只是借个地方初始化而已。
/*由于不能使用 no_init,而 sys 对象又必须在所有对象之前初始化
_clock_init 这里是个初始化的好地方 */
ARMAPI void _clock_init(){
sys.Init();
return ;
}
到这里,使用 cout 的全部工作就完成了!!!
4. 测试结果
最后程序输出的信息很简单,就是 Hello world 啦
注:前三行是自己在系统初始化时加的调试信息,且系统时延迟 1s 启动的,让开发板上 PA8 对应的红色 LED 也亮 1s, 以示意系统启动,这样在下载完程序后串口调试助手才有足够的时间打开串口
又让我回想起当初入门 C++ 的感觉了呢 Y^_^Y
5. 继续拓展?
既然 cout 能实现了,那么 cin 当然也是能够实现的,只要你实现好 fgetc 这个函数就行了。而且,有兴趣的同学还可以把 Fatfs 中的文件系统函数和 C 语言标准库的函数结合起来,还有还有,可以把 RTC 和 <time.h> 的相关函数结合起来,在 STM32 上完善 C/C++ 标准库的使用!还等什么?有兴趣就自己写代码试试吧!
|