
简介本资源是一套基于STM32F10x系列MCU实现的嵌入式小游戏——“打地鼠”完整工程源码面向嵌入式初学者与STM32进阶开发者聚焦ARM Cortex-M3平台下的硬件驱动、图形显示与实时交互逻辑实践。项目涵盖LCD/LED界面驱动、按键中断响应、定时器控制地鼠随机出现节奏、得分统计与存储等核心功能是理解嵌入式系统软硬协同开发的典型教学案例。压缩包共96个文件以44个.h头文件和42个.c源文件为主体支撑HAL库标准外设库混合架构含2个启动汇编文件、1个.hex可执行镜像、Keil工程配置.uvprojx/.uvoptx、J-Link调试配置及README说明文档总大小415KB结构清晰模块划分明确如HARDWARE驱动层、SYSTEM底层支持、USER主逻辑。目前已有1320人学习下载读者可直接编译烧录运行深入掌握GPIO控制、中断优先级配置、图像转换工具IMAGE2LCD集成及FreeRTOS轻量级任务调度等实战要点。1. 用 STM32F10x 做一个能真正在 OLED 屏上“打地鼠”的嵌入式小游戏不是仿真器里跑的 Demo你手头有一块常见的 STM32F103C8T6 最小系统板俗称“蓝 pill”外接一块 0.96 英寸 I²C 接口的 SSD1306 OLED 屏想做一个能实际交互、有计时、有分数、能响应按键的完整打地鼠游戏——不是 Keil 里点“Run”后在逻辑分析仪波形里猜结果的验证代码而是插上 USB-TTL 就能玩、按一次按键就真实击中一只“地鼠”、屏幕刷新不撕裂、倒计时精确到秒的可交付嵌入式应用。这类项目在高校电子类课程设计、嵌入式入门实训、创客展台互动装置中高频出现核心诉求是用最基础的硬件资源无外部 ADC、无专用音频芯片、无 RTOS实现确定性响应 可视化反馈 状态持久化如最高分存入 Flash。它不追求 Unity 那样的动画帧率但必须守住嵌入式系统的底线GPIO 中断响应延迟 ≤ 50μs、OLED 刷新全程禁中断时间 8ms、主循环每帧耗时稳定在 33ms≈30fps以内。本文所有实现均基于标准外设库StdPeriph和裸机编程不依赖 HAL 库或 CubeMX 自动生成代码确保你能看懂每一行寄存器操作背后的物理意义。2. 从 GPIO 按键消抖到 OLED 帧缓冲构建确定性输入输出链路2.1 按键输入必须解决“机械抖动”与“误触发”的双重陷阱打地鼠游戏对按键响应的确定性要求极高玩家猛按一次系统只能识别为一次有效击打且必须在按下沿 20ms 内完成状态判定。若直接读取 GPIO 电平机械开关抖动典型持续 5–20ms会导致单次按下被识别为多次触发分数虚高、体验崩溃。常见错误做法是用delay_ms(20)等待消抖这会阻塞整个主循环导致 OLED 刷新卡顿、倒计时跳秒。正确做法是采用状态机定时器扫描使用 SysTick 定时器产生 5ms 周期中断SysTick_Config(SystemCoreClock / 200)在中断服务函数中对每个按键 GPIO 引脚执行“电平采样 → 状态迁移”逻辑定义按键状态枚举KEY_IDLE未按下、KEY_DOWN刚检测到低电平、KEY_CONFIRMED连续 3 次采样为低确认按下、KEY_UP检测到高电平准备释放仅当状态进入KEY_CONFIRMED时置位全局标志g_key_hit_flag 1并在主循环中清零。// 在 systick.c 中定义按键状态机 typedef enum { KEY_IDLE, KEY_DOWN, KEY_CONFIRMED, KEY_UP } KeyState; static KeyState key_state KEY_IDLE; static uint8_t key_press_count 0; void SysTick_Handler(void) { static uint8_t key_sample[4] {0}; // 假设 4 个按键对应 4 只地鼠洞 for(uint8_t i 0; i 4; i) { uint8_t curr_level GPIO_ReadInputDataBit(KEY_PORT[i], KEY_PIN[i]); if(curr_level Bit_RESET) { // 按下为低电平 if(key_state KEY_IDLE) { key_state KEY_DOWN; key_press_count 0; } else if(key_state KEY_DOWN) { key_press_count; if(key_press_count 3) { // 连续 3 次采样为低15ms key_state KEY_CONFIRMED; g_key_hit_flag 1; // 触发击打事件 } } } else { if(key_state KEY_CONFIRMED) key_state KEY_UP; else if(key_state KEY_UP) key_state KEY_IDLE; } } }提示此处KEY_PORT[i]和KEY_PIN[i]需提前在key.h中宏定义为实际连接的 GPIO如GPIOA和GPIO_Pin_0。状态机避免了延时阻塞5ms 采样周期兼顾了抖动滤除覆盖 99% 机械开关与响应速度最坏延迟 15ms。2.2 I²C OLED 显示必须绕过“逐字节写入”的性能黑洞SSD1306 OLED 通过 I²C 接口通信但标准I2C_SendData()函数每次发送一个字节需等待 ACK、处理起始/停止信号全屏刷新128×64 像素 1024 字节耗时超 120ms远超 33ms 帧周期。更致命的是I²C 总线在传输中若被高优先级中断打断可能引发总线锁死。解决方案是启用 DMA I²C 快速模式 帧缓冲区开启 I²C 的快速模式Fast Mode400kHz将传输速率提升至标准模式100kHz的 4 倍预分配 1024 字节的 RAM 帧缓冲区uint8_t oled_buffer[1024]所有绘图操作画点、画矩形、显示字符均作用于该缓冲区刷新屏幕时调用I2C_DMACmd(I2C1, ENABLE)启动 DMA 通道一次性将整个缓冲区推送到 OLED关键参数DMA 缓冲区大小设为 1024数据宽度为DMA_MemoryDataSize_Byte模式为DMA_Mode_Normal非循环。// 在 oled.c 中实现 DMA 刷新 void OLED_Refresh(void) { // 1. 发送 SSD1306 命令设置内存地址模式为页地址模式 OLED_WriteCmd(0x20); OLED_WriteCmd(0x02); // 2. 发送起始地址列地址 0页地址 0 OLED_WriteCmd(0x00); OLED_WriteCmd(0x10); OLED_WriteCmd(0x40); // 3. 启动 DMA 传输目标地址为 OLED 数据寄存器 (I2C1-DR) DMA_InitTypeDef DMA_InitStructure; DMA_DeInit(DMA1_Channel6); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)I2C1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)oled_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize 1024; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel6, DMA_InitStructure); DMA_Cmd(DMA1_Channel6, ENABLE); // 4. 触发 I2C 发送DMA 会自动搬运数据 I2C_DMACmd(I2C1, ENABLE); I2C_GenerateSTART(I2C1, ENABLE); while(!DMA_GetFlagStatus(DMA1_FLAG_TC6)); // 等待 DMA 传输完成 DMA_Cmd(DMA1_Channel6, DISABLE); I2C_DMACmd(I2C1, DISABLE); }注意DMA 通道需与 I²C1 映射查 STM32F10x 参考手册 Table 47此处使用 DMA1 Channel6。OLED_WriteCmd()函数内部仅发送命令字节不走 DMA因其数据量极小10 字节避免 DMA 启停开销。2.3 地鼠状态机与随机数生成让“钻出”行为真正不可预测游戏核心逻辑是控制 4 只地鼠对应 4 个洞在 3 秒倒计时内随机钻出、停留 800ms、再缩回。若用rand()函数其伪随机种子若固定如srand(1)每次运行序列完全相同玩家可 memorize 出现规律。而 STM32 无硬件 RNG 外设F10x 系列必须利用不可预测的物理源。可靠方案是采集 ADC 噪声 SysTick 计数值混合哈希初始化时启动 ADC1 通道 1假设连接一个悬空引脚采集内部噪声在main()循环中每帧调用ADC_GetConversionValue(ADC1)获取一次 12 位转换值将该值与当前SysTick-VAL倒计时寄存器异或再右移 4 位取低 8 位作为随机种子用此种子驱动线性同余发生器LCGnext (a * current c) % m其中a1664525,c1013904223,m2^32标准 glibc 参数。// 在 game.c 中实现抗预测随机数 static uint32_t rand_seed 0; void GAME_InitRandom(void) { // 启动 ADC1 采集噪声配置见 adc.c ADC_Cmd(ADC1, ENABLE); ADC_SoftwareStartConvCmd(ADC1, ENABLE); } uint8_t GAME_GetRandomUint8(void) { uint16_t noise ADC_GetConversionValue(ADC1); uint32_t val SysTick-VAL; rand_seed (rand_seed * 1664525UL 1013904223UL) 0xFFFFFFFFUL; rand_seed ^ ((uint32_t)noise 16) | val; // 混合噪声与计数值 return (uint8_t)(rand_seed 24); // 取最高字节作为 0-255 随机数 } // 控制地鼠状态0隐藏1钻出2被击中闪烁 uint8_t mole_state[4] {0}; uint32_t mole_timer[4] {0}; // 每只地鼠独立计时器 void GAME_UpdateMoles(uint32_t current_ms) { for(uint8_t i 0; i 4; i) { if(mole_state[i] 0) { // 隐藏状态随机决定是否钻出 if((current_ms % 1500) 50) { // 每 1.5s 有一次钻出机会 if(GAME_GetRandomUint8() 200) { // 20% 概率钻出 mole_state[i] 1; mole_timer[i] current_ms; } } } else if(mole_state[i] 1) { // 钻出状态停留 800ms if(current_ms - mole_timer[i] 800) { mole_state[i] 0; } } else if(mole_state[i] 2) { // 被击中闪烁 300ms 后恢复隐藏 if(current_ms - mole_timer[i] 300) { mole_state[i] 0; } } } }关键点current_ms由 SysTick 中断累加获得volatile uint32_t g_ms_ticks 0;保证时间基准统一。mole_timer[i]存储状态切换时刻避免使用delay_ms()导致主循环阻塞。3. 游戏主循环与状态调度把 30fps 帧率刻进寄存器3.1 主循环必须严格遵循“输入→逻辑→输出”三阶段流水线嵌入式游戏主循环不是无限while(1)而是精密的时间流水线。STM32F10x 主频 72MHz若主循环内混杂复杂计算与阻塞操作帧率必然抖动。必须拆解为三个原子阶段每阶段耗时可控阶段操作目标耗时关键保障输入采集读取g_key_hit_flag、更新current_ms≤ 50μs标志位为volatile避免编译器优化游戏逻辑调用GAME_UpdateMoles()、判断击中、更新分数/倒计时≤ 15ms所有循环上限硬编码如for(i0;i4;i)输出渲染调用OLED_Refresh()刷新缓冲区≤ 8msDMA 传输期间 CPU 可并行处理下一帧逻辑// main.c 中的主循环骨架 int main(void) { SystemInit(); RCC_Configuration(); // 开启 GPIO/I2C/ADC/SysTick 时钟 GPIO_Configuration(); I2C_Configuration(); ADC_Configuration(); OLED_Init(); GAME_InitRandom(); uint32_t frame_start 0; uint32_t frame_end 0; uint32_t current_ms 0; while(1) { frame_start g_ms_ticks; // 记录帧起点 // 阶段 1输入采集 if(g_key_hit_flag) { g_key_hit_flag 0; // 扫描哪个按键被按下 for(uint8_t i 0; i 4; i) { if(GPIO_ReadInputDataBit(KEY_PORT[i], KEY_PIN[i]) Bit_RESET) { if(mole_state[i] 1) { // 击中钻出的地鼠 score; mole_state[i] 2; // 进入被击中状态 mole_timer[i] g_ms_ticks; } break; } } } // 阶段 2游戏逻辑 current_ms g_ms_ticks; GAME_UpdateMoles(current_ms); // 更新倒计时总时长 3000ms每帧减去流逝时间 if(game_time_remaining 0) { game_time_remaining - (g_ms_ticks - frame_start); if(game_time_remaining 0) game_over 1; } // 阶段 3输出渲染 OLED_ClearBuffer(); // 清空帧缓冲区 // 绘制背景4 个洞位矩形 for(uint8_t i 0; i 4; i) { OLED_DrawRectangle(10i*28, 30, 20, 20, 1); // 洞框 if(mole_state[i] 1 || mole_state[i] 2) { // 绘制地鼠用 8×8 点阵图标预存在 const uint8_t mole_icon[8] OLED_DrawBitmap(14i*28, 34, mole_icon, 8, 8, 1); } } // 绘制分数与倒计时文本 OLED_ShowNum(5, 5, score, 4, 12); // 分数 OLED_ShowNum(85, 5, game_time_remaining/1000, 2, 12); // 倒计时秒数 OLED_Refresh(); // 启动 DMA 刷新 // 帧同步强制休眠至下一帧起点 frame_end g_ms_ticks; uint32_t elapsed frame_end - frame_start; if(elapsed 33) { // 目标 33ms/帧30fps DelayMs(33 - elapsed); // 精确补足 } } }提示DelayMs()函数必须基于 SysTick 实现非for循环确保休眠精度。OLED_DrawBitmap()函数将mole_icon数组按位写入oled_buffer对应位置不触发 I²C 传输。3.2 倒计时与分数持久化用 Flash 模拟 EEPROM 存储最高分游戏结束时需将本次分数与历史最高分比较并将新纪录写入 Flash。STM32F10x 的 Flash 不支持单字节擦除最小擦除单位为 1KB 页Page直接写入会破坏原有代码。安全做法是在 Flash 末尾预留一页如 0x0800F000将其划分为 16 个 64 字节扇区每次写入前先擦除整个扇区再写入结构体。// 在 flash.c 中实现安全写入 #define FLASH_PAGE_ADDR 0x0800F000 #define SCORE_STRUCT_SIZE 64 typedef struct { uint32_t highest_score; uint32_t last_play_time; // 时间戳用于防作弊 uint8_t version; // 结构体版本号 } ScoreRecord; ScoreRecord g_score_record {0}; void FLASH_WriteScore(uint32_t new_score) { if(new_score g_score_record.highest_score) return; // 1. 解锁 Flash 编程 FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); // 2. 擦除整页必须先擦除才能写入 FLASH_ErasePage(FLASH_PAGE_ADDR); // 3. 写入新记录按字32bit写入 uint32_t *addr (uint32_t*)FLASH_PAGE_ADDR; *addr new_score; *addr g_ms_ticks; // 当前时间戳 *addr 1; // 版本号 // 4. 锁定 Flash FLASH_Lock(); g_score_record.highest_score new_score; } // 在 main() 开始处读取历史最高分 void FLASH_ReadScore(void) { uint32_t *addr (uint32_t*)FLASH_PAGE_ADDR; if(*addr ! 0xFFFFFFFF) { // 检查是否已写入Flash 擦除后为 0xFF g_score_record.highest_score *addr; g_score_record.last_play_time *(addr1); g_score_record.version *(addr2); } }注意Flash 擦写寿命约 10,000 次因此FLASH_WriteScore()仅在破纪录时调用避免频繁擦写。g_ms_ticks时间戳用于校验若新纪录时间戳早于上次拒绝写入防手动修改 Flash。4. I²C 通信稳定性加固与 OLED 抗干扰绘制技巧4.1 I²C 总线必须配置强上拉与时序容错OLED 的 I²C 接口在嘈杂电磁环境如电机、继电器附近易受干扰表现为屏幕花屏、部分区域不亮、甚至 I²C 挂起。根本原因在于 SDA/SCL 线上的噪声毛刺被误判为起始/停止条件。标准 4.7kΩ 上拉电阻在长导线10cm或高干扰场景下驱动不足。硬件级加固方案将上拉电阻更换为 2.2kΩ增强上升沿陡峭度在 SDA/SCL 线与 GND 间各并联一个 100pF 陶瓷电容滤除高频噪声软件层面在I2C_GenerateSTART()前插入I2C_AcknowledgeConfig(I2C1, DISABLE)避免从机在起始信号期间意外响应。// 修改后的 I²C 初始化i2c.c void I2C_Configuration(void) { I2C_InitTypeDef I2C_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // 配置 GPIOSCL(PB6), SDA(PB7)开漏输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // I²C 初始化400kHz 快速模式 I2C_InitStructure.I2C_ClockSpeed 400000; I2C_InitStructure.I2C_Mode I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle I2C_DutyCycle_2; I2C_InitStructure.I2C_OwnAddress1 0x00; I2C_InitStructure.I2C_Ack I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); // 关键加固禁用 ACK 直到真正需要 I2C_AcknowledgeConfig(I2C1, DISABLE); }提示I2C_AcknowledgeConfig(I2C1, DISABLE)并非永久关闭 ACK而是在发送命令字节如0x20后立即调用I2C_AcknowledgeConfig(I2C1, ENABLE)恢复确保数据字节能被正确 ACK。此举避免了起始信号期间总线误响应。4.2 OLED 绘制必须规避“跨页写入”导致的显示撕裂SSD1306 的显存按“页”组织每页 8 行像素共 8 页若一个图形如地鼠图标跨越两页如第 3 行到第 12 行而绘制函数未原子化操作可能导致上半部分写入旧页、下半部分写入新页造成图像错位。例如地鼠图标高度 8 像素若起始 Y 坐标为 30则占据页 3Y24–31和页 4Y32–39必须确保两页数据同时更新。解决方案是引入“页锁定”机制定义oled_page_lock[8]数组标记每页是否被当前绘制操作占用OLED_DrawBitmap()函数在写入前计算图标覆盖的页范围start_page y/8,end_page (yh-1)/8遍历该范围将对应page_lock置 1OLED_Refresh()在 DMA 传输前检查所有page_lock[i]是否为 0否则等待实际中因主循环帧率固定此等待极少发生。// 在 oled.c 中添加页锁定 static uint8_t oled_page_lock[8] {0}; void OLED_DrawBitmap(uint8_t x, uint8_t y, const uint8_t *bitmap, uint8_t w, uint8_t h, uint8_t color) { uint8_t start_page y / 8; uint8_t end_page (y h - 1) / 8; // 锁定涉及的页 for(uint8_t p start_page; p end_page; p) { oled_page_lock[p] 1; } // 执行位图绘制略去具体算法 for(uint8_t py 0; py h; py) { uint8_t page (y py) / 8; uint8_t bit_pos (y py) % 8; for(uint8_t px 0; px w; px) { uint8_t pixel bitmap[py * ((w7)/8) px/8] (0x80 (px%8)); if(pixel) { // 设置 oled_buffer 对应位 uint16_t idx page * 128 x px; if(color) { oled_buffer[idx] | (1 bit_pos); } else { oled_buffer[idx] ~(1 bit_pos); } } } } // 解锁页 for(uint8_t p start_page; p end_page; p) { oled_page_lock[p] 0; } } // 在 OLED_Refresh() 开头添加检查 void OLED_Refresh(void) { // 等待所有页解锁 uint8_t locked 1; while(locked) { locked 0; for(uint8_t i 0; i 8; i) { if(oled_page_lock[i]) locked 1; } if(locked) DelayUs(10); // 微秒级等待避免死循环 } // ... 后续 DMA 传输代码 }关键点DelayUs(10)使用 SysTick 的微秒级延时SysTick_Config(SystemCoreClock / 1000000)确保等待不阻塞主循环。页锁定将跨页绘制变为原子操作彻底消除撕裂。5. 实战调试用逻辑分析仪抓取 I²C 波形与按键时序验证5.1 用 Saleae Logic 抓取 I²C 通信确认 OLED 刷新无异常当 OLED 出现花屏、部分区域不亮时必须验证 I²C 通信是否符合 SSD1306 时序要求。重点捕获三类波形起始信号SCL 高电平时 SDA 由高变低宽度 ≥ 4.7μs数据位SCL 高电平期间 SDA 稳定低电平期间 SDA 变化每位宽 ≥ 1.3μsACK 信号主机发出 8 位后释放 SDA从机在第 9 个 SCL 下降沿拉低 SDA宽度 ≥ 4μs。实测步骤将 Saleae 通道 0 接 SCLPB6通道 1 接 SDAPB7接地在OLED_Refresh()函数开头添加GPIO_SetBits(GPIOA, GPIO_Pin_8)假设 PA8 为调试引脚结尾添加GPIO_ResetBits(GPIOA, GPIO_Pin_8)运行程序捕获波形观察 PA8 高电平期间的 I²C 通信若发现 ACK 丢失第 9 位 SDA 未被拉低检查上拉电阻是否过小导致从机无法拉低或电容是否过大导致上升沿过缓。提示在 Saleae 软件中启用“I²C 协议解析”可直接查看传输的命令字节如0x20,0x02与数据流比肉眼数脉冲高效百倍。5.2 用示波器测量按键响应延迟确保 ≤ 50μs玩家按下按键到 OLED 显示击中反馈地鼠图标闪烁端到端延迟必须 ≤ 100ms 才符合直觉。其中按键硬件响应占主导需单独测量。将示波器探头接在按键 GPIO 引脚如 PA0另一探头接 OLED 刷新调试引脚PA8触发源设为 PA0 下降沿。合格标准PA0 下降沿按键按下到 PA8 上升沿OLED_Refresh()开始的时间差 ≤ 50μs若实测 100μs检查是否在SysTick_Handler()中执行了耗时操作如printf()或复杂浮点运算若仍超标将按键状态机移至更高优先级的 EXTI 中断如EXTI0_IRQHandler但需自行实现软件消抖因 EXTI 无内置消抖。// 替代方案EXTI 按键中断exti.c void EXTI0_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line0) ! RESET) { // 立即读取电平若为低则置位标志 if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_RESET) { g_key_hit_flag 1; } EXTI_ClearITPendingBit(EXTI_Line0); } }注意EXTI 方案虽延迟更低≤ 1μs但需在EXTI0_IRQHandler中加入 10ms 延时DelayMs(10)防止抖动此时主循环必须能容忍该中断耗时。权衡之下SysTick 状态机仍是更稳健的选择。5.3 OLED 显示质量优化调整对比度与预充电周期SSD1306 的显示效果受两个寄存器影响极大对比度Contrast寄存器0x81值域0x00–0xFF默认0x7F。值过低则地鼠图标灰暗难辨过高则背景泛白预充电周期Pre-charge Period寄存器0xD9低 4 位控制预充电时长推荐0x222 个时钟周期避免因充电不足导致边缘发虚。// 在 OLED_Init() 中添加优化配置 void OLED_Init(void) { // ... 其他初始化代码 OLED_WriteCmd(0x81); // 设置对比度 OLED_WriteCmd(0xCF); // 值 0xCF比默认 0x7F 更亮地鼠轮廓更锐利 OLED_WriteCmd(0xD9); // 设置预充电周期 OLED_WriteCmd(0x22); // 低 4 位为 0x2预充电 2 个时钟 OLED_WriteCmd(0xA1); // 段重映射ADC 正向适配常见接线 OLED_WriteCmd(0xC8); // COM 扫描方向反向适配常见接线 }实战经验0xCF对比度在多数 0.96 英寸 OLED 上呈现最佳黑度与亮度平衡0x22预充电周期可消除地鼠图标右下角的“拖影”现象使像素边缘干净利落。这些参数需根据实际屏幕批次微调建议用OLED_WriteCmd(0x81); OLED_WriteCmd(value);动态测试。本文还有配套的精品资源点击获取