尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
VHDL程序框架详解:从库声明到结构体的FPGA设计入门
写VHDL这么多年我一直觉得很多初学者上来就背语法、抄代码结果一到自己写工程就到处报错。其实VHDL这东西和Verilog不一样它天生的“框架感”特别强一个程序从库声明到实体再到结构体逻辑层级是固定的。你把框架吃透了后面写的每一个模块都是在往这个骨架上填肉思路会清晰得多。这篇是“FPGA之道”系列的第22篇专门把VHDL基本程序框架从头到尾捋一遍。内容包括LIBRARY、ENTITY、ARCHITECTURE各管什么信号与变量的区别进程敏感列表怎么处理组合逻辑怎么避免锁存器最后我会用一个数码管动态扫描的完整例子带你把框架真正跑通。适合刚接触FPGA的初学者也适合那些写VHDL总被编译器和仿真器“教育”的人参考。1. 先搞懂三件套库、实体、结构体各管什么1.1 为什么先要搞懂框架再写代码很多初学者拿到一个FPGA开发板第一件事就是找例程然后照着改。这确实是最快的上手方式但问题在于一旦例程里少了某个库声明或者端口模式写错了你根本不知道从哪里下手排查。这就是不懂框架的代价。VHDL的代码结构和C语言完全不一样。C语言是“顺序执行”的思维一个main函数从上往下跑而VHDL描述的是硬件代码里的语句本质上是“并行”的多个语句同时在硬件上执行。所以VHDL必须用一种严格的结构把“外部接口”和“内部实现”分开这就是实体和结构体存在的意义。框架还有一个实际好处工程可维护性。你半年后回头看你写的代码如果实体端口定义清晰、结构体内部模块划分明确你花十分钟就能想起整个设计思路。如果没有框架全是散装的信号和进程那基本等于给自己埋雷。具体来说一个完整VHDL程序框架由四部分组成库声明LIBRARY、实体ENTITY、结构体ARCHITECTURE和配置CONFIGURATION。配置项在单模块仿真时用得少所以平时写代码前三样是天天都要见的。1.2 库与程序包别什么都往代码里塞库在VHDL里就好比是C语言的头文件它告诉你这个工程里要用到哪些现成的数据类型和函数。最常见的库声明是这两行library ieee; use ieee.std_logic_1164.all;这里ieee是库名std_logic_1164是程序包名all表示把包里的所有内容都拿进来用。std_logic_1164这个包定义了std_logic和std_logic_vector这两种核心数据类型没有它你连信号类型都没法写。除此之外你在写算术运算的时候还会用到ieee.numeric_std程序包里面定义了unsigned和signed类型以及加减乘除等运算。这里有个特别重要的点不要在新写的代码里去用std_logic_arith、std_logic_unsigned这些非IEEE标准包。std_logic_arith是Synopsys公司早年搞出来的东西老工程里大量使用你打开那些历史项目经常会看到。但新代码再用它最大的问题是它和numeric_std混用会导致类型不兼容编译的时候报出一堆莫名其妙的错误。比如你用numeric_std定义了一个unsigned信号又用std_logic_unsigned里的函数去加一个std_logic_vector类型转换的报错能让你怀疑人生。我的习惯是新工程统一只用ieee.std_logic_1164和ieee.numeric_std这两个包。老工程如果必须维护再照旧。这样可以少踩很多坑。1.3 实体 ENTITY芯片的“名片”实体定义了模块对外暴露的所有端口你可以把它理解成一个芯片的引脚排列图。外面的人不关心芯片内部怎么实现只关心每个引脚叫什么名字、信号方向是输入还是输出、以及用什么电平标准。一个最精简的实体是这样的entity led_controller is port ( clk : in std_logic; rst_n : in std_logic; led_out : out std_logic ); end entity led_controller;这里in和out是方向关键字std_logic是类型。很多初学者容易在std_logic和bit之间纠结。bit类型只有0和1两个值听起来似乎够用但实际综合时信号还有高阻态Z、未初始化U这些状态bit类型根本没法定标仿真稍微复杂一点就露馅。所以一律用std_logic端口宽度超过1位的用std_logic_vector这是FPGA开发的事实标准。实体命名的规范也要注意。我见过有同学给实体取名top_1、end_2这种毫无意义的名称等项目做大了根本分不清哪个文件是干什么的。命名最好能一眼看出模块功能比如uart_rx、spi_master、image_fifo这种团队协作和后期维护都会省很多事。1.4 结构体 ARCHITECTURE芯片的“内脏”结构体是VHDL里真正实现功能的地方最标准的写法是architecture rtl of led_controller is -- 内部信号、常量、元件的声明区 begin -- 并行语句、进程、元件例化 end architecture rtl;注意结构体开头有一个is然后声明区里可以放内部信号signal、常量constant、元件component等。begin后面是并行语句并行语句包括信号赋值、进程process、元件例化port map等。多个并行语句之间是同时执行的没有先后顺序。一个实体可以对应多个结构体用configuration来指定用哪一个。比如一个模块可以写一个行为级描述的结构体用于仿真再写一个RTL级结构体用于综合。但实际工程中绝大多数情况一个实体配一个结构体就够了暂时不用在这些高级功能上花太多精力。结构体里最核心的部分就是进程因为所有时序逻辑都写在进程里。进程放到后面细讲这里先把框架的概念建立起来。2. 容易翻车的5个VHDL细节端口模式、信号变量、进程与锁存器2.1 端口方向IN、OUT、INOUT、BUFFER怎么选才能不吃亏端口方向看似简单但实际翻车率很高。in和out没什么好说的容易出问题的是out不能当作输入来读。举个例子假设你的模块要产生一个计数信号cnt同时这个cnt又作为内部逻辑的判断条件。如果你把cnt声明为out端口然后在进程里写if cnt 5 then编译器直接报错。因为out端口在VHDL语法里是“只写不读”的。解决办法有两个。第一个办法比较老派用buffer端口类型。buffer可以读也可以写但它的缺点是上层模块例化你这个模块时必须用一个buffer类型的信号去连接不能直接接普通out端口这在层次化设计里非常麻烦。第二个办法就是推荐做法在结构体里先定义一个内部信号cnt_s所有逻辑都对这个内部信号操作最后再用cnt cnt_s;把它送到端口上。这样做既保留了out端口的通用性又不影响内部逻辑读取是目前工程里的标准解法。inout端口一般用于三态总线比如IO扩展芯片的数据线、外挂SRAM的数据总线等需要用到Z状态。很多初学者喜欢什么事都用inout这是个大坑因为inout端口过多会让综合工具生成大量三态缓冲器时序收敛困难调试时也容易出问题。能用in或out解决的绝不用inout这是一个非常重要的设计习惯。2.2 信号与变量仿真波形里为什么总是差一个时钟VHDL里有两种“容器”信号signal和变量variable。两者区别极其重要不夸张地说搞不懂这个你写的时序逻辑80%是错的。信号的赋值符号是变量是:。信号是全局的可以放在结构体声明区进程内外都能看到变量是局部的只能定义在进程内部外部看不到。最关键的区别在于更新时机。信号赋值不是立即生效的它要等到进程结束之后才更新。也就是说你在进程里给信号A赋值A B紧接着在下一行就读取A读到的仍然是A的旧值而不是B的值。这种情况在时序逻辑里其实是有意为之的——你真正想要的通常就是“下一个时钟沿再生效”。变量则不同:赋值是立即生效的在进程内部变量赋值之后的代码立刻就能读到新值。变量更像C语言的临时变量适合做中间计算。我见过太多初学者在进程里用一系列信号赋值去实现一个累加器结果仿真波形总是比预期慢一个时钟周期甚至完全不对。正确做法是运算过程用变量最终输出用信号。变量负责算信号负责存这样既清晰又不容易出错。-- 信号在进程结束时更新 process(clk) begin if rising_edge(clk) then a b; -- 这个赋值在进程结束后才生效 c a; -- 这里读到的a还是旧值 end if; end process;这段代码在仿真时c得到的是b的旧值这个结果会让很多新手懵半天。理解了信号的更新机制你就不会在这个地方浪费时间了。2.3 进程 PROCESS敏感列表是决定生死的细节进程是VHDL里描述逻辑的核心容器。它有两种基本写法时序逻辑进程和组合逻辑进程。时序逻辑进程的标准写法是这样的process(clk, rst_n) begin if rst_n 0 then cnt (others 0); elsif rising_edge(clk) then cnt cnt 1; end if; end process;敏感列表里只有clk和rst_n。rising_edge(clk)是上升沿触发意思是进程只在时钟上升沿到来那一刻执行一次其他时间全部休眠。这个写法是FPGA时序设计的基石。组合逻辑进程则不同敏感列表里必须包含进程里读到的所有信号。比如process(a, b, sel) begin if sel 1 then y a; else y b; end if; end process;这里敏感列表写了a, b, sel如果写成process(sel)仿真时a和b变化就不会触发进程重算结果自然是错的。更可怕的是这种错误在综合工具里不一定报错但仿真和实际硬件行为不一致排查起来极为痛苦。还有一种写法是不用敏感列表改用wait语句。比如wait on clk;或者wait until rising_edge(clk);。从语法上讲完全合法但我在工程中强烈不推荐。原因很简单wait语句放到不同位置对进程行为的约束不一样代码可读性和可维护性都很差而且很多团队代码规范里明确禁止。老老实实用敏感列表配合rising_edge是风险最低的选择。2.4 组合逻辑最隐蔽的坑一不小心生成锁存器组合逻辑进程中如果if或case分支不完整就会有分支没有赋值通路综合工具此时不会报错而是很“贴心”地给你生成一个锁存器latch。锁存器本身不是不能用但在绝大多数普通逻辑设计里它就是个隐患——时序分析不好做、容易产生毛刺、面积还浪费。典型例子process(sel, a, b) begin if sel 1 then y a; end if; -- 缺少elsesel0时y保持原值 → 生成锁存器 end process;这种写法本意可能是“默认保持”但在组合逻辑里保持就意味着锁存。正确做法是最前面赋默认值process(sel, a, b) begin y b; -- 给默认值 if sel 1 then y a; end if; end process;先赋值再根据条件覆盖这样所有分支都有通路综合工具不会生成锁存器。case语句同理when others必须写上不要偷懒。3. 完整实战数码管动态显示的程序框架搭建与验证3.1 需求拆解与架构设计理论知识讲一堆不如直接做一个完整例子。我选数码管动态显示这个经典实验是因为它麻雀虽小、五脏俱全涉及时钟分频、计数器、译码逻辑、位选和段选信号的产生是练习VHDL框架的绝佳素材。需求是这样的有一个8位共阳极数码管模块输入是50MHz系统时钟要求循环显示一组8位数据每位对应一个数字。如果8位数码管每个都单独接8根段选线就需要64根IO这在FPGA开发板上根本不可能。动态扫描就是解决这个问题的方案同一时刻只点亮一位数码管轮流点亮8位只要刷新频率超过人眼的视觉暂留大约每秒24帧以上我们看到的画面就是稳定的多位数显示。按题目要求8位数码管总刷新率至少要做到1kHz以上也就是每位点亮时间不超过1ms。50MHz时钟一个扫描周期计数50000次就是1ms。8位全部扫一遍就是8ms对应125Hz刷新率完全满足视觉要求。我的架构划分如下第一块是时钟分频模块把50MHz降到1kHz扫描时钟第二块是扫描计数器指示当前点亮第几位第三块是位选译码根据扫描计数器的值决定哪一位导通第四块是段选译码从8位数据中取出当前位对应的BCD码再查表得到七段码。3.2 核心代码逐段实现先写实体:定义端口。library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity seg8_display is port ( clk_50m : in std_logic; rst_n : in std_logic; data_in : in std_logic_vector(31 downto 0); -- 8个BCD数字每个占4bit seg_cs : out std_logic_vector(7 downto 0); -- 位选低有效 seg_out : out std_logic_vector(7 downto 0) -- 段选共阳极低有效 ); end entity seg8_display;结构体内部先定义分频计数器和扫描计数器。architecture rtl of seg8_display is signal cnt_scan : integer range 0 to 49999; signal scan_clk_en : std_logic; signal scan_pos : integer range 0 to 7; signal current_bcd : std_logic_vector(3 downto 0); begin时钟分频进程每50000个时钟周期产生一个scan_clk_en脉冲。这里我不直接把频率降到1kHz的时钟而是用一个时钟使能信号。这是个很关键的设计习惯——FPGA里尽量用单时钟域加使能而不是真的生成一个分频时钟因为分频时钟进入全局时钟网络会造成很多时序上的麻烦。-- 50MHz - 1kHz 时钟使能 process(clk_50m, rst_n) begin if rst_n 0 then cnt_scan 0; scan_clk_en 0; elsif rising_edge(clk_50m) then if cnt_scan 49999 then cnt_scan 0; scan_clk_en 1; else cnt_scan cnt_scan 1; scan_clk_en 0; end if; end if; end process;再来一个扫描位置计数器每当scan_clk_en为1时scan_pos加1到7之后回到0。这相当于8个状态循环。-- 扫描位置计数 process(clk_50m, rst_n) begin if rst_n 0 then scan_pos 0; elsif rising_edge(clk_50m) then if scan_clk_en 1 then if scan_pos 7 then scan_pos 0; else scan_pos scan_pos 1; end if; end if; end if; end process;紧接着是位选译码把scan_pos翻译成8位位选信号。我用的开发板是共阳极数码管位选低电平有效所以只有一个0其余都是1。-- 位选译码 with scan_pos select seg_cs 11111110 when 0, 11111101 when 1, 11111011 when 2, 11110111 when 3, 11101111 when 4, 11011111 when 5, 10111111 when 6, 01111111 when 7, 11111111 when others;然后是段选译码。先把当前位对应的4位BCD码取出来再查七段码。我用的是共阳极数码管段码低电平点亮。数字0的段码是0xC0数字1是0xF9依次类推。这里要注意段选和位选必须在同一个时间段内对准否则会造成“串位”这就是数码管显示最常见的问题。-- 提取当前位的BCD码 with scan_pos select current_bcd data_in(3 downto 0) when 0, data_in(7 downto 4) when 1, data_in(11 downto 8) when 2, data_in(15 downto 12) when 3, data_in(19 downto 16) when 4, data_in(23 downto 20) when 5, data_in(27 downto 24) when 6, data_in(31 downto 28) when 7, 0000 when others; -- 七段译码共阳极低有效 with current_bcd select seg_out 11000000 when 0000, -- 0 11111001 when 0001, -- 1 10100100 when 0010, -- 2 10110000 when 0011, -- 3 10011001 when 0100, -- 4 10010010 when 0101, -- 5 10000010 when 0110, -- 6 11111000 when 0111, -- 7 10000000 when 1000, -- 8 10010000 when 1001, -- 9 11111111 when others; end architecture rtl;到这里一个完整的数码管驱动模块就写完了。注意组合逻辑进程用了with ... select这也是并行语句的一种适合做译码器。整个模块里只有一个时钟分频进程和一个扫描计数进程其余全是组合逻辑框架非常清晰。3.3 仿真与验证要点代码写完先不要急着上板先在仿真环境里过一遍。写一个简单testbench例化这个模块给上时钟和复位再给一组测试数据。library ieee; use ieee.std_logic_1164.all; entity tb_seg8_display is end entity tb_seg8_display; architecture sim of tb_seg8_display is signal clk_50m : std_logic : 0; signal rst_n : std_logic : 0; signal data_in : std_logic_vector(31 downto 0) : x01234567; signal seg_cs : std_logic_vector(7 downto 0); signal seg_out : std_logic_vector(7 downto 0); begin clk_50m not clk_50m after 10 ns; -- 50MHz rst_n 1 after 100 ns; dut : entity work.seg8_display port map ( clk_50m clk_50m, rst_n rst_n, data_in data_in, seg_cs seg_cs, seg_out seg_out ); end architecture sim;仿真时重点观察三个东西一是复位后scan_clk_en有没有按1ms周期产生脉冲二是seg_cs有没有按顺序切换三是seg_out在当前位选下是否对应正确的段码。比如scan_pos 0时seg_cs应该是11111110此时data_in(3 downto 0)是7所以seg_out应该是11111000共阳极数字7的段码。如果看到这两组值能偶合上说明位选和段选时序是对的。上板验证时有个常见现象数码管显示数字有拖影。这通常不是逻辑错误而是扫描频率偏低或者位选切换瞬间段选还在变化导致的“串位”。解决办法是加大扫描频率比如每位0.5ms或者在位选切换的前一小段时间先把段选设成不亮的编码消隐等稳定的信号之后再点亮。这个小细节非常影响最终显示效果。4. 编译报错与仿真异常典型问题排查实录4.1 高频报错速查表看不懂报错就对照这里写VHDL头两年谁的代码都免不了被编译器教育。下面这张表是我在带新人过程中整理的高频报错遇到先对照一下能省下不少时间现象常见原因解决思路cant infer register for signal ... multiple drivers多个进程对同一个信号赋值一个信号只允许在一个进程中赋值检查是否重复驱动type mismatchstd_logic_vector与unsigned混用统一使用numeric_std必要时显式转换unsigned()/std_logic_vector()port ... not declared实体端口名与结构体引用不一致检查拼写IEEE标准对大小写不敏感但命名风格建议统一cant read port xxout端口在内部被读取换内部信号中转见本文2.1的解法incomplete assignment或生成latchif/case分支不完整给默认值补齐所有分支signal ... is used but never assigned声明了内部信号但没赋值删除无用信号或确认是否忘写驱动逻辑failed to synthesize代码中出现了不可综合的语法比如wait forRTL代码只写可综合风格仿真用testbench去写延时4.2 三大“玄学”问题的排查思路有时候代码编译没有错仿真波形也“看起来”是对的但一上板就出问题。这类问题俗称“玄学”其实背后都有原因只是排查思路没对上。第一个高频问题仿真通过但上板没反应。先查最基础的——复位信号极性对不对。很多开发板复位是高电平有效但FPGA内部逻辑在配置完成前引脚可能是拉高的如果代码里写的是rst_n 0有效上电后模块就一直卡在复位状态。解决办法是用开发板配套的复位方案或者看看板子原理图确认复位极性。第二个高频问题显示有毛刺或随机跳变。这种多半是异步信号进来没做同步处理导致的。FPGA内部是同步时序电路外部按钮、拨码开关这类信号直接进rising_edge判断条件很容易产生亚稳态。就算只用组合逻辑外部信号经过内部连线后的数字毛刺也可能形成错误脉冲。解决思路是加两级触发器打拍同步process(clk_50m, rst_n) begin if rst_n 0 then key_syn 00; elsif rising_edge(clk_50m) then key_syn(0) key_in; key_syn(1) key_syn(0); end if; end process;第三类经典问题modelsim仿真和Vivado/Quartus综合后的行为不一致。最典型的原因是库混用。老代码里用了std_logic_unsigned你又加了numeric_std两个包里面对运算符的解析结果不同仿真器按文件顺序解析包综合工具按自己的规则处理两边结果自然不一样。遇到这种情况别急着怀疑工具先把代码里的库和类型理理顺。4.3 几个能救命的实操技巧最后分享几个我这几年实际用下来很管用的习惯。第一个习惯是把常用模块模板保存成一个文件。比如实体、结构体、时钟分频、计数器、testbench的骨架存成template.vhd。新建代码的时候直接复制模板改名字比每次手敲可靠多了还能保证格式统一。第二个习惯是尽量多写注释。有些同学觉得代码短不需要注释但三个月后你再看自己的代码如果当时没写这个信号的用途、为什么这样判断基本等于看天书。我给自己定的规矩每个信号声明旁边写一句注释每个进程开头写三行左右的说明包括这个进程干什么、依赖哪些信号、输出到哪里。成本很低收益巨大。第三个习惯是看RTL视图。Vivado和Quartus都有RTL analysis功能可以把你写的VHDL渲染成逻辑图。当你不确定自己写出来的东西综合成什么电路时打开RTL视图看一眼比埋头读自己的代码快得多。尤其是锁存器、触发器这些结构图上一目了然。最后分享一个体会写VHDL这么多年我的感觉是越是基础的东西越值得反复琢磨。很多人以为程序框架就是个“模板”背下来就行其实框架背后是VHDL对硬件结构的深刻映射——结构体里的每个并行语句都对应一个物理电路进程里的每条赋值都在描述逻辑门之间的连接关系。你什么时候能把“代码行”和“硬件结构”一一对应起来看什么时候才算真正入了FPGA的门。再给一个实用小技巧平时写模块时把顶层实体设计得尽量通用一点。比如数码管驱动这个模块完全可以把扫描频率、位宽都做成顶层参数用常量或者通用声明传进去。这样下次换一块开发板改一两个参数就能复用省下来的时间足够你多调好几个功能。
RELATED

相关推荐

树莓派驱动DLP微投:framebuffer直写854×480显示实战

树莓派驱动DLP微投:framebuffer直写854×480显示实战

最近有个活儿要用树莓派驱动一块DLP微投影模组,型号是TI的DLPDLCR230NPEVM,要求在Linux下通过framebuffer直接显示图片。这套东西不是插上就能用的,里面有很多细节:HDMI时序要手动配、framebuffer的像素格式要对齐、DMD的驱动方式…

📅 2026/10/5 7:48:54
K8s副本数深度解析:从ReplicaSet到HPA与滚动更新

K8s副本数深度解析:从ReplicaSet到HPA与滚动更新

先说个反直觉的现象:一个replicas: 3的 Deployment,在滚动更新期间,集群里最多可能出现 4 个 Pod。也就是说,你在 k8s 里设置的副本数,和实际跑着的 Pod 数量,并不是简单的一对一关系。很多人觉得 k8s 设置…

📅 2026/10/5 7:48:54
PL/SQL Developer导出Oracle表数据全攻略:从右键导出到命令行兜底

PL/SQL Developer导出Oracle表数据全攻略:从右键导出到命令行兜底

上周刚帮一个项目组处理过一次数据倒腾,对方要的是一张两千多万行的订单明细表,说要导出来拿去另一套系统做历史数据归档。接手的人很顺手地打开PL/SQL Developer,右键那张表打算直接导出,结果跑了二十多分钟还在转圈,…

📅 2026/10/5 7:48:54
MORE NEWS

更多资讯

📰

模拟CMOS设计必备:MOS器件物理核心概念与工程应用解析

做模拟IC设计,大部分人开局都是抱着拉扎维那本《模拟CMOS集成电路设计》在啃,第一章还能靠兴趣撑住,到了第二章MOS器件物理基础,很多人直接开始怀疑人生。我当年也是这样,捧着书看了三遍,感觉每个字都认识&…

📰

Agent生产环境落地指南:记忆、编排、安全与并发实践

1. 这份调研报告到底在聊什么 Agent 这个词在 2025 年到 2026 年之间,从技术圈的黑话变成了产品经理、运维、甚至业务侧都在追问的东西。我拿到《2026 Agent 开发者调研报告丨Alibaba Cloud AI Agent Handbook》这份材料的时候,第一反应不是去看它列了多…

📰

AI Agent 生产实战:编排、记忆、安全与多 Agent 协作的工程化指南

1. 从一份调研报告说起:Agent 开发者到底在关心什么2026 年的 Agent 开发领域,和两年前已经完全不是一个面貌了。2024 年大家还在争论"Agent 到底是不是套壳的 Prompt 工程",到了 2026 年,这个问题已经没人再提——因为…

📰

大华客流统计方案深度解析:系统架构、安装参数与避坑指南

简介:浙江大华技术股份有限公司编写的《大华客流统计分析解决方案》,面向商场、连锁店等商业场所的运营管理与智能化项目人员,针对人工客流统计效率低、数据滞后等问题,给出了基于视频智能分析的完整应对思路。包内为单个doc文档&…

📰

电商订单数据清洗实战——Pandas 处理缺失值、重复单与异常金额

AI Agent Function Calling Python 个人主页&#xff1a;> for_ever_love__ < &#xff08;欢迎各位大佬莅临&#x1f60a;&#xff09; 其他栏目: > 我想学python了 < 其他栏目: > iOS项目总结大全 < 其他栏目: > iOS UI < 文章目录电商订单数据清…

📰

乳腺癌病理图像自动分类:从patch到WSI的深度学习落地指南

简介&#xff1a;PDF资料《基于深度学习的乳腺癌病理图像自动分类》定位清晰&#xff0c;面向医学影像处理、计算机辅助诊断与深度学习应用方向的科研人员、临床工程师和学生。乳腺癌是威胁女性健康的常见恶性肿瘤&#xff0c;病理图像是临床确诊和制定治疗方案的重要依据&…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬