简介:面向FPGA与EDA初学者、数字系统设计课程学习者及需要完成综合实验的读者,这份资料围绕基于FPGA的闹钟系统展开,重点解决用VHDL完成计时、校时与定时提醒等模块化设计的问题。包内共1个doc文件,压缩包约2.71MB,以完整设计文档为主体,涵盖EDA与FPGA基础、VHDL开发流程、时间存储、计时、闹钟设置、比较器、显示与用户交互等模块划分,并给出Quartus仿真综合、GW48开发板下载测试的实现思路。已有832人学习,可作为课程设计、实验报告或项目原型的参考,帮助读者理顺从需求分析、系统划分到VHDL建模与验证的路径,理解分频、计数、比较等数字逻辑的组织方式,并对照开发流程排查仿真与板级测试中的常见问题。
1. 把 48MHz 晶振变成“时:分:秒”:一条被反复分频的脉冲链
把一块 FPGA 板子上的 48MHz 晶振,变成数码管上一位一位往前爬的时间显示,中间隔着的其实是一条被反复分频、逐级进位、层层回卷的脉冲链,真正需要理解的东西比代码行数多得多。很多人第一次做基于 FPGA 的数字闹钟系统设计,注意力全放在 VHDL 语法上,结果仿真波形漂亮得不像话,下载到板子却一动不动——问题往往不在语法,而在“哪一级脉冲驱动哪一级触发器”这个最朴素的机制上。一个数字闹钟的全部复杂度,说穿了就是把高频振荡逐级降频、把十进制位做成模 10 和模 6 的回卷、再在某个时刻和一组预设值做一次相等比较。搞懂这条链,任何计数器逻辑在你眼里都会变得眼熟。
2. 秒计数的十进制约束:十位为什么写成「计到 6 立刻清零」
秒模块是整个系统的节拍源,它的输出不只是两个数字,还有一个进位脉冲。这个脉冲的宽度、产生的时刻、以及它由哪种赋值方式产生,直接决定后面分和时能不能正确接住。
2.1 变量与信号:赋值生效的时机决定了你判「当前值」还是「下一个值」
VHDL 里最容易让人翻车的一对概念就是变量(variable)和信号(signal)。变量用:=赋值,在进程内立即生效,下一行读到的就是新值;信号用<=赋值,要等进程挂起(执行到进程末尾或遇到 wait)之后才统一更新,而且在同一个进程里对同一个信号赋值多次,只有最后一次有效。这听起来像是语言细节,但它直接决定你在写回卷判断时该填哪个数字。
| 载体 | 赋值符号 | 生效时机 | 同一进程内读到的是 | 回卷判定应写 |
|---|---|---|---|---|
| 变量 | := | 立即生效 | 自增之后的新值 | 下一个值,如"0110" |
| 信号 | <= | 进程挂起后统一更新 | 自增之前的旧值 | 当前值,如"0101" |
这条规则解释了一个很奇怪的现象:同一份设计文档里,秒模块判的是M2="0110",分模块判的是M2="0101"加M1="1001",两处写法不一致,却都能跑出正确结果。原因不是谁写错了,而是前者用了变量、后者用了信号。抄代码的人如果只抄判断条件不抄载体,就会得到一个十位显示 6 的秒表,或者一个永远不进位的分钟。
2.2 秒模块源码逐层拆解:个位进位、十位回卷、进位脉冲
先把原始结构完整摆出来,再逐段说它为什么这么写:
LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; USE IEEE.STD_LOGIC_UNSIGNED.ALL; ENTITY MIAO IS PORT( CLR, CLK : IN STD_LOGIC; S1, S2 : OUT STD_LOGIC_VECTOR(3 DOWNTO 0); -- 秒个位 / 秒十位 CNT : OUT STD_LOGIC -- 进位脉冲,送给分模块 ); END ENTITY; ARCHITECTURE BEHAV OF MIAO IS BEGIN PROCESS(CLR, CLK) VARIABLE M1, M2 : STD_LOGIC_VECTOR(3 DOWNTO 0); -- 个位 / 十位 BEGIN IF CLR = '1' THEN -- 异步复位,优先级最高 M1 := "0000"; M2 := "0000"; CNT <= '0'; ELSIF CLK'EVENT AND CLK = '1' THEN -- 上升沿 IF M1 < "1001" THEN M1 := M1 + 1; -- 个位 0~9 正常累加 ELSE M1 := "0000"; M2 := M2 + 1; -- 个位满 10,回零并向十位进位 END IF; IF M2 = "0110" THEN -- 十位撞上 6 才回卷 M2 := "0000"; CNT <= '1'; ELSE CNT <= '0'; END IF; END IF; S1 <= M1; S2 <= M2; -- 变量搬到输出端口 END PROCESS; END ARCHITECTURE;第一段是复位。CLR写在时钟沿判断之前,构成异步复位:只要CLR为高,M1、M2、CNT立刻归零,不等时钟沿。这在开发板上很实用,按下复位键立刻见效果;代价是复位释放的那一刻如果正好压在时钟沿附近,触发器可能进入亚稳态,工程上通常还要加一个复位同步器。
第二段是个位计数。M1 < "1001"等价于“小于 9”,成立就加一,不成立就清零点亮十位进位。注意比较用的是无符号比较,所以STD_LOGIC_UNSIGNED这个库不能省,否则+和<都没有定义。
第三段是十位回卷,也是全篇最值得琢磨的地方。因为M2是变量,上面的M2 := M2 + 1已经立即生效,所以这里读到的M2是“自增之后”的值。十位从 5 进位到 6 的那一拍,M2等于"0110",条件成立,立刻被清回"0000",同时把CNT拉高。下一个时钟沿M2已经是 0,条件不成立,CNT又拉低。所以CNT是一个宽度正好一个时钟周期的单脉冲。
这个宽度很关键。如果下游分模块用同一个CLK采样CNT,一拍足够;如果下游用的是另一个时钟(比如某个分频出来的信号),这个单周期脉冲很可能被整拍漏掉,表现出来就是“分钟偶尔不走”。这是踩坑率极高的一处,后面第 5 章还会展开。
再看为什么整个设计坚持用 BCD 码而不是二进制计数。假设内部用 0 到 86399 的二进制计数,结构上确实省触发器——17 位就够,而 BCD 要 6 组 4 位共 24 位。但显示时得把二进制转成十进制,取模和除法在 FPGA 上要么靠长除法流水线,要么靠 DSP 硬核,代价远高于多出来的 7 个触发器。BCD 把“十进制”编码进状态本身,每一位只做一次“是否到 9”的比较,逻辑浅、扇入小、综合结果容易预估。这是典型的设计取舍:拿资源换逻辑深度。
提示:
CNT用的是信号赋值,判断用的是变量,这两者混用在同一个进程里是刻意为之。把CNT改成变量会让它在进程内立即生效,后续判断逻辑就会读到新值,脉冲宽度随之改变。
2.3 1Hz 从哪里来:分频器的参数怎么算
秒模块每来一个上升沿加一,意味着它的CLK必须是一秒一个上升沿。板载晶振通常是几十兆赫兹,中间必须有分频。最常见的写法是“计满半个周期翻转一次”,保证 50% 占空比:
-- 48 MHz 分频到 1 Hz SIGNAL DIV_CNT : INTEGER RANGE 0 TO 24_000_000 := 0; SIGNAL CLK_1HZ : STD_LOGIC := '0'; PROCESS(CLK_48M, CLR) BEGIN IF CLR = '1' THEN DIV_CNT <= 0; CLK_1HZ <= '0'; ELSIF RISING_EDGE(CLK_48M) THEN IF DIV_CNT = 24_000_000 - 1 THEN -- 半周期计满 DIV_CNT <= 0; CLK_1HZ <= NOT CLK_1HZ; -- 翻转,一个完整周期 = 2 秒? 见下 ELSE DIV_CNT <= DIV_CNT + 1; END IF; END IF; END PROCESS;参数要算清楚:48MHz 下,要让输出一秒翻转两次(即输出周期 1 秒),半个周期是 0.5 秒,需要计 24,000,000 个主时钟。所以计数值上限取 24,000,000 减一,计满清零并翻转。INTEGER RANGE 0 TO 24_000_000综合后大约占 25 位触发器,因为 2 的 25 次方是 33,554,432,刚好装得下。如果板子是 50MHz,这个上限就要改成 25,000,000,改错一位,整块表就走快或走慢,而且仿真时看不出来——仿真里你给的时钟周期是多少,它就按多少走,只有上板才对得上真实晶振。
3. 分与时的计数通道:同一个计数器为什么需要两套寄存器
分模块是全篇结构最复杂的一处。它不像秒模块那样只做一件事,而是要同时承担“正常走时”和“被用户手动设置”两个职责,这两件事的驱动源完全不同:一个来自秒进位,一个来自按键。
3.1 两套寄存器的分工:一套负责「设」,一套负责「走」
原始代码里出现了四个四位宽的寄存器M1、M2、Q1、Q2。这不是冗余,而是把两条数据通路彻底分开:Q1/Q2是设置通道的暂存,M1/M2是计时通道的现行值。
ENTITY FEN IS PORT( CLR, CLK, LOAD, EN : IN STD_LOGIC; -- EN 高电平时处于设置态 S1, S2 : OUT STD_LOGIC_VECTOR(3 DOWNTO 0); CNT : OUT STD_LOGIC ); END ENTITY; ARCHITECTURE BEHAV OF FEN IS SIGNAL M1, M2, Q1, Q2 : STD_LOGIC_VECTOR(3 DOWNTO 0); BEGIN -- 进程一:设置通道,由 LOAD 的上升沿推动 Q1/Q2 PROCESS(EN, CLR, LOAD) BEGIN IF CLR = '1' THEN Q1 <= "0000"; Q2 <= "0000"; ELSIF EN = '1' THEN IF LOAD'EVENT AND LOAD = '1' THEN IF Q1 < "1001" THEN Q1 <= Q1 + 1; ELSE Q1 <= "0000"; Q2 <= Q2 + 1; END IF; IF Q1 = "1001" AND Q2 = "0101" THEN -- 59 之后回到 00 Q1 <= "0000"; Q2 <= "0000"; END IF; END IF; END IF; END PROCESS; -- 进程二:计时通道,正常走时或跟随设定值 PROCESS(CLR, LOAD, EN, CLK) BEGIN IF CLR = '1' THEN M1 <= "0000"; M2 <= "0000"; ELSIF EN = '1' THEN M1 <= Q1; M2 <= Q2; -- 设置态下现行值跟随设定值 ELSIF CLK'EVENT AND CLK = '1' THEN IF M1 < "1001" THEN M1 <= M1 + 1; ELSE M1 <= "0000"; M2 <= M2 + 1; END IF; IF M2 = "0101" AND M1 = "1001" THEN -- 判的是旧值 M2 <= "0000"; CNT <= '1'; ELSE CNT <= '0'; END IF; END IF; END PROCESS; END ARCHITECTURE;进程一的门控是EN = '1',也就是“只有处于设置态,按键才有效”。按下SET_M键,LOAD来一个上升沿,Q1加一;Q1到 9 再按一次,Q1归零、Q2加一。在Q1已经是 9、Q2已经是 5 的状态下再按一次,Q1先归零、Q2加到 6,然后第二个条件用旧值判定成立,Q2被重新赋成"0000"。于是 59 分之后按键会绕回 00 分,不会出现 60 分这种非法状态。
进程二的ELSIF EN = '1'分支意味着:设置态下,现行时间直接等于设定值。用户按一下键,屏幕上跳一下,所见即所设,不需要“确认”这一层中间态。但这里的优先级也带来一个副作用——EN = '1'时时钟沿那一路被完全屏蔽,分和时停止走动。设置花多久,时间就冻结多久,秒模块却还在自顾自地跑。退出设置后,分的值突然被跳到新值,秒却还是原来那个秒,中间那段进位就丢了。这个瑕疵在演示里看不出来,实际用起来会觉得“每次调完时间都慢了十几秒”。
3.2 59 到 00 的进位:信号延迟更新与后赋值覆盖怎么互相配合
IF M2 = "0101" AND M1 = "1001"这一行,第一次读会觉得有问题:上面的ELSE分支里明明已经写了M1 <= "0000",怎么下面还判M1 = "1001"?
答案在于信号赋值的两条语义同时起了作用。第一条是延迟更新:M1 <= "0000"之后,进程内读M1读到的仍然是上一拍的值"1001",所以条件里的M1 = "1001"成立,判断的是“进位前”的状态,这正是我们想要的。第二条是后赋值覆盖:M2 <= M2 + 1让M2排进了更新队列,紧接着M2 <= "0000"又排了一次,进程挂起时只有最后一次生效,所以M2最终是"0000"而不是 6。
两条语义缺一不可。假如 VHDL 的信号是立即更新的,那么M1在这一行已经是 0,条件永远不成立,59 分之后会跳成 60 分;假如一个进程内对同一信号的多次赋值是“先到先得”,M2就会变成 6。这两种假设都会导致同一个症状:分钟显示越界。所以这一行不是随手写的,它精确地踩在两个语言特性的交叉点上。
和秒模块对照就更有意思了。秒模块用变量,所以必须判“下一个值”"0110";分模块用信号,所以必须判“当前值”"0101"。同一个功能,两种载体,两套判断数字,全都正确。血泪经验就在这:抄代码的时候,判断条件和载体是一体的,拆开抄必翻车。
3.3 小时的模 24 边界:十位和个位不能各自为政
小时模块原始文档没有给出完整源码,但结构上和分模块同构,只是回卷条件从 60 变成 24。这里最容易写错的地方是:很多人只处理个位的 9 到 0,忘了十位最大值是 2 而不是 5。
-- 小时计时通道(M1 个位、M2 十位) IF M1 < "1001" THEN M1 <= M1 + 1; ELSE M1 <= "0000"; M2 <= M2 + 1; END IF; IF M2 = "0010" AND M1 = "0011" THEN -- 23 点的下一拍回到 00 M1 <= "0000"; M2 <= "0000"; ELSIF M2 = "0011" THEN -- 兜底:十位不允许出现 3 M2 <= "0000"; END IF;第一个条件处理 23 到 00 的正常回卷,用的是旧值判断,和分模块一致。第二个ELSIF是兜底:如果因为某种原因M2变成了 3(比如复位不彻底、或者从设置通道灌进来一个非法值),这里会把它拉回 0,避免显示出一个不存在的“3x 点”。这种防御性写法在计数器里很常见,代价只是一个额外比较器,收益是系统不会卡在非法状态里出不来。
4. 从按键到时间:按下 SET_M 到底触发了什么
按键是这套系统里唯一的异步输入,也是最不听话的部分。它在物理上是弹簧片接触,在电气上是一串抖动的方波,在逻辑上却必须表现为“一次干净的动作”。
4.1 三个按键拼出的隐式状态机
原始设计没有画显式的状态机,而是用几个使能信号拼出了一台隐式状态机:SET_CLOCK控制“是否处于设置时间态”,SET_DINGS控制“是否处于设置闹钟态”,DISPLAY_EN控制“屏幕上看的是时间还是闹钟”。三根线组合出四种界面状态。
最省资源的实现是用一位 toggle 寄存器:
-- SET_CLOCK 每按一次,切换一次设置态 PROCESS(CLK_1K, CLR) BEGIN IF CLR = '1' THEN EN_SET_TIME <= '0'; ELSIF RISING_EDGE(CLK_1K) THEN IF SET_CLOCK_PULSE = '1' THEN EN_SET_TIME <= NOT EN_SET_TIME; -- 奇数次进入,偶数次退出 END IF; END IF; END PROCESS;一个 toggle 位的好处是逻辑极省,一个触发器解决问题;坏处是它对脉冲的完整性极度敏感。如果抖动让一次按键产生两个脉冲,toggle 位翻转两次,状态就错位了——用户按了一下,屏幕没进设置,再按一下才进去,用起来像“按键不灵”。这就是为什么消抖不是可选项。
DISPLAY_EN同理,它只在非设置态下才允许切换显示内容,否则用户正在调闹钟却被切到时间界面,设了一半的值就丢了。原文把“显示切换”和“设置进入”分成两个键,本身就是在回避这个冲突。
4.2 消抖与两级同步:异步按键进时序逻辑前必须做的两件事
机械按键的抖动时间通常在 5 到 20 毫秒之间。做法是拿一个 1kHz 的采样时钟(周期 1 毫秒),连续 20 次采到相同的电平才认账:
-- 20 ms 消抖 + 两级同步 + 上升沿提取 PROCESS(CLK_1K, CLR) VARIABLE CNT_DB : INTEGER RANGE 0 TO 19 := 0; BEGIN IF CLR = '1' THEN KEY_SYNC <= '0'; KEY_STABLE <= '0'; CNT_DB := 0; ELSIF RISING_EDGE(CLK_1K) THEN KEY_SYNC <= KEY_IN; -- 第一级:把异步信号拉进本时钟域 IF KEY_SYNC /= KEY_STABLE THEN -- 与稳定值不同,开始计时 IF CNT_DB = 19 THEN KEY_STABLE <= KEY_SYNC; -- 连续 20 ms 一致,承认新电平 CNT_DB := 0; ELSE CNT_DB := CNT_DB + 1; END IF; ELSE CNT_DB := 0; -- 中途抖回去,重新计时 END IF; END IF; END PROCESS; KEY_PULSE <= KEY_STABLE AND NOT KEY_STABLE_D; -- 上升沿 -> 单周期脉冲KEY_SYNC这一级同步不能省。按键相对采样时钟是随机相位的异步信号,如果直接送进触发器,在建立保持时间窗口内变化就会产生亚稳态,输出既不是 0 也不是 1,持续时间不确定。打一拍之后,第二级采到的概率已经极小,代价只是一个触发器。
CNT_DB用变量而不是信号,是因为它只在进程内做计数,不需要对外可见,用变量省掉一组触发器,综合工具会把它推断成组合逻辑加寄存器,行为更直观。
4.3 闹钟比较与显示切换:相等条件为什么要挂一个「秒=00」的限定
比较逻辑本身很简单:小时的十位个位、分的十位个位全部相等,就认为命中。但如果只比较时分,这个相等条件在整个一分钟内都为真。
ALARM_EQ <= '1' WHEN (H1 = AH1) AND (H2 = AH2) AND (M1 = AM1) AND (M2 = AM2) ELSE '0'; PROCESS(CLK_1HZ, CLR) BEGIN IF CLR = '1' THEN RING_EN <= '0'; ELSIF RISING_EDGE(CLK_1HZ) THEN IF ALARM_EQ = '1' AND S1 = "0000" AND S2 = "0000" AND ALARM_ON = '1' THEN RING_EN <= '1'; -- 只在整分那一拍锁存 ELSIF RING_TIMEOUT = '1' OR RING_STOP = '1' THEN RING_EN <= '0'; -- 超时或手动关闭 END IF; END IF; END PROCESS;如果把这个比较结果直接接到蜂鸣器,命中那一分钟里它会一直响;如果只在时钟沿触发却不加锁存,它会在那一分钟里的每个时钟沿反复触发,永远停不下来。加“秒等于 00”的限定加锁存,把它压缩成一个一次性的边沿事件,再配一个超时计数器(比如 60 秒)自动关掉,才是可用的行为。
还有一个容易被忽略的细节:RING_EN打开之后,真正驱动蜂鸣器的不能是 1Hz 方波,那听起来是一秒一响的“滴…滴…”,不是闹铃。常见的做法是另外起一个 2kHz 左右的分频器做音源,用RING_EN去门控它,得到的是连续的“嘀——”声。门控写错成直接赋值,蜂鸣器要么不响,要么一直响,这是第 5 章里的一条现场记录。
显示通路的切换放在数据前面比放在段码后面省资源:
-- 4 路二选一,放在扫描之前 DISP_H2 <= AH2 WHEN DISPLAY_EN = '1' ELSE H2; DISP_H1 <= AH1 WHEN DISPLAY_EN = '1' ELSE H1; DISP_M2 <= AM2 WHEN DISPLAY_EN = '1' ELSE M2; DISP_M1 <= AM1 WHEN DISPLAY_EN = '1' ELSE M1;一组四位选择器乘四次,比在段码输出端做八路选择便宜得多。数码管本身是动态扫描的,1kHz 节拍轮流点亮四位,刷新率 250Hz,人眼完全看不出闪烁。
5. 避坑与排查:仿真通过、下载到板子却翻车的五个现场
仿真和上板之间隔着一整个物理世界:布线延迟、时钟偏斜、按键抖动、器件电气特性,全都不在仿真模型里。
5.1 时钟与时序:波形对、板子错的两类现场
现象一:仿真波形完全正确,下载后数码管数字长时间不变化,偶尔跳一下。
原因基本可以锁定在分频出来的 1Hz 信号上。它是由一个普通触发器翻转产生的,如果综合工具没有把它分配到全局时钟网络,而是当普通数据走线,那么时钟到达各个触发器的延迟就不一致,时钟偏斜会把窄脉冲吃掉。1Hz 的周期有一秒,偏斜几十纳秒听起来不算什么,但边沿质量差、上升沿缓慢,触发器采样就可能落在不确定区。
解决思路是改结构而不是改参数:不要用分频出来的信号当时钟,改成一路主时钟加时钟使能脉冲。所有时序逻辑共用同一个时钟域,1Hz 只是一个宽度一拍的使能信号,跨模块传递天然安全,综合工具也不需要为衍生时钟单独写约束。
现象二:综合报告里时序分析一片红,或者提示时钟频率远低于预期。
原因往往是分频器把 25 位比较器挂在了时钟路径上,工具为了满足这个比较关系,把大量逻辑塞进了时钟树。解决方法是把“计满清零”改成“计满翻转”,把比较结果转成寄存器的使能条件;同时给主时钟加一条明确的周期约束,让工具按真实频率优化。
5.2 输入与显示:按键和数码管最容易出问题的两处
现象三:按一次SET_M,分钟一下跳三四个数。
原因就是抖动被当成了多次上升沿。消抖逻辑缺失,或者消抖窗口太短(比如只连续采 3 次),弹簧片在前几毫秒的抖动全被计数了。按 4.2 的做法改成 20 毫秒窗口,并且用CNT_DB中途翻转就清零的策略,跳数问题立刻消失。
现象四:数码管某一位偏暗,或者相邻位有重影。
原因是位选信号和段码在同一个时钟沿更新,但两者经过的组合逻辑深度不同,到达数码管的时间有先后,切换瞬间旧段码会在新位上亮一小会儿。解决方法是在位选之后打一拍再送段码,或者把位选和段码都寄存输出,让它们严格同沿更新。如果亮度不均,还要检查扫描频率:低于 50Hz 会看到闪,高于几 kHz 则每位点亮时间太短、亮度被拉低。
5.3 逻辑与资源:进位脉冲和设置模式留下的坑
现象五:闹铃只在某一瞬间“嘀”一下,或者设置完时间后整体走慢十几秒。
前者的原因是把只有一拍宽的进位脉冲直接送去了蜂鸣器,或者用另一个时钟域去采样它,脉冲被整拍漏掉。进位脉冲只应该做进位,音源必须另起一个高频分频器。
后者是 3.1 节提到的设计副作用:EN = '1'期间时钟通道被屏蔽,秒模块却照跑,退出设置时秒和分对不上。补救办法有两个——设置态下把秒模块也一起冻结,或者在退出设置的那一拍统一清秒。选哪个取决于你要的是“设置期间时间暂停”还是“设置期间时间照走”,前者实现简单,后者更符合真实手表的行为。
6. 分频时钟与时钟使能:教学代码和工程代码的分岔口
同一份闹钟,用分频时钟直接当时钟能跑,用主时钟加使能也能跑,但从这里分出了两条路。
| 维度 | 分频信号直接当时钟 | 主时钟 + 时钟使能 |
|---|---|---|
| 时钟域数量 | 秒、分、时、闹钟比较各一个 | 全系统只有一个 |
| 时序约束 | 每个衍生时钟都要单独约束 | 只约束主时钟 |
| 跨域传递 | 单拍脉冲跨域容易丢 | 同一域内传递,天然安全 |
| 资源开销 | 无需使能判断 | 每级多一个使能条件 |
| 换器件移植 | 时钟树要重做 | 改参数即可 |
文档里那套写法在实验箱上能跑,是因为实验箱的时钟资源宽裕、逻辑规模小、频率极低,任何偏差都被一秒的周期淹没了。一旦系统变大、时钟变快、或者要移植到另一块板子上,衍生时钟就会变成最难查的那类问题——它能仿真、能综合、能下载,只是偶尔错一次,像黑匣子一样。
从那以后我每次写计数器,都先问自己一句:这个信号的边沿,是被当作时钟用,还是被当作数据用?只要答案是后者,就老老实实把它做成使能脉冲,让整块板子只有一个时钟域。这个习惯没有让我少写过代码,但确实让我少熬了很多夜。希望帮到你。
本文还有配套的精品资源,点击获取