news 2026/10/20 3:09:20

C++小游戏实战项目:CMake构建+SDL2跨平台开发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++小游戏实战项目:CMake构建+SDL2跨平台开发

简介:这是一份面向C++初学者与编程爱好者的免费小游戏实践资源包,聚焦语法巩固、逻辑训练与项目拆解,帮助学习者通过可运行的完整案例掌握类与对象、循环控制、随机数生成、状态管理等核心概念。资源共117个文件,主体为115个独立.cpp源码文件(如《龙的传人》《狼人杀》《国粹麻将》《技能大乱斗》等),辅以1个可直接运行的exe程序和1个编译输出out文件,便于对比理解编译流程与执行结果;整体压缩包仅1.13MB,轻量易下载。已有10617人学习下载,热度高、实操性强。每个游戏均结构清晰、注释充分,涵盖迷宫寻路、回合对战、规则判定等典型场景,既可逐个调试加深理解,也支持二次开发——例如扩展图形界面、添加存档功能或重构为多线程版本,是夯实C++基础、培养工程思维的优质入门素材。

1. 这不是玩具代码合集:一个能编译、能调试、能改出新玩法的 C++ 小游戏实战包

上周帮某高校实验室带毕设的学生调代码,他甩来一个“C++小游戏集合”压缩包,说“老师让跑通再加个存档功能”。结果打开发现——全是.cpp文件,没CMakeLists.txt,main()分散在七八个文件里,#include "game.h"却找不到game.h在哪。最后花了三小时才理清依赖链。而你手头这份「一个免费的c++小游戏集合」,恰恰反其道而行:它用现代 CMake 统一管理 5 个完整可运行游戏(贪吃蛇、俄罗斯方块、推箱子、扫雷、井字棋),每个游戏独立构建、独立运行,源码里带清晰注释、关键算法有 inline 解释、甚至预留了扩展接口。它不教语法基础,但专治“写完编译不过”“改完逻辑崩了”“想加个音效却卡死在 SDL2 链接阶段”这类真实翻车现场。适合刚学完类和 STL 的 C++ 新手练手,也适合嵌入式/图形方向的开发者快速验证算法或移植到裸机平台。


2. 构建与运行:从解压到双击可执行文件的完整链路

2.1 目录结构即设计意图:为什么这样组织?

项目解压后目录如下(已剔除.git和构建产物):

games-collection/ ├── CMakeLists.txt # 顶层:定义 CMake 最低版本、项目名、启用C++17 ├── build/ # 构建目录(建议始终外置,避免污染源码) ├── assets/ # 所有游戏共用资源:字体、音效WAV、图标ICO ├── common/ # 公共模块:输入抽象层、定时器封装、日志工具 │ ├── input_manager.hpp/cpp # 封装 SDL2 键盘/鼠标事件为统一事件队列 │ ├── timer.hpp/cpp # 基于 std::chrono 的高精度帧计时器 │ └── logger.hpp # 简单宏封装,DEBUG 模式输出带文件行号的日志 ├── games/ # 每个子目录 = 一个完整游戏工程 │ ├── snake/ # 贪吃蛇:核心是双向链表+方向向量控制 │ │ ├── CMakeLists.txt # 子项目:声明依赖 common,链接 SDL2 │ │ ├── main.cpp # 游戏入口:初始化→主循环→清理 │ │ ├── game_state.hpp # 状态机:MENU / PLAYING / PAUSED / GAME_OVER │ │ └── renderer.hpp # 基于 SDL_Renderer 的绘制封装(含网格对齐逻辑) │ ├── tetris/ # 俄罗斯方块:核心是 4x4 旋转矩阵 + 消行检测 │ │ ├── CMakeLists.txt │ │ ├── main.cpp │ │ ├── board.hpp # 二维数组表示游戏区,含碰撞检测方法 │ │ └── piece.hpp # 方块类型枚举 + 旋转数据表(硬编码 4 种旋转) │ └── ... # 其他游戏同构 └── README.md # 写明编译命令、依赖库版本、各游戏快捷键

提示:这种“顶层 CMake + 子项目 CMake”结构,是大型 C++ 工程的标准做法。它让每个游戏像微服务一样独立编译,又共享公共工具——改common/logger.hpp后所有游戏自动获得新日志能力,无需复制粘贴。

2.2 三步编译:Windows/macOS/Linux 通用命令

前提:已安装 CMake 3.16+ 和对应平台的编译器(Windows: MSVC 或 MinGW;macOS: Xcode Command Line Tools;Linux: g++/clang++)

# 1. 进入项目根目录,创建并进入构建目录(关键!禁止在源码目录内 cmake) cd games-collection mkdir build && cd build # 2. 配置项目(自动探测编译器、生成 Makefile/Ninja/Visual Studio Solution) cmake .. -DCMAKE_BUILD_TYPE=Release # 3. 编译全部游戏(-j 参数利用多核,Linux/macOS 用 -j$(nproc),Windows 用 -j%NUMBER_OF_PROCESSORS%) cmake --build . -j

编译成功后,build/games/下会生成 5 个可执行文件(如snake.exe,tetris)。直接运行即可:

# Linux/macOS ./games/snake # Windows(PowerShell) .\games\snake.exe

2.3 运行时行为与调试入口

每个游戏启动后默认进入主菜单(ASCII 艺术字 + 选项高亮),支持键盘导航:

  • ↑/↓:选择菜单项
  • Enter:确认
  • Esc:返回上一级或退出

调试友好设计:

  • 所有游戏在DEBUG模式下(编译时用-DCMAKE_BUILD_TYPE=Debug)会输出详细日志到控制台,例如:
    DEBUG [snake/main.cpp:87] Snake head moved to (12, 5), new direction: RIGHT
  • 主循环中插入#ifdef DEBUG ... #endif区域,方便临时打印变量状态
  • common/timer.hpp提供Timer::frameTimeMs()方法,可实时监控帧率(按F1切换显示/隐藏 FPS)

3. 核心模块拆解:贪吃蛇的双向链表实现与俄罗斯方块的旋转矩阵

3.1 贪吃蛇:用 std::list 实现蛇身,避免数组越界玄学

games/snake/snake.hpp中定义蛇身结构:

#include <list> #include <utility> // std::pair struct Position { int x, y; Position(int x = 0, int y = 0) : x(x), y(y) {} bool operator==(const Position& other) const { return x == other.x && y == other.y; } }; class Snake { private: std::list<Position> body; // 双向链表:头部=蛇头,尾部=蛇尾 Position direction; // 当前移动方向向量:{0,-1}=UP, {1,0}=RIGHT 等 static constexpr int GRID_SIZE = 20; // 游戏区划分为 20x20 网格 public: void move() { // 1. 获取当前蛇头位置 auto head = body.front(); // 2. 计算新蛇头位置(向量加法) Position newHead{head.x + direction.x, head.y + direction.y}; // 3. 将新头插入链表前端 body.push_front(newHead); // 4. 若未吃到食物,删除蛇尾(保持长度不变) if (!isFoodEaten(newHead)) { body.pop_back(); // 关键!pop_back() 安全,无需检查空链表(游戏开始时body.size()>=1) } } // 检测是否撞墙或自撞(遍历链表,跳过蛇头自身) bool isColliding() const { auto head = body.front(); // 撞墙:超出游戏区边界(假设游戏区宽400px,高300px,GRID_SIZE=20 → 20x15格) if (head.x < 0 || head.x >= 20 || head.y < 0 || head.y >= 15) return true; // 自撞:新蛇头位置是否与其他身体节点重合 auto it = body.begin(); ++it; // 跳过蛇头 for (; it != body.end(); ++it) { if (*it == head) return true; } return false; } };

参数说明与设计权衡:

  • 使用std::list而非std::vector:push_front()和pop_back()均为 O(1),且无需预分配内存;vector的insert(begin(), ...)是 O(n)。
  • GRID_SIZE硬编码为 20:简化坐标映射逻辑(屏幕坐标 ÷ GRID_SIZE = 网格坐标),实际项目中应抽为配置项。
  • isColliding()中++it跳过蛇头:避免误判“蛇头撞自己”,这是新手最常写的 bug。

3.2 俄罗斯方块:用旋转矩阵查表法,绕过三角函数计算

games/tetris/piece.hpp定义方块数据结构:

#include <array> #include <vector> enum class TetrominoType { I, O, T, S, Z, J, L }; // 每种方块的 4 种旋转形态(0°, 90°, 180°, 270°),用相对坐标表示 // 以方块中心为原点,坐标范围 [-1,1],例如 'O' 方块永远是 2x2 正方形,无旋转变化 static const std::array<std::array<std::array<int, 2>, 4>, 7> ROTATION_DATA = {{ // I型:长条,4种形态本质是同一组点的旋转 {{{-1,0},{0,0},{1,0},{2,0}}}, // 0° {{{0,-1},{0,0},{0,1},{0,2}}}, // 90° {{{-1,0},{0,0},{1,0},{2,0}}}, // 180°(同0°,因对称) {{{0,-1},{0,0},{0,1},{0,2}}}, // 270°(同90°) // O型:固定2x2,所有旋转形态相同 {{{-1,-1},{-1,0},{0,-1},{0,0}}, {{-1,-1},{-1,0},{0,-1},{0,0}}, {{-1,-1},{-1,0},{0,-1},{0,0}}, {{-1,-1},{-1,0},{0,-1},{0,0}}}, // T型:中心点+上下左右,旋转时中心点固定,其他点绕中心转 {{{-1,0},{0,0},{1,0},{0,-1}}}, // 0°:T字顶朝上 {{{0,-1},{0,0},{0,1},{-1,0}}}, // 90°:T字顶朝左 {{{-1,0},{0,0},{1,0},{0,1}}}, // 180°:T字顶朝下 {{{0,-1},{0,0},{0,1},{1,0}}}, // 270°:T字顶朝右 // ... 其他类型省略,结构同上 }}; class Piece { private: TetrominoType type; int rotation; // 0~3,对应 ROTATION_DATA[type][rotation] Position position; // 当前方块在游戏板上的左上角坐标(以格为单位) public: // 获取当前旋转形态的所有方块坐标(绝对坐标 = position + 相对坐标) std::vector<Position> getAbsolutePositions() const { std::vector<Position> result; const auto& shape = ROTATION_DATA[static_cast<int>(type)][rotation]; for (const auto& offset : shape) { result.emplace_back(position.x + offset[0], position.y + offset[1]); } return result; } // 顺时针旋转:rotation = (rotation + 1) % 4 void rotateClockwise() { rotation = (rotation + 1) % 4; } };

关键实现细节:

  • 查表法替代数学计算:不使用sin/cos或矩阵乘法,直接硬编码所有旋转形态。性能零损耗,且完全规避浮点误差导致的坐标偏移问题。
  • 中心点隐式处理:ROTATION_DATA中所有坐标均以方块“视觉中心”为原点,getAbsolutePositions()中叠加position即完成平移,无需额外计算中心偏移。
  • O型特殊处理:其ROTATION_DATA四个形态完全相同,避免无效旋转操作。

4. 常见问题排查:编译失败、黑屏、逻辑错位的血泪经验

4.1 现象:CMake 配置时报错 “Could NOT find SDL2 (missing: SDL2_LIBRARY SDL2_INCLUDE_DIR)”

原因:CMake 无法定位 SDL2 库文件。常见于:

  • Windows 未安装 SDL2 开发包(仅下载了 runtime DLL)
  • macOS 用 Homebrew 安装但未brew link sdl2
  • Linux 未安装libsdl2-dev(Ubuntu/Debian)或sdl2-devel(CentOS/RHEL)

解决:

  • Windows:去 libsdl.org 下载SDL2-devel-2.28.5-VC.zip,解压后将SDL2-2.28.5\lib\x64路径传给 CMake:
    cmake .. -DSDL2_ROOT_DIR="D:/path/to/SDL2-2.28.5" -DCMAKE_BUILD_TYPE=Release
  • macOS:确保brew install sdl2后执行brew link sdl2,或手动指定:
    cmake .. -DSDL2_ROOT_DIR="/opt/homebrew/opt/sdl2" ...
  • Linux:sudo apt install libsdl2-dev(Ubuntu),然后cmake ..即可自动找到。

4.2 现象:程序编译通过,但运行后窗口一闪而逝或黑屏

原因:SDL2 初始化失败后未正确处理错误,或主循环未启动。典型代码缺陷:

// 错误示范:忽略 SDL_Init 返回值 SDL_Init(SDL_INIT_VIDEO); // 若失败,后续 SDL_CreateWindow 必然崩溃 SDL_Window* window = SDL_CreateWindow(...); // 此处可能为 nullptr

解决:

  • 检查games/*/main.cpp中 SDL 初始化部分,必须添加错误检查:
    if (SDL_Init(SDL_INIT_VIDEO) < 0) { Logger::error("SDL could not initialize! SDL_Error: %s", SDL_GetError()); return -1; } SDL_Window* window = SDL_CreateWindow("Snake", ..., SDL_WINDOW_SHOWN); if (!window) { Logger::error("Window could not be created! SDL_Error: %s", SDL_GetError()); SDL_Quit(); return -1; }
  • 确保主循环中调用SDL_PollEvent()处理窗口关闭事件,否则窗口会无响应:
    SDL_Event e; while (SDL_PollEvent(&e) != 0) { if (e.type == SDL_QUIT) { running = false; // 退出主循环标志 } }

4.3 现象:贪吃蛇移动时“瞬移”或“方向错乱”,比如按右键却向上走

原因:方向更新逻辑写在move()内部,导致每帧都覆盖用户输入。正确做法是:输入只更新方向变量,move()只根据当前方向计算新位置。

解决:

  • 检查snake.hpp的move()方法,确认其中没有类似direction = {1,0};的硬编码赋值。
  • 方向应在输入处理模块(如input_manager.hpp)中捕获,并存入Snake对象的成员变量:
    // 在主循环的输入处理段 if (key == SDLK_RIGHT && currentDir != LEFT) { // 防止180°掉头 snake.setDirection({1, 0}); } // 然后在 move() 中只做:head.x += direction.x; ...
  • setDirection()需校验反向(如当前向左,禁止设为向右),否则蛇会立即自撞。

4.4 现象:俄罗斯方块旋转后位置偏移,方块“飞出”游戏区

原因:旋转时未重新计算方块在游戏板上的锚点(anchor point)。ROTATION_DATA中的坐标是相对中心的,但position成员变量被误认为是左上角,导致旋转后整体偏移。

解决:

  • Piece类中position应明确为方块中心在游戏板上的坐标(整数格),而非左上角。
  • getAbsolutePositions()中的计算改为:
    result.emplace_back(position.x + offset[0], position.y + offset[1]);
  • 初始化position时,需设为游戏板中心附近(如Position{10, 0}),而非(0,0)。
  • 在board.hpp的碰撞检测中,需确保position.x + offset[0]不越界(0 <= x < BOARD_WIDTH)。

5. 二次开发实战:给扫雷添加“标记旗帜”功能与难度选择菜单

5.1 扫雷游戏结构速览:games/minesweeper/的关键文件

minesweeper/ ├── CMakeLists.txt ├── main.cpp # 主循环:处理菜单、游戏状态切换 ├── game_board.hpp # 核心:二维数组存储雷区,含布雷、开格、递归展开逻辑 ├── cell.hpp # 单格状态:HIDDEN / REVEALED / FLAGGED / MINE ├── menu.hpp # ASCII 菜单渲染与输入处理 └── renderer.hpp # SDL2 绘制:格子、数字、旗帜图标(assets/flag.png)

当前版本仅支持固定 10x10 雷区、15 颗雷,且无标记功能。我们要实现:

  1. 按右键在隐藏格上放置/取消旗帜(FLAGGED状态)
  2. 新增难度菜单:初级(9x9, 10雷)、中级(16x16, 40雷)、高级(16x30, 99雷)

5.2 添加右键标记:修改game_board.hpp与输入处理

步骤1:扩展Cell状态枚举

// games/minesweeper/cell.hpp enum class CellState { HIDDEN, // 未翻开 REVEALED, // 已翻开(显示数字或空白) FLAGGED, // 已插旗(仅对 HIDDEN 格有效) MINE // 地雷(翻开后游戏结束) };

步骤2:在GameBoard类中添加标记方法

// games/minesweeper/game_board.hpp class GameBoard { private: std::vector<std::vector<Cell>> board; int width, height, mineCount; public: // 在 (x,y) 格切换旗帜状态(仅当该格为 HIDDEN 时) void toggleFlag(int x, int y) { if (x < 0 || x >= width || y < 0 || y >= height) return; auto& cell = board[y][x]; // 注意:y 是行(垂直),x 是列(水平) if (cell.state == CellState::HIDDEN) { cell.state = CellState::FLAGGED; } else if (cell.state == CellState::FLAGGED) { cell.state = CellState::HIDDEN; } // 不对 REVEALED/MINE 格做任何操作 } // 检查是否所有雷都被正确标记(胜利条件之一) bool allMinesFlagged() const { int flaggedCount = 0; for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { if (board[y][x].state == CellState::FLAGGED && board[y][x].isMine) { // isMine 是 Cell 的布尔成员 flaggedCount++; } } } return flaggedCount == mineCount; } };

步骤3:在主循环中捕获右键事件

// games/minesweeper/main.cpp 的主循环内 SDL_Event e; while (SDL_PollEvent(&e) != 0) { if (e.type == SDL_QUIT) { running = false; } else if (e.type == SDL_MOUSEBUTTONDOWN) { if (e.button.button == SDL_BUTTON_LEFT) { // 左键:翻开格子(原有逻辑) int x, y; SDL_GetMouseState(&x, &y); gameBoard.revealCell(screenToGrid(x, y)); // 屏幕坐标转网格坐标 } else if (e.button.button == SDL_BUTTON_RIGHT) { // 右键:标记旗帜(新增逻辑) int x, y; SDL_GetMouseState(&x, &y); gameBoard.toggleFlag(screenToGrid(x, y)); } } }

5.3 实现难度选择菜单:重构menu.hpp与main.cpp

步骤1:定义难度枚举与配置表

// games/minesweeper/menu.hpp enum class Difficulty { BEGINNER, INTERMEDIATE, EXPERT }; struct DifficultyConfig { int width, height, mineCount; }; static const std::map<Difficulty, DifficultyConfig> DIFFICULTY_TABLE = { {Difficulty::BEGINNER, {9, 9, 10}}, {Difficulty::INTERMEDIATE, {16, 16, 40}}, {Difficulty::EXPERT, {16, 30, 99}} };

步骤2:修改主菜单逻辑,支持难度选择

// games/minesweeper/main.cpp int main(int argc, char* argv[]) { // ... SDL 初始化 ... Difficulty selectedDifficulty = Difficulty::BEGINNER; GameState gameState = GameState::MENU; // 新增状态:MENU / PLAYING / GAME_OVER while (running) { switch (gameState) { case GameState::MENU: { renderMainMenu(); // 显示 "Beginner / Intermediate / Expert / Quit" if (handleMenuInput(selectedDifficulty, gameState)) { // 用户选择了难度,初始化新游戏板 gameBoard = GameBoard( DIFFICULTY_TABLE.at(selectedDifficulty).width, DIFFICULTY_TABLE.at(selectedDifficulty).height, DIFFICULTY_TABLE.at(selectedDifficulty).mineCount ); gameState = GameState::PLAYING; } break; } case GameState::PLAYING: { // 主游戏循环(含左右键处理) break; } } } return 0; }

步骤3:在renderer.hpp中绘制旗帜图标

// games/minesweeper/renderer.hpp void renderCell(const Cell& cell, int gridX, int gridY) { SDL_Rect dest = {gridX * CELL_SIZE, gridY * CELL_SIZE, CELL_SIZE, CELL_SIZE}; if (cell.state == CellState::FLAGGED) { SDL_Texture* flagTex = loadTexture("assets/flag.png"); // 预加载一次,缓存指针 SDL_RenderCopy(renderer, flagTex, nullptr, &dest); } else if (cell.state == CellState::REVEALED) { // 渲染数字或空白... } else if (cell.state == CellState::HIDDEN) { // 渲染未翻开格子... } }

提示:assets/flag.png需自行准备(16x16 像素 PNG),放入assets/目录。若不想处理图片,可用 ASCII 字符F替代,在renderCell()中调用SDL_RenderDrawText()(需集成 SDL2_ttf)。


6. 从那以后我每次改 C++ 游戏,都强制走一遍这三步验证

改完代码,尤其是动了核心数据结构(如Snake::body的容器类型、Piece的坐标系统),我绝不会直接点运行看效果。而是机械地执行以下三步,十年没翻过车:

第一步:编译时开启所有警告,把-Wall -Wextra -Werror加进 CMake
在CMakeLists.txt顶层添加:

if(CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Werror) endif()

理由:std::list::pop_back()对空容器是未定义行为,但编译器不会报错;而std::list::back()对空容器会触发-Wnull-dereference警告。加-Werror后,这类隐患在编译期就被掐死,比运行时崩溃好 debug 一万倍。

第二步:对每个游戏写一个最小化测试用例(test_*.cpp),只验证核心逻辑
例如test_snake.cpp:

#include "snake.hpp" #include <cassert> int main() { Snake snake; // 初始化:蛇长3格,头在(5,5),向右 snake.init({5,5}, {1,0}); // 假设添加了 init() 方法 assert(snake.getBody().size() == 3); snake.move(); // 向右移动一格 auto head = snake.getBody().front(); assert(head.x == 6 && head.y == 5); // 新蛇头应在(6,5) // 测试撞墙 snake.setPosition({19,5}); // 移到最右列 snake.setDirection({1,0}); snake.move(); assert(snake.isColliding()); // 应撞墙 }

编译命令:g++ -std=c++17 test_snake.cpp -o test_snake && ./test_snake。每次改snake.hpp,先跑这个,绿了再跑完整游戏。

第三步:用valgrind(Linux/macOS)或Application Verifier(Windows)扫内存泄漏
特别是加了新资源(如 SDL_Texture、SDL_AudioSpec),必须确保cleanup()函数被调用:

// games/snake/main.cpp void cleanup() { SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); } // 在 main() 结尾、所有 return 前调用 cleanup()

漏掉SDL_DestroyRenderer(),valgrind --leak-check=full ./snake会精准报出:“definitely lost: 128 bytes in 1 blocks”。

这三步看着慢,实则省下 90% 的调试时间。上次给某跨平台系统移植扫雷时,光是valgrind就揪出 3 个 texture 未释放的 bug,避免了客户设备内存耗尽重启的事故。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 3:08:40

从MLP到LeNet:输出层为何总用sigmoid和softmax?

1. 从MLP到LeNet&#xff1a;先搞清楚模型在做什么1.1 全连接层与卷积层的共性&#xff1a;输出层永远是那一层"考卷"很多人学神经网络时&#xff0c;会把MLP&#xff08;多层感知机&#xff09;和LeNet当成两个独立的东西——一个用来入门理解原理&#xff0c;一个用…

作者头像 李华
网站建设 2026/10/20 3:08:16

Selenium无原生POST?封装fetch实现类似requests的请求

简介&#xff1a;面向Web自动化测试与接口调试场景&#xff0c;该压缩包讲解如何借助Selenium封装POST参数并提交请求&#xff0c;以获取异步返回的JSON对象值。内容适合熟悉Python基础、希望将Selenium从UI操作扩展到接口联调的测试或开发人员&#xff0c;也可作为学习fetch、…

作者头像 李华
网站建设 2026/10/20 3:06:46

百考通AI实战:论文降重与AIGC降痕的双链路逻辑

1. 先搞清楚&#xff1a;你的论文到底被什么“盯”上了每年毕业季都有人来问我&#xff1a;论文重复率怎么降&#xff1f;但今年问题变了&#xff0c;变成“查重过了&#xff0c;AI检测又亮了&#xff0c;怎么办”。我前阵子帮几位同学改论文&#xff0c;最强烈的体感就是&…

作者头像 李华
网站建设 2026/10/20 3:06:24

TeamClaw重磅上线!国内首个专为销售团队打造的、可管理可控制的OpenClaw企业级解决方案——TaoToken统一Key接入实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 3:06:21

STM32驱动PN532串口读卡完整指南:从HSU协议到UID解析

简介&#xff1a;面向STM32嵌入式开发者的NFC驱动资源包&#xff0c;以PN532串口驱动为核心&#xff0c;讲解在STM32上实现NFC标签读写的方法。内容涵盖UART引脚选择&#xff08;如PA9/PA10&#xff09;、115200bps波特率配置、PN532初始化命令、标准帧结构解析、NDEF数据读写流…

作者头像 李华
网站建设 2026/10/20 3:06:04

跨境业务汇率API接入实战:数据源选型、多币种换算与稳定性保障

干跨币种业务最怕什么&#xff1f;不是需求改来改去&#xff0c;而是报价单上的人民币金额隔天就对不上账。去年我帮一个跨境记账工具接汇率行情数据&#xff0c;第一个版本是运营手动填表&#xff0c;结果每月对账都要为“昨天的汇率到底是什么”吵一圈。后来我把汇率接口API接…

作者头像 李华