ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

VHDL数字万年历设计:月长解耦、进位链与状态机避坑

VHDL数字万年历设计:月长解耦、进位链与状态机避坑

简介:这份资源是一份基于VHDL实现多功能数字万年历的课程设计实验报告,面向正在学习硬件描述语言、准备数字电路课程设计或FPGA入门项目的学生与自学者,可用于理解从秒到年的完整计时系统如何用可综合代码搭建。文档围绕分频器、计数、按键消抖与显示控制等模块展开,配合状态机完成小时与分钟、月与日、年等界面的切换和设置,并给出闹钟、整点报时、闰月检测与星期LED显示等拓展要求的实现思路,还附有RTL图和逐段程序分析,便于对照理解代码与硬件结构之间的关系。资源包共1个docx文件,约775KB,按设计思路、程序调试、程序分析、实验总结的顺序组织,正文即完整报告本身。目前已有836人学习,适合作为课程设计参考与代码拆解的材料。

1. 万年历的骨架不在计数:日模块的边界值到底该由谁决定

很多做过 VHDL 数字时钟的人都有一个共识:把秒、分、时写成 60 进制、24 进制计数器,仿真波形能一次跑对;可一旦加上“万年历”,事情就开始变得玄学——月份显示对了,日子却在该 31 号的地方跳到 01,或者 2 月过完 28 号直接进 3 月 1 号。翻车的地方几乎从不在“怎么数到 60”,而在“日模块凭什么知道这个月有几天”。这个问题的答案,藏在这个 VHDL 多功能数字万年历最核心的一条设计线索里:日模块自己不存月长,月长由月模块算好了从端口喂进来。理解这条依赖链,才算真正看懂这套万年历的骨架,也才明白为什么它的进位信号要从秒一路“传”到年,而不是各自为政。

2.1 50MHz 到 1000Hz:为什么中间要留一个“千赫兹”档位

板子上的晶振进来是 50MHz,一秒要数 5000 万下。如果直接从 50MHz 分频到 1Hz,计数器要占 26 位,而且秒表功能就没法做了——秒表需要毫秒级精度。所以这套设计把分频拆成了两级:第一级把 50MHz 降到 1000Hz,第二级再从 1000Hz 降到 1Hz。中间这一档 1000Hz 就是秒表的“时基”。

LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; USE IEEE.STD_LOGIC_UNSIGNED.ALL; ENTITY count IS PORT( clk : IN STD_LOGIC; co : OUT STD_LOGIC -- 每 50000 个时钟拉高一个周期,输出 1kHz ); END count; ARCHITECTURE behav OF count IS -- 关键:位宽必须装得下 49999。原文写 range 0 to 2 却判 49999,是硬伤 SIGNAL cnt : INTEGER RANGE 0 TO 49999 := 0; BEGIN PROCESS(clk) BEGIN IF rising_edge(clk) THEN IF cnt = 49999 THEN cnt <= 0; co <= '1'; -- 满一个周期,给出一拍宽度的脉冲 ELSE cnt <= cnt + 1; co <= '0'; END IF; END IF; END PROCESS; END behav;

这里有两个参数值得掰开看。49999不是随便写的,它是 50_000_000 ÷ 1000 − 1,从 0 数到 49999 正好 50000 个时钟周期,20ns × 50000 = 1ms,也就是 1kHz。如果写成50000再判相等,实际会数 50001 个周期,每天累计下来就是几秒的误差——这种误差不大,但一旦要做整点报时,它会在整点那一刻原形毕露。

关于INTEGER RANGE 0 TO 2这个写法,必须单独说明。VHDL 的 integer 默认是 32 位有符号数,但一旦加上 range 约束,综合器就会按能表示这个范围的最小位宽去推。0 TO 2只需要 2 位,于是综合出来的计数器实际上是 2 位宽,永远数不到 49999。仿真器对 integer 的越界检查相对宽松,很多时候波形看着“能动”,综合之后行为就完全对不上了。这是个典型的仿真过、上板死的坑。

2.2 秒模块的两个计数器:sect 与 temp 各管什么

秒模块承担了两件事:把 1kHz 再分频到 1Hz,以及产生一个占空比 50% 的miao信号去驱动板上那颗独立的 LED。这两件事由两个独立的 process 完成,一个跑在 clk 上,一个跑在 temp 上。

ARCHITECTURE behav OF second IS SIGNAL sect : INTEGER RANGE 0 TO 59 := 0; -- 秒计数,0~59 SIGNAL temp : INTEGER RANGE 0 TO 999 := 0; -- 1kHz 分频到 1Hz 的预分频器 BEGIN PROCESS(clk) BEGIN IF rising_edge(clk) THEN IF temp = 999 THEN temp <= 0; IF sect = 59 THEN sect <= 0; co <= '1'; -- 秒进位,送给分钟模块 ELSE sect <= sect + 1; co <= '0'; END IF; ELSE temp <= temp + 1; co <= '0'; END IF; END IF; END PROCESS; -- miao:500ms 亮、500ms 灭,人眼看到的就是“一秒一闪” PROCESS(temp) BEGIN IF temp < 500 THEN miao <= '1'; ELSE miao <= '0'; END IF; END PROCESS; sec <= sect; END behav;

temp = 999这个判断实际是 1000 个周期(0 到 999),1kHz 除以 1000 正好 1Hz,这里是对的。但要注意原文写的是temp = 1000且temp : INTEGER RANGE 0 TO 1000,那就是 1001 个周期,会产生千分之一的系统误差。单看一天不明显,跑一个月就能看出偏差。

miao那个 process 更值得说。为什么不用co直接驱动 LED?因为co只有 20ns 宽,人眼完全看不见。把temp拿来比较,前 500 个周期输出高、后 500 个输出低,得到的是一个 500ms 高、500ms 低的方波,LED 才会呈现“一秒闪一次”的效果。这是把电气信号翻译成人能感知的物理现象,属于设计里最容易被忽略、但体验上最直观的一环。

2.3 进位链的时序:co 在同一个时钟沿里“走”了几级

秒、分、时、日、月、年六个模块全部挂在同一个clk上,co是寄存器输出,不是组合逻辑。这意味着分钟模块看到seco = '1'时,是在下一个时钟沿才动作。整条链是“移位”式传递的,每一级延迟一个时钟周期。

50MHz 下一个周期是 20ns,六级传完也就 120ns,对于人看时间完全无感。但这个细节决定了设计能不能综合出干净的电路。如果图省事写成组合逻辑进位:

-- 反例,不要这么写 co <= '1' WHEN sect = 59 ELSE '0';

那么在sect从 59 翻到 0 的那个瞬间,co会出现毛刺,下一级模块可能在同一时刻被触发两次,也可能完全不触发,取决于综合后的布线延迟。更糟的是,如果哪一级模块图省事,直接把上一级的co当成自己的时钟:

-- 反例:门控时钟,FPGA 上可能能跑,ASIC 上是灾难 PROCESS(co) BEGIN IF rising_edge(co) THEN ...

这就造出了一个门控时钟域。FPGA 的综合工具会报警告,实际也可能因为时钟树走线不均而产生亚稳态。这套设计的做法是老老实实全部同步到系统时钟,用co当使能信号,这是数字设计里最稳的写法,也是为什么它能一路从秒传到年而不会乱。

3. 月长与闰年:29 天是怎么被 month 模块“算”出来的

3.1 日模块的上下界不该自己存:yue 端口的真正作用

日模块的定义里有一个外部输入yue,类型是INTEGER RANGE 1 TO 31。这个端口是整个万年历里最关键的一个解耦设计——日模块只管“加一”和“到了就归零”,至于“到了”是多少,它不关心,也不该关心。

ENTITY day IS PORT( clk : IN STD_LOGIC; houro : IN STD_LOGIC; -- 小时进位 yue : IN INTEGER RANGE 1 TO 31; -- 当月天数,由 month 提供 daykeyadd, daykeysub : IN STD_LOGIC; day : OUT INTEGER RANGE 1 TO 31; co : OUT STD_LOGIC ); END day;

如果让日模块自己判断“现在是几月、是不是闰年”,那日模块就得同时知道月份和年份,等于把三个模块的耦合压到一个模块里,一旦要加“大小月自动切换”之外的功能(比如农历),整个结构就得推翻重写。用yue端口把月长抽出来,日模块就退化成一个纯粹的“可配置模 N 计数器”,N 由外部给。这个思路在硬件设计里非常常见,跟软件里把配置项从函数体挪到参数表是同一个道理。

但原文的实现在这里埋了个坑:signal dayt : INTEGER RANGE 0 TO 31,而且复位时dayt <= 0。输出day <= dayt,于是每个月的第一天,数码管上显示的是00。正确的做法是把dayt的范围改成1 TO 31,初值设为 1:

ARCHITECTURE behav OF day IS SIGNAL dayt : INTEGER RANGE 1 TO 31 := 1; -- 从 1 起,不出现 0 号 BEGIN PROCESS(clk) BEGIN IF rising_edge(clk) THEN IF houro = '1' THEN IF dayt = yue THEN dayt <= 1; -- 归 1,不是归 0 co <= '1'; ELSE dayt <= dayt + 1; co <= '0'; END IF; ELSIF daykeyadd = '0' THEN ...

这个 0/1 边界的处理看着琐碎,但它直接影响每次月切换时的那一秒显示是否正确。硬件不像软件能随时打个日志看,一个偏移就得到下个月才能验证一次,调试周期被拉得极长。

3.2 闰年判定:mod 4 为什么“够用”,又在哪里不够用

年份规则单独放在 year 模块里,用一个组合进程算出run信号:

PROCESS(yeart) BEGIN IF (yeart MOD 4) = 0 THEN run <= '1'; -- 闰年 ELSE run <= '0'; -- 平年 END IF; END PROCESS;

run一路送到 month 模块,month 再根据“当前是 2 月 + run 是否为 1”决定yue输出 29 还是 28。这条链路只有两步,逻辑干净。

但要说清楚:mod 4 = 0是闰年判定的简化版。完整的公历规则是“能被 4 整除且不能被 100 整除,或者能被 400 整除”。1900 年不是闰年,2000 年是闰年。这套设计没有处理整百年的例外,所以它在一个世纪里有三次会错一天。放在课程设计的场景下这没问题——原始需求里明确写了“基本要求不需要提供闰月识别功能”,闰年检测属于加分项。但如果有人想把这份代码直接用在需要长期运行的产品里,这个简化就必须补上,至少得加(yeart MOD 100) /= 0 OR (yeart MOD 400) = 0这一层判断。

还有一个更隐蔽的问题:原文 year 模块里signal yeart : INTEGER RANGE 0 TO 59,但输出口year : OUT INTEGER RANGE 0 TO 99,而yeart判到 59 就归零。也就是说这个“万年历”实际上只有 60 年的跨度,而且端口声明的范围跟实际行为不一致。这种声明和实现脱节的地方,综合器不会报错,但会让人误以为支持 100 年。

3.3 CASE 映射月长表:组合逻辑为什么在这里是正确选择

月长映射用的是纯组合进程,没有时钟:

PROCESS(montht, run) BEGIN CASE montht IS WHEN 2 => IF run = '1' THEN yue <= 29; -- 闰年二月 ELSE yue <= 28; -- 平年二月 END IF; WHEN 4 | 6 | 9 | 11 => yue <= 30; -- 小月 WHEN OTHERS => yue <= 31; -- 大月,含 1、3、5、7、8、10、12 END CASE; END PROCESS;

when 4 | 6 | 9 | 11这种写法在 VHDL-93 里就合法,多个 choice 用竖线分隔,综合出来是一组比较器加一个多路选择器,资源很小。

这里用组合逻辑而不是时序逻辑,是有意为之。yue需要在montht变化的同一时刻就更新,这样日模块在下一个时钟沿采样时拿到的就是新月长。如果给yue加一级寄存器,那月份切换的那一拍,日模块拿到的还是旧值,就会出现“3 月 1 号按 2 月的 28 天判断”的错位。

when others这个分支同样重要。VHDL 的 case 语句要求覆盖所有取值,montht是INTEGER RANGE 1 TO 12,理论上 1 到 12 都列全了,但综合器不一定这么认为。写others => 31既满足了语法完备性,也给异常值兜了底——万一montht因为某种原因跑到了 0 或 13,日子至少还能按 31 天走,不会卡死。

4. 四位数码管撑起六个量:扫描状态机与界面状态机的分工

4.1 段码表:共阳编码里每一位对应什么

板子上是四位数码管,而系统要显示年月日时分秒六个量,还得兼顾秒表和闹钟。显示资源严重不足,只能靠“分时复用 + 界面切换”两条路解决。先看最底层的段码表:

-- 共阳极数码管:0 表示该段点亮,1 表示熄灭 -- 位序 bit7~bit0 对应 dp g f e d c b a CONSTANT led0 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "11000000"; -- 0 CONSTANT led1 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "11111001"; -- 1 CONSTANT led2 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "10100100"; -- 2 CONSTANT led3 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "10110000"; -- 3 CONSTANT led4 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "10011001"; -- 4 CONSTANT led5 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "10010010"; -- 5 CONSTANT led6 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "10000010"; -- 6 CONSTANT led7 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "11111000"; -- 7 CONSTANT led8 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "10000000"; -- 8 CONSTANT led9 : STD_LOGIC_VECTOR(7 DOWNTO 0) := "10010000"; -- 9

以led0 = 11000000为例:最高两位是 dp 和 g,输出 1 表示熄灭;低六位 a 到 f 全是 0,表示点亮。六段亮起来正好拼出数字 0。这套编码是硬件决定的,换个板子如果是共阴数码管,整张表就得按位取反。很多人调试时数码管显示的是一堆乱码,八成就是把共阳当共阴用了,这是硬件层面最常见的踩坑点。

数位分离用的是取模和整除:

centl <= cent MOD 10; -- 个位 centh <= cent / 10; -- 十位

原文里hourl <= hourh / 10和dayl <= dayl MOD 10这两行是笔误——前者把十位的结果又赋给个位,后者是自己取模自己。综合器不会拦这种自引用,但显示出来的效果就是十位和个位永远一样,或者数字不随计数变化。这类笔误单看代码很难发现,必须配合仿真波形一个信号一个信号对。

4.2 两个状态机的分工:界面切换与扫描刷新

显示模块内部其实跑着两个独立的状态机,职责完全不同。

第一个是界面状态机,五个状态对应五个界面:时钟显示、月日显示、年显示、秒表显示、闹钟显示。

TYPE state_type IS (s0, s1, s2, s3, s4); -- 五个界面 SIGNAL pre_s, next_s : state_type;

第二个是扫描状态机,负责在四位数码管之间快速轮流点亮,靠视觉暂留骗过眼睛。

TYPE state IS (c1, c2, c3, c4); -- 四位数码管位选 SIGNAL pre_c, next_c : state;

两者都用“双进程”模板写:一个进程只做现态更新,另一个进程算次态。

-- 界面状态机:现态更新 PROCESS(key0) BEGIN IF falling_edge(key0) THEN pre_s <= next_s; -- 按键下降沿推进界面 END IF; END PROCESS; -- 扫描状态机:现态更新,跑在系统时钟上 PROCESS(clk) BEGIN IF rising_edge(clk) THEN pre_c <= next_c; END IF; END PROCESS;

双进程写法的价值在于把“状态寄存器”和“状态转移逻辑”彻底分开。状态寄存器部分综合出来就是一组触发器,转移逻辑部分是组合电路,两者边界清晰,仿真时能分别观察pre_s(当前状态)和next_s(下一状态),出问题时一眼就能定位是“状态没跳”还是“跳错了”。单进程写法把所有逻辑揉在一个 process 里,虽然代码短,但调试时没有中间观察点,等于黑匣子。

4.3 set 变量:key1 的双重身份与它带来的边界问题

界面上有四个按键,key0 切界面,key1 选设置项,key2 和 key3 做增减。问题在于,“设置项”这个概念是跟界面绑定的:时钟界面下 key1 在“时”和“分”之间切,月日界面下在“月”和“日”之间切,年界面下只有“年”一个可设置量。

原文用一个两位的set变量来记录:

SIGNAL set : STD_LOGIC_VECTOR(1 DOWNTO 0) := "00"; PROCESS(key1) BEGIN IF falling_edge(key1) THEN IF set = 2 THEN set <= "00"; -- 到顶了归零 ELSE set <= set + 1; END IF; END IF; END PROCESS;

set只有 0、1、2 三个有效值。这里的设计意图是“每个界面最多两个可设置量 + 一个不设置的状态”。但set是全局的,不随界面切换清零。也就是说,在时钟界面把set按到 1,切到月日界面后set还是 1,直接就从“日”开始设置,而不是从“月”。这是个体验上的小瑕疵,改法很简单:在pre_s变化时把set复位成"00"。

另一个更本质的问题是:key1 在原文里同时驱动了pre_s的状态转移和set的计数,一个按键承担两件事,还都用falling_edge当时钟。这里必须靠消抖状态机来保证“一次按下只出一个脉冲”,否则按键抖动会让界面连跳好几下。消抖状态机用系统时钟采样按键电平,检测到稳定低电平后只输出一个时钟周期宽的脉冲,这是所有按键设计的标准动作。

4.4 七个 LED 显示星期:weekend 向量的默认值陷阱

星期用 7 个独立 LED 表示,代码里是一个 7 位向量:

SIGNAL weekend : STD_LOGIC_VECTOR(6 DOWNTO 0) := "0000010";

默认值0000010意味着上电时 bit1 为 1,其余为 0,也就是点亮第二个 LED。这个默认值本身没什么问题,但它暴露了一个设计选择:星期不是从日期算出来的,而是独立维护的一个计数器。真正的万年历应该能根据年月日推算出星期几(基姆拉尔森公式之类),而这套设计选择了让它独立计数、独立设置。

这个取舍在课程设计场景下完全合理——星期推算涉及乘除法,综合资源开销不小,而且需求里只要求“用 7 个 LED 显示星期”,没要求自动推算。但它带来一个后果:星期和日期是两条独立的计数链,长时间运行后可能对不上。如果某天手动调了日期,星期不会跟着变,得手动再调一次星期。这就是功能堆叠时常见的“状态不同步”问题。

5. 避坑与排查:仿真全绿、上板翻车的五个现场

5.1 计数器位宽:range 写小了,综合器会“帮你”截断

现象:仿真里co有输出,波形能跑;综合烧板后秒不动,或者秒走得极慢。

原因:SIGNAL count1 : INTEGER RANGE 0 TO 2配合IF count1 = 49999。综合器按 range 推出 2 位宽,计数器在 0、1、2 之间循环,永远等不到 49999,co要么不产生,要么周期完全错。仿真器对 integer 越界检查不严格,所以波形看着“有变化”。

解决:range 必须覆盖计数上限,0 TO 49999。养成习惯:每次写 integer 范围,先算清楚最大值是多少,宁可多留一位也别卡着边界写。

5.2 日期从 0 开始:每月 1 号显示 00

现象:日期在月末从 30 或 31 跳变后,显示的是00而不是01,持续整整一天。

原因:dayt声明为0 TO 31且复位为 0,输出直接连day。计数逻辑里“到达当月天数就归零”,于是归到了 0。

解决:计数变量范围改1 TO 31,初值 1,到界时归 1 而不是归 0。同时检查所有“到界归零”的分支——秒和分归 0 是对的,日和月归 1 才是对的,这两类边界在同一个文件里混着写,特别容易抄错。

5.3 自引用赋值:hourl 和 dayl 那两行笔误

现象:时钟的“分”个位和十位显示相同的数字,或者日期个位不随进位变化。

原因:hourl <= hourh / 10本意是hour MOD 10,写成了对十位再除十;dayl <= dayl MOD 10是自己给自己取模,综合后可能被优化掉,也可能产生锁存器。

解决:数位分离统一写成低位 <= 值 MOD 10、高位 <= 值 / 10,并且用仿真波形逐个确认每个数位信号在数值变化时都跟着变。这类笔误纯靠读代码很难抓,必须看波形。

5.4 按键直接当时钟:一次按下跳三个界面

现象:明明只按了一下 key0,界面却连跳两三个;或者偶尔按了没反应。

原因:PROCESS(key0) ... IF falling_edge(key0)把机械按键的电平直接当边沿触发源。机械按键按下和松开时会有几毫秒的抖动,每一次抖动都被当成一个下降沿,状态机就连跳。另外,按键信号没有同步到系统时钟域,存在亚稳态风险。

解决:先用系统时钟对按键做两级同步,再进消抖状态机(比如“连续检测到 20ms 低电平才认为按下”),消抖后输出一个时钟周期宽的脉冲,其余模块只用这个脉冲做使能,绝不用按键电平当时钟。

5.5 没有复位:上电后所有 integer 是随机值

现象:板子上电瞬间数码管显示乱码,或者时间从一个莫名其妙的值开始走,按复位也没用。

原因:所有SIGNAL的INTEGER类型都没给初值,也没有异步复位端口。仿真时仿真器按类型左边界初始化(看着挺正常),实际硬件上电后触发器状态不确定。

解决:两种方式选一种——信号声明时给初值(FPGA 支持,ASIC 不一定),或者统一加一个异步复位端口,在复位分支里把所有计数器、状态机、显示缓存都赋到确定值。这套设计里状态机、计数器、数码管缓存加起来十几个信号,少复位一个都可能出问题。

6. 设计取舍:把“时间”塞进触发器之后,边界在哪里

这套万年历最值得琢磨的地方,不是它实现了多少功能,而是它在几个关键路口做的选择,以及这些选择各自的代价。

第一个取舍是用 integer 还是 std_logic_vector 做计数器。integer 写起来舒服,cnt <= cnt + 1一行搞定,比较也直观。但综合器对 integer 默认按 32 位处理,一个“数到 59”的秒计数器,理论上 6 位就够,实际可能综合出 32 位的比较器和加法器。板子资源富裕时无所谓,一旦要往小容量器件里塞,这些浪费就会变成布线失败。更稳的做法是用UNSIGNED(5 DOWNTO 0),位宽自己定,边界判断用数值比较,资源占用一目了然。

第二个取舍是单时钟域 vs 多时钟域。整套设计坚持所有模块挂在 50MHz 上,进位靠co使能传递,代价是每一级有一拍延迟,好处是没有门控时钟、没有跨时钟域、时序分析干干净净。如果为了“省事”让每个模块用自己的进位信号当时钟,代码会短一些,但会引入多个时钟域,FPGA 上也许能跑,换到别的实现载体上就是灾难。这个选择没有中间路线,只能一开始就定死。

第三个取舍是控制逻辑放在哪里。这套设计把界面状态机、设置逻辑、扫描逻辑全塞进 display 模块,然后让 display 反向输出一堆centkeyadd、hourkeysub之类的信号去控制各个计数模块。这形成了一个环:计数模块 → 显示模块 → 计数模块。功能上能跑,但耦合度很高,想单独测试“分钟计数是否正确”都很难,因为它的增减依赖 display 输出的按键信号。更清晰的结构是把“设置控制”抽成独立模块,display 只负责显示,控制模块负责把按键翻译成各计数器的加减使能,形成单向数据流。

最后一个取舍是功能的完整性 vs 代码的可验证性。闹钟、整点报时、闰年、星期、秒表,五个加分项全塞进来之后,单个 vhd 文件已经很难一眼看全。这时候“能不能仿真验证”比“功能多不多”更重要——如果每个模块都有独立的测试平台,能单独灌时钟、单独看进位,那功能再多也不怕;如果没有,加一个功能就多一处可能出错的地方。

我自己的血泪经验是:第一次写这类设计时,我图快把所有逻辑堆在一个 architecture 里,仿真跑通就烧板,结果日期每到月末就错一天,查了整整两天才定位到是归零还是归一的区别。从那以后我每次写计数器,都强制先把“上界、下界、到界动作、复位值”这四个参数写成一张表贴在代码顶上,写完对着表逐行核一遍,再开始仿真。这个习惯救过我很多次。希望帮到你。

这套资源的价值也在这里:它把分频链、进位链、月长映射、双状态机显示、按键消抖这几个环节的完整代码和 RTL 结构都摆了出来,包括那些写错的地方。对着它把上面几个取舍重新走一遍,比从零憋一份要快得多,也更容易看清每个选择背后的代价。

本文还有配套的精品资源,点击获取

返回列表