
最近总有人问我同一个问题想用STM32做一辆能遥控的小车练手但又想同时把串口通信、红外通信和蓝牙都摸一遍不知道该先去搞哪个。我去年用一块STM32F103C8T6做过一辆蓝牙遥控小车后来又把串口通信和红外通信一起整合了进来三个通道都能控制同一辆小车跑。今天干脆把这台车的完整思路、引脚规划、关键代码和调试记录整理出来给正在做类似项目的朋友一个能直接参考的改动路径。这篇文章适合两类人一是刚把STM32点灯跑通、想做个稍微完整点的项目的初学者二是已经会基本外设使用、但没把几种通信方式整合到同一个工程里的人。我会把通信协议设计、硬件接线的坑、代码解耦的思路都讲清楚你拿到手就能照着改。1. 项目总览与选型思路一辆车上为什么要装三种通信方式1.1 车架、电机与驱动板的搭配先说硬件底子。我用的车底盘是常见的四驱TT马达底盘但实际控制时只用了两路电机也就是左侧两轮并联成一路、右侧两轮并联成另一路这样L298N驱动板只需要两路输出就能完成差速转向。TT马达的额定电压一般是3V到6V配合4节AA电池或者一块7.4V锂电池都有问题要处理因为直接把电池电压给电机转速会很高而且不受控所以必须通过PWM调速。驱动板我选了L298N这个模块虽然体积大、效率也不算高但胜在皮实抗造非常适合新手阶段。它的每个通道有两个逻辑输入引脚IN1/IN2和IN3/IN4还有一个使能引脚ENA/ENB。使能引脚接PWM信号就能调速逻辑引脚决定电机正反转。如果你手头用的是DRV8833或者TB6612这类低压驱动板代码逻辑完全一样只需要改一下电源连接方式。1.2 为什么选STM32F103C8T6这个型号STM32F103C8T6在淘宝上十几块钱一块资源却一点不寒酸72MHz主频、64KB Flash、20KB RAM、三个USART、四个定时器、丰富的中断和GPIO。做遥控小车需要的资源它全都有而且网上资料多得看不完踩坑了基本都能搜到答案。选择它的另一个原因是通信接口足够分散。USART1留给蓝牙模块USART2作为备用调试口红外接收头用PA0的外部中断输入PWM信号从TIM3的两个通道输出这样每个外设都有独立的引脚不会出现复用冲突。做这类项目最怕的就是调着调着发现两个功能抢同一个引脚这个规划能避免一大半麻烦。1.3 三种通信方式在小车上分别扮演什么角色串口通信、红外通信、蓝牙通信这三种方式在小车上承担的任务完全不同串口通信既负责调试输出也是车载最稳定的控制通道。调试阶段要打印各种状态信息算法验证阶段可以直接从串口助手发指令看小车响应它是一切调试的基础。红外通信老式遥控器方案成本只要几块钱却拥有完全不依赖网络的单向控制能力。NEC协议解码本身就是很好的编程练习能逼着你理解定时器和外部中断配合的精髓。蓝牙通信最终产品形态让手机能无线遥控小车。HC-05模块本质上就是无线串口它把电脑/手机的串口数据无线转发给STM32所以在协议层面可以和串口共用同一套解析逻辑。三段控制通路最终都会汇聚到同一个指令解析函数小车本质上只认识“前进、后退、左转、右转、停止”这几个动作通信方式只是搬运工。1.4 供电与电平的几个隐性坑电源是整个小车最容易出诡异问题的地方。电机启动瞬间电流很大同一时间如果单片机也在工作电压跌落会导致复位重启。我的做法是电机电源和逻辑电源分开电池直接给L298N的12V输入口供电L298N的5V输出端单独给STM32主板供电红外接收头和蓝牙模块也从STM32的3.3V引脚取电。这种供电方式在地线处要连接在一起也就是所有模块的GND必须共地这个条件不满足串口和蓝牙都会出现数据丢失或完全不通的症状。另外要注意TT马达并联时总电流较大L298N芯片发热明显尤其长期堵转时温度会很高。使用3S锂电池11.1V时建议给L298N的12V输入串接一段降压或者降低PWM占空比上限否则电机转速过高会让小车原地打转还容易烧驱动芯片。2. 串口通信printf调试与文本指令接收的完整套路2.1 USART1初始化与printf重定向串口是嵌入式开发的基础设施也是我这台小车调试的“眼睛”。USART1对应PA9TX和PA10RXPA9负责把数据发送出去PA10负责接收外部发来的指令。初始化部分用标准外设库写配置好波特率、停止位、校验位和中断使能即可void UART1_Init(uint32_t baud) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // PA9 TX: 推挽复用输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // PA10 RX: 浮空输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate baud; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_RX | USART_Mode_TX; USART_Init(USART1, USART_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); USART_Cmd(USART1, ENABLE); }波特率我固定用115200还是9600取决于蓝牙模块的设置。HC-05出厂默认是9600所以MCU这边也用9600两边必须严格一致。调试阶段串口助手如果出现乱码第一反应就是波特率没对上。printf重定向是串口调试的灵魂。在C语言工程里重写fputc函数把标准输出终端到USART1这样以后随便printf一行“当前速度: 150”就能在串口助手看到实时状态int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return ch; }重定向之后还要在keil里勾选微库Use MicroLIB否则printf会引出一大堆文件系统代码Flash直接爆掉。2.2 文本行指令的下发与接收逻辑小车接收控制指令我采用的协议是用户友好的文本行格式每条指令以\r\n结尾内容就是单个字符加一个速度数值格式如下F,180表示前进速度180B,150表示后退L,120表示左转R,120表示右转S,0表示停止这个设计最大的好处是手机蓝牙串口助手和电脑串口助手都能直接输入调试时肉眼可读不需要搞二进制帧解析。团队协作或自己复盘时也不会因为忘了协议而重新翻代码。文本行接收在中断里做思路是累积字符直到收到换行符然后把完整的字符串交给指令解析函数char cmd_buffer[16]; uint8_t cmd_len 0; volatile uint8_t cmd_ready 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE)) { uint8_t data USART_ReceiveData(USART1); if (data \n) { cmd_buffer[cmd_len] \0; cmd_ready 1; cmd_len 0; } else if (data ! \r) { if (cmd_len 15) { cmd_buffer[cmd_len] data; } } } }解析函数在主循环中处理cmd_ready标志。因为中断只是搬运数据解析和下发的实际动作放在主循环里执行避免在中断上下文做耗时操作void Execute_Cmd(char *cmd) { char action cmd[0]; int speed 0; if (cmd[1] ,) { speed atoi(cmd[2]); } switch (action) { case F: Car_Forward(speed); break; case B: Car_Backward(speed); break; case L: Car_TurnLeft(speed); break; case R: Car_TurnRight(speed); break; case S: Car_Stop(); break; default: printf(Unknown cmd: %s\r\n, cmd); break; } }注意这里速度值范围我限制在0到255之间因为PWM寄存器是8位精度。2.3 串口调试时最容易迷惑人的三个现场串口通信看似简单实际调试时踩坑点却很多。第一个是乱码表现是串口助手显示一堆看不懂的符号原因通常是波特率不匹配或者USB转串口模块的晶振精度不够。CH340在常见的9600波特率下很稳定但如果你把波特率拉高到115200以上且使用了精度差的晶振误码率会迅速上升。实测下来小车项目用9600最省心。第二个是发送没反应但接收正常。这种问题我用过一次就长记性了八成是TX/RX接反了。STM32的TX要接模块的RXSTM32的RX接模块的TX别一看名字是R就都接R。如果是板载CH340或者通过杜邦线接USB转串口模块还要检查两边的GND有没有连在一起不共地时信号根本没有参考电平数据自然传不过去。第三个是“为什么我一发数据单片机就重启”。这经常是电源问题而不是串口问题——发送瞬间USB转串口模块会给你的板子灌一点电流压降一来电源保护就触发复位了。解决办法是老老实实把电源系统分开别让通信模块和电机共用同一个脆弱电源。3. 红外通信用外部中断加定时器解出NEC协议3.1 红外接收头的反相输出与时序拆解红外遥控方案我用的是最常见的VS1838B接收头和普通NEC协议遥控器成本人民币几元钱。很多新手第一次遇到红外接收头都会愣一下为什么没有红外信号时输出是高电平有信号时反而变低这是因为VS1838B内部集成了放大、滤波和解调电路输出信号与发射端是反相的。NEC协议发射端的时序是先发9ms载波加4.5ms空闲作为引导码后面跟着32位数据位。每一位由560us载波脉冲加不同长度的空闲时间组成空闲560us代表逻辑0空闲1680us代表逻辑1。经过接收头反相之后STM32看到的信号变成9ms低电平加4.5ms高电平、560us低电平加560us高电平或者560us低电平加1680us高电平。这就有意思了——我们不需要去解码载波频率38kHz的载波已经被接收头解调掉了只需要精确测量高低电平的持续时间就能还原NEC协议数据。3.2 用外部中断加16位定时器做软件解码解码方案我选的是外部中断加定时器计时。红外接收头输出引脚接PA0配置成双边沿触发的外部中断同时把TIM2配置成一个自由的微秒级计数器。中断发生时读取TIM2的计数值与前一次记录的时间戳做差就能得到本次电平持续的时间。TIM2配置很简单72MHz时钟源分频72倍后得到1MHz计数频率也就是计数器每增加1就代表1微秒void TIM2_Init(void) { TIM_TimeBaseInitTypeDef TIM_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_InitStructure.TIM_Period 0xFFFF; TIM_InitStructure.TIM_Prescaler 72 - 1; TIM_InitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_InitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_InitStructure); TIM_Cmd(TIM2, ENABLE); }中断处理函数里维护一个last_rise变量记录最近一次上升沿的时间。每次触发下降沿时用当前时间减去last_rise得到刚结束的高电平宽度从而判断引导码、逻辑1和逻辑0volatile uint8_t ir_bit_cnt 0; volatile uint32_t ir_data 0; volatile uint8_t ir_recv_done 0; static uint16_t last_rise 0; void EXTI0_IRQHandler(void) { uint16_t now TIM_GetCounter(TIM2); uint8_t level GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0); if (level SET) { // 上升沿记录高电平开始时刻 last_rise now; } else { // 下降沿高电平刚结束 uint16_t high_us now - last_rise; if (high_us 4000 high_us 5000) { // 引导码的4.5ms高电平 ir_bit_cnt 0; ir_data 0; } else if (high_us 1600 high_us 1900) { // 逻辑1的空闲时间约1.68ms ir_data (ir_data 1) | 1; ir_bit_cnt; } else if (high_us 400 high_us 800) { // 逻辑0的空闲时间约560us ir_data (ir_data 1) | 0; ir_bit_cnt; } if (ir_bit_cnt 32) { ir_recv_done 1; ir_bit_cnt 0; } } }这里用了uint16_t计时TIM2计数器溢出时会自动回绕。C语言里无符号数减法有自动取模的特性所以即使在溢出边界做时间差也能得到正确结果不需要手动处理溢出。主循环中检测到ir_recv_done后把32位数据拆成地址码、地址反码、数据码、数据反码四段做一次反码校验if (ir_recv_done) { uint8_t addr (ir_data 24) 0xFF; uint8_t addr_inv (ir_data 16) 0xFF; uint8_t cmd (ir_data 8) 0xFF; uint8_t cmd_inv ir_data 0xFF; if ((addr addr_inv 0xFF) (cmd cmd_inv 0xFF)) { printf(IR cmd: 0x%02X\r\n, cmd); switch (cmd) { case 0x45: /* 遥控器按键值自己映射到小车动作 */ break; default: break; } } ir_recv_done 0; }这段代码不依赖任何红外解码库纯粹用定时器和中断就能解出完整数据逻辑上也很好理解。3.3 红外实测中的重复码、距离和环境光问题红外遥控器在持续按住按键时不会重新发一帧完整数据而是发引导码加2.25ms高电平的重复码。上面的代码里2.25ms的高电平宽度既不落在引导码区间也不落在数据位的区间所以会被自动忽略。控制小车时你按一下它动一下如果你想让小车持续前进需要自己做一个超时自动停止的机制——比如你期望的是“按下红外按键期间保持前进松开后小车继续跑500ms再停下”。实测距离和环境光的影响也值得提。VS1838B在室内无遮挡情况下遥控距离轻松超过8米但把接收头阳光直射或者靠近荧光灯管时它会收进大量红外噪声导致解码偶尔失败或误触发。摆放接收头时尽量让它朝向遥控器用热缩管遮住侧面光线能明显提高解包成功率。另外在中断里频繁调用GPIO_ReadInputDataBit是非常快的但是当定时器溢出发生时可能出现一次误判我在工程里加了简单的噪声过滤连续收到两次相同的高电平时序才认为是有效把误判率降到了可接受范围。4. 蓝牙模块HC-05的AT配置流程与手机遥控链路排障4.1 HC-05接线与AT模式入口蓝牙部分用的是老牌经典模块HC-05它内部就是一组UART转蓝牙的透传功能所以接线方式和串口模块几乎一样模块的TXD接STM32的PA10RX模块的RXD接STM32的PA9TX模块的VCC接5V或3.3VGND共地。需要注意HC-05的逻辑电平是3.3VSTM32 F103的GPIO也是3.3V可以直接连如果你用的是5V单片机就得加电阻分压否则容易把模块引脚打坏。HC-05有两种工作模式正常透传模式和AT指令配置模式。进入AT模式的经典方法有两个一是按住模块上的那个小按钮再上电二是把模块的EN引脚或KEY引脚拉高后上电。我推荐按住按钮上电这种方式因为不需要额外飞线。进入AT模式后模块的LED会变成慢闪约1秒一次此时模块的串口波特率固定为38400这个很重要——很多人用默认9600发AT指令结果怎么发都没响应。4.2 常用AT指令与参数配置清单下面是配置遥控小车蓝牙模块时最常用的一套AT指令配合电脑的串口助手或者USB转TTL模块就能操作指令功能典型设置AT测试通信返回OKATNAMECar_BLE修改蓝牙广播名称Car_BLEATPSWD1234修改配对密码1234ATUART9600,0,0设置波特率9600,停止位1,无校验9600,0,0ATROLE0设为从机模式0ATCMODE1允许任意设备连接1我的习惯是把波特率设为9600与STM32保持一致手机端配对完成后用蓝牙串口助手下发“F,180”这类指令就能直接驱动小车。蓝牙名称建议改成一个容易识别的名字否则车多的时候会把别人的车也连上。AT指令配置过程中注意每条指令以回车换行结尾也就是串口助手里要选“发送新行”模式。AT模式下模块不会转发蓝牙数据它只响应AT指令配置完成后断电重新上电才能回到透传模式。4.3 蓝牙连不上、数据乱码的排查顺序蓝牙模块是我使用中排查最久的一个环节也是我在热词里看到很多人在问“HC05连接不上”的原因。根据我的经验连接不上时按这个顺序排查模块LED状态上电后如果LED慢闪约1秒一次说明模块在待配对状态此时手机搜“HC-05”或者自定义的名称配对密码输1234或0000。如果LED一直双闪约2Hz说明模块已经连上了一个设备新手机搜不到它要先断开原连接。进入了AT模式没退出AT模式下模块不响应手机配对请求断电重新上电就好。手机搜不到设备可能是模块名称里带了不可见字符或者模块供电不足导致射频发射功率降低。我遇到过一次是USB供电给HC-05连线过长压降太大换成独立3.3V电源立刻解决。连接成功但数据乱码模块和STM32的波特率不一致用ATUART指令统一到9600即可。连接成功后一断开就再也连不上这种情况先检查CMODE是否设置为1如果CMODE0模块会自动连接上次绑定的设备其他设备无法接入。HC-05透传模式下的数据流非常好理解手机蓝牙串口助手发出的字符经过蓝牙无线链路到达HC-05HC-05通过TX引脚发到STM32的UART接收寄存器中断函数收到后跟电脑串口走完全相同路径。这也是为什么我可以让蓝牙和串口共用同一套指令解析函数的根本原因——它们本质都是给USART1喂数据。功耗方面HC-05平时电流大约30mA左右连上手机后会到40mA以上。小车由L298N的5V输出供电时完全没问题。如果电池电压较低蓝牙模块会频繁断连最好在模块电源引脚并联一个100uF电解电容实测能明显改善掉线问题。5. 电机驱动与整车联调L298N接线、PWM调速与真实调车经验5.1 驱动板接线与PWM初始化方法L298N驱动板作为整车的执行核心接线相当直观。我把两组逻辑输入接到STM32的PB0-PB3两组使能引脚接到PA6和PA7这两个引脚刚好是TIM3的通道1和通道2可以输出两路独立PWM。L298N引脚接STM32 / 电源功能IN1PB0A路正转逻辑IN2PB1A路反转逻辑IN3PB2B路正转逻辑IN4PB3B路反转逻辑ENAPA6 (TIM3_CH1)A路PWM调速ENBPA7 (TIM3_CH2)B路PWM调速12V输入电池正极电机供电GND电池负极STM32 GND共地5V输出可选给STM32供电稳压输出L298N内部本质上是两路H桥。H桥四个开关管组合起来可以控制电机两端电压的极性IN11、IN20时电机正转反过来就是反转都等于0时刹车。PWM信号改的是开关管的通断占空比从而实现调速。简单理解就是水龙头开关频率不变、但每次开的时间占比不同水流平均大小就不同电机速度也就跟着变。PWM初始化代码如下周期我设成1000个计数单位也就是1kHz的PWM频率对TT马达来说足够平滑void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 999; TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_OC2Init(TIM3, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }L298N模块出厂默认在ENA、ENB位置插着跳线帽这时使能端内部被拉高电机永远全速转必须拔掉跳线帽才能用PWM控制速度。5.2 统一控制逻辑把三种输入汇成一个动作整车控制逻辑可以整理成一张动作表看完就明白代码在做什么动作IN1IN2IN3IN4PWM1/PWM2前进1010速度值后退0101速度值原地左转0110速度值原地右转1001速度值停止00000串口、红外、蓝牙三种输入最终统一调用同一个运动函数。我在工程里把串口解析函数写的指令和蓝牙通过USART1发的数据完全共享同一个解析流程红外遥控器按键则通过一个映射表转换成同样的“F/B/L/R/S”命令。这样代码结构非常清晰将来再加一个WiFi控制只需要新增一个数据源映射到同一套动作函数就完事了。void Car_TurnLeft(uint8_t speed) { GPIO_WriteBit(GPIOB, GPIO_Pin_1, Bit_SET); // IN21, 左路反转 GPIO_WriteBit(GPIOB, GPIO_Pin_0, Bit_RESET); GPIO_WriteBit(GPIOB, GPIO_Pin_2, Bit_SET); // IN31, 右路正转 GPIO_WriteBit(GPIOB, GPIO_Pin_3, Bit_RESET); TIM_SetCompare1(TIM3, speed); TIM_SetCompare2(TIM3, speed); }原地转向最直观两侧轮子等速反向旋转小车以中心为轴原地转圈在桌上调车时非常方便。如果场地空间大、希望转向平滑可以把左右轮速差调成不同的值比如左轮50%、右轮100%就能画出弧线弯道。5.3 整车联调阶段实测遇到的几个典型问题调试小车是最花时间的一环因为问题往往不是单一原因。我列出几个实际遇到的现象和解决方法可以当作你的排查手册。第一个现象一上电小车猛地向前冲一下然后所有指示灯正常但电机不动。原因通常是电机启动瞬间电流太大导致L298N或单片机电压跌落单片机复位了。解决方法是给电池输出端并联一个大容量电解电容我用的是470uF同时把PWM初始值设为0主函数里等系统稳定后再允许接收指令。第二个现象蓝牙发送的指令经常丢尤其是“F,180”这类带数值的命令。排查下来是波特率配置有问题——HC-05默认9600但我之前用USB转TTL配置AT时给它设成了115200MCU这边还是9600两边通信自然断断续续。统一到9600之后问题彻底消失。每次改完HC-05参数后MCU端和调试助手都确认统一波特率再上电测试。第三个现象红外遥控能收到码但小车动作和遥控器按键对应错乱。这通常是遥控器厂商对数据码位的定义不同我直接用一个打印函数把收到的cmd值打印出来然后逐个按键记录再做映射表比猜测快得多。如果你用的遥控器解码地址和标准NEC不同也可以通过打印地址码来判断是否需要修改地址校验逻辑。第四个现象小车无法直行总是往右偏。这不是软件问题而是TT马达的个体差异导致左右转速不同。解决办法有两个在代码里给慢的一侧加一个速度补偿系数或者直接在硬件上调整左右轮的橡皮圈松紧。我实测发现用速度补偿代码更灵活调整系数0.9到1.1之间就能让小车跑出直线。最后说说个人习惯。我现在做这类整合性项目一定会先做最小验证板——只测串口printf再测PWM输出再跑红外解码最后连蓝牙。每加一个模块都在笔记本上记录当天改了什么引脚、什么参数这习惯帮我省掉了无数排查时间。串口通信、红外通信、蓝牙通信落到一辆小车上核心不在于单个外设怎么用而在于怎么让它们协同、共用一套逻辑这才是做一个完整嵌入式小项目最有价值的部分。