# MCCompass **Repository Path**: LOStacNet/mccompass ## Basic Information - **Project Name**: MCCompass - **Description**: 基于STM32G030的我的世界指南针,包含指北功能和定位指向功能,使用立创面板+EDA设计。受两个开源项目启发,基于STM单片机重构,便于学习和再开发,使用模块设计,便于制作。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2025-08-17 - **Last Updated**: 2025-09-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 开源我的世界指南针复刻 本项目对网上开源的我的世界指南针进行复刻,并添加一些自己的修改和功能。本项目尽量采用模块,降低制作难度。 作者:LO_StacNet ## 声明 本项目游戏素材具有一定版权风险,故项目开源需谨慎。 本文图片来自淘宝店铺和立创商城,有侵权风险。 ## 介绍 本设计参考网络两位大佬的开源工程,并进行一些适应性改造,包括: 1. 灯珠替换为3528封装便于焊接 2. 电源管理、磁场传感、GPS使用模块,减少焊接需求 3. 更换主控为STM32G030,开发更便捷 4. 软件框架分层,移植和添加新功能更简单 5. 自动外设检测,没有GPS可以使用指北功能,没有磁场传感器还有地狱模式(指针乱转) 该指南针默认固件有以下功能: 1. 上电进入指北针模式,若相关外设缺失则进入地狱 2. 指北针模式下,指针指向北方,单击按钮切换到地狱,双击开始校准,长按切换到Home模式 3. Home模式下,指针指向家位置,单击设置当前位置为家,双击重置家位置为初始值,长按切换到指北针 4. 地狱模式下,单击切换回指北针,双击重置配置文件(家位置和校准值) 5. 按下按钮上电,重置配置文件(家位置和校准值) 6. 模式切换和校准时,显示中间红点的十字 7. Home模式第一次定位成功前,显示中间蓝点的十字 注意: * 若有误差请进行校准,校准时屏幕长显示**中心红点十字**,注意让指南针**每个方向轴旋转**,让其每个轴都采到最大最小值 * Home模式重置后初始地址指向**时间广场** * 切换到Home模式后显示**中心蓝点十字**,表示GPS正在定位,请移动到**开放场地**完成定位 ## 项目结构 * 3D——存放外壳模型文件 * document——存放项目文档 * firmware——存放单片机固件 * hardware——存放PCB设计文件 * resource——存放指南针素材 * tools——存放素材处理取模脚本 * video——存放视频企划 ## 元件选型 本节介绍各个元件的选择。 ### 电源管理 **锂电池管理和5V供电** 电源管理直接使用淘宝成品充放电一体化模块,简化设计。 ![image-20250607161623383](README.assets/image-20250607161623383.png) **3V3供电** 3V3供电使用经典AMS1117-3.3V芯片,封装SOT-89,封装参考立创商城C5120796。 ### 传感器 **磁场传感器** 磁场传感器定位方向,是实现指北功能的核心。 本设计使用GY-271 QMC5883L模块,便于焊接制作。由于L已经停产,市面上出现了5833P模块,比L便宜一点,但是寄存器和IIC地址都不一样,三轴方向不一样,读取要求也不一样,非常的坑。 更新:固件已支持QMC5883P。 ![image-20250607171150736](README.assets/image-20250607171150736.png) **GPS** GPS模块获取位置信息,是实现定位指向功能的核心。 本项目使用ATGM336H模块。 ![image-20250607171748386](README.assets/image-20250607171748386.png) ### 灯珠 常见的灯珠封装大小有SMD2x2mm(2x2mm),SMD3538(3.5x2.8mm),SMD3535(3.5x3.5mm),SMD5050(5x5mm),为控制体积、保证显示效果以及便于焊接,选择中等大小SMD3538(3.5x2.8mm)封装的灯珠,~~同时选择自带雾面的版本,不需要加柔光纸。~~(必须加柔光纸) 该型号灯珠可在淘宝搜索WS2812B-3528RGB找到,如果在立创商城上可以参考XL-3528RGBW-WS2812B,商品编号C2890364(不保证能正常运行)。 ![{4CC41D9F-7892-4EF1-B116-AA371865F7A8}](README.assets/%7B4CC41D9F-7892-4EF1-B116-AA371865F7A8%7D.png) ### 主控 主控要求支持IIC,UART,单线数据输出,并最好支持单线数据DMA输出和低功耗。 选择STM32G030F6P6,该芯片主频64MHz,有一个ADC,2个IIC,3个SPI,两个USART以及DMA,符合本设计需求。正好淘宝也有该芯片的开发板,有硬件参考,调试也方便。 ### 存储器 为保存配置信息,使用EEPROM存储数据。(虽然数据可以存储在flash中,但考虑到flash的寿命与其他限制,这里还是采用EEPROM。~~虽然Flash寿命应该用不完,但这里就是懒~~)。 这里选择AT24C02芯片即可。 ### 接口和开关 **电源开关** 开关使用SS-12D00 立式柄长5MM,封装参考立创商城C22355741。 ![image-20250609204032873](README.assets/image-20250609204032873.png) **按钮** 按钮使用卧式弯插轻触开关,封装参考立创商城C2845277。 ![image-20250607154716759](README.assets/image-20250607154716759.png) **调试按钮** 主控还需要复位等调试按钮,使用3\*4\*2mm 立贴 轻触开关,封装参考立创商城C707339。 ![image-20250609205742457](README.assets/image-20250609205742457.png) **固定螺丝** 使用M2x6,头径4mm的螺丝,自攻和平螺丝均可。 ![image-20250709015117451](README.assets/image-20250709015117451.png) ![image-20250709015154795](README.assets/image-20250709015154795.png) ## 硬件设计 本节介绍各个部分电路的设计。 ### 像素设计 Minecraft中的指南针大小为14x12像素,其中共有40个像素为显示区域。 根据灯珠大小,设定一个像素显示区域为6x6mm,设定光栅厚度为1mm,那么可视为一个像素7mm,最终大小为98x84mm。 在文档层绘制像素框,设定矩形线宽为1mm,宽和高分别为7mm,则显示像素区域如图所示: ![image-20250608200103862](README.assets/image-20250608200103862.png) 整个范围如图所示: ![image-20250608200828449](README.assets/image-20250608200828449.png) ### 灯珠电路 WS2812灯珠数据信号的控制逻辑如下: 1. 每个灯珠有24bits颜色数据 2. 每次传输假设有10个灯珠数据,第一个灯珠会保留第一个数据,并将剩下的数据从DOUT发送 3. 间隔50us以上则判定为两次传输 基于上述特性,如果从IO依次发送一个数组,想要数组索引与灯珠顺序相同,设置灯珠顺序为从左到右从上到下排列,灯珠电路如下: ![image-20250609211206307](README.assets/image-20250609211206307.png) 每行设置一个100nF电容去噪,输入输出预留电阻位置用于调试。 ### 电源管理 锂电池充放电和5V电源直接使用淘宝电源模块处理。该模块带一个typec接口,可以自动检测5V输入,当有输入时为充电模式,给锂电池进行充电,带保护;当没有5V输入时,切换到放电模式,锂电池放电。 ![image-20250709013823569](README.assets/image-20250709013823569.png) 同时,再使用AMS1117输出3.3V电源即可。 ### EEPROM EEPROM使用24C02,用于存储校准信息和家位置坐标,连接到STM32的硬件IIC接口,电路图如下: ![image-20250709014012642](README.assets/image-20250709014012642.png) ### 单片机 单片机使用STM32G030F6P6,该单片机外部电路较为简单,只需要提供RST信号和下载接口即可。本设计中除了外设接口,还使用了USART2作为Debug输出,同时添加了LED用于显示程序状态。 ![image-20250709014254174](README.assets/image-20250709014254174.png) ### 3D外壳 设计注意事项: 1. 电池型号为603450,厚6mm 34*50mm,注意预留空间 2. GPS天线需要最终朝上,注意设置固定位置 3. 固定孔统一使用M2*6 头厚0.8mm 头直径4mm自攻螺丝固定 4. 模块与板子并不在同一平面,注意预留位置 5. 光栅尽量窄,保证像素间距不要太大 6. 拨动开关位置、TypeC口、按键位置需要预留 7. 外壳厚度1.8mm 外壳分为前壳和后壳,前壳同时包含光栅。前壳、PCB、后壳通过M2螺丝孔进行连接。本设计使用的是M2x6,头径4mm规格的螺丝,自攻螺丝和普通螺丝均可。 ### 面板设计 面板使用立创EDA 面板设计。首先根据像素大小,在板框层绘制外框,然后在打印层绘制出像素网格,然后根据对MC指南针素材的取色,设置每个像素的颜色。将显示区域在透明控制层标出。 ![image-20250709015625727](README.assets/image-20250709015625727.png) ## 软件设计 本节介绍软件设计。 ### 整体框图 ![image-20250709021554786](README.assets/image-20250709021554786.png) 程序设计了一个简单的架构,首先是**BSP层封装了所有传感器的初始化和访问函数**,程序所有其他程序对硬件的访问都通过BSP提供的API进行。这样,当芯片变动或者传感器改变时,能最大限度的减少对主程序的改动。(这在后面遇到QMC5883L和P不同的坑时十分有用) 其次,由于该指南针有两种不同的模式,这里通过函数指针的形式运行不同的模式,避免出现添加模式需要修改主代码的情况。每个模式有一个Init函数和一个Run函数: * **Init函数**:用于判断能否进入该模式(该模式需要的外设是否存在,如Home模式需要GPS外设和QMC外设),并进行初始化,只在切换模式时运行一次 * **Run函数**:是模式的主while循环函数,该函数用于执行程序的主逻辑,在while中运行。 所有模式的函数通过两个数组管理,函数在数组的索引就是模式号,程序在主循环通过当前的模式号选择对应的Run函数运行。 框架的详细信息见代码,这里给出固件中的三种模式的流程图。 * 模式0-地狱模式-无条件进入,用于所有传感器没有的情况 ![image-20250709023551875](README.assets/image-20250709023551875.png) * 模式1-指北针模式-上电后默认模式,需要QMC外设 ![image-20250709024655483](README.assets/image-20250709024655483.png) * 模式2-Point Home模式-指向设定点,需要QMC+GPS ![image-20250709025651470](README.assets/image-20250709025651470.png) ### BSP层定义 BSP层用于分离底层数据IO与程序主要应用逻辑。在本项目中,需要实现以下几个BSP层的API。 #### LED Panel API 该API用于显示像素。 ```c uint8_t LED_PanelInit(void); ``` **函数名:**LED_PanelInit **描述:**用于初始化SPI、DMA等相关外设,包括硬件初始化(使用CubeMX生成的则留空)以及设置初始界面。 **参数:**无 **返回值:**0——正常;1——初始化失败。 ```c uint8_t LED_PanelDisp(uint16_t pixel[]); ``` **函数名:**LED_PanelDisp **描述:**将pixel中的数据刷新到LED上。如果上次刷新未完成,返回错误码。该函数构造要发送的数据,并通过SPI+DMA进行无阻塞刷新。 **参数:** * `pixel`——要刷新的像素信息,固定长40个,RGB565格式 **返回值:**0——正常;1——设备忙(上次刷新未完成);2——刷新失败。 #### GPS API 该API用于获取解析GPS数据信息。 ```c struct GPS_Data{ float dlat; //维度-北纬正南纬负 float dlong; //经度-东经正西经负 float dalt; //高度-可不采 }; ``` GPS_Data用于传递位置信息。 ```c uint8_t GPS_Init(void); ``` **函数名:**GPS_Init **描述:**用于初始化GPS外设,包括硬件初始化(使用CubeMX生成的则留空)以及串口配置GPS输出模式。同时该函数还要检测是否存在GPS设备。 **参数:**无 **返回值:**0——正常;1——初始化失败。 ```c uint8_t GPS_ReadPos(struct GPS_Data* gps_data); ``` **函数名:**GPS_ReadPos **描述:**读取GPS定位数据,如果不存在GPS设备或者定位未成功,返回随机值。 **参数:**`gps_data`——读取到的位置信息 **返回值:**0——正常;1——定位无效;2——读取失败。 #### QMC API 该API用于读取磁力传感器数据。 ```c struct QMC_Data{ int16_t x; int16_t y; int16_t z; }; ``` 存放三轴磁场数据的结构体。 ```c uint8_t QMC_Init(void); ``` **函数名:**QMC_Init **描述:**用于初始化磁场传感器,包括IIC硬件初始化(使用CubeMX生成的则留空)以及配置传感器。同时该函数还要检测是否存在磁场传感器。 **返回值:**0——正常;1——没有QMC设备;2——初始化失败。 ```c uint8_t QMC_ReadMagneticField(struct QMC_Data* qmc_data); ``` **函数名:**QMC_ReadMagneticField **描述:**读取磁场传感器数据。 **参数:**`qmc_data`——读取到的磁场信息 **返回值:**0——正常;1——读取失败。 ```c uint8_t QMC_GetStatus(void); ``` **函数名:**QMC_GetStatus **描述:**返回QMC数据是否准备完成,可以直接读取引脚或者寄存器获取信息。鉴于QMC5883P的特性,不建议轮询读取,两次调用需要间隔一段时间。 **参数:**无 **返回值:**0——数据准备完成;1——数据未准备完成。 #### EEPROM API 该API用于操作EEPROM中的数据。 ```c uint8_t AT24_Init(void); ``` **函数名:**AT24_Init **描述:**用于初始化EEPROM,包括IIC硬件初始化(使用CubeMX生成的则留空)以及配置。同时该函数还要检测是否存在EEPROM。 **参数:**无 **返回值:**0——正常;1——没有ROM设备;2——初始化失败。 ```c uint8_t AT24_MemRead(uint8_t addr,uint8_t data[],uint8_t length); ``` **函数名:**AT24_MemRead **描述:**从EEPROM中指定地址读取指定长度数据。当数据超长时,不读取,返回错误。 **参数:** * `addr`——读取起始地址 * `data`——存放读取的数据 * `length`——要读取的长度 **返回值:**0——正常;1——读取失败。 ```c uint8_t AT24_MemWrite(uint8_t addr,uint8_t data[],uint8_t length); ``` **函数名:**AT24_MemWrite **描述:**向EEPROM中指定地址写入指定长度数据。当数据超长时,不写入,返回错误。 **参数:** * `addr`——起始地址 * `data`——存放要写入的数据 * `length`——要写入的长度 **返回值:**0——正常;1——写入失败。 #### CRC API 该API用于校验数据是否正确。 ```c uint8_t CRC_Init(void); ``` **函数名:**CRC_Init **描述:**用于初始化CRC计算组件,(使用CubeMX生成的则留空)。 **参数:**无 **返回值:**0——正常;1——初始化失败。 ```c uint8_t CRC_Calculate(const uint8_t data[],uint32_t length,uint32_t* result); ``` **函数名:**CRC_Calculate **描述:**计算一串数据的CRC值。 **参数:** * `data`——存放要计算的数据 * `length`——要计算数据的长度 * `result`——存放计算结果 **返回值:**0——正常;1——计算失败。 #### BAT API 该API用于获取电池电量数据。 ```c uint8_t BAT_Init(void); ``` **函数名:**BAT_Init **描述:**用于初始化读取电池电压的外设(使用CubeMX生成的则留空)。 **参数:**无 **返回值:**0——正常;1——初始化失败。 ```c uint8_t BAT_ReadStatus(uint8_t* status); ``` **函数名:**BAT_ReadStatus **描述:**读取电池电量。 **参数:** * `status`——电池电量(百分制) **返回值:**0——正常;1——读取失败。 #### KEY API 该API用于读取用户按键输入。 ```c typedef enum{ KEY_NONE=0, //无事件 KEY_SINGLE_CLICK, //按键单击 KEY_DOUBLE_CLICK, //按键双击 KEY_LONG_PRESS, //按键长按 }KEY_STATUS; ``` 表示按钮的三种按下状态,分别为单击,双击,长按。 ```c uint8_t KEY_Init(void); ``` **函数名:**KEY_Init **描述:**用于初始化相关的外设(使用CubeMX生成的则留空)以及必要的初始化步骤。 **参数:**无 **返回值:**0——正常;1——初始化失败。 ```c uint8_t KEY_ReadStatus(KEY_STATUS* key_status); ``` **函数名:**KEY_ReadStatus **描述:**无阻塞的查询按键最新的状态。按键状态具体的判断逻辑在中断中实现。 **参数:** * `key_status`——按键的状态 **返回值:**0——正常;1——读取失败。 ```c uint8_t KEY_ReadValue(void); ``` **函数名:**KEY_ReadValue **描述:**直接读取按键当前的值。 **参数:**无 **返回值:**1——按键松开;0——按键按下。 ### 灯珠驱动 #### 灯珠参数 灯珠信号定义如下所示: ![image-20250612151513481](README.assets/image-20250612151513481.png) 以典型值为例,在一个2us的时间内,高电平+电平时间为0.28+1.72us表示0,为0.9+1.1us表示1,持续150us低电平重制。两帧传输之间需要插入RESET让数据生效。 2us分为8份,1bit的时间为0.25us,那么可以得到: * 发送0——1000 0000 -> 0x80 * 发送1——1110 0000 -> 0xE0 * SPI频率——1/2*8-> 4MHz #### 外设配置 基于上面的参数对,SPI1进行配置,设置为Tranmit Only Master模式,设置Data Size为**8bits**,设置Baud Rate为**4MHz**(通过Prescaler设置),同时将CPOL设置为0,空闲时低电平。 ![image-20250612154043582](README.assets/image-20250612154043582.png) 此时,通过SPI**发送0x80则为发送0,发送0xE0则为发送1。** 同时,需要对DMA进行配置,配置如下所示: ![image-20250612154631254](README.assets/image-20250612154631254.png) 设置内存到外设,内存地址递增,搬运Byte数据,Normal单次搬运模式。 为了检查DMA是否传输完毕,还需要开启中断来设置完成标识。DMA传输完成中断会强制开启,最后会调用SPI的中断回调函数,因此直接实现SPI的回调函数即可,**不要开启SPI中断**。 #### 驱动代码 本驱动使用RGB565格式来保存指南针帧数据,在刷新时先将RGB565转换为RGB888,然后再将每一bit转换为对应的byte,再通过DMA搬到SPI。因此,在驱动中需要准备一个大小为40\*3\*24的数组用于DMA传输。 最核心函数如下: ```c static void rgb2spi(uint8_t* buffer, uint16_t color, uint8_t brightness) { uint8_t r,g,b; RGB565ToGRB888(color, &g, &r, &b, brightness); // 转换为GRB888 for (int bit = 0; bit < 8; bit++) { // 绿色高位优先 buffer[bit] = (g & (0x80 >> bit)) ? WS_1 : WS_0; // 红色高位优先 buffer[bit + 8] = (r & (0x80 >> bit)) ? WS_1 : WS_0; // 蓝色高位优先 buffer[bit + 16] = (b & (0x80 >> bit)) ? WS_1 : WS_0; } } ``` 该函数将RGB565转换为RGB888,然后将24个bit的颜色数据分别转换为SPI需要发送的字节。 通过多次调用该函数转换字节,填充整个缓冲区,然后开启DMA传输即可: ```c uint8_t WS_Update(const uint16_t* pixel, uint16_t length, uint8_t brightness) { if(length > WS_LED_NUM) return 2; for(uint16_t i=0;i 360) // 超限,从地狱获取坐标 return GetDirFromNether(); if (angle > 353) { angle = 0; // 特别处理的情况 } uint8_t interval_index = (uint8_t)((angle + 180.0/29) / (360.0/29)); return interval_index; } ``` 该代码中,0度位于一个区间段的中间位置。 #### 指南指北功能实现 在本项目中,传感器的X-Y平面和面板平行,因此只需要根据三轴向量在XY的投影即可判断面板平面内的方向,具体就是根据XY向量与方向轴的夹角(atan值)来判断指针需要旋转的角度。向量的方向就是北方。 根据本项目传感器的位置,X轴正方向指向正下方,Y轴正方向指向右侧,相对于常规坐标系顺时针旋转了90度,同时以顺时针为正角度方向。我们需要求从X轴到向量顺时针需要经过多少度。 ```c //根据XY计算角度-从X轴正方向顺时针0-360 float QmcCalAngle(struct QMC_Data* qmc_data) { float deg = atan2f((float)qmc_data->y,(float)qmc_data->x)*180.0f/3.14159f; // 计算角度输出-pi~pi deg = -deg; // 逆时针转顺时针 if(deg < 0) deg += 360; // 转换到0~360 return deg; } ``` 使用`atan2()`会自动判断相位,从而输出-pi~pi范围的值(以X轴为中心),通过符号取反和归一化操作,可以将输出变换到0~360顺时针域。该函数输出就是北方在X轴顺时针多少度位置。 #### 方位角的计算 方位角:是从某点的指北经线起,依顺时针方向到目标方向线之间的地表夹角,如下图θ角所示。 ![image-20250618201820295](README.assets/image-20250618201820295.png) 通过方位角和北方方向,可以得到另外一处地点的方位位置,方位角的计算公式如下: $$ \theta = \text{atan2}\left(\frac{\sin(\lambda_2 - \lambda_1) \cdot \cos(\varphi_2)}{\cos(\varphi_1) \cdot \sin(\varphi_2) - \sin(\varphi_1) \cdot \cos(\varphi_2) \cdot \cos(\lambda_2 - \lambda_1)}\right) $$ 该公式的输出结果角度,正为北方顺时针方向,负为北方逆时针方向,对负方向,通过归一化转换为0-360范围,计算代码如下: ```c // 计算位置1到位置2的方位角-0~360 float GpsCalBearing(struct GPS_Data* gps_1, struct GPS_Data* gps_2) { // 方位角:是从某点的指北经线起,依顺时针方向到目标方向线之间的地表夹角 // 这里使用球模型进行计算,但实际上不需要这么精确,应该平面模型即可 if (gps_1->dlat == gps_2->dlat && gps_1->dlong == gps_2->dlong) { return 0; // 相同点,返回0 } float rlat1 = gps_1->dlat * 3.14159f/180.0f; float rlong1 = gps_1->dlong * 3.14159f/180.0f; float rlat2 = gps_2->dlat * 3.14159f/180.0f; float rlong2 = gps_2->dlong * 3.14159f/180.0f; float numerator = sinf(rlong2 - rlong1) * cosf(rlat2); float denominator = cosf(rlat1) * sinf(rlat2) - sinf(rlat1) * cosf(rlat2) * cosf(rlong2 - rlong1); float x = atan2f(numerator, denominator); x = x * 180.0f/3.14159f; if(x < 0) x=x+360.0f; return x; } ``` #### 配置存取功能 配置信息使用一个结构体维护,保存时按原始字节直接写入EEPROM,并加上固定2字节帧头和CRC校验码帧尾。 读取时验证CRC来判断是否保存过配置或者配置数据是否正确。 注意不能通过将结构体指针直接指向读取缓冲区的方式提取信息,因为在读取多字节时会触发Cortex内核内存对齐问题,直接进hardfault。可以使用memcpy函数防止直接的内存访问。 具体实现见源码。 ## Tools 本项目使用tools如下(均使用DeepSeek生成~~D学长太强了~~): * `extract_gif.py` 将wiki上的指南针动图拆分为png * `extract_pic.py`从上面拆分的png中提取特定区域的像素并取模 ## 参考 项目1-[简易我的世界指南针](https://oshwhub.com/realtix/jian-yi-mc-zhi-nan-zhen) 项目2-[MCompass一个现实世界中的Minecraft罗盘](https://oshwhub.com/chaosgoo/wcompass) [利用GPS坐标计算方位角](https://johnnyqian.net/blog/gps-locator.html) [QMC5883P坐标系方向](https://blog.csdn.net/u014436243/article/details/122358056)