移植micropython的最小工程到lpc5500微控制器(3) - 调整部分源代码

355 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

移植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()函数。

pyexec_friendly_repl.png

  • 之后就是收尾工作,回收内存,关闭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中使用额外的组件间接初始化硬件的操作。

(未完待续。。。)