第二十届全国大学生智能汽车竞赛报名系统开放那天,我在群里跟队友说:要不这届我们用MicroPython试试?群里沉默了三秒,然后有人回了一句:“RT1021能跑MicroPython?”我知道他真正想问的是另一个问题——“放着好好的C不用,为什么非要在恩智浦RT1021上折腾Python?”
当时我没有立刻回答。因为严格来说,这个问题我的确没底。i.MX RT1021是NXP的跨界处理器,Cortex-M7内核、500MHz主频、带FPU和DSP,属于那种“跑起来比普通MCU猛得多、但又不像Linux SoC那么复杂”的芯片。MicroPython是解释型脚本语言在单片机上的实现,主打快速迭代。这两者放在一起,听上去就有点“拧巴”。但第二十届极速光电组拼的早就不只是纯代码性能了,还拼一圈代码烧进板子要多久、调一个参数要多少次编译、半夜三点把车从赛道上拎回来之后能不能用十秒钟改完策略再来一发。C当然可以做到这些,但MicroPython在这些场景里几乎是降维打击。
这篇文章,就是把我从“RT1021能不能跑MicroPython”到真正把赛车跑起来的完整过程、选型逻辑、外设编程姿势和踩坑记录写下来。如果你正在备赛,或者对手头这块芯片动过类似心思,这篇应该能帮你省下不少试错时间。
1. 在RT1021上跑MicroPython,值不值得折腾
1.1 极速光电组对主控的真实需求
极速光电组这名字听起来唬人,但说白了就是“车要快、路径要认准、控制要稳”。这类车对主控的要求基本可以拆成四块:必须有足够算力跑图像或传感器融合算法、要有足够多的定时器/PWM/编码器接口伺候电机和轮速传感器、中断响应要够快、开发迭代效率要高。恩智浦RT1021在这四块里的表现其实很突出,它不是那种大而全的应用处理器,而是把Cortex-M7拉到500MHz,再配上DSP指令集和FPU,专为“要性能又不想上Linux”的场景准备的。
和常用的STM32F4、TC264这些芯片相比,RT1021最大的优势就是主频和浮点运算能力。它跑数学运算时FPU能帮忙做不少事,这对极速光电组里常见的PID控制、图像灰度处理、弯道曲率估算都很有用。而它又保留了MCU的确定性——中断延迟、外设控制、启动时间都是可以预期的,不像跑Linux的板子那样换个系统负载就得担心调度抖动。
但这里有个很现实的问题:在第二十届这种级别的比赛里,C语言几乎垄断了RT1021的技术方案。你去网上搜,能找到的全是MCUXpresso IDE、SDK例程、寄存器配置。想找个现成的MicroPython工程?几乎没有。所以当时我们面临的其实是一场赌注:赌MicroPython的迭代效率能抵消它在实时性上的不足,同时还要自己把社区生态里缺失的那几块拼图补上。
1.2 MicroPython带来的时间收益到底有多大
我说一个特别直观的对比。C开发跑智能车的日常是这样的:改一个PID参数,重新编译,烧录,等板子重启,看现象。如果现象不对,再改,再编译,再烧录。一次编译几十秒到几分钟不等,烧录和重启又是十几秒。哪怕一切顺利,一晚上调二十版参数就已经很高效了。而且C代码一旦跑飞,你还要接调试器看寄存器、看调用栈、找各种诡异的内存问题,一晚可能就没了。
MicroPython完全不同。代码通过串口REPL或者文件系统直接上传,Python文件在板子上就是文本,没有编译过程。你可以在赛车运行过程中,通过REPL改一个PWM占空比,立刻观察电机反应;可以把赛道元素识别逻辑写成Python函数,直接在命令行里喂几帧图像测输出。最夸张的一次,我在场地边用手机连上串口蓝牙模块,直接在REPL里把入弯减速点往前挪了半米,全程没有拔过一块板子,大概不到一分钟。
这种效率对齐到整个备赛周期,省下的时间是巨大的。毕竟智能车竞赛拼的不只是谁的算法先进,还拼谁有更多轮次去跑赛道、收集数据、修正bug。MicroPython把“代码改动->上赛道验证”的闭环压缩到了以秒为单位,这在赛前三天作用尤为致命。
1.3 得先把丑话说在前面:Python不是万能钥匙
但如果你以为MicroPython能包打一切,那后面八成要翻车。解释型语言在实时控制上有天然短板:字节码解释执行比原生机器码慢一个量级、垃圾回收会带来不确定的停顿、Python层的函数调用开销在某些高频中断里没法接受。
我自己划分过一个“什么场景能用Python、什么场景不能用”的清单,后来基本成了整个项目的红线:
| 场景 | 是否适合MicroPython | 原因 |
|---|---|---|
| 主控制循环(低频策略、状态机) | 很适合 | 逻辑复杂、改动频繁,解释器开销可接受 |
| 高速PID实时运算(1kHz以上) | 勉强适合 | 简单计算可以,复杂公式建议用viper或C扩展 |
| 摄像头图像逐行处理 | 不适合 | 数据量大、循环密集,Python层撑不住 |
| 编码器高频计数、电机堵转检测 | 不太适合 | 依赖定时器中断和寄存器操作,Python回调有延迟 |
| 启动初始化、外设自检 | 很适合 | 逻辑多、跑一次就行,Python开发最快 |
| 与传感器通过UART/SPI通信 | 很适合 | 主要瓶颈在IO速度,MicroPython封装很成熟 |
我刚上手的第一周,几乎每天都在“真香”和“想换回C”之间反复横跳。后来想明白了:MicroPython不是用来替代C的,而是用来替代“C里那些跟硬件无关、纯粹写逻辑”的部分。把实时要求高的放到底层,把策略迭代放到Python层,这才是RT1021上MicroPython的正确打开方式。
2. 固件从零到一:mimxrt移植版编译与烧录
2.1 先确认芯片:RT1021的硬件编程入口
如果你用过STM32,再来看i.MX RT1021,第一感觉是“这芯片怎么这么像单片机又这么不像单片机”。它没有内部Flash,程序放在外部QSPI Flash里,上电后由BootROM通过FlexSPI接口把代码映射到0x60000000地址直接XIP执行。这种架构的好处是Flash大小和成本很好控制,坏处是如果外部Flash初始化配置不对,板子根本起不来。
MicroPython官方仓库其实维护了一个叫mimxrt的移植分支,支持的板卡里就有MIMXRT1020-EVK这一档。也就是说,只要你的板子原理图跟EVK接近,或者自己照着EVK画了板子,那官方固件基本可以直接烧进去用。这一点比很多人想象中要省事。但注意,官方支持的是“标准EVK外设布局”,不同队伍自己画的底板引脚千奇百怪,所以后面多半还要自己编译一版。
顺带说一句,如果你还没定芯片,市面上支持MicroPython的单片机其实不少:STM32、ESP32、RP2040都是官方一等公民,开箱即用。RT1021属于跨界处理器里比较高性能的选择,但它对MicroPython的适配深度不如STM32那边完善。选它的理由应该是“看中Cortex-M7的算力”,而不是“想少折腾”。如果只是想最快跑通MicroPython,ESP32其实更省心。
2.2 编译自己的固件:环境准备与自定义板卡
编译mimxrt移植版并不复杂,但有几个前置条件容易卡住新手。先说环境:建议用Linux或者WSL,Windows原生编译虽然也能跑但坑比较多。需要安装arm-none-eabi-gcc交叉编译器、make、git,以及Python3(编译脚本里有个环节用得上)。
git clone --recurse-submodules https://github.com/micropython/micropython.git cd micropython make -C mpy-cross cd ports/mimxrt make BOARD=MIMXRT1020_EVK submodules make BOARD=MIMXRT1020_EVK编译完之后,固件在build-MIMXRT1020_EVK/firmware.hex和firmware.bin两个文件。如果只是用官方EVK,这把就够烧了。但自制底板一般要改这三个地方:
boards/MIMXRT1020_EVK/mpconfigboard.h:配置时钟、Flash大小、引脚复用、调试串口等信息。boards/MIMXRT1020_EVK/pins.csv:引脚别名映射表,写清楚哪个名字对应哪个物理Pin。boards/MIMXRT1020_EVK/mpconfigboard.mk:选择使用的MicroPython模块和功能(比如是否启用UART、SPI、PWM等),以及固件名。
我自己踩过的坑是时钟树配置。RT1021支持内部RC和外部晶振,如果板子上晶振频率跟EVK不一样,而你还是按EVK的配置去编译,烧进去之后UART波特率会偏得一塌糊涂,REPL根本连不上。当时我一度以为是USB转串口坏了,换了三根线才反应过来是固件里晶振频率不对。后来我把底板的24MHz晶振改成跟EVK一致的配置,问题立刻消失。
2.3 烧录、REPL与文件系统的打通
i.MX RT的烧录方式基本有三条路:板载调试器、外部J-Link、串行下载模式。对EVK来说,板载DAPLink是最方便的,系统里会识别成一个调试接口加一个串口。烧录命令我习惯用pyOCD:
pyocd erase --target mimxrt1020 pyocd flash -t mimxrt1020 -f 0x60000000 firmware.bin先擦除再烧是给自己留退路。如果直接往一个写有旧固件的Flash里灌新固件,偶尔会因为FlexSPI配置区域变化导致启动失败,擦一遍一了百了。烧完之后,打开串口终端,波特率选115200,按下板子复位键,如果看到MicroPython v1.xx on 2024-xx-xx; MIMXRT1020_EVK with MIMXRT1021开头的提示符,恭喜你,RT1021已经正式开始跑Python了。
REPL连上只是第一步。接下来要把文件系统弄通。mimxrt移植版默认启用内部Flash文件系统,MicroPython启动时会自动执行根目录下的main.py。所以开发流程就变成了:在电脑上写好Python代码,用rshell或mpfshell之类工具传到板子的文件系统,再按复位键让代码跑起来。不要再找烧录器了,不需要。
一个小技巧:调试时别把main.py写得太大,否则每次启动都有明显的文件读取时间。把常用工具函数放进boot.py,主逻辑放main.py,这样启动流程更清晰,也方便在boot.py里做串口初始化和硬件自检。
3. 极速光电组的外设驱动:Python代码打开RT1021
3.1 引脚规划先于代码
MicroPython里操作引脚很简单:pin = machine.Pin(1, machine.Pin.OUT)写一个、value = machine.Pin(2).value()读一个。但越简单越容易让人忽略一个问题——引脚分配。RT1021的大部分引脚都是多功能复用引脚,同一个物理Pin可能同时能当GPIO、UART、PWM、ADC用,但同一个外设功能并不一定出现在所有引脚上。如果你拿到一块自己画的底板,第一件事不是写代码,而是拿原理图把所有用到模块的引脚画成一张表,确认没有两个外设冲突。
我的习惯是在项目仓库里放一个pinout.py,把所有引脚定义集中管理,后面所有外设初始化都从这个模块导入。这样改线的时候只改一个文件,不用满工程搜索machine.Pin。比如:
# pinout.py PIN_MOTOR_PWM = 24 PIN_MOTOR_DIR1 = 25 PIN_MOTOR_DIR2 = 26 PIN_ENCODER_A = 13 PIN_ENCODER_B = 14 PIN_LED = 7 PIN_UART_TX = 34 PIN_UART_RX = 353.2 电机PWM输出:频率与死区是两回事
极速光电组最常见的动力方案是直流有刷电机加MOS管H桥驱动。MicroPython的PWM接口在mimxrt移植版里走的是标准machine.PWM封装,用法和STM32的pyb.PWM类似但API更接近ESP32:
from machine import Pin, PWM pwm_pin = Pin(PIN_MOTOR_PWM) motor_pwm = PWM(pwm_pin, freq=20000, duty_u16=32768)freq=20000意思是PWM频率20kHz,这个值是有讲究的。太低(比如几百Hz)电机会发出尖锐的啸叫,甚至被听成“电流声”,但实际是PWM的开关频率落在人耳可听范围内。20kHz刚好超过人耳听力上限,安静又不容易引起机械共振。不过要留意:PWM频率越高,开关损耗越大、MOS管发热越明显。如果你用的是普通半桥驱动芯片,建议先看手册上的最高PWM频率,不要无脑奔着20k去。
再说占空比。mimxrt移植版支持16位占空比精度,也就是duty_u16的范围是0到65535。32768正好是50%,65535是全速。写代码时要注意方向控制和PWM是两回事:H桥需要一个方向引脚(或者两个方向引脚做互锁),再加一个使能/PWM引脚。如果方向引脚和PWM引脚状态配合不当,比如方向切换的瞬间PWM还是高电平,就可能出现一瞬间的刹车电流。这个电流对MOS管很不友好。我的处理方式是:先拉低PWM,再切换方向,再恢复PWM。
def set_motor(duty, direction): motor_pwm.duty_u16(0) # 先断电 dir1.value(1 if direction else 0) dir2.value(0 if direction else 1) motor_pwm.duty_u16(max(0, min(65535, duty)))这个“先断电再换向”的细节在C代码里都一样,但Python写起来太顺手,新手特别容易忽略。极速光电组赛道上频繁加减速,方向切换死区控制不好,轻则电机顿挫,重则烧驱动。
3.3 编码器读取:mimxrt没有现成封装怎么破
这是我在RT1021上最头疼的部分,没有之一。STM32上MicroPython虽然也没有完整的正交解码封装,但大家习惯用定时器外部计数模式蒙混过关;到了mimxrt移植版,连蒙的路径都少了。RT1021内部其实有专门的ENC(Enhanced Quadrature Decoder)模块,但MicroPython没有把它暴露给Python层。这意味着你有三条路可以走:
第一种,低速验证时用GPIO中断。A相接上升沿中断,在回调里读B相电平判断方向。这个方法实现简单,代码量不到二十行,但中断回调在MicroPython里的开销比较大,电机高速转起来很容易丢脉冲。我只把它用在硬件自检和电机堵转保护上。
第二种,外接正交解码芯片,通过SPI或I2C读计数值。这是很多智能车队的老办法,典型的芯片是LS7366R。Python只需要在需要的时候发起SPI读操作,不需要任何中断,实时性靠芯片本身保证。缺点是板上多一颗芯片、多花几块钱。但换来的是稳定和省心,我觉得很值。
第三种,也是最彻底的:写一个C扩展,把RT1021的ENC寄存器操作封装成一个简单的Python模块。如果你用官方SDK,配置过程其实很清楚——选择ENC通道、配置输入引脚、清零计数值、启动计数。C扩展暴露出来的函数只需要两个:encoder.read()和encoder.clear()。这个方案性能最好,但需要你对mimxrt移植版的构建系统有一定了解,适合备赛时间充裕时做。
我最终采用的方式是第二种加第三种的混合:调车阶段用LS7366R,稳定之后把C扩展接上,去掉外部芯片。这样既保证了前期开发效率,又保住了正式比赛的可靠性和精度。
3.4 中断回调与看门狗:安全类功能的落地
MicroPython支持machine.Timer和Pin的外部中断,但使用时有几条规矩必须刻在脑子里:回调函数里不要分配内存、不要调用print(除非你接受每次触发都卡几百微秒)、不要做复杂计算。最好的实践是“中断里只置标志位,主循环里处理业务”。
看门狗在第二十届的比赛规则里是明确要求的,不允许用任何方式绕过去。MicroPython的WDT封装在mimxrt上也能用:
from machine import WDT wdt = WDT(timeout=1000) while True: # 主循环逻辑 wdt.feed()注意看门狗的超时时间不能设太短。MicroPython主循环里如果做了垃圾回收或者文件操作,时间抖动比C裸机大不少。我的经验是设1500ms左右比较稳,前提是主循环本身不能真的卡死超过这个时间。
还有一点,喂狗尽量放在主循环里,不要放在中断回调里。如果你放在一个定时器中断里喂狗,那主程序哪怕死锁了狗也不会复位,等于白装。当时我跟队友排查一个“程序看起来跑着但车完全不响应”的诡异问题,最后发现就是有人在中断里喂了狗,主循环早就螺旋升天了。
4. 摄像头和控制回路:性能敏感的三大难题
4.1 图像识别到底放哪里:RT1021不是万能的
极速光电组最核心的信息来源就是图像。赛道上有没有十字、有没有坡道、弯道半径多大,全都得从一帧帧图像里抠出来。如果让RT1021直接用MicroPython去读摄像头数据,处理每一帧完整的图像,那基本上是死路一条。500MHz的Cortex-M7很强,但Python解释器一条条翻译字节码的速度,碰上一张QVGA灰度图的像素遍历就崩溃了。
所以我的建议非常明确:图像识别这一步,不要让Python裸扛。第二十届极速光电组常用的方案有两种。
方案一是用OpenMV这类本身就跑MicroPython的视觉模块做前端感知,RT1021只负责接收OpenMV通过串口发来的赛道信息。OpenMV的帧率能到几十上百fps,识别赛道左右边线、丢线状态、十字标志这些任务完全够用。RT1021串口收到的不是图像,而是赛道元数据,比如“左偏30像素、前方直道、速度建议4.2m/s”。这种方式的好处是主控完全不需要碰图像处理,实时性压力小很多。
方案二是用RT1021的CSI接口接摄像头,DMA把图像搬运到内存,但Python层只处理关键行/关键区域,比如只读图像中间三行判断赛道偏差。这个方案本质上还在RT1021上做图像处理,只是把数据量砍到了Python能承受的范围。它适合车道简单、不需要复杂图像理解的场景。
我自己两个方案都试了,最终在正式车上用的是方案一。不是因为方案二技术不行,而是因为备赛后期时间太紧,把图像处理放在OpenMV上,调试时可以直接看OpenMV的画面和识别结果,人脑不需要想象“这幅图经过处理后变量是什么”,直接看可视化比什么打印都管用。
4.2 控制周期到底能做多快:一个实测没有水分
很多人在决定用MicroPython跑控制回路前,心里最没底的就是:一个循环到底是1kHz还是10Hz?这个数据不能靠猜,我的做法是直接在RT1021上做了个基准测试。
先写一个空循环,测量10000次迭代耗时。然后在循环里加上一个简单的PID计算(三个乘法、三个加法),再测一次。最后再把编码器读取和PWM输出加进去,得到一个接近实际控制任务的耗时。三个数字放在一起:
| 控制循环内容 | 单次耗时(微秒级) | 对应最大控制频率 |
|---|---|---|
| 空循环(pass) | 约15us | 数十kHz以上 |
| 空循环 + 单轮PID计算 | 约45us | 约22kHz |
| 空循环 + PID + 编码器读取 + PWM输出 | 约80us | 约12kHz |
这个数据在500MHz的RT1021上测出来,已经相当可观。也就是说,只要你的控制循环里不做图像处理、不做列表操作、不打印调试信息,纯Python跑到2kHz的控制频率完全没有问题。极速光电组对速度环和方向环的常规需求也就在1kHz到2kHz之间,MicroPython能顶住。
但这里有个隐形杀手:垃圾回收。MicroPython的内存管理会在你需要的时候“暂停世界”,如果GC在控制循环中间发生,单次循环耗时会从80us瞬间跳到几毫秒。为了不让GC捣乱,我一般会预分配所有循环内的对象,避免在循环里创建任何新对象。比如PID计算不要写成err = target - current然后创建一个新变量,而是提前把误差存到一个列表或者直接用可变对象。
4.3 viper、native与C扩展的三级提速
如果你的控制循环确实遇到了性能瓶颈,别急着换语言,MicroPython自带一套从软到硬的提速方案。
第一级是@micropython.viper装饰器。viper模式会把函数编译成原生机器码,同时允许你用int、uint、ptr等类型注解,告诉解释器变量类型,避免装箱拆箱。写法上牺牲一点Python的灵活性,但换来接近C的速度。
@micropython.viper def pid_calc(target: int, current: int, kp: int, ki: int) -> int: err = target - current out = kp * err return out第二级是@micropython.native装饰器。native模式把整个函数编译成原生机票吗?听起来和viper差不多,区别在于native保留Python动态类型,提升不如viper明显,但兼容性更好。一般我在函数里需要调用其他Python对象时用native,纯数值计算用viper。
第三级才是C扩展。C扩展是把性能敏感的函数用C实现,通过MicroPython的模块系统暴露给Python。这条路最彻底,但也最重。我的建议是:先用viper试试,看性能能不能满足需求;如果满足不了,再考虑C扩展。不要一上来就写C扩展——那是把MicroPython的优势又丢回给C了,开发效率重新变低,不如一开始就用纯C写整个控制程序。
5. 从裸机思维切换到Python思维:实战踩坑记录
5.1 调试方式的巨变:print、REPL和热更新
第一次在REPL里输入motor_pwm.duty_u16(30000)然后看着电机真的转起来的时候,我脑子里“嵌入式开发必须编译烧录调试”的固有模式被打碎了。从此,我的调试习惯从“接J-Link、看寄存器、打断点”变成了“print大法加REPL手动喂数据”。
这种切换带来一个特别直观的效率提升:调试摄像头参数不再需要反复烧录代码,只需要把一帧图像数据导入Python环境,然后不断调用识别函数,看返回值对不对。改一版识别逻辑,回车执行,结果立刻出来。这在C开发流程里是不可想象的。
顺带说一个老话题:经常有人问VB6能不能用来写嵌入式硬件程序。结论是能,但没有意义。嵌入式实时系统的开发语言选择不是“能不能操作寄存器”,而是有没有完整的交叉编译工具链、运行时环境、外设驱动库和调试手段。VB6离这些太远了,连MicroPython这种解释型脚本方案,都已经是比较激进的高层选择。如果你看到有人用VB6写单片机,那多半是十几年前做串口上位机的老项目,不是拿来跑控制回路的。
5.2 我踩过的三个坑:堆栈、垃圾回收与float
第一个坑是堆栈溢出。MicroPython的构造函数、回调函数、表达式求值都需要栈空间,如果你在main.py里写了深层次递归,或者在一个中断回调里做了大量操作,默认的栈配置很容易爆。现象是程序运行到一半突然抛出MemoryError或者NameError,有时候连异常信息都没打完就卡死了。解决方法是把mpconfigport.h里的栈配置调大,或者严格限制递归深度和回调复杂度。
第二个坑是垃圾回收的“随机抖动”。前面提到GC会让控制循环产生毫秒级抖动,但实际影响比我想象的更大。有一段时间,赛车在直道上速度曲线出现规律性波动,接上示波器看PWM输出才发现每隔几百毫秒就有一个明显的毛刺,正是gc.collect()的间隔。后来我在主循环里主动控制GC时机:不在控制循环里分配对象、定期在循环外显式调用gc.collect(),速度曲线才平了。
第三个坑是float运算的隐形成本。RT1021有FPU,硬件浮点运算很快,但MicroPython的float对象是堆分配的。你在循环里写temp = temp + 0.1,它每次都可能创建一个新的float对象,触发堆分配,最终拖慢循环速度。解决方式是尽量用整数运算代替浮点,或者在viper模式下用原生float类型。我最终把所有PID系数扩大1000倍,用整数做计算,最后再除以1000输出,速度提升非常明显。
5.3 一个小而完整的赛车骨架
最后给一个可以直接改来用的最小骨架。这不算成品,但它把“MicroPython控制一辆极速光电组赛车”所需的关键模块串起来了:PWM电机输出、编码器读取、定时器触发控制周期、看门狗保护、串口打印调试信息。
import machine from machine import Pin, PWM, Timer, UART, WDT import time # ---------- 引脚定义 ---------- PIN_MOTOR_PWM = 24 PIN_MOTOR_DIR1 = 25 PIN_MOTOR_DIR2 = 26 PIN_ENCODER_A = 13 PIN_ENCODER_B = 14 # ---------- 外设初始化 ---------- dir1 = Pin(PIN_MOTOR_DIR1, Pin.OUT) dir2 = Pin(PIN_MOTOR_DIR2, Pin.OUT) pwm = PWM(Pin(PIN_MOTOR_PWM), freq=20000, duty_u16=0) count = 0 last_a = 0 def encoder_isr(pin): global count a = Pin(PIN_ENCODER_A).value() b = Pin(PIN_ENCODER_B).value() if a != last_a: count += 1 if b else -1 last_a = a # 简化版:只有A相中断,方向靠B相判断 enc_a = Pin(PIN_ENCODER_A, Pin.IN, Pin.PULL_UP) enc_b = Pin(PIN_ENCODER_B, Pin.IN, Pin.PULL_UP) enc_a.irq(handler=encoder_isr, trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING) # 看门狗 wdt = WDT(timeout=1500) # 串口调试 uart = UART(1, 115200) # ---------- 控制参数 ---------- target_speed = 2000 # 编码器计数目标 kp = 10 ki = 0.5 integral = 0 prev_error = 0 motor_duty = 0 def control_loop(timer): global integral, prev_error, motor_duty speed = count count = 0 error = target_speed - speed integral = max(-3000, min(3000, integral + error)) derivative = error - prev_error prev_error = error motor_duty = kp * error + ki * integral + 2 * derivative motor_duty = max(-65535, min(65535, motor_duty)) if motor_duty > 0: dir1.value(1) dir2.value(0) else: dir1.value(0) dir2.value(1) pwm.duty_u16(abs(motor_duty)) timer = Timer(0) timer.init(period=1, mode=Timer.PERIODIC, callback=control_loop) # 上面period单位是毫秒,1ms对应控制频率1kHz while True: wdt.feed() uart.write("speed=%d duty=%d\r\n" % (target_speed, motor_duty)) time.sleep_ms(20)这段代码不是我直接拿来做完赛准备的版本,但它包含了一个微型赛车控制器必须具备的所有要素。等你在REPL里验证过每一个模块,再往里面加图像识别、路况判断、状态机,就是水到渠成的事。
如果你问我现在回看这个“RT1021加MicroPython”的选择后不后悔——我会说不后悔。第二十届极速光电组备赛的过程中,MicroPython的快速迭代让我和队友把大量时间花在了真正重要的地方:赛道数据收集、策略调整、稳定性的打磨,而不是一遍遍重复“改代码-编译-烧录”的无意义循环。当然,如果你打算走这条路,一定要像我在文章里反复强调的那样,先把芯片启动、固件编译、外设驱动这些地基打牢,再往上盖Python这座快节奏的高楼。地基不牢的话,脚本语言带来的效率红利,大概率会在后期被各种玄学bug反向吞掉。