第一次在实验室值夜班我盯着走廊尽头那排仪器突然就冒出一个念头要是手边有个遥控小车能在工位之间钻一圈、看看温湿度、拍几张照片晚上巡检就不用我整个人来回跑了。这个想法落地之后就有了这篇要聊的项目——基于单片机的实验室巡检遥控小车。它可以遥控前进后退、左右转向、调速采集温湿度数据做简单的避障防撞还能预留摄像头升级位。整套方案不挑人电子相关专业的学生、嵌入式入门爱好者、要做课程设计或毕业设计的同学都可以直接参考复现。更重要的是这辆小车几乎把单片机开发的基本功都串起来了GPIO操作、定时器与PWM、串口或SPI通信、传感器时序读取、状态机设计还有电源管理和抗干扰处理。很多嵌入式面试里爱问的底层概念在这辆车上都是真实存在、需要亲手调通的问题。1. 这个项目到底在解决什么问题1.1 实验室巡检场景的真实需求先别急着选芯片、画原理图做项目第一步是把场景看明白。实验室和普通家庭环境不太一样地面大多是地砖或防静电地板仪器台之间过道窄地上经常有线缆、电源插排偶尔还有小门槛或斜坡需要巡检的通常是温湿度环境、设备工作状态、烟雾报警、门窗异常或者干脆就是帮老师看一眼某台设备亮没亮灯。这种环境下大轮子越野车反而不合适因为太宽进不去工位纯靠自动导航又太复杂前期没必要。手动遥控加传感器采集是性价比最高的起步方案人在门口或者楼上用遥控器或手机控制小车进到仪器区实时读取环境数据再通过屏幕确认现场情况。1.2 把需求拆成一张功能清单我的习惯是画一张简单表格把“想做的事”翻译成“需要什么硬件”功能需求对应模块技术要点前进、后退、左右转、差速转向两个直流减速电机 电机驱动板PWM调速、方向控制无线遥控nRF24L01 或蓝牙模块通信协议、数据校验温湿度采集DHT11 或 DHT22单总线时序、状态机读取防撞/防跌落超声波或者红外对管测距门槛、自动刹车逻辑电池状态显示单片机ADC采样电池电压分压电阻、滑动平均云台摄像头升级舵机 串口摄像头舵机PWM控制、图像数据透传这个表格看着简单但每个点展开都能讲很多。我实际做完之后的体会是不用追求一步到位先把手动遥控和温湿度采集跑通再一点点加避障、摄像头项目就变得非常耐做。1.3 我的整体技术路线这个项目我最终敲定的方案是STC8 系列增强型 51 单片机做主控搭配 TB6612 电机驱动模块两个 N20 减速电机带编码器遥控端用 nRF24L01 无线模块传感器配 DHT11、红外避障模块电源用两节 18650 串联加 5V 稳压。这个组合是兼顾了入门难度、可靠性和扩展性之后选出来的后面每个模块我都会说清楚为什么这么选。2. 硬件选型哪些模块是必须的哪些是踩坑换来的2.1 主控芯片51够用STM32更舒展主控是整个小车的大脑。对这个项目来说经典 51 单片机比如 STC89C52完全能跑因为遥控解析、PWM输出、传感器读取这些功能并不需要多高的主频。但如果你想把速度闭环、多路传感器、串口图传都塞进去51 的资源就会比较紧张。我当时用的是 STC8 系列增强型 51主频可以跑到 24MHz 以上内置 ADC、硬件 PWM、串口、SPI一个芯片搞定所有外设价格还便宜非常适合做完整方案。当然如果你对嵌入式开发有更长远的规划直接用 STM32F103C8T6 也可以。它的定时器资源更丰富独立 ADC 通道多后续跑 RTOS 也有余量。选型逻辑是这样如果项目目标是快速出一台能跑的课程设计小车STC8 就够了如果目标是做一个可以持续迭代的嵌入式学习平台STM32 更合适。我不建议一开始就上树莓派或者嵌入式 Linux 板子因为单片机底层的 GPIO、中断、状态机都没理解透就跳过去后面很容易虚。2.2 电机驱动L298N、TB6612与DRV8833的取舍电机驱动芯片是新手特别容易踩坑的地方。很多教程推荐 L298N但我实际用过之后觉得它在这类小车上不太划算内部是三极管 H 桥饱和压降接近 2V本来电池 7.4V到电机上可能只剩 5.5V发热还大体积也大装在小车底盘上很占地方。我更推荐 TB6612FNG。它是 MOSFET H 桥导通压降低发热小支持 100kHz PWM体积只有一个拇指盖大。配合 N20 减速电机电机响应非常跟手。DRV8833 也是不错的选择电压范围宽内置电流限制只是市面上模块相对少一些。选型对比可以参考下面这张表芯片类型压降最大电流PWM频率适合场景L298N双极性H桥约2V2A低频大功率电机、教学演示TB6612FNGMOSFET H桥约0.3V1.2A100kHz小型小车、精密调速DRV8833MOSFET H桥约0.2V1.5A100kHz低压供电、微型电机我这辆车左右轮各用一个 N20 减速电机额定电压 6V堵转电流大概 1A 左右TB6612 的 1.2A 连续电流是能扛住的。如果你的电机更大更猛那就老老实实用大电流驱动板不要硬缩。2.3 无线方案蓝牙、2.4G模块或成品遥控器遥控方案有三种常见路线我都在不同项目里试过。手机蓝牙HC-05/HC-06最适合演示手机装个蓝牙串口助手就能控制而且代码写起来很简单本质就是把串口收到的字符串解析成指令。缺点是配对和波特率偶尔会闹脾气距离近了没问题远了就断。nRF24L01 是更“嵌入式”的选择。它工作在 2.4GHz通过 SPI 接口读写支持一对多通信协议完全自己定灵活性极高。加 PA 和 LNA 的版本能做到百米级别室内穿一堵墙也没压力。缺点是初次调试 SPI 时序需要一点耐心模块对电源质量比较挑剔。成品 315M/433M 遥控器套装最省事买回来直接接 IO 就能用但只能单向控制没有数据回传小车把温湿度发不回来所以我没选。我最终选了 nRF24L01 方案原因很简单它能把“发射端按键状态”和“接收端小车状态”双向打通后续做巡检数据上报也顺手通信协议属于嵌入式面试里的高频考点做完这个项目再聊 nRF24L01 就非常有底气了。2.4 传感器配置避障、温湿度、测速和云台传感器这块我分了四个层次按需配置避障防撞用的是 HC-SR04 超声波模块测量距离 2cm 到 400cm放在车头正前方检测到 30cm 内有障碍就自动减速或停车。超声波模块 5V 供电触发脚给 10us 高电平回响脚输出 50us 到 25ms 的高电平单片机通过定时器测高电平时间就能换算距离。车底四周我还会放几个红外对管做防跌落尤其是桌面级的巡检场景防止小车从台面边缘掉下去。温湿度采集用 DHT11便宜、资料多、单总线协议非常锻炼人。虽然 DHT11 采样周期是 1 到 2 秒精度约 ±2℃对实验室环境监测足够了。想要更高精度可以换 DHT22代码逻辑基本一致。测速用的带霍尔编码器的 N20 电机编码器输出两路正交信号单片机通过外部中断计数就能算出轮子转速后面做 PID 速度闭环必备。云台部分用一个 SG90 舵机就够了给舵机输出 50Hz、脉宽 0.5ms 到 2.5ms 的 PWM就能控制角度摄像头架在上面可以左右扫视。3. 电路连接与供电设计九成故障都出在这层3.1 主控、电机、传感器三者必须分开供电很多小车跑不动的根源不是程序写错而是电源设计一团糟。电机启动瞬间电流很大如果主控和传感器都依赖同一路稳压输出电机会把电压瞬间拉低单片机直接复位或者传感器读数乱飘。我的供电结构是这样设计的两节 18650 锂电池串联得到 7.4V 左右的电池电压7.4V 直接接到 TB6612 的 VM 引脚给电机供电7.4V 再进一个降压模块输出稳定的 5V给主控、nRF24L01、DHT11、超声波模块供电如果主控是 3.3V 的 STM32就在 5V 后面再增加一级 3.3V 稳压。三个部分之间必须共地也就是电机驱动 GND、稳压模块 GND、主控 GND 全部连到一起。共地是嵌入式项目里最基础也最容易被忽视的一步地线不共遥控模块可能频繁乱码传感器数值会像过山车一样跳。TB6612 的具体接线可以照下面这张表TB6612引脚接到哪里VM电池正极 7.4VVCC5V 稳压输出GND电池负极PWMA主控定时器PWM引脚AIN1/AIN2主控 GPIO控制电机A方向PWMB主控另一个PWM引脚BIN1/BIN2主控 GPIO控制电机B方向AO1/AO2右侧电机正负BO1/BO2左侧电机正负STBY接主控高电平使能芯片3.2 电平匹配3.3V和5V混接怎么处理如果你用的是 STM32 这类 3.3V 主控和 5V 的超声波、DHT11 混接时会遇到电平不匹配的问题。5V 传感器输出的高电平可能把 3.3V 单片机的引脚烧掉反过来3.3V 单片机输出的高电平又可能驱动不了 5V 传感器。处理办法有两个如果是输入信号比如超声波回响脚可以用两个电阻分压把 5V 降到 3.3V如果是双向信号比如 I2C、单总线就加一颗电平转换模块或者干脆选宽电压版本的传感器。我实际做 STM32 版本时直接买了一排 4 路的 MOS 管电平转换模块成本几块钱接线还整齐。如果你用的是 STC8 系列 51 单片机它不少型号是宽电压供电可以工作在 2.0V 到 5.5V这种情况下直接整板跑 3.3V就省去很多电平转换的麻烦。3.3 电机干扰从源头掐掉噪声电机是车上最大的噪声源换向火花会在电源线上叠加尖峰轻则传感器数值漂移重则无线模块直接断连。我处理干扰就三步第一每个电机正负引脚之间并一颗 104 瓷片电容让换向火花的高频分量就地吸收第二驱动板电源输入端并联 470uF 电解电容和 0.1uF 瓷片电容作为储能和滤波第三传感器杜邦线和电机线在底盘上分两侧走实在需要交叉就垂直交叉避免平行贴在一起长距离走线。这几步做完DHT11 读数稳定很多nRF24L01 也不会动不动丢包。3.4 关于低电平驱动的一个多年老问题有同学问我为什么 51 单片机驱动 LED 时不能用输出高电平的方式驱动这个问题放到项目里其实很典型。51 的 P1、P2、P3 口内部结构是准双向口输出高电平时靠内部上拉电阻拉电流能力非常弱只有两百微安左右推不亮 LED输出低电平时灌电流能力却能达到十几毫安。所以正确接法是把 LED 阳极接 VCC、阴极经过限流电阻接到单片机引脚单片机引脚输出低电平时 LED 点亮这就是经典的灌电流驱动方式。这个思路延伸到整个项目里就是别指望单片机 IO 直接驱动蜂鸣器、继电器、风扇这些功率器件一律要加三极管或 MOS 管做开关放大。驱动板选型也遵循同样的道理一级一级往外推。4. 软件框架状态机、非阻塞扫描与指令协议4.1 为什么main()里不能用一堆delay()很多入门教程喜欢在循环里用 delay 控制动作小车前进一秒、停一秒、后退一秒。这么做在单一任务下问题不大但一旦接入遥控、传感器、无线通信delay 就是灾难执行 delay 期间程序完全卡死遥控指令收不进来传感器数据得不到及时处理车身在转向时也听不到新指令。我的解决办法是建立“主循环 定时器中断 状态机”的软件框架。定时器每隔 1ms 产生一次中断作为系统心跳主循环只做一遍所有状态的检查与更新需要延时的操作用软件定时计数代替硬 delay。这套思路就是嵌入式里说的非阻塞调度也是热词里“嵌入式按键非阻塞扫描”的底层逻辑。举例来说按键扫描如果每次循环都去读一次 IO 并判断边沿很容易漏按更好的做法是在 10ms 软件定时器里做扫描检测到电平变化后启动消抖计时再判断按键是否稳定实现还顺带处理了长按、短按的区分。遥控接收也一样不在中断里做复杂逻辑只把字节收进缓冲区主循环统一解析。4.2 无线遥控指令协议如何解析一帧可靠的数据nRF24L01 收到的原始数据是字节流直接拿第一个字节当指令碰到垃圾数据就会误动作。所以我自定义了一个很轻量的协议帧字节内容0帧头 0xAA1帧头 0x552主指令前进/后退/转向/停止/巡检3速度值0到1004附加参数比如转向角度、云台角度5校验字节前三个字节异或结果解析代码用状态机写每一帧只消费一个字节不会因为意外数据卡死typedef struct { uint8_t cmd; uint8_t speed; uint8_t param; } RcCmd_t; RcCmd_t rc_cmd; uint8_t frame_ready 0; uint8_t rx_buf[3] {0, 0, 0}; uint8_t rx_state 0; void rc_parse_byte(uint8_t byte) { switch (rx_state) { case 0: if (byte 0xAA) rx_state 1; break; case 1: if (byte 0x55) rx_state 2; else rx_state 0; break; case 2: rx_buf[0] byte; rx_state 3; break; case 3: rx_buf[1] byte; rx_state 4; break; case 4: rx_buf[2] byte; rx_state 5; break; case 5: if (byte (rx_buf[0] ^ rx_buf[1] ^ rx_buf[2])) { rc_cmd.cmd rx_buf[0]; rc_cmd.speed rx_buf[1]; rc_cmd.param rx_buf[2]; frame_ready 1; } rx_state 0; break; default: rx_state 0; break; } }每次从 nRF24L01 的接收 FIFO 读出数据就调用一个rc_parse_byte()主循环检测到frame_ready再更新运动状态。这样做的好处是不管发射端传什么乱七八糟的数据只要帧头校验不过小车就纹丝不动安全性和可靠性都高了很多。4.3 运动状态机把操作语义化小车不能收到一个前进指令就一直前进还得处理转向、停止、避障打断、用户手动抢控等场景。把这些行为串起来最优雅的方式就是状态机。我先定义几个运动状态typedef enum { S_IDLE, S_FORWARD, S_BACK, S_LEFT, S_RIGHT, S_AUTO_SCAN } MotionState; MotionState motion S_IDLE;主循环里反复执行motion_update()根据当前状态设置两个电机的占空比void motion_update(void) { switch (motion) { case S_IDLE: motor_set(0, 0); break; case S_FORWARD: motor_set(base_speed, base_speed); break; case S_BACK: motor_set(-base_speed, -base_speed); break; case S_LEFT: motor_set(base_speed * 0.5, base_speed); break; case S_RIGHT: motor_set(base_speed, base_speed * 0.5); break; case S_AUTO_SCAN: auto_scan_update(); break; default: motor_set(0, 0); break; } }状态之间的转移规则是新遥控指令到来时优先执行用户指令超声波检测到障碍时从任何非停止状态切到 S_IDLE 并刹车退出避障状态后根据最近一次遥控指令回到目标状态。这其实就是一个事件驱动的四层转移表我整理成一张表格贴在调试板旁边了当前状态事件下一状态任意收到前进指令S_FORWARD任意收到停止指令S_IDLE任意超声波距离阈值S_IDLE同时刹车S_IDLE距离正常且收到前进S_FORWARDS_FORWARD收到左转S_LEFTS_LEFT收到右转S_RIGHT4.4 PWM调速中的死区、软启动和速度闭环电机调速靠 PWM但 PWM 不是简单给个占空比就能跑得顺。N20 减速电机存在静摩擦和启动电压占空比低到一定值根本不转这个区域叫死区。如果程序里把占空比 10% 当作“慢速档”电机可能纹丝不动看起来就像死机一样。处理死区最直接的办法是测试出本车电机的启动占空比。上电后从小往大调记下轮子刚好转起来的值比如是 22%那么所有指令的最小占空比限制在 30% 以上。同时加一个软启动逻辑从停止到目标速度不是瞬间拉满而是每隔 10ms 递增一个步长比如 30 毫秒内从 20% 升到目标值。这样小车起步平滑齿轮冲击也小转向的时候车身不会点头。如果上了编码器电机后续速度闭环几乎是必然需求。用外部中断对编码器 AB 相计数计算实际轮速再和目标速度做 PID。比例系数 Kp 从 0.3 起步先观察响应有没有振荡再加积分系数 Ki 消除稳态误差最后用微分系数 Kd 抑制超调。需要特别注意PID 在单片机里要在固定周期执行我一般放在 20ms 定时器里跑周期不同参数不能直接照搬别人的车。4.5 DHT11和编码器的非阻塞处理思路DHT11 的单总线时序最考验状态机基本功。正常读取数据单片机要拉低 18ms 再释放等传感器响应然后接收 40bit 数据整个过程约 4 到 5ms。如果直接用 delay 阻塞读取小车在这 5ms 里完全失去响应遥控就会出现明显的“卡顿感”。我处理 DHT11 的方式是把读取过程拆成多个阶段再用定时器扫状态机typedef enum { DHT_START_LOW, DHT_START_RELEASE, DHT_WAIT_RESPONSE, DHT_RECEIVE_BITS, DHT_DONE } DhtState;定时器每 20us 进入一次中断状态机根据当前状态和时间片去拉低引脚、释放引脚、读取电平、记录脉宽。整个过程不占用主循环主循环照样处理遥控指令。编码器计数也用同样的思路外部中断只负责累加计数不在中断里做速度计算速度值由主循环每 50ms 读取一次增量就算出来。5. 组装联调时踩过的坑从下载失败到遥控失灵5.1 单片机下载失败的完整排查链路如果你是第一次自己接线大概率会在下载程序这一步卡住。我遇到过的下载失败场景有这么几种STC 单片机点击下载后需要冷启动也就是先点“下载/编程”按钮再给板上电顺序反了会一直提示握手失败USB 转 TTL 模块的 TX 要接单片机的 RXRX 接单片机的 TX很多人接反了数据根本传不进去还要检查 GND 有没有共地USB 转 TTL 如果没和板子共地串口是完全不通的。如果是 STM32BOOT0 要拉到高电平才能进入串口下载模式下载完再拉回低电平复位运行。用 ST-Link 在线调试会更方便不用管 BOOT0但接线要确认 SWDIO、SWCLK、GND、3.3V 四根线。另外CH340 这类 USB 转串口芯片虽然便宜但有些劣质模块波型不干净STC 单片机对波特率非常敏感我遇到过 115200 下载失败、换成 9600 一次成功的案例。遇到下载失败优先级永远是查线序、查共地、查冷启动时序最后才换波特率。5.2 电机带不动、车身抖动的供电检查小车低速能走一加速就抽搐或者倒车时整车抖得像筛子八成是供电瓶颈。先拿万用表测电池空载电压再测电机启动瞬间的电压如果从 7.4V 掉到 5V 以下问题就清楚了要么是电池旧了内阻大要么是导线太细压降太大要么是电机并联电容没加导致驱动板误触发。我的处理方案是换 22AWG 硅胶线重新给电池和驱动板供电电机电源端加 1000uF 电解电容驱动板 PWM 频率从 10kHz 调到 20kHz抖动立刻缓解。还要注意一点别用面包板供电给两个电机跑面包板触点电阻非常大电流一大就发热电压也跟着塌。5.3 遥控乱码与信号干扰的处理nRF24L01 模块最典型的问题就是电源不干净。模块发射瞬间电流能到十几毫安如果 3.3V 稳压输出纹波大模块会疯狂丢包甚至死机。我给模块的 VCC 和 GND 之间就近焊了 10uF 电解电容加 0.1uF 瓷片电容电源就稳了很多。协议上我也加了自动重发和丢包统计发射端连续 3 次没收到 ACK就判断接收端失联把油门归零防止小车失控冲出去。如果是蓝牙方案乱码常常是波特率不匹配。HC-05 默认 9600但有些模块被别人改成 38400或者上位机选错了波特率屏幕上全是乱码。解决办法是按住模块按键上电进入 AT 模式重新配置成统一波特率。5.4 一起典型的地环路问题我调试云台舵机时遇到过非常诡异的情况只要舵机一旋转DHT11 温度值就跳高两三度超声波距离也开始乱漂。单独测都没问题装在车上就出事。后来排查发现是舵机的电源负极和传感器地线形成了环路舵机启动的大电流在地线上产生压降导致传感器参考地电位被抬升。解决方案很简单舵机单独一路电源负极在主控板附近单独一点接地避免所有地线都从一个长环路里来回串。从那以后我给自己定了条规矩底盘上的所有模块地线尽量走星型一块板子一个接地点回电池负极不要一条长线串全部。5.5 常见问题速查表现象排查顺序解决手段程序下载失败线序、共地、冷启动重新接线按顺序上电降波特率电机不转供电、使能引脚STBY、死区检查STBY拉高提高PWM下限电机抖动电容、电池、PWM频率并104电容加电解电容调PWM遥控断连天线接触、电源纹波、距离模块就近加电容降低发射功率测试传感器读数飘地环路、线材干扰星型接地信号线和电机线分离6. 这辆车还能怎么长后续功能扩展思路6.1 自动巡航与循迹控制遥控跑通之后可以让小车自己干活。底盘中间装两到三个红外循迹模块沿着实验室地面上贴的黑色胶带或白线走这就是最简单的循迹车。循迹的控制也可以用状态机扩展检测到左传感器压线就往左修正右传感器压线就往右修正两个都压在线上就直行。再进一步利用编码器做里程计记录左右轮转数就能粗略估算小车走了多远配合预设路径做“一来一回”的自动巡检。6.2 环境监测与报警联动现有的 DHT11 只是数据采集真正的巡检要带报警逻辑。我后面在车上加了一个 MQ-2 烟雾传感器和一个小蜂鸣器程序里设两个阈值温度超过 35℃ 或者烟雾浓度超过阈值小车立即停车蜂鸣器响三声同时通过 nRF24L01 把报警状态发到接收端手柄手柄上对应的 LED 点亮。这样值班的人不用一直盯着屏幕异常时自然会被提醒。6.3 图传和嵌入式AI方向如果想把车升级成“带眼睛”的巡检平台可以用 ESP32-CAM 做摄像头模组让它在小车上通过 Wi-Fi 把实时图像传到手机或电脑。这时候主控建议升级成 STM32F407 或者 ESP32因为图像压缩和网络协议栈需要更强的处理器。再往后配合嵌入式 AI 推理比如对烟雾、火焰做目标检测甚至巡检时自动识别实验室设备面板上的指示灯状态这其实就是把项目从“遥控车”往“智能巡检机器人”的方向推进了。单片机上可以先跑轻量模型重一些的推理放到后端服务器小车负责采集和上传。6.4 从单片机迁移到嵌入式Linux平台当功能多到单片机撑不住时自然就会往嵌入式 Linux 平台迁移。i.MX6ULL 这类核心板、树莓派或者香橙派跑 Linux 系统后GPIO、I2C、SPI、摄像头采集都有现成驱动Python 或 C 程序的开发效率比裸机高很多。但迁移的时候有一件事是通用的老单片机上的状态机框架、协议帧解析、避免阻塞式 delay 的思路在 Linux 应用层里依然成立。我用 Python 重写小车主程序时核心逻辑几乎没有变化只是把中断换成线程、把寄存器操作换成 sysfs/libgpiod。做完这辆小车再回头看整个项目里真正难的不是某一颗芯片或某一个模块而是把“供电分层、状态机驱动、非阻塞调度”这三件事想明白。我后来又把摄像头、烟雾传感器、喇叭全挂上去前面搭的框架没有推翻一次就顺利跑通。最后留一个小建议底盘固定电池和传感器时尽量用扎带、骑马扣或者 3D 打印的卡座调试期拆装比胶水省心太多等你哪天真要把模块换位置就知道这个细节有多值了。