打印
[菜农群课笔记]

20120110群课笔记

[复制链接]
2044|5
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
John Lee(1513562323) 20:29:06
我们今天讨论“位对象”的其它操作符函数,临时对象的析构,和代码效率问题。
John Lee(1513562323) 20:31:14
上次我们讲了 write() 函数的参数和返回值,现在我们接着讨论 write() 函数的操作。
John Lee(1513562323) 20:32:18
file:///C:/DOCUME%7E1/ADMINI%7E1/LOCALS%7E1/Temp/moz-screenshot.png
John Lee(1513562323) 20:32:38
模板参数 S 和 W,在定义“位对象”(BRD, DIV_X_ONE等)时,就已经给出了,这是常量。对于 BRD 来说,S = 0,W = 16。
John Lee(1513562323) 20:33:24
S 和 W 指定了这个“位对象”在 baud_t 临时对象的成员 val(寄存器缓冲)中的位域,S 表示起始位,W 表示位宽度。
John Lee(1513562323) 20:34:14
那么 write() 函数里就使用普通的位操作运算,把参数 v 写到 val 对应的位域。
John Lee(1513562323) 20:35:08

John Lee(1513562323) 20:37:04
形参 v 左移 S 位,val 对相应的位域清 0,然后两者“相或”,结果写回 val。
John Lee(1513562323) 20:37:56
就这样,通过对临时对象中的“位对象”一个一个连续访问,就可以对临时对象的 val 中各个位域分别赋予相应的数据。
日期:2012/1/10
John Lee(1513562323) 20:39:37
好,下面我们讲讲如何“读”某一个“位对象”的值。
John Lee(1513562323) 20:40:19
“位对象”有 两个 read() 函数:
1、__INLINE P read() { return P((reinterpret_cast<R*>(this)->val & MASK) >> LSB); }
2、__INLINE R& read(P& v) { v = read(); return *reinterpret_cast<R*>(this); }
John Lee(1513562323) 20:40:35
和一个类型转换操作符函数:
3、__INLINE operator P() { return read(); }
John Lee(1513562323) 20:41:18
其中的 P 和 R 都是模板参数,对于 baud_t 的成员 BRD,我们先回顾一下 BRD 的定义:
sfb_t<baud_t, uint32_t, 0, 16> BRD;
John Lee(1513562323) 20:41:57

John Lee(1513562323) 20:42:20
模板参数 R 就是“baud_t”,P 就是“uint32_t”。
John Lee(1513562323) 20:42:48
我们手工替换一下,得到:

John Lee(1513562323) 20:43:29
1、__INLINE uint32_t read() { return uint32_t((reinterpret_cast<baud_t*>(this)->val & MASK) >> LSB); }
2、__INLINE baud_t& read(uint32_t& v) { v = read(); return *reinterpret_cast<baud_t*>(this); }
3、__INLINE operator uint32_t() { return read(); }

John Lee(1513562323) 20:44:24
第1个函数 uint32_t read() 的实现很简单:读取了 baud_t 临时对象的 val 成员的值,并对这个值进行适当的位运算,然后返回了它。
John Lee(1513562323) 20:44:56
使用方法是这样的:uint32_t brd = UART0.BAUD().BRD.read();
John Lee(1513562323) 20:45:56
第3个函数,类型转换操作符 operator uint32_t() 也只是对 read() 函数的一个包装,使程序读取“位对象”的表达式更简洁优雅一些。
John Lee(1513562323) 20:46:22
使用方法是这样的:uint32_t brd = UART0.BAUD().BRD;
John Lee(1513562323) 20:46:58
但以上两个方法都有一个弱点:无法连续访问“临时对象”中的其它“位对象”了。
John Lee(1513562323) 20:47:41
原因是,这两个方法的返回值类型是 uint32_t,而“连续访问”要求,每一个访问(函数)的返回值,都必须是“临时对象”的引用。
John Lee(1513562323) 20:48:54
如果我们对“临时对象”的操作中,除了一个是“读”以外,其余的都是“写”,那么可以把“读”操作放在“连续操作”的表达式的末尾:
John Lee(1513562323) 20:49:36
// 写 BRD,写 DIV_X_ONE,读 DIVIDER_X:
uint32_t divider = UART0.BAUD().BRD(20).DIV_X_ONE(1).DIVIDER_X.read();
// 或者
uint32_t divider = UART0.BAUD().BRD(20).DIV_X_ONE(1).DIVIDER_X;

John Lee(1513562323) 20:50:55
为了在一个“临时对象”中读取多个“位对象”,我们要使用第2个函数:baud_t& read(uint32_t& v)。
John Lee(1513562323) 20:52:00
这个函数的返回值是 baud_t&,可以连续访问,因为返回值被用作了连续访问,那么读取的“位对象”的值就改到了函数的形参:uint32_t& v。
John Lee(1513562323) 20:52:58
这个形参是个引用类型,大家还记得上次群课中,我们讨论的 C++ 引用的特性吗?
murex(344582199) 20:53:15

John Lee(1513562323) 20:53:27
修改引用就是修改引用对应的变量!
John Lee(1513562323) 20:55:07
所以,函数调用了 read() 后,把得到的值赋值到了形参 v,也就修改了实际的变量,例如:
John Lee(1513562323) 20:56:04
// 读 BRD,写 DIV_X_ONE,读 DIVIDER_X
uint32_t brd, divider;
UART0.BAUD().BRD.read(brd).DIV_X_ONE(1).DIVIDER_X.read(divider);
John Lee(1513562323) 20:57:31
好了,“位对象”的基本操作函数,就讲完了。
murex(344582199) 20:58:03
谢谢老师
John Lee(1513562323) 20:58:42
下面我们讲缓冲数据如何写回硬件寄存器。

John Lee(1513562323) 21:00:05
当我们对“临时对象”中的缓冲数据 val 修改完毕后,需要把 val 的值再写回到硬件寄存器。
John Lee(1513562323) 21:02:04
为此,我们在“临时对象”的类型中,定义了一个成员函数:
baud_t& apply()
{
    if (changed != 0)
        ref = val;
        changed = 0;
    }
    return *this;
}

John Lee(1513562323) 21:03:20
在这个函数中,val 被赋值到 ref,这个 ref 就是硬件寄存器的引用。
John Lee(1513562323) 21:05:25
但何时调用此函数呢?我们先用 C 来模拟整个过程来对照一下:
type union {
    struct {
        uint32_t BRD : 16;
        uint32_t : 8;
        uint32_t DIVIDER_X : 4;
        uint32_t DIV_X_ONE : 1;
        uint32_t DIV_X_EN : 1;
    };
    uint32_t data;
} BAUD_T;
John Lee(1513562323) 21:05:46
typedef struct {
    ...
    BAUD_T BAUD;
    ...
} UART_T;

UART_T* UART0;                        // UART0外设

John Lee(1513562323) 21:08:11
void foo()
{
    BAUD_T baud;                      // 定义baud变量,C中没有临时对象的概念,只能定义一个具名的变量。
    baud.data = UART0->BAUD.data;     // 把硬件寄存器BAUD的数据,赋值到baud变量。
    baud.BRD = 20;                    // 在baud变量中修改BRD
    baud.DIV_X_ONE = 1;               // 和 DIV_X_ONE。
    UART0->BAUD.data = baud.data;     // 修改完成后,把baud变量赋值写回到硬件寄存器BAUD。
}

John Lee(1513562323) 21:09:52
这个程序,大家都没有困难吧?
batsong@21IC(6335473) 21:10:20
没有  
John Lee(1513562323) 21:10:42
上面的C程序中 foo() 函数里的 5 句,效果相当于 C++ 的:
UART0.BAUD().BRD(20).DIV_X_ONE(1);
John Lee(1513562323) 21:11:48
可以看出,C 的实现方法较繁琐,特别是不能忘了最后的“写回”操作,这个不如 C++ 来得优雅和安全。
murex(344582199) 21:12:45
是优雅多了
John Lee(1513562323) 21:12:56
而在 C++ 的方法中,我们并没有在表达式中看到这个“写回”的操作。
murex(344582199) 21:13:03
就是开始的时候实现这个优雅动作繁琐点
John Lee(1513562323) 21:13:54
编译器会“了解”我们的需求,自动写回吗?不可能的,不要指望编译器为我们自动完成这个操作。
John Lee(1513562323) 21:15:53
我们定义的写回操作在 apply() 函数中实现,为了让编译器调用这个函数,我们利用了 C++ 中“临时对象”生存期的概念,以及对象“析构”的概念。
John Lee(1513562323) 21:16:52
【C++ 小知识】:
临时对象的生存时限,限制在其出现的“完整”的表达式中,“完整”的表达式结束了,临时对象也就销毁了。
例外是把临时对象被引用或者初始化给具名对象,临时对象的生存期会加长到引用或者具名对象的生存期。
John Lee(1513562323) 21:19:32
当 UART0.BAUD().BRD(20).DIV_X_ONE(1); 表达式结束时,产生的 baud_t “临时对象”的生存期也就结束了,编译器将销毁这个“临时对象”,
John Lee(1513562323) 21:20:49
如果“临时对象”的类型定义中,含有“析构函数”,那么,“析构函数”会被编译器自动调用。
John Lee(1513562323) 21:22:17
在 baud_t 类型中,我们定义了析构函数:
~baud_t() { apply(); }

John Lee(1513562323) 21:23:00
这个析构函数调用了 apply() 函数。
John Lee(1513562323) 21:25:55
这样,所有的逻辑都连贯在了一起:外设->寄存器->临时对象(缓冲数据)->位对象(修改缓冲数据)......->析构(写回缓冲数据)。
John Lee(1513562323) 21:27:26
为什么不在析构函数中直接写回?而要饶一个弯,调用 apply() 函数来写回?
murex(344582199) 21:29:23
是否就是为了访问多个位,还有就是为了书写结构更加漂亮?
John Lee(1513562323) 21:29:50
因为可能有这样的情况:当修改了某个“位对象”后,逻辑上需要立即生效。

John Lee(1513562323) 21:30:38
这时,我们就可以直接调用 apply() 函数来做这个操作:
// 修改了 BRD 后,需要立即生效。
UART0.BAUD().BRD(20).apply().DIV_X_ONE(1);

相关帖子

沙发
xyz549040622|  楼主 | 2012-1-11 19:54 | 只看该作者
John Lee(1513562323) 22:05:30
好了,耽误大家了,我们最后讨论一下代码效率。
John Lee(1513562323) 22:05:51
在 baud_t 类型中,有 3 个成员:
John Lee(1513562323) 22:06:01
volatile uint32_t& ref;
uint32_t val;
uint32_t changed;

John Lee(1513562323) 22:06:24
在各个“位对象”的类型(sfb_t<baud_t, uint32_t, S, W>)的 write() 成员函数中,也有一大堆运算,有若干变量,还涉及指针。
John Lee(1513562323) 22:06:50
猛一看,效率肯定不高。
John Lee(1513562323) 22:07:32
但实际上不是那样的,C/C++ 中,有一个很重要的编译优化规则:常量叠算。
John Lee(1513562323) 22:07:46
【编译器优化小知识】:常量叠算(Constant folding):一种编译器的优化技巧,指在编译时就对常量表达式进行预求值。
John Lee(1513562323) 22:11:39
例如:
int arg = ((5 + 2) * 8 * 9) >> 10;        // 一堆常量运算,这些常量有可能是宏定义
foo(arg);                                            // 函数调用,编译器会直接把 arg 实参进行“常量叠算”后,以求得的“常数”作为实参调用foo()。
John Lee(1513562323) 22:13:14
让我们来分析一下“UART0.BAUD().BRD(20).DIV_X_ONE(1);”整个过程中用到的各种数据:
John Lee(1513562323) 22:13:38
“临时对象”的类型成员:
volatile uint32_t& ref:是硬件寄存器的引用,逻辑上是“常量指针”。
uint32_t val:硬件寄存器的数据缓冲,是“变量”。
uint32_t changed:由于初始值以及其后的操作数据,都是常量的,所以,本身也是“常量”。
John Lee(1513562323) 22:14:20
“临时对象”的 this 指针:“临时对象”的 this,C++ 认为是“常量”(在生成后,没有任何赋值或转移等操作)。
John Lee(1513562323) 22:16:20
write() 函数中用到的数据:
“位对象”this 指针:这个“值”与“临时对象”的 this相等(上次群课说过),由于“临时对象”的 this 是常量,所以它也是“常量”。
register baud_t* p:这个值就是 this 强制而来的“register baud_t* p = reinterpret_cast<baud_t*>(this);”,是“常量”。
register auto _v:这个值是就是形参 v“register auto _v = decltype(p->val)(v);”,如果形参 v 是常量,那么这个也是“常量”。
John Lee(1513562323) 22:16:56
好了,所有的这些数据中,只有“临时对象”的类型成员 val 是真正的变量,其它的都是常量。
John Lee(1513562323) 22:18:08
最后,编译器只会对 val 的分配空间,而直接求出各个常量的值,作为 CPU 指令的立即数。
John Lee(1513562323) 22:19:27
下面,我们看看编译器对这个表达式“UART0.BAUD().BRD(20).DIV_X_ONE(1);”的编译结果:
John Lee(1513562323) 22:20:43
ldr r3,=0x40050000       // 常数:UART0 地址
ldr r1,=0x10000014       // 常数:BRD:20, DIV_X_ONE:1
ldr r2,[r3,#36]          // 取 BAUD 寄存器值到缓冲数据
lsr r0,r2,#16            // 缓冲数据清除 BRD 位域中的原数据
lsl r2,r0,#16
orr r2,r1                // 缓冲数据“或”常数
str r2,[r3,#36]          // 缓冲数据写回到 BAUD 寄存器
John Lee(1513562323) 22:23:19
可以看到,缓冲数据被分配在了 CPU 寄存器中,而其它的数据都没有直接地分配空间。
John Lee(1513562323) 22:24:13
由于 ARM 指令的限制,常量也临时性地占用了 CPU 寄存器。
John Lee(1513562323) 22:27:29
我们可以看到,编译器生成的代码,效率是非常高的,同时,源程序也是非常优雅和安全的。
murex(344582199) 22:27:47
嗯,非常强悍
John Lee(1513562323) 22:30:55
这个帖子:https://bbs.21ic.com/icview-295372-2-1.html#pid2046953,讨论了 C 访问硬件寄存器的方法。
John Lee(1513562323) 22:31:28
也谈到了 C++ 访问硬件寄存器的“使用方法”,但没有谈实现原理。
John Lee(1513562323) 22:32:30
这几次的群课,便是比较充分地讨论了“实现原理”。
murex(344582199) 22:34:19
嗯,是比较详细了
John Lee(1513562323) 22:34:30
算是全部完成了。

CountryMan(176419557) 22:54:48
比较大型的嵌入式项目吧
John Lee(1513562323) 22:55:04
强迫你在项目中,按C++的思想来设计。
日期:2012/1/10
John Lee(1513562323) 22:55:35
从总体到各个细节,逐级抽象。
CountryMan(176419557) 22:55:56
抽象真不懂
John Lee(1513562323) 22:56:28
抽象,是软件设计中永恒的话题。
M0小白菜(465834115) 22:56:55
思路  ?说到思路       我现在编程是没什么思路的   就是想到哪就写到哪   ?   
John Lee(1513562323) 22:57:56
抽象,就是按面向对象的观点,来分析和归类项目中模块,和各个模块之间的关系。
CountryMan(176419557) 22:58:52
这种好想还有个专门的岗位呀
CountryMan(176419557) 22:58:56
好像
CountryMan(176419557) 22:59:07
系统工程师?
John Lee(1513562323) 22:59:15
抽象实际上项目架构设计师的工作。
CountryMan(176419557) 23:00:11


CountryMan(176419557) 23:00:24
这个要求忒高吧
John Lee(1513562323) 23:01:35
架构设计师,不仅要对项目的软件进行合理的抽象,而且要对硬件上影响抽象的不合理的地方,对硬件设计人员提出要求。
CountryMan(176419557) 23:03:10
这个暴强
John Lee(1513562323) 23:03:21
甚至于,硬件架构的总体设计都需要软件架构设计师参与。
John Lee(1513562323) 23:04:53
而要做到这些,你就要从学会“抽象”入手。
道可道(549040622) 23:06:45
抽象好难  
道可道(549040622) 23:07:03
中国人缺少抽象思维

CountryMan(176419557) 23:07:10
是啊
老师到时候多举例子
CountryMan(176419557) 23:08:43
怎样去培养抽象思维呢
M0小白菜(465834115) 23:08:59
同上
John Lee(1513562323) 23:10:47
一个好的“抽象”,不仅能设计出稳定的系统,重要的是,降低“模块”之间的耦合性,使各个模块的软件能够相对独立,这样当系统设计变更,增加或减少的模块时,才具有良好的“弹性”,不至于做出大的修改或推倒重来。
CountryMan(176419557) 23:12:47
低耦合
俺目前就只能理解到这里
John Lee(1513562323) 23:12:57
而且,这些相对独立的模块,可以形成文档和程序库,当以后的项目中,含有类似的模块时,可以立即使用。
CountryMan(176419557) 23:15:00
这个层次就高了
日期:2012/1/10
John Lee(1513562323) 23:15:32
道可道(549040622)  23:11:51
李老师,是不是这个得靠经验呢   
--------------
是的,任何事情都是经验,知识本身就是对经验的“抽象”。
道可道(549040622) 23:17:53
所以,大量的练习,练习多了,就理解了吧

CountryMan(176419557) 23:18:11
有没有速成法
John Lee(1513562323) 23:18:55
多实践,多总结。总结就是“抽象”,把经验“抽象”为知识。
John Lee(1513562323) 23:19:37
特别是要多参与大型项目的开发。
John Lee(1513562323) 23:20:29
了解系统的运行原理,即使不是你来设计,你也可以把它作为练习。
John Lee(1513562323) 23:23:04
还有,就是对一些 MCU 的常用外设,你也可以去练习“抽象”。
CountryMan(176419557) 23:23:21
比如USART
CountryMan(176419557) 23:23:25
怎样去抽象呢
CountryMan(176419557) 23:23:59
他有属于自己的东西
波特率啥的
John Lee(1513562323) 23:24:20
抽象的目的是形成重用性良好的软件模块。
John Lee(1513562323) 23:24:58
你要按着这个目的去抽象。
道可道(549040622) 23:25:46
等工作去,还是打基础吧
(来自手机QQ: http://mobile.qq.com/v/ )
CountryMan(176419557) 23:25:57
可不可以简单理解为让人容易调用

John Lee(1513562323) 23:26:14
这个是一方面
CountryMan(176419557) 23:26:15
API那样的接口函数
John Lee(1513562323) 23:26:49
是的
CountryMan(176419557) 23:28:25

他算是抽象的一部分
然后系统就是这样各个API的集合
John Lee(1513562323) 23:28:27
除了重用性外,适用性、易用性和效率都是很重要的。
CountryMan(176419557) 23:28:35

John Lee(1513562323) 23:29:57
一个系统,当然有它自己的、与其他系统不同的私有属性。
CountryMan(176419557) 23:30:10

John Lee(1513562323) 23:30:26
不能简单说,是各个API的集合。
CountryMan(176419557) 23:31:00
俺是肤浅理解
CountryMan(176419557) 23:31:06

John Lee(1513562323) 23:32:09
就像PC一样,操作系统(windows, linux)提供给了我们API和各种库,但我们的程序还是要靠我们自己来写。
CountryMan(176419557) 23:32:35
还要自己去抽象
John Lee(1513562323) 23:32:39

CountryMan(176419557) 23:33:30
这个难度巨大

CountryMan(176419557) 23:34:38
目前只能很肤浅的去理解这个抽象
日期:2012/1/10
John Lee(1513562323) 23:35:17
操作系统只能提供给我们一些重用性很高的程序库,但我们系统还要加上的私有软件逻辑。
John Lee(1513562323) 23:36:18
实际上,各种软件都是这样形成的。
CountryMan(176419557) 23:36:25
MCU的官方LIB也算是一种底层的抽象吧
John Lee(1513562323) 23:36:45
是的
CountryMan(176419557) 23:37:19

John Lee(1513562323) 23:38:16
一方面它避免了我们之间操作寄存器的麻烦,另一方面,可以是我们的程序在各个不同的 MCU 之间具有一定的移植性。
John Lee(1513562323) 23:39:22
抽象也不是只有一个层次,而是多层次的。
John Lee(1513562323) 23:40:19
每个层次都依赖其它层次的抽象成果。
CountryMan(176419557) 23:40:31

那有没有一个最高级的抽象
如果违背了它的抽象特点就出现比较大的问题呢
对系统的编写或者其他
CountryMan(176419557) 23:40:53
因为系统必须有核心
同样抽象是不是也需要呢
John Lee(1513562323) 23:41:12

CountryMan(176419557) 23:41:45
就类似是一个最核心的抽象
任何抽象都由它派生而得
John Lee(1513562323) 23:42:05
有的。
CountryMan(176419557) 23:42:09
就是类似一个由主到次的设计过程
John Lee(1513562323) 23:42:52
这是一个在层次上相互依赖的体系。
CountryMan(176419557) 23:43:06

John Lee(1513562323) 23:44:02
比如 UART,就 FIFO 这一个小小的机构,也可以抽象。
John Lee(1513562323) 23:44:50
有些 UART 是没有 FIFO 的,这个抽象出来就是一个层次。
CountryMan(176419557) 23:45:07
怎样抽象呢
CountryMan(176419557) 23:45:11
俺就知道队列
John Lee(1513562323) 23:45:50
而另一些 UART 是有 FIFO 的,这个抽象又是一个层次。
CountryMan(176419557) 23:46:12
但是它就是属于USART独有
这样如果系统有多个类似这样的FIFO
感觉代码量就增加了
John Lee(1513562323) 23:46:44
因为除了 FIFO 这一个机构外,UART 其它的部分,相同的地方很多。
CountryMan(176419557) 23:46:58
好像OS的控件就是干这个活的
John Lee(1513562323) 23:48:13
如果你为没有 FIFO 的 UART,和具有 FIFO 的 UART 都各自抽象,而没有层次关系的话,那么它们之间的代码就没有重用性。
CountryMan(176419557) 23:48:42
是的
CountryMan(176419557) 23:49:01
老师有没有这样的例程呀
John Lee(1513562323) 23:49:34
正确的抽象方法应该是,先抽象没有 FIFO 的 UART,形成一个模块。
John Lee(1513562323) 23:50:15
然后在“继承”这个模块,抽象出具有 FIFO 的 UART 模块。
CountryMan(176419557) 23:51:08

但是假如UART具有这个FIFO
是不是就不需要这个抽象的FIFO呢
日期:2012/1/10
John Lee(1513562323) 23:51:54
就是说,没有 FIFO 的 UART 是这个抽象层次中的“底层”,而具有 FIFO 的 UART 是“高层”,它依赖“底层”。
CountryMan(176419557) 23:52:46

John Lee(1513562323) 23:53:04
除了它私有的 FIFO 属性外,对于 UART 其它的属性,它重用着“底层”的属性代码。
CountryMan(176419557) 23:54:28
有点明白
从无到有
从有到完善
John Lee(1513562323) 23:54:54
这样,你就不必写“两套”除了 FIFO 外,代码都相同的程序。
CountryMan(176419557) 23:55:21

MCS8098(285216815) 23:55:48
老师 这就是  传说中的 继承 封装 多态吗
John Lee(1513562323) 23:56:13
而只对“高层”的、具有私有属性的抽象,写出私有代码即可。
CountryMan(176419557) 23:56:14

应该是继承的一种
CountryMan(176419557) 23:57:39
模板是不是抽象的一个实例
John Lee(1513562323) 23:57:42
嗯,你们理解得很快。
John Lee(1513562323) 23:58:13
好了,该休息了。

使用特权

评论回复
板凳
john_lee| | 2012-1-12 00:03 | 只看该作者
不错,还是道可道勤快,笔记也很清晰,不过笔记的后面部分,是群课结束后,大家聊天的内容,怎么也记录了啊。

使用特权

评论回复
地板
xyz549040622|  楼主 | 2012-1-12 05:46 | 只看该作者
3# john_lee 谢谢李老师夸奖,感觉后面讲得那部分也挺重要,就都加上了

使用特权

评论回复
5
hotpower| | 2012-1-12 08:06 | 只看该作者
楼主继续努力!眼过千遍也不如手过一遍。笔记多了不会都不由你。

使用特权

评论回复
6
Cortex-M0| | 2012-1-12 15:47 | 只看该作者
笔记记的不错,顶~~~

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

个人签名:qq群: 嵌入式系统arm初学者 224636155←← +→→点击-->小 i 精品课全集,21ic公开课~~←←→→点击-->小 i 精品课全集,给你全方位的技能策划~~←←

2782

主题

19267

帖子

104

粉丝