嵌入式GUI字体管理:emWin四种格式原理、选型与优化实战

AI助手已提取文章相关产品:

1. 嵌入式GUI字体管理的核心挑战与emWin的应对之道

在嵌入式系统上做图形界面开发,字体管理往往是那个最容易被忽视,却又最能“卡脖子”的环节。你精心设计了炫酷的UI,动画流畅,交互顺滑,结果一上大段文本,或者需要显示多国语言,系统内存瞬间告急,渲染帧率断崖式下跌。这不是危言耸听,而是很多嵌入式GUI开发者踩过的坑。问题的根源在于,字体,尤其是高质量的字体,本质上是一种空间换时间的资源:你想要显示效果好(高分辨率、抗锯齿、多字符集),就必然需要更多的存储空间和运行时内存。

emWin作为一款久经考验的嵌入式GUI库,其字体系统的设计哲学非常务实: 提供多种解决方案,让开发者能在显示效果、内存占用、存储介质和CPU开销之间找到最适合自己项目的平衡点 。它没有试图用一种“银弹”格式解决所有问题,而是清晰地划分了赛道。理解这一点,是高效使用emWin字体系统的前提。简单来说,你可以把emWin的字体支持看作一个光谱:一端是极致节省内存的“C文件字体”,另一端是极致灵活但开销巨大的“TTF矢量字体”,中间则是兼顾了外部存储和动态加载的“XBF字体”。你的任务就是根据项目的ROM大小、RAM余量、是否有外部存储器(如SPI Flash、SD卡)、以及是否需要动态改变字体(如用户自定义字号)这些约束条件,来挑选合适的“武器”。

2. emWin字体格式深度解析:从原理到选型

2.1 内嵌式C文件字体:简单直接,但缺乏弹性

这是最传统,也是emWin默认自带的字体形式。字体数据(通常是位图)通过 FontCvt 工具被转换成C语言数组,直接编译链接到你的程序ROM中。例如, GUI_Font8x16 就是一个经典的等宽位图字体。

核心原理 :每个字符的像素信息被预先计算并存储为静态常量数组。当调用 GUI_DispString 时,库函数根据字符编码直接索引到这个数组,将位图数据拷贝到显示缓冲区。

优点

  • 零运行时开销 :无需解析格式,数据已在ROM中,访问速度最快。
  • 确定性 :内存占用和性能完全可预测。
  • 使用简单 :声明后直接 GUI_SetFont(&GUI_Font8x16) 即可使用。

缺点与局限

  • ROM占用固定 :字体一旦编译进去,即使当前界面不用,也无法释放。多套字体会迅速膨胀固件体积。
  • 无法动态变更 :字号、样式(粗体、斜体)在编译时已确定,运行时无法调整。
  • 字符集固定 :通常只包含ASCII或有限的扩展字符(如ISO 8859-1),要支持中文等大字符集,ROM占用会变得不可接受。

实操心得 :对于产品型号固定、界面文字简单(如纯英文菜单)、且ROM空间相对宽裕的工控设备,C文件字体依然是首选。它的稳定性和性能是无与伦比的。但在启动需要多语言或动态换肤的项目时,应尽早考虑其他方案。

2.2 系统独立字体(SIF):动态加载的位图字体

SIF可以看作是C文件字体的“外部数据”版本。它同样是由 FontCvt 工具生成的二进制数据块,包含了字体度量和每个字符的位图数据。但与C文件不同,SIF数据不需要在编译时链接,而是可以在运行时从任何存储介质(如已下载到RAM的数据块、文件系统)加载。

核心API GUI_SIF_CreateFont() 。你需要提供指向SIF二进制数据的指针、一个 GUI_FONT 结构体(用于接收字体信息)以及字体类型(如 GUI_SIF_TYPE_PROP 表示比例字体)。

工作原理 GUI_SIF_CreateFont 会解析你提供的数据块,填充 GUI_FONT 结构体,建立起字符编码到该数据块中位图位置的映射关系。此后,文本渲染就通过这个结构体进行。

优点

  • 动态性 :字体数据可以作为资源文件存储,在需要时加载,不用时可以释放(通过 GUI_SIF_DeleteFont ),极大提高了RAM利用率。
  • 保持位图性能 :渲染时依然是直接拷贝位图,性能与C文件字体几乎一致。

缺点

  • 仍需完整加载到RAM :尽管是动态加载,但在使用期间,整个SIF数据块必须驻留在可寻址的内存中。对于包含数千个汉字的大字体文件,这仍然是个沉重的负担。
  • 格式依然固定 :和C文件一样,字号样式在生成时确定。

典型应用场景 :系统有足够RAM,但需要支持多种字体切换(比如正常、加粗、大号标题),且这些字体不会同时使用。你可以设计一个字体管理器,在进入不同界面时加载对应的SIF数据。

2.3 外部位图字体(XBF):将内存压力转移至存储介质

XBF格式是emWin为资源极度受限系统设计的“大杀器”。它彻底解决了SIF格式的最大痛点: 字体数据在使用时也无需全部驻留内存

核心原理 :XBF文件被存储在外部介质(如SPI Flash、SD卡)上。emWin通过一个由你提供的 GetData 回调函数,按需读取字体文件中的特定数据块。文件结构分为三部分:

  1. 字体信息头 :包含基础信息,如字符编码范围。
  2. 访问表 :一个数组,记录了文件中每个字符数据块的偏移量和大小。如果某个字符不存在,对应项为零。
  3. 字符数据区 :所有字符的像素数据连续存放。

当需要渲染字符‘A’时,emWin通过回调函数,根据访问表查到‘A’数据块的偏移和大小,只读取这一小块数据到内存中进行渲染。

核心API与实现 : 使用 GUI_XBF_CreateFont() 创建字体。关键在于实现 GUI_XBF_GET_DATA_FUNC 类型的回调函数。

int myGetData(U32 Off, U16 NumBytes, void * pVoid, void * pBuffer) {
    // Off: 在XBF文件中的偏移量
    // NumBytes: 需要读取的字节数
    // pVoid: 用户自定义指针,可传递文件句柄、存储设备对象等
    // pBuffer: 读取的数据需要拷贝到的缓冲区

    // 1. 将外部存储设备的读写指针定位到 Off 处
    // 2. 读取 NumBytes 字节的数据到 pBuffer
    // 3. 返回 0 表示成功,1 表示失败
    FILE* fp = (FILE*)pVoid;
    if(fseek(fp, Off, SEEK_SET) != 0) return 1;
    if(fread(pBuffer, 1, NumBytes, fp) != NumBytes) return 1;
    return 0;
}

// 创建字体
GUI_XBF_DATA XBF_Data;
GUI_FONT XBF_Font;
FILE* fontFile = fopen("font.xbf", "rb");
GUI_XBF_CreateFont(&XBF_Font, &XBF_Data, GUI_XBF_TYPE_PROP, myGetData, (void*)fontFile);

优点

  • 内存占用极低 :仅需缓存当前屏幕显示所需的少量字符数据,理论上可以支持无限大的字体文件。
  • 支持大字符集 :是显示中文、日文等包含成千上万个字符字体的唯一实用方案。

缺点与注意事项

  • 性能依赖存储介质 :每次渲染未缓存的字符都会触发I/O操作。如果存储介质速度慢(如低速SPI Flash),会导致文本渲染卡顿。 必须配合缓存策略
  • 实现复杂度高 :需要开发者自己实现文件系统或存储设备的驱动,并集成到回调函数中。
  • 默认字符数据大小限制 :emWin默认每个字符数据不超过200字节。对于大的抗锯齿字符可能不够,需要在 GUIConf.h 中定义 GUI_MAX_XBF_BYTES 来增大限制。

避坑指南 :使用XBF字体时, 一定要实现一个简单的LRU(最近最少使用)缓存层 。在你的 GetData 回调中,先检查请求的数据块是否在内存缓存中,命中则直接返回,未命中再从存储设备读取并更新缓存。即使是一个只有几十个字符条目的小缓存,也能将渲染性能提升数个数量级,因为界面文本通常重复度很高。

2.4 TrueType字体(TTF):矢量字体的强大与代价

TTF代表了字体技术的另一个方向:矢量轮廓。字符不是存储为像素位图,而是存储为数学描述的轮廓(贝塞尔曲线)。这意味着字体可以无损缩放到任意大小。

核心原理 :emWin通过集成开源的FreeType库来实现TTF支持。使用 GUI_TTF_CreateFont() 创建字体时,需要指定TTF文件在内存中的位置和期望的像素高度。FreeType引擎会进行“栅格化”:将矢量轮廓在指定尺寸下转换为位图。这个过程计算密集,因此emWin内部维护了一个 位图缓存 ,存储已栅格化的字符位图,避免重复计算。

资源要求(官方手册明确指出的硬约束)

  • CPU :仅支持32位CPU( sizeof(int) == 4 )。
  • ROM :FreeType引擎本身需要约250KB的代码空间。
  • RAM
    • 引擎基础开销约50KB。
    • 创建字体时,需要加载TTF文件的字体表,根据字体复杂程度,可能需要额外80KB到1MB以上的RAM。
    • 位图缓存 :默认分配200KB。这是性能的关键,缓存太小会导致频繁的栅格化,严重拖慢速度。

API与内存管理

GUI_TTF_DATA TTF_Data;
GUI_TTF_CS   TTF_Cs;
GUI_FONT     TTF_Font;

// 1. 配置TTF数据源(假设aTTFArray是已加载到内存的TTF文件数据)
TTF_Data.pData = aTTFArray;
TTF_Data.NumBytes = sizeof(aTTFArray);

// 2. 配置创建参数
TTF_Cs.pTTF = &TTF_Data;
TTF_Cs.PixelHeight = 24; // 希望渲染的字号高度
TTF_Cs.FaceIndex = 0;    // 通常为0

// 3. (可选)在首次调用GUI_TTF_CreateFont前,调整缓存大小
// 如果你需要同时使用3种字体的2种大小,建议设置
GUI_TTF_SetCacheSize(3, 6, 300*1024); // 最大3种字体,6个尺寸对象,300KB缓存

// 4. 创建字体
if(GUI_TTF_CreateFont(&TTF_Font, &TTF_Cs) == 0) {
    // 创建成功
    GUI_SetFont(&TTF_Font);
    GUI_DispString("Hello TTF!");
}

优点

  • 无限缩放 :一套字体文件,满足所有字号需求。
  • 显示质量高 :矢量轮廓在放大后依然平滑,结合抗锯齿,效果远优于位图字体放大。
  • 字体资源丰富 :可直接使用电脑上的标准TTF字体文件。

缺点

  • 资源消耗巨大 :对ROM和RAM的要求是其他格式的数十倍甚至上百倍。
  • 初始化慢 :首次创建字体和栅格化字符时有明显延迟。
  • 不适合低端MCU :几乎只适用于带有MMU、主频在几百MHz以上、且RAM充裕的ARM Cortex-A或高性能Cortex-M7等平台。

选型决策流程图 : 面对一个项目,你可以通过回答以下问题来快速定位字体方案:

  1. 是否需要运行时改变字号或使用非常用字号? 是 -> 考虑TTF。
  2. 字符集是否非常大(如中日韩文字)? 是 -> 优先考虑XBF。
  3. 是否有外部存储介质(Flash, SD卡)且速度尚可? 是 -> XBF方案可行。
  4. 系统RAM是否非常紧张(< 100KB)? 是 -> 优先C文件或SIF(仅加载一种)。
  5. 字体是否固定且数量少? 是 -> C文件最简单可靠。
  6. 需要在几种固定字体间切换? 是 -> 使用SIF动态加载。

3. 字体API实战:从基础设置到高级查询

掌握了格式,接下来就是如何驾驭它们。emWin提供了一套统一的API来操作字体,无论底层是哪种格式。

3.1 字体的设置与选择

设置当前字体 GUI_SetFont() 这是最常用的函数。它接受一个 GUI_FONT* 指针,并将该字体设置为后续所有文本输出的当前字体。它返回之前设置的字体指针,方便临时切换后恢复。

const GUI_FONT* pOldFont;
pOldFont = GUI_SetFont(&GUI_Font16_ASCII); // 切换到16点阵字体
GUI_DispStringAt("Title", 10, 10);
GUI_SetFont(&GUI_Font8x16); // 切换到8x16等宽字体
GUI_DispStringAt("Content", 10, 30);
GUI_SetFont(pOldFont); // 恢复原字体

设置默认字体 GUI_SetDefaultFont() 这个函数通常在 GUI_X_Config() 中调用,用于设置调用 GUI_Init() 初始化库之后,在没有主动设置字体时系统使用的字体。如果你不想链接任何emWin自带的字体,需要在 GUIConf.h 中定义 GUI_DEFAULT_FONT NULL ,然后在此函数中设置你自己的字体。

3.2 字体信息查询API

这些API让你能在运行时获取字体的各种度量信息,是实现精确文本布局的基础。

  • GUI_GetFont() : 获取当前字体指针。
  • GUI_GetFontSizeY() : 获取字体的Y方向像素高度。这是字符本体(如从‘a’的底部到‘h’的顶部)的高度,不包括行间距。
  • GUI_GetFontDistY() : 获取字体的Y方向间距。这是两行文本基线之间的推荐距离,通常比 FontSizeY 大一些,包含了上行字母(如‘b’)和下行字母(如‘g’)的空间。 在计算换行位置时,应该使用这个值。
  • GUI_GetCharDistX(U16 c) : 获取指定字符在当前字体中的像素宽度。对于等宽字体,所有字符返回值相同;对于比例字体,每个字符宽度不同。
  • GUI_GetStringDistX(const char* s) : 获取整个字符串的像素宽度。内部就是遍历字符串并累加每个字符的 CharDistX
  • GUI_GetTextExtend() : 更强大的函数,一次性计算字符串的矩形范围。它考虑了字符宽度和字体高度,结果存储在 GUI_RECT 结构体中,非常适合在绘制前计算文本占据的空间。
GUI_RECT Rect;
char* text = "Hello World";
GUI_GetTextExtend(&Rect, text, strlen(text));
// Rect.x0, Rect.y0 通常是0, Rect.x1 是字符串宽度, Rect.y1 是字体高度
int textWidth = Rect.x1 - Rect.x0;
int textHeight = Rect.y1 - Rect.y0;
  • GUI_IsInFont() : 查询某个字符是否存在于指定字体中。在显示用户输入或外部数据时,可以用它来做回退处理,避免显示乱码。
if (GUI_IsInFont(pMyFont, unicodeChar) == 0) {
    // 字体不支持该字符,用‘?’或空格代替
    drawChar = '?';
}

3.3 字符集与编码处理

emWin内部使用 双字节编码 。它原生支持:

  1. ASCII (0-127) : 基础英文、数字、符号。
  2. ISO 8859-1 (160-255) : 西欧语言扩展,包含了带重音符号的字母(如ä, é, ñ)和一些货币符号。
  3. Unicode : 通过 GUI_UC_ 系列函数实现。 但emWin本身不包含任何非ASCII字符的图形数据 。这意味着,即使你设置了Unicode编码,要显示一个汉字,你必须提供包含该汉字的字体文件(无论是C、SIF、XBF还是TTF格式),并且该字体的编码映射是正确的。

重要原则 :字体文件(由 FontCvt 生成或TTF本身)的编码必须与你传递给emWin的字符串编码一致。如果你使用 GUI_UC_Encode() 等函数处理Unicode字符串,那么你的字体文件也必须包含对应的Unicode字符位图。通常,使用 FontCvt 工具转换时,需要指定源文件的编码和输出的编码范围。

4. 内存优化实战策略与性能调优

4.1 针对不同格式的优化技巧

C文件字体

  • 按模块裁剪 :如果项目不同模块使用不同字体,可以考虑将字体定义移到独立的C文件中,并利用链接器的“垃圾回收”功能,确保未使用的字体不被链接到最终镜像中。
  • 使用 GUI_FONT_FLASH :确保大型字体数组被放置在正确的存储区域(如NOR Flash),而不是默认的RAM中。

SIF字体

  • 生命周期管理 :严格配对使用 CreateFont DeleteFont 。在界面切换时,及时删除不再需要的字体,释放其占用的RAM。
  • 数据源优化 :如果SIF数据来自文件系统,可以考虑在系统启动时将常用字体预读到一片“字体缓存区”,避免运行时频繁的文件I/O。

XBF字体

  • 实现智能缓存 :如前所述,在 GetData 回调中实现缓存是必须的。缓存的设计可以很简单:
    typedef struct {
        U32 startOffset;
        U32 size;
        U8  data[512]; // 缓存块
        U32 timestamp;  // 用于LRU算法
    } FontCacheBlock;
    
    FontCacheBlock cache[CACHE_BLOCK_NUM];
    
    每次请求,遍历缓存块,如果 offset [startOffset, startOffset+size) 范围内,则命中。未命中则读取数据,替换掉最久未使用的缓存块。
  • 存储介质选择 :优先使用读写速度快的存储介质,如并行NOR Flash或RAM Disk。避免使用低速SPI Flash。
  • 字体文件分段 :对于超大字符集(如全汉字库),可以考虑按部首或使用频率分成多个XBF文件,按需加载。

TTF字体

  • 精细控制缓存 GUI_TTF_SetCacheSize 是你的主要工具。你需要评估:
    • MaxFaces : 同时存在的不同字体文件数量。
    • MaxSizes : 同一字体不同字号的数量。例如,同一字体有12pt和24pt两种显示,就算2个size对象。
    • MaxBytes : 位图缓存总大小。这是最重要的参数。 一个简单的估算方法 :假设你同时显示的最大文本长度包含50个字符,最大字号为48px,且是抗锯齿的(4bpp)。那么一个字符位图最大约为 48*48*0.5 = 1152 字节。50个字符约需56KB。考虑到缓存效率,设置为128KB或256KB是合理的起点。 务必通过实测调整
  • 预栅格化常用字符 :在系统启动或空闲时,主动用 GUI_DispString 显示一遍常用字符(如数字、字母、标点),让它们提前进入缓存,避免在用户交互时产生卡顿。
  • 限制字号范围 :如果UI设计允许,尽量将可用的字号限制在几个固定值,而不是允许任意缩放,这样可以减少 MaxSizes

4.2 通用性能优化建议

  1. 避免频繁切换字体 GUI_SetFont 本身开销很小,但如果是XBF或TTF字体,切换字体可能涉及I/O或缓存失效。尽量将相同字体的文本输出操作集中在一起。
  2. 重用文本范围计算结果 :如果一个静态文本的位置需要多次计算,将 GUI_GetTextExtend 的结果缓存起来,而不是每次重算。
  3. 对于动态文本(如数值、时间) ,如果更新频率高,考虑使用 GUI_DispStringAt 配合背景重绘,而不是每次都清除整个区域再输出。可以配合 GUI_GetStringDistX 计算新旧字符串宽度差,只重绘必要的区域。
  4. 启用emWin的内存设备 :对于复杂的文本渲染区域,特别是需要透明效果或与图形叠加时,使用内存设备( GUI_MEMDEV_ )先将文本绘制到内存中,再一次性拷贝到屏幕,可以显著减少闪烁和提高复杂区域的渲染速度。

5. 常见问题排查与调试实录

在实际项目中,字体相关的问题层出不穷。下面是一些典型问题的排查思路和解决方法。

问题现象 可能原因 排查步骤与解决方案
文本显示为乱码或方块 1. 字体编码与字符串编码不匹配。
2. 字体文件中不包含该字符。
3. 当前设置的字体不正确。
1. 确认字符串编码。如果是Unicode,是否使用了 GUI_UC_ 函数处理?
2. 使用 GUI_IsInFont() 检查字符是否在字体中。
3. 检查 GUI_SetFont 调用是否成功,传入的字体指针是否有效。
使用XBF字体显示异常或崩溃 1. GetData 回调函数实现有误。
2. XBF文件损坏或格式不对。
3. 字符数据超限。
1. 在 GetData 回调中添加调试输出,确认偏移和大小参数是否正确,读取是否成功。
2. 使用 FontCvt 工具重新生成XBF文件,并确保选择正确的输出格式。
3. 检查是否出现“Char data exceeds max size”警告,在 GUIConf.h 中增大 GUI_MAX_XBF_BYTES
TTF字体创建失败,返回错误 1. TTF文件数据指针或大小错误。
2. RAM不足。
3. FreeType引擎未初始化(通常是首次调用失败)。
1. 检查 GUI_TTF_DATA 结构中的 pData NumBytes
2. 检查系统剩余堆内存。TTF创建需要大量连续RAM。
3. 确保在调用 GUI_TTF_CreateFont 前, malloc/free 函数可用且工作正常。
使用TTF字体渲染速度极慢 1. 位图缓存 MaxBytes 设置过小。
2. 同时使用的字号过多,超出 MaxSizes
3. CPU性能不足。
1. 使用 GUI_TTF_SetCacheSize 增大 MaxBytes ,并使用工具监控缓存命中率。
2. 减少同时使用的字体字号变体。
3. 考虑降级使用XBF或SIF等位图字体。
切换字体后,之前绘制的文本样式变了 字体指针管理混乱。 GUI_SetFont 设置的是全局状态。 遵循“设置-绘制-恢复”的原则。在局部修改字体前,保存旧指针,绘制完成后立即恢复。
多行文本行间距过密或过疏 错误地使用了 GUI_GetFontSizeY() 而不是 GUI_GetFontDistY() 来计算行高。 计算换行Y坐标时,务必使用 GUI_GetFontDistY() 作为行增量。 FontSizeY 是字符绘制高度, FontDistY 才是排版行距。
自定义字体显示位置有偏移 字体度量信息(如基线、起始偏移)在 FontCvt 转换时设置不正确。 重新使用 FontCvt 工具生成字体,注意调整“Vertical distance”和“Character offset”等参数,并在模拟器中预览效果。

调试技巧

  • 启用emWin调试输出 :在 GUIConf.h 中定义 GUI_DEBUG_LEVEL ,可以将库内部的警告和错误信息输出到调试串口,对于定位字体加载失败、缓存溢出等问题非常有帮助。
  • 使用模拟器先行验证 :SEGGER提供了Windows版的emWin模拟器。在PC上先用模拟器验证你的字体文件、API调用逻辑和缓存策略,可以节省大量在目标板上调试的时间。
  • 内存占用分析 :对于XBF和TTF,密切关注动态内存的分配和释放。可以使用 GUI_ALLOC_GetNumUsedBytes() 等函数来监控emWin内存池的使用情况,确保没有内存泄漏。

字体管理是嵌入式GUI开发中连接美学与工程学的桥梁。没有最好的方案,只有最合适的方案。从资源捉襟见肘的8位MCU到功能丰富的32位应用处理器,emWin提供的这套多层次、可裁剪的字体方案,几乎覆盖了所有场景。关键在于,作为开发者,你需要像一位建筑师一样,清晰地了解手中的“材料”(字体格式)特性,并基于项目的“地基”(硬件资源)和“蓝图”(产品需求)做出明智的选择。开始时多花一点时间在设计和测试上,就能避免项目后期在内存不足和渲染卡顿的泥潭中挣扎。

您可能感兴趣的与本文相关内容

内容概要:本文围绕可变桨叶四旋翼无人机的规范控制点对点运动模拟展开,重点研究优化推力分配策略在翻转动作中的应用性能比较。通过Matlab代码实现,构建了四旋翼动力学模型,并设计了多种控制算法以实现精确的姿态调整轨迹跟踪。研究对比了不同推力分配方案在执行高机动性翻转动作时的稳定性、能耗效率响应速度,旨在提升无人机在复杂飞行任务中的动态性能控制精度。该仿真研究为无人机飞控系统的设计优化提供了理论依据和技术支持。; 适合人群:具备一定自动控制理论基础和Matlab编程能力,从事无人机控制、飞行器动力学或机器人系统研究的科研人员及研究生。; 使用场景及目标:① 实现四旋翼无人机在三维空间中的精确点对点运动控制;② 对比分析不同推力分配策略在执行翻转等高难度动作时的控制效果能耗表现,优化飞行性能;③ 为无人机自主飞行、特技飞行及复杂环境下的机动控制提供算法验证平台。; 阅读建议:此资源以Matlab仿真为核心,建议读者结合相关控制理论知识,深入理解代码实现细节,重点关注动力学建模、控制律设计推力分配模块。在学习过程中,应动手调试参数,复现文中翻转动作的仿真结果,并尝试拓展至其他复杂飞行任务,以加深对无人机控制机理的理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值