移植micropython最小工程到mm32f3270微控制器
苏勇, 2021-08
Introduction
micropython v1.6发布已经有一段时间, 相比于之前的v1.3, 在内核中增强了一些功能并修复了一些bug, 支持的芯片也多了一些. 特别注意的是, micropython除了对STM32继续投入主要支持资源的同时, 加强了对NXP MIMXRT支持资源, 在MIMXRT移植的micropython大量增加了新的模块.
完成的changelog可见:
https://micropython.org/resources/micropython-ChangeLog.txt
这里面比较让我惊喜的是, 我竟然发现了SAMD的CM0P内核的微控制器. 从changelog上看, 实际在v1.2就已经支持了Microchip SAMD的CM4和CM0, 并且后续并没有继续更新, 但我之前的重点都在CM4F上, 并没有特别关注它. 但目前我手上只有CM3内核的芯片, 这让我不得不再看看CM4F和CM7F之外的移植. 既然CM0P也能支持(实际上circuitpython就是针对CM0P平台的衍生版), 那么CM3从理论上讲也是可行的.
我在之前对micropython的开发经历中, 已经移植过NXP KE18F(CM4F, micropython v1.3)和NXP LPC5500(CM33, micropython v1.4), 这次移植到MindMotion MM32F3270平台, 还是遵循老套路.
初试micropython v1.6
从micropython官网上下载代码包, 而不是从github上clone代码仓库. 这是因为之前的代码仓库中引用了很多submodule, 有一些submodule的地址已经失效了(例如lwip), 所以总是不能下载到完整的代码仓库. 但是从changelog上看, 至少关于lwip的引用地址已经改到了github上的镜像地址, 似乎已经解决了部分无效外链仓库的问题.
在Windows 10上搭建好Linux的工作环境, 参见我之前的文章, https://blog.csdn.net/suyong_yq/article/details/112797556 , 准备好msys2, make, python3, gcc和armgcc编译器.
先编一下mpy-corss, 无误.
再编一下minimal试试. 原本只是手贱, 做个double check, 结果竟然真的报错 !!!
原版代码报错, 还是在python内核里, 我自问目前还没有闲心调python内核. 只能寄希望于内核更新没有同步到minimal工程的makefile里. 但无论如何, 可以用来做最小工程的参考样例少了一个, 同时, 我的心中也飘过一道阴霾, 颇有点出师不利的兆头.
那就再试着编一下bare-arm, 心惊胆战啊. 还好, 编过了.
继续随手编几个工程. stm32的不用编, 那是micropython投入支持资源最多的, 我不想耽误时间, 毕竟编一次micropython还是挺长时间的. 试试mimxrt和samd的, 都没有报错. 终于拉高了点平均分.
我通常会找一个比较简单的工程作为模板开始移植, minimal已经阵亡了, MIMXRT的移植工程里增加了很多模块, 代码也扩充得比较复杂, 不再适合作为模板了. 我特别留意了SAMD的移植工程, SAMD移植的main.c代码结构同MIMXRT很像, 看来除了早期的STM32, 后来增加的移植都已经开始建立开发规范. SAMD移植的文件组织结构也同MIMXRT保持一致, 在ports目录下还有boards文件夹, 然后是各个板子相关的代码. 既然已经有了这样的规范, 那么这次我在移植MM32的时候也尽量遵循规范.
增加MM32的移植
从MM32 SDK中提取ARMGCC版本的MM32F3270的相关源码文件, 包含芯片头文件、启动汇编文件、设备驱动源文件, 以及一个简单的BSP(board_init.c, pin_init.c和clock_init.c). 集成到micropython之前, 先原地build一下, 确保代码无误.
- 向lib目录下新建"mm32"目录, 添加平台级的代码, 包括芯片头文件、启动汇编文件、设备驱动源文件, 同时要为以后新增其它MM32微控制器芯片支持文件预留位置. 并且直接使用lib自带的cmsis目录, 不再另外添加MM32 SDK中的CMSIS.
- 在ports目录下新建"mm32"目录, 在"mm32"目录下创建"boards"目录,
在"boards"目录下创建"MB_F3270"目录.
这里特别讲一下这些文件的功能:
-
"ports/mm32/boards/MB_F3270"目录下的文件(board_init、pin_init、clock_init)直接来自于MM32 SDK样例工程, 主要实现BOARD_Init()函数, 用以对芯片的系统时钟和通信UART串口初始化.
-
新增的"mpconfigboard.h"文件, 是按照micropython的移植规范添加的, 但其实在里面仅仅定义了板子和芯片的名字.
#define MICROPY_HW_BOARD_NAME "MB_F3270"
#define MICROPY_HW_MCU_NAME "MM32F3277G7P"
-
新增的"mpconfigboard.mk"文件, 指定了芯片的系列, 型号, 以及链接命令文件相对于"ports/mm32"目录的位置.
-
"ports/mm32/boards"目录下放置了链接命令文件, 放在这里的意义在于, 它可以被多个使用相同芯片的不同板子引用. 若以后还有不同的芯片, 或者相同芯片的不同链接配置, 都可以放在同级目录下, 供更多的板子引用.
-
“ports/mm32"根目录下的"mpconfigport.h”, 是直接从SAMD的移植中复制过来的,是对py核心的裁剪. 之前的移植都是从minimal或者mimxrt中拿过来,但现在minimal已阵亡,mimxrt也已经变得庞大,不适合用来做最小工程。
-
“ports/mm32"根目录下的"qstrdefsport.h”, 也是直接从SAMD的移植中复制过来的, 这个文件将被用于在build之前传入python脚本用于自动生成关键字清单的, 移植的时候别动它, 让makefile照例处理。这个文件里面实际是空的,但是保存了给后期字符串处理的标签.
// qstrs specific to this port
// *FORMAT-OFF*
- "mphalport.h"和"mphalport.c"顾名思义是micropython系统对硬件平台提出的移植需求. 目前主要在mphalport.c中实现了串口终端到硬件平台的重映射, mphalport.h用内联函数的方式实现定时器的底层函数. 实际上, micropython最小工程中还应该包含一个核心模块utimer, 做定时器用的, 可以在这里简单支持一下(基于Systic即可). 但我为了精简, 竟然连这个模块也给移除了,真的是能简尽简了. 在此处, 对定时器底层的实现都是空的.
"mphalport.c"文件内容如下:
#include "py/runtime.h"
#include "py/stream.h"
#include "py/mphal.h"
#include "board_init.h"
#include "hal_uart.h"
int mp_hal_stdin_rx_chr(void)
{
while ( 0u == (UART_STATUS_RX_DONE & UART_GetStatus(BOARD_DEBUG_UART_PORT)) )
{}
return UART_GetData(BOARD_DEBUG_UART_PORT);
}
void mp_hal_stdout_tx_strn(const char *str, mp_uint_t len)
{
while (len--)
{
while ( 0u == (UART_STATUS_TX_EMPTY & UART_GetStatus(BOARD_DEBUG_UART_PORT)) )
{}
UART_PutData(BOARD_DEBUG_UART_PORT, *str++);
}
}
/* EOF. */
"mphalport.h"内容如下:
static inline mp_uint_t mp_hal_ticks_ms(void) {
return 0;
}
static inline void mp_hal_set_interrupt_char(char c) {
}
- “modmachine.c”, 这个文件里, 是实现micropyhton中"machine"模块的根类, 就是可以import的模块. 原型文件来自于MIMXRT的移植, 但我为了简化, 减少不必要的麻烦, 仅剩下了"machine"类的定义.
#include "py/runtime.h"
#include "py/obj.h"
#include "extmod/machine_mem.h"
//STATIC mp_obj_t machine_reset(void) {
// NVIC_SystemReset();
// return mp_const_none;
//}
//MP_DEFINE_CONST_FUN_OBJ_0(machine_reset_obj, machine_reset);
STATIC mp_obj_t machine_freq(void) {
//return MP_OBJ_NEW_SMALL_INT(CLOCK_GetFreq(kCLOCK_CpuClk));
return MP_OBJ_NEW_SMALL_INT(96000000);
}
MP_DEFINE_CONST_FUN_OBJ_0(machine_freq_obj, machine_freq);
STATIC const mp_rom_map_elem_t machine_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_umachine) },
// { MP_ROM_QSTR(MP_QSTR_reset), MP_ROM_PTR(&machine_reset_obj) },
{ MP_ROM_QSTR(MP_QSTR_freq), MP_ROM_PTR(&machine_freq_obj) },
{ MP_ROM_QSTR(MP_QSTR_mem8), MP_ROM_PTR(&machine_mem8_obj) },
{ MP_ROM_QSTR(MP_QSTR_mem16), MP_ROM_PTR(&machine_mem16_obj) },
{ MP_ROM_QSTR(MP_QSTR_mem32), MP_ROM_PTR(&machine_mem32_obj) },
};
STATIC MP_DEFINE_CONST_DICT(machine_module_globals, machine_module_globals_table);
const mp_obj_module_t mp_module_machine = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t *)&machine_module_globals,
};
-
“main.c"是顶级应用, 程序入口. 之前mimxrt的移植是比较简单的, 但仍有pyexec_frozen_module()函数的调用. 现在看SAMD的移植比较清爽, 所以这次直接从SAMD的移植拿来. 去掉关于tusb的部分, 在main()函数里最开始的位置插入"BOARD_Init()”, 调用函数对具体板子进行初始化.
-
“Makefile”. Makefile是整个移植过程中最耽误时间的, 我曾经花费了很多时间想把makefile改成可视化的keil或者iar工程, 至少是方便阅读的CMakeLists.txt也行啊. 奈何我对Makefile的各种灵活用法实在搞不定, 所以还是老老实实改. SAMD的Makefile写得还是很清爽的, 之前MIMXRT也曾经清爽过, 但现在已经开始变得庞大了. 关于Makefile, 实在需要一整章来说明.
为新移植创建Makefile
以SAMD移植项目下的Makefile作为模板, 改写.
最重要的事情写在前面, 不要手贱把TAB键换成4个空格, Makefile会报错! 我因为这事郁闷了一个晚上, 能想到改回来真tmd是天意.
- 改板子的名字
BOARD ?= MB_F3270
BOARD_DIR ?= boards/$(BOARD)
BUILD ?= build-$(BOARD)
CROSS_COMPILE ?= arm-none-eabi-
# UF2CONV ?= $(TOP)/tools/uf2conv.py
ifeq ($(wildcard $(BOARD_DIR)/.),)
$(error Invalid BOARD specified: $(BOARD_DIR))
endif
- 添加芯片相关源文件的路径引用
MCU_DIR = lib/mm32/$(MCU_SERIES)
Makefile后面好像没有直接指定相对路径的, 总是在前面要加个$(xxx), 我之前曾经试过类似如下的写法, 但总是觉得有点怪怪的, 同时考虑到这个路径可能会在个地方用到, 定义个变量复用字符串也不错.
INC += -Iboards/MB_F3270
这就是最终写的看起来不错并验证正确的做法.
INC += -I$(BOARD_DIR)
另外, 在设定路径的时候, 有时是以micropython的根目录作为相对路径, 有时又以具体移植项目根目录作为相对路径的. 瑞祥随俗吧. 实际上, 这也是micropython的makefile搞得鬼, make会把编号的obj文件放到另外创建的目录中, 这个目录已经不是原始代码所在目录了, 所以后来还导致了用ozone调试elf时看不到同源代码的映射.
- 增加源文件搜索路径
# includepath.
INC += -I.
INC += -I$(TOP)
INC += -I$(BUILD)
INC += -I$(BOARD_DIR)
INC += -I$(TOP)/lib/cmsis/inc
# INC += -I$(TOP)/lib/asf4/$(shell echo $(MCU_SERIES) | tr '[:upper:]' '[:lower:]')/include
INC += -I$(TOP)/$(MCU_DIR)/devices/$(CMSIS_MCU)
INC += -I$(TOP)/$(MCU_DIR)/drivers
# INC += -I$(TOP)/lib/tinyusb/src
- CFLAGS
这是SAMD移植中最良心的地方了. 我一直很头大armgcc编译器的CFLAGS, 这个不像keil或者iar提供可视化的选项, 没有典型配置可以使用, 文档解释又简单难懂. 但是SAMD的makefile同时提供了CM0P和CM4F的FLAGS组合, 我对照着改了一个CM3的版本, 竟然通了, 不可抑制地庆幸, 开心!
# flags.
CFLAGS = $(INC) -Wall -Werror -std=c99 -nostdlib -mthumb $(CFLAGS_MCU_$(MCU_SERIES)) -fsingle-precision-constant -Wdouble-promotion
CFLAGS_MCU_CM3 = -mtune=cortex-m3 -mcpu=cortex-m3 -msoft-float
CFLAGS_MCU_CM0P = -mtune=cortex-m0plus -mcpu=cortex-m0plus -msoft-float
ifeq ($(MCU_SERIES), mm32f3270)
CFLAGS += $(CFLAGS_MCU_CM3)
else
CFLAGS += $(CFLAGS_MCU_CM0P)
endif
CFLAGS += -DMCU_$(MCU_SERIES) -D__$(CMSIS_MCU)__
LDFLAGS = -nostdlib $(addprefix -T,$(LD_FILES)) -Map=$@.map --cref
LIBS = $(shell $(CC) $(CFLAGS) -print-libgcc-file-name)
# Tune for Debugging or Optimization
ifeq ($(DEBUG),1)
CFLAGS += -O0 -ggdb
else
CFLAGS += -Os -DNDEBUG
LDFLAGS += --gc-sections
CFLAGS += -fdata-sections -ffunction-sections
endif
从后面的代码可以看到, micropython可能天然就支持CM3吧, 即使CM4F和CM7F, 甚至是我曾经移植成功过的CM33, 使用的都是gchelper_m3.s文件.
# also use cm3 as gchelper_m3.s
ifeq ($(MCU_SERIES),mm32f3270)
SRC_S = lib/utils/gchelper_m3.s
else
SRC_S = lib/utils/gchelper_m0.s
endif
- 加源文件
略.
- QSTR扫描文件清单
micropython的make在启动编译之前, 会用python脚本扫描源代码, 提取其中的关键字, 生成专门包含关键字的头文件.
后续添加模块是, 必然包含了micropython需要调用的关键字, 一定要在这里加上对应的源文件.
# List of sources for qstr extraction
SRC_QSTR += modmachine.c
- 编译OBJ文件输出路径及规则
这里特别加了大写S后缀的汇编文件的规则, 用了"SRC_SS", 专门为了兼容xxxxx_startup.S文件. 我在早期移植KE18F的时候, 曾经使用过用c代码内嵌汇编的方式, 把S文件改写成c文件, 这样就可以省却此处的麻烦了. 后面抽空可以试试再改一次.
OBJ += $(PY_O)
OBJ += $(addprefix $(BUILD)/, $(SRC_C:.c=.o))
OBJ += $(addprefix $(BUILD)/, $(SRC_S:.s=.o))
OBJ += $(addprefix $(BUILD)/, $(SRC_SS:.S=.o))
- 创建规则
真是良心啊, 不仅仅可以生成elf文件, 把bin和hex的脚本也写好了.
all: $(BUILD)/firmware.bin
$(BUILD)/firmware.elf: $(OBJ)
$(ECHO) "LINK $@"
$(Q)$(LD) $(LDFLAGS) -o $@ $^ $(LIBS)
$(Q)$(SIZE) $@
$(BUILD)/firmware.bin: $(BUILD)/firmware.elf
$(Q)$(OBJCOPY) -O binary $^ $@
$(BUILD)/firmware.hex: $(BUILD)/firmware.elf
$(Q)$(OBJCOPY) -O ihex -R .eeprom $< $@
- 包含其它mk文件
文件结尾的
include $(TOP)/py/mkrules.mk
和文件开始的
include ../../py/mkenv.mk
include $(BOARD_DIR)/mpconfigboard.mk
就这么放着吧, 别乱动.
附件
最后, 附上完整的项目压缩包:
https://download.csdn.net/download/suyong_yq/20969468
还有一个简单的演示视频:
mm32