基于SAMD21与RFM69HCW的无线战舰对战游戏机全栈开发实战
1. 项目概述:打造一台无线战舰对战游戏机
几年前,我在一个创客展上看到有人用两块Arduino Uno和点阵屏做了一对战棋游戏机,但拖着长长的串口线,总觉得少了点“对战”的仪式感。当时就想,如果能像小时候玩红白机那样,两台设备隔空对战,那才带劲。这个念头一直没放下,直到我接触到了SAMD21和RFM69HCW这对组合,才觉得时机成熟了。SAMD21作为一款基于ARM Cortex-M0+内核的现代微控制器,性能远超传统的8位AVR芯片,而RFM69HCW则是一款在创客圈口碑极佳的Sub-GHz无线模块,以低功耗和远距离通信著称。把它们俩凑在一起,再配上能显示丰富色彩的LED矩阵,不正是实现无线对战游戏机的绝佳平台吗?
这个项目的核心,就是打造两台完全独立、通过无线电通信的战舰对战游戏机。每台设备都拥有一块8x8的双色LED矩阵作为战场显示屏,玩家可以在这个“海图”上布置自己的舰队,并通过无线电向对手的阵地发起攻击。整个过程,从微控制器最底层的固件烧写、PCB的焊接组装,到游戏逻辑与无线通信协议的代码编写,都需要亲自动手。它不仅仅是一个游戏,更是一个完整的嵌入式系统开发实践,涵盖了硬件设计、通信协议、驱动编写和人机交互等多个层面。无论你是想深入学习SAMD21这款芯片,还是希望搞明白如何让两块板子通过无线电“对话”,这个项目都能给你带来一次扎实的实战体验。
2. 核心硬件选型与设计思路拆解
2.1 微控制器:为什么是ATSAMD21E18A?
在项目启动时,最直接的选择可能是Arduino Nano,它开箱即用,生态成熟。但我最终选择了从“空白”的ATSAMD21E18A芯片开始,主要基于以下几点考量:
首先,是性能与资源的提升。ATSAMD21E18A运行在48MHz,拥有256KB的Flash和32KB的SRAM,这比常见的ATmega328P(16MHz,32KB Flash,2KB SRAM)高出一个数量级。更大的内存空间意味着我们可以更从容地处理双色LED矩阵的显存、无线通信的数据包缓存以及复杂的游戏状态机,而不用担心内存溢出。其ARM Cortex-M0+内核也提供了更高效的指令集和更低的功能。
其次,是学习与定制的价值。使用预编程的Arduino开发板,很多底层细节被封装了起来。而从一个空白芯片开始,你需要亲自处理从引导加载程序(Bootloader)烧录、时钟树配置到引脚功能映射的所有步骤。这个过程虽然增加了初期的复杂度,但能让你彻底理解一块微控制器是如何从“砖头”变成“智能大脑”的,这种经验对于后续进行更复杂的定制化硬件设计至关重要。
最后,是芯片本身的特性。SAMD21系列芯片支持丰富的通信接口(多个SPI、I²C、UART)和模拟功能(12位ADC、10位DAC),为未来功能扩展留下了充足空间。例如,项目中未使用的10位DAC,未来完全可以用来增加声音效果。
注意:ATSAMD21E18A采用32引脚TQFP封装,手工焊接有一定难度,需要准备好尖头烙铁、助焊剂和放大镜。焊接时务必确认芯片上的小圆点或缺口与PCB丝印对齐。
2.2 无线模块:RFM69HCW的平衡之道
无线方案有很多,比如更常见的蓝牙(BLE)或Wi-Fi(ESP8266/32)。选择RFM69HCW这种工作在Sub-GHz频段(北美915MHz,欧洲868MHz)的射频模块,是基于游戏场景的特殊需求。
通信距离与可靠性:蓝牙的有效距离通常在10米以内,且穿墙能力较弱。而RFM69HCW在低功耗模式下,配合一个简单的¼波长天线,在复杂的室内环境(如多房间的公寓)中实现50-100米的稳定通信并不困难。这对于在家里任何角落展开“海战”至关重要。它的通信机制相对底层,你可以精确控制发射功率、频率和调制方式,从而在功耗和距离间取得最佳平衡。
“感觉”更对味:这一点听起来有点主观,但却很重要。Wi-Fi或蓝牙总让人联想到手机、电脑,而一个独立的、通过特定无线电频率通信的专用游戏设备,其体验更纯粹,更有“玩具”或“专业设备”的质感,这与战舰对战的复古机械感非常契合。
功耗与实时性:RFM69HCW在接收和发射状态下的电流消耗可以控制在十几到几十毫安,配合SAMD21优秀的低功耗管理模式,整个设备用电池驱动运行很长时间是可行的。此外,其点对点的通信方式延迟极低,适合需要快速响应的游戏交互。
实操心得:RFM69HCW模块有多个变种(HCW代表高功率版本),务必根据你所在地区法规选择正确频段(如915MHz用于北美,868MHz用于欧洲)。擅自使用不合规频段是违法的。在代码中,可以通过
rf69.setFrequency(915.0)来设置。
2.3 显示核心:64像素双色LED矩阵的取巧设计
显示部分是本项目的视觉亮点。我们使用了128颗0606封装的贴片LED(64颗橙色,64颗蓝色),将它们排列成两个紧密相邻的8x4网格,共同组成一个8x8的“像素”阵列。这里的巧妙之处在于,每个“像素”实际上由一颗橙色LED和一颗蓝色LED紧挨着组成。
混色原理:当只点亮橙色LED时,该像素显示橙色;只点亮蓝色LED时,显示蓝色;当同时以相同亮度点亮橙、蓝两颗LED时,由于人眼的视觉暂留和混色效应,我们会看到第三种颜色——在这里是粉色(橙+蓝)。这样,我们仅用两种物理颜色的LED,就实现了橙、蓝、粉三种视觉颜色的显示。这比直接使用三色RGB LED成本更低,布线也更简单。
驱动方案:直接使用微控制器的GPIO驱动128颗LED是不可行的,需要太多的引脚和电流。这里采用了专用的LED驱动芯片HT16K33A。这款芯片通过I²C总线与主控通信,内部集成了显存和扫描逻辑,可以驱动最多16x8(128段)的LED矩阵。我们使用两片HT16K33A,分别驱动左半屏(4列x8行x2色=64路)和右半屏(4列x8行x2色=64路),完美解决了驱动问题。I²C总线只需两根线(SDA, SCL),极大地节省了宝贵的GPIO资源。
3. 电路设计与PCB组装实战
3.1 原理图关键节点解析
整个系统的电路可以看作是以SAMD21为核心,连接了三个主要外设:RFM69HCW(SPI)、HT16K33A x2(I²C)和用户按钮(GPIO)。电源部分由Micro USB口输入5V,通过AP2112K线性稳压器转换为稳定的3.3V,为整个系统供电。
SPI接口连接:RFM69HCW是标准的4线SPI设备。连接时需注意:
SAMD21.MOSI->RFM69HCW.MOSISAMD21.MISO->RFM69HCW.MISOSAMD21.SCK->RFM69HCW.SCKSAMD21.PIN_x(任意GPIO) ->RFM69HCW.NSS(片选)- 另外,还需要连接中断引脚
RFM69HCW.DIO0到SAMD21的一个外部中断引脚,用于高效处理无线电数据接收。
I²C接口连接:两片HT16K33A共享同一条I²C总线(SDA, SCL),但具有不同的I²C地址。通常HT16K33A的地址可以通过一个地址引脚(A0)的电平来设置。在设计中,需要将两个驱动器的A0引脚分别接高电平和低电平,以便SAMD21能够区分它们。
按钮与LED布局:五个战术按钮(上、下、左、右、开火)通过10kΩ电阻上拉到3.3V,按下时接地,SAMD21检测低电平。LED矩阵的布局是手工设计中最耗时的部分,务必在PCB设计软件中反复核对原理图与封装,确保每个LED的正负极(阳极/阴极)方向与驱动芯片的输出通道一一对应。一个常见的技巧是在PCB丝印层为LED的阴极(通常是带有绿色标记或较短引脚的一端)添加明确的标识。
3.2 PCB焊接组装:从钢网到热风枪
对于包含大量0603、0805封装的贴片元件和QFP芯片的PCB,使用焊锡膏和热风枪/加热板进行回流焊是最有效率的方法。
- 准备工作:首先,确保你有一张与PCB完全匹配的激光钢网。将PCB固定在平稳的台面上,对齐并贴上钢网。
- 涂抹焊锡膏:用刮刀将适量的焊锡膏均匀地刮过钢网的每一个开孔。移开钢网后,PCB的每个焊盘上应该留下一座座大小均匀的“焊锡膏小山”。
- 贴装元件:这是最需要耐心和稳定性的环节。使用尖头镊子,借助放大镜或显微镜,按照从低到高、从小到大的顺序放置元件。先贴电阻、电容、二极管,然后是LED,最后是QFP封装的芯片。对于ATSAMD21E18A和HT16K33A这类有方向性的芯片,必须百分百确认其方向(第1引脚标记)与PCB丝印对齐。
- 回流焊接:将贴好元件的PCB小心地放置在预热好的加热板上。观察焊锡膏的变化:先会变成灰色,然后逐渐熔化,呈现光亮、圆润的液态,最后冷却凝固。整个过程通常在两三分钟内完成。我使用的加热板设定在215°C左右关闭,利用余热完成焊接,这样可以防止过热损坏LED。
- 检查与修补:焊接完成后,必须用放大镜仔细检查。重点查看QFP芯片引脚间是否有桥接,LED是否虚焊或放反。发现桥接可以用烙铁配合吸锡带或助焊剂清理;发现元件错误则需要用热风枪局部加热后取下重焊。
踩过的坑:我在第一版设计中,遗漏了用于JTAG编程的VIN和GND测试点。这导致在烧录引导加载程序时,不得不从RFM69HCW模块的过孔上“借电”,操作非常别扭且危险。强烈建议:在任何自定义的SAMD21设计上,务必预留出至少5个测试点:VCC(3.3V)、GND、SWDIO、SWCLK、RESET。这会为后续的调试和编程省去无数麻烦。
4. 软件基石:为空白SAMD21注入灵魂
4.1 引导加载程序(Bootloader)烧录指南
一块全新的ATSAMD21芯片内部是空的,没有程序,甚至没有通过USB与电脑通信的能力。我们需要先通过SWD接口,为其烧录一个引导加载程序。这个过程相当于给一台新电脑安装最基本的BIOS系统。
所需工具:你需要一个SWD调试器。我使用的是J-LINK EDU Mini,它性价比高且被广泛支持。当然,ST-LINK V2等兼容工具也可以。还需要四根杜邦线连接调试器与板上的SWD接口(SWDIO, SWCLK, RESET, GND),以及供电(VCC, 通常是3.3V)。
操作步骤:
- 环境搭建:在Windows电脑(或虚拟机)上安装Atmel Studio(现为Microchip Studio)。这是一个功能强大的集成开发环境,内置了器件编程工具。
- 连接硬件:用杜邦线将J-LINK与PCB上的对应测试点连接牢固。确保供电正常。
- 识别器件:打开Atmel Studio,进入
Tools -> Device Programming。选择工具为J-LINK,接口为SWD,设备选择ATSAMD21E18A。点击Apply,然后Read。如果连接正确,你会看到读出的设备签名和电压。 - 配置熔丝位(Fuses):这是关键一步。熔丝位是芯片内部的一些特殊配置位。我们需要将
BOOTPROT(引导保护区大小)先设置为0字节(禁用保护),以便擦写引导加载程序区域。在Fuses标签页找到BOOTPROT,设置为0,点击Program。 - 烧录.hex/.bin文件:切换到
Memories标签页。这里我们需要一个现成的引导加载程序二进制文件。我强烈推荐使用Adafruit维护的版本,稳定且兼容Arduino IDE。找到你下载的bootloader-xxx.bin文件,在Flash部分点击...选择它,然后执行Erase & Program,最后Verify。 - 重新保护引导区:烧录完成后,回到
Fuses页,将BOOTPROT设置为8192(即8KB,保护我们刚刚烧录的引导程序区域不被意外覆盖),再次Program。 - 验证:断开J-LINK,通过Micro USB线将PCB连接到电脑。如果一切顺利,电脑会识别出一个新的串口设备(如
COMx或/dev/ttyACM0),并且设备管理器中可能会出现一个“Arduino Zero”或“Generic SAMD21”之类的设备。恭喜,你的芯片现在“活”了!
4.2 开发环境搭建与项目代码结构
有了引导加载程序,我们就可以使用更熟悉的Arduino IDE进行后续开发了。首先需要在Arduino IDE的“开发板管理器”中安装“Arduino SAMD Boards (32-bits ARM Cortex-M0+)”支持包。
引脚定义的坑:由于我们使用了Adafruit的引导程序,其引脚编号映射可能与官方Arduino定义略有不同。例如,SAMD21的PA08引脚,在Arduino IDE中可能被定义为数字引脚8,但实际的物理位置需要对照你所使用的开发板定义文件(例如,Adafruit Qt Py M0的定义)。在代码中,你需要根据自己PCB的实际连线,重新定义这些引脚。我的做法是,在代码开头用#define进行清晰的别名定义,例如:
#define LED_DRIVER_LEFT_ADDR 0x70 #define LED_DRIVER_RIGHT_ADDR 0x71 #define RFM69_CS_PIN 4 #define RFM69_IRQ_PIN 3 #define BUTTON_UP_PIN 0 // ... 以此类推代码模块化设计:整个项目的代码可以清晰地分为几个模块:
- 硬件抽象层:包含对HT16K33A驱动芯片的封装类(用于控制LED显示)、对RFM69HCW的初始化与数据收发封装。
- 游戏逻辑引擎:负责管理两个8x8的游戏网格(一个自己的,一个对手的),处理船只的随机布置、攻击判定、胜负判断等。
- 状态机与用户界面:这是主循环的核心。它管理着几个状态:
MENU(菜单)、PLACING_SHIPS(布阵)、MY_TURN(我的回合)、OPPONENT_TURN(对手回合)、GAME_OVER(游戏结束)。根据当前状态,它决定读取哪个按钮、更新哪个屏幕、发送什么无线电消息。 - 通信协议:定义简单的数据包结构。例如,一个攻击数据包可能包含:
[消息类型, 发送者ID, 坐标X, 坐标Y]。消息类型可以是ATTACK、HIT、MISS、WIN等。使用RHReliableDatagram库可以简化确认重传机制,确保关键指令不丢失。
实操心得:在调试无线通信时,务必先让两块板子使用相同的频率、加密密钥和网络ID。可以先编写一个简单的“回环测试”程序:板A每秒发送一个递增的数字,板B收到后通过串口打印出来。这能最快地验证你的硬件连接和基础配置是否正确。
5. 游戏逻辑与无线通信协议实现
5.1 双屏显示与游戏状态管理
游戏机的8x8 LED矩阵在逻辑上被划分为左右两个独立的4x8区域,分别由两片HT16K33A驱动。在软件层面,我们将其抽象为两个8x8的二维数组(或位图),分别代表“我的海域”和“对手海域”。
显示映射:我们需要编写一个函数,将逻辑上的“坐标”(x, y)和“颜色”(橙、蓝、粉)映射到具体的HT16K33A芯片及其内部的段控RAM地址。例如,drawPixel(int x, int y, int color)函数内部需要判断:如果x<4,则操作左屏驱动芯片,计算对应的显存位;如果x>=4,则操作右屏驱动芯片。颜色参数则决定是点亮橙色LED、蓝色LED还是两者都点亮。
游戏状态机:这是整个游戏流畅运行的核心。一个典型的状态流转如下:
- 初始化/等待:设备上电,随机生成己方船只位置(例如,一艘占3格、两艘占2格、两艘占1格),并显示在左屏(己方海域)。右屏(对手海域)清空。状态进入
MY_TURN或WAITING(根据预设的先后手,例如Radio 2先手)。 - 我的回合(MY_TURN):一个光标(比如一个闪烁的粉色像素)显示在右屏(对手海域)。玩家使用方向键移动光标。按下“开火”键后,程序将当前光标坐标打包成
ATTACK消息,通过RFM69HCW发送给对手。同时,状态转为WAITING_FOR_RESULT。 - 等待结果/对手回合:在
WAITING_FOR_RESULT状态,设备监听无线电。收到对手回复的HIT或MISS消息后,在右屏的对应坐标更新显示(击中为橙色,未中为粉色)。然后,状态转为OPPONENT_TURN,此时右屏的WS2812B状态LED变为红色,提示玩家等待。 - 对手回合处理:在
OPPONENT_TURN状态,设备持续监听无线电。收到对手发来的ATTACK数据包后,解析坐标,检查自己的海域数组在该位置是否有船只。然后,在左屏的对应坐标更新显示(被击中为橙色,被攻击但未中为粉色),并发送HIT或MISS的回复消息给对手。之后,状态转回MY_TURN,WS2812B状态LED变为绿色或熄灭。 - 胜负判定:每次击中后,检查己方或对手的剩余船只格子数是否为零。如果为零,则发送
WIN广播消息,并在屏幕上显示胜利动画,状态进入GAME_OVER。
5.2 RFM69HCW通信的可靠性与优化
使用RadioHead库中的RH_RF69和RHReliableDatagram类可以极大地简化开发。但为了达到最佳效果,仍需进行一些配置和优化。
关键配置代码:
#include <RH_RF69.h> #include <RHReliableDatagram.h> #define RF69_FREQ 915.0 #define RF69_POWER 18 // 发射功率,单位dBm,需符合当地法规 RH_RF69 rf69(RFM69_CS_PIN, RFM69_IRQ_PIN); RHReliableDatagram manager(rf69, MY_ADDRESS); // MY_ADDRESS为1或2 void setupRadio() { if (!rf69.init()) { // 初始化失败处理 } if (!rf69.setFrequency(RF69_FREQ)) { // 设置频率失败处理 } rf69.setTxPower(RF69_POWER, true); // 第二个参数true表示使用高功率模式(PA_BOOST) // 可选的加密密钥,双方必须一致 uint8_t key[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; rf69.setEncryptionKey(key); manager.setRetries(3); // 设置发送失败后的重试次数 manager.setTimeout(200); // 设置等待确认的超时时间(毫秒) }数据包设计:为了简洁高效,我定义了一个struct作为数据包格式:
struct GamePacket { uint8_t type; // 消息类型:0x01攻击,0x02命中,0x03未中,0x04胜利 uint8_t sender; // 发送者地址(1或2) uint8_t x; // 坐标X (0-7) uint8_t y; // 坐标Y (0-7) };发送时,将其转换为uint8_t数组;接收时,再转换回来。RHReliableDatagram的sendtoWait()和recvfromAck()函数会自动处理数据包的封装、发送、确认和重传,确保了通信的可靠性。
抗干扰与功耗平衡:在密集的无线环境中,可以启用RFM69HCW的内置前向纠错(FEC)和CRC校验,这能提高抗干扰能力,但会增加数据包长度和传输时间。对于这种小数据量、低频率的交互,通常开启CRC就够了。如果设备由电池供电,可以在等待对手回合的长时间段内,让RFM69HCW进入周期唤醒的监听模式,SAMD21也进入空闲睡眠模式,可以大幅降低整体功耗。
6. 系统调试与故障排查实录
6.1 上电“三无”问题排查流程
组装完成后第一次上电是最紧张的时刻。如果设备毫无反应(屏幕不亮,USB无识别),请按以下步骤排查:
- 检查电源:首先用万用表测量3.3V稳压器(AP2112K)的输出端是否有稳定的3.3V电压。如果没有,检查USB口输入5V是否正常,检查稳压器及其周边电容(特别是输入输出端的1μF和10μF电容)是否焊接正确。
- 检查晶振:SAMD21需要外部32.768kHz晶振来运行。用示波器或逻辑分析仪探头(需高阻抗)轻触晶振引脚,看是否有正弦波波形。如果没有,检查晶振及两个负载电容(通常为12-22pF)是否焊接良好。
- 检查复位电路:确保复位引脚(RESET)通过一个10kΩ电阻上拉到3.3V,并且复位按钮按下时能将其拉低到地。可以用万用表测量复位引脚电压,正常应为高电平(3.3V左右)。
- 检查SWD连接:如果以上都正常,但芯片仍不工作,尝试通过SWD接口连接J-LINK。如果Atmel Studio能识别到芯片,说明芯片核心是好的,问题可能出在引导程序或后续代码上。如果识别不到,则可能是芯片焊接问题(桥接、虚焊)或已损坏。
6.2 常见功能故障与解决方法
| 故障现象 | 可能原因 | 排查步骤与解决方法 |
|---|---|---|
| USB无法识别 | 1. 引导加载程序未烧录或烧录错误。 2. USB数据线仅供电,不支持数据传输。 3. USB接口或ESD保护二极管损坏。 | 1. 用SWD工具重新烧录引导程序并验证。 2. 更换一条已知良好的数据线。 3. 检查USB D+和D-线路是否连通,有无短路。 |
| LED矩阵部分不亮或显示错乱 | 1. HT16K33A焊接问题或I²C地址错误。 2. LED极性焊反或损坏。 3. 限流电阻值过大或未焊接。 | 1. 用逻辑分析仪抓取I²C总线波形,确认地址和数据是否正确发送。 2. 用万用表二极管档单独测试可疑LED。 3. 检查原理图中分配给LED的驱动电流是否合理,HT16K33A每个引脚的灌电流能力有限(通常约20mA)。 |
| 按钮无反应 | 1. 上拉电阻未焊接或虚焊。 2. 按钮本身损坏或引脚氧化。 3. 代码中引脚模式未设置为 INPUT_PULLUP。 | 1. 测量按钮未按下时,对应GPIO电压是否为3.3V(高)。按下时应接近0V(低)。 2. 更换按钮。 3. 检查代码 pinMode(pin, INPUT_PULLUP)。 |
| 无线电无法通信 | 1. 天线未焊接或型号不匹配。 2. 两块板子的频率、网络ID、加密密钥不一致。 3. 发射功率设置过低或模块损坏。 4. SPI引脚配置错误。 | 1. 确保天线牢固焊接,并使用对应频段的¼波长天线。 2. 在代码中打印并确认双方的射频配置参数完全一致。 3. 在法规允许范围内,适当提高 setTxPower值。用频谱仪或另一台接收机检查是否有信号发出。4. 用逻辑分析仪检查SPI的SCK, MOSI, NSS信号是否正常。 |
| 游戏逻辑混乱 | 1. 状态机逻辑错误,导致状态切换异常。 2. 随机数种子相同,导致双方船只布局每次都一样。 3. 数据包解析错误。 | 1. 在关键状态切换点,通过串口打印当前状态和触发事件,进行调试。 2. 使用SAMD21的硬件随机数发生器(RNG)或结合未连接的ADC引脚噪声来生成更随机的种子。 3. 在收发数据包时,同时将原始数据通过串口打印出来,进行比对。 |
6.3 性能优化与体验提升技巧
- 显示刷新优化:HT16K33A通过I²C更新整个显存。频繁刷新全屏会占用大量总线时间。优化方法是只更新发生变化的那部分显存。可以维护一个“脏矩阵”标记,只有当某个8x8区域的内容需要改变时,才发送该区域对应的I²C数据。
- 降低无线电干扰:RFM69HCW在发射时会在电源上产生较大的电流尖峰。务必在模块的VCC和GND引脚附近放置一个10μF和一个0.1μF的电容进行去耦,并确保电源走线足够宽。这能有效防止因电压跌落导致的微控制器复位。
- 增加视觉反馈:除了LED矩阵,板载的WS2812B RGB LED是一个强大的状态指示器。可以用它来丰富反馈:绿色闪烁表示“轮到你了”,红色常亮表示“对手回合”,彩虹流光表示“胜利”,蓝色呼吸表示“设备待机”等。
- 引入声音效果:利用SAMD21内置的10位DAC,连接一个简单的放大电路和微型扬声器,可以为击中、未中、胜利等事件添加简单的音效,极大提升游戏沉浸感。DAC输出模拟电压,通过PWM或R2R电阻网络也能实现,但音质会差一些。
完成所有这些步骤后,你将得到两台功能完整、对战体验流畅的无线战舰游戏机。从一块空白的芯片开始,到最终能和朋友隔空对战的成品,这个过程充满挑战,但获得的嵌入式系统全栈开发经验是无价的。这个项目的框架具有很强的扩展性,你可以很容易地将显示部分换成OLED,增加更多的游戏模式,甚至将无线电协议改为LoRa以实现超远距离对战。最重要的是,你亲手打造了一个真正“活”起来的嵌入式设备,这种成就感,是任何现成开发板都无法给予的。
