资讯动态

基于Hi3863/WS63的智能小车控制链路设计与实现

发布时间:2026/10/21 7:54:17 来源:尧图企业网站定制
前面几篇把环境搭建、点灯、联网都跑通了小车依然原地趴着原因很简单控制层还没做。这一篇是整个系列里真正让小车“动起来”的关键部分。我要聊的不只是怎么接两根线、写几行 PWM而是把控制链路从上层指令到轮子转动的完整逻辑拆开从驱动选型、引脚规划、PWM 调速,到命令解析、状态机、任务拆分的全过程都过一遍。如果你正好在用或准备用 hi3863ws63做类似的小车项目这份实践记录可以直接当参考。先说明一点我用的是一块国产低功耗 WiFi SoC 的开发板核心是 RISC-V 架构常见的型号标识就是 hi3863 / ws63 系列。不同批次板子的引脚定义、SDK 接口名字略有差异但控制思路完全一致。文中的函数名我尽量写成和官方 SDK 风格接近的伪代码实际编译时请以你手里那份 SDK 头文件为准。整套控制方案的结论放在前面用两路 PWM 控制差速、用状态机组织运动指令、用任务队列隔离网络和电机的实时性冲突。1. 控制层在整个小车项目里的定位很多朋友做小车上来就把电机驱动模块怼到 GPIO 上写个高低电平翻转轮子转了就觉得完事了。等你真正想让它按指令跑直线、转弯、避障就会发现问题全出在控制层的设计上。1.1 控制层解决的不只是“转不转”而是“怎么转”点亮 LED 和驱动电机的本质区别在于LED 只有两种稳定状态电机却有无级的速度和持续的方向变化。小车控制层要回答三个问题往哪走方向前进、后退、左转、右转、停止走多快速度PWM 占空比什么时候切换动作之间的过渡从前进到左转轮速怎么变化这三个问题不提前设计好代码写起来就会非常痛苦。我见过有人在主循环里堆了一百多行 if-else最后没加一个转向功能就乱成一团。1.2 从指令到轮子的完整链路我习惯把控制链路画成三段,这样定位问题的时候非常方便控制指令网络/串口 → 解析和状态机 → PWM 波形 → 驱动模块 → 电机 → 轮子每段之间是解耦的网络收包只负责把数据塞进队列状态机只负责计算目标转速PWM 只负责把转速变成波形。哪一段出问题单独查哪一段就行不用把整个程序从头看到尾。1.3 这一篇的边界本篇文章聚焦“小车控制”本身不包括具体的 Wi-Fi 接入步骤、App 开发或者传感器检测。车联网之后能收到什么指令理解成“上游”控制层收到指令后怎么执行理解成“下游”。我这里做的就是把从上游拿到的那条消息真正变成轮子转动的那一段。硬件清单很简单主控板一块、带减速箱的直流电机两个、电机驱动模块一个、电池组一套。没有用复杂的舵机转向结构靠双轮差速实现转向这也是这类入门小车最通用的方案。2. 电机驱动选型与引脚分配控制层的第一个实际决定是选哪种驱动模块、怎么接引脚。这一步做错了后面代码写得再好也白搭。2.1 驱动模块大板子还是小模块市面上最常见的两种方案是 L298N 大板和 TB6612 这类小型驱动模块。我最终选的是 TB6612原因很实际对比项L298N 大板小型驱动模块TB6612 风格体积大占底板空间小方便排线压降约 2V电池利用率低约 0.5V续航更好发热电流大时明显烫手发热小逻辑电平需要 5V 逻辑和 3.3V MCU 配合麻烦直接兼容 3.3V适合场景大功率电机、教学演示低压小功率小车hi3863 / ws63 的 GPIO 是 3.3V 电平如果直接用 L298N 的老版本逻辑信号可能出现不触发的情况。TB6612 这类模块的 VCC 支持 2.7V~5.5V直接给 3.3V 毫无压力。当然如果你手头只有 L298N 也可以用只要注意共地和逻辑电平阈值就行。2.2 引脚分配上电瞬间的安全要放在第一位很多人选引脚只看“哪个空着用哪个”这是新手最容易踩的坑。GPIO 在上电瞬间的电平状态比运行时的功能更重要。如果某个引脚默认上拉而它刚好接了驱动板的 IN 脚开机的瞬间电机就会猛冲一下非常危险。我当时选了一组比较稳妥的引脚功能引脚方向说明左电机 IN1GPIO4输出控制方向高/低左电机 IN2GPIO5输出控制方向高/低左电机 PWMGPIO14输出复用为 PWM 通道右电机 IN1GPIO7输出控制方向高/低右电机 IN2GPIO8输出控制方向高/低右电机 PWMGPIO15输出复用为 PWM 通道选这组引脚的逻辑有三条。第一避开上电默认高电平的引脚。我查了原理图这几个脚复位后的默认状态是下拉或者浮空即使驱动板在有电的情况下也不会因为 MCU 还没初始化完就让电机乱转。第二PWM 引脚要确认有定时器通道。不是所有 GPIO 都能输出 PWMhi3863/ws63 的 PWM 通道是有限的。我选的 GPIO14、GPIO15 正好能映射到两个独立通道这样可以分别控制左右两轮互不干扰。第三避开下载调试串口。如果 GPIO 和固件下载的 UART0 复用每次烧录程序时电平来回跳驱动板可能会收到错误信号。轻则电机抖动重则连烧录都失败。2.3 驱动板的使能脚和逻辑电压TB6612 这类模块通常还有 STBY 引脚把它直接拉高即可正常工作。但直接接 3.3V 没问题注意别悬空。悬空状态下模块内部逻辑不确定会导致电机不受控制。至于 VM 和 VCC 分开接VM 接电池正极VCC 接 3.3V两个不要搞混。我见过有人把 VM 和 VCC 接一起结果模块冒烟最后查出来是电压超了规格。连线的时候还有一个细节MCU 的地和驱动模块的地必须共地。不共地逻辑信号根本没有参考电位PWM 波形会乱飘。我用的是同一条电池负极母线从小车底板上统一拉地线避免形成环路。3. PWM 调速的本质与实现细节有了硬件连接接下来就是软件层面最关键的部分PWM 调速。搞懂频率、占空比、死区这三个概念电机控制就成功了一半。3.1 从“开关”到“平均电压”PWM 的原理说白了就是快速开关电源让电机两端电压的平均值变化。假设电池 7.4V给电机 50% 占空比等效平均电压就是 3.7V。电机转速和平均电压近似成正比这就是调速的基础。但是电机不是灯泡它有电感、有惯性。占空比太低时电流平均值不足以克服静摩擦轮子根本不转一旦升到某个阈值轮子又会突然窜动。这个阈值就是“死区”。具体数值和电机型号、电池电压、负载有关我手里的减速电机大概在 20%~25% 占空比才开始稳定转动。3.2 频率选 10kHz 还是 20kHzPWM 频率直接影响噪音和发热。频率太低比如 1kHz电机线圈会发出明显的“滋滋”声人听着难受电机也容易发热。频率太高开关损耗增大驱动模块温度升高。我在 ws63 平台上试了 10kHz 和 20kHz实际听感 20kHz 更安静驱动模块也不怎么热所以最终定在 20kHz。频率怎么算出来假设 MCU 的 PWM 时钟源是 160MHz要得到 20kHz 的 PWM需要时钟分频加计数周期配合。通常配置接口会给频率参数你直接填 20000 就行。如果 SDK 只暴露分频和周期寄存器那就按这个公式反推周期计数值 时钟源频率 / (分频系数 × PWM频率)比如 160MHz预分频 80PWM 频率 20kHz那么计数值 160000000 / (80 × 20000) 100。占空比 50% 就是前 50 个计数输出高电平后 50 个输出低电平。3.3 软件里怎么初始化 PWM我习惯分三步初始化IO 复用配成 PWM 功能、打开 PWM 通道、设置初始占空比。伪代码如下/* 伪代码API 名称以实际 SDK 为准 */ void motor_pwm_init(void) { /* 1. 把 GPIO14 / GPIO15 复用成 PWM 功能 */ pin_set_mode(GPIO14, MODE_PWM0); pin_set_mode(GPIO15, MODE_PWM1); /* 2. 打开 PWM 通道频率 20kHz */ pwm_channel_cfg_t cfg; cfg.frequency 20000; cfg.duty_cycle 0; /* 上电先给 0防止乱转 */ pwm_open(PWM_CH0, cfg); pwm_open(PWM_CH1, cfg); /* 3. 方向引脚全部拉低刹车状态 */ gpio_write(IN1, 0); gpio_write(IN2, 0); gpio_write(IN3, 0); gpio_write(IN4, 0); }注意最后一步先把方向引脚拉到确定电平再开 PWM。这样即使 PWM 配置出现意外也不会立即让电机转动。3.4 软加减速给电机一个启动斜坡直接给电机 70% 占空比轮子会“噌”地一下弹出去车身姿态很难控制也伤电机齿轮。我加了一个简单的软启动斜坡每次目标速度和当前速度的差值分多步逼近。/* 伪代码斜坡限幅 */ #define MAX_STEP 5 /* 每次控制周期最多改变 5% */ int ramp_to_target(int current, int target) { if (current target) { current MAX_STEP; if (current target) return target; } else if (current target) { current - MAX_STEP; if (current target) return target; } return current; }控制周期 10ms 执行一次从 0% 到 70% 大约要 140ms。这个坡度的体感比较舒服起步稳健也不会迟钝。刹车同理不要直接把 PWM 清零而是快速但平缓地降下来否则小车会点头尤其在地面滑的时候容易掉头。4. 命令协议与状态机设计小车不是一台只按固定程序跑的机器它必须能响应外部指令。Wi-Fi 或串口传过来的数据杂乱无章控制程序必须把它们解析成明确动作。这部分我用了最简单的自定义协议加状态机稳定且容易调试。4.1 命令协议越简单越不容易出错有人喜欢折腾 JSON 或者很复杂的二进制协议但对控制小车来说浪费资源且没必要。我定义的指令帧就一个字节命令加一个字节速度命令含义速度参数F前进0~100B后退0~100L原地左转0~100R原地右转0~100S停止忽略A左侧微调前进时修正跑偏0~100举例来说收到F 70就代表“前进速度 70%”。这样一条指令只有 3 个字节解析简单出现乱码也容易定位。协议解析的核心是接收端按分隔符切包不认识的字符直接丢弃不要因为半包数据卡住整个解析流程。4.2 指令解析与状态机实现状态机是控制系统里最经典的结构。我把它做成四个状态STOP、FORWARD、BACKWARD、TURN。每种状态对应不同的电机输出模式/* 伪代码状态机执行逻辑 */ typedef enum { STATE_STOP, STATE_FORWARD, STATE_BACKWARD, STATE_TURN } car_state_t; void control_step(car_state_t state, uint8_t speed) { switch (state) { case STATE_FORWARD: motor_set(MOTOR_LEFT, DIR_FORWARD, speed); motor_set(MOTOR_RIGHT, DIR_FORWARD, speed); break; case STATE_BACKWARD: motor_set(MOTOR_LEFT, DIR_BACKWARD, speed); motor_set(MOTOR_RIGHT, DIR_BACKWARD, speed); break; case STATE_TURN: motor_set(MOTOR_LEFT, DIR_FORWARD, speed); motor_set(MOTOR_RIGHT, DIR_FORWARD, 0); /* 原地转向简化版 */ break; case STATE_STOP: default: motor_set(MOTOR_LEFT, DIR_STOP, 0); motor_set(MOTOR_RIGHT, DIR_STOP, 0); break; } }这套状态机最关键的优点是不会出现“左右轮一个前进一个后退”的逻辑矛盾。所有可能输入最后都落到四种明确的输出模式上排查行为问题时只需要看当前状态对不对不需要去追几十行条件判断。4.3 转向动作的可预测性小车转向有两种常见实现原地转向两轮反向和差速转向一轮减速。我把我这块小车做成差速转向因为直行时用微调可以让它跑直转弯时则变成内外侧轮速差。差速转向的实际逻辑是把基础速度设为目标速度的 70%内侧轮保持 30%这样转弯半径比较可控不会原地打转也不会转不过弯。如果你的车需要灵活掉头可以改成两轮反向只要保证转向状态下左右轮的方向组合正确即可。4.4 不要在主循环里做串口接收阻塞刚开始我犯过一个错在控制主循环里直接等串口数据结果等于用电机控制阻塞了网络接收。正确做法是用队列。串口中断或者网络回调收到数据后放进队列控制任务从队列取数据解析。这样就算一条指令解析没完成也不会耽误下一次 PWM 更新。5. 实时任务拆分别再让主循环扛所有事到了这个阶段代码量已经不小了。如果还把所有功能塞进一个 while1网络一点卡顿电机控制就跟着抖。这块板子的 SDK 默认基于一个轻量级 RTOS把任务拆开是顺手的事。5.1 单线程主循环的问题在哪想象一下主循环在等待 Wi-Fi 事件阻塞了 50ms这期间 PWM 占空比没有更新。对电机来说 50ms 的失控已经能产生明显顿挫感了。特别是两轮速度不一致时一顿一挫小车就跑偏得很厉害。所以控制任务必须和网络任务分家。控制任务只关心定时器节拍网络任务只关心收发数据两者通过任务间通信交换数据。5.2 任务怎么划分我最终划分成四个任务分工很明确walrus/network 任务负责从 Wi-Fi 或串口接收原始数据转发到命令队列。parse 任务从命令队列取数据解析成结构体再投递到控制队列。control 任务以 10ms 周期运行从控制队列取指令更新状态机和 PWM。monitor 任务可选定期打印电机电流、电池电压等状态信息方便调试。任务间通信用的就是 RTOS 的队列和信号量。控制任务只需要等待一个“有新指令”的信号收到后再从队列拿数据完全不用关心上层是怎么收到的数据。5.3 定时器中断里直接改 PWM有朋友图省事直接在定时器中断回调里改 PWM 占空比。我劝你别这么干。PWM 占空比的修改虽然是个寄存器写操作但你的控制逻辑可能需要判断当前状态、做斜坡限幅、甚至切换方向。这些逻辑放进中断里时间不固定容易影响系统稳定。我采用的方式是定时器中断只置一个标志位真正的控制逻辑放在 control 任务里跑。虽然每次控制更新会有最多一个周期的延迟但 10ms 的周期对电机来说完全够用换来的是逻辑清晰、系统稳定。控制任务的伪代码结构如下/* 伪代码控制任务主体 */ void control_task(void) { uint8_t cmd_buf[8]; while (1) { /* 等待指令信号超时 10ms */ if (queue_receive(cmd_queue, cmd_buf, 10) OK) { parse_command(cmd_buf); } /* 每 10ms 执行一次状态推进和 PWM 更新 */ update_ramp(); update_pwm_output(); } }这个结构里update_ramp()负责软加减速update_pwm_output()负责把目标速度写进实际 PWM。这样即使没有收到新指令控制任务也会按当前状态持续更新不会因为网络停顿而让小车失控。6. 实测阶段最容易翻车的几个现场代码调通不代表小车就能跑好。真正让我长教训的都是实测里的坑这里挑四个最典型的分享出来每个都是自己或者同行真实遇到过的情况。6.1 事故一电机一启动主控就重启现象小车放在地上按下启动按钮Wi-Fi 连接正常一旦发送前进指令电机刚转起来主控立刻重启。原因是供电瞬断。电机启动瞬间电流很大直接把电池电压拉低MCU 供电不足导致复位。我的电池是两节串联的锂电内阻不小电机启动电流瞬间到 2A 甚至更高电压跌落超过 MCU 的欠压阈值。处理办法分三层硬件层电机电源和 MCU 电源分开走MCU 用独立的稳压降压模块供电。储能层在电机电源两端并联一个 470uF 电解电容吸收启动瞬态电流。软件层用前面说到的软启动斜坡减少启动电流尖峰。特别是软启动效果很明显。起步速度从 40% 开始爬升电流冲击比直接给 70% 小很多。改完这三层之后主控再也没在启动时复位过。6.2 事故二PWM 一开Wi-Fi 丢包严重现象电机没转的时候Wi-Fi 收到 App 指令一切正常PWM 占空比一拉高网络立刻丢包、延迟飙升。电机是很大的干扰源。PWM 的高频开关在电机线缆上辐射电磁干扰直接影响板载天线的接收。Wi-Fi 对 2.4GHz 频段周围的噪声很敏感电机一转信噪比就崩了。我的处理思路把 PWM 频率从 20kHz 降到 10kHz干扰频谱整体下移对 Wi-Fi 的干扰明显减弱。电机线尽量短并且绞成双绞线减小辐射环面积。电机两端并联 100nF 瓷片电容吸收换向火花噪声。天线区域远离电机和驱动模块至少留 1~2cm 距离。改完之后即使电机全速跑App 端也能稳定收到实时状态回传丢包率从肉眼可感知降到几乎为零。6.3 事故三上电瞬间小车自己往前冲现象开发板刚上电程序还没开始跑小车就往前窜了一下。这个问题在第二次开机的时候差点撞到东西。原因就是我在第 2.2 节里提到的默认电平问题。GPIO 在上电后、程序初始化前有一段不确定状态如果驱动板的 IN 引脚恰好被上拉到高电平电机就会短时间通电转动。解决方法是三层保险查原理图避开上电默认高电平的引脚。驱动模块的 STBY待机引脚不直接接高电平而是通过一个 10kΩ 电阻上拉到 3.3V。但注意上电初期 MCU 还没运行直接上拉其实也等于高电平。更稳妥的做法是把 STBY 引脚接到一个 GPIO 上由程序控制上电先拉低初始化完成后再拉高。程序启动时第一步就把所有方向引脚设为确定输出PWM 通道设为 0%然后再进行后续初始化。我现在所有小车项目的启动顺序都是拉低所有使能 → 拉低方向 → PWM 输出 0 → 初始化系统 → 延时 100ms → 放开使能。这个顺序写死在启动流程里宁可慢半拍也不能让车自己冲出去。6.4 事故四左轮右轮转速不一致车子跑不直现象明明给了左右轮同样的 PWM 占空比小车在地板上却向右偏而且越跑越偏。原因很朴素两个电机虽然是同型号但个体差异明显齿轮箱摩擦力也不一样。同样的 70% 占空比左轮空载转速和右轮实测差了差不多 8%。在闭环速度反馈没做的情况下跑直线只能靠微调。我加了一个速度校准表在调试模式下分别测量左右轮的实际转速把差值折算成 PWM 补偿量。比如右轮偏快就把它目标速度乘 0.92然后硬编码到控制逻辑里。这个方法虽然笨但是实用。如果你手头有编码器后续可以上真正的闭环 PID没有编码器的话先靠占空比补偿表也能把直线跑得像模像样。校准表从 30% 到 80%每 10% 测一次存成一个数组运行时按目标速度插值取补偿值。关于调车的一个小习惯最后分享一个我个人调车时的习惯轮子悬空测试和地面测试要分开做。悬空时轮子没有负载占空比响应和地面差别很大。我在地面测试前一定先把小车垫起来用 App 完成所有状态切换验证确认方向、速度、转向逻辑都对再放到地面做负载测试。这样能省下大量因为“悬空能转但落地不跑”而产生的无效排查时间。控制这块做到这里小车的骨架就算真正立起来了。下一步可以考虑加编码器、做速度闭环或者上测距模块做自动避障。反正控制层已经拆分好加功能只是在状态机和任务框架里加新分支不用推翻重来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑