STM32井字棋无视觉方案:传感器检测与AI算法实战
最近在准备电赛的同学应该都注意到了2024年电赛E题井字棋装置确实是个热门题目。很多队伍都在纠结视觉方案的成本和复杂度问题特别是对于大一新生来说视觉识别系统的搭建确实存在不小的技术门槛。本文将分享一套完整的无视觉方案实现思路从基础原理到代码实现帮助大家用更简单的方式完成1-6问的全部要求。1. 井字棋电赛题目解析与无视觉方案优势1.1 2024年电赛E题核心要求分析2024年全国大学生电子设计竞赛E题井字棋装置主要考察参赛选手在自动控制、人机交互和智能决策方面的综合能力。题目要求设计一个能够自动运行井字棋游戏的装置实现人机对弈功能。从题目要求来看核心功能点包括棋盘状态检测实时获取棋盘上棋子的分布情况落子机制实现机械臂或其它执行机构在指定位置落子决策算法设计合理的AI算法进行智能对弈胜负判断实时判断游戏状态并显示结果人机交互提供友好的操作界面和提示信息1.2 无视觉方案的技术优势传统方案多采用摄像头进行棋盘状态识别但这种方法存在以下痛点环境光线影响大识别稳定性难以保证图像处理算法复杂对处理器要求高摄像头标定和安装精度要求严格整体成本较高调试周期长无视觉方案通过传感器直接检测棋盘状态具有明显优势响应速度快实时性更好稳定性高不受光照条件影响硬件成本低适合学生预算调试简单上手难度低1.3 方案整体架构设计我们的无视觉方案采用分层架构设计传感器层 → 信号处理层 → 决策控制层 → 执行机构层 ↓ ↓ ↓ ↓ 棋盘检测 状态解析 AI算法 落子控制这种架构确保了系统的模块化和可扩展性每个层次都可以独立开发和测试。2. 硬件系统搭建与元器件选型2.1 核心控制器选择对于大一新生来说STM32系列单片机是最佳选择。推荐使用STM32F103C8T6最小系统板理由如下资源丰富72MHz主频64KB Flash20KB RAM开发简单有完善的HAL库和丰富的教程资源成本低廉核心板价格在10-15元之间外设齐全足够支持本项目所有功能需求// 系统时钟配置示例 void 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.2 棋盘状态检测方案采用9个红外对管传感器阵列检测棋盘状态每个格子对应一对红外发射和接收管// 红外传感器引脚定义 #define SENSOR_ROW1_PINS {GPIO_PIN_0, GPIO_PIN_1, GPIO_PIN_2} #define SENSOR_ROW2_PINS {GPIO_PIN_3, GPIO_PIN_4, GPIO_PIN_5} #define SENSOR_ROW3_PINS {GPIO_PIN_6, GPIO_PIN_7, GPIO_PIN_8} // 传感器状态读取函数 uint8_t read_board_status(void) { uint8_t board[3][3] {0}; // 扫描第一行传感器 for(int col 0; col 3; col) { HAL_GPIO_WritePin(SENSOR_POWER_GPIO_Port, SENSOR_POWER_Pin, GPIO_PIN_SET); HAL_Delay(1); // 短暂延时确保传感器稳定 board[0][col] HAL_GPIO_ReadPin(SENSOR_GPIO_Port, SENSOR_ROW1_PINS[col]); HAL_GPIO_WritePin(SENSOR_POWER_GPIO_Port, SENSOR_POWER_Pin, GPIO_PIN_RESET); } // 同理扫描第二、三行 // ... return encode_board_status(board); }2.3 落子执行机构设计采用二自由度机械臂结构使用SG90舵机实现精准控制// 舵机控制参数定义 typedef struct { uint16_t current_angle; uint16_t target_angle; TIM_HandleTypeDef* htim; uint32_t channel; } Servo_TypeDef; // 舵机初始化函数 void servo_init(Servo_TypeDef* servo, TIM_HandleTypeDef* htim, uint32_t channel) { servo-htim htim; servo-channel channel; servo-current_angle 90; // 初始位置 servo-target_angle 90; // 启动PWM输出 HAL_TIM_PWM_Start(htim, channel); servo_set_angle(servo, 90); } // 设置舵机角度 void servo_set_angle(Servo_TypeDef* servo, uint16_t angle) { if(angle 180) angle 180; uint16_t pulse_width 500 (angle * 2000 / 180); // 0.5ms-2.5ms __HAL_TIM_SET_COMPARE(servo-htim, servo-channel, pulse_width); servo-current_angle angle; }3. 棋盘状态检测与数据处理3.1 传感器数据处理算法红外传感器数据需要经过滤波处理以提高稳定性// 传感器数据滤波结构体 typedef struct { uint8_t raw_data[10]; // 原始数据缓冲区 uint8_t filtered_data; // 滤波后数据 uint8_t buffer_index; // 缓冲区索引 uint8_t stable_count; // 稳定计数 } SensorFilter_TypeDef; // 中值滤波算法 uint8_t median_filter(SensorFilter_TypeDef* filter, uint8_t new_data) { // 更新数据缓冲区 filter-raw_data[filter-buffer_index] new_data; filter-buffer_index (filter-buffer_index 1) % 10; // 复制数据并排序 uint8_t temp[10]; memcpy(temp, filter-raw_data, sizeof(temp)); // 冒泡排序 for(int i 0; i 9; i) { for(int j 0; j 9 - i; j) { if(temp[j] temp[j1]) { uint8_t swap temp[j]; temp[j] temp[j1]; temp[j1] swap; } } } // 取中值 return (temp[4] temp[5]) / 2; } // 传感器状态更新函数 uint8_t update_sensor_status(SensorFilter_TypeDef* filter, uint8_t new_reading) { uint8_t filtered median_filter(filter, new_reading); if(filtered filter-filtered_data) { filter-stable_count; } else { filter-stable_count 0; filter-filtered_data filtered; } // 连续5次读数稳定才确认状态变化 if(filter-stable_count 5) { return filter-filtered_data; } return 0xFF; // 表示状态未稳定 }3.2 棋盘状态编码与解码为了高效处理棋盘状态我们采用位编码方式// 棋盘状态编码 uint16_t encode_board_state(uint8_t board[3][3]) { uint16_t encoded 0; for(int i 0; i 3; i) { for(int j 0; j 3; j) { uint8_t pos i * 3 j; if(board[i][j] 1) { // 玩家棋子 encoded | (1 pos); } else if(board[i][j] 2) { // AI棋子 encoded | (1 (pos 9)); } } } return encoded; } // 棋盘状态解码 void decode_board_state(uint16_t encoded, uint8_t board[3][3]) { for(int i 0; i 3; i) { for(int j 0; j 3; j) { uint8_t pos i * 3 j; if(encoded (1 pos)) { board[i][j] 1; // 玩家棋子 } else if(encoded (1 (pos 9))) { board[i][j] 2; // AI棋子 } else { board[i][j] 0; // 空位置 } } } } // 棋盘状态打印函数调试用 void print_board(uint8_t board[3][3]) { printf(\n当前棋盘状态\n); printf(-------------\n); for(int i 0; i 3; i) { printf(|); for(int j 0; j 3; j) { if(board[i][j] 0) printf( |); else if(board[i][j] 1) printf( X |); else printf( O |); } printf(\n-------------\n); } }4. 井字棋AI算法实现4.1 极小化极大算法基础井字棋是个完全信息零和游戏适合使用极小化极大算法// 游戏状态评估函数 int evaluate_board(uint8_t board[3][3]) { // 检查行 for(int i 0; i 3; i) { if(board[i][0] board[i][1] board[i][1] board[i][2]) { if(board[i][0] 2) return 10; // AI获胜 else if(board[i][0] 1) return -10; // 玩家获胜 } } // 检查列 for(int j 0; j 3; j) { if(board[0][j] board[1][j] board[1][j] board[2][j]) { if(board[0][j] 2) return 10; else if(board[0][j] 1) return -10; } } // 检查对角线 if(board[0][0] board[1][1] board[1][1] board[2][2]) { if(board[0][0] 2) return 10; else if(board[0][0] 1) return -10; } if(board[0][2] board[1][1] board[1][1] board[2][0]) { if(board[0][2] 2) return 10; else if(board[0][2] 1) return -10; } return 0; // 平局或未结束 } // 检查是否还有空位置 int is_moves_left(uint8_t board[3][3]) { for(int i 0; i 3; i) { for(int j 0; j 3; j) { if(board[i][j] 0) return 1; } } return 0; }4.2 极小化极大算法实现// 极小化极大算法核心函数 int minimax(uint8_t board[3][3], int depth, int is_maximizing) { int score evaluate_board(board); // 如果AI获胜返回正分 if(score 10) return score - depth; // 如果玩家获胜返回负分 if(score -10) return score depth; // 如果没有可走的位置平局 if(!is_moves_left(board)) return 0; // AI回合最大化分数 if(is_maximizing) { int best -1000; for(int i 0; i 3; i) { for(int j 0; j 3; j) { if(board[i][j] 0) { board[i][j] 2; // AI落子 best max(best, minimax(board, depth 1, 0)); board[i][j] 0; // 撤销落子 } } } return best; } // 玩家回合最小化分数 else { int best 1000; for(int i 0; i 3; i) { for(int j 0; j 3; j) { if(board[i][j] 0) { board[i][j] 1; // 玩家落子 best min(best, minimax(board, depth 1, 1)); board[i][j] 0; // 撤销落子 } } } return best; } } // 寻找最佳落子位置 void find_best_move(uint8_t board[3][3], int* best_row, int* best_col) { int best_val -1000; *best_row -1; *best_col -1; for(int i 0; i 3; i) { for(int j 0; j 3; j) { if(board[i][j] 0) { board[i][j] 2; // 尝试AI落子 int move_val minimax(board, 0, 0); board[i][j] 0; // 撤销落子 if(move_val best_val) { *best_row i; *best_col j; best_val move_val; } } } } }4.3 算法优化与剪枝为了提升算法效率我们加入Alpha-Beta剪枝// 带Alpha-Beta剪枝的极小化极大算法 int minimax_ab(uint8_t board[3][3], int depth, int is_maximizing, int alpha, int beta) { int score evaluate_board(board); if(score 10) return score - depth; if(score -10) return score depth; if(!is_moves_left(board)) return 0; if(is_maximizing) { int best -1000; for(int i 0; i 3; i) { for(int j 0; j 3; j) { if(board[i][j] 0) { board[i][j] 2; int val minimax_ab(board, depth 1, 0, alpha, beta); board[i][j] 0; best max(best, val); alpha max(alpha, best); if(beta alpha) break; // Beta剪枝 } } } return best; } else { int best 1000; for(int i 0; i 3; i) { for(int j 0; j 3; j) { if(board[i][j] 0) { board[i][j] 1; int val minimax_ab(board, depth 1, 1, alpha, beta); board[i][j] 0; best min(best, val); beta min(beta, best); if(beta alpha) break; // Alpha剪枝 } } } return best; } }5. 机械臂运动控制与路径规划5.1 坐标系统建立建立棋盘坐标系与机械臂运动坐标的映射关系// 棋盘坐标定义 typedef struct { float x; // 机械臂X坐标 float y; // 机械臂Y坐标 } BoardPosition; // 棋盘位置映射表 const BoardPosition board_positions[3][3] { {{-40.0, 40.0}, {0.0, 40.0}, {40.0, 40.0}}, // 第一行 {{-40.0, 0.0}, {0.0, 0.0}, {40.0, 0.0}}, // 第二行 {{-40.0, -40.0},{0.0, -40.0},{40.0, -40.0}} // 第三行 }; // 棋子仓库位置 const BoardPosition piece_storage {50.0, 50.0}; // 当前机械臂位置 BoardPosition current_arm_position {0.0, 0.0};5.2 运动轨迹规划实现平滑的运动轨迹控制// 轨迹规划函数 void plan_trajectory(BoardPosition start, BoardPosition end, BoardPosition* trajectory, int* step_count) { float dx end.x - start.x; float dy end.y - start.y; float distance sqrt(dx*dx dy*dy); *step_count (int)(distance / 2.0) 1; // 每2mm一个步进 for(int i 0; i *step_count; i) { float ratio (float)i / (*step_count - 1); trajectory[i].x start.x dx * ratio; trajectory[i].y start.y dy * ratio; } } // 执行运动轨迹 void execute_trajectory(BoardPosition* trajectory, int step_count) { for(int i 0; i step_count; i) { // 控制X轴舵机 float x_angle position_to_angle_x(trajectory[i].x); servo_set_angle(servo_x, x_angle); // 控制Y轴舵机 float y_angle position_to_angle_y(trajectory[i].y); servo_set_angle(servo_y, y_angle); HAL_Delay(20); // 运动间隔 } // 更新当前位置 current_arm_position trajectory[step_count - 1]; } // 坐标到角度的转换函数 float position_to_angle_x(float x) { // 机械臂运动学正解计算 // 这里需要根据实际机械结构进行调整 return 90.0 x * 180.0 / 100.0; // 示例转换公式 } float position_to_angle_y(float y) { return 90.0 y * 180.0 / 100.0; }5.3 完整的落子流程控制// 完整的落子操作 void make_move(int row, int col) { BoardPosition target board_positions[row][col]; BoardPosition trajectory[50]; int step_count; printf(AI落子位置: (%d, %d)\n, row, col); // 1. 移动到棋子仓库 plan_trajectory(current_arm_position, piece_storage, trajectory, step_count); execute_trajectory(trajectory, step_count); // 2. 抓取棋子 grab_piece(); HAL_Delay(500); // 3. 移动到目标位置上方 BoardPosition above_target {target.x, target.y 10.0}; plan_trajectory(current_arm_position, above_target, trajectory, step_count); execute_trajectory(trajectory, step_count); // 4. 下降到落子位置 plan_trajectory(current_arm_position, target, trajectory, step_count); execute_trajectory(trajectory, step_count); // 5. 释放棋子 release_piece(); HAL_Delay(300); // 6. 抬起到安全高度 plan_trajectory(current_arm_position, above_target, trajectory, step_count); execute_trajectory(trajectory, step_count); printf(落子完成\n); } // 抓取和释放棋子函数 void grab_piece(void) { servo_set_angle(servo_gripper, 0); // 闭合夹爪 } void release_piece(void) { servo_set_angle(servo_gripper, 90); // 打开夹爪 }6. 系统集成与主控程序6.1 主程序状态机设计采用状态机模式管理游戏流程// 游戏状态定义 typedef enum { GAME_INIT, // 初始化 GAME_WAIT_PLAYER, // 等待玩家落子 GAME_CHECK_PLAYER, // 检测玩家落子 GAME_AI_THINK, // AI思考 GAME_AI_MOVE, // AI落子 GAME_CHECK_RESULT, // 检查游戏结果 GAME_OVER // 游戏结束 } GameState; // 游戏控制结构体 typedef struct { GameState current_state; uint8_t board[3][3]; uint8_t current_player; // 1:玩家, 2:AI uint8_t game_result; // 0:进行中, 1:玩家胜, 2:AI胜, 3:平局 } GameController; // 主控程序循环 void game_main_loop(void) { GameController game; game_init(game); while(1) { switch(game.current_state) { case GAME_INIT: game_state_init(game); break; case GAME_WAIT_PLAYER: game_state_wait_player(game); break; case GAME_CHECK_PLAYER: game_state_check_player(game); break; case GAME_AI_THINK: game_state_ai_think(game); break; case GAME_AI_MOVE: game_state_ai_move(game); break; case GAME_CHECK_RESULT: game_state_check_result(game); break; case GAME_OVER: game_state_over(game); break; } HAL_Delay(10); // 防止CPU过载 } }6.2 各状态具体实现// 初始化状态 void game_state_init(GameController* game) { printf(井字棋游戏初始化...\n); // 清空棋盘 memset(game-board, 0, sizeof(game-board)); game-current_player 1; // 玩家先手 game-game_result 0; // 机械臂回原点 arm_go_home(); // 显示欢迎信息 display_show_message(欢迎玩井字棋!); game-current_state GAME_WAIT_PLAYER; } // 等待玩家落子状态 void game_state_wait_player(GameController* game) { display_show_message(请落子...); // 检测玩家操作 if(detect_player_move()) { game-current_state GAME_CHECK_PLAYER; } } // 检测玩家落子状态 void game_state_check_player(GameController* game) { uint8_t new_board[3][3]; if(read_board_status(new_board)) { // 找出玩家落子的位置 int move_row, move_col; if(find_player_move(game-board, new_board, move_row, move_col)) { // 验证落子合法性 if(is_valid_move(game-board, move_row, move_col)) { game-board[move_row][move_col] 1; printf(玩家落子: (%d, %d)\n, move_row, move_col); print_board(game-board); game-current_state GAME_CHECK_RESULT; } else { display_show_message(无效落子请重试); game-current_state GAME_WAIT_PLAYER; } } } } // AI思考状态 void game_state_ai_think(GameController* game) { display_show_message(AI思考中...); int best_row, best_col; find_best_move(game-board, best_row, best_col); game-board[best_row][best_col] 2; game-ai_next_row best_row; game-ai_next_col best_col; game-current_state GAME_AI_MOVE; } // AI落子状态 void game_state_ai_move(GameController* game) { make_move(game-ai_next_row, game-ai_next_col); print_board(game-board); game-current_state GAME_CHECK_RESULT; } // 检查游戏结果状态 void game_state_check_result(GameController* game) { int result check_game_result(game-board); if(result ! 0) { game-game_result result; game-current_state GAME_OVER; } else { // 切换玩家 game-current_player (game-current_player 1) ? 2 : 1; if(game-current_player 1) { game-current_state GAME_WAIT_PLAYER; } else { game-current_state GAME_AI_THINK; } } } // 游戏结束状态 void game_state_over(GameController* game) { switch(game-game_result) { case 1: display_show_message(玩家获胜!); printf(游戏结束玩家获胜\n); break; case 2: display_show_message(AI获胜!); printf(游戏结束AI获胜\n); break; case 3: display_show_message(平局!); printf(游戏结束平局\n); break; } // 等待重启信号 if(detect_restart_signal()) { game-current_state GAME_INIT; } }7. 人机交互与显示界面7.1 OLED显示模块集成使用SSD1306 OLED显示屏显示游戏状态// OLED显示初始化 void oled_init(void) { // SSD1306初始化序列 uint8_t init_cmds[] { 0xAE, // 关闭显示 0xD5, 0x80, // 设置显示时钟分频 0xA8, 0x3F, // 设置多路复用率 0xD3, 0x00, // 设置显示偏移 0x40, // 设置显示起始行 0x8D, 0x14, // 电荷泵设置 0x20, 0x00, // 内存地址模式 0xA1, // 段重映射 0xC8, // COM扫描方向 0xDA, 0x12, // COM引脚硬件配置 0x81, 0xCF, // 对比度设置 0xD9, 0xF1, // 预充电周期 0xDB, 0x40, // VCOMH电平 0xA4, // 整体显示开启 0xA6, // 正常显示 0xAF // 开启显示 }; for(int i 0; i sizeof(init_cmds); i) { oled_write_cmd(init_cmds[i]); } } // 显示棋盘函数 void oled_draw_board(uint8_t board[3][3]) { oled_clear(); // 绘制棋盘网格 for(int i 1; i 2; i) { oled_draw_line(i*32, 0, i*32, 96, WHITE); oled_draw_line(0, i*32, 96, i*32, WHITE); } // 绘制棋子 for(int i 0; i 3; i) { for(int j 0; j 3; j) { if(board[i][j] 1) { // 绘制X玩家 oled_draw_line(j*328, i*328, j*3224, i*3224, WHITE); oled_draw_line(j*3224, i*328, j*328, i*3224, WHITE); } else if(board[i][j] 2) { // 绘制OAI oled_draw_circle(j*3216, i*3216, 8, WHITE); } } } oled_update(); }7.2 用户输入处理使用按钮和LED指示灯提供交互// 按钮检测函数 uint8_t read_buttons(void) { uint8_t buttons 0; if(HAL_GPIO_ReadPin(BTN_START_GPIO_Port, BTN_START_Pin) GPIO_PIN_RESET) { buttons | BTN_START; } if(HAL_GPIO_ReadPin(BTN_RESTART_GPIO_Port, BTN_RESTART_Pin) GPIO_PIN_RESET) { buttons | BTN_RESTART; } return buttons; } // LED控制函数 void set_led_status(uint8_t status) { HAL_GPIO_WritePin(LED_PLAYER_GPIO_Port, LED_PLAYER_Pin, (status LED_PLAYER) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LED_AI_GPIO_Port, LED_AI_Pin, (status LED_AI) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LED_WIN_GPIO_Port, LED_WIN_Pin, (status LED_WIN) ? GPIO_PIN_SET : GPIO_PIN_RESET); }8. 系统调试与性能优化8.1 调试信息输出通过串口输出详细的调试信息// 调试信息输出函数 void debug_print(const char* format, ...) { char buffer[128]; va_list args; va_start(args, format); vsnprintf(buffer, sizeof(buffer), format, args); va_end(args); HAL_UART_Transmit(huart1, (uint8_t*)buffer, strlen(buffer), 1000); } // 传感器数据调试 void debug_sensor_data(void) { uint8_t board[3][3]; if(read_board_status(board)) { debug_print(传感器原始数据:\n); for(int i 0; i 3; i) { debug_print(行%d: %d %d %d\n, i, board[i][0], board[i][1], board[i][2]); } } } // 机械臂位置调试 void debug_arm_position(void) { debug_print(机械臂位置: X%.1f, Y%.1f\n, current_arm_position.x, current_arm_position.y); }8.2 性能优化技巧针对STM32的资源限制进行优化// 使用查表法优化三角函数计算 const float sin_table[91] { 0.0000, 0.0175, 0.0349, 0.0523, 0.0698, // 0-4度 // ... 完整的正弦表 1.0000 // 90度 }; float fast_sin(float angle) { while(angle 0) angle 360; while(angle 360) angle - 360; int index (int)angle; if(index 90) return sin_table[index]; else if(index 180) return sin_table[180-index]; else if(index 270) return -sin_table[index-180]; else return -sin_table[360-index]; } // 优化棋盘评估函数 int fast_evaluate(uint8_t board[3][3]) { // 使用位运算快速检查胜负 uint16_t player_bits 0; uint16_t ai_bits 0; for(int i 0; i 3; i) { for(int j 0; j 3; j) { if(board[i][j] 1) player_bits | (1 (i*3j)); else if(board[i][j] 2) ai_bits | (1 (i*3j)); } } // 胜利模式掩码 const uint16_t win_patterns[8] { 0x007, 0x038, 0x1C0, // 行 0x049, 0x092, 0x124, // 列 0x111, 0x054 // 对角线 }; for(int i 0; i 8; i) { if((player_bits win_patterns[i]) win_patterns[i]) return -10; if((ai_bits

相关新闻