从零到一:用STM32CubeMX和HAL库重构那个经典的篮球记分器项目

张开发
2026/6/8 18:00:38 15 分钟阅读
从零到一:用STM32CubeMX和HAL库重构那个经典的篮球记分器项目
从零到一用STM32CubeMX和HAL库重构篮球记分器项目篮球记分器是嵌入式系统课程设计的经典选题之一它综合了GPIO控制、定时器中断、外部中断、显示驱动等多个关键技术点。传统的开发方式往往基于标准外设库SPL但随着STM32生态的发展HAL库和STM32CubeMX工具链已成为现代嵌入式开发的主流选择。本文将带你从零开始使用最新的开发工具链重构这个经典项目体验现代化嵌入式开发的效率优势。1. 开发环境搭建与CubeMX工程配置1.1 工具链准备开始之前需要准备以下开发环境STM32CubeMX 6.x或更高版本Keil MDK-ARM或STM32CubeIDESTM32F1xx HAL库0.96寸OLED显示屏(I2C接口)红外接收模块(如VS1838B)提示建议使用STM32CubeIDE它集成了CubeMX配置功能和开发环境支持代码自动补全和调试。1.2 CubeMX基础配置新建工程选择STM32F103C8Tx芯片后进行以下基础配置时钟配置启用外部高速时钟(HSE)系统时钟设置为72MHzAPB1总线时钟设为36MHz调试接口启用Serial Wire(SWD)调试接口避免占用调试引脚作为普通IOGPIO配置为OLED显示屏配置I2C引脚(PB6-SCL, PB7-SDA)为红外接收配置外部中断引脚(PB9)// CubeMX生成的时钟配置代码片段 SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct {0}; // HSE配置 RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.PLL.PLLState RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL RCC_PLL_MUL9; HAL_RCC_OscConfig(RCC_OscInitStruct); // 时钟树配置 RCC_ClkInitStruct.ClockType RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider RCC_HCLK_DIV1; HAL_RCC_ClockConfig(RCC_ClkInitStruct, FLASH_LATENCY_2); }2. 红外遥控模块的HAL库实现2.1 外部中断配置在CubeMX中配置红外接收引脚(PB9)为外部中断模式触发方式下降沿触发开启NVIC中断设置合适的优先级// CubeMX生成的中断初始化代码 static void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); // 红外接收引脚配置 GPIO_InitStruct.Pin GPIO_PIN_9; GPIO_InitStruct.Mode GPIO_MODE_IT_FALLING; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); // 中断优先级配置 HAL_NVIC_SetPriority(EXTI9_5_IRQn, 1, 0); HAL_NVIC_EnableIRQ(EXTI9_5_IRQn); }2.2 红外解码算法优化HAL库下实现红外解码需要结合定时器捕获功能。我们使用TIM4作为红外解码定时器// 红外解码数据结构 typedef struct { uint32_t rawData; uint8_t decodeOk; uint8_t repeatFlag; uint8_t keyValue; } IR_Data; // 定时器捕获配置 void TIM4_IRQ_Init(void) { TIM_HandleTypeDef htim4; TIM_IC_InitTypeDef sConfigIC; htim4.Instance TIM4; htim4.Init.Prescaler 72-1; // 1MHz计数频率 htim4.Init.CounterMode TIM_COUNTERMODE_UP; htim4.Init.Period 0xFFFF; htim4.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_IC_Init(htim4); sConfigIC.ICPolarity TIM_INPUTCHANNELPOLARITY_FALLING; sConfigIC.ICSelection TIM_ICSELECTION_DIRECTTI; sConfigIC.ICPrescaler TIM_ICPSC_DIV1; sConfigIC.ICFilter 0x03; HAL_TIM_IC_ConfigChannel(htim4, sConfigIC, TIM_CHANNEL_4); HAL_TIM_IC_Start_IT(htim4, TIM_CHANNEL_4); HAL_NVIC_SetPriority(TIM4_IRQn, 0, 0); HAL_NVIC_EnableIRQ(TIM4_IRQn); }3. OLED显示驱动与界面设计3.1 HAL库下的I2C驱动优化CubeMX生成的I2C配置可能需要进行优化以适应OLED的时序要求// I2C配置优化 void MX_I2C1_Init(void) { hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 400000; // 400kHz快速模式 hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 0; hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 0; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(hi2c1); // 调整I2C时序 __HAL_I2C_DISABLE(hi2c1); I2C1-CR2 | (36 0); // 36MHz I2C1-CCR 180; // 100kHz时180, 400kHz时45 I2C1-TRISE 37; // 1000ns/(1/36MHz)361 __HAL_I2C_ENABLE(hi2c1); }3.2 记分器界面布局设计篮球记分器的显示界面需要合理安排以下元素两队队名和比分比赛节次和倒计时24秒进攻计时比赛状态(进行中/暂停)// 显示布局示例代码 void OLED_Display_Update(void) { // 清屏 OLED_Clear(); // 显示队名和比分 OLED_ShowString(0, 0, HOME, 16, 1); OLED_ShowNum(40, 0, homeScore, 3, 16, 1); OLED_ShowString(80, 0, AWAY, 16, 1); OLED_ShowNum(120, 0, awayScore, 3, 16, 1); // 显示比赛时间 OLED_ShowString(0, 2, Q, 16, 1); OLED_ShowNum(10, 2, quarter, 1, 16, 1); OLED_ShowString(20, 2, TIME:, 16, 1); OLED_ShowNum(70, 2, minute, 2, 16, 1); OLED_ShowString(90, 2, :, 16, 1); OLED_ShowNum(100, 2, second, 2, 16, 1); // 24秒计时 OLED_DrawCircle(110, 4, 6, 1); OLED_ShowNum(108, 4, shotClock, 2, 8, 1); // 比赛状态 if(gameState PAUSED) { OLED_ShowString(0, 4, PAUSED, 16, 1); } // 刷新显示 OLED_Refresh(); }4. 比赛逻辑与状态机实现4.1 比赛状态定义使用状态机模式管理比赛状态定义以下状态状态描述READY准备状态等待开始PLAYING比赛进行中PAUSED比赛暂停QUARTER_BREAK节间休息GAME_OVER比赛结束// 状态枚举定义 typedef enum { GAME_READY, GAME_PLAYING, GAME_PAUSED, GAME_QUARTER_BREAK, GAME_OVER } GameState; // 全局状态变量 volatile GameState gameState GAME_READY; volatile uint8_t quarter 1; volatile uint16_t homeScore 0; volatile uint16_t awayScore 0; volatile uint8_t shotClock 24;4.2 定时器中断处理使用TIM3作为主定时器实现10ms精度的计时// 定时器中断回调 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { static uint16_t count10ms 0; if(htim-Instance TIM3) { count10ms; // 每100次1秒 if(count10ms 100) { count10ms 0; gameTimeUpdate(); } // 24秒计时处理 if(gameState GAME_PLAYING) { shotClockUpdate(); } } } // 比赛时间更新 void gameTimeUpdate(void) { if(gameTime.second 0) { gameTime.second--; } else { gameTime.second 59; if(gameTime.minute 0) { gameTime.minute--; } else { // 节次结束处理 quarterEndHandler(); } } } // 24秒计时更新 void shotClockUpdate(void) { if(shotClock 0) { shotClock--; } else { // 24秒违例处理 shotClockViolation(); } }5. 按键功能映射与响应5.1 红外遥控键值映射定义红外遥控按键与功能的映射关系按键功能电源键开始/暂停比赛上键主队得分1下键客队得分1左键主队得分-1右键客队得分-1OK键节次切换数字124秒复位数字214秒复位5.2 按键处理实现// 红外按键处理函数 void IR_Key_Handler(uint8_t keyValue) { switch(keyValue) { case IR_KEY_POWER: toggleGameState(); break; case IR_KEY_UP: homeScore; break; case IR_KEY_DOWN: awayScore; break; case IR_KEY_LEFT: if(homeScore 0) homeScore--; break; case IR_KEY_RIGHT: if(awayScore 0) awayScore--; break; case IR_KEY_OK: nextQuarter(); break; case IR_KEY_1: shotClock 24; break; case IR_KEY_2: shotClock 14; break; default: break; } // 更新显示 OLED_Display_Update(); } // 比赛状态切换 void toggleGameState(void) { if(gameState GAME_PLAYING) { gameState GAME_PAUSED; HAL_TIM_Base_Stop_IT(htim3); } else if(gameState GAME_PAUSED) { gameState GAME_PLAYING; HAL_TIM_Base_Start_IT(htim3); } else if(gameState GAME_READY) { gameState GAME_PLAYING; HAL_TIM_Base_Start_IT(htim3); } }6. 项目优化与扩展思考6.1 代码结构优化建议模块化设计将红外解码、OLED显示、比赛逻辑等分离为独立模块使用头文件定义清晰的接口低功耗优化在比赛暂停时进入低功耗模式合理配置外设时钟门控错误处理增强添加I2C通信失败重试机制实现红外信号解码错误检测6.2 功能扩展方向无线连接添加蓝牙模块实现手机控制通过WiFi上传比赛数据数据记录使用EEPROM或Flash存储历史比赛数据实现比赛回放功能显示增强添加LED矩阵显示增强视觉效果支持多语言界面切换// 示例使用FreeRTOS管理多任务 void StartDefaultTask(void const * argument) { // 初始化外设 MX_GPIO_Init(); MX_I2C1_Init(); MX_TIM3_Init(); MX_TIM4_Init(); // 创建任务 xTaskCreate(IR_Task, IR, 128, NULL, 3, NULL); xTaskCreate(Display_Task, OLED, 256, NULL, 2, NULL); xTaskCreate(GameLogic_Task, Logic, 256, NULL, 4, NULL); // 启动调度器 vTaskStartScheduler(); }重构这个经典项目时最大的挑战不是功能实现本身而是如何利用现代工具链提高开发效率和代码质量。CubeMX的图形化配置大大减少了底层初始化代码的编写量HAL库的统一接口则增强了代码的可移植性。在实际开发中我发现合理规划中断优先级对系统稳定性至关重要特别是当红外解码、定时器更新和显示刷新等多个实时任务需要协同工作时。

更多文章