
简介这份资源是面向嵌入式初学者与无线通信开发者的STM32F103C8T6驱动433MHz模块解码程序针对低功耗短距离无线通信场景如智能家居遥控、传感器网络等帮助读者理解并实现433MHz信号的接收与解析。压缩包共144个文件约3.5MB以33个c源文件与34个h头文件为核心涵盖定时器、串口、中断等外设驱动另含18个d依赖文件、18个o编译产物、17个crf交叉引用文件及uvprojx工程、hex固件、map映射等构成可直接编译调试的完整工程框架。内容围绕STM32定时器捕获脉冲、UART串行通信、中断实时响应以及ASK/OOK等调制协议的解码算法展开配合HAL库与Keil开发环境便于读者对照源码梳理初始化配置、中断服务与数据解码函数的组织方式。目前已有4926人学习下载适合希望快速上手433MHz无线解码、积累嵌入式通信实战经验的开发者参考借鉴。1. 433MHz 解码在 STM32F103C8T6 上到底难在哪很多人第一次做 433MHz 无线解码都会掉进同一个坑买一对几块钱的发射接收模块接上 STM32F103C8T6写个 GPIO 中断以为能像串口一样直接读出数据。结果串口助手打开一看全是乱码或者干脆一个字节都收不到。问题不在模块坏了而在于 433MHz 的廉价接收模块输出的是“模拟量整形后的数字波形”它没有时钟线没有起始位约定全靠发射端的编码协议和接收端的软件解码对齐。STM32F103C8T6 这颗芯片主频 72MHz、带定时器输入捕获和外部中断资源足够但前提是你得先搞清楚你面对的是哪种编码是固定码 EV1527 这类脉宽编码还是带前导码和同步头的自定义协议。这篇文章就围绕“基于 STM32F103C8T6 的 433MHz 解码程序”这个具体目标把选型、定时器捕获配置、中断状态机、参数整定和常见翻车点一次讲透适合手里已经有模块、想自己写解码而不是直接买成品超外差接收头的人。2. 先分清编码类型EV1527、PT2262 和自定义协议2.1 为什么不能直接读 GPIO 电平433MHz 接收模块的数据脚在没有信号时输出的是噪声有信号时输出的是经过比较器整形后的方波。这个方波的高电平和低电平宽度携带信息而不是电平本身。如果你用 GPIO 轮询或者普通外部中断只记录“有跳变”你会得到一堆时间戳但不知道哪个宽哪个窄。常见做法是用定时器输入捕获记录每次边沿跳变时的计数器值两次捕获之差就是脉宽。STM32F103C8T6 的 TIM1 到 TIM4 都支持输入捕获其中 TIM2 是 32 位做长间隔测量更方便但 16 位的 TIM3、TIM4 也够用只要在溢出时做扩展计数。2.2 EV1527 的帧结构和时间参数EV1527 是遥控器里最常见的一款编码芯片一帧通常由 24 位组成20 位地址加 4 位数据或者 20 位地址加 4 位按键状态。它用脉宽区分 0 和 1一个窄高电平加一个宽低电平表示 0一个宽高电平加一个窄低电平表示 1。典型时间参数是高电平窄为 1 个基本单位宽为 3 个基本单位低电平窄为 1 个基本单位宽为 3 个基本单位。基本单位常见在 250us 到 400us 之间具体取决于发射端振荡电阻。接收端解码时先测量高电平宽度再测量低电平宽度如果高宽低窄判为 1高窄低宽判为 0。一帧开始前通常有一个同步头表现为一个很宽的低电平用来标识帧起始。2.3 PT2262 和自定义协议的差异PT2262 也是脉宽编码但它的地址位和数据位排列方式与 EV1527 不同同步头宽度也不一样。更麻烦的是很多低成本发射模块用的是单片机软件编码前导码、同步头、数据位长度都是自定义的。这时候你不能靠猜得先用逻辑分析仪或者示波器抓一段原始波形量出高电平和低电平的实际时间再决定解码阈值。我一般会先用 TIM 输入捕获把边沿时间打到串口人肉看几帧确认编码规律后再写状态机。没有这一步直接套 EV1527 的代码大概率解不出来。3. 用 TIM 输入捕获搭一个能跑的脉宽测量框架3.1 定时器配置预分频和计数周期怎么算STM32F103C8T6 的 TIM 时钟来自 APB1 或 APB2。TIM2、TIM3、TIM4 挂在 APB1 上默认 36MHz但定时器时钟倍频后是 72MHz。假设我们用 TIM3预分频器设为 71则计数频率为 72MHz / (711) 1MHz每个计数代表 1us。自动重装载值设为 65535溢出周期约 65.5ms足够覆盖 433MHz 编码里最长的同步头低电平。输入捕获通道配置为上升沿和下降沿交替触发或者用两个通道分别捕获上升沿和下降沿。更省事的做法是单通道在中断里切换极性。// TIM3 输入捕获初始化通道1PA6 void TIM3_Cap_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPD; // 下拉输入空闲时保持低 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period arr; TIM_TimeBaseStructure.TIM_Prescaler psc; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x03; // 滤波抗毛刺 TIM_ICInit(TIM3, TIM_ICInitStructure); TIM_ITConfig(TIM3, TIM_IT_CC1 | TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM3, ENABLE); }这段代码把 PA6 设为下拉输入定时器计数频率 1MHz捕获通道 1 先设为上升沿触发滤波参数 0x03 表示采样 8 次取有效边沿能滤掉一部分射频噪声。预分频和重装载值根据你的实际信号调整如果基本单位是 250us1MHz 计数下就是 250 个计数16 位定时器完全够用。3.2 中断里做什么记录边沿和计算脉宽中断服务函数里要区分是捕获中断还是溢出中断。捕获中断发生时读取 CCR1 的值与上一次捕获值相减得到脉宽同时切换捕获极性准备捕获下一个边沿。溢出中断用来处理两次边沿间隔超过 65.5ms 的情况通常意味着帧结束或者噪声这时候重置状态机。volatile u16 cap_val 0; // 当前捕获值 volatile u16 last_cap 0; // 上次捕获值 volatile u8 cap_edge 0; // 0上升沿1下降沿 volatile u32 pulse_width 0; // 脉宽单位us volatile u8 frame_buf[6]; // 解码缓冲区 volatile u8 bit_cnt 0; void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_CC1) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); cap_val TIM_GetCapture1(TIM3); if (cap_edge 0) // 刚捕获到上升沿 { pulse_width cap_val - last_cap; // 低电平宽度 last_cap cap_val; // 切换为下降沿捕获 TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Falling); cap_edge 1; // 这里可以把低电平宽度送入解码状态机 decode_low_level(pulse_width); } else // 刚捕获到下降沿 { pulse_width cap_val - last_cap; // 高电平宽度 last_cap cap_val; TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Rising); cap_edge 0; decode_high_level(pulse_width); } } if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 超时重置解码状态 bit_cnt 0; last_cap TIM_GetCounter(TIM3); } }这段中断代码的核心是交替捕获上升沿和下降沿每次捕获后立刻切换极性。pulse_width 的单位是微秒因为定时器计数频率是 1MHz。decode_high_level 和 decode_low_level 是两个状态机函数根据脉宽判断是同步头、逻辑 0 还是逻辑 1。注意 last_cap 在溢出时也要更新否则下一次计算会出错。3.3 解码状态机从同步头到 24 位数据状态机通常分三个状态等待同步头、接收数据位、校验并输出。同步头的特征是低电平宽度远大于基本单位比如基本单位 250us同步头低电平可能达到 3ms 到 10ms。检测到同步头后开始按位接收。每接收一位需要同时看高电平和低电平的宽度。常见做法是高电平宽、低电平窄判为 1高电平窄、低电平宽判为 0。如果高低都宽或都窄判为无效位重置状态机。#define SYNC_MIN 2000 // 同步头最小宽度单位us #define SYNC_MAX 12000 // 同步头最大宽度 #define BIT_MIN 150 // 基本单位下限 #define BIT_MAX 500 // 基本单位上限 u8 decode_state 0; // 0等同步1收数据 u8 bit_val 0; u8 high_width 0, low_width 0; void decode_low_level(u32 width) { if (decode_state 0) { if (width SYNC_MIN width SYNC_MAX) { decode_state 1; bit_cnt 0; memset((void*)frame_buf, 0, sizeof(frame_buf)); } } else { low_width width; // 高低电平都拿到了判位 if (high_width BIT_MIN high_width BIT_MAX low_width BIT_MIN low_width BIT_MAX) { // 高宽低窄为1高窄低宽为0 if (high_width low_width * 2) bit_val 1; else if (low_width high_width * 2) bit_val 0; else { decode_state 0; // 无效位重置 return; } // 存入 frame_buf frame_buf[bit_cnt / 8] | bit_val (7 - (bit_cnt % 8)); bit_cnt; if (bit_cnt 24) { // 一帧收完输出 output_frame(frame_buf); decode_state 0; } } else { decode_state 0; } } } void decode_high_level(u32 width) { if (decode_state 1) { high_width width; } }这个状态机假设 24 位一帧先收高位。实际 EV1527 可能是 24 位PT2262 可能是 12 位地址加 4 位数据需要根据你的遥控器调整 bit_cnt 上限。output_frame 里可以做地址匹配、按键判断或者直接通过串口打印出来。4. 参数整定和抗干扰让解码稳定下来的几个关键动作4.1 基本单位怎么测、阈值怎么设基本单位不是固定值它随发射端振荡电阻、电池电压和温度变化。我一般会先用串口把每次捕获的高电平和低电平宽度打印出来按一下遥控器看几十个数据找到出现频率最高的那个宽度那就是基本单位。然后设 BIT_MIN 为基本单位的 0.6 倍BIT_MAX 为 1.8 倍。同步头阈值设为基本单位的 8 倍到 30 倍之间。如果遥控器有多个按键每个按键的编码可能不同但基本单位通常一致。4.2 中断优先级和串口打印的冲突STM32F103C8T6 的 TIM3 中断优先级如果设得太低会被串口中断打断导致捕获值读取延迟脉宽测量出现偏差。常见做法是把 TIM 捕获中断设为抢占优先级 1串口中断设为 2 或更低。另外不要在中断里直接调用 printf那会阻塞太久。正确做法是在中断里把解码结果存入全局变量置一个标志位主循环里检测标志位再打印。4.3 接收模块的供电和天线433MHz 超再生接收模块对电源噪声非常敏感。如果直接用 STM32 板上的 3.3V 供电电机或 LED 一动作接收模块输出就会出现大量毛刺。我一般会给接收模块单独加一个 100uF 电解电容加 0.1uF 陶瓷电容天线用 17cm 左右的软线尽量拉直。如果还是误码率高可以把接收模块的数据脚先过一个施密特触发器再进 STM32或者软件上把输入捕获滤波参数加大到 0x0F。5. 避坑与排查解码程序最常见的 5 个翻车现场5.1 现象串口一直打印乱码没有一帧完整数据原因通常是捕获极性切换写反了或者 last_cap 没有在溢出时更新导致脉宽计算出现巨大跳变。解决方法是先在中断里只打印原始捕获值不跑状态机确认上升沿和下降沿交替出现且相邻捕获值之差在合理范围。如果发现连续两次都是上升沿说明极性切换没生效检查 TIM_OC1PolarityConfig 的调用位置。5.2 现象能解出数据但地址位经常错一位这是位对齐问题。EV1527 的 24 位里前 20 位是地址后 4 位是数据。如果你的状态机在同步头之后没有严格从第一位开始收或者把同步头的尾部当成了数据位就会错位。解决方法是同步头检测成功后先等一个完整的高电平加低电平周期再开始计位。另外有些遥控器会连发多帧第一帧可能不完整可以在 output_frame 里做连续两帧比较一致才输出。5.3 现象按键按一次解出好几帧重复数据这是正常现象遥控器通常连发 3 到 5 帧。你可以在 output_frame 里加一个简单的去重逻辑如果本次帧数据和上次相同且间隔小于 200ms就丢弃。或者只在按键释放后输出一次。注意不要用延时去等那会阻塞解码。5.4 现象距离一远就解不出来近了就正常接收模块的灵敏度有限而且超再生模块的增益受电源电压影响。先检查天线长度和焊接再检查接收模块的供电是否稳定。如果发射端是电池供电电压下降会导致发射功率降低基本单位也会变长。可以在解码程序里把 BIT_MIN 和 BIT_MAX 设得宽一点比如 0.5 倍到 2.2 倍但太宽会引入误判。5.5 现象换一个遥控器就解不出来不同厂家的遥控器编码协议可能不同基本单位、同步头宽度、数据位长度都不一样。这时候不能硬套参数得重新抓波形。我一般会保留一个“调试模式”把原始脉宽通过串口以十六进制打印用逻辑分析仪或者串口助手抓一段手动分析。如果遥控器是滚动码那解码出来每次都不一样这种不适合做固定地址匹配需要换方案。6. 进阶技巧用 DMA 加空闲中断做批量解码如果你已经跑通了单帧解码想进一步提高响应速度或者同时处理多路 433MHz 接收模块可以考虑用 TIM 的 DMA 请求把捕获值直接搬到内存数组再配合串口空闲中断或者定时器溢出中断做批量处理。具体做法是把 TIM3 的 CCR1 捕获事件映射到 DMA 通道每次捕获自动把 CCR1 的值写入一个环形缓冲区。主循环里检查缓冲区里是否有足够多的边沿数据然后一次性计算所有脉宽并跑状态机。这样中断里几乎不做事CPU 占用率大幅下降。// TIM3 捕获 DMA 配置示例 void TIM3_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel6); // TIM3_CH1 对应 DMA1_Channel6 DMA_InitStructure.DMA_PeripheralBaseAddr (u32)TIM3-CCR1; DMA_InitStructure.DMA_MemoryBaseAddr (u32)cap_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize CAP_BUF_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel6, DMA_InitStructure); TIM_DMACmd(TIM3, TIM_DMA_CC1, ENABLE); DMA_Cmd(DMA1_Channel6, ENABLE); }这段代码把 TIM3 通道 1 的捕获值循环写入 cap_buffer缓冲区大小设为 64 或 128。主循环里维护一个读指针每次处理时计算当前写指针和读指针之间的数据量逐对计算脉宽。注意 DMA 循环模式下写指针会回绕读指针也要跟着回绕。这种方案的好处是即使主循环在忙其他任务捕获数据也不会丢适合做多路接收或者高速编码。验证方法很简单用同一个遥控器分别跑中断版和 DMA 版对比解码成功率。如果 DMA 版成功率更高说明之前中断里处理太慢导致丢边沿。如果两者差不多那瓶颈可能在接收模块本身不用折腾 DMA。我自己的习惯是先用中断版把协议跑通确认参数没问题再决定要不要上 DMA。毕竟调试 DMA 的翻车概率比调中断高得多没有示波器或者逻辑分析仪的时候中断里打打印反而更直观。希望帮到你。本文还有配套的精品资源点击获取