单核无操作系统如何实现任务并行运行demo之ardiuno读取MPU6050进行oled显示和控制ws2812B灯阵模式显示

该项目使用Arduino Nano V3构建了一个床头灯,结合MPU6050陀螺仪、OLED显示屏和WS2812B LED灯阵。通过实现无delay的多任务轮询,控制灯阵的不同闪烁模式,如呼吸灯、流水灯和亮度控制。用户可以通过陀螺仪传感器输入调整灯光模式,OLED屏幕实时显示传感器数据。代码中展示了如何用millis()替代delay()实现延迟,以及如何处理多任务并行。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

实物演示视频请转向哔站

1. 起源

一直想做一个多种模式显示的灯阵控制小玩意作为床头灯, 这样每次一个人在乌漆嘛黑的卧室刷手机时能够给自己带来一丝暖意!!! 此外在大约5年前玩单片机的时候正好有过一阵子玩ardiuno, 想来ardiuno的性能足够实现对灯阵的控制要求了.

于是乎我在哔站找了几个视频对于床头灯的DIY方案, 本次借鉴的是利用ESP8266控制ws2812B闪烁模式, 实现手机联网操作的方案. 因为不需要联网只需要手动控制即可, 所以选择了性能跟接口都更好的ardiuno nanoV3, 对于手动控制的想法, 我是想做一个方块, 对于不同的摆放, 会有不同的闪烁模式, 比如呼吸灯, 流水灯, 亮度控制灯.

本项目的核心点是对多任务并行实现, 因为ardiuno本身没操作系统来实现多任务功能, 对此本项目利用millis()来实现多任务轮训的功能. 这是由于每条指令的执行时间近乎不计, 系统时间当部分被delay()函数给消耗掉了(delay函数是独占系统时间), 因此本项目摒弃delay函数, 通过不断读取millis(), 计算时间段, 完成对应任务的延时.

ps: 可能有人会刚, 并行任务不能使用定时器来控制么? 但是我在实践过程中确实发现定时器对于定时扫描MPU6050并将结果显示到OLED上会失效, 至于原因, 希望刚的人能够解释😄.

1.1 DIY清单

硬件

  1. ardiuno nanoV3 (typeC接口带下载的模块)
  2. mpu6050 (陀螺仪, IIC接口, 作为系统输入)
  3. oled (IIC接口, 作为系统输出的屏幕)
  4. ws2812B (IIC接口, 作为系统输出的灯光)

软件

  1. Ardiuno IDE 2.0
  2. 软件库
    //直接在库中搜索下载即可
    #include <Adafruit_MPU6050.h> // mpu6050库
    #include <Adafruit_SSD1306.h> // oled库
    #include <Adafruit_Sensor.h> // 数据格式库
    #include <Adafruit_NeoPixel.h>  // WS2812B库
    

2 ardiuno基础玩法

2.1 串口数据打印以及key输入和led输出

void setup() {
  //设置按键管脚上拉输入模式, 默认为高电平, 触发读取后是低电平
  pinMode(PD2, INPUT_PULLUP);  
  // 初始化自带的LED灯珠引脚
  pinMode(LED_BUILTIN, OUTPUT);
  // 串口初始化
  Serial.begin(115200);
}
bool led_flag = HIGH;
void loop() {
  if(digitalRead(PD2) == LOW) {
	led_flag = !led_flag; 
      Serial.print("串口打印数据: led_flag=");
      Serial.println(led_flag);
  }
  digitalWrite(LED_BUILTIN, led_flag); 
  delay(500);
}

2.2 随机数

因为后面的炫彩模式, 需要用到随机闪烁

void setup() {
	randomSeed(analogRead(0)); // 设置随机数种子, 下次随机会同样的随机数
}
void loop() {
	int i = random(1, 20); // 随机1-20之间
}

2.3 非delay()延时封装实现led闪烁任务

class Flasher {
  int ledPin;
  long OnTime;  // 开的时间长度
  long OffTime;  // 关的时间长度
  int ledState;                  // ledState used to set the LED
  unsigned long previousMillis;  // 开始时刻标记
public:
  Flasher(int pin, long on, long off) {
    ledPin = pin;
    pinMode(ledPin, OUTPUT);
    OnTime = on;
    OffTime = off;
    ledState = LOW;
    previousMillis = 0;
  }
  void Update() { // 当时间长度满足要求, 执行命令
    unsigned long currentMillis = millis();
    if ((ledState == HIGH) && (currentMillis - previousMillis >= OnTime)) {
      ledState = LOW;                  // Turn it off
      previousMillis = currentMillis;  // Remember the time
      digitalWrite(ledPin, ledState);  // Update the actual LED
    }
    else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime)) {
      ledState = HIGH;                 // turn it on
      previousMillis = currentMillis;  // Remember the time
      digitalWrite(ledPin, ledState);  // Update the actual LED
    }
  }
};

Flasher led1(LED_BUILTIN, 123, 400);
Flasher led2(D3, 350, 350); // D3和D4接个灯, 就能看到三个灯同时按照顺序进行闪烁了
Flasher led3(D4, 200, 222);

void setup() {
  Serial.begin(115200);
}

void loop() {
  led1.Update();
  led2.Update();
  led3.Update();
}

2.4 MPU6050通过oled的显示demo

#include <Adafruit_MPU6050.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_Sensor.h>

Adafruit_MPU6050 mpu;
Adafruit_SSD1306 display = Adafruit_SSD1306(128, 32, &Wire);

void setup() {
  Serial.begin(115200);
  // while (!Serial);
  Serial.println("MPU6050 OLED demo");

  if (!mpu.begin()) {
    Serial.println("Sensor init failed");
    while (1)
      yield();
  }
  Serial.println("Found a MPU-6050 sensor");

  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
    Serial.println(F("SSD1306 allocation failed"));
    for (;;)
      ; // Don't proceed, loop forever
  }
  display.display();
  delay(500); // Pause for 2 seconds
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setRotation(0);
}

void loop() {
  sensors_event_t a, g, temp;
  mpu.getEvent(&a, &g, &temp);

  display.clearDisplay();
  display.setCursor(0, 0);

  Serial.print("Accelerometer ");
  Serial.print("X: ");
  Serial.print(a.acceleration.x, 1);
  Serial.print(" m/s^2, ");
  Serial.print("Y: ");
  Serial.print(a.acceleration.y, 1);
  Serial.print(" m/s^2, ");
  Serial.print("Z: ");
  Serial.print(a.acceleration.z, 1);
  Serial.println(" m/s^2");

  display.println("Accelerometer - m/s^2");
  display.print(a.acceleration.x, 1);
  display.print(", ");
  display.print(a.acceleration.y, 1);
  display.print(", ");
  display.print(a.acceleration.z, 1);
  display.println("");

  Serial.print("Gyroscope ");
  Serial.print("X: ");
  Serial.print(g.gyro.x, 1);
  Serial.print(" rps, ");
  Serial.print("Y: ");
  Serial.print(g.gyro.y, 1);
  Serial.print(" rps, ");
  Serial.print("Z: ");
  Serial.print(g.gyro.z, 1);
  Serial.println(" rps");

  display.println("Gyroscope - rps");
  display.print(g.gyro.x, 1);
  display.print(", ");
  display.print(g.gyro.y, 1);
  display.print(", ");
  display.print(g.gyro.z, 1);
  display.println("");

  display.display();
  delay(100);
}

2.5 ws2812Bdemo

#include "Adafruit_NeoPixel.h"  //直接在库中搜索 大约第四个就是
#define PIN A0 // 定义的引脚图
#define NUMWS2812B 16
Adafruit_NeoPixel WS2812B(NUMWS2812B, PIN, NEO_GRB + NEO_KHZ800); //(灯总数,使用引脚,WS2812B一般都是800这个参数不用动)

/* 呼吸灯曲线表 呼吸函数中取得离散点 */
const uint16_t index_wave[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 
                               4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 12, 12, 
                               13, 13, 14, 14, 15, 15, 16, 16, 17, 18, 18, 19, 20, 20, 21, 22, 23, 24, 25, 25, 26, 27, 28, 30, 31, 32, 33, 
                               34, 36, 37, 38, 40, 41, 43, 45, 46, 48, 50, 52, 54, 56, 58, 60, 62, 65, 67, 70, 72, 75, 78, 81, 84, 87, 90, 
                               94, 97, 101, 105, 109, 113, 117, 122, 126, 131, 136, 141, 146, 152, 158, 164, 170, 176, 183, 190, 197, 205, 
                               213, 221, 229, 238, 247, 254, 254, 247, 238, 229, 221, 213, 205, 197, 190, 183, 176, 170, 164, 158, 152, 146, 
                               141, 136, 131, 126, 122, 117, 113, 109, 105, 101, 97, 94, 90, 87, 84, 81, 78, 75, 72, 70, 67, 65, 62, 60, 58, 
                               56, 54, 52, 50, 48, 46, 45, 43, 41, 40, 38, 37, 36, 34, 33, 32, 31, 30, 28, 27, 26, 25, 25, 24, 23, 22, 21, 20, 
                               20, 19, 18, 18, 17, 16, 16, 15, 15, 14, 14, 13, 13, 12, 12, 11, 11, 10, 10, 10, 9, 9, 9, 8, 8, 8, 7, 7, 7, 7, 6, 
                               6, 6, 6, 6, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 
                               2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};



void setup() {
  WS2812B.begin();           // 初始化
  WS2812B.clear();           // 将所有像素初始化为关闭
  // WS2812B.setBrightness(255);// 设置亮度
  WS2812B.setBrightness(145);// 设置亮度
}

void loop() {
  colorful();
  breathing();
}


/********************
   设置灯带中某一个灯的颜色。
   单灯颜色(灯位置,红,绿,蓝)
********************/
void Dan_Deng_Yan_Se(int d, int R, int G, int B) {
    WS2812B.setPixelColor(d - 1, (((R & 0xffffff) << 16) | ((G & 0xffffff) << 8) | B));
    WS2812B.show();
    delay(10);
}


void breathing(){//呼吸灯
  for(int i=0;i<300;i++) {
    WS2812B.setBrightness(index_wave[i]);
    WS2812B.show();
    delay(10);
  }
}

void colorful()  //炫彩光效
{
  int lumm = 40;
  WS2812B.setBrightness(lumm);
  for(int i=0;i<=NUMWS2812B;i++){
    WS2812B.setPixelColor(i+1, WS2812B.Color(255,0,0)); //红
    WS2812B.setPixelColor(i+3, WS2812B.Color(255,255,0)); //黄
    WS2812B.setPixelColor(i+5, WS2812B.Color(0,255,0)); //绿
    WS2812B.setPixelColor(i+7, WS2812B.Color(0,0,255)); //蓝
    WS2812B.setPixelColor(i+9, WS2812B.Color(0,255,255)); //青
    WS2812B.setPixelColor(i+11, WS2812B.Color(255,0,255)); //紫
    WS2812B.show();
    delay(500);
    }
}

3. ardiuno读取MPU6050进行oled显示和控制ws2812B灯阵模式显示

3.1 引脚接线图

在这里插入图片描述

3.2 任务流程图

在这里插入图片描述

3.3 最终的代码

/* 汇总:
引脚 
1. A4A5接oled和mpu6050的硬IIC
2. A0接 彩灯ws2812B 16矩阵灯珠的in

任务:
1. led1: nano自带led的闪烁, 表示程序运行正常
2. task1: 不断读取更新mpu6050数据, 通过oled显示
3. task2: 不断读取mpu6050数据, 根据数据变化选择不同的灯光模式
4. Flasher_ws2812B: 设置灯光模式
*/


#include <Adafruit_MPU6050.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_NeoPixel.h"  //直接在库中搜索 大约第四个就是

#define PIN_WS2812B A0 // 这里如果是
#define NUMWS2812B 16
Adafruit_NeoPixel WS2812B(NUMWS2812B, PIN_WS2812B, NEO_GRB + NEO_KHZ800); //(灯总数,使用引脚,WS2812B一般都是800这个参数不用动)
Adafruit_MPU6050 mpu;
Adafruit_SSD1306 display = Adafruit_SSD1306(128, 32, &Wire);


class Flasher_led {
  // Class Member Variables
  // These are initialized at startup
  int ledPin;
  long OnTime;
  long OffTime;  // the number of the LED pin
  // milliseconds of on-time
  // milliseconds of off-time
  // These maintain the current state
  int ledState;                  // ledState used to set the LED
  unsigned long previousMillis;  // will store last time LED was updated
  // Constructor - creates a Flasher
  // and initializes the member variables and state
public:
  Flasher_led(int pin, long on, long off) {
    ledPin = pin;
    pinMode(ledPin, OUTPUT);
    OnTime = on;
    OffTime = off;
    ledState = LOW;
    previousMillis = 0;
  }
  void Update() {
    // check to see if it's time to change the state of the LED
    unsigned long currentMillis = millis();
    if ((ledState == HIGH) && (currentMillis - previousMillis >= OnTime)) {
      ledState = LOW;                  // Turn it off
      previousMillis = currentMillis;  // Remember the time
      digitalWrite(ledPin, ledState);  // Update the actual LED
    }
    else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime)) {
      ledState = HIGH;                 // turn it on
      previousMillis = currentMillis;  // Remember the time
      digitalWrite(ledPin, ledState);  // Update the actual LED
    }
  }
};

Flasher_led led1(LED_BUILTIN, 123, 400);



sensors_event_t a, g, temp;
void display_mpu6050_info() {
  mpu.getEvent(&a, &g, &temp);
  
  Serial.print("Temperature: ");
  Serial.print(temp.temperature);
  Serial.println(" degC");

  display.clearDisplay();
  display.setCursor(0, 0);

  Serial.print("Accelerometer ");
  Serial.print("X: ");
  Serial.print(a.acceleration.x, 1);
  Serial.print(" m/s^2, ");
  Serial.print("Y: ");
  Serial.print(a.acceleration.y, 1);
  Serial.print(" m/s^2, ");
  Serial.print("Z: ");
  Serial.print(a.acceleration.z, 1);
  Serial.println(" m/s^2");

  display.println("Accelerometer - m/s^2");
  display.print(a.acceleration.x, 1);
  display.print(", ");
  display.print(a.acceleration.y, 1);
  display.print(", ");
  display.print(a.acceleration.z, 1);
  display.println("");

  Serial.print("Gyroscope ");
  Serial.print("X: ");
  Serial.print(g.gyro.x, 1);
  Serial.print(" rps, ");
  Serial.print("Y: ");
  Serial.print(g.gyro.y, 1);
  Serial.print(" rps, ");
  Serial.print("Z: ");
  Serial.print(g.gyro.z, 1);
  Serial.println(" rps");

  display.println("Gyroscope - rps: temp");
  display.print(g.gyro.x, 1);
  display.print(", ");
  display.print(g.gyro.y, 1);
  display.print(", ");
  display.print(g.gyro.z, 1);
  display.print("; ");
  display.print(temp.temperature, 1);
  display.println("");

  display.display();
} 

// 每个任务的轮询延长时间, 比如让oled和mpu6050不停轮询更新数据
class Flasher_task {
  long OnTime;
  unsigned long previousMillis;  // will store last time LED was updated
  void (*func)();
public:
  Flasher_task(void (*task)(), long on) {
    OnTime = on;
    previousMillis = 0;
    func = task;
  }
  void Update() {
    unsigned long currentMillis = millis();
    if (currentMillis - previousMillis >= OnTime) {
      previousMillis = currentMillis;  // Remember the time
      func();
    }
  }
};
Flasher_task task1(display_mpu6050_info,150); // 让oled和mpu6050不停轮询更新数据

class Flasher_ws2812B {
  unsigned char brightness; // 亮度
  unsigned long previousMillis_colorful; 
  unsigned long previousMillis_breathing; 
  unsigned int delay_color; // 炫彩灯延时时间
  unsigned int delay_breath; // 呼吸灯延时时间
  int breath_i=2; // 不然1-(-5) = 655555了就
  bool breath_flag=true;

public:
  Flasher_ws2812B(unsigned int delay_color=100, // 炫光延时
                  unsigned int delay_breath=10,  // 呼吸灯延时
                  unsigned char brightness=20) {  // 默认灯亮度
    this->brightness = brightness;
    this->delay_color = delay_color;
    this->delay_breath = delay_breath;

    this->previousMillis_breathing = 0;
    this->previousMillis_colorful = 0;
  }

  static void init() {
    WS2812B.begin();           // 初始化
    WS2812B.clear();           // 将所有像素初始化为关闭
    WS2812B.setBrightness(145);// 设置亮度
  }

  // 设置某一个灯的 RGB 红绿蓝
  void set_one_led_color(int d, int R, int G, int B) {
    WS2812B.setPixelColor(d - 1, (((R & 0xffffff) << 16) | ((G & 0xffffff) << 8) | B));
    WS2812B.show();
  } 

  void breathing() {  //呼吸灯
    unsigned long currentMillis = millis();
    if (currentMillis - this->previousMillis_breathing >= delay_breath) {
      this->previousMillis_breathing = currentMillis;  
      WS2812B.setBrightness(breath_i);// 设置亮度
      WS2812B.show();
      Serial.print(breath_i);
      Serial.print("---");
      Serial.println(breath_flag);
      if(breath_flag) {
        breath_i+=3;
        if(breath_i >100) breath_i+=20;
        if(breath_i >=255) {
          breath_i = 254;
          breath_flag=false;
        }
      }else{
        breath_i-=3;
        if(breath_i >100) breath_i-=20;
        if(breath_i <2) {
          breath_i = 2;
          breath_flag=true;
        }
      }
    }
  }


  void colorful_display() {
    unsigned long currentMillis = millis();
    if (currentMillis - this->previousMillis_colorful >= delay_color) {
      this->previousMillis_colorful = currentMillis; 
      this->colorful();
    }
  }
  // http://t.csdn.cn/3SXbb
  void colorful() { //炫彩光效
    int i = random(1, 20)%NUMWS2812B;
    //红
    WS2812B.setPixelColor(i , WS2812B.Color(255,0,0));
    WS2812B.setPixelColor(i+1, WS2812B.Color(255,0,0));
    //黄
    WS2812B.setPixelColor(i+2, WS2812B.Color(255,255,0));
    WS2812B.setPixelColor(i+3, WS2812B.Color(255,255,0));
    //绿
    WS2812B.setPixelColor(i+4, WS2812B.Color(0,255,0));
    WS2812B.setPixelColor(i+5, WS2812B.Color(0,255,0));
    //蓝
    WS2812B.setPixelColor(i+6, WS2812B.Color(0,0,255));
    WS2812B.setPixelColor(i+7, WS2812B.Color(0,0,255));
    //青
    WS2812B.setPixelColor(i+8, WS2812B.Color(0,255,255));
    WS2812B.setPixelColor(i+9, WS2812B.Color(0,255,255));
    //紫
    WS2812B.setPixelColor(i+10, WS2812B.Color(255,0,255));
    WS2812B.setPixelColor(i+11, WS2812B.Color(255,0,255));
    //紫
    WS2812B.setPixelColor(i-1, WS2812B.Color(255,0,255));
    WS2812B.setPixelColor(i-2, WS2812B.Color(255,0,255));
    //青
    WS2812B.setPixelColor(i-3, WS2812B.Color(0,255,255));
    WS2812B.setPixelColor(i-4, WS2812B.Color(0,255,255));
    //蓝
    WS2812B.setPixelColor(i-5, WS2812B.Color(0,0,255));
    WS2812B.setPixelColor(i-6, WS2812B.Color(0,0,255));
    //绿
    WS2812B.setPixelColor(i-7, WS2812B.Color(0,255,0));
    WS2812B.setPixelColor(i-8, WS2812B.Color(0,255,0));
    //黄
    WS2812B.setPixelColor(i-9, WS2812B.Color(255,255,0));
    WS2812B.setPixelColor(i-10, WS2812B.Color(255,255,0));
    //红
    WS2812B.setPixelColor(i-11, WS2812B.Color(255,0,0));
    //刷新颜色
    WS2812B.show();
  }

  // 设置所有的灯光, 默认设置为所有的灯光为白色
  void set_all_color(int R=255, int G=255, int B=255) {
    for(int i=0; i<NUMWS2812B; i++) {
      WS2812B.setPixelColor(i, (((R & 0xffffff) << 16) | ((G & 0xffffff) << 8) | B));
    } 
    WS2812B.show();
  }
};

Flasher_ws2812B ws2812B(500,15); // 这里其实只是设置了炫光跟呼吸灯的延时时间, 任务是由另一个task2进行调度的

void set_brightness() {
  // 根据x轴变化改变亮度
  int b = a.acceleration.x*10;
  if(b<=0) b=10;
  WS2812B.setBrightness(b);// 设置亮度 
}
// 根据x和y的反应, 显示不同的灯光效应
void select_model() {
  // 根据y轴变化改变闪烁模式
  if(a.acceleration.y <= 3.3) {
    set_brightness();
    ws2812B.colorful_display();  // colorful_display默认就是炫彩+x轴翻转变亮度
  }else if(a.acceleration.y <=6.6) {
    ws2812B.breathing();
  }else {
    set_brightness();
    ws2812B.set_all_color();     // 单纯设置为白色灯光, 亮度未变
  }
}

Flasher_task task2(select_model,200); // 让oled和mpu6050不停轮询更新数据
void setup() {
  pinMode(PD2, INPUT_PULLUP);     //设置按键管脚上拉输入模式, 默认为高电平, 触发读取后是低电平
  randomSeed(analogRead(0)); // 设置随机数
  Serial.begin(115200);

  if (!mpu.begin()) {
    Serial.println("Sensor init failed");
    while (1)
      yield();
  }
  Serial.println("Found a MPU-6050 sensor");

  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
    Serial.println(F("SSD1306 allocation failed"));
    for (;;)
      ; // Don't proceed, loop forever
  }
  display.display();
  delay(500); // Pause for 2 seconds
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setRotation(0);
  
  Flasher_ws2812B::init();
}

void loop() {
  led1.Update();
  task1.Update();
  task2.Update();
}

// 1. 通过x轴翻转控制亮度: 随着翻转角度的增加亮度发生变化
// 2. 通过y轴翻转空灯光模式: 随着翻转角度的模式有变化: 模式1: 纯白, 模式2:炫彩, 模式3 呼吸灯

4. 总结

目前只有上述三种闪光模式(炫彩/呼吸/纯白), 有好想法可以在下方留言

作为并行多任务的一种基础思想, 希望能对读者有所帮助, 感谢阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

落子无悔!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值