Marlin是一款开源3D打印机固件,相信自己DIY过打印机的人对这个固件都不会陌生吧!目前市面上的桌面级3D打印机也都或多或少会有他的影子。Marlin的强大之处在于支持多种不同结构的3D打印机(如:xyz直角结构,coreXY、SCARA、三角洲等结构),支持多种硬件电路板,支持多种语言还附加了一些额外功能,如:自动调平等。正是这些强大的功能让Marlin固件的源代码看起来就比较难懂了。
最初接触3D打印是在大学的时候了,当时一直想自己做一台自己的3D打印机。后面接触到了Marlin固件和RAMPS控制板,熟悉过之后便有了将它移植到STM32的想法。之后便开始了对Marlin固件的漫漫学习之路,当时网上关于Marlin的资料还很少,不过就那些很少的资料中还是给到了我很大的帮助,我还加了很多3D打印机DIY的群向群中的各位大虾请教不懂得问题。后面到要做毕设的时候,我就直接找到毕设的老师商量,最终确定让我毕设做3D打印机的题目。最后经过风雨(查阅资料,各种请教,移植的代码,绘制的PCB,搭建机械结构)之后终于见到了彩虹(打印机原型机成功问世)。对于屏幕菜单部分当时也想直接移植Marlin固件的屏幕菜单的结构,当时看到各种宏定义,直接当场崩溃。只能另寻他路参考了一篇12864菜单设计的帖子,参照这个菜单结构完成了打印机的菜单设计。但当时遇到一个问题感觉用这个结构解决不是很好—SD卡中的文件的显示(我不知道SD卡中有多少个文件,如何提前确定当前菜单的条目数呢?)。
最近有些时间,想再看看Marlin固件源码,将之前没有理解的地方再深入了解一下,竟然无意中看到了game的文件,点击进入竟然是游戏的文件,直接把游戏的宏打开下载到RAMPS板中,真的有游戏可以玩。这一下又燃起了我对Marlin固件屏幕这一部分的兴趣。现在再看这些代码,比之前看起来轻松了一些。不过最新版的代码还是抽象性太高不好理解,我就回到了之前的1.0.2-2版本(更接近于C代码)的固件来看。看懂了直接将该部分的代码移植到STM32平台上测试了一下,扩展性上确实要比之前的那套菜单结构要好一些(该菜单结构是基于条目设计的,再将条目封装成菜单。而之前的菜单结构看是基于菜单页设计的)。但这套菜单结构需要单独维护大量的绘制函数和功能函数。不过总体来说这套菜单的结构还是优于之前的结构的。在当前菜单结构下,Marlin固件引入了u8glib(一种单色屏的GUI库),使得屏幕可以显示5行(lcd12864)而且状态菜单可以绘制各种动画效果。这样一看是不是,这就为后面写这种单色屏菜单提供了一种很好的思路呢。
本想这样就可以了,我又突发奇想—是不是可以将Marlin固件拆分成各个组件呢,既可以学习其中涉及到的算法知识又可以为后续写其他代码预留基础何乐而不为呢?
以下部分便是Marlin固件菜单结构的核心代码了。
/* Helper macros for menus */
#define START_MENU() do { \
if (encoderPosition > 0x8000) encoderPosition = 0; \
if (encoderPosition / ENCODER_STEPS_PER_MENU_ITEM < currentMenuViewOffset) currentMenuViewOffset = encoderPosition / ENCODER_STEPS_PER_MENU_ITEM;\
uint8_t _lineNr = currentMenuViewOffset, _menuItemNr; \
bool wasClicked = LCD_CLICKED;\
for(uint8_t _drawLineNr = 0; _drawLineNr < LCD_HEIGHT; _drawLineNr++, _lineNr++) { \
_menuItemNr = 0;
#define MENU_ITEM(type, label, args...) do { \
if (_menuItemNr == _lineNr) { \
if (lcdDrawUpdate) { \
const char* _label_pstr = PSTR(label); \
if ((encoderPosition / ENCODER_STEPS_PER_MENU_ITEM) == _menuItemNr) { \
lcd_implementation_drawmenu_ ## type ## _selected (_drawLineNr, _label_pstr , ## args ); \
}else{\
lcd_implementation_drawmenu_ ## type (_drawLineNr, _label_pstr , ## args ); \
}\
}\
if (wasClicked && (encoderPosition / ENCODER_STEPS_PER_MENU_ITEM) == _menuItemNr) {\
lcd_quick_feedback(); \
menu_action_ ## type ( args ); \
return;\
}\
}\
_menuItemNr++;\
} while(0)
#define MENU_ITEM_DUMMY() do { _menuItemNr++; } while(0)
#define MENU_ITEM_EDIT(type, label, args...) MENU_ITEM(setting_edit_ ## type, label, PSTR(label) , ## args )
#define MENU_ITEM_EDIT_CALLBACK(type, label, args...) MENU_ITEM(setting_edit_callback_ ## type, label, PSTR(label) , ## args )
#define END_MENU() \
if (encoderPosition / ENCODER_STEPS_PER_MENU_ITEM >= _menuItemNr) encoderPosition = _menuItemNr * ENCODER_STEPS_PER_MENU_ITEM - 1; \
if ((uint8_t)(encoderPosition / ENCODER_STEPS_PER_MENU_ITEM) >= currentMenuViewOffset + LCD_HEIGHT) { currentMenuViewOffset = (encoderPosition / ENCODER_STEPS_PER_MENU_ITEM) - LCD_HEIGHT + 1; lcdDrawUpdate = 1; _lineNr = currentMenuViewOffset - 1; _drawLineNr = -1; } \
} } while(0)
程序中涉及到的变量的含义:
encoderPosition //记录了编码器位置的变量(如:编码器左转编码器位置加一,编码器右转编码器位置减一)
ENCODER_STEPS_PER_MENU_ITEM //编码器转几个脉冲对应于菜单一个条目
encoderPosition / ENCODER_STEPS_PER_MENU_ITEM //相当于记录了按键的位置
currentMenuViewOffset //屏幕顶行显示的条目对应于当前所有菜单条目的的偏移量(菜单开始显示的顶行)
_lineNr //当前需要绘制和处理的菜单条目
_menuItemNr //每个菜单中条目的索引
_drawLineNr //LCD显示行的索引(如:0-3)
wasClicked //记录了确认按键时候按下
lcdDrawUpdate //lcd绘制更新的标志位
通过START_MENU的宏函数开始创建菜单,通过MENU_ITEM的宏函数向该菜单中增加条目,通过END_MENJ的宏函数结束当前菜单的创建。MENU_ITEM的宏函数需要调用以下两个函数为为菜单的条目生成绘制和处理代码。
lcd_implementation_drawmenu_[type](sel,row,label,arg...)
menu_action_[type](arg...)
这意味着我们每创建一个条目就要维护这样的两个函数。通过对菜单类型进行划分可以一定程度上减少一部分该工作。对于Marlin固件菜单的类型大致分为以下几类:子菜单、编辑菜单、返回菜单、G代码菜单、功能菜单。
对于Marlin的菜单部分基本理解了上面的菜单结构整个菜单的框架就已经清晰明了了。对于该菜单结构移植到STM32的测试代码可以移步到:https://github.com/Apex-yuan/STM32F103_MARLIN_MENU具体的测试屏幕用的是lcd12864(ST7920)。
对于自动调平功能上述框图中列出的是最新版的Marlin固件所支持的调平方式。自动调平功能的本质原理并不复杂,复杂的是整个调平过正中的流程,这流程是为了确保整个调平过程中的安全。这里先简单对以下两种调平方式简单分析,这两种调平方式要却保打印床是一个平整的平面。
我们根据之前学过的只是,很容易知道3个不在同一直线上的点可以确定一个平面,该方式正是利用的这个原理。
在高中大家应该都会学过线性回归分析的课程,中间用到了最小二乘法的知识,这里其实利用的也是这方面的原理。只不过我们当时使用一系列的平面点来拟合直线,而我们现在是利用一系列的空间点来拟合平面。
对于调平背后的算法而言,如果不深入研究,可以将它视为一个黑箱,只需知道输入和输出即可。