第二十届全国大学生智能汽车竞赛极速光电组赛道依旧是白底黑线赛车依旧是那辆靠转向和速度刷圈的小车。但这几年我注意到一个变化越来越多的队伍开始把 MicroPython 跑在恩智浦 RT1021 上做硬件编程这在整天和寄存器、编译链打交道的嵌入式圈子里是个值得认真聊的话题。这篇文章就是我带队备战这个组别时从固件烧录、底层驱动到巡线算法、双环控制一路踩坑总结出来的实战方案适合想快速起步的参赛队也适合想给 RT1021 换一种开发方式的工程师参考。先说结论用 MicroPython 跑智能车竞赛不是要用脚本语言取代 C而是把 Python 的迭代效率用到调车全流程里。极速光电组对算力要求不像摄像头组那么高但又是纯竞速组别代码改动极其频繁。传统 C 工程每次改个参数都要编译、烧录、复位一套下来少说十几秒MicroPython 下直接改脚本重启配合串口 REPL 改参数那体验真是用过就回不去。当然这种便利是有代价的性能、内存、实时性都得花心思去平衡。这篇文章就围绕这些平衡点展开。1. 赛道认知与方案选型极速光电组为什么需要 MicroPython1.1 极速光电组的比赛逻辑极速光电组是智能车竞赛里一个传统的竞速组别。比赛场地通常铺设白色底板赛道中央有一条黑色引导线车辆从起点出发在完全自主的情况下沿黑线高速行驶最终以完赛时间和稳定性决定名次。这个组别多年前叫“光电组”大家用光电传感器排成一排检测黑线后来逐渐演变成以摄像头为主要感知方式比赛规则也在不断调整比如引入障碍、坡道、环岛等元素。和完全依靠视觉识别大幅图像的摄像头组相比极速光电组的“视觉任务”相对可控只需要区分黑和白找到黑线的横向位置不需要识别数字、锥桶、交通标志这类复杂目标。所以它对处理器的算力要求是“够用但不高”但对系统的实时性、稳定性和调参效率要求非常高。比赛现场留给每支队伍的准备时间有限遇到图像歪了、丢线频繁、转向过晚这类问题能否快速定位和修正直接决定最终圈速。1.2 为什么 RT1021 配 MicroPython 是可行路线先看硬件。恩智浦 RT1021 属于 i.MX RT 跨界处理器系列核心是主频最高能到 500MHz 的 Cortex-M7片上自带 288KB SRAM封装最小的做到 LQFP100 甚至更小非常适合智能车这种对体积和重量敏感的场景。它在恩智浦的芯片路线图里定位低于 RT1062、RT1064但跑 MicroPython 完全绰绰有余。RT1021 的外设也很齐全FlexIO 可以模拟摄像头并行接口时序SAI、PWM、编码器接口、CAN、USB 都有做一辆小车不需要外挂太多芯片。再看软件生态。恩智浦官方维护了一个 MicroPython 分支仓库NXPmicro/micropython专门支持 i.MX RT 系列RT1020、RT1050、RT1060 都在支持列表里。这个分支和 OpenMV 使用的 MicroPython 同源所以如果你用过 OpenMV上手会非常快。更重要的是RT1021 的 MicroPython 固件里已经带了 machine 模块可以操作 Pin、PWM、UART、I2C、SPI 等基本外设直接拿来写控制逻辑足够了。这套组合的吸引力在哪我用一句话概括底层的苦活累活交给固件和 C 扩展上层的算法和策略交给 Python。摄像头采集一帧图像、DMA 搬运数据、图像二值化、角点扫描、提取中线这些高频重复操作在 C 层实现而 PID 参数调整、速度规划、特殊赛道元素处理、启动逻辑这些需要频繁改动的部分全部放在 Python 层。这样既保证了实时性又保住了开发效率。1.3 MicroPython 方案的边界与代价必须诚实地说MicroPython 不是万能的。Python 是解释执行一条简单的循环可能比 C 慢几十倍垃圾回收GC偶发的工作会造成时间抖动极端情况下可能让控制周期从 1ms 突然变成 10ms如果图像分辨率开到 640x480再把每一帧都拿到 Python 层逐像素处理直接卡成幻灯片。这些问题我都踩过后面会详细讲对策。还有一个容易被忽视的点MicroPython 不是所有 RT1021 的引脚都默认开放不同核心板的引脚布局、时钟配置、外部 Flash 型号不一样直接烧官方固件可能出现串口识别不到、PWM 输出无波形这类问题。所以别拿到板子就把固件烧进去一定先确认你的板子原理图和官方 EVK 的差异再做引脚适配。这个细节能帮你省掉后面大量排查时间。2. 先用 15 分钟把 MicroPython 跑起来RT1021 环境搭建与最小系统验证2.1 硬件准备动手之前先把东西备齐。我的建议清单如下一块基于 RT1021 的核心板或者主板优先选择已经适配过 MicroPython 的型号。如果是自制板务必确认外部 NOR Flash 型号、晶振频率、调试下载接口定义。一个 USB 转 TTL 串口模块推荐带 DTR/RTS 的型号便于后面用串口工具自动复位进入下载模式。一根能连电脑的 USB 线用于给核心板供电和调试。建议用带屏蔽层的短线工业现场和比赛场地附近电磁干扰大劣质 USB 线容易导致掉盘、复位。可选一个 J-Link 或者 DAP-Link 调试器。虽然 MicroPython 开发流程不依赖调试器但烧录引导程序和排查固件问题时会非常有用。这些硬件加起来成本不高核心板一两百元串口模块几十元比买一套完整开发套件便宜得多也贴合智能车竞赛“低成本、可快速复制”的调车需求。2.2 烧录固件到 RT1021把 MicroPython 固件烧进 RT1021有两条常用路径两条我都试过各有适用场景。第一条路径是使用恩智浦的 MCUXpresso IDE。在 IDE 里新建一个工程选择 RT1021 对应型号然后导入官方 MicroPython 仓库中编译好的固件工程。编译完成后直接点下载按钮IDE 会通过 J-Link 或板载 DAP-Link 把固件写到外部 Flash。这条路径最稳妥适合第一次上手因为 IDE 会帮你处理好链接脚本和 Flash 下载算法不会出现“固件烧了但跑不起来”这类玄学问题。缺点是 IDE 体积大、启动慢如果你只改 Python 脚本完全不需要每次打开它。第二条路径是使用恩智浦的 Flashloader 工具。RT1021 支持通过串口或 USB 进入 ISP 下载模式你可以在上电时把 BOOT 引脚拨到相应电平然后使用 Flashloader 命令行工具把固件 bin 文件下载到外部 Flash。这条路径的优点是不需要额外的调试器只要有一根串口线就能干活非常适合在比赛现场快速恢复一台“变砖”的车。缺点是需要手动设置 BOOT 引脚和拨码开关对新手稍微有点门槛。无论走哪条路都建议在烧录前先把原厂 EVK 的示例固件备份一份。别问我为什么我遇到过不止一次固件刷坏导致板子黑屏最后靠备份固件救回来的情况。2.3 第一个 MicroPython 测试代码固件烧好后串口连接电脑打开你习惯的串口工具我用的是串口调试助手或者 minicom波特率通常设置为 115200复位开发板后能看到 MicroPython 的交互提示符。这时你就可以在 REPL 里敲代码了。先做个最基础的验证点亮板载 LED 或者控制一个空闲引脚翻转from machine import Pin import time led Pin(0, Pin.OUT) while True: led.toggle() time.sleep_ms(200)这段代码的含义很简单把第 0 号引脚配置为输出然后每 200ms 翻转一次电平。如果 LED 按预期闪烁说明固件、串口、时钟三个环节都基本正常可以继续往下走了。这里有个小经验首次测试尽量用板载 LED不要用杜邦线外接 LED因为板载 LED 的引脚映射已经写在固件里成功率最高。等你确认环境没问题再外接自己的传感器和执行器。2.4 引脚和外设资源规划正式写智能车代码前先做一张引脚分配表。我一般先把 RT1021 的关键外设和对应引脚列清楚外设功能建议使用的外设说明图像采集FlexIO / GPIO 模拟DVP 并口摄像头优先用 FlexIO 接 PCLK、VSYNC、HSYNC舵机控制PWM 定时器50Hz 左右信号脉宽 1ms 到 2ms电机驱动PWM 输出 GPIO一般用两路 PWM 或一路 PWM 加方向脚编码器测速编码器定时器 / 外部中断需要确认定时器是否支持正交解码串口调试UART至少留一路给上位机调参拨码开关 / 按键GPIO 输入用于模式切换和参数选择指示灯GPIO 输出调试状态显示强烈建议留至少两个这张表的价值不在于推荐具体引脚号而在于提醒你别把关键外设挤在同一个定时器或者同一组冲突引脚上。RT1021 的引脚复用非常灵活但也容易踩坑比如同一个定时器的两个通道被占用了另一个外设结果 PWM 死活输出不了。所以动手布线之前先打开芯片手册的引脚复用表把分配确认好再画板、再接杜邦线这是老调车人都会做的事。3. 核心算法落地图像预处理、巡线与双环控制3.1 图像采集层的“朴实”设计智能车主流的感知是摄像头。RT1021 本身没有直接的高清摄像头接口我看到很多队伍用 FlexIO 去模拟 DVP 并行口时序接 MT9V034 这类灰度摄像头也有用 OV7725 的。图像采集这种高频操作不建议全放到 Python 层做因为单帧图像像素传输和行场中断处理在解释执行下根本扛不住。我的做法是在 C 扩展层完成帧同步、行数据 DMA 搬运、简单的二值化最后只把处理好的灰度数组或二值化数组交给 Python 层。这样设计有个很大的好处Python 层不需要关心摄像头时序、像素时钟这些底层细节拿到的 frame 就是一个普通的二维数组。比如可以这样封装import camera # camera 是 C 扩展模块已经封装好底层采集逻辑 w, h camera.init(resolutioncamera.R160x120) frame camera.snapshot() # 返回 bytes 或 memoryview长度为 w * h底层 C 扩展每采集一帧把灰度数据放到一块固定的缓冲区Python 层只是拿到这个缓冲区的引用。这里有一个关键优化点不要让 Python 层每次重新拷贝数据而是复用同一个缓冲区减少内存分配和 GC 压力。下面这段代码就是典型的“反例”# 不推荐每次 snapshot 都新建大对象内存会被快速吃光 for i in range(1000): frame camera.snapshot() # 每次都重新分配内存推荐改成复用内存的方式。比如 Camera 模块内部维护固定缓冲区snapshot 返回 memoryviewPython 层直接读写同一块内存。这样跑一晚上循环内存占用也稳定。3.2 二值化与中线提取的代码优化拿到灰度图后最简单的巡线方法是把图像二值化像素灰度低于阈值记为黑色高于阈值记为白色。然后从下往上扫描每一行找到左右黑边计算中线位置。下面是一段比较实用的中线提取代码def extract_center(frame, w, h, threshold): mid w // 2 sum_x 0 count 0 # 每隔 4 行采样一次降低计算量 for y in range(0, h, 4): row_offset y * w left -1 right -1 # 从左往右找第一个黑点 for x in range(0, w): if frame[row_offset x] threshold: left x break # 从右往左找第一个黑点 for x in range(w - 1, -1, -1): if frame[row_offset x] threshold: right x break if left 0 and right 0: center (left right) // 2 sum_x center count 1 if count 0: return None # 丢线 return sum_x // count - mid # 返回中心偏差正数表示线在右侧这段代码有几个细节值得展开隔行扫描。逐像素扫全图当然也行但 RT1021 跑 MicroPython 时纯 Python 循环的开销不小隔 4 行采样可以显著降低耗时而扇形区中线信息基本保留。左、右分向扫描。从左往右找第一个黑点从右往左找第一个黑点取中点作为该行中心。这种算法在直线和缓弯上非常稳比“先整体二值化再找联通域”简单得多。丢线返回 None。这是一个非常关键的设计。你在赛道上一定会遇到黑线跑出画面、车在环岛里、或者前车压线等场景这时候如果算法强行返回一个错误中心转向会瞬间乱跳。正确的做法是让上层做“丢线补偿”比如沿用上几次偏差外推。阈值选取上固定阈值最简单但不同光照环境下效果差异很大。我的建议是做一个自适应阈值取图像顶部区域的灰度均值或直方图谷底作为阈值而不是在所有环境下都用一个数。比赛场地灯光变化频繁固定阈值在调试时表现不错一上赛场就翻车的情况我见过太多次了。3.3 方向环 PD 与速度环 PI 联合调试巡线偏差计算出来后控制部分可以拆成两个环方向环负责转向速度环负责车速。方向环我习惯用 PD 控制速度环用 PI 控制。原因很简单方向环需要快速响应偏差变化D 项可以抑制超调速度环要消除稳态误差I 项是必要的。方向环代码示例如下from machine import PWM, Pin import time servo PWM(Pin(5), freq50) # 舵机引脚50Hz center_servo 75000 # 对应中位脉宽单位 ns kp 1.2 kd 3.5 last_err 0 def set_servo_from_error(err): global last_err # 丢线时把误差放大逼车大幅转向找回黑线 if err is None: if last_err 0: err 100 else: err -100 else: err max(-80, min(80, err)) # 限幅 duty center_servo int(kp * err kd * (err - last_err)) duty max(50000, min(100000, duty)) # 舵机脉宽限幅 servo.duty_ns(duty) last_err err while True: frame camera.snapshot() err extract_center(frame, w, h, threshold) set_servo_from_error(err) time.sleep_ms(5) # 控制周期 5ms约 200Hz这里提两个容易踩的坑第一个坑是单位不统一。MicroPython 的 duty_ns 单位是纳秒但新手经常拿示波器量出来的毫秒值直接填结果舵机打死。建议在代码里写清楚单位并且用min/max限幅避免异常值把舵机搞到极限位置。第二个坑是 D 项噪声放大。误差信号来自图像图像本身有噪点D 项会对高频跳变非常敏感。如果车在高频抖动先别急着加 D先确认图像是不是稳定了图像稳定后 D 的范围一般在 kp 的 2 到 5 倍之间具体还得看赛道曲率。速度环相对简单增量式 PI 是主流speed_p 0.8 speed_i 0.15 target_speed 2500 # 编码器计数值单位取决于测速周期 integral 0 def speed_control(current_speed): global integral err target_speed - current_speed integral err integral max(-500, min(500, integral)) output speed_p * err speed_i * integral return output注意积分限幅。速度环的积分如果没有限幅车刚起步时积分会快速累积导致电机全力输出出现“一松手就冲出去”的惊险画面。比赛现场常见的起步飞车八成就是积分饱和闹的。3.4 参数存储与标定MicroPython 开发最大的优势是调参快但调参快也带来一个新问题参数散落在脚本各处每次改完都不知道是哪一版。我的做法是把参数集中放到一个 config.py 文件里并且用串口做一个简单的标定指令。比如你可以在 REPL 里直接执行import config config.kp 1.5 config.kd 4.0 config.save()这里的 save 函数把当前参数序列化后写到 Flash 文件系统下次开机自动加载。MicroPython 的 Flash 文件系统是可以读写的利用它做参数持久化非常方便。这样整个调车过程就变成了改参数、看效果、再改参数完全不用重新编译。另外我强烈建议在车上保留一个拨码开关用来切换“竞速参数”和“调试参数”。比赛时用保守参数保证完赛调试时用激进参数试极限。这个习惯救了我很多次因为激进参数调车往往会翻车一翻车电机就可能扫齿结果连保守跑法的车都没了。4. 性能优化与可靠性设计让脚本车也能跑出稳定圈速4.1 解释执行与 C 扩展的边界MicroPython 上做控制最核心的原则是把高频、固定模式的操作全部下沉到 C把低频、需要灵活调整的策略留在 Python。图像采集、DMA 搬运、编码器计数、PWM 输出这些都应该是 C 层固件的事情Python 层只拿结果、做判断、发指令。我见过有的同学在 Python 层写了一个巨大的 for 循环去翻转 GPIO模拟 I2C 时序结果周期长、波形乱怎么调都不稳定。这种问题不是 MicroPython 不行而是用错了工具。就好比你非要用一把螺丝刀去钉钉子钉子没进去你不能怪螺丝刀不行。在 RT1021 上做硬件编程先分清哪些是“该用 C 的”哪些是“可以用 Python 的”这是最基础的认知。4.2 内存和垃圾回收避坑MicroPython 的内存管理有两个关键点内存池有限和 GC 抖动。RT1021 的 288KB SRAM 本来不小但固件、C 缓冲区、Python 堆都要瓜分它留给 Python 对象的内存可能只有几十 KB。图像数组这种大对象千万不能随便建否则跑几圈内存就满了。我的三个实用建议大缓冲区统一走 C 层分配Python 层只拿 memoryview 引用不要反复创建 bytes 对象。避免在控制循环里使用append、字符串拼接、字典遍历这类容易触发 GC 的操作。把这些操作放到初始化阶段做循环里只做纯计算。善用micropython.alloc_emergency_exception_buf和micropython.mem_info()查看内存使用情况定期检查内存峰值。如果你发现内存占用持续上涨多半是某处产生了内存泄漏。GC 抖动在调车时表现得很隐蔽车子平时跑得好好的突然某一次转向延迟了 50ms然后恢复正常。比赛现场大家通常怀疑是舵机问题其实很可能是 GC 在回收大对象。网上的一个技巧是把可能触发 GC 的代码放在启动时预执行一遍让内存分配稳定下来这样运行期的 GC 次数会大幅减少。4.3 用中断和 DMA 接线Python 只做控制RT1021 的硬件资源非常丰富如果把中断、DMA、定时器全用起来MicroPython 下也能实现比较强的实时控制。我的推荐架构是摄像头帧完成通知走 FlexIO 中断或 DMA 完成中断中断里只做标记把帧数据交给 C 缓冲。编码器计数由硬件定时器完成不需要占 CPUPython 层定时读取寄存器的值即可。舵机控制 PWM 由硬件 PWM 输出Python 层只需要定时更新比较寄存器。主控循环保持 200Hz 到 500Hz 的周期每个周期只做图像处理和控制计算不做任何阻塞式等待。这样设计之后Python 层的负担很小控制周期也容易保持稳定。我曾经把控制周期压到 2ms500Hz这种情况下 MicroPython 依然可以稳定运行前提是控制循环里不要有 print、sleep 这类阻塞调用。调试时 print 没问题比赛前记得全部关掉或者用条件编译屏蔽。4.4 可靠性设计至少要有这几件事智能车不是玩具一台调好的车在赛道上可能跑到每秒好几米一旦失控冲出赛道轻则丢分重则撞坏摄像头、扫掉舵机齿轮、烧掉电机驱动。可靠性设计必须有看门狗必须有。RT1021 的 MicroPython 固件支持machine.WDT在主循环里定期喂狗。一旦程序跑飞或者死循环看门狗自动复位整车至少保证车子不会带着故障继续冲。我见过没开看门狗的车在赛道上突然失控直接撞上护栏电机驱动板冒烟的场面真的后怕。电压监控必须有。加一个分压电阻把电池电压采样到 ADCPython 层每 100ms 读一次低于阈值就降速或者停车。锂电池过放对电池寿命影响很大比赛现场如果换电池不及时很容易把电池搞到无法恢复。电机堵转保护必须有。当编码器读数为零但 PWM 输出很大时大概率电机被卡住此时应立即切断输出并报警。这些可靠性逻辑用 MicroPython 写非常简单几十行代码就能搞定。很多队伍只关注速度和算法忽略这些“看不见”的代码结果一到现场就出各种幺蛾子。把这些可靠性设计当作一个硬性模块优先级等同于巡线算法。5. 常见问题排查与避坑清单5.1 烧录后串口无响应这是最基础也最常见的问题。插上 USB 转串口模块后打开串口工具完全看不到 MicroPython 的提示符。按我的经验原因通常有三个固件没烧进去或者烧到了错误地址。用 IDE 烧录时会自动处理地址但用手动烧录很容易把固件下载偏移搞错。解决办法是先确认 BOOT 模式再看烧录工具输出是否提示成功。串口模块的 TX/RX 接反了。这个听起来很傻但我确实在比赛现场见过好几组人因为杜邦线插反而折腾半天。波特率不对。MicroPython 默认波特率一般是 115200但有些改版固件是 921600建议两个都试一下。如果以上都确认没问题再试试上电瞬间按复位键很可能固件在等待你手动进入下载模式。RT1021 的 BOOT 引脚组合决定了启动来源仔细看核心板的丝印一般会有标注。5.2 图像花屏、条纹、颜色不对图像花屏面前一片雪花或者左右错位大概率出在硬件时序上。很多同学一上来就怀疑代码其实先要用示波器看三根信号PCLK 像素时钟、VSYNC 帧同步、HSYNC 行同步。如果 PCLK 电平不对多半是引脚复用或者时钟配置有问题如果 HSYNC 跟 VSYNC 的极性反了画面会整体错位。灰度摄像头还有一个常见问题阈值设置不合理导致二值化后黑线断裂或者大面积黑斑。建议先在 REPL 里把灰度图数据打印出来统计一下最大值、最小值、平均值再确定阈值。我觉得与其反复试阈值不如直接写一个小函数采集 10 帧图像自适应计算阈值这样应对不同光线环境会更从容。5.3 车在直道上左右摆头直道上左右摆头常见原因有两个一是 D 项过大二是图像中线提取不够稳。D 项过大时车会频繁修正方向表现为高频小幅抖动图像中线提取不稳时误差序列本身就有跳动D 项会放大这种噪声。处理办法分两步先把 D 项降为零看直道是否还抖如果还抖就去检查图像看每帧中线是不是在轻微左右跳。如果是阈值太敏感导致边缘频繁变化就适当提高阈值或者做一次中值滤波如果图像稳定了还抖再慢慢加 D 项每次加一点观察实际效果。调车不需要高深理论多一点耐心就能调好。5.4 “VB6.0 能不能编程嵌入式硬件”这类问题给我的启发最近网上有个讨论还挺有意思VB6.0 可以编程嵌入式硬件吗从严格意义上说VB6.0 不能用来写单片机固件因为对应的编译器、链接器、芯片支持包都不存在。但如果你把它理解成“用 VB6.0 和单片机通信、做上位机控制”那完全可以串口控件、网络控件都能和 RT1021 交互。这个问题的本质是嵌入式开发是一个工具链生态的集合不是说一门语言“能不能”搞定一切而是这套工具链适不适合你的目标硬件。这和 MicroPython 在 RT1021 上的定位很像——它不是万能的但在合适场景下它是效率极高的工具。用 MicroPython 调智能车也是一样的道理。你不需要用它写所有代码只需要把它放到最能发挥价值的层面——快速迭代算法、灵活调整策略、降低开发门槛。至于底层的高速驱动、严格要求时序的部分该用 C 还用 C该用中断还用中断。这样的混合开发模式才是 MicroPython 在嵌入式硬件上最正确的打开方式。6. 一点个人体会和后续可以扩展的方向如果让我重新带一次极速光电组的队伍我依然会选 MicroPython 加 RT1021 这套组合但我会在第一天就跟队员说清楚MicroPython 是我们调车的“油门”不是我们躲开底层的“避风港”。花一周时间把 C 扩展、DMA、摄像头时序摸透后面 Python 层的所有算法才有底气反过来如果只会 C 不接触脚本调车效率又会被编译周期拖垮。两者配合才是一个真正能打胜仗的车队该有的技术栈。在赛场上真正让我觉得这套方案值得推广的场景是中午刚调整完赛道元素策略下午就要上场比赛别的队伍还在等编译器烧程序我们已经用串口改了 20 次参数把环岛处理逻辑从“进环岛减速”改成“进环岛后加速出环”整个过程不超过 10 分钟。这种开发效率正是智能车竞赛这种“时间紧迫、环境多变”的场景最需要的。后续如果想继续玩深一点可以从这几个方向扩展把 RT1021 的 MicroPython 工程升级到支持双核协作把图像识别部分放到第二个核上跑或者把上位机调参系统升级成 Web Dashboard手机直接在局域网里改 PID、看实时曲线再往后RT1170 这类更高性能的跨界处理器也已经有了 MicroPython 支持留给你的发挥空间真的很大。总而言之工具只是起点怎么把工具用到极致才是技术能力的分水岭。