Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
okll00
rt-thread
提交
a8f9f85f
R
rt-thread
项目概览
okll00
/
rt-thread
与 Fork 源项目一致
Fork自
RT-Thread / rt-thread
通知
5
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
R
rt-thread
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
a8f9f85f
编写于
8月 19, 2013
作者:
B
Bernard Xiong
浏览文件
操作
浏览文件
下载
差异文件
Merge pull request #143 from grissiom/ringbuffer-pipe
Ringbuffer and Pipe
上级
6f75988c
0340300f
变更
4
隐藏空白更改
内联
并排
Showing
4 changed file
with
261 addition
and
80 deletion
+261
-80
components/drivers/include/rtdevice.h
components/drivers/include/rtdevice.h
+69
-27
components/drivers/src/pipe.c
components/drivers/src/pipe.c
+90
-44
components/drivers/src/ringbuffer.c
components/drivers/src/ringbuffer.c
+97
-4
components/drivers/usb/usbdevice/class/cdc_vcom.c
components/drivers/usb/usbdevice/class/cdc_vcom.c
+5
-5
未找到文件。
components/drivers/include/rtdevice.h
浏览文件 @
a8f9f85f
...
...
@@ -45,9 +45,9 @@ struct rt_ringbuffer
/* use the msb of the {read,write}_index as mirror bit. You can see this as
* if the buffer adds a virtual mirror and the pointers point either to the
* normal or to the mirrored buffer. If the write_index has the same value
* with the read_index, but in
differenct mirro, the buffer is full. While
*
if the write_index and the read_index are the same and within the sam
e
* mirror, the buffer is empty. The ASCII art of the ringbuffer is:
* with the read_index, but in
a different mirror, the buffer is full.
*
While if the write_index and the read_index are the same and within th
e
*
same
mirror, the buffer is empty. The ASCII art of the ringbuffer is:
*
* mirror = 0 mirror = 1
* +---+---+---+---+---+---+---+|+~~~+~~~+~~~+~~~+~~~+~~~+~~~+
...
...
@@ -73,31 +73,22 @@ struct rt_ringbuffer
rt_int16_t
buffer_size
;
};
/** return the size of data in rb */
rt_inline
rt_uint16_t
RT_RINGBUFFER_SIZE
(
struct
rt_ringbuffer
*
rb
)
{
if
(
rb
->
read_index
==
rb
->
write_index
)
{
if
(
rb
->
read_mirror
==
rb
->
write_mirror
)
/* we are in the same side, the ringbuffer is empty. */
return
0
;
else
return
rb
->
buffer_size
;
}
else
{
if
(
rb
->
write_index
>
rb
->
read_index
)
return
rb
->
write_index
-
rb
->
read_index
;
else
return
rb
->
buffer_size
-
(
rb
->
read_index
-
rb
->
write_index
);
}
}
/** return the size of empty space in rb */
#define RT_RINGBUFFER_EMPTY(rb) ((rb)->buffer_size - RT_RINGBUFFER_SIZE(rb))
/* pipe device */
#define PIPE_DEVICE(device) ((struct rt_pipe_device*)(device))
enum
rt_pipe_flag
{
/* both read and write won't block */
RT_PIPE_FLAG_NONBLOCK_RDWR
=
0x00
,
/* read would block */
RT_PIPE_FLAG_BLOCK_RD
=
0x01
,
/* write would block */
RT_PIPE_FLAG_BLOCK_WR
=
0x02
,
/* write to this pipe will discard some data when the pipe is full.
* When this flag is set, RT_PIPE_FLAG_BLOCK_WR will be ignored since write
* operation will always be success. */
RT_PIPE_FLAG_FORCE_WR
=
0x04
,
};
struct
rt_pipe_device
{
struct
rt_device
parent
;
...
...
@@ -105,6 +96,8 @@ struct rt_pipe_device
/* ring buffer in pipe device */
struct
rt_ringbuffer
ringbuffer
;
enum
rt_pipe_flag
flag
;
/* suspended list */
rt_list_t
suspended_read_list
;
rt_list_t
suspended_write_list
;
...
...
@@ -157,28 +150,77 @@ void rt_ringbuffer_init(struct rt_ringbuffer *rb,
rt_size_t
rt_ringbuffer_put
(
struct
rt_ringbuffer
*
rb
,
const
rt_uint8_t
*
ptr
,
rt_uint16_t
length
);
rt_size_t
rt_ringbuffer_put_force
(
struct
rt_ringbuffer
*
rb
,
const
rt_uint8_t
*
ptr
,
rt_uint16_t
length
);
rt_size_t
rt_ringbuffer_putchar
(
struct
rt_ringbuffer
*
rb
,
const
rt_uint8_t
ch
);
rt_size_t
rt_ringbuffer_putchar_force
(
struct
rt_ringbuffer
*
rb
,
const
rt_uint8_t
ch
);
rt_size_t
rt_ringbuffer_get
(
struct
rt_ringbuffer
*
rb
,
rt_uint8_t
*
ptr
,
rt_uint16_t
length
);
rt_size_t
rt_ringbuffer_getchar
(
struct
rt_ringbuffer
*
rb
,
rt_uint8_t
*
ch
);
enum
rt_ringbuffer_state
{
RT_RINGBUFFER_EMPTY
,
RT_RINGBUFFER_FULL
,
/* half full is neither full nor empty */
RT_RINGBUFFER_HALFFULL
,
};
rt_inline
rt_uint16_t
rt_ringbuffer_get_size
(
struct
rt_ringbuffer
*
rb
)
{
RT_ASSERT
(
rb
!=
RT_NULL
);
return
rb
->
buffer_size
;
}
rt_inline
enum
rt_ringbuffer_state
rt_ringbuffer_status
(
struct
rt_ringbuffer
*
rb
)
{
if
(
rb
->
read_index
==
rb
->
write_index
)
{
if
(
rb
->
read_mirror
==
rb
->
write_mirror
)
return
RT_RINGBUFFER_EMPTY
;
else
return
RT_RINGBUFFER_FULL
;
}
return
RT_RINGBUFFER_HALFFULL
;
}
/** return the size of data in rb */
rt_inline
rt_uint16_t
rt_ringbuffer_data_len
(
struct
rt_ringbuffer
*
rb
)
{
switch
(
rt_ringbuffer_status
(
rb
))
{
case
RT_RINGBUFFER_EMPTY
:
return
0
;
case
RT_RINGBUFFER_FULL
:
return
rb
->
buffer_size
;
case
RT_RINGBUFFER_HALFFULL
:
default:
if
(
rb
->
write_index
>
rb
->
read_index
)
return
rb
->
write_index
-
rb
->
read_index
;
else
return
rb
->
buffer_size
-
(
rb
->
read_index
-
rb
->
write_index
);
};
}
/** return the size of empty space in rb */
#define rt_ringbuffer_space_len(rb) ((rb)->buffer_size - rt_ringbuffer_data_len(rb))
/**
* Pipe Device
*/
rt_err_t
rt_pipe_init
(
struct
rt_pipe_device
*
pipe
,
const
char
*
name
,
enum
rt_pipe_flag
flag
,
rt_uint8_t
*
buf
,
rt_size_t
size
);
rt_err_t
rt_pipe_detach
(
struct
rt_pipe_device
*
pipe
);
#ifdef RT_USING_HEAP
rt_err_t
rt_pipe_create
(
const
char
*
name
,
rt_size_t
size
);
rt_err_t
rt_pipe_create
(
const
char
*
name
,
enum
rt_pipe_flag
flag
,
rt_size_t
size
);
void
rt_pipe_destroy
(
struct
rt_pipe_device
*
pipe
);
#endif
...
...
components/drivers/src/pipe.c
浏览文件 @
a8f9f85f
...
...
@@ -26,6 +26,26 @@
#include <rtthread.h>
#include <rtdevice.h>
static
void
_rt_pipe_resume_writer
(
struct
rt_pipe_device
*
pipe
)
{
if
(
!
rt_list_isempty
(
&
pipe
->
suspended_write_list
))
{
rt_thread_t
thread
;
RT_ASSERT
(
pipe
->
flag
&
RT_PIPE_FLAG_BLOCK_WR
);
/* get suspended thread */
thread
=
rt_list_entry
(
pipe
->
suspended_write_list
.
next
,
struct
rt_thread
,
tlist
);
/* resume the write thread */
rt_thread_resume
(
thread
);
rt_schedule
();
}
}
static
rt_size_t
rt_pipe_read
(
rt_device_t
dev
,
rt_off_t
pos
,
void
*
buffer
,
...
...
@@ -39,13 +59,26 @@ static rt_size_t rt_pipe_read(rt_device_t dev,
pipe
=
PIPE_DEVICE
(
dev
);
RT_ASSERT
(
pipe
!=
RT_NULL
);
if
(
!
(
pipe
->
flag
&
RT_PIPE_FLAG_BLOCK_RD
))
{
level
=
rt_hw_interrupt_disable
();
read_nbytes
=
rt_ringbuffer_get
(
&
(
pipe
->
ringbuffer
),
buffer
,
size
);
/* if the ringbuffer is empty, there won't be any writer waiting */
if
(
read_nbytes
)
_rt_pipe_resume_writer
(
pipe
);
rt_hw_interrupt_enable
(
level
);
return
read_nbytes
;
}
thread
=
rt_thread_self
();
/* current context checking */
RT_DEBUG_NOT_IN_INTERRUPT
;
do
{
do
{
level
=
rt_hw_interrupt_disable
();
read_nbytes
=
rt_ringbuffer_get
(
&
(
pipe
->
ringbuffer
),
buffer
,
size
);
if
(
read_nbytes
==
0
)
...
...
@@ -60,23 +93,8 @@ static rt_size_t rt_pipe_read(rt_device_t dev,
}
else
{
if
(
!
rt_list_isempty
(
&
pipe
->
suspended_write_list
))
{
/* get suspended thread */
thread
=
rt_list_entry
(
pipe
->
suspended_write_list
.
next
,
struct
rt_thread
,
tlist
);
/* resume the write thread */
rt_thread_resume
(
thread
);
rt_hw_interrupt_enable
(
level
);
rt_schedule
();
}
else
{
rt_hw_interrupt_enable
(
level
);
}
_rt_pipe_resume_writer
(
pipe
);
rt_hw_interrupt_enable
(
level
);
break
;
}
}
while
(
read_nbytes
==
0
);
...
...
@@ -84,7 +102,30 @@ static rt_size_t rt_pipe_read(rt_device_t dev,
return
read_nbytes
;
}
struct
rt_pipe_device
*
_pipe
=
RT_NULL
;
static
void
_rt_pipe_resume_reader
(
struct
rt_pipe_device
*
pipe
)
{
if
(
pipe
->
parent
.
rx_indicate
)
pipe
->
parent
.
rx_indicate
(
&
pipe
->
parent
,
rt_ringbuffer_data_len
(
&
pipe
->
ringbuffer
));
if
(
!
rt_list_isempty
(
&
pipe
->
suspended_read_list
))
{
rt_thread_t
thread
;
RT_ASSERT
(
pipe
->
flag
&
RT_PIPE_FLAG_BLOCK_RD
);
/* get suspended thread */
thread
=
rt_list_entry
(
pipe
->
suspended_read_list
.
next
,
struct
rt_thread
,
tlist
);
/* resume the read thread */
rt_thread_resume
(
thread
);
rt_schedule
();
}
}
static
rt_size_t
rt_pipe_write
(
rt_device_t
dev
,
rt_off_t
pos
,
const
void
*
buffer
,
...
...
@@ -97,16 +138,32 @@ static rt_size_t rt_pipe_write(rt_device_t dev,
pipe
=
PIPE_DEVICE
(
dev
);
RT_ASSERT
(
pipe
!=
RT_NULL
);
if
(
_pipe
==
RT_NULL
)
_pipe
=
pipe
;
if
((
pipe
->
flag
&
RT_PIPE_FLAG_FORCE_WR
)
||
!
(
pipe
->
flag
&
RT_PIPE_FLAG_BLOCK_WR
))
{
level
=
rt_hw_interrupt_disable
();
if
(
pipe
->
flag
&
RT_PIPE_FLAG_FORCE_WR
)
write_nbytes
=
rt_ringbuffer_put_force
(
&
(
pipe
->
ringbuffer
),
buffer
,
size
);
else
write_nbytes
=
rt_ringbuffer_put
(
&
(
pipe
->
ringbuffer
),
buffer
,
size
);
_rt_pipe_resume_reader
(
pipe
);
rt_hw_interrupt_enable
(
level
);
return
write_nbytes
;
}
thread
=
rt_thread_self
();
/* current context checking */
RT_DEBUG_NOT_IN_INTERRUPT
;
do
{
do
{
level
=
rt_hw_interrupt_disable
();
write_nbytes
=
rt_ringbuffer_put
(
&
(
pipe
->
ringbuffer
),
buffer
,
size
);
if
(
write_nbytes
==
0
)
...
...
@@ -122,26 +179,11 @@ static rt_size_t rt_pipe_write(rt_device_t dev,
}
else
{
if
(
!
rt_list_isempty
(
&
pipe
->
suspended_read_list
))
{
/* get suspended thread */
thread
=
rt_list_entry
(
pipe
->
suspended_read_list
.
next
,
struct
rt_thread
,
tlist
);
/* resume the read thread */
rt_thread_resume
(
thread
);
rt_hw_interrupt_enable
(
level
);
rt_schedule
();
}
else
{
rt_hw_interrupt_enable
(
level
);
}
_rt_pipe_resume_reader
(
pipe
);
rt_hw_interrupt_enable
(
level
);
break
;
}
}
while
(
write_nbytes
==
0
);
}
while
(
write_nbytes
==
0
);
return
write_nbytes
;
}
...
...
@@ -157,6 +199,7 @@ static rt_err_t rt_pipe_control(rt_device_t dev, rt_uint8_t cmd, void *args)
*
* @param pipe the pipe device
* @param name the name of pipe device
* @param flag the attribute of the pipe device
* @param buf the buffer of pipe device
* @param size the size of pipe device buffer
*
...
...
@@ -164,6 +207,7 @@ static rt_err_t rt_pipe_control(rt_device_t dev, rt_uint8_t cmd, void *args)
*/
rt_err_t
rt_pipe_init
(
struct
rt_pipe_device
*
pipe
,
const
char
*
name
,
enum
rt_pipe_flag
flag
,
rt_uint8_t
*
buf
,
rt_size_t
size
)
{
...
...
@@ -177,6 +221,8 @@ rt_err_t rt_pipe_init(struct rt_pipe_device *pipe,
/* initialize ring buffer */
rt_ringbuffer_init
(
&
pipe
->
ringbuffer
,
buf
,
size
);
pipe
->
flag
=
flag
;
/* create pipe */
pipe
->
parent
.
type
=
RT_Device_Class_Char
;
pipe
->
parent
.
init
=
RT_NULL
;
...
...
@@ -204,7 +250,7 @@ rt_err_t rt_pipe_detach(struct rt_pipe_device *pipe)
RTM_EXPORT
(
rt_pipe_detach
);
#ifdef RT_USING_HEAP
rt_err_t
rt_pipe_create
(
const
char
*
name
,
rt_size_t
size
)
rt_err_t
rt_pipe_create
(
const
char
*
name
,
enum
rt_pipe_flag
flag
,
rt_size_t
size
)
{
rt_uint8_t
*
rb_memptr
=
RT_NULL
;
struct
rt_pipe_device
*
pipe
=
RT_NULL
;
...
...
@@ -223,7 +269,7 @@ rt_err_t rt_pipe_create(const char *name, rt_size_t size)
return
-
RT_ENOMEM
;
}
return
rt_pipe_init
(
pipe
,
name
,
rb_memptr
,
size
);
return
rt_pipe_init
(
pipe
,
name
,
flag
,
rb_memptr
,
size
);
}
RTM_EXPORT
(
rt_pipe_create
);
...
...
components/drivers/src/ringbuffer.c
浏览文件 @
a8f9f85f
...
...
@@ -44,6 +44,9 @@ void rt_ringbuffer_init(struct rt_ringbuffer *rb,
}
RTM_EXPORT
(
rt_ringbuffer_init
);
/**
* put a block of data into ring buffer
*/
rt_size_t
rt_ringbuffer_put
(
struct
rt_ringbuffer
*
rb
,
const
rt_uint8_t
*
ptr
,
rt_uint16_t
length
)
...
...
@@ -53,7 +56,7 @@ rt_size_t rt_ringbuffer_put(struct rt_ringbuffer *rb,
RT_ASSERT
(
rb
!=
RT_NULL
);
/* whether has enough space */
size
=
RT_RINGBUFFER_EMPTY
(
rb
);
size
=
rt_ringbuffer_space_len
(
rb
);
/* no space */
if
(
size
==
0
)
...
...
@@ -88,6 +91,59 @@ rt_size_t rt_ringbuffer_put(struct rt_ringbuffer *rb,
}
RTM_EXPORT
(
rt_ringbuffer_put
);
/**
* put a block of data into ring buffer
*
* When the buffer is full, it will discard the old data.
*/
rt_size_t
rt_ringbuffer_put_force
(
struct
rt_ringbuffer
*
rb
,
const
rt_uint8_t
*
ptr
,
rt_uint16_t
length
)
{
enum
rt_ringbuffer_state
old_state
;
RT_ASSERT
(
rb
!=
RT_NULL
);
old_state
=
rt_ringbuffer_status
(
rb
);
if
(
length
>
rb
->
buffer_size
)
length
=
rb
->
buffer_size
;
if
(
rb
->
buffer_size
-
rb
->
write_index
>
length
)
{
/* read_index - write_index = empty space */
memcpy
(
&
rb
->
buffer_ptr
[
rb
->
write_index
],
ptr
,
length
);
/* this should not cause overflow because there is enough space for
* length of data in current mirror */
rb
->
write_index
+=
length
;
if
(
old_state
==
RT_RINGBUFFER_FULL
)
rb
->
read_index
=
rb
->
write_index
;
return
length
;
}
memcpy
(
&
rb
->
buffer_ptr
[
rb
->
write_index
],
&
ptr
[
0
],
rb
->
buffer_size
-
rb
->
write_index
);
memcpy
(
&
rb
->
buffer_ptr
[
0
],
&
ptr
[
rb
->
buffer_size
-
rb
->
write_index
],
length
-
(
rb
->
buffer_size
-
rb
->
write_index
));
/* we are going into the other side of the mirror */
rb
->
write_mirror
=
~
rb
->
write_mirror
;
rb
->
write_index
=
length
-
(
rb
->
buffer_size
-
rb
->
write_index
);
if
(
old_state
==
RT_RINGBUFFER_FULL
)
{
rb
->
read_mirror
=
~
rb
->
read_mirror
;
rb
->
read_index
=
rb
->
write_index
;
}
return
length
;
}
RTM_EXPORT
(
rt_ringbuffer_put_force
);
/**
* get data from ring buffer
*/
...
...
@@ -100,7 +156,7 @@ rt_size_t rt_ringbuffer_get(struct rt_ringbuffer *rb,
RT_ASSERT
(
rb
!=
RT_NULL
);
/* whether has enough data */
size
=
RT_RINGBUFFER_SIZE
(
rb
);
size
=
rt_ringbuffer_data_len
(
rb
);
/* no data */
if
(
size
==
0
)
...
...
@@ -143,7 +199,7 @@ rt_size_t rt_ringbuffer_putchar(struct rt_ringbuffer *rb, const rt_uint8_t ch)
RT_ASSERT
(
rb
!=
RT_NULL
);
/* whether has enough space */
if
(
!
RT_RINGBUFFER_EMPTY
(
rb
))
if
(
!
rt_ringbuffer_space_len
(
rb
))
return
0
;
rb
->
buffer_ptr
[
rb
->
write_index
]
=
ch
;
...
...
@@ -163,6 +219,43 @@ rt_size_t rt_ringbuffer_putchar(struct rt_ringbuffer *rb, const rt_uint8_t ch)
}
RTM_EXPORT
(
rt_ringbuffer_putchar
);
/**
* put a character into ring buffer
*
* When the buffer is full, it will discard one old data.
*/
rt_size_t
rt_ringbuffer_putchar_force
(
struct
rt_ringbuffer
*
rb
,
const
rt_uint8_t
ch
)
{
enum
rt_ringbuffer_state
old_state
;
RT_ASSERT
(
rb
!=
RT_NULL
);
old_state
=
rt_ringbuffer_status
(
rb
);
rb
->
buffer_ptr
[
rb
->
write_index
]
=
ch
;
/* flip mirror */
if
(
rb
->
write_index
==
rb
->
buffer_size
-
1
)
{
rb
->
write_mirror
=
~
rb
->
write_mirror
;
rb
->
write_index
=
0
;
if
(
old_state
==
RT_RINGBUFFER_FULL
)
{
rb
->
read_mirror
=
~
rb
->
read_mirror
;
rb
->
read_index
=
rb
->
write_index
;
}
}
else
{
rb
->
write_index
++
;
if
(
old_state
==
RT_RINGBUFFER_FULL
)
rb
->
read_index
=
rb
->
write_index
;
}
return
1
;
}
RTM_EXPORT
(
rt_ringbuffer_putchar_force
);
/**
* get a character from a ringbuffer
*/
...
...
@@ -171,7 +264,7 @@ rt_size_t rt_ringbuffer_getchar(struct rt_ringbuffer *rb, rt_uint8_t *ch)
RT_ASSERT
(
rb
!=
RT_NULL
);
/* ringbuffer is empty */
if
(
!
RT_RINGBUFFER_SIZE
(
rb
))
if
(
!
rt_ringbuffer_data_len
(
rb
))
return
0
;
/* put character */
...
...
components/drivers/usb/usbdevice/class/cdc_vcom.c
浏览文件 @
a8f9f85f
...
...
@@ -188,7 +188,7 @@ static rt_err_t _ep_in_handler(udevice_t device, uclass_t cls, rt_size_t size)
eps
=
(
cdc_eps_t
)
cls
->
eps
;
level
=
rt_hw_interrupt_disable
();
remain
=
RT_RINGBUFFER_SIZE
(
&
tx_ringbuffer
);
remain
=
rt_ringbuffer_data_len
(
&
tx_ringbuffer
);
if
(
remain
!=
0
)
{
/* although vcom_in_sending is set in SOF handler in the very
...
...
@@ -453,7 +453,7 @@ static rt_err_t _class_sof_handler(udevice_t device, uclass_t cls)
eps
=
(
cdc_eps_t
)
cls
->
eps
;
size
=
RT_RINGBUFFER_SIZE
(
&
tx_ringbuffer
);
size
=
rt_ringbuffer_data_len
(
&
tx_ringbuffer
);
if
(
size
==
0
)
return
-
RT_EFULL
;
...
...
@@ -611,7 +611,7 @@ static int _vcom_putc(struct rt_serial_device *serial, char c)
* data out soon. But we cannot rely on that and if we wait to long, just
* return. */
for
(
cnt
=
500
;
RT_RINGBUFFER_EMPTY
(
&
tx_ringbuffer
)
==
0
&&
cnt
;
rt_ringbuffer_space_len
(
&
tx_ringbuffer
)
==
0
&&
cnt
;
cnt
--
)
{
/*rt_kprintf("wait for %d\n", cnt);*/
...
...
@@ -628,7 +628,7 @@ static int _vcom_putc(struct rt_serial_device *serial, char c)
}
level
=
rt_hw_interrupt_disable
();
if
(
RT_RINGBUFFER_EMPTY
(
&
tx_ringbuffer
))
if
(
rt_ringbuffer_space_len
(
&
tx_ringbuffer
))
{
rt_ringbuffer_putchar
(
&
tx_ringbuffer
,
c
);
}
...
...
@@ -646,7 +646,7 @@ static int _vcom_getc(struct rt_serial_device *serial)
result
=
-
1
;
level
=
rt_hw_interrupt_disable
();
if
(
RT_RINGBUFFER_SIZE
(
&
rx_ringbuffer
))
if
(
rt_ringbuffer_data_len
(
&
rx_ringbuffer
))
{
rt_ringbuffer_getchar
(
&
rx_ringbuffer
,
&
ch
);
result
=
ch
;
...
...
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录