本期(最终期)核心:人机交互怎么设计?LoRa 摇杆如何映射成速度/转向?菜单系统怎么组织?以及整套系统在实车调试中沉淀下来的通用经验。
1. 先想清楚:人机交互的价值
一辆小车只有算法是不够的——它需要被控制、被配置、被调试。这台轮腿车有两套交互手段:
- LoRa 无线遥控器:实车操控、路径录制;
- IPS200 屏 + 按键:菜单配置、状态显示。
交互系统的设计目标就一句话:让"操作"和"调试"都能在车边上完成,不用反复烧程序。
2. LoRa 遥控:摇杆数据怎么变控制指令
遥控器通过 LoRa 发来摇杆原始值(joystick[2],两个摇杆)和按键状态。代码在remote.c里做两件事:
2.1 摇杆线性映射
/* 摇杆1:前后推 → 目标速度 */int16_tremote_map_value(int16_tx,int16_tin_min,int16_tin_max,int16_tout_min,int16_tout_max){return(x-in_min)*(out_max-out_min)/(in_max-in_min)+out_min;}把摇杆的模拟范围(如 -2010~2000)线性映射到速度输出范围(如 -600~600):
car.target_speed=remote_map_value(lora_driver_get_joystick(1),JOY1_IN_MIN,JOY1_IN_MAX,JOY1_OUT_MIN,JOY1_OUT_MAX);2.2 摇杆增量转向(偏航)
摇杆2 不直接给"目标偏航角",而是给"偏航增量"——打杆就往那个方向慢慢转,回杆保持当前方向:
floatremote_update_angle_incremental(int16_tjoy_raw,float*cur_angle){floatdelta=0.0f;if(joy_raw>JOY2_DEADZONE){delta=(float)(joy_raw-JOY2_DEADZONE)/(JOY2_IN_MAX-JOY2_DEADZONE)*MAX_ANGLE_STEP;}elseif(joy_raw<-JOY2_DEADZONE){delta=(float)(joy_raw+JOY2_DEADZONE)/(-JOY2_IN_MIN-JOY2_DEADZONE)*MAX_ANGLE_STEP;}*cur_angle+=delta;/* 累加 → 增量转向 */while(*cur_angle>180.0f)*cur_angle-=360.0f;/* 归一化 */return*cur_angle;}为什么转向用"增量"而速度用"绝对"?
这非常符合操控直觉:
- 速度:摇杆推到哪,速度就要到哪 → 绝对映射(推杆即加速,回中即停);
- 转向:打杆只是"给个转动的角速度",松杆后希望车保持方向,而不是回正 → 增量映射。
一句话:位置量用绝对映射,速度/方向量用增量映射。这个原则做任何遥控/航模都适用。
2.3 超时保护
遥控链路断了车必须停,不能继续开。isr 里做超时检测:
voidlora_driver_tick(CarState_t*s){s->lora_timeout_cnt++;if(s->lora_timeout_cnt>LORA_TIMEOUT_MS/10){/* 500ms 没收到数据 */lora3a22_state_flag=0;/* 标记失联 */s->lora_timeout_cnt=0;}}主循环/中断里一旦发现失联:
if(!lora_driver_is_connected()){car.target_speed=0;/* 失联即停 */}无线设备必须有超时保护,失联必须有个安全默认动作(这里是停车)。这是遥控系统的安全底线。
3. 按键系统:消抖与"单次触发"
车上有 4 个按键 K1~K4。按键最烦的是抖动和长按连发。代码用两招解决:
intkey_driver_scan(void){/* 读 GPIO */ints1=hal_gpio_get(P20_1);...if(s1==1&&s2==1&&s3==1&&s4==1)return0;/* 全高 = 无按键 */hal_system_delay_ms(20);/* 20ms 软件消抖 *//* 再读一遍,确认按下 */s1=hal_gpio_get(P20_1);...if(s1==0)return1;.../* 返回按键号 */return0;}intkey_driver_get_single(void){staticintlast_key=0;intcur=key_driver_scan();if(cur!=0&&last_key==0){/* 只在"从未按下→按下"沿触发 */last_key=cur;returncur;}elseif(cur==0){last_key=0;}return0;}- 软件消抖:读两次、间隔 20ms,两次都低才算按下;
- 边沿触发:
last_key记住上次状态,只在"释放→按下"的上升沿返回一次,长按不会连发。
消抖 + 边沿触发,是按键系统的标配。尤其菜单导航里,长按连发会让菜单乱跳,边沿触发必须做。
4. 菜单系统:页面状态机
菜单用"页面"管理,本质上又是一个状态机:
voidmenu_process(CarState_t*s){intkey=key_driver_get_remote_or_key(s);/* 支持遥控或按键 */if(s->menu_page==PAGE_MAIN){if(key==1)s->menu_page=PAGE_RUN;/* K1: 运行菜单 */if(key==2)s->menu_page=PAGE_DEBUG;/* K2: 调试菜单 */if(key==3){s->run_flag=1;s->menu_page=PAGE_NONE;}/* K3: 开始复现 */if(key==4){s->run_flag=2;s->menu_page=PAGE_NONE;}/* K4: 遥控模式 */s->menu_need_redraw=1;}...}voidmenu_display(CarState_t*s){if(!s->menu_need_redraw)return;/* 只在需要时重绘,省屏寿命/CPU */...if(s->menu_page==PAGE_MAIN){display_driver_show_str(...);...}}菜单设计的两个要点
- 页面状态机:每个页面响应按键跳转,
run_flag切换"菜单态/运行态"——菜单和运行是互斥的模式,用标志位切换; - 脏标记重绘:
menu_need_redraw标志,只有页面切换/参数变化才重绘,平时不刷屏。
菜单系统的本质就是"状态机 + 脏标记"。从单页菜单到多级菜单,骨架都是这个。
5. 参数配置哲学:集中在一个头文件
所有可调参数集中在config/car_config.h——这是全项目最好用的一个文件:
/* PID */#definePID_PITCH_KP210.0f#definePID_PITCH_KI0.1f#definePID_PITCH_KD50.0f/* 腿部机械常数 */#defineLEG_L16.0f#defineLEG_L53.7f/* 跳跃时序 */#defineJUMP_T_EXTEND100#defineJUMP_T_RETRACT60设计规则(代码注释里明明白白写着):
- 所有可调参数集中定义,不在 .c 里出现"魔法数字";
- 每个参数配注释说明含义;
- 需要运行时改的参数(曝光、阈值、路段时长)存 Flash,开机读回(第4期)。
为什么集中?调参 = 改一个头文件 = 全项目可见。散落在十几个 .c 文件里的魔法数字,是嵌入式项目最大的维护噩梦。
6. 实车调试心得(踩坑总结)
最后分享这套系统实车调试沉淀下来的通用经验:
6.1 分模块验证,别一次全开
- 先只测平衡(电机不动、只看俯仰),再测转向,再测遥控,最后才测路径/视觉;
- 一次只调一个变量,其他参数全部固定。
6.2 传感器数据先"打印"再"信"
- 任何算法上阵前,先把 raw 数据打到屏上/串口人工检查一遍;
- IMU 是否零漂、GPS 星数够不够、摄像头阈值合不合适——不验证数据就调算法,等于盲调。
6.3 时间驱动的开环动作,先在地面做
- 跳跃这类动作,先架空轮子看腿部时序是否合理,再上真车;
- 跳跃时序
JUMP_T_*这种参数,单独抽出单独调,别和平衡耦合。
6.4 保护机制越早加越好
- 按键急停、遥控失联停车、腿长越界钳制——这些保护一开始就加,后期省下无数麻烦;
- 出现 NAN、越界,全部要有兜底(第3期讲的
ik_solveNAN 处理)。
6.5 参数可持久化,调参才高效
- 曝光、阈值、路段时长这些要存 Flash、菜单可改、开机读回;
- 不然每改一次参数烧一次程序,调试效率极低。
7. 系列总结
回到第1期的一句话:思路大于实现。回头看这 7 期,其实核心思路就几条:
| 主题 | 核心思路 |
|---|---|
| 架构 | 分层 + 状态体通信 + 分级调度 |
| 平衡 | 级联 PID,越内层越快,符号解耦 |
| 腿部 | IK 逆解 + 时间状态机跳跃 |
| 路径 | 投影 → 骨架 → 样条 → 纯追踪 |
| 跟踪 | 三问(在哪/往哪/多快)+ 路段状态机 |
| 视觉 | 二值化 → 边线 → PD 修正叠加 |
| 交互 | 绝对/增量映射 + 状态机 + 集中配置 |
代码会过时,架构和思路不会。把这套骨架换成别的 MCU、别的传感器、别的小车,照样能跑。
8. 后记
本项目代码已开源在 GitHub:
https://github.com/L-F-S-max/wheel-legged-robot
欢迎 Star / Issue 交流。如有问题或想深入某个模块,欢迎评论区留言。