本文已参与「新人创作礼」活动,一起开启掘金创作之路。
移植micropython的最小工程到lpc5500微控制器(3) - 调整部分源代码
调整必要的代码
在新移植的最小工程中,仅仅实现用户能通过REPL与MicroPython内核进行通信。
这个过程涉及到:
- main.c文件中的main()函数。这是整个应用程序的入口。
但实际上,minimal工程中演示的程序入口是Reset_Handler()复位向量。按照我的理解,这可能跟armgcc编译环境的使用有关。armgcc编译工具链同商业编译器iar或者keil不同,没有将硬件复位之后初始化运行环境(有点像Java的JRE)封装到自己的库中,而是需要用户在代码中显式地执行一些对运行环境的初始化操作,例如在minimal工程中演示的,需要将bss内存段的内容清零,需要将data内存段的内容赋初值,之后像调用正常函数一样,在c语言代码中调用main()函数,进入到用户熟悉的编程环境。不过我在移植过程中使用了SDK的启动代码文件“startup_LPC55S69_cm33_core0.S",这里面的Reset_Handler()函数已经帮助用户完成了相关的初始化操作。在Reset_Handler()函数的最后一个操作,就是跳转到main()函数。
#ifndef __START
#define __START _start
#endif
#ifndef __ATOLLIC__
ldr r0,=__START
blx r0
#else
ldr r0,=__libc_init_array
blx r0
ldr r0,=main
bx r0
#endif
- board_init.c文件中的board_init()函数。这个函数是进入main()之后首先执行的函数,用于实现对硬件的初始化操作。默认用于交互通信的UART串口也是在board_init()函数中完成初始化的,并且相关的对接函数也被整合在board_init.c文件中。而在minimal工程中的uart_core.c文件和原main.c文件中关于初始化串口的代码就不再使用了,可以删掉。
整理main()函数
目前在最小的移植工程中,我需要实现的仅仅是一个通过串口跟micropython的终端进行交互通信,并能够成功运行micropython的内核。
实际上,这里有三个版本的main()函数可以让我参考,分别是功能最完整的stm32移植、minimal工程和mimxrt工程。我一开始的思路是结合stm32和minimal编写main()函数,但是后来通过阅读代码发现,这两个工程的代码都比较老,相比而言,较新的mimxrt移植代码更清晰一些,至少是关于堆和栈的初始化以及对micropython的初始化过程更加明确地体现在代码中。因此,最后我在整理main()函数的时候更多地参考了mimxrt的移植。
这里把我最终完成的main()函数代码列在这里:
extern uint8_t _sstack, _estack, _gc_heap_start, _gc_heap_end;
int main(void)
{
board_init();
mp_stack_set_top(&_estack);
mp_stack_set_limit(&_estack - &_sstack - 1024);
for (;;) {
gc_init(&_gc_heap_start, &_gc_heap_end);
mp_init();
mp_obj_list_init(MP_OBJ_TO_PTR(mp_sys_path), 0);
mp_obj_list_append(mp_sys_path, MP_OBJ_NEW_QSTR(MP_QSTR_));
mp_obj_list_init(MP_OBJ_TO_PTR(mp_sys_argv), 0);
for (;;) {
if (pyexec_mode_kind == PYEXEC_MODE_RAW_REPL) {
if (pyexec_raw_repl() != 0) {
break;
}
} else {
if (pyexec_friendly_repl() != 0) {
break;
}
}
}
mp_printf(MP_PYTHON_PRINTER, "MPY: soft reboot\n");
gc_sweep_all();
mp_deinit();
}
return 0;
}
这份代码比较清晰地展现了micropython的启动和执行过程:
-
进入main()函数之前先调用board_init()函数对硬件初始化,所有对硬件的准备工作都将放在这个函数里。我在项目根目录下单独创建了board_init.c文件用于存放board_init()函数及相关的硬件初始化函数的实现。
-
使用mp_stack_set_top()和mp_stack_set_limit()初始化micropython使用的栈,如前文所述。
-
使用gc_init()初始化micropython使用的堆,如前文所述。
这里有一个有趣的设计,使用了一个for循环重复执行micropython的主线程,并且把初始化堆和释放堆的操作也放在循环中。这实际是虚拟了一个多线程(但不是并行)的运行环境。这样的好处是,当micropython运行过程中因为资源限制或者其它操作(人为exit)退出时,代码将通过循环重新初始化堆并启用一个新的micropython线程。实际上,创作本文前半部之后停更的两周时间里,我再次阅读了micropython官网上提供的移植指南,发现最新版移植指南建议的main()函数实现方式同我最终版的main()几乎是相同的,但是移植指南文档里main()函数实现比较简单,没有这个for循环,如果当前的micropython崩溃了,那就只好复位硬件重新开始了。
-
mp_init()顾名思义就是初始化micropython的。
-
两个mp_obj_list_init()夹着一个mp_obj_list_append()函数的功能暂时没搞明白,但我看现有的很多移植都是这么用,就先放在这里了。
-
接下来就是启动命令行交互系统,“pyexec”前缀是“python execution”的意思,就是开始运行点什么东西了。这个地方出现了很多“pyexec”前缀的函数和变量,它们的定义和实现可以在“py/pyexec.c”文件中找到。我后来通过单步调试运行发现实际执行的是pyexec_friendly_repl()函数。

- 之后就是收尾工作,回收内存,关闭micropython主线程等。
对接硬件UART串口驱动
原有的“minimal”工程中有一个“uart_core.c”的源文件,其中实现了mp_hal_stdin_rx_chr()和mp_hal_stdout_tx_strn()两个函数,实现了micropython的命令行交互工具REPL对基本数据收发的通信接口。通过在这两个函数内部相应地实现UART串口的收发操作,从而能够将REPL对接到UART串口上,进一步通过PC机上的终端窗口通过串口同micropython交互。
实际上,我在新移植的工程下面另外创建了board_init.c文件,在这个文件中实现的board_init()函数中,实现了对硬件相关模块的初始工作,同时也包含了对用于REPL通信的UART外设,并将这两个需要对接的函数也放在其中。
void BOARD_InitDebugConsole(void)
{
/* attach 12 MHz clock to FLEXCOMM0 (debug console) */
CLOCK_AttachClk(BOARD_DEBUG_UART_CLK_ATTACH);
RESET_ClearPeripheralReset(BOARD_DEBUG_UART_RST);
usart_config_t usart_config;
USART_GetDefaultConfig(&usart_config);
usart_config.baudRate_Bps = 115200;
usart_config.enableTx = true;
usart_config.enableRx = true;
USART_Init(BOARD_DEBUG_UART_INSTANCE, &usart_config, BOARD_DEBUG_UART_CLK_FREQ);
}
int mp_hal_stdin_rx_chr(void)
{
unsigned char c = 0;
USART_ReadBlocking(BOARD_DEBUG_UART_INSTANCE, &c, 1u);
return c;
}
// Send string of given length
void mp_hal_stdout_tx_strn(const char *str, mp_uint_t len)
{
USART_WriteBlocking(BOARD_DEBUG_UART_INSTANCE, (const uint8_t *)str, len);
}
从代码中可以看到,我这在这里直接使用了NXP MCUX SDK中的UART驱动程序完成了对硬件UART模块的操作。
初始化MCU硬件的board_init()函数
在描述main()函数的调用序列和实现UART串口对接REPL的过程中,都提到了board_init.c文件和board_init()函数。这里集中说明一下board_init()函数的作用。
在进入main()函数后,首先调用board_init()函数执行对硬件的初始化,包括对芯片时钟系统的初始化,后续可能使用到的端口引脚,以及串口终端等。这里面大部分的函数都可以从NXP MCUX SDK中的样例工程中获取源代码。
例如,在本次移植中使用的BOARD_InitPins()和BOARD_BootClockFROHF96M()函数,就分别来自于样例工程“hello_world”的pin_mux.c和clock_config.c文件。而BOARD_InitDebugConsole()函数名也是从board.c文件中借用的,只是其中的内容是直接调用UART驱动程序对UART串口通信模块进行初始化,而没有遵循SDK中使用额外的组件间接初始化硬件的操作。
(未完待续。。。)