
简介基于PIC16F877A的无线遥控小车设计文档适合电子、嵌入式方向的高校学生与初学者参考内容涵盖芯片选型、无线收发模块对比、电机驱动与传感器扩展并融合了基于AT89C52与ISD2560的语音存储播放系统及LCD显示模块可用于课程设计或创新项目立项。压缩包包含1个doc文件大小2.13MB文档内含章节目录、原理框图与设计思路便于按章节研读。已有165人在CSDN学习浏览。读者可从中获得从硬件选型到系统集成的完整方案包括PIC与51系列单片机的对比分析、433MHz/2.4GHz射频遥控应用、语音播报及避障功能的实现思路对理解单片机控制与无线通信结合具有直接参考价值。1. 为什么“PIC16F877A无线遥控小车”值得自己重新做一遍PIC16F877A 是一颗在很多高校教材里出现二十多年的8位单片机资料多、引脚好买但正因为“老”不少新入行的同学以为它只能点亮流水灯。实际上用这一颗芯片做一辆无线遥控小车恰好能体验到嵌入式系统里最核心的三件事异步协议、外部中断和PWM控制。标题里的“无线遥控小车”听起来像课程设计但真正动手后会发现难点不是让电机转而是让无线模块不产生乱码、让解码不丢帧、让电源在电机启动时不把单片机复位。下文按一条完整链路来写从无线链路选型、硬件连接到在 PIC16F877A 上用定时器和外部中断自己实现一套遥控协议最后给出调试距离和干扰的常见手段。适合正在做相关课题的学生也适合想用这颗低成本芯片做无线遥控产品的开发者。2. 无线遥控小车的系统组成与PIC16F877A的资源分配2.1 小车的基本组成发射端、接收端、运动执行器先明确定义一辆遥控小车不是一块电路板而是两个相互独立的节点加一个执行机构。发射端是手持设备包含4个方向按键、一片PIC16F877A、一个433MHz ASK发射模块以及电池接收端安装在小车上包含433MHz接收模块、第二片PIC16F877A、一个直流电机驱动桥和两个直流减速电机。两个节点之间只通过空气传输信号没有地线连接因此电源隔离、独立稳压和共地处理都比“单板调试”要复杂。为什么要在发射端和接收端各用一片PIC16F877A而不是在发射端用更便宜的小单片机主要原因是减少学习成本两边用同一块芯片烧录器、引脚定义、编程习惯完全一致课程设计或原型验证时只需要维护一套工程框架。如果你觉得浪费发射端可以换用PIC12F675这类8脚芯片本文逻辑不变但代码和引脚映射需要调整。2.2 无线链路选型433MHz ASK模块为什么是默认答案无线遥控的通信链路有几种常见方案315M/433M ASK模块、nRF24L01、蓝牙串口模块、WiFi模块。对于这个标题最经典的搭配是433MHz ASK发射/接收模块理由有四点一是成本低一对模块通常不超过十元二是数据率低但足够遥控指令每秒只需要50帧而ASK模块支持1~10kbps三是接收输出是直接可读的TTL电平不需要像nRF24L01那样处理SPI时序和寄存器配置四是它和PIC16F877A的5V电平完全兼容不需要额外电平转换。选型时要区分超再生和超外差接收模块。超再生便宜约5元级别但灵敏度低、噪声大超外差接收模块价格高一些抗干扰和接收距离明显更好推荐用在遥控车上。下表给出典型参数实际模块略有差异但可以参考。参数超再生接收模块超外差接收模块说明工作电压4.5~5.5V4.5~5.5V都用5V供电3.3V会明显掉距离接收灵敏度-105dBm-112dBm灵敏度差7dB距离约差一倍待机输出不定噪声高电平超外差静噪后输出稳定高电平价格约5元约15元预算允许优先超外差抗同频干扰差好室内多遥控器时差异明显如果是做量产或者需要跳频抗干扰nRF24L01会是更好的选择但它在PIC16F877A上的实现要多写SPI驱动、自动重发和加帧序号等逻辑复杂度提升一个数量级。对标题中的“设计”而言433MHz ASK方案才是最常见、最容易一次跑通的路线。2.3 引脚分配与电源预算先算完再接线PIC16F877A共有五个IO口端口复用很多建议在写代码之前就把每个引脚的角色固定下来。下面是一份经过多次验证的分配表既照顾了外部中断需求也兼顾了PWM输出。节点引脚功能配置说明发射端RA0~RA3四个按键输入外接10kΩ上拉按键接地发射端RC6编码数据输出作为普通IO输出连接到发射模块DATA发射端RC7发射模块电源控制通过三极管控制模块VCC低功耗时断开接收端RB0接收模块DATA输入启用外部中断测量脉冲宽度接收端RC1PWM输出至驱动ENA配置CCP2接收端RC2PWM输出至驱动ENB配置CCP1接收端RD0~RD3驱动方向控制IN1~IN4普通IO输出电源预算要分节点计算。PIC16F877A在4MHz时钟下工作电流约2mA接收模块耗电6~8mAL298N静态电流约15mA电机堵转瞬间电流可超过1A。因此建议用两节锂电池串联得到7.4V经LM7805降压到5V给单片机、接收模块和逻辑电路驱动桥直接接7.4V并和5V共地否则电机PWM信号没有参考地转速会不受控制。发射端建议用一节9V叠层电池或两节AA电池如果使用9V电池在模块供电回路串入一个10Ω~22Ω电阻和100μF电解电容避免发射电流突变导致单片机复位。引脚初始化代码如下配置了发射端的IO方向和默认电平。// 发射端IO初始化XC8编译器4MHz晶振 void tx_init(void) { ADCON1 0b00000110; // PORTA和PORTE全部作为数字IO TRISA 0b00001111; // RA0~RA3设为输入其他输出 PORTA 0; // 清空端口值按键未按下为高 TRISC 0; // PORTC全部输出或只设置RC6/RC7 PORTC 0; // RC6拉低RC7关断发射模块电源 }这段代码里ADCON1的设置很关键。PIC16F877A复位后ADCON1默认值可能使RA0~RA3处于模拟输入状态如果不修改读到的按键电平也许是0。TRISA的低4位为1对应输入RC6和RC7作为普通IO输出PORTC清零确保上电瞬间不会把模块电源打开。需要注意PORTC同时与USART和CCP复用未初始化对应外设时输出配置不受影响。3. 搭出硬件细节PIC16F877A与433MHz模块的接线和参数计算3.1 发射端按键、上拉电阻与发射模块的电源开关发射端电路的核心是保证单片机可以稳定读取按键并且能给发射模块提供一个干净的电源控制。按键电路采用低电平有效每个按键一端接PIC的RA引脚另一端接地。IO内部虽然有弱上拉但建议外部再接10kΩ电阻到VCC因为内部弱上拉约20kΩ~50kΩ在潮湿环境或走线较长时噪声容易把低电平“抬高”。在按键两端并联一个0.1μF电容与电阻组成RC低通滤波器时间常数约1ms能有效抑制机械抖动。发射模块的供电需要单独控制。常见做法是用一个NPN三极管型号如8050做电子开关集电极接发射模块VCC发射极接地基极经1kΩ电阻接PIC的RC7。当RC7输出高电平时三极管导通模块得电RC7输出低模块断电。注意发射模块的GND必须和PIC共地否则数据信号没有回路。如果模块的DATA引脚在低电平时仍需要拉电流应在DATA到地之间加一个10kΩ下拉电阻以免模块内部逻辑悬空。下面给出发射端初始化代码它在2.3的基础上增加了对模块电源引脚的“先开后关”时序这也是测试时最容易忽略的点上电后如果直接发送数据模块电源还没稳定前几帧会丢失。void tx_init(void) { ADCON1 0b00000110; // 数字IO TRISA 0b00001111; // 按键输入 TRISC 0; // RC6DATA, RC7POWER均输出 PORTA 0; // 按键未按下 PORTC 0; // 数据线低电源关闭 } void tx_power_up(void) { RC7 1; // 打开发射模块电源 delay_ms(20); // 等待模块内部振荡稳定 }delay_ms(20)是一个软件延时在4MHz时钟下大约需要空转2万条指令。为什么不是1ms因为ASK发射模块内部的自激振荡器在通电后需要几个毫秒才稳定如果立即给高速数据波形前几个脉冲宽度会偏移接收端解不出同步头。3.2 接收端模块输出与单片机的电平匹配接收模块的DATA输出通常可以直接接PIC16F877A的RB0但要注意两个问题电平极性和噪声。不同接收模块的极性不同有些有信号时为低有些有信号时为高接法只能通过示波器确认。先假设模块在收到载波时输出低电平、无载波时输出高电平也就是和我们上一章协议定义保持一致。如果极性相反需要在代码里把同步头判断反过来。第二个问题是噪声。超再生模块在没有信号时输出端会随机跳动频率可能到几十kHz。如果RB0开着外部中断单片机几乎无法做其他事。硬件上可以在RB0输入到地之间并联一个1kΩ电阻和0.01μF电容组成RC低通滤波截止频率约16kHz高于编码信号的最高频率又能滤掉高频噪声。这个电容不能太大否则脉冲边沿变缓外部中断触发时检测到的脉宽值会偏小。用示波器量一下最窄脉冲的上升沿边沿时间应不超过高电平持续时间的10%。接收端软件初始化除了常规的端口配置外还要设置外部中断触发方式和清零中断标志。void rx_init(void) { ADCON1 0b00000110; // 数字IO TRISB 0b00000001; // RB0输入其他输出 OPTION_REG 0x7F; // 启用PORTB弱上拉可选项 INTCONbits.INTF 0; // 清外部中断标志 INTCONbits.INTEDG 0; // 下降沿触发 INTCONbits.INTE 1; // 使能RB0外部中断 INTCONbits.GIE 1; // 开全局中断 }这里选择下降沿触发对应接收模块“有载波输出低”的逻辑。如果模块极性相反INTEDG设为1。TRISB设置时注意TRISB默认复位值已经让RB0~RB7全部为输入这里把RB0以外设为输出方便驱动LED。弱上拉是否启用取决于按键是否有外部上拉接收端建议关闭弱上拉避免输入阻抗变化影响RC滤波。3.3 电机驱动L298N的使能脚与PWM频率的匹配L298N是最常用的直流电机驱动芯片内部包含两个全桥适合控制两个直流电机。每个通道有三个控制引脚IN1、IN2正向反转信号ENA使能。在PIC16F877A中方向由RD0~RD3输出高低电平速度由PWM输出连到ENA和ENB。连接时要注意逻辑地和电机地必须连在一起但驱动部分电源要单独接不要把电机电流灌到单片机电源线上。PWM频率的选择影响噪音和温升。L298N的开关频率最好在10kHz到20kHz之间高于20kHz时驱动芯片的开关损耗增大低于5kHz时电机会出现明显啸叫。PIC16F877A的CCP模块产生PWM周期由PR2寄存器决定。以4MHz晶振为例指令周期为1μsTMR2分频1PR299时PWM周期计算公式为周期 (PR21) × 4 × Tosc × 分频 100 × 4 × 0.25μs 100μs即10kHz。代码中的占空比由CCPR1L决定范围是0~99。void pwm_init(void) { PR2 99; // 10kHz周期PR21100 T2CON 0b00000100; // TMR2开启分频1 CCPR1L 0; // 占空比初始0 CCP1CON 0b00001100; // CCP1工作在PWM模式输出低2位为0 TRISCbits.TRISC2 0; // RC2/CCP1输出 CCPR2L 0; // 通道2同样初始化 CCP2CON 0b00001100; TRISCbits.TRISC1 0; }这段初始化里CCP1CON低两位是PWM占空比10位数据的最低两位0b00001100表示最高位组合“00”所以10位占空比数据由CCPR1L作为高8位低2位为0。如果要设置50%速度写CCPR1L 50即可。注意如果通过CCPR1L改变占空比应在TMR2周期翻转后立即更新否则会出现一个周期的异常占空比实际影响很小但严格的项目中可以在中断里同步更新。硬件参数总结如下驱动电源7.4V逻辑电源5VPWM频率10kHz最高占空比99对应100%。使用L298N时ENA如果不接PWM而是直接接5V电机就是全速转想要调速必须接PWM信号。很多初次做遥控小车的人把IN1/IN2直接接PWM虽然能转但方向控制会乱因为IN1/IN2是逻辑输入不是控制速度的。4. 用C语言实现无线遥控协议帧结构、发射与解码4.1 协议帧设计同步头、地址和数据位的时序定义由于没有使用PT2262/2272这类专用编解码芯片我们就可以自定义一套简化协议。设计协议时主要考虑三点接收端能准确找到一帧的起点能区分逻辑0和逻辑1在干扰环境下容忍一定脉宽误差。参考常用遥控器格式采用“同步头 地址码 数据码”的结构。地址码固定发送这样可以区分多个遥控器避免同时按下两辆车时互相命令。协议帧格式定义如下字段长度时序定义说明同步头1个高电平2.5ms 低电平0.5ms接收端据此定位帧起点地址码4位每位为1.3ms周期逻辑0和逻辑1各有一个高低组合固定为0101数据码4位同上对应四个按键状态帧间隔1个低电平10ms留给接收端处理也避免载波连续发射每位都采用“高电平低电平”的方式表示总周期固定1.3ms。逻辑1高电平900μs低电平400μs逻辑0高电平400μs低电平900μs。这样接收端只要测量一次高电平时间如果大于600μs则判为1小于600μs则判为0不需要测量整个周期。同步头的高电平2.5ms显著长于位高电平可以快速识别。帧的总时间为3ms 4×1.3ms 4×1.3ms 10ms 约23.4ms换算成重复频率约43Hz。这个频率对遥控车操控足够了人手的反应速度大约是10Hz43Hz的刷新率可以保证指令连续性。若想提高刷新率可以把帧间隔缩短到5ms但需要接收端及时处理中断否则容易缓冲区溢出。4.2 发射端编码用PIC16F877A定时器精确输出脉冲发射端的主循环只需做一件事扫描按键状态然后按固定节奏发送当前帧。最直接的方式是忙等待延时但为了让时序稳定建议使用TMR0中断产生一个1ms或更小的时间基准再根据协议设置电平。不过很多入门项目会简化成“延时翻转法”为了让代码更易读这里先展示这个简化版实际项目中可以继续改成时间片驱动。为了便于说明定义几个延时函数假设4MHz晶振_delay_us 由编译器提供。发送函数如下void send_bit(unsigned char b) { if (b) { RC6 1; _delay_us(900); RC6 0; _delay_us(400); } else { RC6 1; _delay_us(400); RC6 0; _delay_us(900); } } void send_frame(unsigned char keys) { // 同步头高2.5ms低0.5ms RC6 1; _delay_us(2500); RC6 0; _delay_us(500); // 地址码 0101 send_bit(0); send_bit(1); send_bit(0); send_bit(1); // 数据码按位发送 send_bit((keys 3) 1); send_bit((keys 2) 1); send_bit((keys 1) 1); send_bit(keys 1); // 帧间隔 RC6 0; _delay_us(10000); } void main(void) { tx_init(); tx_power_up(); unsigned char keys 0; while (1) { keys 0; if (RA0 0) keys | 0x08; // 前进 if (RA1 0) keys | 0x04; // 后退 if (RA2 0) keys | 0x02; // 左转 if (RA3 0) keys | 0x01; // 右转 send_frame(keys); // 主循环持续发送重复率由一帧总时长决定 } }这段代码每次扫描按键后立即发送一帧发送过程中不重复扫描因此按键变化最多延迟一个帧周期约23ms属于可接受范围。_delay_us 是自动调整的延时函数编译器会考虑调用开销。需要注意的是_delay_us(2500)在8MHz晶振和4MHz晶振下的指令周期不同编译器内部会计算但必须确保传入参数是常量不能是变量。4.3 接收端解码外部中断测量脉宽并还原控制字接收端的代码要同时完成“测脉宽”和“解码帧”两件事。为了避免丢失第二个脉冲建议使用双缓冲在中断中只保存脉宽测量值和边沿信息在主循环中做协议解析。如果中断里直接做解析而解析中又有乘法或数组操作很容易超过下一个脉冲到达的间隔。1.3ms脉宽而中断服务函数本身执行时间应小于100μs。下面是一份中断服务程序它在每次RB0下降沿时启动定时器上升沿时读取低脉冲宽度。为什么只测低脉冲因为接收模块输出与发射端反相协议中逻辑1对应接收端低电平900μs逻辑0对应低电平400μs同步头对应低电平2.5ms。测量低脉冲宽度即可区分三种状态。volatile unsigned int low_pulse; volatile unsigned char low_pulse_ready 0; void __interrupt() isr(void) { if (INTF) { if (INTEDG 0) { TMR1 0; // 下降沿开始计时 INTEDG 1; } else { low_pulse TMR1; // 上升沿低电平结束 low_pulse_ready 1; INTEDG 0; } INTF 0; } }初始化代码在3.2节中已经给出这里在中断服务程序中通过INTEDG自动切换触发边沿。变量low_pulse_ready标志表示一个低脉冲已经测量完成主循环必须及时读取并清0否则后续脉冲数据会被覆盖。由于TMR1是16位在4MHz下计满65536需要约65ms不会溢出所以不需要处理溢出中断。主循环中的解析逻辑如下unsigned char bit_array[8]; unsigned char bit_count 0; unsigned char rx_address, rx_data; void process_pulse(unsigned int width) { if (width 1500) { // 同步头低电平1.5ms bit_count 0; // 开始接收新的8位 } else if (width 600) { // 逻辑1 if (bit_count 8) bit_array[bit_count] 1; } else { // 逻辑0 if (bit_count 8) bit_array[bit_count] 0; } if (bit_count 8) { rx_address bit_array[0]*8 bit_array[1]*4 bit_array[2]*2 bit_array[3]; rx_data bit_array[4]*8 bit_array[5]*4 bit_array[6]*2 bit_array[7]; if (rx_address 0b0101) { // 地址匹配 car_control(rx_data); } bit_count 0; } }process_pulse通常在while(1)中检查low_pulse_ready读取low_pulse后调用。注意同步头的宽度阈值选1.5ms介于同步头2.5ms和逻辑1低电平0.9ms之间逻辑1/0阈值选0.6ms介于0.9ms和0.4ms之间。这个判断不依赖绝对精确的定时器值因此对模块的频率漂移有较好容忍度。4.4 小车运动状态机根据遥控数据切换电机方向一旦解析出符合地址的控制数据就需要将4位数据映射到两个电机的方向组合。由于后面的数据位只有4个按键最简单是前进、后退、左转、右转单选但实际开车时经常要“边转边校正”所以建议支持组合。下面给出一个支持四个按键任意组合的完整控制表。void car_control(unsigned char key) { unsigned char fwd (key 0x08) ! 0; // 前进键 unsigned char rvs (key 0x04) ! 0; // 后退键 unsigned char left (key 0x02) ! 0; unsigned char right (key 0x01) ! 0; if (fwd !rvs) { IN11; IN20; IN31; IN40; // 两电机正转 CCPR1L speed; CCPR2L speed; } else if (rvs !fwd) { IN10; IN21; IN30; IN41; // 两电机反转 CCPR1L speed; CCPR2L speed; } else if (left !right) { IN11; IN20; IN30; IN41; // 左转左轮正转/右轮反转 CCPR1L speed; CCPR2L speed; } else if (right !left) { IN10; IN21; IN31; IN40; // 右转左轮反转/右轮正转 CCPR1L speed; CCPR2L speed; } else { IN10; IN20; IN30; IN40; // 无命令停车 CCPR1L 0; CCPR2L 0; } }实际如果要原地转需要两个电机转速绝对值相同但方向相反这样小车会围绕中心旋转如果要差速转弯则应该用两个不同的PWM值。上面代码中的speed是个全局变量你可以通过引脚上的旋转编码器或距离传感器来调节也可以用按键调整速度档位。考虑到PIC16F877A的CCP1和CCP2是独立寄存器设置两个通道的占空比不会互相干扰。5. 调试方法与工程化补全让你的遥控车跑得远、不失控5.1 用LED和蜂鸣器做链路诊断无线遥控车最让人头疼的是“不知道是发射问题还是接收问题”。我一般会在接收端预留一个调试LED每次收到有效帧就让LED翻转一次。如果LED闪烁频率和按下按键的节奏一致说明无线链路和协议解析都是通的如果LED不闪先用发射端的RC7电源控制引脚测试模块供电是否正常再检查接收模块DATA端有没有信号。也可以用示波器观察RB0波形但示波器不常在手边时LED是最高效的诊断工具。5.2 天线、地线与电源去耦距离上不去的三个原因做好基础功能后你会发现遥控距离常常只有几米而不是理论上的几十米。第一天线不能乱接。433MHz模块的1/4波长天线长度约17cm如果使用拉杆天线要全拉开使用导线天线要按这个长度裁剪。第二接收模块距离单片机太近单片机的数字噪声会干扰接收。PCB布局上接收模块尽量放在板边下方不要走长距离的数字地线。第三电机瞬间电流会拉低系统电压导致调制波变形。对策是在驱动板电源端并联一个470μF电解电容同时在单片机电源脚并联0.1μF去耦电容而且电机电源线要用绞线减少对外辐射。5.3 给协议加上超时保护防止失控后小车继续跑最后一个实用性技巧是增加“超时停车”。在接收端开启TMR0中断每隔50ms检查一次如果超过50ms没有收到任何有效帧就强制关断PWM并停止电机。代码量不大但能避免遥控器没电或信号丢失时小车撞墙。实现时在每次收到有效帧后清除一个计数器在TMR0中断里给计数器加1当计数超过设定时间就执行刹车。这个逻辑本质上是把“通信正常”当成一个软锁存信号一旦链路断开就进入安全状态。加了这一步你的遥控小车才算能脱离调试台真正放到地面上跑。本文还有配套的精品资源点击获取