在本篇博客中我们将常用的Verilog系统任务与函数的用法进行总结,并给出一些实用的matlab或Python脚本,这些脚本可以帮助我们在仿真或上板中更好地分析数据
在上一篇博客中已经对仿真时间相关的系统函数进行了分析,因此我们在Verilog 1364-2005的标准中看一下还需要对哪些系统任务和函数进行分析,一般而言,我们经常能用到的如下:
- 显示任务
- 文件I/O任务
- 仿真控制任务
- 转换函数
一些参考的博客与资料如下:
- Verilog中$finish、$stop的使用与区别
- Verilog 其他系统任务
- 7.1 Verilog 显示任务
- 常识:Verilog语法- $display等
- Verilog的系统任务(显示打印类)--$display, $write,$strobe,$monitor
- 【BUG历险记】$fdisplay与$fwrite,存储数据个数不对
- 【芯片前端】verilog语法中的有符号数signed的本质是什么?
- 数字IC秋招面试专题(二)verilog的signed和unsigned
仿真控制任务
仿真控制任务只有两种:
$finish(type);:结束仿真,参数type可选择退出仿真时是否打印信息- type=0:直接退出不打印
- type=1:打印仿真时间和该语句所在的位置行信息,同时也是默认值
- type=2:打印仿真时间、位置、存储器和 CPU 时间的使用情况
$stop(type);:暂停仿真,使仿真进入一种交互模式,设计者可以在此模式下对设计进行调试,当设计者想要暂停仿真来检查信号的值时,可以使用$stop;,用法格式与$finish相同
测试代码如下所示,需要注意的是这两个系统任务在不同的编译器下使用会有区别,我这里只在vivado仿真器中进行测试:
`timescale 1ns / 1ps
module simulation_control_test;
initial begin
forever #100 begin
if ($time >= 10000) begin
// test $finish
// $finish(0);
// $finish(1); // equivalent to $finish;
// $finish(2);
// test $stop
// $stop(0);
// $stop(1); // equivalent to $stop;
$stop(2);
end
end
end
endmodule一些仿真结果如下,部分仿真结果中vivado的编辑器有点bug,没有取消注释:
- $finish(0);
- $finish(1);/$finish;
- $finish(2);
- $stop(0);
- $stop(1);或$stop;
- $stop(2);
在上面测试的时候我发现在vivado 2018.2中$stop无论采用什么参数在Tcl console中都没有输出,但是在vivado 2020.2中就可以正常输出,因此上面的图中和$finish相关的是在vivado 2018.2中仿真的,而和$stop相关的是在vivado vivado 2020.2中仿真的
显示任务
显示任务中我们主要用到以下4种:
$display$write$monitor$strobe
$display
$display是用于显示变量、字符串或表达式的主要系统任务,其用法和C语言中的printf函数类似,可以直接打印字符串,也可以在字符串中指定变量的格式对相关变量进行打印,其用法如下:
$display(p1, p2, p3,...,pn);
其中p1,p2等是双引号括起来的字符串、变量或表达式,$display会自动在字符串的结尾处插入一个换行符,因此如果参数列表为空,则$display的效果是显示光标移到下一行
下面我们采用如下的代码进行测试:
// *********** $display *********** //
reg [3:0] num;
initial begin
num = 4'b0001;
$display("display test"); // directly print string
$display("display variable: %b", num); // format string
$display();
$display("string", num, , 1 + 3); // multiple parameters
$display("string", num, 1 + 3); // multiple parameters
$display("string", num, -1 - 3); // multiple parameters
$display("%h", 1 + 3);
$display("%h", -1 - 3);
end仿真结果下图所示:
可以看到,$display打印出了我们需要的东西,但是其中仍有一些地方需要注意,总结如下:
- 如果没有指定格式,$display默认显示是十进制。
$displayb、$displayo以及$displayh显示格式分别为二进制、八进制和十六进制,同样也有$writeb、$writeo、$writeh和$strobeb等 数据的显示宽度是自动按照输出格式进行调整的,经过格式转换以后总是用表达式的最大可能值所占的位数来显示表达式的当前值,在用十进制数格式输出时,输出结果前面的0值用空格带代替,对于其他进制,输出结果前面的0依然显示出来
- 因此上面的仿真结果中num输出为十进制,并对应两位数据01,而1+3这个表达式输出也是十进制,但是对应11位数据----------4(这里-代表空格),这里是因为这个表达式返回的是一个整数类型的变量,且是一个32位的有符号数,而32位有符号数的范围为-2147483648~2147483647,可以发现带上那个负号一共有11位
- 在参数列表中两个相邻的逗号
,,表示加入一个空格 可以对字符串进行格式化,其部分说明如下所示:
格式 显示 %d或%D 用十进制显示变量 %b或%B 用二进制显示变量 %s或%S 显示字符串 %h或%H 用十六进制显示变量 %c或%C 显示ASCII字符 %m或%M 显示层次名 %v或%V 显示线网信号的强度 %o或%O 用八进制显示变量 %t或%T 显示当前时间格式 %e或%E 用科学计数法格式显示实数 %f或%F 用十进制浮点数格式显示实数 %g或%G 用科学计数法或十进制格式显示实数,显示较短的格式 同样可以采用转义字符来表示一些特殊字符
转义字符 描述 \n 换行符 \t 制表符,Tab键 \\ 反斜杠\ \" 双引号 %% 百分号
$write
$wirte的使用方法与$display完全一样,只是前者会在每次显示信息完毕后不会自动换行,而后者会自动换行,因此当输出后不需要换行时,可以使用显示任务$write
采用如下的代码进行测试:
// *********** $write *********** //
reg [3:0] num;
initial begin
num = 4'b0001;
$write("display test"); // directly print string
$write("display variable: %b", num); // format string
$write("string", num, , 1 + 3); // multiple parameters
$write("string", num, 1 + 3); // multiple parameters
$write("string", num, -1 - 3); // multiple parameters
end仿真结果如下图所示:
可见和$display的结果一样,只是没有换行符
$monitor
通过系统函数$monitor,Verilog提供了对信号值变化进行动态监视的手段,只要变量发生了变化,$monitor就会打印显示出对应的信息,其用法如下:
$monitor(p1, p2, p3,...,pn);
参数p1,p2等可以是变量、信号名或双引号括起来的字符串,$monitor对其参数列表的变量值或者信号值进行不间断的监视,当其中任何一个发生变化时,显示所有参数的数值,$monitor只需调用一次即可在整个仿真过程中生效,这一点与$display不同
但是由于$monitor在整个仿真过程中有效,因此在**任意仿真时刻只有一个监视列表有效**,如果用户调用了多个$monitor,则只有最后一次调用生效,前面的调用被覆盖
测试代码如下:
`timescale 1ns / 1ps
// *********** $monitor *********** //
reg [3:0] cnt;
initial begin
cnt = 3;
forever begin
#5;
if (cnt<7) cnt = cnt + 1 ;
end
end
initial begin
$monitor("Counter change to value %d at the time %t", cnt, $time);
end仿真结果如下图所示:
可见每次#5后cnt都会发生变化,首先是设置初始值cnt=3,然后cnt在forever循环中每次加1,直到cnt=7不再发生变化,这里还打印了cnt发生变化的时间,由于这里设置的时间精度为1 ps,而#5对应的是5 ns,因此这里显示的时间都是延迟值乘以1000
$strobe
选通显示由关键字为$strobe的系统任务完成,这个任务与$display任务非常相似,如果其他语句与$display任务在同一个时间单位执行,那么这些语句与$display任务的执行顺序是不确定的,但是**如果使用$strobe,则该语句总是在同一时刻的其它赋值语句执行完成后才执行**,因此$strobe提供了一种同步机制,可以确保所有在同一时钟沿赋值的其它语句在执行完毕后才显示数据
测试代码如下:
// *********** $strobe *********** //
reg [3:0] a ;
initial begin
a = 1 ;
#1;
a <= a + 1 ;
$display("$display excuting result: %d.", a);
$strobe("$strobe excuting result: %d.", a);
#1;
$display();
$display("$display excuting result: %d.", a);
$strobe("$strobe excuting result: %d.", a);
end仿真结果如下图所示:
可以看到对于第一次显示时,非阻塞赋值与$display同时执行,因此$display显示仍为1,而$strobe显示赋值后的值2
再用第二个例子理解一下$strobe,代码如下:
integer i ;
initial begin
for (i=0; i<4; i=i+1) begin
$display("Run times of $display: %d.", i);
$strobe("Run times of $strobe: %d.", i);
end
end仿真结果如下图所示:
$display按照程序结构,循环执行显示操作4次,每次显示都是循环变量i的当前值,而循环是在仿真0时刻执行的,因此$strobe显示的变量值是循环结束时变量的结果,即i=4退出循环后$strobe才会执行,这也体现了$strobe的时刻显示特性
最后我们再通过一个例子理解$strobe,程序如下:
reg a, b, c, d;
reg clk;
always #5 clk = ~ clk;
initial begin
clk = 1;
a = 0;
b = 0;
c = 0;
d = 0;
#5; b = 1;
d = 1;
#10; $stop;
end
always @(posedge clk)
begin
a <= b;
c <= d;
end
always @(posedge clk) begin
$strobe("storbe a = %b, c = %b", a, c);
$display("display a = %b, c = %b", a, c);
end仿真结果如下图所示:
我们看后面的两行,可以发现使用$strobe则时钟上升沿的值在语句a=b和c=d执行完之后才显示,如果使用$display,$display在语句a=b和c=d之前执行,结果显示不同的值
文件I/O任务
文件I/O任务中我们主要关注以下3种:
文件打开/关闭
$fopen$fclose
文件写入
$fdisplay$fwrite$fstrobe$fmonitor
存储器加载
$readmemh$readmemb
文件打开/关闭
$fopen
- 打开文件:文件可以用系统任务
$fopen打开,其用法如下:
$fopen("<name_of_file>");
<file_handle> = $fopen("<name_of_file>", mode);
其中<name_of_file>为打开文件的路径+名字,mode用于指定文件打开的方式
任务$fopen返回一个被称为多通道描述符(multichannel descriptor)的32位值,正确打开时为非零值,打开出错时为零之。多通道描述符中只有1位被设置为1,标准输出有一个多通道描述符,其最低位(第0位)被设置为1,因此标准输出被称为通道0,同时标准输出也是一直开放的。以后对$fopen的每一次调用都会打开一个新的通道,并会返回一个32位的描述符,其中可能设置了第1位、第2位等,最多可设置到第30位,第31位是保留位,同时总为0,通道号与多通道描述符中被设置为1的位相对应,但这也限制为最多31个通过多通道描述符打开输出的文件,同时使用多通道描述符的多个文件也可以通过多通道描述符的按位或(bitwise OR)来操作多个文件
然而必须指出的是,上面关于任务$fopen的返回的描述并不完整,在Verilog 1364-2005标准中提到:
$fopen opens the file specified as the filename argument and returns either a 32-bit multichannel descriptor or a 32-bit file descriptor, determined by the absence or presence of the type argument.
If type is omitted, the file is opened for writing, and a multichannel
descriptor mcd is returned. If type is supplied, the file is opened as specified by the value of type, and a file descriptor fd is returnedThe file descriptor fd is a 32-bit value. The most significant bit (bit 31) of a fd is reserved and shall always be set; this allows implementations of the file input and output functions to determine how the file was opened. The remaining bits hold a small number indicating what file is opened. Three file descriptors are pre-opened; they are STDIN, STDOUT, and STDERR, which have the values 32'h8000_0000, 32'h8000_0001, and 32'h8000_0002, respectively.
Unlike multichannel descriptors, file descriptors cannot be combined via bitwise OR in order to direct output to multiple files. Instead, files are opened via file descriptor for input, output, and both input and output, as well as for append operations, based on the value of type, according to Table 17-7
If a file cannot be opened (either the file does not exist and the type specified is "r", "rb", "r+", "r+b", or "rb+", or the permissions do not allow the file to be opened at that path), a zero is returned for the mcd or fd.
因此$fopen并不仅仅返回多通道描述符,还可能返回文件描述符(file descriptor),这取决于我们选择的mode,当mode省略时会返回多通道描述符,当mode提供时,文件会根据mode来打开文件,并返回文件描述符。文件描述符的特点如下:
- 文件描述符的msb(第31位比特)始终为1,并且有3个文件描述符是预先就打开的,分别是32'h8000_0000、32'h8000_0001以及32'h8000_0002,分别对应STDIN、STDOUT和STDERR
- 和多通道描述符不同,文件描述符不能通过逐位或结合起来以输出给多个文件
- 如果一个文件不能被打开(这里有两种情况,一是文件不存在但是mode选择了r、rb、r+或rb+,二是文件本身可能由于权限不够不能打开),无论是多通道描述符还是文件描述符都会返回0
下面的程序说明了如何使用多通道描述符:
// ************** test $fopen ************** //
integer handle1, handle2, handle3, handle4; // integer data have 32 bits
// standard output is always on: descriptor=32'h0000_0001(0th bit set to 1)
initial
begin
handle1 = $fopen("file1.txt"); // handle1=32'h0000_0002(1st bit set to 1)
handle2 = $fopen("file2.txt"); // handle2=32'h0000_0004(2nd bit set to 1)
handle3 = $fopen("file3.txt"); // handle3=32'h0000_0008(3rd bit set to 1)
handle4 = $fopen("D:/work/FPGA/Verilog test/Verilog_test_project/test_cos.txt"); // handle3=32'h0000_0010(4th bit set to 1)
if (handle1 == 0 || handle2 == 0 || handle3 == 0 || handle4 == 0) begin
$display ("can not open the file!");
$finish;
end
end仿真结果如下:
多通道描述符的优点在于可以有选择地同时写多个文件
文件打开方式mode类型及其描述如下:
| mode类型 | 描述 |
|---|---|
| r | 只读打开一个文本文件,只允许读数据 |
| w | 只写打开一个文本文件,只允许写数据。如果文件存在,则原文件内容会被删除。如果文件不存在,则创建新文件 |
| a | 追加打开一个文本文件,并在文件末尾写数据。如果文件如果文件不存在,则创建新文件 |
| rb | 只写打开一个文本文件,只允许写数据。如果文件存在,则原文件内容会被删除。如果文件不存在,则创建新文件 |
| wb | 只写打开或建立一个二进制文件,只允许写数据。 |
| ab | 追加打开一个二进制文件,并在文件末尾写数据 |
| r+ | 读写打开一个文本文件,允许读和写 |
| w+ | 读写打开或建立一个文本文件,允许读写。如果文件存在,则原文件内容会被删除。如果文件不存在,则创建新文件 |
| a+ | 读写打开一个文本文件,允许读写。如果文件不存在,则创建新文件。读取文件会从文件起始地址的开始,写入只能是追加模式 |
| rb+ | 读写打开一个二进制文本文件,功能与r+类似 |
| wb+ | 读写打开或建立一个二进制文本文件,功能与w+类似 |
| ab+ | 读写打开一个二进制文本文件,功能与a+类似 |
$fclose
- 关闭文件:文件可以用系统任务
$fclose来关闭,其用法如下:
用法:$fclose(<file_descriptor>);
需要注意的是,文件一旦被关闭就不能再写入,多通道描述符中相应的位被设置为0,下一次$fopen的调用可以重用这一位
文件写入
写文件的系统任务主要包括:$fdisplay、$fwrite、$fstrobe以及$fmonitor,用法如下:
$fdisplay(<file_descript or>, p1, p2,..., pn);
$fmonitor(<file_descript or>, p1, p2,..., pn);
$fwrite(<file_descript or>, p1, p2,..., pn);
$fstrobe(<file_descript or>, p1, p2,..., pn);
其中p1, p2,..., pn可以是变量、信号名或带引号的字符串,其中文件描述符可以是一个文件句柄或多个句柄按位的组合,此时Verilog会把输出写入与文件描述符中值为1的位相关联的所有文件,不同系统任务的功能总结如下:
- $fdisplay:按顺序或条件写文件,自动换行
- $fwrite:按顺序或条件写文件,不自动换行
- $fstrobe:语句执行完毕后选通写文件
- $fmonitor:只要数据有变化就写文件
相较于前面介绍的显示任务,这里的文件写入任务除了需要在用法格式上需要多指定文件描述符fd,其余打印条件、时刻特性等均与其对应的显示任务保持一致
我们采用以下的程序进行测试,其中部分程序和上面测试$fopen的程序相同:
// ************** test $fopen ************** //
integer handle1, handle2, handle3, handle4; // integer data have 32 bits
// standard output is always on: descriptor=32'h0000_0001(0th bit set to 1)
initial
begin
handle1 = $fopen("file1.txt"); // handle1=32'h0000_0002(1st bit set to 1)
handle2 = $fopen("file2.txt"); // handle2=32'h0000_0004(2nd bit set to 1)
handle3 = $fopen("file3.txt"); // handle3=32'h0000_0008(3rd bit set to 1)
handle4 = $fopen("D:/work/FPGA/Verilog test/Verilog_test_project/test_cos.txt", "a+"); // handle3=32'h0000_0010(4th bit set to 1)
if (handle1 == 0 || handle2 == 0 || handle3 == 0 || handle4 == 0) begin
$display ("can not open the file!");
$finish;
end
end
// ************** test $fdisplay $fstrobe $fwrite $fmonitor ************** //
integer desc1, desc2, desc3;
reg [3:0] num;
initial
begin
num = 4'b0001;
desc1 = handle1 | 1; // bitwise or, desc1=32'h0000_0003
$fdisplay(desc1, "Display 1", , "%b", num); // write to file1.txt and standard output
$fdisplay(desc1, "Display 1", "%b", num);
desc2 = handle2 | handle1; // bitwise or, desc2=32'h0000_0006
num <= 4'b0010;
$fdisplay(desc2, "Display 2", "%b", num); // write to file1.txt and file2.txt
$fstrobe(desc2, "Display 2", "%b", num);
desc3 = handle3; // desc3=32'h0000_0008
$fdisplay(desc3, "Display 3", "%b", num); // only write to file3.txt
$fwrite(desc3, "%b", num);
$fdisplay(desc3, "Display 3", "%b", num); // only write to file3.txt
end
integer desc4;
reg [3:0] a;
reg [3:0] b;
initial begin
a = 4'd0;
b = 4'd0;
forever begin
#5;
if (a < 5) begin
a = a + 1;
b <= b + 1;
end
end
// repeat (5) begin
// a = a + 1;
// b <= b + 1;
// end
end
initial begin
desc4 = handle4; // desc4=32'h0000_0010
$fmonitor(desc4, "%b ", a, "%b", b);
end
// close file
initial begin
#1000; $fclose(handle1);
$fclose(handle2);
$fclose(handle3);
$fclose(handle4);
end其中handle1、handle2和handle3对应的文件均不存在(文件不存在会创建在<project>.sim/<simset>/<simtype>/xsim路径中),handle4对应的文件输入一些内容,如下图所示:
这里注意一点在仿真过程中打开文件查看,可能会发现没有文件内容,此时需要关闭仿真界面或者在代码中加入$fclose,下面我们对仿真结果进行分析
首先看一下四个文件对应的描述符,如下图所示,可见如何前面所说的,在使用$fopen没有设置mode参数的handle1、handle2和handle3采用的都是多通道描述符,而采用"a+"打开文件的handle4就是文件描述符,第31位比特为1:
接下来我们看一下四个文件在操作完成后写入的内容并进行分析:
- file1.txt以及标准输出:对file1.txt的写入有两次,第一次是和标准输出一起,第二次是和file2.txt一起。第一次采用$fdisplay输出两次数据我们可以看到$fdisplay和$display的用法和结果类似,两个连续的逗号输出为一个空格;第二次的输出是为了测试$fdisplay和$fstrobe的区别,可见$fdisplay输出的是非阻塞赋值前的值,而$fstrobe输出的是赋值后的值
- file2.txt:对file2.txt的输出只有一次,是和file1.txt一起,在前面已经分析过了,file2.txt的内容和file1.txt内容的后半部分一致
- file3.txt:对file3.txt的输出只有一次,是单独输出,这里主要对比$fdisplay和$fwrite的区别,可以看到$fwrite的输出不会自动换行
- test_cos.txt:对test_cos.txt的输出用于测试$fmonitor的功能,由于test_cos.txt是采用"a+"打开的,因此是在.txt文件原本的内容后面写入的
同时也可以看到,在仿真前file1.txt、file2.txt以及file3.txt是没有创建的,而调用$fopen则创建了新文件
下面对代码前面的$fopen部分进行一些修改,加入mode的选择,来测试不同mode的功能:
integer handle1, handle2, handle3;
initial
begin
handle1 = $fopen("file1.txt", "r+");
handle2 = $fopen("file2.txt", "w+");
handle3 = $fopen("file3.txt", "a+");
if (handle1 == 0 || handle2 == 0 || handle3 == 0) begin
$display ("can not open the file!");
$finish;
end
end
integer desc1, desc2, desc3;
reg [3:0] num;
initial
begin
num = 4'b0001;
desc1 = handle1;
$fdisplay(desc1, "Display 1", , "%b", num); // write to file1.txt
$fdisplay(desc1, "Display 1", "%b", num);
desc2 = handle2;
num <= 4'b0010;
$fdisplay(desc2, "Display 2", "%b", num); // write to file2.txt
$fstrobe(desc2, "Display 2", "%b", num);
desc3 = handle3;
$fdisplay(desc3, "Display 3", "%b", num); // write to file3.txt
$fwrite(desc3, "%b", num);
$fdisplay(desc3, "Display 3", "%b", num);
end
// close file
initial begin
#1000; $fclose(handle1);
$fclose(handle2);
$fclose(handle3);
end其中file1.txt和file3.txt不预先创建,file2.txt预先创建并写入一些内容,如下图所示:
经过仿真三个文件的内容如下:
- file1.txt
- file2.txt
- file3.txt
可以看到,mode设置为"w+"时,如果文件存在,则原文件内容会被删除,因此file2.txt原先的内容没有了;而mode设置为"a+"时如果文件不存在,则创建新文件,比如file3.txt;mode设置为"r+"时文件不存在也创建了新文件,比如file1.txt,这一点有些奇怪,和前面提到的Verilog标准中the file does not exist and the type specified is "r", "rb", "r+", "r+b", or "rb+"不符,但可能是仿真器的原因,因此我测试了一下mode选择"r"时的情况,代码如下:
integer handle;
initial begin
handle = $fopen("test.txt", "r");
if (handle == 0) begin
$display ("can not open the file!");
$finish;
end
end仿真结果如下图所示:
可见在文件不预先创建时会出现错误,因此我们可以得出结论,在mode为"r+"时会自动创建文件,而在mode为"r"时必须要保证文件预先被创建
存储器加载
Verilog提供了系统任务来根据数据文件对存储器进行初始化,有两个任务可用于读取二进制数或十六进制数,关键字$readmemb和$readmemh用于初始化存储器,其用法如下:
$readmemb("<file_name>", <memory_name>);
$readmemb("<file_name>", <memory_name>, <start_addr>);
$readmemb("<file_name>", <memory_name>, <start_addr>, <finish_addr>);
$readmemh的用法和$readmemb类似,其中<file_name>和<memory_name>是必需的,<start_addr>和<finish_addr>是可选的,<start_addr>的默认值是存储器数组的开始位置,<finish_addr>的默认值是数据文件或存储器的结束位置,文件内容只应该有空白符(或换行、空格符)、二进制或十六进制数据,注释用"//"进行标注,数据间建议用换行符区分
我们采用下面的程序来测试存储器加载系统任务的用法:
reg [7:0] memory [0:7]; // 8 8bits memory
reg [7:0] array1 [0:3]; // 4 8bits memory
reg [7:0] array2 [0:3]; // 4 8bits memory
integer i1, i2;
initial
begin
$readmemb("init1.dat", memory); // 将数据文件读入存储器中的给定地址
$readmemh("init2.dat", array1);
// display memory after initialization
for(i1=0; i1<8; i1=i1+1)
$display("memory [%0d] = %b", i1, memory[i1]);
for(i2=0; i2<4; i2=i2+1)
$display("array1 [%0d] = %b", i2, array1[i2]);
array2[0][6:3] = 4'b0001;
array2[1][6:3] = 4'b0010;
array2[2][6:3] = 4'b0100;
array2[3][6:3] = 4'b1000;
end其中文件init1.dat和init2.dat包含初始化数据,可以用@<address>在数据文件中指定地址,地址以十六进制数说明,数据用空格符分隔,数据可以包含x或z,未初始化的位置默认值为x
init1.dat文件内容如下:
@002
11111111 01010101
00000000 10101010@006
1111zzzz 00001111
init2.dat文件内容如下:
40
41
42
43
注意init1.dat和init2.dat文件需要放在<project>.sim/<simset>/<simtype>/xsim路径中(除非自己指定详细的路径),仿真结果如下图所示:
可以看到,我们在init1.dat和init2.dat文件中指定的位置和数据都通过$readmemb和$readmemh写入了寄存器,同时可以通过对数组的直接索引来写入数据
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。