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 進行反饋。
