...或者我如何根据喜好和喜好发明自己的自行车和艺妓-我从头开始为光敏聚合物打印机编写固件。目前,固件已完全正常运行。
以速卖通上出售的MKS DLP板为基础,制造商提供了电路和固件源代码,我拒绝了,而是赞成从头开始编写所有内容。
文章篇幅很长,所以我决定将其分为两部分。在这一部分中,将提供触摸屏的自制GUI的背景和描述。最后,将提供指向欺凌主题和GitHub存储库的链接。
-第1部分: 1.用户界面。
-第2部分: 2.在USB记忆棒上使用文件系统。3.步进电机控制平台移动。
-第3部分: 4.在背光显示器上显示图层的图像。5.每件事,例如控制灯光和风扇,加载和保存设置等。6.舒适和便利的附加功能。
为了更好地理解,我将为不熟悉光敏聚合物LCD 3D打印机的人做一个简短的描述:
关于大多数“消费型”光敏聚合物打印机工作方式的简短说明
— LCD- ( , 5.5" 25601440 ( — 47.25 ). 405 . , FEP-. , «» . , -. , «» , . . , , . , . , .
背景
我是怎么想到的,为什么我开始编写自己的固件,而不仅仅是自己调整制造商的源代码。
事实证明背景故事很长,所以我将其删除了
5 3D-. , , . FDM-, — Anet A8. - , , . - — - , , . , . - — Anycubic Photon S. , .
, , «» — , . , .., FDM-. , , — 11565 , :) «» , , , . . «» — . , , 20-30 . , — , . .
. , , .. , , , , , . . , (), . , , . - . , 3D- — MKS DLP. : , (5.5", 25601440) (3.5", 480320). — ! , , .
, , . , - , , . . , . … -, CMSIS HAL ST ( STM32F407). -, Marlin 3D. — Marlin 3D — FDM 3D-. 6 , , , G- - . 3 . . — G- . , . , FDM- .
, GUI- , . , , - .
, , «» — , . , .., FDM-. , , — 11565 , :) «» , , , . . «» — . , , 20-30 . , — , . .
. , , .. , , , , , . . , (), . , , . - . , 3D- — MKS DLP. : , (5.5", 25601440) (3.5", 480320). — ! , , .
, , . , - , , . . , . … -, CMSIS HAL ST ( STM32F407). -, Marlin 3D. — Marlin 3D — FDM 3D-. 6 , , , G- - . 3 . . — G- . , . , FDM- .
, GUI- , . , , - .
所以我们有:
- MKS DLP套件,包括:主板,3.5“ 480x320接口显示器和5.5” 2560x1440背光显示器
- 来自制造商的本地资源
- 主板图(无有源组件名称和无源组件标称值)
主板基于STM32F407微控制器。为了控制背光显示,该板包含中国制造商GW1N-LV4LQ144ES的FPGA,SDRAM和两个SSD2828 MIPI接口芯片。微控制器将层的图像驱动到FPGA中,FPGA将其存储在SDRAM中,然后通过SSD2828刷新显示。顺便说一下,制造商未在源代码中提供FPGA配置(固件):(此外,主板还具有:
- 电源输入12-24伏
- USB A /
- A4988
- Z —
- WiFi
- FLASH- W25Q64
- EEPROM- AT24C16
带电阻式触摸屏的界面显示器通过扁平的40针电缆连接。显示控制器-ILI9488,触摸屏控制器-HR2046(与TSC2046类似)。
为了初始化外围设备,我使用了STM32CUBE MX程序。但是我没有直接使用从中获得的结果,而是将必要的片段插入了我的资源中。在使用外设时,我使用了ST的HAL库,在需要获得最大速度的地方,我直接使用了寄存器。
因此,有一个任务-该工具包应该能够使用户轻松地从闪存驱动器中打印文件。我将整个问题大致分为主要部分,最终产生了三篇文章。
我想立即警告您,我的代码和方法都不会假装是理想的,甚至不会是假装的,我会尽力而为。对我来说,编程不是一种职业,而是更多的业余爱好。因此,请不要以过于严格的标准来判断。
1.用户界面
首先是显示的初始化。ILI9488控制器的标准序列在这里没有什么有趣的。我从本机源中删除了它,删去了其他类型显示器的初始化代码(这些源的FDM生命周期中可能还保留着这些代码)。然后我进入字体。
1.1字体
网上有许多用于微控制器的字体库,但是其中绝大多数都使用等宽字体,我不是很喜欢这种字体。这是当所有字符都具有相同的宽度(如字母“ z”)时,即字母“ i”。我曾经为我的一个宠物项目编写了一个比例字体库。它为每种字体使用两个数组-一个包含字符本身的位数据的数组和一个包含每个字符宽度的数组。还有一个带有字体参数的小结构-指向数组的指针,字体高度,字体中的字符数:
typedef struct
{
uint16_t *width;
uint8_t *data;
uint8_t height;
uint16_t symcount;
} LCDUI_FONT;
看起来这样的字体组织应该比仅等距的位图占用更多的存储空间,但这并不是完全正确的。首先,等宽空间本身会导致存储的数据过多。例如,如果在字体中高8像素,宽5像素,1字节(1位宽和8位高)足以满足字母“ i”的要求,那么它仍将占用5字节的数据(5位宽和8位高),以来宽度是固定的。其次,通常,在这种字体中,根据数据的组织方式,在每一行或每一列的字节边界上进行对齐。
例如,采用相同的5x8字体。如果逐行存储位数据,则每行多出3位。或每个字符3个字节:
或7x12字体在列中存储数据,则每列4位或每个字符3.5字节的数据过多:
在我的库中,字符的位数据是连续的,并且字节边界上的对齐仅在字符的末尾。
另外,还有一个小技巧可以使您稍微减少存储的字体大小:一个字符可能没有位数据,但是引用了具有相同样式的另一个字符。例如,西里尔字母“ A”,“ B”,“ E”,“ K”等。可以引用具有相同样式的拉丁字母。这是通过在字符宽度数组中为相应字符的宽度指定一个负值来完成的。如果那里有一个负值,则从位置(宽度* -1)处的字符获取该字符的图像。
以下是在数组中查找字符的过程:
uint8_t* _lcdui_GetCharData(char c)
{
if (c < 32)
return 0;
if (c > 126)
c -= 65;
c -= 32;
if (c >= lcdui_current_font->symcount)
return 0;
uint16_t c1 = lcdui_current_font->width[c];
if (c1 & 0x8000)
c = (c1 & 0x7FFF);
uint16_t ch = lcdui_current_font->height;
int32_t i = 0, ptr = 0, bits = 0, line_bits = ch;
for (i = 0; i < c; i++)
{
if (lcdui_current_font->width[i] & 0x8000)
continue;
bits = lcdui_current_font->width[i] * line_bits;
ptr += bits >> 3;
if (bits & 0x07)
ptr++;
}
return &(lcdui_current_font->data[ptr]);
}
所有这些通常甚至可以增加字体的数据量。更不用说,比例类型看起来更自然。
由于具有窗口输出,因此这种字体的渲染速度相当不错-首先给显示器显示命令,以将输出窗口限制为所需位置的字符大小,然后将整个字符的数据倒入其中。无需为每个像素分别设置坐标。
例如,在下面的照片中,蓝色的文字和顶部的白线是由我的库渲染的,白色的底线是由本地来源的标准arduino类库
渲染的:蓝色的文本比底白线的渲染速度快了好几倍。
同时,我不得不发明一种实用程序来创建字体阵列,以准备从图像中使用程序。在Photoshop中,将使用字体的所有字符创建所需高度的图像,然后将每个字符的最后一列的X坐标手动输入到文本文件中,然后在该图像和此文本文件上设置实用程序。这将创建一个具有所需数组的.c文件。当然有点乏味,但是很简单。
显示文本的过程能够在屏幕末尾将文本换行或根据遇到的换行符将文本换行,可以向左,向右和居中对齐,限制文本不会超出的区域(将被截断)。并且能够显示带有背景画或背景保存的符号。第二个选项的工作速度较慢,因为不再可能将字符数据以一个流的形式填充到显示中,但是仍然足够快,以至于肉眼看不到3-4行的输出。
1.2显示界面图像
对于用户界面,您需要显示图像-背景,图标,按钮。刚开始,我决定不打扰,并将所有.bmp格式的图像存储在板上可用的8MB闪存中。我什至为此写了一个程序。该文件以16位格式(R5 G6 B5)以端到端或端到端的行顺序保存,并且可能已经直接送入渲染例程。但是480x320背景图片的大小超过300 KB。考虑到其中的某些闪存专用于固件更新,因此30个背景图像将占用所有内存。看起来很多,但仍然少于我想要的,以防万一。但是也应该有按钮,图标等。因此,决定将图像转换为某种压缩格式。
压缩选项很少-或多或少很好地压缩图像的所有算法都需要适当的RAM(根据微控制器的标准)或相当长的时间来解压缩。另一方面,应该显示图片,并且要实时展开,并且希望图片在显示时不类似于爬行进度条。因此,我决定使用RLE压缩-1个字节编码重复的次数,其后两个编码-颜色。为此,还编写了一个实用程序,可将.bmp文件转换为以此方式压缩的图像。标头仅包含4个字节-2个字节用于图像的宽度和高度。平均而言,以这种方式将背景图像压缩5-7倍,这在很大程度上取决于单色区域的大小(这是可以预期的)。例如,这样的图片从原来的307 KB缩小为74 KB:
但是,这个-从同一个307到23 KB:
顺便说一句,我的设计师比程序员更烂...
我对这个结果感到满意。解码和显示图像非常快-每个完整的背景图像大约40毫秒。所以我停了下来。
顺便说一句,切换到DMA模式以将数据输出到显示器几乎没有任何输出加速。显示器通过外部16位数据总线作为外部存储器进行连接,但其时序却令人遗憾,几乎抵消了DMA输出优于手动像素输出的优势。
1.3 GUI框架
显示文本,绘制图片,现在是时候考虑如何组织用户界面的基础了。
使用触摸屏,一切都很简单-微控制器通过中断不断轮询触摸屏控制器,并平均最后获得的4个结果,将其转换为显示坐标。因此,随时可以知道传感器的状态-是否按下,如果按下,则在何处。触摸面板和程序主要部分之间的另一层是处理按钮单击的过程,该过程已经在项目之间徘徊了很长时间,针对特定条件进行了少量修改。
这是它如何工作的简要总结。
«». (100-150 ). , «». , . , , «», . , «», «». «», «». - «» «», . ( «»), - . , , .
触摸面板是界面的唯一按钮,除了除了按下按钮外,还分析了单击发生的坐标。
现在需要完成所有工作,以便可以在屏幕上显示各种界面元素,这些元素可能会或可能不会对点击做出反应,由事件进行更新,具有不同的大小和图像等。
最终,我想到了这个方案:界面包含两种主要类型的元素-屏幕和按钮。
屏幕是一种用于按钮的全屏容器。该屏幕具有以下属性:
- 背景图片
- 背景颜色
- 绘制背景的方式-用背景色填充或显示图像
- 标题文本
- 标题文字颜色
- 标头文字字体
- 指向父屏幕的指针(关闭此窗口时将返回该指针)
- 指向按钮的指针数组
- 指向事件过程的指针(在主程序循环中定期调用)
- 指向屏幕绘制例程的指针
屏幕结构
typedef struct
{
void *addparameter;
char *bgimagename;
void *prevscreen;
LNG_STRING_ID name;
TG_RECT nameposition;
TG_TEXTOPTIONS nameoptions;
uint8_t btns_count;
TG_BUTTON *buttons;
LCDUI_FONT_TYPE font;
LCDUI_FONT_TYPE namefont;
uint16_t textcolor;
uint16_t nametextcolor;
uint16_t backcolor;
struct {
paintfunc _callpaint; // repaint screen
processfunc _process; // screen process handling (check for changes, touch pressed, etc)
} funcs;
} TG_SCREEN;
实际上,按钮不仅可以是按钮,还可以是文本,图标,某种变化的元素(例如计数器或时钟)。事实证明,将所有内容组合为一种类型并通过其属性设置每个特定按钮的行为很方便。
按钮属性:
- 屏幕上的坐标
- 背景颜色
- 自由状态的背景图片
- 按下状态的背景图片
- 禁用状态的背景图片
- 活动状态的背景图像(例如,用于单选按钮组的活动元素)
- 渲染方法-图像或背景色
- 按下并释放时是否重画按钮
- 按钮文字
- ( )
- (, )
- ( )
- ,
- ,
typedef struct
{
void *addparameter;
uint8_t button_id;
int8_t group_id; // for swithed options buttons, >0 - single selection from group (select), <0 - multiple selection (switch)
TG_RECT position;
void *parentscreen;
void *childscreen;
char *bgimagename_en;
char *bgimagename_press;
char *bgimagename_dis;
char *bgimagename_act; // for swithed options buttons
LNG_STRING_ID text;
TG_RECT textposition;
LCDUI_FONT_TYPE font;
uint16_t textcolor_en;
uint16_t textcolor_press;
uint16_t textcolor_dis;
uint16_t textcolor_act; // for swithed options buttons
uint16_t backcolor_en;
uint16_t backcolor_press;
uint16_t backcolor_dis;
uint16_t backcolor_act; // for swithed options buttons
struct {
uint8_t active:1; // for swithed options buttons
uint8_t needrepaint:1;
uint8_t pressed:1;
uint8_t disabled:1;
uint8_t repaintonpress:1; // repaint or not when pressed - for indicate pressed state
BGPAINT_TYPE bgpaint:2;
} options;
TG_TEXTOPTIONS textoptions;
struct {
paintfunc _call_paint; // repaint button
pressfunc _call_press; // touch events handling
pressfunc _call_longpress; // touch events handling
processfunc _call_process; // periodical processing (for example text value refresh)
} funcs;
} TG_BUTTON;
借助这组属性,可以基于此类元素在界面中创建几乎所有内容。如果屏幕或按钮的任何一个程序指针都为空,则调用相应的标准程序。例如,可能会有一个特殊的标识符代替按下按钮的过程指针,该标识符指示您需要进入子屏幕或上一个屏幕,然后标准过程将执行该操作。通常,标准过程几乎涵盖使用普通按钮的所有情况,并且仅在非标准情况下(例如,当按钮像时钟一样工作或作为文件列表的元素时),才需要为按钮创建自己的过程。
但是该方案缺乏的功能是带有消息或问题的模态窗口(例如Windows API中的MessageBox),因此我为它们创建了单独的屏幕类型。没有背景图像,其大小由标题或消息本身确定。这些消息可以创建四个版本-使用“是/否”按钮,使用“确定/取消”按钮,使用一个“确定”按钮或根本不使用任何按钮(例如“等待,正在加载数据...”)。
消息窗口的结构
typedef struct
{
MSGBOXTYPE type;
void *prevscreen;
char caption[128];
char text[512];
TG_RECT boxpos;
uint8_t btns_count;
TG_BUTTON buttons[TG_BTN_CNT_MSGBOX];
uint16_t caption_height;
LCDUI_FONT_TYPE font_caption;
LCDUI_FONT_TYPE font_text;
uint16_t text_color;
uint16_t box_backcolor;
uint16_t capt_textcolor;
uint16_t capt_backcolor;
} TG_MSGBOX;
正是基于这三种类型,构建了整个接口,事实证明它非常灵活。现在,所有元素的初始化都可以在固件中严格执行,但是有一种想法可以让用户有机会通过描述配置文件中所有元素的属性并添加一些必要的图片来创建自己的界面。从理论上讲,可以更改不同屏幕的内容-在主屏幕上放置哪些按钮,在服务屏幕上放置哪些按钮,等等。
1.4多国语言
最初使用多种语言。但是起初我走了一条愚蠢的道路-初始化所有元素时,我从语言表中为其分配了文本作为当前元素。切换语言意味着重新初始化所有文本元素,并且当界面中有两个以上的屏幕以及20个以上的按钮和标签时,我意识到我不能再这样生活了。然后,他通过该过程对文本进行了所有引用。给该过程一个文本标识符作为参数,它以当前语言返回指向文本的指针:
char *mshortname = LANG_GetString(LSTR_SHORT_JANUARY);
更改语言时,指针仅从旧语言的文本数组更改为包含新语言的文本数组:
void LANG_SetLanguage(uint8_t lang)
{
lngCurrent = lngLanguages[lang].strings;
return;
}
所有源文本均采用UTF-8编码。我还不得不修改这些编码。文本-UTF-8中的西里尔文文件-Unicode-16中的文本,一些字符串-常规ANSI中的文本。我不想将整套库支持固件中支持多字节编码的功能,因此编写了一些函数来实现从编码到编码的转换以及对具有不同编码的文本的操作,例如,在Unicode16字符串的末尾添加UTF-8字符串。
现在,添加新语言归结为在其中创建文本表并更改常量LNG_LANGS_COUNT的值。的确,如果新语言使用的不是西里尔字母和拉丁字母,则字体仍然存在问题。现在,我在源代码中支持俄语和Google翻译的英语。
1.5存储图像和其他资源
为了存储大量资源,该评估板具有8 MB的SPI闪存W25Q64。最初,我想像往常一样-为Flash中的每个资源设置偏移量,然后将其保存为二进制数据。但是后来我意识到,只要节省的资源数量超过几十个,并且想更改(例如,某些图片按顺序保存第六张),我就可以保证使用此方法可以解决问题。如果其大小增加,则必须转移以下所有资源的地址并重写它们。或者在每个资源之后留出未知大小的备用空间-谁知道任何资源都可以改变。是的,在棺材中我看到了这样的大惊小怪:)因此,我吐口水并在此闪存上整理了一个文件系统。到那时,我已经有一个基于FatFS库的USB文件系统,因此我只要为扇区编写单独的低级读/写功能就足够了。只有一件事使我有点不高兴-该微电路中擦除扇区的大小已经高达4 KB。这首先导致以下事实:文件将占用4 KB的部分空间(文件被写入200字节-占用了4 KB的闪存),其次,每个文件指针结构中的缓冲区将占用相同的4 KB RAM,微控制器不是那么多-192 KB。当然,可以将其中一个变态并编写低级函数,以便它们可以在较小的部分中进行读写,并报告扇区大小(例如512字节)。但这会降低Flash的速度,因此它将扇区大小保留为4KB。因此,任何资源都可以简单地通过其文件名进行访问,这非常方便。例如,目前,存储的资源数量已超过90。而且,我使它们的更新尽可能简单-将更新的(或新的)资源写入某个目录中的USB闪存驱动器,将闪存驱动器插入板中,然后将板重启至服务模式(在打开或重新启动,按住显示屏的右上角),然后自动将该目录中找到的所有文件从USB闪存驱动器复制到SPI闪存。板将重新引导进入服务模式(在开机或重新引导期间,按住显示屏的右上角),并自动将在此目录中找到的所有文件从USB闪存驱动器复制到SPI闪存。板将重新引导进入服务模式(在开机或重新引导期间,按住显示屏的右上角),并自动将在此目录中找到的所有文件从USB闪存驱动器复制到SPI闪存。
未完待续...
也许最庞大的部分出现在界面上。如果本文对社区感兴趣,那么在第二部分中,我将尝试容纳其他所有内容。
好吧,我很乐意提出问题和意见。
-第1部分: 1.用户界面。
-第2部分: 2.在USB闪存驱动器上使用文件系统。3.步进电机控制平台移动。
-第3部分: 4.在背光显示器上显示图层的图像。5.每件事,例如控制灯光和风扇,加载和保存设置等。6.舒适和便利的附加功能。
链接
Aliexpress上的MKS DLP套件
GitHub上制造商的原始固件资源GitHub
上两个版本的板的制造商的方案资源GitHub上的
我的资源