用STM32F103C8T6和红外遥控DIY一个篮球记分器(附完整代码和PCB图)
从零打造STM32篮球记分器红外遥控OLED显示实战指南篮球比赛记分器是电子爱好者入门嵌入式开发的经典项目。本文将带你用STM32F103C8T6核心板、0.96寸OLED和红外遥控模块打造一个功能完备的篮球记分系统。不同于简单的课程设计报告本指南将重点呈现实际制作中的关键细节包括硬件选型技巧、PCB设计要点、红外解码原理以及代码架构设计确保即使初学者也能成功复现。1. 硬件选型与电路设计1.1 核心器件选型分析选择STM32F103C8T6作为主控主要基于三点考量性价比突出Cortex-M3内核72MHz主频20KB RAM64KB Flash完全满足需求开发生态完善标准库和HAL库支持良好调试工具链成熟引脚资源充足提供37个GPIO便于扩展其他功能显示模块选用0.96寸OLED(IIC接口)而非LCD的三大优势接口简洁仅需SCL/SDA两根信号线显示效果自发光无需背光对比度高刷新速率远高于普通LCD适合动态显示红外遥控方案对比传统按键矩阵的优势对比特性红外遥控矩阵按键布线复杂度仅需1个IO需要NM个IO控制距离可达5-8米有线连接扩展性21键标准布局受限于PCB面积抗干扰能力需考虑环境光稳定可靠1.2 电路设计关键点红外接收电路设计注意事项供电滤波建议在VS脚添加100nF去耦电容信号整形可在OUT脚串联1kΩ电阻并并联10nF电容引脚配置推荐使用定时器输入捕获功能引脚(如PB9/TIM4_CH4)OLED接口电路优化建议// GPIO配置参考(使用软件IIC) #define OLED_SCL_PIN GPIO_Pin_2 #define OLED_SDA_PIN GPIO_Pin_3 #define OLED_PORT GPIOA void OLED_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin OLED_SCL_PIN | OLED_SDA_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(OLED_PORT, GPIO_InitStructure); }提示实际布线时IIC信号线建议控制在15cm以内过长可能导致通信不稳定。若必须延长可考虑降低上拉电阻值(如2.2kΩ)。2. 软件架构设计2.1 系统状态机模型篮球记分器的核心逻辑可采用有限状态机(FSM)实现stateDiagram-v2 [*] -- Idle Idle -- Running: 开始键按下 Running -- Paused: 暂停键按下 Paused -- Running: 开始键按下 Paused -- Adjusting: 时间调整键按下 Adjusting -- Paused: 确认调整对应代码实现框架typedef enum { GAME_IDLE, GAME_RUNNING, GAME_PAUSED, TIME_ADJUSTING } GameState; GameState currentState GAME_IDLE; void HandleStateTransition(u8 keyEvent) { switch(currentState) { case GAME_IDLE: if(keyEvent KEY_START) currentState GAME_RUNNING; break; case GAME_RUNNING: if(keyEvent KEY_PAUSE) currentState GAME_PAUSED; break; // 其他状态转换逻辑... } }2.2 定时器资源配置方案STM32F103C8T6的定时器分配建议定时器用途配置参数中断优先级TIM2系统时钟基准(1ms)72MHz/720001kHz最高TIM3比赛时间倒计时10ms间隔(7200分频)高TIM4红外输入捕获1μs分辨率(72分频)中定时器初始化代码片段void TIM3_Init(void) { TIM_TimeBaseInitTypeDef TIM_BaseInitStruct; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_BaseInitStruct.TIM_Period 10000; // 10ms中断 TIM_BaseInitStruct.TIM_Prescaler 7200 - 1; TIM_BaseInitStruct.TIM_ClockDivision TIM_CKD_DIV1; TIM_BaseInitStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_BaseInitStruct); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); TIM_Cmd(TIM3, ENABLE); }3. 红外遥控解码实现3.1 NEC协议解码原理常见红外遥控使用的NEC协议时序特征引导码9ms高电平4.5ms低电平数据码560μs高电平逻辑0(565μs低电平)或逻辑1(1690μs低电平)结束码560μs高电平典型解码流程捕获下降沿间隔时间判断脉冲宽度属于引导码、逻辑0还是逻辑1组合32位数据(地址码地址反码命令码命令反码)3.2 输入捕获实现要点使用TIM4_CH4(PB9)实现红外解码的关键配置void IR_Remote_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; TIM_ICInitTypeDef TIM_ICInitStruct; // PB9配置为浮空输入 GPIO_InitStruct.GPIO_Pin GPIO_Pin_9; GPIO_InitStruct.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, GPIO_InitStruct); // TIM4输入捕获配置 TIM_ICInitStruct.TIM_Channel TIM_Channel_4; TIM_ICInitStruct.TIM_ICPolarity TIM_ICPolarity_Falling; TIM_ICInitStruct.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStruct.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStruct.TIM_ICFilter 0x03; TIM_ICInit(TIM4, TIM_ICInitStruct); TIM_Cmd(TIM4, ENABLE); TIM_ITConfig(TIM4, TIM_IT_CC4|TIM_IT_Update, ENABLE); }解码状态机处理逻辑void TIM4_IRQHandler(void) { static u32 rawData 0; static u8 bitCount 0; if(TIM_GetITStatus(TIM4, TIM_IT_Update)) { // 处理超时情况 irState IR_IDLE; } if(TIM_GetITStatus(TIM4, TIM_IT_CC4)) { u16 pulseWidth TIM_GetCapture4(TIM4); TIM_SetCounter(TIM4, 0); switch(irState) { case IR_IDLE: if(pulseWidth 4000 pulseWidth 5000) { irState IR_HEADER; } break; case IR_HEADER: if(pulseWidth 2000 pulseWidth 3000) { rawData 0; bitCount 0; irState IR_DATA; } break; case IR_DATA: if(pulseWidth 1000 pulseWidth 1500) { rawData (rawData 1) | 0x01; } else if(pulseWidth 400 pulseWidth 800) { rawData 1; } if(bitCount 32) { ProcessIRCode(rawData); irState IR_IDLE; } break; } } TIM_ClearITPendingBit(TIM4, TIM_IT_CC4|TIM_IT_Update); }4. OLED显示优化技巧4.1 双缓冲显示机制为避免屏幕闪烁可采用双缓冲技术内存缓冲区在RAM中开辟与屏幕分辨率相同的缓冲区绘制阶段所有绘图操作先在内存缓冲区完成刷新阶段通过DMA将整个缓冲区一次性传输到OLED实现代码框架#define OLED_WIDTH 128 #define OLED_HEIGHT 64 u8 oledBuffer[OLED_WIDTH * OLED_HEIGHT / 8] {0}; void OLED_DrawPixel(u8 x, u8 y, u8 color) { if(x OLED_WIDTH || y OLED_HEIGHT) return; if(color) { oledBuffer[x (y/8)*OLED_WIDTH] | 1 (y%8); } else { oledBuffer[x (y/8)*OLED_WIDTH] ~(1 (y%8)); } } void OLED_Refresh(void) { for(u8 page0; page8; page) { OLED_WriteCommand(0xB0 page); // 设置页地址 OLED_WriteCommand(0x00); // 设置列地址低位 OLED_WriteCommand(0x10); // 设置列地址高位 for(u8 col0; colOLED_WIDTH; col) { OLED_WriteData(oledBuffer[col page*OLED_WIDTH]); } } }4.2 比分显示特效实现为增强视觉效果可添加分数变化动画void ShowScoreWithAnimation(u8 team, u16 newScore) { u16* targetScore (team TEAM_A) ? scoreA : scoreB; u16 diff abs(newScore - *targetScore); u8 step (diff 10) ? 2 : 1; while(*targetScore ! newScore) { if(*targetScore newScore) { *targetScore step; if(*targetScore newScore) *targetScore newScore; } else { *targetScore - step; if(*targetScore newScore) *targetScore newScore; } OLED_ClearSection(team TEAM_A ? 20 : 80, 30, 40, 16); OLED_ShowNumber(team TEAM_A ? 20 : 80, 30, *targetScore, 3, 16); OLED_Refresh(); DelayMs(50); } }5. 系统调试与优化5.1 常见问题排查指南红外接收不稳定检查供电电压是否稳定(建议3.3V)尝试调整接收头的角度(避免直对强光)在代码中增加去抖逻辑u8 DebounceKey(u8 keyCode) { static u8 lastKey 0; static u8 count 0; if(keyCode lastKey) { if(count 255) count; } else { count 0; lastKey keyCode; } return (count 5) ? keyCode : 0; }OLED显示残影在清屏前先关闭显示OLED_WriteCommand(0xAE)清屏后延迟10ms再开启显示OLED_WriteCommand(0xAF)检查IIC时序是否符合规格(SCL频率建议400kHz)5.2 功耗优化措施动态时钟调整void AdjustClockSpeed(GameState state) { if(state GAME_PAUSED) { RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_4); // 降频到32MHz } else { RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); // 恢复72MHz } RCC_PLLCmd(ENABLE); while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) RESET); SystemCoreClockUpdate(); }外设电源管理非活跃期间关闭OLED背板电荷泵OLED_WriteCommand(0x8D)红外接收器间隔采样(如每100ms激活一次)6. 项目扩展方向6.1 无线数据传输模块添加蓝牙/WiFi模块可实现手机APP远程控制比赛数据云端存储实时比分推送HC-05蓝牙模块接线示例蓝牙引脚STM32连接VCC3.3VGNDGNDTXDPA10(RX)RXDPA9(TX)STATEPC136.2 音效反馈系统利用PWM驱动蜂鸣器实现24秒违例提示音节间休息提醒得分提示音音效生成代码片段void Beep(u16 freq, u16 duration) { TIM_OCInitTypeDef TIM_OCInitStruct; // 配置TIM1_CH1(PA8)输出PWM TIM_OCInitStruct.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStruct.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStruct.TIM_Pulse SystemCoreClock / freq / 2; TIM_OCInitStruct.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStruct); TIM_Cmd(TIM1, ENABLE); DelayMs(duration); TIM_Cmd(TIM1, DISABLE); }实际制作中发现使用STM32的硬件IIC驱动OLED时偶尔会出现死锁现象。解决方法是在IIC初始化后添加总线恢复程序void I2C_Recovery(void) { GPIO_InitTypeDef GPIO_InitStruct; // 临时配置SCL/SDA为通用输出 GPIO_InitStruct.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStruct); // 模拟IIC总线恢复序列 GPIO_SetBits(GPIOB, GPIO_Pin_6); // SCL高 for(u8 i0; i9; i) { GPIO_ResetBits(GPIOB, GPIO_Pin_7); // SDA低 GPIO_ResetBits(GPIOB, GPIO_Pin_6); // SCL低 DelayUs(5); GPIO_SetBits(GPIOB, GPIO_Pin_6); // SCL高 DelayUs(5); GPIO_SetBits(GPIOB, GPIO_Pin_7); // SDA高 DelayUs(5); } // 发送停止条件 GPIO_ResetBits(GPIOB, GPIO_Pin_7); DelayUs(5); GPIO_SetBits(GPIOB, GPIO_Pin_6); DelayUs(5); GPIO_SetBits(GPIOB, GPIO_Pin_7); DelayUs(5); }