Arduino双色LED井字棋:嵌入式系统综合实践项目
2026/6/7 21:23:47 网站建设 项目流程

1. 项目概述与核心思路

电子井字棋,这个几乎人人都玩过的经典游戏,把它从纸笔搬到实体电子设备上,听起来就很有意思。我最近就动手做了一个,核心是用一块Arduino Mega 2560作为大脑,控制一个3x3的双色LED矩阵来显示“X”和“O”,再配上两个独立的小键盘让两位玩家轮流操作,最后用一块I2C LCD屏幕来显示游戏状态和提示信息。整个设备被我塞进了一个自己设计制作的木盒子里,从电路焊接、代码编写到木工切割,算是过了一把全栈创客的瘾。

这个项目的价值,远不止是做出了一个能玩的游戏。对于刚接触嵌入式系统和Arduino的朋友来说,它是一个绝佳的综合性练手项目。它几乎涵盖了入门到进阶的多个关键知识点:GPIO(通用输入输出)的扩展使用、矩阵键盘的扫描原理、I2C通信协议的应用、多任务状态机编程思想,以及如何将电子模块与实体结构(木工)结合。你不仅能学会让灯亮起来、让屏幕显示字,更能理解一个完整交互系统是如何从零开始搭建、调试并最终稳定运行的。无论是学生做课程设计,还是工程师寻找一个有趣的周末项目,这个电子井字棋都能提供扎实的实践路径。

我最初在网上搜过类似的项目,发现大多要么只用单色LED,交互简陋;要么逻辑完全在电脑上运行,失去了嵌入式硬件的味道。我的设计思路很明确:硬件上追求清晰与模块化,软件上追求健壮与可读性。用双色LED能直观区分双方棋子;用两个独立键盘避免了轮询和防抖的复杂逻辑,让操作更接近真实棋盘;用I2C LCD则极大节省了宝贵的IO口。整个制作过程,就是不断在“功能”、“成本”、“复杂度”和“可靠性”之间做权衡和验证。

2. 核心硬件选型与电路设计解析

2.1 主控与显示模块的抉择

主控芯片我选择了Arduino Mega 2560,而不是更常见的Uno。这是项目初期一个关键决策。井字棋需要控制18个LED引脚(9个双色LED,每个有红绿两个阳极)、14个键盘引脚(两个3x4矩阵键盘)以及LCD。粗略一算就需要超过30个IO口,Uno的20个口(其中14个数字口)显然捉襟见肘,即使使用扩展芯片也会增加复杂度和成本。Mega 2560拥有54个数字IO口和16个模拟口,资源绰绰有余,让我可以给每个LED和键盘列线分配独立的引脚,简化了电路和代码逻辑。虽然“杀鸡用牛刀”,但对于保证项目顺利进行、减少 multiplexing(多路复用)带来的编程复杂度,这个选择是值得的。

显示部分分为两大块:棋盘和状态提示。棋盘由9个共阴极双色LED(如红绿双色)构成。每个LED内部有两个独立的芯片(发红光和发绿光),共享一个阴极。这意味着控制它需要两个IO口:一个接红色阳极,一个接绿色阳极,阴极通过一个限流电阻接地。当红色阳极给高电平、绿色阳极给低电平时,发红光,代表玩家A的“X”;反之则发绿光,代表玩家B的“O”;两者都低则不亮,为空位。这种设计视觉区分度极高。

状态提示我选用了一块16x2字符型I2C LCD屏。这是另一个优化点。传统的1602 LCD需要连接至少6根线(RS, RW, E, D4-D7),而I2C版本只需要4根线(VCC, GND, SDA, SCL),通过一个背面的转接板与Arduino通信。这节省了连线,更重要的是节省了IO口。I2C是一种总线协议,理论上你可以在同一组SDA/SCL线上挂载多个设备,地址不同即可。对于这个项目,它让代码中更新显示信息变得异常简单,只需调用几行lcd.print()即可。

2.2 输入模块与供电方案

输入方面,我使用了两个3x4矩阵薄膜键盘。为什么是两个?而不是用一个键盘让玩家轮流按?这是为了极致简化游戏逻辑和避免误操作。设想一下,如果用一个键盘,程序需要不断扫描,还要判断当前是哪个玩家的回合,并防止玩家连续按两次。而使用两个独立的键盘,物理上就分开了玩家A和玩家B的输入设备。在代码中,我可以将两个键盘视为独立的输入源,只在轮到对应玩家时,才去读取其键盘的输入。这大大降低了软件复杂度,提升了操作体验,更接近真实世界中两个玩家对面而坐的感觉。

每个3x4键盘有7根线(4条行线,3条列线)。我将其直接连接到Arduino Mega的普通数字IO口上。由于Mega口线充足,我没有采用矩阵扫描常见的“行输出-列输入”并共用部分引脚的方法,而是为每个键盘的7根线都分配了独立引脚,并将其全部设置为INPUT_PULLUP模式。这样,在代码中检测某个键是否按下,就变成了检测特定引脚是否为低电平,逻辑清晰直白。

供电部分,整个系统由一块9V方块电池通过一个拨动开关供电。Arduino Mega的Vin引脚可以接受7-12V的输入,内部稳压器会将其降至5V为板子和外围模块供电。双色LED的工作电压一般在2V左右,电流约20mA,通过330欧姆的限流电阻连接到5V,是安全且亮度合适的。LCD屏和键盘模块的工作电压也是5V。选择电池供电是为了便携和完整性,让这个游戏盒子可以脱离电脑和电源适配器独立运行。

注意:330欧姆的限流电阻计算基于典型红色LED正向压降约1.8V,绿色约2.2V。Arduino IO口输出高电平时电压为5V。以红灯为例,电阻需分担的电压为 5V - 1.8V = 3.2V。期望电流为15mA(略低于最大值以保护IO口),根据欧姆定律 R = V / I = 3.2V / 0.015A ≈ 213欧姆。选用330欧姆是标准值,实际电流约为 3.2V / 330Ω ≈ 9.7mA,亮度足够且对IO口更安全。如果你使用的LED参数不同,需要重新计算。

2.3 电路连接图与布线心得

虽然原始资料提供了示意图,但在实际焊接时,清晰的布线规划至关重要。我的连接规划如下:

  1. LED矩阵:9个双色LED,共18个阳极。我将红色阳极依次连接到数字引脚 35-43,绿色阳极依次连接到 44-52。每个LED的共阴极引脚,先串联一个330欧姆电阻,然后将9个电阻的另一端全部用导线并联在一起,最后统一接到Arduino的GND引脚。务必确保电阻是接在阴极(接地端),这是共阴极接法的标准操作。
  2. 玩家A键盘:将其7根线连接到数字引脚 2-8。
  3. 玩家B键盘:将其7根线连接到模拟引脚 A0-A6(它们也可以作为数字引脚使用)。
  4. I2C LCD:四根线,VCC接5V,GND接GND,SDA接Mega的20号引脚(SDA),SCL接Mega的21号引脚(SCL)。
  5. 电源:9V电池正极通过开关接Mega的Vin引脚,负极接GND。

在面包板上搭建原型时,建议用不同颜色的杜邦线区分功能:例如红色线用于VCC,黑色线用于GND,黄色线用于LED控制,蓝色和绿色线分别用于两个键盘。这样在排查故障时一目了然。当所有功能测试无误后,再考虑转移到洞洞板或定制PCB上进行焊接,以获得更稳固的最终产品。

3. 木工结构设计与制作详解

3.1 木盒设计与尺寸考量

一个精致的木盒不仅能保护内部电路,更是提升项目整体质感的关键。我设计的盒子外观尺寸约为35cm(长)x 15cm(宽)x 4cm(高)。这个尺寸是经过计算的:内部需要容纳Arduino Mega(约10cm x 5.3cm)、9个LED以3x3排列(每个LED加间隔约需2.5cm,矩阵总长约7.5cm)、两个键盘(每个约6cm x 7cm)以及LCD屏幕(约8cm x 3.5cm)。留出足够的走线空间和元件间隙后,15x35的底板面积是合适的。4cm的高度则考虑了Mega板子加上排针的高度、电池的厚度,以及上下盖板的厚度。

我用CorelDraw绘制了激光切割文件。设计采用指接榫结构,这样不用胶水也能实现牢固拼接,也方便后期拆开维修。文件主要包括:底板一块、侧板四块、面板一块。面板是设计的重点,上面需要精准开孔:

  • 9个直径5mm的圆孔,用于嵌入LED,使其灯头刚好露出面板。
  • 两个矩形孔,用于放置薄膜键盘,尺寸需比键盘实际电路部分略大,以便键盘能卡在面板内侧。
  • 一个矩形孔,用于安装LCD屏幕,尺寸需与屏幕外框匹配。
  • 侧面还需开一个小孔,用于安装电源开关。

实操心得:在绘制开孔时,一定要在CorelDraw中建立准确的坐标参考线。先将所有内部元件(Arduino、LED矩阵等)在底板上按实际位置摆放好,用尺子量出它们面板对应开孔的中心坐标。面板的开孔位置必须与底板上的元件位置严格对应,否则组装时会对不上。建议先打印一份1:1的图纸,将实物元件放上去核对,确认无误后再送去切割。

3.2 材料选择与加工要点

我选择的是3mm厚的椴木板进行激光切割。椴木质细腻,切割边缘光滑,易于激光加工,且价格适中。切割完成后,用细砂纸轻轻打磨所有边缘,特别是开孔的内壁,去除激光灼烧产生的焦痕,使手感更光滑。

组装顺序很重要:

  1. 先将四块侧板与底板通过指接榫拼合,形成一个无盖的盒子。此时不用胶水,检查是否严丝合缝。
  2. 将Arduino Mega用铜柱或尼龙柱固定在底板的预定位置。同样,将电池座、LCD屏的I2C转接板也用热熔胶或螺丝固定好。
  3. 先进行内部布线。这是最耗时但也最需要耐心的一步。按照之前规划,将所有元件用杜邦线连接起来。线要留出足够长度,并尽量沿着盒子边缘走,用扎带或线卡固定,避免杂乱。尤其注意LED的引脚线,因为数量多,容易混乱,建议每连接一个就用标签纸做标记。
  4. 内部线路检查无误后,将9个LED从面板内侧穿过对应的圆孔,用热熔胶在面板背面固定。同样,将两个键盘和LCD屏从内侧卡入各自的孔位并固定。
  5. 将面板盖在盒体上。此时,所有元件的引脚都已在盒子内部。将面板上LED、键盘、LCD的引线与盒体内已经布置好的主线进行焊接或连接。强烈建议在此步骤前,给所有需要焊接的线头先上好锡
  6. 最后,将电源开关安装在侧孔,连接好电池线。盖上底板(如果设计了下盖),或用另外一块板子封底。

整个木工部分的核心是“由内而外”的装配逻辑。先固定核心主板和大型模块,再布置主线,最后安装面板元件并连接,能最大程度避免在狭小空间内操作不便的问题。

4. 软件逻辑与代码实现剖析

4.1 游戏状态机与数据结构

软件是项目的灵魂。井字棋的游戏逻辑虽然不复杂,但用嵌入式C++实现一个稳定、可读的状态机,需要好好设计。我首先定义核心的数据结构:

// 定义棋盘状态:0为空,1为玩家A(红X),2为玩家B(绿O) int board[3][3] = {0}; // 定义游戏全局状态 enum GameState { GAME_START, PLAYER_A_TURN, PLAYER_B_TURN, CHECK_WIN, GAME_OVER }; GameState currentState = GAME_START; // 玩家得分 int scorePlayerA = 0; int scorePlayerB = 0;

board数组是游戏的记忆核心。GameState枚举定义了游戏可能处于的几种状态,这是一种清晰的状态机编程模式,比用一堆布尔标志要优雅得多。主程序loop()函数将成为一个大的switch-case结构,根据currentState执行不同的代码块。

4.2 键盘扫描与去抖处理

尽管我们为两个键盘分配了充足的IO口,但机械按键的抖动问题必须处理。我采用了一种简单的“状态记录+延时判断”软件去抖法,而不是使用中断,以保持代码的简单性。

// 示例:检测玩家A键盘上“1”键是否被按下(假设接在引脚2上) bool isKeyAPressed(int keyPin) { if (digitalRead(keyPin) == LOW) { // 检测到低电平(按下) delay(50); // 延时约50ms,跳过抖动期 if (digitalRead(keyPin) == LOW) { // 再次确认 while(digitalRead(keyPin) == LOW); // 等待按键释放 return true; } } return false; }

在实际代码中,我需要为两个键盘的每个键(1-9)都编写类似的检测逻辑,并将按下的键值映射到棋盘的对应位置(例如,键盘上的‘1’键对应棋盘左上角board[0][0])。

4.3 LED控制与棋盘渲染

控制18个LED实质上就是控制18个数字引脚的高低电平。我定义了两个数组来管理引脚编号:

int redPins[9] = {35, 36, 37, 38, 39, 40, 41, 42, 43}; // 红阳极引脚 int greenPins[9] = {44, 45, 46, 47, 48, 49, 50, 51, 52}; // 绿阳极引脚

渲染函数updateBoard()会根据board数组的状态,遍历9个位置:

  • 如果board[i][j] == 1,则设置对应的redPins[index]HIGHgreenPins[index]LOW
  • 如果board[i][j] == 2,则设置redPins[index]LOWgreenPins[index]HIGH
  • 如果board[i][j] == 0,则红绿引脚都设为LOW(熄灭)。

这里有一个重要技巧:Arduino的IO口在初始化时应设置为OUTPUT模式,并且在改变某个LED状态时,最好先熄灭所有LED,再点亮目标LED,或者直接进行精确控制。对于这个项目,由于更新不频繁,采用直接精确控制即可。

4.4 胜负判定与游戏流程控制

胜负判定函数checkWinner()是经典逻辑:遍历所有8种可能连线(三行、三列、两条对角线),检查是否全部被同一玩家占据。此外,还需要判断是否平局(棋盘满且无胜者)。

主循环loop()中的状态迁移逻辑如下:

  1. GAME_START:在LCD显示欢迎信息,初始化棋盘为空,等待任意玩家按键开始。然后进入PLAYER_A_TURN
  2. PLAYER_A_TURN:LCD提示“Player A Turn”。扫描玩家A的键盘。当检测到有效按键(对应空位)时,将相应board位置设为1,更新LED显示,然后状态迁移到CHECK_WIN
  3. CHECK_WIN:调用checkWinner()。如果A赢,则显示获胜信息,A得分加一,状态跳至GAME_OVER。如果B赢,同理。如果平局,显示平局信息,进入GAME_OVER。如果暂无结果,则状态切换到PLAYER_B_TURN(如果刚才轮到A)或PLAYER_A_TURN(如果刚才轮到B)。
  4. PLAYER_B_TURN:逻辑同A。
  5. GAME_OVER:显示最终比分,等待一段时间(如5秒)或等待一个“重新开始”按键,然后重置棋盘,状态回到GAME_START

这个状态机确保了游戏流程的清晰和可控,避免了逻辑混乱。

4.5 I2C LCD显示优化

使用LiquidCrystal_I2C库可以轻松驱动LCD。初始化后,显示信息主要用到lcd.clear()lcd.print()。为了提升体验,我做了两点优化:

  • 分屏显示:第一行显示当前状态(如“Player A Turn”),第二行显示提示或比分(如“Score: 3-2”)。
  • 闪烁提示:在等待玩家操作时,可以让当前玩家的提示信息闪烁,增加互动感。这可以通过在loop()中定时切换显示内容来实现。

5. 系统集成、调试与问题排查实录

5.1 分模块测试流程

在将所有东西塞进盒子之前,分模块测试是避免灾难性错误的关键。我的测试顺序如下:

  1. Arduino基础测试:上传一个简单的Blink程序,确保板子本身工作正常。
  2. 单个LED测试:将一个双色LED通过电阻接在5V和GND之间,分别触碰红色和绿色阳极到5V,确认两种颜色都能正常点亮,且引脚对应正确。
  3. 键盘测试:编写一个简单程序,将单个键盘的引脚设置为INPUT_PULLUP,并在串口监视器中打印哪个引脚被拉低。依次按下每个键,确认键位映射正确。
  4. LCD测试:使用I2C扫描程序,确认LCD的I2C地址(通常是0x27或0x3F)。然后运行一个显示“Hello World”的示例程序。
  5. 集成测试(面包板阶段):将部分LED(如3个)和单个键盘连接到Arduino,编写一个小程序,实现按一个键点亮一个LED的功能。验证硬件连接和基础软件逻辑。
  6. 全功能测试(面包板阶段):将所有18个LED、两个键盘、LCD全部连接到面包板上。上传完整的游戏代码,进行一轮完整的双人对战测试。这个阶段可能会暴露电源带载能力不足(所有LED全亮时电流较大)、线缆连接错误、代码逻辑bug等问题。

5.2 常见问题与解决方案

在实际制作中,我遇到了以下几个典型问题,这里分享排查思路:

问题一:某个LED不亮或颜色不对。

  • 排查:首先检查该LED的焊接或连接是否牢固。用万用表二极管档测量LED本身是否完好。如果硬件无误,检查代码中该LED对应的引脚编号是否正确,以及控制逻辑(红/绿阳极电平设置)是否有误。特别注意:共阴极LED,阴极必须通过电阻接地,如果阴极接成了高电平,LED永远不会亮。

问题二:键盘按键无反应或反应混乱。

  • 排查:确认所有键盘引脚模式已设置为INPUT_PULLUP。用万用表测量按键按下时,对应引脚是否从高电平(约5V)可靠地变为低电平(接近0V)。如果电平变化正常但程序没反应,检查去抖代码的延时时间是否合适(通常20-50ms)。如果多个键互相影响,可能是接线错误导致引脚间短路。

问题三:LCD屏幕无显示或显示乱码。

  • 排查:首先确认I2C地址。检查SDA和SCL线是否接反(Mega上20是SDA,21是SCL)。用万用表测量LCD的VCC引脚是否有5V电压。如果显示乱码,可能是对比度问题,尝试调节I2C转接板上的电位器(如果有)。确保代码中lcd.init()lcd.backlight()被正确调用。

问题四:游戏运行一段时间后复位或行为异常。

  • 排查:这很可能是电源问题。当9个红色LED全亮时,总电流约为 9 * 10mA = 90mA。加上Arduino板自身消耗(约50mA)和其他模块,总电流可能接近200mA。劣质9V电池在高负载下电压会急剧下降,导致Arduino工作不稳定。解决方案:使用质量好的碱性9V电池,或者改用5V/2A的移动电源通过USB口供电。也可以在电源入口处并联一个470μF或更大的电解电容,以平滑电压波动。

问题五:木盒盖板后,某些功能失灵。

  • 排查:这通常是机械应力导致连接线松动或短路。在封盒前,确保所有导线都有适当的松弛度,没有被过度拉扯。用热熔胶或绝缘胶带固定关键连接点。检查面板上的元件(如键盘)是否安装过紧,压迫到了背面的焊盘或导线。

5.3 代码调试与优化技巧

  1. 善用串口调试:在关键状态切换处(如currentState改变、按键检测成功)使用Serial.print()输出信息,这是追踪程序流程最有效的方法。
  2. 状态可视化:在开发初期,可以用串口打印出board数组的当前状态,辅助判断胜负逻辑是否正确。
  3. 模拟输入:在键盘硬件连接好之前,可以编写模拟按键输入的代码,用串口发送指令来模拟玩家操作,提前测试游戏核心逻辑。
  4. 功耗考虑:如果希望电池续航更久,可以在loop()中,当游戏处于等待状态(如GAME_OVER等待重启)时,适当加入delay()以减少CPU空转。更进阶的做法是使用休眠模式,但这会复杂很多。

完成所有调试,将稳定的程序烧录进Arduino,组装好木盒,一个独一无二的电子井字棋游戏机就诞生了。它不仅是一个玩具,更是一个融合了数字逻辑、电路设计、嵌入式编程和手工制作的综合作品。通过这个项目,你会对如何将一个想法转化为实实在在的、可交互的物理设备有一个完整而深刻的理解。这种从虚到实、全链路打通的能力,正是创客精神的核心。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询