简介:本项目围绕51系列单片机展开,聚焦于P10单色LED点阵屏的驱动开发与动态显示功能实现。采用Keil C51编程语言编写控制程序,实现了广告文字滚动、动画效果等移动显示功能,并集成电子时钟模块,在点阵屏上实时显示时间信息。项目包含完整的硬件接口设计、驱动代码、定时器与中断控制逻辑,涵盖嵌入式系统开发中的核心知识点,适用于学习单片机控制、LED显示技术和嵌入式软硬件协同设计的实践应用。
1. 51单片机基础架构与应用
51单片机核心架构解析
51单片机采用经典的冯·诺依曼架构,集成CPU、ROM、RAM、定时器、串口及四个8位并行I/O端口(P0–P3)。CPU执行指令周期短,适合实时控制任务。程序存储器(ROM)通常为4KB Flash,支持多次烧录;数据存储器(RAM)为128字节,满足基本变量存储需求。
I/O端口特性与P1.0应用分析
P1.0作为P1端口的首位引脚,具有准双向I/O特性,内部上拉电阻约50kΩ,驱动能力可达20mA灌电流,适用于LED直接驱动。在P10点阵屏控制中常用于输出CLK或STB信号,需注意电平匹配与时序同步问题。
最小系统构建要素
单片机最小系统包含晶振电路(典型值12MHz)、复位电路(10μF电容+10kΩ电阻构成RC充电回路)以及时钟源配置。其中,XTAL1/XTAL2引脚连接无源晶振,RST引脚高电平持续2个机器周期以上触发复位,确保程序从0000H地址开始执行。
2. Keil C51编程环境配置与使用
在嵌入式系统开发中,选择一个高效、稳定且功能完善的集成开发环境(IDE)是项目成功的基础。对于基于51单片机的控制系统设计而言, Keil μVision 作为业界广泛使用的开发平台,提供了从代码编辑、编译链接到仿真调试的一体化解决方案。其核心优势在于对C51语言的高度适配性、丰富的库支持以及强大的调试能力。本章将系统性地介绍如何搭建并高效使用Keil C51开发环境,涵盖工程创建流程、编译器特性优化、程序调试技术以及固件烧录方法,帮助开发者构建可重复、易维护的开发工作流。
2.1 Keil μVision集成开发环境搭建
Keil μVision 是由德国Keil Software公司开发的专业级嵌入式开发工具,特别针对8051架构进行了深度优化。它不仅提供直观的图形界面,还集成了ARM编译器(用于现代MCU)、C51编译器、A51汇编器、BL51链接定位器等关键组件。正确安装和配置该环境,是进行后续开发的前提。
2.1.1 软件安装与授权激活流程
Keil μVision 的安装过程看似简单,但涉及版本选择、路径规范及授权机制等多个细节,稍有疏忽可能导致后续无法生成HEX文件或调试失败。
目前主流版本为 Keil μVision 4 和 μVision 5 ,其中μVision 5支持更多新型芯片,并具备更好的UI交互体验。建议优先选用μVision 5。
安装步骤如下:
- 访问 https://www.keil.com 下载“MDK-ARM”或“C51”版本安装包(根据是否仅用于51单片机选择)。
- 运行安装程序,推荐自定义安装路径,避免中文目录(如
C:\Keil_v5)。 - 安装过程中会提示安装设备家族包(Device Family Pack, DFP),务必勾选“8051”相关支持包。
- 完成安装后首次启动时,需进行授权激活。
⚠️ 注意:免费版(Evaluation Version)限制代码大小为2KB,超出后编译报错“*** ERROR C206: NOT ENOUGH MEMORY”,因此必须获取合法许可证。
授权激活方式有两种:
| 激活类型 | 获取方式 | 适用场景 |
|---|---|---|
| 免费试用(Trial License) | 输入邮箱注册获取30天全功能授权 | 学习测试 |
| 正式授权(Full License) | 购买后获得LIC文件导入 | 商业项目 |
| 使用注册机(非官方渠道) | 网络获取(存在法律风险) | 不推荐 |
graph TD
A[下载Keil安装包] --> B[运行Setup程序]
B --> C{选择安装路径}
C --> D[勾选C51/8051支持包]
D --> E[完成安装]
E --> F[首次运行μVision]
F --> G[打开License Management]
G --> H[输入Product Number]
H --> I[连接Keil服务器验证]
I --> J[激活成功]
参数说明:
- Product Number :购买时提供的唯一产品密钥,格式为
XXXX-XXXX-XXXX-XXXX-XXXX-XXXX。 - License File (.LIC) :包含加密授权信息的文本文件,需通过菜单
Help → License Management导入。 - Node-Locked vs Floating License :前者绑定特定机器MAC地址;后者允许多用户共享服务器授权。
💡 实践建议:若实验室多人共用,建议部署浮动授权服务器以提高资源利用率。
2.1.2 工程创建与项目结构组织规范
良好的项目结构不仅能提升开发效率,还能增强团队协作性和后期维护性。Keil μVision 支持多层级分组管理源码文件。
创建新工程的操作流程:
- 打开μVision,点击
Project → New μVision Project。 - 选择工程保存路径(建议新建独立文件夹,如
P10_Display_Project)。 - 命名工程(例如
main.uvprojx)。 - 弹出“Select Device for Target”对话框,搜索所用单片机型号(如
STC89C52RC或AT89S52)。 - 添加启动代码(Startup Code),通常Keil自动提示是否添加
STARTUP.A51。
随后可通过右键点击“Source Group 1”添加 .c 和 .h 文件:
// main.c
#include <reg52.h>
#include "led_matrix.h"
void main() {
while(1) {
display_char('A');
}
}
// led_matrix.h
#ifndef _LED_MATRIX_H_
#define _LED_MATRIX_H_
void sendByte(unsigned char dat);
void display_char(char c);
#endif
推荐的标准项目结构:
| 目录/文件 | 用途说明 |
|---|---|
./Src/ | 存放所有 .c 源文件 |
./Inc/ | 存放 .h 头文件 |
./Lib/ | 第三方库或驱动函数 |
./Startup/ | 启动代码(可选) |
config.h | 全局配置宏定义 |
board.h / board.c | 板级硬件抽象层 |
📌 示例:通过
Project → Manage → Components, Books and Environment可建立模块化组件库,便于跨项目复用。
2.1.3 目标芯片选型与编译器设置
目标芯片的选择直接影响寄存器映射、内存布局和外设可用性。错误选型会导致头文件不匹配、I/O操作异常等问题。
芯片选型要点:
- 必须与实际使用的MCU完全一致(包括后缀,如RC/P/ED等);
- 若使用国产兼容芯片(如STC系列),应选择最接近的通用型号(如AT89S52),并在代码中手动修正差异;
- 特别注意Flash/RAM容量、定时器数量、串口数量等资源参数。
进入 Project → Options for Target 'Target 1' 设置窗口,主要配置项包括:
| 标签页 | 关键设置项 | 推荐值 |
|---|---|---|
| Device | 选择具体MCU型号 | STC89C52RC |
| Target | 设置晶振频率(XTAL) | 11.0592 MHz |
| Output | 勾选“Create HEX File” | ✅ |
| C51 | Code Optimization Level | 8(平衡速度与体积) |
| Debug | 选择调试工具(Simulator / ULINK / ST-Link) | Simulator |
// reg52.h 中的部分定义示例
sfr P0 = 0x80;
sfr P1 = 0x90;
sfr P2 = 0xA0;
sfr P3 = 0xB0;
sfr TCON = 0x88;
sfr TMOD = 0x89;
上述 sfr 声明表示特殊功能寄存器的地址映射,只有正确选型才能确保这些地址准确无误。
🔍 编译器优化等级说明:
- Level 0:关闭优化,便于调试;
- Level 6~8:启用循环展开、函数内联等高级优化;
- Level 9:最大优化,可能改变执行顺序,影响调试精度。
flowchart LR
A[开始新项目] --> B[选择MCU型号]
B --> C[设置晶振频率]
C --> D[配置输出HEX文件]
D --> E[设定编译优化级别]
E --> F[保存设置并编译]
✅ 最佳实践:每次更换硬件平台前,必须重新核对Target Settings中的各项参数,尤其是XTAL值,否则定时器中断周期计算将出现偏差。
2.2 C51语言语法特性与单片机适配
C51并非标准ANSI C的简单移植,而是专为8051架构定制的扩展语言,引入了多个关键字和存储模型来应对有限资源和直接硬件访问的需求。掌握这些特性能显著提升程序效率与稳定性。
2.2.1 特殊关键字(如sbit、code、volatile)的应用场景
C51扩展了一系列专用关键字,使程序员能够更精细地控制硬件行为和内存分配。
sbit :位寻址变量声明
8051支持对某些SFR的单个位进行读写(地址能被8整除的SFR)。 sbit 用于定义可单独操作的位变量。
sbit LED = P1^0; // 定义P1.0引脚为LED控制位
sbit TF0 = TCON^5; // 定时器0溢出标志位
void toggle_led() {
LED = ~LED; // 直接翻转P1.0电平
}
✅ 逻辑分析:
- P1^0 表示P1端口第0位,编译器将其映射到底层BITADDR;
- 使用 sbit 比用位掩码操作( P1 |= 0x01; )更高效,生成的是 SETB 或 CLR 指令;
- 仅可用于可位寻址区域(20H~2FH RAM 和 地址为X0H/X8H的SFR)。
code :常量数据存入程序存储器
默认情况下,全局数组存储在RAM中,而ROM空间更大且断电不丢失。使用 code 关键字可将数据固化至Flash。
unsigned char code font_8x8[] = {
0x00,0x7E,0x42,0x42,0x42,0x42,0x7E,0x00 // 字符'H'点阵
};
📌 参数说明:
- code 等价于 __code ,指示编译器将数据放置在CODE段;
- 访问时需用指针读取: value = *(font_8x8 + index);
- 节省宝贵RAM资源,适合字库、图像模板等静态数据。
volatile :防止编译器优化访问序列
当变量被硬件或中断修改时,必须用 volatile 告知编译器“此值随时可能变化”。
volatile unsigned char flag_timer;
void timer0_isr() interrupt 1 {
flag_timer = 1; // 被中断服务程序修改
}
void wait_for_timeout() {
while(!flag_timer); // 若无volatile,编译器可能优化为死循环
flag_timer = 0;
}
🔍 逐行解读:
- 第1行:声明 flag_timer 为易变变量;
- 第4行:中断中更新标志;
- 第9行:主循环等待标志置位;
- 若未加 volatile ,编译器认为 flag_timer 不会被其他路径修改,可能缓存其初始值0,导致无限等待。
2.2.2 存储器模式(Small/Medium/Large)选择策略
C51提供三种存储器模型,决定默认变量的存放位置,直接影响性能与寻址能力。
| 模式 | 默认变量存储区 | 指针类型 | 适用场景 |
|---|---|---|---|
| Small | 内部RAM(DATA) | 小型指针(1字节) | 变量少于128字节 |
| Medium | 外部RAM(PDATA) | 分页指针(1字节) | 使用外部RAM但总量小 |
| Large | 外部RAM(XDATA) | 大型指针(2字节) | 数据量大、需要大量缓冲区 |
#pragma small // 或 #pragma large
char buffer[256]; // 在Small模式下会导致堆栈溢出!
💡 建议:
- 对于STC89C52类芯片(仅有256B内部RAM),推荐使用 Small模式 ;
- 若外扩62256(32KB SRAM),可选 Large模式 ;
- 修改方式: Project → Options → C51 → Memory Model
⚠️ 错误示例:在Small模式下定义大型数组,会导致LINK时报错“OVERFLOW IN SEGMENT”。
2.2.3 位操作与字节操作的效率对比分析
在控制LED、按键、通信协议时,常需精确到位级操作。C51支持两种方式:
方式一:位运算符(&, |, <<)
P1 = (P1 & 0xFE) | (state << 0); // 设置P1.0
优点:通用性强;缺点:需多次读改写,生成多条指令。
方式二:sbit + 直接赋值
sbit P1_0 = P1^0;
P1_0 = state;
优点:编译为单条 SETB 或 CLR 指令,速度快、代码紧凑。
| 操作方式 | 生成汇编 | 执行周期 | 适用场合 |
|---|---|---|---|
| 位运算 | MOV A,P1 → ANL/ORL → MOV P1,A | 6~8周期 | 批量修改多位 |
| sbit赋值 | SETB C or CLR C | 1~2周期 | 单独控制某一位 |
pie
title 不同位操作方式CPU周期占比
“sbit直接操作” : 1
“位运算操作” : 6
“函数封装调用” : 12
🔧 性能提示:高频扫描驱动(如点阵屏)应优先使用
sbit减少延迟。
2.3 程序调试与仿真技术实践
高质量的软件离不开高效的调试手段。Keil μVision内置的仿真器无需硬件即可模拟CPU运行状态,结合断点、变量监控等功能,极大提升了问题排查效率。
2.3.1 利用μVision内置仿真器进行单步调试
仿真器基于指令级模拟器(Simulator),可模拟时钟、中断、定时器等行为。
启用步骤:
1. Debug → Start/Stop Debug Session (Ctrl+F5)
2. 点击“Run”或“Step Into”(F11)开始执行;
3. 观察寄存器、内存、外设视图。
void delay_ms(unsigned int ms) {
unsigned int i, j;
for(i=ms; i>0; i--)
for(j=115; j>0; j--); // 延时约1ms@11.0592MHz
}
调试时可在该函数处设断点,查看 i , j 的变化过程。
🔎 提示:仿真器不模拟真实IO电平变化,仅反映内部状态。GPIO输出状态可在“Peripheral → I/O Ports”中查看。
2.3.2 断点设置与变量实时监控方法
断点是调试中最常用的工具之一。Keil支持多种断点类型:
| 类型 | 设置方式 | 功能 |
|---|---|---|
| 软件断点 | F9快捷键 | 暂停执行 |
| 条件断点 | 右键Breakpoint → Condition | 达到条件才触发 |
| 数据断点 | 在Memory Window设Watch | 数据变更时暂停 |
int counter = 0;
while(1) {
if(++counter >= 100) break; // 设条件断点:counter==100
}
此外,可在“Watch & Call Stack”窗口添加变量监控:
| Variable | Value | Type |
|---|---|---|
| P1 | 0xFE | unsigned char |
| counter | 99 | int |
🛠 技巧:使用
printf("cnt=%d\n", counter);配合串口窗口输出日志(需启用ITM调试通道)。
2.3.3 与Proteus联合仿真验证硬件交互逻辑
单一软件仿真无法验证真实硬件行为。 Keil + Proteus联调 可实现虚拟电路与代码同步运行。
配置步骤:
- 在Proteus中绘制含51单片机的电路图;
- 右键MCU → Edit Properties → Program File 设置为Keil生成的
.hex; - 在Keil中设置:
Debug → Use Proteus VSM Simulator; - 启动调试,两者自动同步运行。
sequenceDiagram
participant Keil
participant Proteus
Keil->>Proteus: 发送调试命令
loop 实时同步
Keil->>Keil: 执行下一条指令
Proteus->>Proteus: 更新引脚电平
Keil->>Proteus: 查询引脚状态
end
✅ 应用价值:可在无实物板的情况下验证P10点阵屏驱动波形、通信时序等复杂逻辑。
2.4 固件烧录与在线下载流程
最终程序需写入单片机Flash才能脱离开发环境运行。不同厂商提供各自的ISP工具,掌握烧录流程至关重要。
2.4.1 HEX文件生成机制与格式解析
HEX是Intel HEX格式,ASCII文本形式记录机器码及其加载地址。
样例片段:
:020000040000FA
:100000007802E6FF75810BD2A9000000000000005C
:10001000000000000000000000000000000000004C
:00000001FF
每行结构:
| 字段 | 含义 | 示例 |
|---|---|---|
: | 起始符 | : |
10 | 数据长度(字节数) | 16 |
0000 | 偏移地址 | 0x0000 |
00 | 记录类型(00=数据) | 00 |
... | 实际数据 | 机器码 |
5C | 校验和 | ~(len+addr+type+data)%256 |
🔍 Keil中通过勾选“Create HEX File”自动生成,路径位于
Objects/目录下。
2.4.2 STC ISP等常用烧录工具的操作步骤
以STC官方工具为例:
- 打开STC-ISP.exe;
- 选择MCU型号(如STC89C52RC);
- 加载HEX文件;
- 设置串口号与波特率(建议115200);
- 点击“Download/Program”;
- 给单片机上电触发ISP模式。
📌 注意事项:
- 必须冷启动(先点下载,再通电);
- TXD/RXD交叉连接;
- 使用可靠电源,避免复位失败。
2.4.3 常见下载失败问题排查与解决方案
| 故障现象 | 可能原因 | 解决方案 |
|---|---|---|
| 无法识别COM口 | 驱动未安装 | 安装CH340/CP2102驱动 |
| 提示“校验失败” | 晶振不稳定 | 更换优质晶振 |
| “无目标单片机响应” | 接线错误 | 检查RST、TXD、GND连接 |
| 下载成功但不运行 | 选项字设置错误 | 清除EEPROM或重设起始地址 |
✅ 高级技巧:使用USB转TTL模块时,务必断开DTR自动复位线干扰。
3. P10 LED点阵屏工作原理与接口设计
P10 LED点阵屏作为户外广告、公共信息显示系统中的主流显示模块,凭借其高亮度、长寿命和良好的可扩展性,在工业控制、交通指示、商业宣传等领域广泛应用。该模块以红绿双色LED构成基本像素单元,支持级联拼接形成大面积显示屏,具备较强的视觉冲击力和信息承载能力。本章将深入剖析P10模块的物理结构、电气特性及信号时序机制,解析其与51单片机之间的硬件连接方式,并探讨在实际工程中如何保障接口的稳定性和可靠性。通过系统化地讲解从元器件选型到电路实现再到故障诊断的全流程技术要点,帮助开发者构建扎实的硬件驱动基础。
3.1 P10模块物理结构与电气参数
P10 LED点阵模块是基于标准16×8像素阵列设计的双色显示单元,广泛用于构建大型LED显示屏的基础构件。每个模块内部由两组8×8单色点阵组成,分别对应红色和绿色LED阵列,整体构成一个16列×8行的显示区域,共计128个像素点。这种结构既保证了足够的信息密度,又便于通过级联方式横向扩展显示宽度,满足不同尺寸需求的应用场景。
3.1.1 单元板构成(16×8红绿双色LED阵列)
P10模块的核心为一块PCB板上集成的128个发光二极管,按照8行16列的方式排列。其中每列包含两个LED:上方为红色,下方为绿色,形成红绿双色显示能力。每个LED均通过限流电阻连接至驱动芯片输出端,确保电流控制在安全范围内。整个阵列采用共阴极结构,即所有LED的负极(阴极)统一接地,而正极(阳极)则分别受控于列驱动电路。行方向通过行选通信号进行扫描控制,实现动态扫描显示。
为了提升亮度和驱动能力,P10模块通常使用恒流驱动IC(如MBI5026或TLC5921),这些芯片能够提供稳定的输出电流,避免因电压波动导致的亮度不均问题。此外,模块背面集成了电源滤波电容和去耦电路,有效抑制高频噪声干扰,提高系统稳定性。
下表展示了典型P10模块的主要电气参数:
| 参数 | 数值 | 单位 | 说明 |
|---|---|---|---|
| 工作电压 | 5.0 | V | 推荐使用稳压5V供电 |
| 最大功耗 | 6 | W | 全亮状态下峰值功耗 |
| 平均功耗 | 2~3 | W | 正常显示内容时功耗 |
| 行数 | 8 | - | 扫描行数 |
| 列数 | 16 | - | 每行16列数据 |
| 颜色类型 | 红/绿双色 | - | 支持独立控制 |
| 控制方式 | 动态扫描 | - | 逐行刷新 |
| 接口类型 | 12Pin IDC接口 | - | 标准输入输出引脚 |
该表格提供了开发过程中必须关注的关键参数,特别是在电源设计阶段需根据最大功耗估算总电流需求。例如,若级联10块P10模块,则总功率可能达到60W,需选用足够容量的开关电源并配备散热措施。
graph TD
A[P10模块正面] --> B[16×8 LED阵列]
B --> C[红色LED: 上半部分]
B --> D[绿色LED: 下半部分]
A --> E[IDC 12Pin 接口]
E --> F[数据输入: R1/G1]
E --> G[数据输出: R2/G2]
E --> H[控制信号: CLK, STB, OE, A/B/C]
A --> I[背面电路]
I --> J[驱动IC MBI5026 ×2]
I --> K[限流电阻阵列]
I --> L[滤波电容 100μF + 0.1μF]
上述流程图清晰地描绘了P10模块的内部结构布局及其关键组成部分之间的逻辑关系。可以看到,数据从IDC接口进入后,首先经过移位寄存器类驱动IC处理,再经锁存后驱动对应LED点亮;同时,行地址信号通过A/B/C三位编码选择当前激活行,实现逐行扫描。
在实际应用中,开发者应特别注意LED极性的连接方式以及驱动IC的数据流向。由于P10模块支持级联,前一级的R2/G2输出会连接到下一级的R1/G1输入,因此必须确保数据链路正确无误,否则会导致后续模块无法正常显示或出现错位现象。
此外,考虑到环境光影响,P10模块多采用超高亮LED,典型发光强度可达5000mcd以上,适合室外强光环境下使用。但这也意味着长时间全亮运行会产生较大热量,需合理设计通风或加装散热片以延长使用寿命。
最后,P10模块的封装形式通常为铝基板结构,具有良好的导热性能,可在-20°C至+60°C温度范围内稳定工作,适用于多种复杂环境条件下的部署任务。
3.1.2 级联方式与级间信号传输机制
P10模块的一大优势在于其支持无缝级联,允许用户将多个单元板串联成任意宽度的连续显示屏。级联过程主要依赖于模块间的12Pin IDC接口进行信号传递,包括数据信号(R1/G1 → R2/G2)、时钟信号(CLK)、锁存信号(STB)、使能信号(OE)以及行地址信号(A/B/C)。所有控制信号在各级之间并联连接,而数据信号则采用串行级联方式逐级传递。
具体而言,主控制器(如51单片机)将第一块模块的R1(红数据)和G1(绿数据)引脚接入,随后第一块模块的R2和G2输出连接至第二块模块的R1和G1输入,依此类推,形成一条“数据流水线”。当发送一帧图像数据时,控制器先向第一个模块发送第16列的红绿数据,然后依次推送后续模块所需的数据,直到最后一块模块接收完毕。此时,通过拉高STB信号完成锁存操作,所有模块同步更新显示内容。
该机制的本质是利用 级联移位寄存器 结构,使得整个显示屏如同一个超长的串入并出(SIPO)寄存器。假设共有N块P10模块级联,则每帧需要发送 N × 16 列数据(每列8位,共8行),总计 N × 16 × 8 = N × 128 位数据。例如,当N=4时,需连续输出512位数据才能完成一次完整刷新。
以下是典型的级联连接示意图:
[MCU]
│
├── CLK ──┬──> [P10-1] ── CLK ──> [P10-2] ── CLK ──> ... ──> [P10-N]
├── STB ──┼──> [P10-1] ── STB ──> [P10-2] ── STB ──> ... ──> [P10-N]
├── OE ──┼──> [P10-1] ── OE ──> [P10-2] ── OE ──> ... ──> [P10-N]
├── A/B/C ─┼──> [P10-1] ── A/B/C ─> [P10-2] ── A/B/C ─> ... ──> [P10-N]
│
├── R1 ─────────> [P10-1].R1
│ [P10-1].R2 ─────────────> [P10-2].R1
│ [P10-2].R2 ─────────────> ...
│
└── G1 ─────────> [P10-1].G1
[P10-1].G2 ─────────────> [P10-2].G1
[P10-2].G2 ─────────────> ...
在此拓扑中,所有控制信号共享同一总线,仅数据线呈链式传递。这种方式极大简化了布线复杂度,但也带来了信号衰减的风险,尤其是在级数较多(>10)时,原始信号边沿可能变得圆滑,引发误触发。
为解决这一问题,建议采取以下措施:
- 使用屏蔽双绞线或带状电缆减少电磁干扰;
- 在长距离传输时增加74HC245等缓冲器对信号进行整形放大;
- 控制CLK频率不宜过高(一般≤500kHz),防止建立时间不足;
- 每隔5~8块模块加装一级信号再生电路。
此外,还需注意数据极性配置。某些P10模块出厂时默认启用“反相输出”,即高电平关闭LED,低电平点亮。这会影响显示效果,需通过跳线或软件补偿方式进行调整。
综上所述,掌握P10模块的级联机制不仅是实现大屏显示的前提,更是优化系统稳定性的重要环节。只有理解数据流动路径与时序配合要求,才能在复杂项目中高效排障并提升整体性能表现。
3.1.3 扫描频率与刷新率对视觉效果的影响
P10模块采用动态扫描方式工作,即通过快速切换行选通信号,逐行点亮对应的LED列数据,利用人眼的视觉暂留效应(Persistence of Vision)产生连续画面感。然而,扫描频率设置不当将直接影响显示质量,可能导致明显的闪烁或残影现象。
所谓 扫描频率 ,是指单位时间内完成一轮全部8行扫描的次数,通常以Hz表示。例如,若每行显示时间为1ms,则一轮扫描耗时8ms,对应扫描频率为125Hz。研究表明,当扫描频率低于75Hz时,大多数人可明显感知到屏幕闪烁;而当频率提升至100Hz以上时,闪烁感显著减弱,达到“无感”级别。
另一方面, 刷新率 指的是整个显示屏内容被重新绘制的频率,即每秒更新多少帧图像。它不仅取决于扫描频率,还与每帧所需传输的数据量有关。例如,若每帧需发送512字节数据,且SPI速率(模拟模式下)为200kbps,则传输时间为 (512×8)/200000 ≈ 20.5ms ,加上扫描时间,总帧周期约为28.5ms,对应刷新率约35fps。显然,刷新率过低会导致动画卡顿、滚动文字拖尾等问题。
因此,在设计驱动程序时,必须综合考虑以下几个因素:
| 影响因素 | 合理范围 | 说明 |
|---|---|---|
| 扫描频率 | ≥100Hz | 避免肉眼可见闪烁 |
| 刷新率 | ≥30fps | 保证动画流畅性 |
| 每行持续时间 | 0.8~1.2ms | 太短则亮度下降,太长则易闪烁 |
| 占空比 | 1/8 | 8行扫描对应1/8占空比,需恒流补偿 |
值得注意的是,由于P10模块采用1/8动态扫描,每一行LED仅在1/8的时间内导通,因此要维持足够亮度,必须提高瞬时驱动电流。这也是为何驱动IC普遍支持高达40mA恒流输出的原因之一。
下面是一段用于计算理想扫描周期的C语言伪代码片段:
#define ROWS 8
#define COLS_PER_PANEL 16
#define PANEL_COUNT 4
#define BITS_PER_COL 8
// 计算每帧总位数
uint32_t total_bits = PANEL_COUNT * COLS_PER_PANEL * BITS_PER_COL;
// 假设CLK频率为400kHz(每位传输时间2.5μs)
uint32_t bit_time_us = 2500; // ns转换为μs
uint32_t transfer_time_us = total_bits * bit_time_us / 1000; // 转换为μs
// 每行显示时间(含数据传输)
uint32_t row_display_time_us = (transfer_time_us + 50) / ROWS; // 加上切换开销
// 扫描频率 = 1 / (row_display_time_us * 8) * 1e6
float scan_freq = 1000000.0f / (row_display_time_us * ROWS);
// 输出结果
printf("Total Bits: %lu\n", total_bits);
printf("Transfer Time: %lu μs\n", transfer_time_us);
printf("Row Display Time: %lu μs\n", row_display_time_us);
printf("Scan Frequency: %.2f Hz\n", scan_freq);
代码逻辑逐行分析:
-
#define ROWS 8:定义扫描行数,固定为8。 -
total_bits:计算整屏所需传输的总比特数,体现数据负载。 -
bit_time_us:根据设定的CLK频率计算每位数据传输时间(400kHz → 2.5μs)。 -
transfer_time_us:估算整帧数据传输所需时间,单位微秒。 -
row_display_time_us:将总时间平均分配给每一行,反映实际点亮时间。 -
scan_freq:最终得出扫描频率值,判断是否满足≥100Hz要求。
通过该算法,开发者可在不同级联数量和通信速率下预估显示性能瓶颈,进而调整设计方案。例如,若发现扫描频率低于阈值,可通过降低级联数量、提升CLK频率或采用DMA加速等方式优化。
总之,科学设定扫描频率与刷新率,是实现高质量视觉体验的核心所在。唯有兼顾硬件能力与生理感知特性,方能打造出稳定、清晰、舒适的LED显示系统。
4. 点阵屏动态扫描与驱动算法实现
在现代嵌入式显示系统中,LED点阵屏因其高亮度、低功耗和良好的可视性被广泛应用于广告牌、信息公告栏及工业人机界面。其中,P10双色LED点阵模块凭借其16×8的单元结构与易于级联扩展的特点,成为51单片机控制下的主流选择。然而,要实现稳定、流畅的图文显示效果,仅依赖硬件连接远远不够,必须深入理解并精确实施 动态扫描机制 与 高效驱动算法 。本章将从底层时序原理出发,系统阐述动态扫描的工作逻辑、内存映射模型的设计方法,并构建完整的软件驱动架构,涵盖数据发送、行选通控制、双缓冲机制等关键技术环节。进一步地,通过字库编码转换与汉字显示策略的引入,提升系统的实用性与可扩展性。最后,针对资源受限的51单片机平台,提出一系列性能优化手段,确保在有限RAM和CPU周期下仍能维持高质量视觉输出。
4.1 动态扫描基本原理与内存映射模型
LED点阵屏由多个发光二极管按矩阵排列组成,若对每个LED单独引出控制线,则布线复杂度将随规模指数增长。为解决此问题,普遍采用 动态扫描(Dynamic Scanning) 技术,结合 时分复用(Time Division Multiplexing, TDM) 思想,在不增加物理引脚的前提下实现全阵列控制。
4.1.1 逐行扫描机制与时分复用思想
以典型的P10单元板为例,其内部包含一个16行×8列的红绿双色LED阵列(每列两个颜色共用列线,独立行选)。虽然共有256个LED(16×8×2),但实际仅需约24根信号线即可完成控制。其核心在于“ 逐行点亮 ”:每次只选通一行,同时向所有列输入该行对应的亮灭状态,利用人眼视觉暂留效应(Persistence of Vision, POV),快速循环扫描各行,从而形成连续图像。
该过程遵循严格的时序规则:
- 在某一时刻,仅有一行处于导通状态(低电平有效);
- 列数据通过串行移位寄存器(如MBI5026)并行输出至每一列;
- 扫描频率通常需高于60Hz,避免肉眼察觉闪烁;
- 每帧图像被分解为16个子场(sub-field),分别对应16行的显示周期。
这种设计本质上是时间上的多路复用——不同行共享同一组列驱动线路,通过时间错开来避免冲突。只要切换速度足够快,观察者感知到的就是一幅完整静态画面。
以下是一个简化的扫描流程图,使用Mermaid语法描述:
graph TD
A[开始新帧] --> B{当前行为第0行?}
B -- 是 --> C[加载第0行列数据]
B -- 否 --> D[递增行索引]
C --> E[锁存列数据 STB=1→0]
E --> F[选通当前行 (拉低对应行线)]
F --> G[延时短暂时间 (~100μs)]
G --> H[关闭所有行 (消隐)]
H --> I[行索引+1]
I --> J{是否扫完16行?}
J -- 否 --> C
J -- 是 --> K[重新开始下一帧]
该流程体现了动态扫描的核心闭环: 数据准备 → 锁存 → 行选通 → 消隐 → 循环 。其中“消隐”操作至关重要,用于防止上下行之间出现串扰或残影。
4.1.2 显示缓冲区(Display Buffer)的数据组织方式
为了支持灵活的内容更新与动画处理,必须在单片机内存中建立一个与屏幕像素一一对应的 显示缓冲区(Frame Buffer) 。对于16×8双色点阵,若支持红绿双显,则至少需要两个独立缓冲区,或采用复合格式存储。
常见的组织方式如下表所示:
| 缓冲类型 | 存储结构 | 占用空间 | 特点 |
|---|---|---|---|
| 单色缓冲(8列×16行) | 每字节表示一列8行,共16字节 | 16 bytes | 简洁高效,适合单色 |
| 双缓冲(红/绿分离) | red_buf[16], green_buf[16] | 32 bytes | 支持双色独立控制 |
| 合并缓冲(高位红低位绿) | display[16],bit7~bit4: red, bit3~bit0: green | 16 bytes | 节省RAM,但处理复杂 |
推荐使用 双缓冲分离法 ,便于后续实现双色交替、混合特效等高级功能。
示例代码定义显示缓冲区:
// 定义显示缓冲区(每字节代表一列,bit表示行)
unsigned char red_buffer[16]; // 红色层:16列数据
unsigned char green_buffer[16]; // 绿色层:16列数据
// 初始化清空缓冲区
void clear_display_buffer() {
for(int i = 0; i < 16; i++) {
red_buffer[i] = 0x00;
green_buffer[i] = 0x00;
}
}
代码逻辑分析:
- red_buffer[16] 和 green_buffer[16] 分别保存每一列的红色与绿色LED状态。
- 数组索引对应列号(0~15),每个字节的每一位(bit0~bit7)对应一行(0~7?注意此处有误!)
⚠️ 参数说明与纠错提醒:
实际上,P10模块常见为 16行 × 8列 ,而非8行×16列。因此正确的缓冲应为每列8位,共8列。但部分厂商模块采用横向拼接方式形成16列视觉效果。此处假设使用的是“16列×8行”结构,即每列8个LED,共16列。
若真实硬件为16行,则需调整为 每行一个字节,共16行,每字节8列 的结构,即buffer[16]每个元素表示某一行的所有列状态。
修正后的典型结构(16行×8列):
// 正确结构:每行8列,共16行
unsigned char red_row[16]; // red_row[i] 表示第i行红灯的8位列数据
unsigned char green_row[16]; // green_row[i] 表示第i行绿灯的8位列数据
此时,在扫描中断服务程序中,依次读取 red_row[row_index] 并通过SPI-like接口发送至级联移位寄存器。
4.1.3 消除残影与闪烁的关键帧率阈值分析
尽管动态扫描节省了I/O资源,但也带来了两大视觉缺陷: 闪烁(flicker) 与 残影(ghosting) 。
- 闪烁成因 :当帧率过低时,人眼能感知到画面断续刷新的过程。研究表明,临界无感刷新率为 75Hz以上 ,理想值建议 ≥100Hz。
- 残影成因 :前一行未完全关闭时下一行已开启,导致两行内容重叠显示。这常因“消隐时间不足”或“行切换延迟”引起。
设总行数为 R = 16,目标帧率 F ≥ 80Hz,则每帧持续时间为:
T_{frame} = \frac{1}{F} ≤ 12.5ms
每行显示时间:
T_{row} = \frac{T_{frame}}{R} = \frac{12.5ms}{16} ≈ 781μs
考虑到数据传输、锁存、地址切换等开销,真正可用于点亮的时间可能只有约600μs。因此,必须合理分配时间预算。
下表列出不同帧率下的时间分配对比:
| 帧率 (Hz) | 每帧时间 (ms) | 每行时间 (μs) | 是否可见闪烁 | 推荐使用 |
|---|---|---|---|---|
| 50 | 20 | 1250 | 明显 | ❌ |
| 60 | 16.7 | 1042 | 轻微 | △ |
| 75 | 13.3 | 833 | 不明显 | ✅ |
| 100 | 10 | 625 | 几乎不可见 | ✅✅ |
实验表明,在51单片机主频12MHz条件下,使用定时器中断驱动扫描,配合汇编优化的数据发送函数,可稳定达到100Hz帧率。关键是在中断服务程序中尽量减少非必要运算,优先保障时序精度。
此外,可通过 PWM调光 技术调节整体亮度而不影响帧率。例如,在每行显示期间插入可变长度的关断时段,实现灰度控制。
4.2 驱动程序架构设计与代码实现
构建稳定高效的驱动程序是实现高质量LED点阵显示的核心任务。本节围绕三大核心组件展开: 移位寄存器写入函数封装 、 行地址切换机制 以及 双缓冲防撕裂技术 ,形成模块化、可复用的驱动框架。
4.2.1 移位寄存器写入函数封装(sendByte函数设计)
P10模块通常采用串行输入、并行输出的恒流驱动IC(如MBI5026),其工作模式类似于74HC595。数据通过CLK上升沿逐位移入,STB下降沿触发锁存输出。
定义关键IO口如下:
sbit CLK = P2^0; // 移位时钟
sbit STB = P2^1; // 锁存信号(低有效)
sbit DATA = P2^2; // 数据输入
编写通用的字节发送函数:
void sendByte(unsigned char dat) {
unsigned char i;
for(i = 0; i < 8; i++) {
DATA = (dat & 0x80) ? 1 : 0; // 取最高位输出
dat <<= 1; // 左移一位
CLK = 0;
CLK = 1; // 上升沿移入
}
}
随后,在锁存阶段统一更新输出:
void latchData() {
STB = 0;
STB = 1; // 下降沿锁存
}
代码逐行解析:
- DATA = (dat & 0x80) ? 1 : 0; :提取当前字节最高位(MSB-first协议),决定DATA引脚电平;
- dat <<= 1; :左移使次高位变为最高位,准备下一次发送;
- CLK = 0; CLK = 1; :手动模拟时钟上升沿,触发芯片采样;
- 整个循环执行8次,完成一个字节传输。
⚠️ 注意事项:
- 若级联多个芯片(如4片MBI5026控制32列),需连续调用sendByte()四次,先发最远端数据;
- 使用latchData()统一锁存,避免中间状态泄露;
- 建议在sendByte前后添加微小延时(NOP),适配较慢芯片。
4.2.2 行地址切换与消隐处理时机控制
行选通信号通常由单片机直接或经译码器(如3-8译码器)产生。以直接控制为例,使用P1口低4位选择行(支持16行):
#define ROW_PORT P1
void selectRow(unsigned char row) {
ROW_PORT = (ROW_PORT & 0xF0) | (row & 0x0F); // 更新低4位
}
结合前面的扫描逻辑,完整的扫描中断服务程序如下:
unsigned char current_row = 0;
void timer0_isr() interrupt 1 {
static unsigned long counter = 0;
// 第一步:关闭当前行(消隐)
ROW_PORT = 0xFF; // 所有行关闭
// 第二步:发送当前行的列数据
sendByte(green_row[current_row]); // 先发绿色?
sendByte(red_row[current_row]); // 再发红色(取决于级联顺序)
// 第三步:锁存数据
latchData();
// 第四步:选通当前行
selectRow(current_row);
// 第五步:更新行索引
current_row = (current_row + 1) % 16;
}
逻辑分析:
- 中断周期设定为 ~780μs(对应100Hz帧率);
- 每次中断处理一行,顺序推进;
- “消隐”放在开头,防止上一行残留;
- sendByte 顺序需与硬件级联方向一致;
- 使用模运算自动回绕到第0行。
4.2.3 双缓冲机制防止显示撕裂现象
当主程序正在修改显示内容的同时,中断正在扫描输出旧数据,可能导致部分区域显示新内容、部分显示旧内容,造成“ 显示撕裂(Tearing) ”。
解决方案是引入 双缓冲机制(Double Buffering) :
- 前台缓冲(Front Buffer) :供扫描中断读取;
- 后台缓冲(Back Buffer) :供主程序写入;
- 当后台缓冲更新完成后,原子交换指针。
实现如下:
unsigned char front_red[16], front_green[16];
unsigned char back_red[16], back_green[16];
// 更新显示:交换前后台
void swapBuffers() {
unsigned char temp;
for(int i = 0; i < 16; i++) {
temp = front_red[i];
front_red[i] = back_red[i];
back_red[i] = temp;
temp = front_green[i];
front_green[i] = back_green[i];
back_green[i] = temp;
}
}
中断中改为读取 front_red 和 front_green 。
提示:可在VSync(垂直同步)时刻调用
swapBuffers(),即每帧结束时进行切换,确保完整性。
4.3 文字与图形显示编码转换
静态图像显示的基础是正确映射字符或图案到点阵坐标。本节重点介绍如何将ASCII字符、自定义图标乃至汉字转化为点阵数据,并嵌入程序运行。
4.3.1 点阵字库提取与ASCII字符映射表构建
标准ASCII字符集可用5×7或8×8点阵表示。可通过取模工具生成C数组。
例如,字符‘A’的8×8点阵数据:
const unsigned char font_A[8] = {
0x3C, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x00
};
构建ASCII查找表:
const unsigned char* const ascii_font[128] = {
[65] = font_A,
[66] = font_B,
// ...
};
显示函数示例:
void showChar(char c, int col_offset) {
const unsigned char* pattern = ascii_font[c];
if(!pattern) return;
for(int i = 0; i < 8; i++) {
back_red[col_offset + i] = pattern[i];
}
}
4.3.2 自定义图标与符号的取模工具使用技巧
推荐使用“PCtoLCD2002”等取模软件,设置:
- 输出格式:C数组
- 扫描方向:列行式,顺向
- 位深:1bpp
生成后复制进代码段,声明为 code 段以节省RAM:
const unsigned char icon_wifi[] code = { ... };
4.3.3 汉字显示实现(GB2312编码与16×16点阵转换)
GB2312汉字库每个汉字占16×16=32字节。通过区位码计算偏移:
int getHanziOffset(unsigned char h, unsigned char l) {
return ((h - 0xA1) * 94 + (l - 0xA1)) * 32;
}
外部Flash或ROM中存放字库,运行时加载至缓冲区。
4.4 性能优化与资源占用分析
面对51单片机RAM小(仅128~256B)、Flash有限的现实,必须精细化管理资源。
4.4.1 中断服务中最小化执行时间的策略
- 避免在ISR中调用复杂函数;
- 使用查表代替计算;
- 关键路径用
_nop_()精确延时; - 开启编译器优化(Keil中选Level 2以上)。
4.4.2 RAM空间优化与常量数据固化至Flash的方法
使用 code 关键字:
const unsigned char logo[] code = { /* 大型图片 */ };
启用Large存储模式,允许访问64KB ROM。
4.4.3 多任务环境下显示任务优先级调度建议
若使用RTOS或轮询状态机,应保证扫描中断具有最高优先级,其他任务不得阻塞其执行。
建议:Timer0设为高优先级中断,主循环处理按键、通信等低实时需求任务。
5. 滚动显示与动画效果的软件控制
5.1 字符滚动算法设计与实现
在P10 LED点阵屏应用中,字符滚动是提升信息展示效率和视觉吸引力的核心功能之一。其本质是通过不断更新显示缓冲区中的像素数据,模拟文字在屏幕上的平移运动。以水平左移为例,每一帧将整个字符位图向左移动一个或多个像素列,并在右侧补入新的空白或下一个字符的数据。
5.1.1 水平左移滚动的像素级位移计算
假设使用16×16点阵汉字,每列8位,共需2字节表示一列像素。若显示缓冲区 display_buf[32] 用于存储两行(红绿双色)各16字节,则每次左移操作需对每一行执行逐字节右移,并处理跨字节的位溢出:
void scroll_left(unsigned char *buf, int len) {
for (int i = 0; i < len - 1; i++) {
buf[i] = (buf[i] << 1) | (buf[i + 1] >> 7); // 跨字节拼接
}
buf[len - 1] <<= 1; // 最后一字节仅左移
}
该函数实现的是单像素左移。若要实现更快速度,可通过一次移动多位(如4位),但会损失平滑性。推荐结合速度参数动态调节帧率而非大幅跳跃像素。
5.1.2 帧间隔控制与速度调节机制
为保证滚动流畅且可调,应基于定时器中断控制刷新频率。例如设置Timer0工作于16位自动重载模式,每2ms触发一次中断,在中断服务程序中调用滚动逻辑:
| 定时器配置项 | 值 |
|---|---|
| 工作模式 | MODE1 (16位定时) |
| 晶振频率 | 11.0592 MHz |
| 分频系数 | 12 (传统8051) |
| 计数初值 | 60536 (TH0=0xEC, TL0=0x78) |
| 中断周期 | 2ms |
void timer0_isr() interrupt 1 {
static unsigned char frame_cnt = 0;
TH0 = 0xEC;
TL0 = 0x78;
if (++frame_cnt >= speed_level) { // speed_level可调(1~10)
scroll_left(display_buf, 32);
frame_cnt = 0;
}
refresh_display(); // 更新硬件
}
5.1.3 边界判断与无缝衔接处理逻辑
当字符完全移出屏幕后,需从头加载新内容。为此引入环形缓冲区思想,预加载多条消息形成队列:
typedef struct {
unsigned char data[64]; // 支持最多4个汉字
unsigned char len;
} msg_t;
msg_t messages[5] = { /* 初始化消息 */ };
unsigned char curr_msg_idx = 0;
当当前字符位移超过总宽度(如64列)时,自动切换至下一条消息并重新载入缓冲区,实现“首尾相连”的无缝滚动效果。
5.2 多区域并发显示与特效叠加
现代LED广告屏常需支持分区独立控制,如左侧滚动标题、右侧静态图标、底部闪烁标语等。
5.2.1 分区显示缓冲区划分与独立更新机制
将 display_buf 划分为多个逻辑区域:
| 区域 | 起始偏移 | 大小(字节) | 功能 |
|---|---|---|---|
| A区 | 0 | 8 | 左侧滚动文字 |
| B区 | 8 | 8 | 右侧图标 |
| C区 | 16 | 4 | 底部状态灯 |
| D区 | 20 | 12 | 预留动画区 |
各区域由不同任务独立写入,主刷新函数合并输出:
void refresh_display() {
disable_irq();
send_to_p10(display_buf, 32); // 统一发送
enable_irq();
}
5.2.2 闪烁、渐显、跳动等视觉效果实现
- 闪烁 :通过标志位交替清零/恢复对应区域数据
- 渐显 :逐步增加点亮行数(PWM模拟亮度)
- 跳动 :周期性放大缩小字符(需预存多尺寸点阵)
// 示例:B区图标闪烁(每500ms翻转)
if (tick_500ms) {
for(int i=8; i<16; i++)
display_buf[i] ^= 0xFF;
}
5.2.3 双色交替显示控制策略(红/绿独立寻址)
P10模块通常将奇偶行分别映射到红色与绿色。可通过分离RGB缓冲实现颜色动画:
unsigned char red_buf[16], green_buf[16];
void dual_color_blink() {
static bit state = 0;
memset(state ? red_buf : green_buf, 0xFF, 16);
memset(state ? green_buf : red_buf, 0x00, 16);
merge_rgb_buffer(red_buf, green_buf, display_buf);
state = !state;
}
5.3 定时器中断驱动的精确时序控制
5.3.1 利用Timer0/TIMER1生成固定扫描周期
采用Timer1作为主扫描定时器,设定800Hz刷新率(1.25ms/帧),确保无明显闪烁:
TMOD |= 0x10; // Timer1 Mode1
TH1 = (65536 - 1113) / 256;
TL1 = (65536 - 1113) % 256;
ET1 = 1; // Enable Timer1 interrupt
EA = 1;
TR1 = 1;
注:1113 = 11.0592MHz / 12 / 800
5.3.2 中断嵌套与优先级配置避免响应延迟
设置ET1 > ES(串口)优先级,防止通信阻塞动画:
IP = 0x04; // Timer1 High Priority
5.3.3 时间基准同步确保动画节奏一致性
使用统一时间戳变量同步多个动画:
volatile uint32_t sys_tick;
void timer1_isr() interrupt 3 {
sys_tick++;
update_animation(sys_tick);
TH1 = reload_h;
TL1 = reload_l;
}
5.4 综合项目:广告招牌控制系统实现
5.4.1 需求分析与功能模块划分
| 功能模块 | 技术要点 |
|---|---|
| 显示内容管理 | 支持ASCII+GB2312混合编码 |
| 滚动模式切换 | 左移/右移/上下翻页 |
| 亮度调节 | 4级PWM调光(通过EN脚占空比) |
| 运行模式选择 | 按键输入+状态指示 |
5.4.2 主控程序状态机设计与事件响应机制
stateDiagram-v2
[*] --> Idle
Idle --> ScrollLeft: KEY1_PRESSED
Idle --> ScrollUp: KEY2_PRESSED
ScrollLeft --> FadeOut: TIMER_EXPIRE
FadeOut --> ShowIcon: TRANSITION_DONE
ShowIcon --> Idle: LONG_PRESS_MENU
状态机由主循环轮询按键并响应中断更新显示:
while(1) {
key_scan();
switch(current_state) {
case SCROLL_LEFT:
run_scroll_left();
break;
case SHOW_ICON:
show_logo();
break;
}
yield(); // 让渡CPU
}
5.4.3 系统整体调试与现场部署注意事项
- 使用示波器验证CLK/STB信号完整性
- 添加看门狗(WDT)防止死机
- 电源冗余设计,避免级联压降
- 户外防护等级不低于IP65
简介:本项目围绕51系列单片机展开,聚焦于P10单色LED点阵屏的驱动开发与动态显示功能实现。采用Keil C51编程语言编写控制程序,实现了广告文字滚动、动画效果等移动显示功能,并集成电子时钟模块,在点阵屏上实时显示时间信息。项目包含完整的硬件接口设计、驱动代码、定时器与中断控制逻辑,涵盖嵌入式系统开发中的核心知识点,适用于学习单片机控制、LED显示技术和嵌入式软硬件协同设计的实践应用。
1760

被折叠的 条评论
为什么被折叠?



