Arduino三原色猜色游戏:从电路搭建到编程实践

发布时间:2026/6/4 20:16:39
Arduino三原色猜色游戏:从电路搭建到编程实践
1. 项目概述一个能“玩”的色彩学实验如果你对物理课上的三棱镜分光实验或者美术课上的调色板还有印象大概会知道红、绿、蓝这三种光被称为“三原色”。它们就像色彩世界的三个基本音符通过不同的“和声”方式能谱写出我们眼中所见的一切颜色。这个项目就是把这个抽象的理论变成一个你可以亲手操作、即时反馈的电子游戏。我做的这个“三原色猜色游戏”核心玩法很简单系统会随机生成一个目标颜色这个颜色可能是红、绿、蓝中的一种也可能是其中任意两种混合出来的新颜色比如红绿黄绿蓝青蓝红品红。你的任务就是通过按下对应的按钮来“合成”出这个目标色。按对了皆大欢喜按错了它会告诉你再试试。整个过程通过一块LCD屏幕来显示目标和你的操作结果非常直观。为什么用Arduino来做因为它足够简单。对于电子爱好者、教育工作者或者任何想入门硬件编程的朋友来说Arduino提供了一个近乎“傻瓜式”的起点。你不需要从零开始设计复杂的电路也不用深究寄存器配置它的开发环境和丰富的库函数能让你把精力集中在“实现想法”本身。这个项目就是一个绝佳的练手机会它涵盖了数字输入按钮、输出LED、以及一种相对复杂的输出设备LCD屏的控制同时还需要一点基础的逻辑编程思维。做完它你对一个典型微控制器项目的完整流程——从电路搭建、代码编写到调试封装——会有一个非常清晰的认识。2. 核心设计思路与物料清单2.1 设计哲学从理论到实物的桥梁这个项目的设计核心是构建一个“理论-输入-处理-输出”的完整闭环。色彩混合理论是基石玩家的按钮操作是输入Arduino是进行逻辑判断和处理的大脑LCD屏幕和LED则是向玩家展示结果的窗口。我选择用硬件按钮而非触摸屏或传感器是出于可靠性和学习成本的考虑。实体按钮能给玩家明确的触觉反馈“按下”这个动作本身也更具仪式感在教学中尤其能加深印象。LCD屏幕则承担了主要的交互界面功能它需要清晰地显示目标颜色、玩家当前选择的颜色以及游戏状态正确/错误。而额外的LED我把它设计成一个独立的“色彩预览器”当你按下按钮组合时对应的LED会亮起让你在屏幕反馈之外还能直观地看到光色混合的物理效果这种“双重验证”能极大地增强学习体验。整个系统的逻辑流非常清晰上电初始化后Arduino随机选择一个目标颜色共6种可能红、绿、蓝、黄、青、品红并在LCD上显示。玩家通过三个分别代表红、绿、蓝的按钮进行组合输入。Arduino实时检测按钮状态将其转换为对应的颜色信号一方面驱动LED显示混合光另一方面与目标颜色比对。若匹配则显示成功信息并进入下一轮若不匹配则提示错误允许玩家继续尝试。2.2 物料清单与选型考量工欲善其事必先利其器。下面是我实际用到的所有元件以及为什么选它们。Arduino Uno 开发板 x1项目的核心大脑。选择Uno是因为它最为经典资源充足14个数字I/O6个模拟输入32KB存储兼容性无敌几乎所有的教程和库都以它为基准。对于本项目它的I/O口和计算能力绰绰有余。1602 LCD 屏幕带I2C接口x1人机交互的核心。我强烈推荐使用带有I2C接口的版本。传统的1602LCD需要连接多达6根线RS, EN, D4, D5, D6, D7, VCC, GND而I2C版本通过一个转接板只需要连接4根线VCC, GND, SDA, SCL极大地简化了布线也节省了宝贵的I/O口。显示内容为2行16字符足够显示“Target: RED”和“Your: GREEN”这样的信息。5mm LED红、绿、蓝各1个用于物理色彩混合演示。选择5mm是因为其亮度适中便于观察。务必注意不同颜色的LED其正向压降Vf不同通常红色约1.8-2.2V绿色和蓝色约3.0-3.4V这意味着它们不能直接使用相同的限流电阻下文电路部分会详细说明。轻触开关按钮x3玩家的输入设备。选择最常用的6x6mm四脚轻触开关价格便宜手感明确。我们需要三个分别对应红、绿、蓝三原色。220欧姆电阻 x3用于限制LED的电流防止烧毁。阻值选择基于Arduino的5V输出和LED的压降计算而来。10k欧姆电阻 x3作为按钮的下拉电阻。这是确保按钮未按下时输入引脚能稳定读到低电平0的关键避免因引脚悬空产生随机误触发。面包板 x1 及若干杜邦线用于快速搭建和测试电路。面包板是原型开发阶段的神器免去了焊接的麻烦可以随意调整连接。纸盒或亚克力外壳 x1用于最终产品的封装。一个好看的外壳能让项目从“实验品”升级为“产品”。我使用硬卡纸制作成本低且易于加工。你也可以使用激光切割亚克力板效果更佳。USB数据线及5V电源为Arduino供电。在调试阶段通过USB连接电脑即可若想独立运行则需要一个5V的USB电源适配器。注意关于LED电阻的计算。以蓝色LED为例假设其正向压降(Vf)为3.2VArduino输出高电平为5V我们希望工作电流(I)在10-20mA之间足够亮且安全。根据欧姆定律限流电阻 R (Vcc - Vf) / I。取I15mA0.015A则 R (5 - 3.2) / 0.015 120欧姆。市面上没有120欧姆的标准电阻故选用最接近的220欧姆。虽然电流会略小约8.2mA但亮度完全可接受且更安全。红色LED的Vf较低约2.0V用220欧姆电阻时电流约为13.6mA也在安全范围内。3. 电路设计与连接详解电路是项目的骨架可靠的连接是一切功能的基础。我将整个电路分为三个相对独立的部分主控与供电、输入按钮、输出LCD和LED这样便于理解和排查故障。3.1 主控核心与电源电路首先确保你的Arduino Uno已经通过USB线连接到电脑或5V电源适配器上。此时板上的电源指示灯应该亮起。Arduino Uno本身会通过USB口或外部电源接口获得5V电压并通过板上的稳压电路为自身和外部元件供电。我们需要从Arduino上引出两种电源5V和GND地线。在面包板上通常会用两条长排孔作为电源正极总线红色和地线总线蓝色或黑色。将Arduino的5V引脚连接到面包板的总线将GND引脚连接到面包板的-总线。这样所有其他元件都可以方便地从这两条总线上取电。3.2 按钮输入电路搭建三个按钮的接法是完全相同的遵循典型的“下拉电阻”接法。这是数字输入电路的经典配置目的是在按钮未按下时给输入引脚一个确定的低电平0V。连接按钮一侧取一个轻触开关将其跨接在面包板的中沟上。将开关同一侧的两个引脚它们在内部是短接的中的其中一个用杜邦线连接到面包板的GND总线。连接下拉电阻在开关另一侧的两个引脚中选择一个连接一个10kΩ的电阻。这个电阻的另一端也连接到面包板的GND总线。这样无论开关状态如何这一侧通过电阻始终与GND相连。连接信号线与电源在连接了10kΩ电阻的那个引脚上再引出一根杜邦线这根将作为信号线连接到Arduino的某个数字输入引脚例如我分配红按钮到引脚2绿到3蓝到4。最后从面包板的5V总线引一根线连接到开关这一侧的另一个空闲引脚上。现在来分析原理当按钮未按下时信号线通过10kΩ电阻“下拉”到GNDArduino读取到低电平0。当按钮按下时5V电源通过按钮的金属触点直接连接到信号线由于这条通路的电阻远小于10kΩ信号线被“上拉”到接近5V的高电平1。10kΩ的电阻在这里起到了关键作用在按钮按下时它限制了从5V流向GND的电流避免短路在未按下时它确保了信号线电位被牢牢固定在GND。3.3 LCD屏幕与LED输出电路连接LCD屏幕I2C版本连接极其简单。找到屏幕背面的I2C转接板上面通常有4个引脚VCC、GND、SDA、SCL。VCC- 面包板5V总线GND- 面包板GND总线SDA- Arduino Uno的A4引脚在Uno上SDA固定为A4SCL- Arduino Uno的A5引脚在Uno上SCL固定为A5 I2C通信的优势就在这里只需要两根数据线就能控制屏幕大大简化了布线。LED电路三个LED的接法类似但需注意极性。LED有两个引脚长脚是阳极正极短脚是阴极负极。将红色LED的阳极长脚通过一个220Ω的电阻连接到Arduino的一个数字输出引脚例如我分配红LED到引脚5。LED的阴极短脚直接连接到面包板的GND总线。绿色和蓝色LED同理分别连接到引脚6和7并各自串联一个220Ω电阻后接地。 这样当Arduino对应的引脚输出高电平5V时电流从引脚流出经过电阻和LED流向GNDLED点亮。电阻必不可少它限制了电流大小保护LED和Arduino引脚不被过大的电流损坏。实操心得面包板布局的艺术。在面包板上布线时切忌“飞线”纵横交错。我的习惯是将Arduino放在面包板一侧电源总线靠近Arduino。输入元件按钮集中在一区输出元件LED集中在另一区LCD单独放置。所有连接尽量横平竖直使用不同颜色的杜邦线区分功能如红色接5V黑色接GND黄色为信号线。这样不仅美观在出现故障时也能一眼看清连接关系快速定位问题。在连接每个元件前最好用万用表的通断档检查一下导线和元件本身是否完好特别是按钮有时会因氧化导致接触不良。4. 程序代码的逐行解析与编写电路是身体程序是灵魂。下面我将完整代码拆解成几个功能模块并详细解释每一部分的作用和编写逻辑。你需要使用Arduino IDE来编写和上传代码。4.1 库引入与全局变量定义任何Arduino程序都从引入必要的库开始。对于I2C LCD我们需要Wire.h和LiquidCrystal_I2C.h。#include Wire.h #include LiquidCrystal_I2C.h // 定义按钮引脚 const int buttonRed 2; const int buttonGreen 3; const int buttonBlue 4; // 定义LED引脚 const int ledRed 5; const int ledGreen 6; const int ledBlue 7; // 定义目标颜色和玩家当前颜色 int targetColor 0; int playerColor 0; // 定义颜色常量用二进制位表示便于组合 #define COLOR_RED 1 // 二进制 001 #define COLOR_GREEN 2 // 二进制 010 #define COLOR_BLUE 4 // 二进制 100 // 组合颜色 #define COLOR_YELLOW (COLOR_RED | COLOR_GREEN) // 011 #define COLOR_CYAN (COLOR_GREEN | COLOR_BLUE) // 110 #define COLOR_MAGENTA (COLOR_RED | COLOR_BLUE) // 101 // 初始化LCD对象参数为I2C地址、列数、行数。常见的1602 I2C地址是0x27或0x3F LiquidCrystal_I2C lcd(0x27, 16, 2);代码解析#define宏定义这里用了一个小技巧。用1、2、4这种2的幂次方数来代表红、绿、蓝是因为它们的二进制表示中只有一位是1001, 010, 100。这样当需要表示混合色时只需要使用按位或操作符|即可。例如COLOR_RED | COLOR_GREEN的结果是二进制的011即十进制3它唯一地代表了黄色。这种表示法在判断颜色组合时非常高效。LiquidCrystal_I2C对象如果你的屏幕不显示最常见的原因就是I2C地址不对。你可以使用一个简单的扫描程序来查找地址或者尝试将0x27改为0x3F。4.2 初始化设置setup()setup()函数只在设备上电或复位后运行一次用于初始化各种设置。void setup() { // 初始化串口用于调试输出可选但强烈建议保留 Serial.begin(9600); // 初始化按钮引脚为输入模式并启用内部上拉电阻 pinMode(buttonRed, INPUT_PULLUP); pinMode(buttonGreen, INPUT_PULLUP); pinMode(buttonBlue, INPUT_PULLUP); // 初始化LED引脚为输出模式 pinMode(ledRed, OUTPUT); pinMode(ledGreen, OUTPUT); pinMode(ledBlue, OUTPUT); // 初始状态下关闭所有LED digitalWrite(ledRed, LOW); digitalWrite(ledGreen, LOW); digitalWrite(ledBlue, LOW); // 初始化LCD lcd.init(); lcd.backlight(); // 打开背光 lcd.clear(); lcd.setCursor(0, 0); lcd.print(Oh!! Color Game); lcd.setCursor(0, 1); lcd.print(Initializing...); // 随机数种子初始化用模拟引脚0的“浮空”噪声作为随机源 randomSeed(analogRead(0)); // 生成第一个目标颜色 generateNewTarget(); delay(2000); // 显示初始化信息2秒 lcd.clear(); }代码解析INPUT_PULLUP模式这里我使用了Arduino引脚的内置上拉电阻而不是前面电路里提到的外部下拉电阻。这是一种更简洁的接法。当设置引脚为INPUT_PULLUP时内部一个约20kΩ的电阻会将引脚电平上拉到5V高电平。此时你的按钮电路需要稍作修改按钮的一端接信号引脚另一端直接接GND。当按钮按下时引脚被接地读到低电平0未按下时读到高电平1。逻辑与之前的下拉电阻电路相反。请务必注意如果你按照本文第3节的电路连接了外部下拉电阻则这里应使用INPUT模式而不是INPUT_PULLUP否则会造成冲突。本文代码以内部上拉模式为例。randomSeed(analogRead(0))random()函数如果不“播种”每次重启产生的随机序列是一样的。analogRead(0)读取未连接任何信号的模拟引脚0会得到一个不断变化的噪声值用它作为种子可以产生更随机的序列。4.3 核心游戏逻辑loop()与辅助函数loop()函数会周而复始地运行是游戏的主循环。void loop() { // 1. 读取玩家输入 readPlayerInput(); // 2. 根据玩家输入更新LED显示 updateLEDs(); // 3. 在LCD上更新玩家当前选择的颜色 displayPlayerColorOnLCD(); // 4. 检查是否匹配 if (playerColor ! 0) { // 确保玩家至少按了一个按钮 if (playerColor targetColor) { // 匹配成功 gameSuccess(); } else { // 可以在这里添加匹配失败的提示比如让LED闪烁一下 // 本版本设计为不提示让玩家继续尝试 } } delay(100); // 一个小延迟用于去抖动和降低CPU占用 }关键辅助函数解析readPlayerInput()- 读取并解码按钮状态void readPlayerInput() { playerColor 0; // 每次循环先清零 // 注意由于使用了内部上拉按钮按下时为LOW if (digitalRead(buttonRed) LOW) { playerColor | COLOR_RED; } if (digitalRead(buttonGreen) LOW) { playerColor | COLOR_GREEN; } if (digitalRead(buttonBlue) LOW) { playerColor | COLOR_BLUE; } }这个函数利用按位或操作符|来组合颜色。例如同时按下红和绿键playerColor会先被设为COLOR_RED1然后与COLOR_GREEN2进行按位或结果就是3二进制011即黄色。updateLEDs()- 根据playerColor变量控制LEDvoid updateLEDs() { // 使用按位与操作符 来判断playerColor中是否包含某种颜色 digitalWrite(ledRed, (playerColor COLOR_RED) ? HIGH : LOW); digitalWrite(ledGreen, (playerColor COLOR_GREEN) ? HIGH : LOW); digitalWrite(ledBlue, (playerColor COLOR_BLUE) ? HIGH : LOW); }这里使用了三元运算符? :和按位与操作符。(playerColor COLOR_RED)会检查playerColor的二进制表示中代表红色的那一位是否为1。如果是表达式结果为非零真则设置红色LED引脚为HIGH点亮否则为LOW熄灭。displayPlayerColorOnLCD()和displayTargetColorOnLCD()- LCD显示void displayPlayerColorOnLCD() { lcd.setCursor(0, 1); // 第二行开头 lcd.print(Your: ); lcd.print(colorToString(playerColor)); } void displayTargetColorOnLCD() { lcd.setCursor(0, 0); // 第一行开头 lcd.print(Target: ); lcd.print(colorToString(targetColor)); }这两个函数调用了一个通用的colorToString()函数将数字颜色代码转换为可读的字符串。colorToString()- 颜色代码转字符串String colorToString(int colorCode) { switch (colorCode) { case COLOR_RED: return RED ; case COLOR_GREEN: return GREEN; case COLOR_BLUE: return BLUE ; case COLOR_YELLOW: return YELLOW; case COLOR_CYAN: return CYAN ; case COLOR_MAGENTA: return MAGENTA; default: return NONE ; // 无输入时 } }generateNewTarget()和gameSuccess()- 游戏流程控制void generateNewTarget() { // 随机生成0-5的数字对应6种颜色 int randomIndex random(0, 6); switch (randomIndex) { case 0: targetColor COLOR_RED; break; case 1: targetColor COLOR_GREEN; break; case 2: targetColor COLOR_BLUE; break; case 3: targetColor COLOR_YELLOW; break; case 4: targetColor COLOR_CYAN; break; case 5: targetColor COLOR_MAGENTA; break; } displayTargetColorOnLCD(); lcd.setCursor(0, 1); lcd.print(Your: NONE); // 清空玩家行 } void gameSuccess() { lcd.clear(); lcd.setCursor(0, 0); lcd.print(Congratulations!); lcd.setCursor(0, 1); lcd.print(Color Matched!); // 让所有LED闪烁三次以示庆祝 for (int i 0; i 3; i) { digitalWrite(ledRed, HIGH); digitalWrite(ledGreen, HIGH); digitalWrite(ledBlue, HIGH); delay(300); digitalWrite(ledRed, LOW); digitalWrite(ledGreen, LOW); digitalWrite(ledBlue, LOW); delay(300); } delay(2000); // 显示成功信息2秒 // 开始新一局 generateNewTarget(); // 重置玩家输入 playerColor 0; updateLEDs(); }编程心得状态机思维。这个游戏程序本质上是一个简单的状态机等待输入-判断-成功/失败-重置/继续。在loop()中清晰地划分出“读取输入”、“更新输出”、“逻辑判断”这几个阶段能让代码结构非常清晰易于调试和扩展。例如如果你想增加一个“倒计时”功能只需要在“逻辑判断”阶段加入时间检查即可不会干扰其他部分的逻辑。5. 系统调试与问题排查实录即使按照教程一步步来第一次成功前也难免遇到问题。下面是我在制作和教学过程中遇到的一些典型问题及解决方法希望能帮你快速通关。5.1 LCD屏幕一片空白或乱码这是最常见的问题。排查电源首先确认LCD的VCC和GND是否正确连接到5V和GND。用万用表测量一下转接板上的电压是否为5V左右。排查I2C地址80%的空白屏问题源于地址错误。尝试将代码中的0x27改为0x3F或者运行一个I2C扫描程序来确认地址。排查接线确认SDA和SCL是否分别接在了Arduino Uno的A4和A5引脚上。这两个引脚是固定的不能接错。调节对比度很多I2C模块上有一个蓝色的电位器用螺丝刀旋转它可以调节屏幕对比度。如果对比度设置不当即使屏幕在工作你也看不到字符。在通电状态下慢慢旋转电位器直到字符出现。5.2 按钮按下无反应或一直显示按下接线模式与代码模式不匹配这是最关键的检查点。如果你按照本文第3节使用了外部下拉电阻电路按钮一端接信号线另一端接5V那么代码中引脚模式应设为INPUT并且逻辑是digitalRead(pin) HIGH表示按下。如果你按照本文代码示例使用了内部上拉电阻INPUT_PULLUP那么按钮的接法应是一端接信号引脚另一端直接接GND。此时逻辑是digitalRead(pin) LOW表示按下。检查按钮好坏使用万用表通断档测量按钮四脚对角线两两之间的通断。按下时应导通松开时应断开。软件去抖动机械按钮在按下和松开的瞬间触点会发生物理弹跳导致Arduino在几毫秒内读到多次快速变化的电平。虽然本代码中的delay(100)有一定去抖效果但对于更敏感的应用可能需要更专业的去抖逻辑比如检测到电平变化后等待一段时间如50ms再读取一次确认。5.3 LED不亮或亮度异常检查极性LED是二极管电流只能从阳极长脚流向阴极短脚。反接不会亮。务必确认长脚通过电阻接信号引脚短脚接GND。检查电阻值确保使用了限流电阻如220Ω。直接连接5V到LED会瞬间烧毁它。如果LED微亮可能是电阻值过大如果非常亮甚至发热可能是电阻值过小或短路。检查代码输出用digitalWrite(ledRed, HIGH);点亮红色LED后用万用表电压档测量该引脚对GND的电压应该接近5V。如果为0检查代码中引脚号定义是否正确或者该引脚是否被其他部分代码意外控制了。5.4 颜色混合显示不正确物理混合观察确保红、绿、蓝三个LED在面包板上的位置足够靠近。理想情况下它们的发光点应该几乎在同一点这样光在空间中才能充分混合。你可以尝试用一小段乳白色的热缩管或磨砂塑料片将三个LED头部套在一起作为简易的“混光器”效果会好很多。代码逻辑检查通过串口监视器设置波特率为9600打印出playerColor和targetColor的值。当同时按下红和绿键时playerColor应该等于3COLOR_YELLOW。如果不对检查readPlayerInput()函数中的按位或逻辑。5.5 游戏逻辑故障如无法判断成功串口调试大法在loop()函数中添加串口打印语句将关键变量如targetColor,playerColor以及各个按钮的读数实时打印出来。这是排查逻辑错误最有效的手段。你可以清楚地看到程序“以为”发生了什么与你“实际”做了什么进行对比。确保玩家输入不为零在成功判断if (playerColor targetColor)之前我加了一个条件if (playerColor ! 0)。这是为了防止玩家没有按任何按钮时playerColor为0误触发与目标颜色为“NONE”虽然我们没定义这个状态的比较。6. 外壳制作与项目优化建议当电路和码都调试无误后一个自制的外壳能让你的项目瞬间提升档次从一堆线材和元件变成一个真正的“产品”。6.1 简易纸壳制作我使用的是厚度约1.5mm的硬卡纸它易于切割和折叠且有一定强度。设计在纸上画出展开图。你需要一个底板放置面包板和Arduino一个前方面板开孔用于LCD、按钮和LED以及四个侧壁。为LCD开一个矩形窗为三个按钮开圆孔为LED开一个小圆孔或直接让LED凸出。切割与折叠用美工刀和钢尺精确切割用没有墨水的圆珠笔沿着折痕线划一下便于折叠。用白胶或热熔胶粘合接缝。固定元件将按钮从面板内侧穿过圆孔用螺母固定。LCD可以用热熔胶从四周固定在面板内侧。面包板和Arduino可以用双面胶或扎带固定在底板上。美化用彩色马克笔或贴纸装饰外壳写上游戏名称和操作说明。6.2 进阶优化建议如果你想让这个项目更完善可以尝试以下扩展增加声音反馈加入一个无源蜂鸣器在成功时播放一段欢快的旋律失败时播放一个低音体验更佳。增加难度与计分引入计时功能要求玩家在限定时间内猜出颜色或者增加一个计分系统连续成功得分更高。更复杂的颜色目标颜色可以不限于两色混合尝试生成一个需要同时按下三个按钮才能匹配的“白色”目标或者引入“亮度”概念通过快速按击按钮模拟PWM来调节某种颜色的强弱从而匹配更复杂的颜色。无线化用蓝牙模块如HC-05替换有线连接制作一个手机App来发送目标颜色或显示游戏状态将硬件项目与软件开发结合起来。这个基于Arduino的三原色猜色游戏从概念上讲它是对色彩理论的一次生动实践从技术上讲它是一次完整的嵌入式开发流程体验。它涉及了电路设计、微控制器编程、人机交互和简单的机械封装。无论你是想用于STEAM教学还是作为个人创客入门的第一件作品它都提供了一个结构清晰、可扩展性强的模板。最重要的是当你按下按钮看到LED混合出预想中的颜色并在屏幕上看到“Congratulations!”时那种将抽象想法变为可触摸现实的成就感正是创客精神的精髓所在。