为 STM32 移植 Berry 脚本语言
Posted skiars
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为 STM32 移植 Berry 脚本语言相关的知识,希望对你有一定的参考价值。
Berry 是我为单片机设计的一款脚本语言,该语言具有资源占用小、平台无关、执行速度快和易于掌握等优点。在单片机上使用脚本语言可以提高单片机的二次开发能力以及调试效率,同时也是一种比较新颖的玩法。本教程将简要介绍在 STM32F103RBT6 单片机上移植 Berry 脚本语言的方法。教程的末尾给出了移植完成的示例工程,读者可以根据本教程的内容和示例工程完成自己的移植工作。
我使用 ST 推出的 CubeMX 软件进行单片机固件库的配置,选择用 CubeMX 生成 HAL 库工程而不用标准库是考虑到以下因素:
- 不必编写底层外设的驱动代码,减少工作量
- 方便后续支持更多的 STM32 型号
- 方便生成各种开发环境的工程
这只是一个简单的例子,只需要使用 CubeMX 建立一个基础的工程并进行少量的配置。开始本教程之前,读者需要先安装 CubeMX 软件和 STM32CubeF1 支持包,然后我们就可以开始建立工程了。
基础配置
打开 CubeMX 后,点击菜单栏中的 New -> NewProjext 来启动工程配置向导。按下图进行配置:
建立工程后将进入类似下图的界面(这是最终配置好的工程)
打开 Project Manager选项卡,我们进行以下配置:
这个界面用于进行工程的配置,除了工程名和路径等基本信息,我们需要注意 Toolchain/IDE 和 Linker Settings 中堆栈大小的设置。这里我选择了比较常用的 MDK-ARM V5 作为目标 IDE。对于堆栈大小,建议最小堆容量(Minimum Heap Size)不低于 4KB(0x1000),而最小栈容量不低于 2KB(0x800)。
读者可根据实际情况进行时钟配置,即使不进行任何配置也可以正常使用(将使用内部的 HSI 时钟源,且主频只有 8MHz)。
最后我们需要配置一个串口以方便运行脚本的交互模式。串口外设在 Pinout & Configuration 选项卡下的 Connectivity 目录中,我需要使用 USART1 进行通信,这里就只对它进行配置:
到此,基本的配置工作就完成了,点击 GENERATE CODE 按钮就可以生成 Keil MDK 的工程,接下来的移植工作将在 MDK 工程中进行。
移植 Berry
准备文件
目前项目的目录结构如下所示:
首先到 GitHub 中下载 Berry 的源代码并进行编译(这需要电脑上安装 GCC 工具链并执行 make prebuild
命令,如果没有的话读者可以直接使用文末我已经移植好的工程),该过程是为了生成需要自动生成的代码。完成之后我们需要进行以下操作:
- 将整个 berry 文件夹移动到 stm32f103rb_berry 文件夹下,该文件夹中包含了 Berry 解释器的核心代码
- 将 berry/generate 文件夹移动到 stm32f103rb_berry 文件夹下,这是在使用
make prebuild
命令时由 berry/tools/map_build/map_build.exe 工具自动生成的代码,文末的参考工程也给出了这些代码 - 将 berry/default 文件夹中的源文件和头文件分别移动到 Src 文件夹和 Inc 文件夹下,该文件夹包含了一个 Berry 交互式解释器的默认实现,后面我们将通过调用它的主函数来运行该解释器
Berry 解释器需要从一种输入设备中读取字符流输入,在 PC 上可以使用 C 标准库中的 fgets()
函数或者 GNU/Readline 库中的 readline()
函数。本教程中使用 STM32 的 USART1 进行字符流传输,因此要实现基于串口的 readline()
函数。参考工程中的 stm32f103rb_berry/Src/readline.c 和 stm32f103rb_berry/Inc/readline.h 文件即用于实现该功能。从 readline.h 中我们可以看到一些公共的函数:
// 向输入队列中放入一个字符
// 该函数在串口接收中断服务函数中调用,实参为串口收到的字符
int queue_putchar(int ch);
// 从输入设备(串口)中读取一个字符串
// 参数 prompt 为导言字符串,导言会在开始接收字符流之前输出
// 返回值为接收到的一行字符串,如果没有接收到 '\\r' 或 '\\n' 则该函数会一直等待
const char* readline(const char *prompt);
// 从输入设备中(串口)读取一个字符,如果没有接收到字符则该函数会一直等待
int readchar(void);
现在我们将得到以下文件结构:
打开 MDK-ARM 目录下的 Keil 工程进行下一部的移植。在 Project 窗口下工程的根目录的右键菜单中打开 Manage Project Items 对话框,新建一个名为 berry 的 Group,然后将 stm32f103rb_berry/berry/src 中的所有源文件加入该分组。同时将刚才新加入 stm32f103rb_berry/Src 文件夹中的源文件加入 Application/User 分组。
MDK 工程配置
到此为止,源文件的配置就完成了。我们还要到工程的 Options 中将 ../berry/src 路径加入到 Include Paths 并勾选 C99 Mode:
注意,除非需要调试代码,建议将优化选项(Optimize)开到 O2 或更高以减少代码体积并提高运行速度。
源码修改
Applicatio/User 下的 berry.c 包含了一个 Berry 的交互式解释器的入口,不过其主函数名确是 main
,这里需要将其改掉以避免和 STM32 工程的 main
函数冲突:
int berry_main(int argc, char *argv[])
// ...
将 REPL 的字符串输入函数修改为 readline
:
// ...
#include "readline.h"
// ...
static int analysis_args(bvm *vm)
// ...
if (args & arg_i) /* enter the REPL mode */
return be_repl(vm, readline);
return 0;
在 main.c 中修改 main()
函数以启动解释器:
int main(void)
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
berry_main(0, NULL); /* ADD: start berry interpreter */
while (1);
串口通信支持
为了让 readline()
函数能接收到字符,我们需要对串口中断服务函数进行修改,该函数在 stm32f1xx_it.c 文件中,以下是修改后的中断服务函数:
// ...
#include "readline.h"
// ...
void USART1_IRQHandler(void)
/* USER CODE BEGIN USART1_IRQn 0 */
if ((USART1->SR & USART_SR_RXNE) != RESET)
USART1->SR &= ~USART_SR_RXNE;
queue_putchar(USART1->DR);
return;
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
// ...
为了让 Berry 解释器的输入输出重定向到 USART1,需要修改 be_port.c 文件中的 be_writebuffer
函数和 be_readstring
函数:
// ...
#include "stm32f1xx_hal.h"
/* USART 字符输出函数 */
static void usart_putchar(USART_TypeDef *USARTx, int ch)
USARTx->DR = (uint16_t)(ch & 0x01FF);
while (!(USARTx->SR & UART_FLAG_TXE));
static int usart_putc(int ch)
if (ch == '\\n') /* 将 '\\n' 转换为 "\\r\\n" */
usart_putchar(USART1, '\\r');
usart_putchar(USART1, ch);
return ch;
void be_writebuffer(const char *buffer, size_t length)
while (length--)
usart_putc(*buffer++);
char* be_readstring(char* buffer, size_t size)
return be_fgets(stdin, buffer, (int)size);
Berry 解释器配置
berry_conf.h 是 Berry 解释器的配置文件,默认的配置文件是为运行在 PC 上的解释器设计的,它并不适合于资源受限的嵌入式系统,为此我们需要对该文件中定义的宏定义进行修改:
#define BE_SINGLE_FLOAT 1 // 使用单精度浮点数
#define BE_INTGER_TYPE 0 // 使用 int 类型作为整数类型
#define BE_DEBUG_RUNTIME_INFO 2 // 使用内存占用较少的调试信息
#define BE_STACK_TOTAL_MAX 100 // 最大堆栈数量无需太大
// 关闭所有模块,如果读者需要可以根据需要打开部分模块
#define BE_USE_STRING_MODULE 0
#define BE_USE_JSON_MODULE 0
#define BE_USE_MATH_MODULE 0
#define BE_USE_TIME_MODULE 0
#define BE_USE_OS_MODULE 0
// 部分系统相关的标准库函数定义,注意单片机中实际上一般没有 abort 和 exit 函数的实现
#define BE_EXPLICIT_ABORT abort
#define BE_EXPLICIT_EXIT (void)
#define BE_EXPLICIT_MALLOC malloc
#define BE_EXPLICIT_FREE free
#define BE_EXPLICIT_REALLOC realloc
编译及运行
到此,对工程进行编译并下载到一块开发板将可以正常运行。将开发板使用串口(波特率为 115200bps)连接到 PC 后就可以使用 Putty、SecureCRT 等终端模拟工具来使用运行在单片机中的 Berry 解释器了。
直接使用键盘来输入脚本代码,按下回车后将会执行并输出结果:
Berry 0.1.1 (build in Jul 29 2019, 21:38:36)
[ARMCC] on STM32 (default)
> print('Hello World!')
Hello World!
> 100 + 4 * 10
140
>
示例工程
示例工程的百度网盘链接: https://pan.baidu.com/s/1vfndyNaHJLsNvPeMlOFPQw,提取码: hxri 。
后续
本篇教程只涉及简单的移植,并没有包含单片机外设的支持,这些内容会在后续的教程中给出。另外,以后的示例将迁移到 STM32F407VET6 上,而底层外设的驱动依然由 CubeMX 来生成。
我还会提供更多关于 Berry 的资料并完善语言文档。如果有任何需求或者问题,读者可以通过留言、邮箱或者 GitHub 进行反馈。
以上是关于为 STM32 移植 Berry 脚本语言的主要内容,如果未能解决你的问题,请参考以下文章