BTStack嵌入式开发避坑指南:那些官方文档没说的HAL接口实现细节

深入解析btstack驱动层架构:单片机裸机环境下的关键实现 本文深入解析了BTStack驱动层架构在单片机裸机环境下的关键实现,重点介绍了其分层设计思想、时间管理与中断控制的精妙实现,以及UART通信的实战优化技巧。通过STM32F103与CSR8311的移植案例,详细剖析了硬件连接、常见问题及性能优化方法,为嵌入式蓝牙开发提供了实用指导。 阅读详情

BTStack嵌入式开发避坑指南:那些官方文档没说的HAL接口实现细节

在嵌入式蓝牙开发领域,BTStack以其轻量、可移植和开源的特点,成为了许多开发者连接单片机与蓝牙世界的首选。官方文档和源码示例为我们勾勒了清晰的框架,但当你真正将其移植到裸机环境,面对一个具体的MCU和蓝牙模组时,往往会发现文档里轻描淡写的几行接口描述,背后隐藏着足以让你调试数日的“深坑”。这篇文章不是对官方指南的复述,而是从一个趟过无数坑的实践者角度,分享那些在真实项目中,关于HAL(硬件抽象层)接口实现时,你必须知道但文档绝不会写的细节。如果你已经通读了BTStack的platform/embedded目录下的代码,却依然在稳定性、功耗或调试输出上遇到棘手问题,那么接下来的内容正是为你准备的。

1. 时间基准:hal_time_ms()远不止一个定时器

几乎所有移植教程都会告诉你,实现hal_time_ms(),返回一个自系统启动以来的毫秒数即可。这没错,但关键在于如何选择和维护这个时间基准。

在裸机系统中,我们通常依赖一个硬件定时器(如SysTick)来提供周期性中断,递增一个全局变量。问题往往出在定时器溢出和中断优先级上。以一个32位变量存储毫秒数为例,大约49.7天后会溢出归零。BTStack内部的一些超时逻辑(如连接超时、链路监控)如果简单地用“当前时间戳 - 记录时间戳”计算差值,在溢出时会导致计算错误,得到巨大的超时值。

提示:虽然BTStack内部的时间比较逻辑通常能处理溢出(使用(uint32_t)(a - b)这样的无符号减法),但为了绝对安全,建议在应用层涉及长时间定时的逻辑中,自行处理溢出情况。

更隐蔽的坑在于定时器中断的优先级。如果你的蓝牙模组使用UART+DMA通信,DMA传输完成中断或UART空闲中断需要及时响应以搬运数据。如果hal_time_ms()依赖的定时器中断优先级设置过高,可能会阻塞这些通信中断,导致数据丢失;如果设置过低,又可能被其他高优先级任务打断,导致时间戳“跳变”或不精确。

我的经验是,将SysTick或用于毫秒定时的基础定时器中断优先级设置为中等偏低。例如,在Cortex-M内核中,将系统节拍定时器的中断优先级设置为低于UART和DMA中断,但高于一些非实时性的后台任务。同时,确保hal_time_ms()函数的实现是可重入的,或者是在中断上下文中安全访问的。通常,我们这样实现:

volatile uint32_t system_ms_tick = 0;

void SysTick_Handler(void) {
    system_ms_tick++;
}

uint32_t hal_time_ms(void) {
    return system_ms_tick;
}

看起来简单,但请务必检查编译器优化等级。在高优化等级下,直接返回全局变量可能被优化,导致读取到寄存器中的旧值。使用volatile关键字是基本要求。

2. 数据通道的命脉:DMA传输与中断丢失的实战解决

hal_uart_dma_send_block和hal_uart_dma_receive_block是数据吞吐的核心。官方示例可能只展示了用查询方式发送、用中断方式接收。但在高数据量或低功耗场景下,DMA几乎是必选项。这里最大的陷阱是DMA传输完成中断丢失。

很多开发者(包括最初的我)会这样配置:设置好DMA源地址、目标地址和长度,启动传输,然后在DMA传输完成中断(TC中断)里调用(*tx_done_handler)()或(*rx_done_handler)()。但在实际运行中,偶尔会发现回调函数没有被调用,协议栈因此“卡住”,不再发送或接收后续数据。

经过示波器抓取UART信号和调试器单步跟踪,问题根源通常有两个:

  1. DMA中断标志清除时机不当:在中断服务函数(ISR)中,读取状态寄存器后,如果没有正确清除中断标志,可能会导致中断只触发一次,后续中断被屏蔽。
  2. 数据流冲突与缓冲区管理:当发送和接收同时进行,或前一个传输还没完成就启动下一个时,DMA控制器或UART外设可能进入错误状态,中断无法产生。

解决方案是一套组合拳:

  • 中断标志严格管理:在DMA TC中断ISR中,首先读取并判断中断标志,然后立即清除该标志,再进行回调等操作。
    void DMA1_Channel4_IRQHandler(void) {
        if (DMA_GetITStatus(DMA1_IT_TC4)) {
            DMA_ClearITPendingBit(DMA1_IT_TC4); // 先清除标志
            if (tx_done_handler) {
                tx_done_handler(); // 再执行回调
            }
        }
    }
    
  • 引入状态机与互斥锁:在HAL层维护一个简单的发送/接收状态(如IDLE, BUSY)。在hal_uart_dma_send_block开始时检查状态,如果为BUSY,则不应直接覆盖DMA设置,而是等待或返回错误。BTStack上层通常有重试机制。
  • 启用UART空闲中断作为接收辅助:对于可变长度的HCI数据包,仅靠DMA接收固定长度不够灵活。可以配置DMA为循环模式接收至一个环形缓冲区,同时开启UART的空闲中断(IDLE Interrupt)。当UART总线空闲一段时间后,产生中断,此时根据DMA的当前传输地址计算出已接收的数据量,再手动调用(*rx_done_handler)()通知协议栈取数据。这种方式比单纯依赖DMA TC中断更可靠。

下表对比了三种数据接收方案的优劣:

方案实现复杂度可靠性功耗影响适用场景
中断字节接收低高,但高波特率下CPU负载大CPU频繁中断,功耗高低波特率(<=115200),数据量小
DMA+TC中断固定长度中中,可能丢失中断低,数据搬运由DMA完成数据包长度固定且已知
DMA循环缓冲+UART空闲中断高高,能处理变长包低,且能快速进入休眠推荐:HCI等变长协议通信

3. 硬件复位电路:不只是拉低再拉高那么简单

hal_uart_dma_init()函数要求实现硬件复位蓝牙模组。文档只说“看模组手册”,但手册往往只告诉你复位引脚的电平要求。真正的坑在于复位时序、电源序列和内部状态机。

以常见的CC256x或CSR系列模组为例,复位引脚通常要求低电平有效。一个天真的实现是:

void hal_uart_dma_init(void) {
    RESET_GPIO_LOW();   // 拉低复位
    hal_time_delay(100); // 延迟100ms
    RESET_GPIO_HIGH();  // 释放复位
    hal_time_delay(100); // 再等100ms
    // 初始化UART...
}

这可能在90%的情况下工作。但在以下情况会失败:

  • 冷启动 vs 热启动:系统完全断电再上电(冷启动)与仅通过复位引脚复位(热启动),模组内部稳压器和晶振的稳定时间不同。冷启动后需要的稳定时间更长。
  • 复位与供电时序:如果模组的IO电源(VDDIO)和核心电源(VBAT)不是同时上电,复位信号必须在两者都稳定后生效。否则可能导致模组进入一种奇怪的锁死状态。
  • 复位期间的电平毛刺:GPIO在上电瞬间可能处于不确定状态,产生毛刺,被模组误认为是复位信号。

经过验证的稳健复位流程如下:

  1. 确保模组主电源和IO电源已稳定(通常上电后延时至少10-50ms)。
  2. 将复位引脚配置为推挽输出模式,并先置于无效状态(根据手册是高电平或低电平)。
  3. 执行有效复位脉冲(拉低或拉高,持续手册要求的最小时间,通常10-100ms)。建议使用硬件定时器或精确的软件延时,避免使用不准确的循环延时。
  4. 释放复位引脚到无效状态。
  5. 等待更长的“就绪时间”:这是关键!在释放复位后,模组需要加载固件、初始化射频电路,这个时间远长于复位脉冲本身。对于CC2564B等模组,可能需要等待300-500ms才能通过UART发送第一条HCI命令。在此期间发送任何数据都可能无响应或导致后续通信异常。

注意:有些蓝牙模组(如某些国产兼容型号)可能需要特定的上电复位序列,甚至需要在复位前给一个特定的UART信号。务必查阅你手中模组的最新版数据手册或应用笔记。

4. 调试信息输出:HCI日志与printf的和平共处

调试BTStack时,HCI命令和事件日志(hci_log_through_frontline)是无价之宝。同时,我们自己的调试信息也需要通过printf输出。在裸机环境下,两者通常都重定向到同一个UART(比如USART1)。这就产生了冲突:协议栈打印HCI日志和应用程序调用printf可能同时发生,导致输出交错、乱码,甚至阻塞协议栈运行。

官方示例可能有两个独立的输出函数,但硬件上只有一个UART。简单的做法是使用互斥锁(如关闭中断)来保护UART发送函数。但这会影响实时性,特别是在低功耗模式下频繁开关中断会增加功耗。

更优雅的解决方案是构建一个非阻塞的、基于队列的串口打印驱动:

  1. 创建一个环形缓冲区作为发送队列。
  2. 实现一个统一的uart_send_bytes(const uint8_t* data, uint16_t len)函数,它将数据拷贝到环形缓冲区,并尝试启动一次DMA传输。
  3. 在DMA发送完成中断中,检查环形缓冲区是否还有数据,如果有,则启动下一次DMA传输;如果没有,则置为空闲状态。
  4. 将hci_log_through_frontline和_write(或fputc)函数都改造为调用这个统一的uart_send_bytes函数。

这样,无论来自协议栈还是应用层的打印请求,都会被缓冲起来,按顺序发送,完全解耦了打印请求与实际的硬件操作。协议栈不再需要等待字节发送完毕,极大地提高了系统的响应能力。

// 简化的示例结构
typedef struct {
    uint8_t buffer[1024];
    volatile uint16_t head;
    volatile uint16_t tail;
    volatile bool dma_busy;
} uart_tx_queue_t;

void enqueue_and_start_tx(const uint8_t* data, uint16_t len) {
    // 将数据放入环形队列...
    // 如果DMA空闲,则从队列取出数据启动DMA传输
    if (!uart_queue.dma_busy) {
        start_dma_tx_from_queue();
    }
}
// hci_log_through_frontline 和 _write 都调用 enqueue_and_start_tx

5. 低功耗模式下的IRQ处理:让蓝牙与MCU协同休眠

hal_cpu_enable_irqs_and_sleep()这个函数的名字就暗示了它与低功耗相关。在裸机系统中,主循环往往在完成所有任务后,调用这个函数进入低功耗模式(如WFI或WFE),等待中断唤醒。但实现它时,有几个极易忽略的细节。

首先,不是所有中断都能唤醒CPU。你需要确保蓝牙模组产生的中断(如UART接收中断、模组GPIO中断)对应的外部中断线(EXTI)和NVIC中断都已使能,并且被配置为可以唤醒当前的低功耗模式(如Stop模式或Sleep模式)。

其次,进入休眠前的状态保存与清理。在调用hal_cpu_enable_irqs_and_sleep()之前,你需要:

  • 确保所有必要的唤醒中断已使能。
  • 关闭或配置好可能产生不必要中断的外设(比如一个用于用户按钮但当前不需要的EXTI)。
  • 如果使用DMA,要了解在低功耗模式下DMA是否还能工作。有些MCU在深度睡眠下DMA会停止,这意味着你不能指望DMA在休眠期间接收数据。对于蓝牙这种需要随时响应的设备,通常MCU只能进入浅度睡眠(Sleep模式),此时外设和DMA时钟仍在运行。

最关键的陷阱在于中断处理函数与休眠函数的竞争条件。考虑这个场景:

  1. 主循环准备进入休眠,正在执行hal_cpu_enable_irqs_and_sleep()中的代码。
  2. 此时,一个蓝牙数据到达中断发生,但CPU尚未执行WFI指令。
  3. 中断服务程序(ISR)被触发,处理数据,并可能设置了某个“有任务待处理”的标志。
  4. ISR返回后,CPU继续执行,最终进入了WFI休眠。
  5. 因为中断已经处理完毕,没有新的中断到来,CPU将一直休眠,尽管有任务标志被设置,但主循环不会再被唤醒执行。

解决方案是使用“原子性”的进入休眠操作:

void hal_cpu_enable_irqs_and_sleep(void) {
    __disable_irq(); // 先关闭全局中断
    if (no_pending_work()) { // 检查在关闭中断前,是否有工作标志被设置
        __enable_irq(); // 如果没有,立即开启中断
        __WFI();        // 然后立即执行休眠指令
    } else {
        __enable_irq(); // 如果有工作,直接开启中断,不进入休眠
    }
}

这段代码的关键在于,检查“是否有待处理工作”和“执行WFI”这两个操作,是在关闭全局中断的窗口内判断和决定的,避免了竞争条件。当然,no_pending_work()函数需要检查所有可能由中断设置的任务标志。

最后,别忘了在MCU被唤醒后,可能需要重新初始化某些外设的时钟或状态(特别是从Stop等深度睡眠唤醒时),这超出了BTStack HAL的范畴,但直接影响到整个系统的稳定性。

移植BTStack到裸机平台,就像在微型的硬件舞台上编排一场精密的舞蹈。HAL接口就是那些关键的舞步指令。官方文档给了你基本的步法,但舞台的光线、地面的摩擦、舞伴的反应,这些细节只能靠一次次排练(调试)来掌握。希望这些从实际项目泥潭中总结出的细节,能帮你照亮那些文档未曾提及的角落,让你的蓝牙设备跑得更稳、更省电。每当我调通一个坑,看到设备稳定地广播、连接、传输数据时,都会觉得那些对着逻辑分析仪和调试器的深夜是值得的。嵌入式开发的乐趣,不就在于这种与硬件直接对话、解决具体问题的成就感吗?如果你在移植中也遇到了独特的挑战,欢迎分享你的故事。

STM32F103裸机移植BTStack实战:从CSR8311模组驱动到SPP通信全流程 本文详细介绍了在STM32F103裸机环境下移植BTStack蓝牙协议栈的全过程,从CSR8311模组驱动开发到SPP通信实现。通过源码分析、硬件连接设计、DMA串口驱动优化等实战步骤,帮助开发者掌握蓝牙协议栈在单片机上的移植技巧,解决裸机环境下的常见问题。 阅读详情

相关推荐

超轻量级开源蓝牙协议栈btstack在STM32上的实战指南

本文详细介绍了超轻量级开源蓝牙协议栈btstack在STM32上的实战应用。通过解析btstack的模块化设计和事件驱动模型,提供硬件环境搭建、协议栈移植步骤及典型应用场景实现,帮助开发者高效完成蓝牙协议栈开发,特别适合资源受限的嵌入式系统。

red88的博客 753

BTstack跨平台开发:从ESP32到STM32的完整移植指南

BTstack是一款轻量级双模蓝牙协议栈,专为嵌入式系统设计,具有极小的内存占用和出色的跨平台兼容性。本指南将带你了解如何将BTstack从ESP32无缝移植到STM32平台,掌握蓝牙协议栈在不同嵌入式环境下的适配技巧,轻松实现跨硬件平台的蓝牙功能开发。 ## 一、BTstack架构解析:跨平台移植的基础 BTstack的模块化设计是其实现跨平台移植的核心优势。从架构图可以清晰看到,协议栈分为

gitblog_00216的博客 417

btstack移植到stm32f103问题记录

通过打断点、查看变量值发现经常出现期望接收size个数据,但只接收了size-1个字节,也就是少了一个字节数据,所以导致不会产生DMA中断,这个问题还需继续排查,解决(2月17日晚,经排查发现在波特率较高时,串口DMA经常接收数据不全,待证明协议栈是因为使用了HAL库导致效率较低,而没有及时调用串口DMA接收数据,导致数据丢失)。(这里卡了好久,估计有一天半,主要是之前解决问题的思路不对,总是在驱动那里找问题,找了很久也没发现问题,后来改变思路,在L2cap层找问题,几个小时就发现问题了)。

yyyang88的博客 794

如何快速掌握 BTstack:嵌入式蓝牙开发的终极指南

BTstack 是一款专为小型、资源受限设备打造的开源蓝牙协议栈,以超小内存占用和高度可配置性著称,完美支持 Bluetooth Low Energy (BLE) 与经典蓝牙功能。无论是物联网设备、智能硬件还是嵌入式系统开发,BTstack 都能提供稳定高效的蓝牙通信解决方案,帮助开发者轻松实现跨平台蓝牙应用开发。 ## ???? 为什么选择 BTstack?三大核心优势解析 ### 1. 极致...

gitblog_00332的博客 596

btstack源码分析---基于单片机裸机运行的驱动层架构介绍

基于单片机裸机运行的驱动层架构介绍

yyyang88的博客 2597

用BTStack学蓝牙协议栈:从USB和串口驱动看C语言如何抽象硬件操作

本文深入解析了BTStack硬件抽象层设计,展示了如何通过C语言实现蓝牙协议栈的硬件无关性。文章详细对比了USB和串口驱动的实现方式,揭示了BTStack如何通过分层抽象和统一接口屏蔽硬件差异,为嵌入式蓝牙开发提供高效解决方案。

weixin_30608503的博客 373

深入解析BTStack驱动层架构:单片机裸机环境下的关键实现

本文深入解析了在单片机裸机环境下移植BTStack蓝牙协议栈的核心——驱动层架构。文章结合实战经验,详细剖析了时间管理、中断保护、低功耗休眠以及基于UART/DMA的数据通道等关键接口的实现方法,并提供了源码分析与调试技巧,帮助开发者高效构建稳定、低功耗的嵌入式蓝牙应用。

i1j2k的博客 282

蓝牙系列五:BTStack硬件抽象层与跨平台移植实战解析

本文深入解析了BTStack硬件抽象层(HAL)的设计原理与跨平台移植实战技巧。通过分层架构设计,BTStack实现了蓝牙协议栈与硬件操作的解耦,支持多种平台快速移植。文章详细介绍了硬件抽象层、操作系统抽象层的实现方法,并提供了UART接口适配、Run Loop机制等关键步骤的代码示例,帮助开发者高效完成蓝牙协议栈的跨平台部署。

weixin_30542079的博客 427

【亲测免费】 探索 BTstack:一款强大的蓝牙协议栈

是一个开源、跨平台的 Bluetooth Low Energy (BLE) 和经典蓝牙协议栈,由 BlueKitchen GmbH 开发并维护。这款项目旨在简化蓝牙设备开发,提供给开发者一个易于理解和使用的框架,使得创建创新的蓝牙应用变得更加容易。 ## 技术分析 ### 架构设计 BTstack 的架构设计清晰,分为两层:硬件抽象层(HAL)和蓝牙核心系统服务层。HAL 允许 BTstac...

gitblog_00013的博客 1801

深入对比五大开源蓝牙Host协议栈:从Zephyr、Btstack到BlueZ,开发者该怎么选?

本文深入对比了Zephyr、Btstack、BlueZ、NimBLE和Bluedroid五大开源蓝牙Host协议栈,从架构适配性、资源占用和开发效率三个维度进行技术选型分析。特别针对嵌入式场景,探讨了如何通过协议栈选型节省开发周期并避免技术债务,为开发者提供蓝牙协议栈选择的实用指南。

weixin_30555125的博客 469

从USB到串口:手把手教你用BTStack搞定不同蓝牙模块的硬件抽象层

本文深入解析BTStack开源蓝牙协议栈的硬件抽象层设计,详细讲解如何适配USB和串口等不同接口的蓝牙模块。通过分析`hci_transport_t`和`btstack_uart_block_t`关键结构体,提供从驱动实现到多平台移植的完整解决方案,帮助开发者快速掌握蓝牙模块的硬件适配技巧。

weixin_30698297的博客 256

BTStack事件驱动模型解析:从硬件中断到应用回调,数据如何流动?

本文深入解析了BTStack事件驱动模型,从硬件中断到应用回调的数据流动全过程。通过分析蓝牙协议栈的核心代码和架构设计,揭示了BTStack如何高效处理HCI事件,实现低延迟的数据传输。文章详细介绍了硬件抽象层、运行循环引擎和协议栈回调架构,为嵌入式蓝牙开发提供了实用的性能优化和调试技巧。

weixin_30598225的博客 202

BTStack事件处理机制拆解:从HCI数据包到应用回调的完整链路

本文深入解析了开源蓝牙协议栈BTStack的事件处理机制,详细追踪从HCI数据包到应用回调的完整链路。通过剖析其事件驱动架构、硬件抽象层设计及多级回调系统,揭示BTStack如何实现高效稳定的蓝牙通信,特别适合嵌入式系统开发。文章还提供了性能优化策略和开发者实践指南,帮助开发者深入理解蓝牙协议栈内部工作原理。

weixin_42567497的博客 396

一款便于学习的开源蓝牙协议栈

.

HW-BASE的博客 853

btstack-stm32f1/csr8311移植笔记(3)-hci driver & hci log driver

btstack文件树结构: 可以看出: 1)chipset下是bluekitchen目前所支持的所有bt chip集合 2)platform下是可供选择的跑bluekitchen协议栈的软件平台,比如embedded(即裸机系统),freertos,posix,windows等 3)example下是各种profile和service demo 我们在port下添加stm32f103zet6_csr8x11例程。这样platform对应embedded,chipset对应csr,我们需要做的是实现

weixin_38177846的博客 4139

btstack-stm32f1/csr8311移植笔记(4)-database driver

首先需要搞清楚我们所用硬件的flash配置情况,我们使用的mcu型号为smt32f103zet6,查看stm32开发手册,得知这个型号是大容量产品: 有256 page,page size = 2K,共512K容量的片上flash. #include <stdint.h> #include <string.h> // memcpy #include "hal_flash_bank_stm32.h" #include "stm32f1xx_hal.h" #define STM

weixin_38177846的博客 931

蓝牙系列之BlueDroid与MTK代码分布

标签: 蓝牙bluetoothBlueDroid 2014-10-08 19:59 4867人阅读 评论(3) 收藏 举报  分类: 无线通信与网络子系统(13)  版权声明:本文为博主原创文章,未经博主允许不得转载。 目录: 0    应用层设计相关 一、application Framework 二、Bluetooth server层

kerson的专栏 3739

Solidworks转URDF避坑实录:从履带机器人到ROS仿真的完整配置流程

本文详细介绍了将Solidworks履带机器人模型转换为URDF格式并配置ROS仿真的完整流程,重点解析了坐标系设置、复杂关节定义、插件配置等关键步骤中的常见问题与解决方案,帮助开发者高效实现履带机器人在RVIZ和Gazebo中的精确仿真。

weixin_30693683的博客 457

Bluedroid的结构和代码分布

在android4.2中,Google更换了android的蓝牙协议栈,从Bluez换成Bluedroid,我也是初涉这个方面,顺便记录一下。 http://source.android.com/devices/bluetooth.html android development对于4.3蓝牙的介绍: android提供BlueDroid作为默认的协议栈,BlueDroid分为两

yawnooo的专栏 763

JavaSpringBoot + VUE 相关开发环境配置合集

1.Java环境-JDK安装 参照:Java基础1-环境篇:JDK安装与环境变量配置 Java基础1-环境篇:JDK安装与环境变量配置_第一段代码的博客-优快云博客_jdk 2.Web服务器-Tomcat安装 原创Java服务器Tomcat安装和配置 3.Maven(后面为了项目兼容性,使用了3.0.5版本) 原创Maven安装和配置(Maven3.0.5) 4.nginx 原创nginx轻量级Web服务器安装和配置 5 数据库mysql 原创数据库Mysql安装和配置 ...

第一段代码的博客 3164

终端效率神器:Tmux 安装与使用全指南

终端效率神器:Tmux 安装与使用全指南

身在被窝_心在编程 1360
上一篇: Python+Selenium自动化登录智慧树:从环境配置到实战代码(附常见报错解决)
下一篇: C++排序算法实战:从冒泡到快速排序,哪种更适合你的项目?
java5
博客等级 码龄22年 85粉丝 2103原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值