
简介这是一套基于STM32F1微控制器与SU-03T语音识别模块的完整语音控制空调方案面向嵌入式学习者、智能家居DIY玩家及准备电子竞赛/毕设的学生适用于空调控制、智能家居等场景。压缩包内共245个文件以C源码、H头文件、Keil工程文件及原理图/PCB设计为主并包含编译好的hex/bin固件整体约9.15MB可直接打开工程学习或烧录调试。整套方案基于Keil5标准库开发硬件部分包含红外发射电路与SU-03T模块接线说明软硬件配套完整。目前已有200人学习/浏览。方案提供硬件原理图、PCB文件、STM32标准库工程源码及红外发射电路设计能够帮助读者快速复刻出实物实现通过语音控制美的空调同时源码注释清晰方便修改唤醒词与交互词可进一步扩展为其他语音控制家电原型。配套资料完整可快速搭建开发环境缩短项目周期。1. 从SU-03T语音识别到STM32F1红外控制的整体路线把语音识别和空调控制做成实物常见的做法是买一个万能红外遥控器再塞进WiFi模块但那是云端或本地协议栈的玩法调试起来反而不可控。这里说的方案完全不同用SU-03T做离线语音识别识别结果通过串口发给STM32F1由STM32F1解析出命令之后再通过红外发射电路把空调码发出去全程不依赖网络也没有额外的协议栈。很多人第一反应是红外发射电路难做实际上三极管加红外LED十几分钟就能搭起来真正卡住进度的往往是SU-03T的输出格式和美的空调的红外编码时序。这篇文章把硬件电路、STM32F1串口解析、38kHz载波和空调发包流程拆开讲需要的器件都很常规按步骤来可以直接复刻出实物。这个组合适合手里已经有STM32F1最小系统板、想低成本做一个离线语音遥控器的人。不需要懂语音算法SU-03T已经把识别做完了也不需要懂射频红外电路就是最普通的电平开关。要花心思的是把语音模块吐出来的字符串变成一段时序正确的红外波形以及确认不同批次美的空调对帧格式的容忍度。后面所有代码都以STM32F103C8T6为准硬件上换成其他F1芯片也基本通用。2. 硬件可复刻SU-03T接线、红外发射电路与电源2.1 为什么是这个组合离线语音识别与MCU分工SU-03T本质上是一个“识别完就把结果从串口吐出来”的模块它不需要你给它写算法但需要你通过官方SDK或配套工具预先把唤醒词、命令词和对应编号配置好。把SU-03T和STM32F1放在一起的分工很清晰语音模块负责采集声音、识别关键字然后把类似{cmd:open,id:1}的文本或短指令通过UART异步发出来STM32F1负责接收、解析并控制红外发射引脚的电平与时序。这样做的好处是如果以后要换空调品牌只需要改STM32F1里的红外码表和命令映射语音这边不用动。另一个选型理由是SU-03T的串口输出是3.3V TTL和STM32F1的IO电平兼容不需要额外的电平转换电路。SU-03T工作电流在唤醒识别时会有几十毫安的抖动直接和STM32F1共用同一个3.3V LDO容易造成复位或识别率下降常见做法是给它单独一个AMS1117-3.3供电输入来自同一个5V电源地线还是要共在一起。2.2 SU-03T的串口接线与固件参数接线部分很简单SU-03T的TXD接STM32F103C8T6的PA10USART1_RXRXD接PA9USART1_TXVCC接3.3VGND共地。需要注意不要把TXD和RXD接反且ST-Link或USB转串口不要同时占用PA9/PA10否则会互相干扰。烧录SU-03T固件时用的是它自己的UART烧录工具和运行时串口是同一个口所以烧完程序后要记得把烧录线拔掉再接STM32F1否则两边都在驱动TXD会出现电平冲突。在SU-03T的配置工具里串口波特率建议设为9600或115200后续STM32F1的串口初始化要和这个值保持一致。命令词设置时可以给每一条语句绑定ID比如“打开空调”对应1“关闭空调”对应2“制冷模式”对应3。最终模块输出的是一段带头带尾的字符串STM32F1侧不能只按字符串精确匹配需要做包含匹配或按ID来截取因为不同版本的SU-03T固件在输出格式上偶尔会多出空格或换行符。2.3 红外发射电路三极管驱动与载波参数红外发射电路是整个硬件里最容易被小瞧的部分。直接用STM32F1的GPIO驱动红外LED也可以亮但驱动能力不够距离一远就接收不到所以必须加一级三极管放大。常见做法是用NPN三极管S8050做开关GPIO输出高电平时三极管导通红外LED通过限流电阻接到3.3V或5V电源GPIO输出低电平时LED截止。具体连接方式STM32F1的PB1通过1k电阻接S8050的基极发射极接地集电极接红外LED的负极红外LED的正极经过一个10Ω电阻接到3.3V电源。选用10Ω是为了在发射瞬间让电流能达到100mA左右同时靠PWM低占空比控制平均电流避免LED过热。如果想让发射距离更远可以把两个红外LED串联总压降约2.43.0V输入电压从3.3V提高到5V但要注意STM32F1的GPIO电平仍然是3.3V三极管的基极电阻不需要变。载波频率方面美的空调普遍使用38kHz红外载波也就是红外LED以38kHz的频率快速亮灭亮灭本身代表载波而空调关注的是载波包络的时序。频率偏差只要不超过±1kHz绝大多数空调都能正常识别所以STM32F1用定时器PWM来产生38kHz比用软件延时翻转IO更可靠。占空比建议控制在1/3左右也就是一个周期内高电平时间约8.8us低电平约17.5us这样可以在保证发射距离的同时降低LED平均功耗。2.4 电源分配与器件清单电源规划直接影响识别率和红外发射稳定性。STM32F1开发板通常通过MicroUSB输入5V板载AMS1117-3.3给MCU供电但那个3.3V的带载能力有限。SU-03T和红外LED如果全部挂在同一个3.3V上发射瞬间电流会拉低电压导致STM32F1复位这是最常见的“一按语音就重启”现象。建议独立给SU-03T供电红外LED则直接由5V经过10Ω电阻供电三极管开关控制回路这样发射瞬间的电流不经过LDO对MCU的影响会小很多。器件清单大致是这样的STM32F103C8T6最小系统板一块SU-03T语音识别模块一个S8050三极管两个5mm红外发射LED两到三只10Ω和1k电阻各几个AMS1117-3.3模块一个5V电源或USB线一根。逻辑分析仪不是必须但强烈建议备一个几十块钱的就能抓红外波形后面调空调码会省很多时间。整个电路可以先在面包板上搭确认能控温之后再转洞洞板或打样PCB射频部分没有特殊要求走线短一点就行。3. STM32F1软件链路串口DMA解析、38kHz载波与红外状态机3.1 串口DMA接收SU-03T语音结果STM32F1的USART1接收用DMA加空闲中断是一套很稳的组合SU-03T每次输出一帧短字符串空闲中断代表一帧结束这时从DMA缓冲区里取数据解析比逐字节中断更省CPU。先看初始化代码// dma_uart.c: USART1 DMA1_Channel4 接收 #define RX_BUF_LEN 64 uint8_t uart_rx_buf[RX_BUF_LEN]; volatile uint16_t dma_rx_len 0; void UART1_DMA_Init(uint32_t baud) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; DMA_InitTypeDef DMA_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // PA9 TX, PA10 RX 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); 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); DMA_DeInit(DMA1_Channel4); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)uart_rx_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize RX_BUF_LEN; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel4, DMA_InitStructure); USART_DMACmd(USART1, USART_DMAReq_RX, ENABLE); USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); USART_Cmd(USART1, ENABLE); DMA_Cmd(DMA1_Channel4, ENABLE); }这里把DMA配置成循环模式收到数据自动写入uart_rx_buf不需要CPU参与。空闲中断触发时从DMA当前计数寄存器反推实际接收长度再把缓冲区的字符串交给解析函数。要注意的是读取DMA_GetCurrDataCounter时如果一帧数据超过RX_BUF_LEN循环模式会覆盖前面的内容因此SU-03T的帧长最好不要超过64字节一般命令词输出都在20字节以内够用。然后是中断处理函数void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { USART_ReceiveData(USART1); // 读取DR清零IDLE标志 uint16_t remain DMA_GetCurrDataCounter(DMA1_Channel4); uint16_t recv_len RX_BUF_LEN - remain; if (recv_len 0) { // 收到的数据已经在 uart_rx_buf 里 HandleVoiceCommand((char *)uart_rx_buf, recv_len); } DMA_Cmd(DMA1_Channel4, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel4, RX_BUF_LEN); DMA_Cmd(DMA1_Channel4, ENABLE); } }空闲中断的处理逻辑是一旦串口线上出现超过一个字节时间的空闲就认为一帧结束这时去DMA计数器看已经搬了多少字节。HandleVoiceCommand会在缓冲区里做字符串搜索匹配到“open”或者“close”之类的关键字之后触发对应的红外发射任务。有个细节是首次开机DMA缓冲区的旧数据可能残留建议初始化时把uart_rx_buf全部清零否则第一次收到短帧时可能解析出乱码。3.2 定时器PWM生成38kHz载波红外发射需要38kHz的载波这个用STM32F1的定时器PWM输出最稳。我通常用TIM2_CH1映射到PA0让载波一直在PA0上产生然后用另外一个GPIO控制三极管的通断但更简洁的做法是直接把PWM输出脚当作红外驱动脚发射时开启PWM空闲时关闭PWM。这样PA0就通过电阻接三极管基极省掉一个控制脚。// ir_pwm.c: TIM2_CH1 PA0 输出38kHz PWM void TIM2_IR_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Prescaler 0; // 72MHz 不预分频 TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 1893; // 72MHz / 1894 ≈ 38.01kHz TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 630; // 占空比约1/3 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_Cmd(TIM2, DISABLE); // 初始不输出载波 }TIM_Period 1893和TIM_Pulse 630这个组合需要重点理解STM32F103C8T6的主频是72MHzARR寄存器设为1893意味着一个完整周期有1894个计数单位72MHz除以1894得到约38.01kHz误差极小。TIM_Pulse 630决定了高电平持续630个计数单位占空比约33%也就是1/3。这样做的目的是让红外LED的峰值电流可以做到100mA以上而平均电流只有大约30mA长时间发射也不会烫。发射的时候要控制PWM的开关void IR_Carrier_On(void) { TIM_Cmd(TIM2, ENABLE); } void IR_Carrier_Off(void) { TIM_Cmd(TIM2, DISABLE); }这里不推荐用改变TIM_Pulse为0的方式来关载波因为PWM输出脚在禁用定时器后会保持空闲状态逻辑更干净。后面状态机里所有“发高电平”的动作都是调用IR_Carrier_On“发低电平”调用IR_Carrier_Off。3.3 红外位流时序状态机的实现有了载波之后剩下的问题就是怎么按照空调协议的时序去开关载波。最简单的方法是阻塞式延时但那种方式在延时期间MCU做不了别的事而且一旦语音模块又发来新指令就会丢失。更稳的做法是维护一个红外发射状态机由定时器中断驱动主循环只需要提交待发送的数据。状态机的思路是把红外时序抽象成几个阶段先发送9ms载波作为引导码然后关闭载波4.5ms接着逐位发送数据位。每个数据位都是先发560us载波再根据该位是0还是1决定低电平时间0对应560us低电平1对应1.69ms低电平。这个节奏是NEC协议的标准形态美的空调部分型号的引导码或位极性不同但状态机的框架不需要变只需要改时间参数。// ir_state.c: 红外发送状态机 typedef enum { IR_IDLE, IR_HEADER_HIGH, IR_HEADER_LOW, IR_BIT_HIGH, IR_BIT_LOW, IR_GAP, IR_DONE } ir_state_t; volatile ir_state_t ir_state IR_IDLE; volatile uint16_t ir_tick_cnt 0; volatile uint32_t ir_frame[4]; volatile uint8_t ir_frame_len 0; volatile uint8_t ir_bit_idx 0; volatile uint8_t ir_repeat 0; void IR_StartTx(uint32_t *frame, uint8_t bit_len, uint8_t repeat) { while (ir_state ! IR_IDLE); // 等待上一帧发完 for (int i 0; i 4; i) ir_frame[i] frame[i]; ir_frame_len bit_len; ir_repeat repeat; ir_bit_idx 0; ir_state IR_HEADER_HIGH; IR_Carrier_On(); ir_tick_cnt 9 * 1000 / 50; // 9ms按50us一个tick计算 }定时器中断每50us进入一次所以9ms就是180个tick。50us的基准可以用TIM3产生配置比较简单预分频71自动重装49即72MHz / 72 / 50 20kHz也就是50us。每次中断里把ir_tick_cnt减一减到0就切换到下一个阶段。// TIM3_IRQHandler 中执行 void IR_StateMachineTick(void) { if (ir_state IR_IDLE) return; if (--ir_tick_cnt 0) return; switch (ir_state) { case IR_HEADER_HIGH: IR_Carrier_Off(); ir_state IR_HEADER_LOW; ir_tick_cnt 4500 / 50; break; case IR_HEADER_LOW: IR_Carrier_On(); ir_state IR_BIT_HIGH; ir_tick_cnt 560 / 50; break; case IR_BIT_HIGH: IR_Carrier_Off(); ir_state IR_BIT_LOW; if ((ir_frame[ir_bit_idx / 32] (31 - (ir_bit_idx % 32))) 1) { ir_tick_cnt 1690 / 50; } else { ir_tick_cnt 560 / 50; } break; case IR_BIT_LOW: ir_bit_idx; if (ir_bit_idx ir_frame_len) { ir_state IR_BIT_HIGH; IR_Carrier_On(); ir_tick_cnt 560 / 50; } else { ir_state IR_GAP; ir_tick_cnt 10000 / 50; // 帧间隔10ms } break; case IR_GAP: if (--ir_repeat 0) { ir_state IR_HEADER_HIGH; IR_Carrier_On(); ir_tick_cnt 9000 / 50; } else { ir_state IR_IDLE; } break; default: ir_state IR_IDLE; break; } }逻辑上可以分成三段来看。第一段是header阶段完成一次完整的载波开和关目的是让空调知道一帧数据要开始了。第二段是bit阶段每发送一个bit都要先打560us载波再根据数据位拉长或保持低电平时间。第三段是收尾阶段一帧发完后加一段空闲间隔如果需要重发就从header再来一遍不需要就回到IDLE。ir_frame数组用uint32_t[4]是为了承载最多128位的数据标准NEC只有32位美的一些变频空调会到45位甚至更多但这个数组长度够用。位偏移那里写成31 - (ir_bit_idx % 32)是因为从高位开始发送这在空调码抓取转换时会比较直观。3.4 语音命令到空调控制字的映射SU-03T识别出来的字符串进入HandleVoiceCommand之后要映射成具体的空调红外帧。映射逻辑一般放在一张表里维护起来比散落的if-else清晰。比如定义一个结构体数组每个元素包含语音指令关键字和对应的空调码。typedef struct { const char *keyword; uint32_t frame[4]; uint8_t frame_len; } voice_cmd_map_t; const voice_cmd_map_t voice_cmd_table[] { {open, {0x88C00500, 0x0, 0x0, 0x0}, 45}, {close, {0x88C00600, 0x0, 0x0, 0x0}, 45}, {cool, {0x88C00100, 0x0, 0x0, 0x0}, 45}, }; void HandleVoiceCommand(char *buf, uint16_t len) { for (uint8_t i 0; i sizeof(voice_cmd_table) / sizeof(voice_cmd_table[0]); i) { if (strstr(buf, voice_cmd_table[i].keyword) ! NULL) { IR_StartTx((uint32_t *)voice_cmd_table[i].frame, voice_cmd_table[i].frame_len, 3); break; } } }命令表里的帧数据是示意值不代表真实美的空调码实际使用时需要替换成你自己抓取的空调码。这里用strstr做包含匹配是因为SU-03T的输出格式可能会带序号、引号或其他修饰符只要关键字对得上就能触发。IR_StartTx的第三个参数设为3意思是整帧连发三次很多空调对单帧不回应红外遥控器本身也是重复发送的。映射表的好处是以后加语音指令非常快加一行数据就行不需要改代码逻辑。如果后来发现某个模式空调没反应优先检查表里的帧数据是不是抓错了而不是怀疑状态机。4. 美的空调编码实测、发射参数表与故障排查4.1 用逻辑分析仪抓原装遥控器波形美的空调的遥控器编码不是千篇一律的标准NEC早期的定频机和现在的变频机在引导码、数据位数和位极性上都有差别。最稳妥的办法是用一个逻辑分析仪去抓原装遥控器发出的波形把波形转成高低电平持续时间再反推成数据帧。市面上的USB逻辑分析仪配合PulseView采样率设2MHz以上就能看得很清楚抓一次完整的发射过程里面通常有2到3帧相同的数据。抓波形时把逻辑分析仪的通道夹在红外接收头的OUT引脚上或者直接夹在原装遥控器红外LED两端也可以但灵敏度会低一些最好还是用红外接收头做转换。红外接收头型号用VS1838B它输出的是解调后的电平信号低电平代表收到载波高电平代表空闲注意软件里要反转电平。抓到波形之后把各个边沿之间的时间间隔记下来对照NEC协议去切分第一个9ms左右的高电平是引导码跟着4.5ms低电平再往后就是一个个560us和1690us交替的数据位。美的空调常见的45位帧其实就是标准NEC的32位前面多了厂商码或者温度校验字段逐个bit拆出来就能填到前面那个ir_frame数组里。4.2 发射参数表与调节方法参数调优的重点是时间误差控制。红外遥控的时序虽然要求不是微秒级精确但误差超过20%就不容易被识别所以所有时间值建议用定时器tick来算不要用HAL_Delay或手写延时循环。以50us为一个tick常用参数可以整理成一张表参数项推荐值说明载波频率38kHz通过TIM2 ARR1893实现载波占空比1/3高电平630个计数单位引导码高电平9ms180个tick引导码低电平4.5ms90个tick数据位0高电平560us11个tick数据位0低电平560us11个tick数据位1高电平560us11个tick数据位1低电平1.69ms33个tick帧间隔10ms200个tick重复帧数3每帧之间间隔10ms这张表是从NEC协议推导出来的如果抓到的波形里发现引导码是8.4ms而不是9ms或者数据位低电平不是560us而是600us直接修改ir_tick_cnt右边分子就行。注意每次改动要同步改IR_StartTx里的初始化tick数和状态机里的case分支避免出现前一段是9ms后一段却变成8ms的混乱情况。有一个很多人踩过的坑美的空调有些型号反着发也就是空闲时输出高电平收到载波时输出低电平而标准NEC是载波输出高电平。如果按照标准NEC把数据位取反后依然无法控制可以在抓波形时直接把原始高低电平序列打印出来对比而不是强行套标准协议。4.3 常见故障排查流程如果整套硬件按照上面做完还是控制不了空调不要急着改代码按照下面的顺序排查更容易定位问题。第一步用手机摄像头对着红外LED按一下发射按键摄像头画面里应该能看到LED闪烁。手机摄像头能感知红外光如果看不到任何闪烁说明载波没起来或者三极管没导通检查IR_Carrier_On有没有真正执行以及PWM输出的引脚是否复用配置正确。第二步用逻辑分析仪看PA0引脚确认波形里引导码和数据位的宽度如果偏差特别大说明定时器分频或tick计算有误。第三步把逻辑分析仪放到红外接收头后面确认接收头输出的电平和预期一致如果完全没有输出可能是发射距离太近导致LED过载保护也可能是红外LED接反了。如果波形正常但空调没反应大概率是帧数据不对。这时候可以连续发送抓到的原装遥控器波形先不要加任何降温或调温逻辑直接从“开关”这种最简单的指令试起。还有一种常见情况是SU-03T识别成功也发串口了但STM32F1的串口没收到这种情况优先查波特率是不是9600/115200以及SU-03T和STM32F1的TXD/RXD有没有交叉连接。5. 复刻前必做的红外波形验证与数据回放技巧5.1 手机摄像头快速验证红外发射红外光人眼看不到但几乎所有手机摄像头 CMOS 都能捕捉到红外LED的亮灭。最快捷的验证方式是打开手机相机对准红外LED按发射键如果硬件电路和代码都没问题屏幕上会看到LED发出紫白色光。这个方法不用接示波器就能确认PB1到三极管这条路是通的、定时器PWM也正常输出了38kHz。不过只能确认有没有载波不能确认时序对不对所以只能做第一道检查。如果想让验证更严格可以拿一颗红外接收头接到另一个空闲的STM32F1引脚上自己发自己收。接收头输出脚接PA8给这个引脚配一个输入捕获把接收到的脉冲宽度打印到串口跟预期时间比较。这样做的好处是不需要额外的逻辑分析仪就能在电脑上看到一组脉冲时间序列是用来确认发送数据帧是否完整的好办法。5.2 从采集波形生成C数组并回放逻辑分析仪抓完波形之后通常得到的是一串边沿时间戳手动转成ir_frame容易出错。我一般会把导出的CSV做一个简单转换变成可以直接放进状态机里的时间序列。这里提供一个Python脚本从Saleae导出的CSV中提取高低电平持续时间import csv # 假设CSV格式: Time[s], Value with open(ir_capture.csv) as f: rows list(csv.reader(f))[1:] edges [] prev_t float(rows[0][0]) for row in rows: cur_t float(row[0]) edges.append(int((cur_t - prev_t) * 1_000_000)) # 转成us prev_t cur_t print(edges)这个脚本输出的是一个整数列表每一项代表一段电平持续了多少微秒。把这些数值直接填进状态机里比重新拼ir_frame更贴近实物。IR_StartTx需要改造成既能按位模式发送也能按时间序列发送的版本在时间序列模式下每次tick就把edges数组下一个值装载到ir_tick_cnt里就能完整复刻原装遥控器波形。等到波形能控制空调之后再回头把这些时间序列手工编码成45位的ir_frame数组也行或者干脆保留时间序列方式毕竟48个uint16也才96字节STM32F103C8T6的RAM完全没压力。最后用手机摄像头验证三次再分别测试制冷、制热和开关三个命令确认无误就可以打板装箱了。本文还有配套的精品资源点击获取