我们将Lua解释器嵌入到微控制器(stm32)的项目中



在相当大的应用程序中,项目的重要部分是业务逻辑。 在计算机上调试程序的这一部分,然后将其嵌入到微控制器的项目中是很方便的,因为期望这部分将完全按预期执行,而无需任何调试(理想情况)。

由于大多数用于微控制器的程序都是用C / C ++编写的,因此,出于这些目的,它们通常使用提供低级实体接口的抽象类(如果仅使用C编写项目,则通常使用函数指针结构)。 这种方法提供了所需的抽象级别,但是需要不断地重新编译项目,然后再使用大型二进制固件文件对微控制器的非易失性存储器进行编程,这是一种困扰。

但是,还有另一种方法-使用脚本语言,您可以在设备本身上实时调试业务逻辑,或者直接从外部存储器加载工作脚本,而无需将此代码包含在微控制器固件中。

我选择Lua作为脚本语言。

为什么是卢阿?


您可以在微控制器的项目中嵌入多种脚本语言。 一些简单的类似于BASIC的PyMite,Pawn ...每个都有其优缺点,本文所讨论的问题列表中未包含对其的讨论。

可以在“ 60分钟内的Lua”一文中简要了解lua的优点。 这篇文章给了我很多启发,对于这个问题的更详细的研究,我阅读了罗伯特·杰鲁扎林斯基(Robert Jeruzalimsky)语言“ 编程在Lua中 ”的作者的官方指南(官方俄语版中提供)。

我还要提及eLua项目。 就我而言,我已经有一个现成的软件底层层,可以与微控制器的外围设备和位于设备板上的其他所需外围设备进行交互。 因此,我没有考虑过这个项目(因为它被认为提供了将Lua内核与微控制器的外围设备连接在一起的各个层)。

关于将嵌入Lua的项目


按照传统 ,我的沙盒项目将用作实验领域的质量(链接到已集成lua的提交,并具有以下所述的所有必要改进)。

该项目基于具有1 MB非易失性和192 KB RAM的stm32f405rgt6微控制器(当前使用总容量为128 KB的较旧的2个块)。

该项目具有FreeRTOS实时操作系统,以支持硬件外围基础设施。 用于任务,信号量和其他FreeRTOS对象的所有内存在链接阶段(位于RAM的.bss区域)静态分配。 所有FreeRTOS实体(信号量,队列,任务堆栈等)都是其类的私有区域中全局对象的一部分。 但是,仍然分配了FreeRTOS堆以支持mallocfreecalloc函数(对于诸如printf之类的函数是必需的),这些函数已重新定义以与其一起使用。 有一个改进的API可用于MicroSD(FatFS)卡以及调试UART(115200,8N1)。

关于将Lua用作项目一部分的逻辑


为了调试业务逻辑,假定命令将由UART发送,打包(作为单独的对象)到完成的行中(以字符“ \ n” + 0终止符结束),然后发送到lua机器。 如果执行不成功,则通过printf进行输出(因为该输出先前已包含在项目中)。 调试逻辑后,可以从microSD卡的文件中下载最终的业务逻辑文件(本文材料中未提供)。 同样,出于调试Lua的目的,计算机将在单独的FreeRTOS线程内执行(将来,将为每个调试的业务逻辑脚本分配一个单独的线程,并在其中与环境一起执行该脚本)。

将lua子模块包含在项目中


github上项目官方镜像将用作lua库的源代码(因为我的项目也发布在该库中。您可以直接从官方站点使用源代码)。 由于项目有一个已建立的系统,用于将子模块作为项目的一部分进行组装,因此每个子模块都有单独的CMakeLists,因此我创建了一个单独的子模块 ,其中包含了此fork和CMakeLists以维护单个构建样式。

CMakeLists使用以下子模块编译标志(取自主项目中的子模块配置文件 )将lua存储库的源构建为静态库:

SET(C_COMPILER_FLAGS "-std=gnu99;-fshort-enums;-fno-exceptions;-Wno-type-limits;-ffunction-sections;-fdata-sections;") SET(MODULE_LUA_COMP_FLAGS "-O0;-g3;${C_COMPILER_FLAGS}" 

以及所使用处理器的规范标志(在CMakeLists根目录中设置):

 SET(HARDWARE_FLAGS -mthumb; -mcpu=cortex-m4; -mfloat-abi=hard; -mfpu=fpv4-sp-d16;) 

重要的是要注意需要根CMakeLists指定一个不允许使用双精度值的定义(因为微控制器不支持双精度,只有浮点型):

 add_definitions(-DLUA_32BITS) 

好吧,仅是要通知链接器有关需要汇编此库并将结果包括在最终项目的布局中的信息:

CMakeLists图,用于将项目与lua库链接
 add_subdirectory(${CMAKE_SOURCE_DIR}/bsp/submodules/module_lua) ... target_link_libraries(${PROJECT_NAME}.elf PUBLIC # -Wl,--start-group       #      . #  Lua    ,      #  . "-Wl,--start-group" ..._... MODULE_LUA ..._... "-Wl,--end-group") 

定义用于内存的功能


由于Lua本身不处理内存,因此此责任已转移给用户。 但是,当使用捆绑的lauxlib库和来自其中的luaL_newstate函数时, l_alloc函数将绑定为一个内存系统。 定义如下:

 static void *l_alloc (void *ud, void *ptr, size_t osize, size_t nsize) { (void)ud; (void)osize; /* not used */ if (nsize == 0) { free(ptr); return NULL; } else return realloc(ptr, nsize); } 

如本文开头所提到的,该项目已经覆盖了mallocfree函数,但是没有realloc函数。 我们需要修复它。

在用于FreeRTOS堆的标准机制中,在项目中使用的heap_4.c文件中,没有用于调整先前分配的内存块大小的功能。 在这方面,有必要在mallocfree的基础上实现它。

由于将来可以更改内存分配方案(使用另一个heap_x.c文件),因此决定不使用当前方案的内部空间(heap_4.c),而是进行更高级别的加载。 虽然效果较差。

重要的是要注意, realloc方法不仅删除旧块(如果存在)并创建一个新块,还将数据从旧块移到新块。 此外,如果旧块中的数据比新块中的数据多,则将新块中的旧块填充到极限,然后丢弃其余数据。

如果不考虑这一事实,那么您的机器将能够从“ a = 3 \ n ”行执行三次这样的脚本,之后它将陷入严重故障。 通过研究硬故障处理程序中寄存器的残像,可以解决该问题,从中可以发现在尝试扩展解释器代码及其库的肠道中的表后发生崩溃。 如果您调用诸如“ print'test'”之类的脚本,则该行为将根据固件文件的组装方式而改变(换句话说,该行为未定义)。

为了将数据从旧块复制到新块,我们需要找出旧块的大小。 FreeRTOS heap_4.c(像其他提供堆处理方法的文件一样)没有为此提供API。 因此,您必须完成自己的工作。 作为基础,我采用了vPortFree函数并将其功能切割为以下形式:

VPortGetSizeBlock功能代码
 int vPortGetSizeBlock (void *pv) { uint8_t *puc = (uint8_t *)pv; BlockLink_t *pxLink; if (pv != NULL) { puc -= xHeapStructSize; pxLink = (BlockLink_t *)puc; configASSERT((pxLink->xBlockSize & xBlockAllocatedBit) != 0); configASSERT(pxLink->pxNextFreeBlock == NULL); return pxLink->xBlockSize & ~xBlockAllocatedBit; } return 0; } 

现在很小了,根据mallocfreevPortGetSizeBlock编写realloc

基于malloc,free和vPortGetSizeBlock重新分配实现代码
 void *realloc (void *ptr, size_t new_size) { if (ptr == nullptr) { return malloc(new_size); } void* p = malloc(new_size); if (p == nullptr) { return p; } size_t old_size = vPortGetSizeBlock(ptr); size_t cpy_len = (new_size < old_size)?new_size:old_size; memcpy(p, ptr, cpy_len); free(ptr); return p; } 

添加对使用stdout的支持


从官方描述中可以知道,lua解释器本身无法使用I / O。 为此,连接了一个标准库。 对于输出,它使用stdout流。 标准库中的luaopen_io函数负责连接到流。 为了支持使用stdout (与printf不同),您将需要覆盖fwrite函数。 我根据上一篇文章中描述的功能重新定义了它。

Fwrite功能
 size_t fwrite(const void *buf, size_t size, size_t count, FILE *stream) { stream = stream; size_t len = size * count; const char *s = reinterpret_cast<const char*>(buf); for (size_t i = 0; i < len; i++) { if (_write_char((s[i])) != 0) { return -1; } } return len; } 

如果没有定义,lua中的打印功能将成功执行,但是将没有输出。 此外,机器的Lua堆栈上不会有任何错误(因为该函数已正式成功执行)。

除了此功能外,我们还将需要fflush功能( 要使交互模式起作用,将在后面讨论 )。 由于无法覆盖此功能,因此您将不得不对其稍加命名。 该函数是fwrite函数的精简版本,旨在发送缓冲区中现在包含的内容并进行后续清理(无需额外的回车)。

Mc_fflush函数
 int mc_fflush () { uint32_t len = buf_p; buf_p = 0; if (uart_1.tx(tx_buf, len, 100) != mc_interfaces::res::ok) { errno = EIO; return -1; } return 0; } 


从串行端口检索字符串


为了获得lua机器的字符串,我决定编写一个简单的uart-terminal类,该类:

  • 在串行端口上逐字节接收数据(在中断中);
  • 将接收到的字节添加到队列中,流从该队列中接收它;
  • 如果不是换行,则以字节流的形式发送回它到达的形式;
  • 如果换行已到达(' \ r '),则发送2个字节的终端回车(“ \ n \ r ”);
  • 发送响应后,将调用到达的字节的处理程序(行布局对象);
  • 控制按下删除字符键(以避免从终端窗口删除服务字符);

链接到资源:


另外,我注意到,为了使该对象正常工作,您需要在允许的范围内为UART中断分配优先级,以便使用来自中断的FreeRTOS功能。 否则,您会得到有趣的难以调试的错误。 在当前示例中,在FreeRTOSConfig.h文件中设置了以下用于中断的选项。

FreeRTOSConfig.h中的设置
 #define configPRIO_BITS 4 #define configKERNEL_INTERRUPT_PRIORITY 0XF0 //   FreeRTOS API   //   0x8 - 0xF. #define configMAX_SYSCALL_INTERRUPT_PRIORITY 0x80 

在项目本身中,类nvic的对象设置了中断0x9的优先级,该优先级包含在有效范围内(类nvic 在此处此处描述)。

Lua机器的弦形成


从uart_terminal对象收到的字节被传输到简单类serial_cli的实例,该类提供了用于编辑字符串并将其直接传输到执行lua机器的线程的最小接口(通过调用回调函数)。 接受字符“ \ r”后,将调用回调函数。 此函数应向其自身复制一条线并“释放”控制权(因为在调用过程中阻止了新字节的接收。这对于正确区分优先级的流和足够低的UART速度而言不是问题)。

链接到资源:


重要的是要注意,此类认为长度超过255个字符的字符串无效,并将其丢弃。 这是有意的,因为lua解释器允许您逐行输入构造,等待块的结尾。

将字符串传递给Lua解释器及其执行


Lua解释器本身不知道如何逐行接受块代码,然后自己执行整个块。 但是,如果在计算机上安装Lua并以交互方式运行解释器,我们可以看到执行过程是逐行的,键入时带有相应的符号,表示该块尚未完成。 由于交互模式是标准软件包中提供的,因此我们可以看到其代码。 它位于lua.c文件中。 我们对doREPL函数及其使用的一切感兴趣。 为了不拿自行车,要在项目中获得交互模式功能,我在单独的类中创建了此代码的端口,我用原始函数的名称命名为lua_repl ,该函数使用printf将信息打印到控制台,并具有公共方法add_lua_string来添加从类对象接收的行上述的serial_cli。

参考文献:


该类根据单例Myers模式进行,因为不需要在同一设备内提供几种交互模式。 lua_repl类的对象在此处从serial_cli类的对象接收数据。

由于项目已经具有用于初始化和服务全局对象的统一系统,因此将指向lua_repl类的对象的指针传递给全局类player :: base的对象。 在class player :: base类的对象的start方法中( 在此声明。也从main中调用),以FreeRTOS 3任务的优先级调用lua_repl类的对象的init方法(在项目中,您可以将任务的优先级从1分配到4。其中1是最低的优先级,而4是最高的)。 成功初始化之后,全局类将启动FreeRTOS调度程序,并且交互模式将开始其工作。

移植问题


下面是在机器的Lua端口期间遇到的问题的列表。

执行2-3个变量分配的单行脚本,然后一切都会陷入严重故障


问题出在realloc方法上。 不仅需要重新选择块,而且还需要复制旧块的内容(如上所述)。

尝试打印值时,解释器会陷入严重错误


发现问题已经更加困难,但是最后我设法找出snprintf用于打印。 由于lua将值存储为double(在我们的情况下为float),所以需要带浮点支持的printf(及其派生类)(我在这里写过printf的复杂性)。

非易失性(闪存)存储器的要求


这是我进行的一些测量,目的是判断将Lua机器集成到项目中需要分配多少非易失性(闪存)内存。 使用gcc-arm-none-eabi-8-2018-q4-major进行编译。 使用了Lua 5.4版本。 在下面的测量中,短语“无Lua”表示不包括解释器及其与其库的交互方法以及该项目中lua_repl类的对象。 所有低级实体(包括printffwrite函数的替代)都保留在项目中。 FreeRTOS堆大小为1024 * 25字节。 其余的则由全球项目实体占用。

结果汇总表如下(所有大小以字节为单位):
构建选项没有lua仅核心带有基础库的Lua带有库库,协程,表,字符串的LualuaL_openlibs
-O0 -g3103028220924236124262652308372
-O1 -g374940144732156916174452213068
-Os -g071172134228145756161428198400

RAM要求


由于RAM的消耗完全取决于任务,因此在打开具有不同库集的计算机后,我将立即给出消耗的内存的摘要表(它通过print显示(collect(垃圾(“ count”)* 1024)命令))。
组成使用的RAM
带有基础库的Lua4809
带有库库,协程,表,字符串的Lua6407
luaL_openlibs12769

在使用所有库的情况下,与以前的库相比,所需RAM的大小显着增加。 但是,没有必要在相当一部分应用程序中使用它。

另外,还将4 kb分配给执行Lua机器的任务堆栈。

进一步使用


为了在项目中充分利用机器,您将需要进一步描述项目的硬件或服务对象的业务逻辑代码所需的所有接口。 但是,这是另一篇文章的主题。

总结


本文介绍了如何将Lua机器连接到用于微控制器的项目,以及如何启动功能全面的交互式解释器,使您可以直接从终端的命令行尝试业务逻辑。 此外,针对Lua机器的不同配置,考虑了对微控制器硬件的要求。

Source: https://habr.com/ru/post/zh-CN459602/


All Articles