文章目录
- 背景
- 挑战与困难
- 如何整合编译?
- error: non-ASM statement in naked function is not supported
- error: '#pragma import' is an ARM Compiler 5 extension, and is not supported by ARM Compiler 6
- error: redefinition of '__FILE'
- 改造demo中的cout
- 改造delete运算符
- 总结与展望
背景
在文章使用C/C++实现线性代数计算——环境bringup 中,围绕Eigen介绍了使用在各种场合下的环境bringup和编译问题,本文接着上文的内容,详细记录一下基于MDK5+Eigen+STM32来实现一个线性代数计算器的过程,里面还是有很多问题可以学习分享一下的。
挑战与困难
首先,Eigen是一个C++框架的开源项目,虽说不依赖什么OS之类的东西,但是若要完全跑在STM32裸机中,还是有一些问题和适配难点需要解决的,本文先详细介绍一下如果规避这些问题,给大家一个参考,解决思路可能不是最优的,如果你有更好的解决思路,不妨评论一下,我们一起相互学习交流一下~~
本文使用的环境时MDK5.35 + eigen3.4.0 + STM32F103ZE系列开发板
如何整合编译?
在 使用C/C++实现线性代数计算——环境bringup 一文的文末,简单提了一下在Keil中的编译环境配置,借助MDK官方提供的启动文件,能编译链接生成一个bin文件。如果要想真的在STM32里面跑起来是远远不够的,首先里面的printf函数、std::cout输出流在stm32上就没有,所以为了验证编译出的bin文件到底能不能直接烧写到stm32里面直接跑,笔者找了一个stm32串口demo程序,在里面接入Eigen,然后调用Eigen提供的矩阵运算API,通过串口发出来(本文末会将改造后的demo发出来供大家参考)。具体实现的效果如下:
项目中关键文件目录树如下:
添加官方提供的example.c文件和cpp文件(详见 使用C/C++实现线性代数计算——环境bringup ,后面不再赘述),不过需要修改一下,具体如下:
- 考虑到在main.c里已经有一个main函数了,并且后面会主要借助main.c里的main函数初始化外设,所以需要将example.c里面有一个int main函数改个名字,
void test_eigen()
,然后在main函数里加上如下语句:
(PS:我这里的改法仅仅是为了方便验证Eigen的功能,一个完整、规范的项目最好不要瞎几把跨文件用extern声明函数,项目复杂之后可读性会非常差,这是一个反例,大家不要学我!)
然后,按照之前的文章,修改Target里面的ARM Compiler选择版本6,C/C++里面版本和之前文章一样即可。然后点击编译,会有很多错误,我们一个一个来解决。
error: non-ASM statement in naked function is not supported
完整的报错是:…/CORE/core_cm3.c(445): error: non-ASM statement in naked function is not supported
这个报错的根因是ARM Ver6 Compiler不支持旧版本的core_cm3.c里面的C中的汇编指令,解决的思路有三个:
- 编译器换回V5(换回V5不支持C++编译,行不通);
- 把core_cm3.c和core_cm3.h升级到新版本(这个没试过,不过应该可以,用新版的stcCubeMX生成一个demo,看看里面是不是新的,然后替换掉旧的好不好使,笔者没试这个路子);
- 把core_cm3.c从项目中删了(试了,可以,为啥可以删掉?可能是用V5编译生成过.o文件,即便从项目里把core_cm3.c删掉了,链接时仍能用缓存的目标文件);
error: ‘#pragma import’ is an ARM Compiler 5 extension, and is not supported by ARM Compiler 6
具体报错信息是:
…/SYSTEM/usart/usart.c(39): error: ‘#pragma import’ is an ARM Compiler 5 extension, and is not supported by ARM Compiler 6 [-Warmcc-pragma-import]
#pragma import(__use_no_semihosting)
^
这个错误说的很明白,V6版本的arm编译器不支持’#pragma import’ 语法,那为啥要用到 ‘#pragma import’ 语句呢?回到代码里看一下,这一行具体是#pragma import(__use_no_semihosting)
,它的作用是关掉arm的半主机模式。
所谓半主机模式:是用于ARM目标的一种机制;可将来自STM32单片机应用程序的输入输出请求传送至运行仿真器的PC主机。使用此机制可以启用C库中的函数,如printf()和scanf(),来使用PC主机的屏幕和键盘。禁掉后方可使用重定向手段,将printf函数的标准输出重定向到串口上,这样就可以用printf函数打印字符串,然后在串口里读到数据,这种骚操作非常适合调试,具体细节可以参考:【stm32串口打印】printf函数的使用方法,注意事项,原理以及拓展,个人学习理解总结。
那如果用了V6版本的编译器,就不能禁掉半主机模式了,下面会将平提方法,我们先接着看错误。
error: redefinition of ‘__FILE’
具体报错信息是这个:
…/SYSTEM/usart/usart.c(41): error: redefinition of ‘__FILE’
struct __FILE
对应的源码还是:
//
//加入以下代码,支持printf函数,而不需要选择use MicroLIB
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
原因是在stdio.h里面已经有一个struct __FILE定义了,为啥这里又要重新定义一个struct __FILE?还是因为要用printf函数,因为printf函数本质上就是将格式化后的字符串打印到标准输出上,本质上标准输出就是一个FILE类型的变量(一切皆文件?以后再探究了,这里先不展开了)。
所以综合来看,我们只需要先不用printf函数打印字符串应该就能规避上述两个问题了,这里笔者提供的平提方案是:
#define USART_SEND_BUFFER_SIZE (128)
#define USART_RECV_BUFFER_SIZE (128)
void USART_SendString(char *str)
{
uint8_t idx = 0;
while (*(str+idx))
{
USART_SendData(USART1, *(str+idx));
while(USART_GetFlagStatus(USART1, USART_FLAG_TC)!=SET);
idx++;
}
}
void u_printf(const char *format,...)
{
char String[USART_SEND_BUFFER_SIZE] = {0};
__va_list arg;//定义一个参数列表变量va_list是一个类型名,arg是变量名
va_start(arg,format); //从format位置开始接收参数表放在arg里面
//sprintf打印位置是String,格式化字符串是format,参数表是arg,对于封装格式sprintf要改成vsprintf
vsprintf(String,format,arg);
va_end(arg); //释放参数表
USART_SendString(String);//发送String
}
原理相当于是重写了一个接口叫u_printf,传参啥的跟printf函数是一样的,但是输出到的是串口。
改造demo中的cout
除了上面的问题,还有在binary_library.cpp中用了std::cout方法打印矩阵的值,由于stm32没有OS,所以也不存在什么标准IO流了,这里也需要改造,具体做法是:
将:
void MatrixXd_print(const C_MatrixXd *m)
{
std::cout << c_to_eigen(m) << std::endl;
}
改为:
void MatrixXd_print(const C_MatrixXd *m)
{
MatrixXd cpp_m = c_to_eigen(m);
unsigned char r = cpp_m.rows();
unsigned char c = cpp_m.cols();
char val[32] = {0};
while (r)
{
while(c)
{
sprintf(val, "%.3f \t", cpp_m(r - 1, c - 1));
USART_SendString(val);
c--;
}
r--;
c = cpp_m.cols();
USART_SendString("\r\n");
}
}
除了void MatrixXd_print(const C_MatrixXd *m)
函数,void Map_MatrixXd_print(const C_Map_MatrixXd *m)
也是一样的,改造后的内容如下:
void Map_MatrixXd_print(const C_Map_MatrixXd *m)
{
MatrixXd cpp_m = c_to_eigen(m);
unsigned char r = cpp_m.rows();
unsigned char c = cpp_m.cols();
char val[32] = {0};
while (r)
{
while(c)
{
sprintf(val, "%.3f \t", cpp_m(r - 1, c - 1));
USART_SendString(val);
c--;
}
r--;
c = cpp_m.cols();
USART_SendString("\r\n");
}
}
本质上原理是将Eigen中MatrixBase类提供的operator<<运算符重载方法改成了C语言中能直接用的方法。
改造delete运算符
在binary_library.cpp文件中,void MatrixXd_delete(C_MatrixXd *m)
和void Map_MatrixXd_delete(C_Map_MatrixXd *m)
函数用了delete运算符释放堆内存,本身在cpp文件中是支持的,但是放到stm32中这么操作就会导致Hardfault,这里也需要改造一下,改成free函数,如下:
void MatrixXd_delete(C_MatrixXd *m)
{
// delete &c_to_eigen(m);
if (NULL != m)
{
free(m);
m = NULL;
}
}
// skip .....
void Map_MatrixXd_delete(C_Map_MatrixXd *m)
{
// delete &c_to_eigen(m);
if (NULL != m)
{
free(m);
m = NULL;
}
}
这里还需要啰嗦一句,按理说stm32里面没跑什么os,连内存管理机制都没有,free()函数还是delete运算符都是无意义的,为啥用new或者malloc就没区别,free()换成delete就不行呢?先埋个引子,以后再去探究了,如果有懂的大哥,也帮忙解答一下,感谢。
总结与展望
本文主要介绍了将Eigen项目移植到stm32开发板上遇到的一些问题以及解决办法,完整的例程请关注VX公众号“24K纯学渣”回复关键词“stm32_eigen”获取。
当前,笔者提供的demo还是太简单,基本上就做了一个矩阵运算和打印,只是卖出了第一步,可扩展的空间着实不小,例如:
- 加上交互功能,可以是串口式的、CLI式的、或者复杂一些整个可触摸的LCD,做成像手机APP一样的;
- 除了矩阵基本运算,还可以加一些复杂的比如正交分解、求解行列式值、求特征值、特征向量;
- 除了线性代数运算以外,还可以求解微分方程,机器人运动学解逆、无人机视觉定位等等;
如果你也刚好对上述内容感兴趣,欢迎来学习交流噢!