使用STM32实现一个线性代数计算器

背景

在文章使用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 ,后面不再赘述),不过需要修改一下,具体如下:

  1. 考虑到在main.c里已经有一个main函数了,并且后面会主要借助main.c里的main函数初始化外设,所以需要将example.c里面有一个int main函数改个名字,void test_eigen(),然后在main函数里加上如下语句: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中的汇编指令,解决的思路有三个:

  1. 编译器换回V5(换回V5不支持C++编译,行不通);
  2. 把core_cm3.c和core_cm3.h升级到新版本(这个没试过,不过应该可以,用新版的stcCubeMX生成一个demo,看看里面是不是新的,然后替换掉旧的好不好使,笔者没试这个路子);
  3. 把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还是太简单,基本上就做了一个矩阵运算和打印,只是卖出了第一步,可扩展的空间着实不小,例如:

  1. 加上交互功能,可以是串口式的、CLI式的、或者复杂一些整个可触摸的LCD,做成像手机APP一样的;
  2. 除了矩阵基本运算,还可以加一些复杂的比如正交分解、求解行列式值、求特征值、特征向量;
  3. 除了线性代数运算以外,还可以求解微分方程,机器人运动学解逆、无人机视觉定位等等;

如果你也刚好对上述内容感兴趣,欢迎来学习交流噢!

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 要使用STM32实现俄罗斯方块,您需要学习如何在STM32开发板上编写程序,并了解STM32的硬件特性。您可以使用C或C++语言编写程序,并利用STM32的显示器、按键和其他硬件资源进行开发。 实现俄罗斯方块的步骤如下: 1. 了解俄罗斯方块的基本游戏规则。 2. 设计游戏的界面,并确定游戏所需的硬件资源。 3. 编写程序,使用STM32的硬件特性实现游戏的各项功能。 4. 在STM32开发板上测试程序,确保游戏正常运行。 5. 如有需要,可以进行程序优化和错误修复。 如果您是初学者,建议您先学习STM32的基础知识,再尝试实现俄罗斯方块。 ### 回答2: 使用STM32实现俄罗斯方块游戏需要首先确定硬件平台和开发环境。我们可以选择一个合适的STM32开发板作为硬件平台,例如STM32F4系列或STM32F7系列开发板,并下载安装相应的软件开发工具。 在开发环境搭建完成后,我们可以开始编写俄罗斯方块的游戏逻辑和图形界面。首先,需要设计游戏的界面结构,包括游戏区域、下一个方块区域和得分区域等。然后,需要定义方块的形状和移动的操作,并实现相应的碰撞检测逻辑。 接下来,我们可以利用STM32的GPIO接口来控制LED灯或LCD屏幕显示游戏界面。通过控制LED灯或LCD屏幕的亮灭或像素显示,可以实现方块的移动和旋转等操作。 在游戏过程中,我们可以使用STM32的定时器功能来设置刷新游戏界面的频率。通过定时器,可以定时更新游戏界面,使方块能够流畅地移动和显示。 最后,我们可以利用STM32的外部中断功能来处理用户的输入操作。通过接入按钮或开关等输入设备,并配置外部中断,可以实现用户控制方块下落、旋转等操作。 通过以上步骤的设计和实现,就可以在STM32上成功实现一个俄罗斯方块游戏。在游戏中,玩家可以通过操作按钮或开关来控制方块的移动和旋转,并在游戏过程中获得相应的得分。这样,我们就可以在STM32上玩游戏并享受游戏的乐趣。 ### 回答3: 俄罗斯方块是一种经典的益智游戏,使用STM32微控制器可以轻松实现该游戏。 首先,我们需要一个合适的STM32微控制器,例如STM32F4系列或STM32F7系列,因为这些型号具有足够的RAM和处理器速度来处理游戏逻辑和图像显示。 接下来,我们需要连接一个合适的显示器。可以选择TFT LCD显示器或OLED显示屏,它们都具有较高的分辨率和色彩深度,可以展示丰富的游戏界面。 为了实现俄罗斯方块的操作逻辑,我们可以利用STM32的GPIO(通用输入输出)来读取玩家的按键输入。例如,可以设置四个按键来控制方块的移动(向左、向右、旋转、下落)。通过轮询这些按键是否被按下,我们可以根据玩家的输入来更新方块的位置和状态。 同时,我们需要使用定时器来创建一个恒定的时间间隔,用于控制方块的下落速度。一旦经过指定的时间间隔,我们就可以更新方块的位置和检测碰撞,如果方块碰到了底部或其他方块,就需要生成新的方块。 在显示方面,我们可以使用图形库(如STemWin或Tiny Graphics Library)来绘制方块、背景和游戏界面。通过更新屏幕上的像素值来实现动画效果,也可以使用双缓冲技术来减少闪烁。 最后,我们还可以添加一些特效和声音效果,例如方块下落时的声音和消除方块时的爆炸特效,这样可以增加游戏的趣味性。 通过以上步骤,我们可以使用STM32微控制器实现一个完整的俄罗斯方块游戏,使玩家能够在小小的显示屏上尽情享受经典的益智游戏乐趣。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值