头文件中联合体、结构体定义位的方法

1.2.1 传统#define 方法
1.2 外设位域结构体方法综述

DSP281x 头文件和外设示例使用位域结构体方法,映射和访问基于F28x 外设寄存器。本节将介绍这种方法,并把它和传统的#define 方法加以比较。

1.2.1 传统#define 方法

C代码访问寄存器的传统方法是使用#define宏为每一个寄存器分配一个地址。例如:

//*****************************************************************************
//传统的头文件
//*****************************************************************************
//存储器映像地址寄存器
#define CPUTIMER0_TIM (volatile unsigned long *)0x0C00
//0xC00定时器0 计数器低位
//0xC01定时器0 计数器高位
#define CPUTIMER0_PRD (volatile unsigned long *)0x0C02
//0xC02定时器0 周期寄存器低位
//0xC03定时器0 周期寄存器高位
#define CPUTIMER0_TCR (volatile unsigned int *)0x0C04
//0xC04定时器0 控制寄存器
//0xC05保留
#define CPUTIMER0_TPR (volatile unsigned int *)0x0C06
//0xC06定时器0 预定标寄存器低位
#define CPUTIMER0_TPRH (volatile unsigned int *)0x0C07
//0xC07定时器0 预定标寄存器高位
同样的#define 方法将在每个外设寄存器上不断重复。甚至对于诸如SCI-A 和SCI-B这样完全相同的外设,每个寄存器都必须被一一分配地址。传统#define 方法有以下显著弊端:

(1)不容易访问寄存器中的位域部分;

(2)不容易在CCS 观察窗内显示位域的值;

(3)不能利用CCS 的自动完成功能;

(4)对于重复的外设,头文件开发者不能获得重复使用的便利。

1.2.2 位域及结构体方法
1.2.2 位域及结构体方法

位域及结构体方法采用C 代码结构体方式,将属于某个指定外设的所有寄存器组成一个集合。通过链接器,每个C 代码结构体就是外设寄存器的内存映射。这一映射允许编译器通过使用CPU 数据页指针(DP)直接访问外设寄存器。另外,多数寄存器都定义了位域,从而使编译器能读取或者操作某个寄存器中的单个位域。

1)外设寄存器结构体

在1.2.1节中,我们使用传统#define方法定义了CPU定时器0寄存器(CPU -Timer0)。本节改为采用C 代码结构体方法将CPU 定时器的寄存器集合在一起,定义同一个CPU定时器0 寄存器。通过使用链接器,将结构体映射到内存CPU-Timer0 寄存器上。

以下代码示例是一个与DSP281x CPU 定时器外设对应的C 代码结构体类型:

//*****************************************************************************
//采用结构体形式的CPU 定时器头文件
//*****************************************************************************
struct CPUTIMER_REGS //仅仅定义了一个结构体类型CPUTIMER_REGS,还没有
//定义变量
{ Uint32 TIM; //定时器计数寄存器
Uint32 PRD; //定时器周期寄存器
Uint16 TCR; //定时器控制寄存器
Uint16 rsvd1; //保留
Uint16 TPR; //定时器预定标寄存器低位
Uint16 TPRH; //定时器预定标寄存器高位
};
该结构体类型由6 个成员组成,前后顺序与它们在内存中的顺序相同。“CPUTIMER_
REGS”代表了一个结构体类型。即它和系统已经定义的标准类型(如int,char 等)一
样可以用来作为定义变量的类型。

注意以下几点:

(1)寄存器名出现的顺序必须与它们在内存中被安排的顺序相同;

(2)在结构体中,通过使用保留变量(rsvd1,rsvd2 等)来预留内存中的保留位置。这种保留结构仅仅用以预留内存中的空间;

(3)Uint16 和Uint32 分别是无符号16 位或者32 位数的类型定义,在DSP281x 中,则用来定义无符号整型和无符号长整型。这样使用起来就方便一些。相应的类型定义声明由DSP281x_Device.h 文件建立。

2)声明可访问寄存器的变量

寄存器结构体类型可被用于声明一个可访问寄存器的变量,对器件的每个外设都采用这一相同的做法,同一种外设的复用外设可以采用同样的结构体类型定义。例如,如果一个器件上有3 个CPU-Timers,可以创建如下所示的3 个具有“struct CPUTIMER_REGS”结构体类型的变量。

//*****************************************************************************
//采用结构体形式的CPU 定时器头文件
//*****************************************************************************
volatile struct CPUTIMER_REGS CpuTimer0Regs; //定义CpuTimer0Regs 是
//一个具有CPUTIMER_REGS
//类型的变量,下同
volatile struct CPUTIMER_REGS CpuTimer1Regs;
volatile struct CPUTIMER_REGS CpuTimer2Regs;
这里,关键字volatile 在变量声明中十分重要。它告诉编译器,这些变量的内容可由硬件改变,并且编译器无须优化使用volatile 变量的代码。

注意:定义结构体类型变量都需要关键字struct。变量CpuTimer0Regs,CpuTimer1-Regs,CpuTimer2Regs 都是具有CPUTIMER_REGS 结构体类型的结构体变量。这里把CPUTIMER_REGS 看做是CPU 定时器的同一种外设,而把CpuTimer0Regs,CpuTimer1-Regs,CpuTimer2Regs 看做是同一外设的3 次复用。每一次复用均包含前一个代码框定义的那些专用寄存器变量。这种定义方式与F28x 外设寄存器的分配相对应。

SPRC097 1.00 版本用头文件方式为F28x 的所有外设提供了一个完整的位域结构体体系。对接触C 语言时间不长的读者,花点时间把它弄懂是非常必要的。

单有上面两个定义是不够的,还必须为3 个CPU 定时器分配数据区。分配给某个定时器在数据区的起始地址必须与系统定义的该定时器的内存地址一致。这可通过#pragmaDATA_SECTION 等指令完成。否则,编译器将其视为普通的结构体类型变量,统一分配数据区,导致对CPU 定时器不能有效地访问。

3)分配专用的数据区

在DSP281x_GlobalVariableDefs.c 文件中(该文件位于DSP281x_Headers\common\source 目录内),通过使用编译器的#pragma DATA_SECTION 指令,与外设寄存器结构体类型相对应的每一个变量都将被分配一个专用的数据区。在下面所示的代码中,变量CpuTimer0Regs 被分配到CpuTimer0RegsFile 的数据区。

//*****************************************************************************
//DSP281x_headers\source\DSP281x_GlobalVariableDefs.c
//*****************************************************************************
//采用#pragma 编译器声明,将CpuTimer0Regs 变量分配到CpuTimer0RegsFile 数据
//区。C 或C++采用不同的#pragma 声明方式。当对一个C++程序进行编译时,编译器自
//动定义__cplusplus。
#ifdef __cplusplus //用于 C++代码
#pragma DATA_SECTION(“CpuTimer0RegsFile”);
#else //用于C 代码
#pragma DATA_SECTION(CpuTimer0Regs,“CpuTimer0RegsFile”);
#endif
Volatile structCPUTIMER_REGS CpuTimer0Regs; //定义CpuTimer0Regs 是一
//个具有CPUTIMER_REGS 类
//型的变量
//#ifdef、#else 及#endif 为预处理器条件编译指令。其中#ifdef 和#else 格式类似
//C 中的if 和else。主要差异为预处理器不能识别标记代码块的大括号"{}",因此使用
//#else(如果需要)和#end if(必须存在)来标记指令块。上面指令的含义为:如果采
//用的是C++,则执行语句#pragma DATA_SECTION(“CpuTimer0RegsFile”);
//倘若采用的是C,则执行语句#pragma DATA_SECTION(CpuTimer0Regs,
//“CpuTimer0RegsFile”);
对器件的每个外设寄存器结构体变量,都会重复这一数据区分配操作。

4)映射到外设寄存器

当每个结构体都分配到自身的数据区之后,通过使用链接命令文件DSP281x_Headers_nonBIOS.cmd,每个数据区都将被直接映射到外设内存映射寄存器上,如以下代码所示:

//*****************************************************************************
//DSP281x_headers\include\DSP281x_Headers_nonBIOS.cmd
//*****************************************************************************
MEMORY /* 定义存储区域。注意:cmd 文件不可以用“//”注释符 /
{
PAGE 1: /
PAGE 1 在cmd 文件中表示数据区 /
CPU_TIMER0 : origin = 0x000C00, length = 0x000008
/
将起始地址为0x0C00,长度为8 个单元的内存区域 /
/
定义为CPU 定时器0 寄存器存储区 /
}
SECTIONS /
分配存储区域 /
{
CpuTimer0RegsFile : > CPU_TIMER0, PAGE = 1
/
将 CpuTimer0RegsFile 段分配到CPU_TIMER0 区域 */
}
通过把变量直接映射到外设寄存器的同一内存地址,用户采用C 代码对寄存器进行访问,只需要通过访问变量中所需的成员即可进行。例如,要对CPU-Timer0TCR 寄存器进行写操作,只需访问CpuTimer0Regs 变量中的TCR 成员,如以下代码所示:

//***********************************************


//用户源文件
//**************************************************


CpuTimer0Regs.TCR.all = TSS_MASK; //访问TCR 寄存器示例

1.2.3 添加位域结构体
1.2.3 添加位域结构体

1)增加位域定义

我们经常需要直接访问寄存器中的某个位域。C281x C/C++头文件及外设示例所涉及的位域结构体方法,为多数片上外设寄存器提供了位域定义。例如,可以为CPU 定时器(CPU-Timer)中的每个寄存器定义一个位域结构体类型。CPU 定时器(CPU-Timer)控制寄存器的位域定义如下所示:

//*****************************************************************************
//DSP281x_headers\include\DSP281x_CpuTimers.h CPU 定时器头文件
//*****************************************************************************
struct TCR_BITS //定义一个TCR_BITS 结构体类型(不是变量)
{ Uint16 rsvd1:4; //3:0 保留,从最低位开始,顺序取位到最高位。取低4 位
Uint16 TSS:1; //4 定时器开始/停止,取第5 位
Uint16 TRB:1; //5 定时器重装,取第6 位
Uint16 rsvd2:4; //9:6 保留,取第7 位到第10 位
Uint16 SOFT:1; //10 仿真模式,取第11 位
Uint16 FREE:1; //11 仿真模式,取第12 位
Uint16 rsvd3:2; //12:13 保留,取第13 位到第14 位
Uint16 TIE:1; //14 输出使能,取第15 位
Uint16 TIF:1; //15 中断标志,取第16 位
};
然后,通过共用体进行声明,以便访问位域结构体定义的各个成员或者16 位或32位寄存器的值。例如,定时器的控制寄存器共用体如下所示:

//*****************************************************************************
//DSP281x_headers\include\DSP281x_CpuTimers.h CPU 定时器头文件
//*****************************************************************************
union TCR_REG //定义共用体类型TCR_REG(不是变量)
{ Uint16 all;
struct TCR_BITS bit; //bit 是一个具有TCR_BITS 结构体类型的变量
};
//all 和bit 是共用体的两个成员,它们都是16 位结构,占用内存的同一单元
一旦每个寄存器的位域结构体类型和共用体的定义都建立起来了,则在CPU 定时器(CPU-Timer)的寄存器结构体类型中,各个成员可通过采用共用体定义的形式重写:

//*****************************************************************************
//DSP281x_headers\include\DSP281x_CpuTimers.h CPU 定时器头文件
//*****************************************************************************
struct CPUTIMER_REGS
{ union TIM_GROUP TIM; //定时器计数寄存器,TIM 是一个具有 TIM_GROUP 共
//用体类型的变量
union PRD_GROUP PRD; //定时器周期寄存器
union TCR_REG TCR; //定时器控制寄存器
Uint16 rsvd1; //保留
union TPR_REG TPR; //定时器预定标寄存器低位
union TPRH_REG TPRH; //定时器预定标寄存器高位
};
现在,既可以通过C 代码以位域的方法访问CpuTimer 寄存器中的某位,也可以对整个寄存器进行访问:

//*****************************************************************************
//用户源文件
//*****************************************************************************
CpuTimer0Regs.TCR.bit.TSS = 1; //访问一个单独的位域的示例
CpuTimer0Regs.TCR.all = TSS_MASK; //访问整个寄存器的示例
采用位域结构体的方法具有以下优点:

(1)无须用户确定掩模值,就可对位域进行操作;

(2)可在CCS 观察窗中看到寄存器和位域的值;

(3)当使用CCS 时,编辑器会提供一张现有结构体/位域成员的列表以供选择。这一功能是CCS 自动完成的,它使编写代码变得更容易,而不必查阅寄存器和位域名文件。

掩模值是指位掩码(位屏蔽码),在下面的代码段中,常数TCR_MASK 是位掩码是用于置位或清除较大字段中的一个特殊位的常数值。

#define TCR_MASK 0x0010

CpuTimer0Regs.TCR.all = TCR_MASK;
2)使用位域时,“读—修改—写”的注意事项当对寄存器中的单个位域进行写操作时,硬件将执行一个读—修改—写的操作,即读出寄存器中的内容,修改单个位域的值及回写整个寄存器。上述操作在F28x 上的单个周期内完成。当发生回写操作时,寄存器内的其他位将被写入读出时所读到的同一个数值。有些寄存器没有采用共用体定义,是因为不推荐采用这种方式访问,也存在一些例外情况,包括:

(1)具有写1 清除位的寄存器,如事件管理标志寄存器;

(2)无论在什么时候访问寄存器,都必须用特殊方式对位进行写入操作的寄存器,如看门狗控制寄存器。

没有位域结构体和共用体定义的寄存器,不使用*.bit 或*.all 名称进行访问,例如:

//*****************************************************************************
//用户源文件
//*****************************************************************************
SysCtrlRegs.WDCR = 0x0068;
3)代码长度考量

采用位域定义访问寄存器,可使代码变得易读、易修改和易维护。当需要对寄存器中单独某位域进行访问或者查询时,使用这种方法也非常有效。然而,值得注意的是:当对一个寄存器进行一定数量的访问时,使用*.bit 位域定义形式进行访问将导致比使用*.all 形式对寄存器进行写操作需要更多的代码,例如:

//*****************************************************************************
//用户源文件
//*****************************************************************************
CpuTimer0Regs.TCR.bit.TSS = 1; //1 = 停止定时器
CpuTimer0Regs.TCR.bit.TRB = 1; //1 = 重装定时器
CpuTimer0Regs.TCR.bit.SOFT = 1; //当SOFT=1 且FREE=1 时,定时器自由运行
CpuTimer2Regs.TCR.bit.FREE = 1;
CpuTimer2Regs.TCR.bit.TIE = 1; //1 = 使能定时器中断
采用上述的方法,可以得到可读性非常强并且易于修改的代码。不足是代码有些长。如果用户更加关心代码的长度,可使用*.all 结构对寄存器进行一次性的写操作。

//*****************************************************************************
//用户源文件
//*****************************************************************************
CpuTimer0Regs.TCR.all = TCR_MASK; //TCR_MASK 可在文件头部用#define 定义

1.2.4 共用体结构体位域的应用实例
1.2.4 共用体结构体位域的应用实例

【例】设count 是一个16 位的无符号整型计数器,最大计数为十六进制0xffff,要求将这个计数值以十六进制半字节的形式分解出来。

对于上述实例通常采用移位的方法求解,而采用共用体结构体位域的方法不需要通过移位运算。以下,对CCS 在头文件中大量使用的共用体结构体位域进行注解。

先定义一个共用体结构体位域:


Uint16 cont,g,s,b,q; //16 位无符号整型变量定义
cont=0xfedc; //对cont 赋值

union //共用体类型定义
{ Uint16 i; //定义i 为16 位无符号整型变量
struct //结构体类型定义
{
Uint16 low:4; //最低4 位在前。从最低4 位开始,取每4 位构成半字节
Uint16 mid0:4;
Uint16 mid1:4;
Uint16 high:4; //最高4 位在后
}HalfByte; //HalfByte 为具有所定义的结构体类型的变量
}Count; //Count为具有所定义的共用体类型的变量
union 定义一个共用体类型,它包含两个成员:一个是16 位无符号整型变量i,另一个是包含4 个半字节变量(low,mid0,mid1,high)的结构体类型。它们占用同一个内存单元,通过对i(Count.i)进行赋值,可以完成对结构体4 个变量的赋值。

上面的程序,在定义共用体类型和结构体类型的同时,直接完成了这两个类型变量的定义,而未定义共用体和结构体类型名。即HalfByte 是一个具有所定义的结构体类型的变量,Count 是一个具有所定义的共用体类型的变量。理解了共用体与结构体之间的关系,下面的赋值指令就清楚了。

Count.i = cont; //对共用体类型成员i 进行赋值

g = Count.HalfByte.low; //将cont 的0~3 位赋值给g,g=0x000c
s = Count.HalfByte.mid0; //将cont 的4~7 位赋值给s,s=0x000d
b = Count.HalfByte.mid1; //将cont 的8~11 位赋值给b,b=0x000e
q = Count.HalfByte.high; //将cont 的12~15 位赋值给q,q=0x000f
通过共用体结构体定义,当对共用体类型成员i 进行赋值时,由于结构体类型变量HalfByte 与i 占用同一个内存单元,因此,也就完成了对HalfByte 的各成员的赋值。

C 语言的共用体结构体位域定义,可以完成对寄存器位域的访问。至于被访问的位域在内存中的具体位置则由编译器安排,编程者可以不必关注。

下面是一个访问寄存器位域的例子,供读者参考。

先建立一个共用体结构体位域定义,将某个寄存器的16 位,从最低位到最高位分别

定义为Bit1,Bit2,…,Bit16。

union //共用体类型定义
{ Uint16 all; //定义all 为16 位无符号整型变量
struct //结构体类型定义
{
Uint16 Bit1:1; //0 位Bit1 取寄存器最低位0 位,以下顺序取1 位直到最高位
Uint16 Bit2:1; //1
Uint16 Bit3:1; //2
Uint16 Bit4:1; //3
Uint16 Bit5:1; //4
Uint16 Bit6:1; //5
Uint16 Bit7:1; //6
Uint16 Bit8:1; //7
Uint16 Bit9:1; //8
Uint16 Bit10:1; //9
Uint16 Bit11:1; //10
Uint16 Bit12:1; //11
Uint16 Bit13:1; //12
Uint16 Bit14:1; //13
Uint16 Bit15:1; //14
Uint16 Bit16:1; //15
}bit; //bit为具有所定义的结构体类型的变量
}CtrlBit; //CtrlBit 为具有所定义的共用体类型的变量
有了上面的定义之后,要访问某一个位或某些位就很容易了。比如要置Bit4,Bit8,Bit12 及Bit16 为1,可用两种方法进行:

方法一:

CtrlBit.bit.Bit4 = 1;
CtrlBit.bit.Bit8 = 1;
CtrlBit.bit.Bit12 = 1;
CtrlBit.bit.Bit16 = 1;
方法二:

CtrlBit.all = 0x8888;

------------------------------------------@TOC

欢迎使用Markdown编辑器

你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。

新的改变

我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:

  1. 全新的界面设计 ,将会带来全新的写作体验;
  2. 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
  3. 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
  4. 全新的 KaTeX数学公式 语法;
  5. 增加了支持甘特图的mermaid语法1 功能;
  6. 增加了 多屏幕编辑 Markdown文章功能;
  7. 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
  8. 增加了 检查列表 功能。

功能快捷键

撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G
查找:Ctrl/Command + F
替换:Ctrl/Command + G

合理的创建标题,有助于目录的生成

直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。

如何改变文本的样式

强调文本 强调文本

加粗文本 加粗文本

标记文本

删除文本

引用文本

H2O is是液体。

210 运算结果是 1024.

插入链接与图片

链接: link.

图片: Alt

带尺寸的图片: Alt

居中的图片: Alt

居中并且带尺寸的图片: Alt

当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。

如何插入一段漂亮的代码片

博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片.

// An highlighted block
var foo = 'bar';

生成一个适合你的列表

  • 项目
    • 项目
      • 项目
  1. 项目1
  2. 项目2
  3. 项目3
  • 计划任务
  • 完成任务

创建一个表格

一个简单的表格是这么创建的:

项目Value
电脑$1600
手机$12
导管$1

设定内容居中、居左、居右

使用:---------:居中
使用:----------居左
使用----------:居右

第一列第二列第三列
第一列文本居中第二列文本居右第三列文本居左

SmartyPants

SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:

TYPEASCIIHTML
Single backticks'Isn't this fun?'‘Isn’t this fun?’
Quotes"Isn't this fun?"“Isn’t this fun?”
Dashes-- is en-dash, --- is em-dash– is en-dash, — is em-dash

创建一个自定义列表

Markdown
Text-to- HTML conversion tool
Authors
John
Luke

如何创建一个注脚

一个具有注脚的文本。2

注释也是必不可少的

Markdown将文本转换为 HTML

KaTeX数学公式

您可以使用渲染LaTeX数学表达式 KaTeX:

Gamma公式展示 Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n1)!nN 是通过欧拉积分

Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t   . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=0tz1etdt.

你可以找到更多关于的信息 LaTeX 数学表达式here.

新的甘特图功能,丰富你的文章

Mon 06 Mon 13 Mon 20 已完成 进行中 计划一 计划二 现有任务 Adding GANTT diagram functionality to mermaid
  • 关于 甘特图 语法,参考 这儿,

UML 图表

可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图:

张三 李四 王五 你好!李四, 最近怎么样? 你最近怎么样,王五? 我很好,谢谢! 我很好,谢谢! 李四想了很长时间, 文字太长了 不适合放在一行. 打量着王五... 很好... 王五, 你怎么样? 张三 李四 王五

这将产生一个流程图。:

链接
长方形
圆角长方形
菱形
  • 关于 Mermaid 语法,参考 这儿,

FLowchart流程图

我们依旧会支持flowchart的流程图:

Created with Raphaël 2.3.0 开始 我的操作 确认? 结束 yes no
  • 关于 Flowchart流程图 语法,参考 这儿.

导出与导入

导出

如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。

导入

如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。


  1. mermaid语法说明 ↩︎

  2. 注脚的解释 ↩︎

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值