ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA模拟电话计费器设计:Verilog状态机与BCD码应用

FPGA模拟电话计费器设计:Verilog状态机与BCD码应用 简介面向可编程逻辑器件课程设计与期末综合设计场景这份文档以电话计费系统为案例完整展示了基于FPGA的模拟电话计费器设计流程。内容首先介绍FPGA发展现状与电话计费器相关背景随后依据时钟管理、按键处理、计费算法和通信接口四个关键因素确定方案设计基本原则。硬件设计部分详述了FPGA芯片选型、PLL分频产生秒脉冲、按键分配与延时消抖并分别给出控制与计费模块、计时器模块、按键检测模块、费率计算模块和显示驱动模块的实现思路软件设计部分对应展示整体软件流程、主要模块代码和引脚分配。资源包为1个docx文件体积约499KB目录章节清晰包含完整Verilog HDL源程序代码、参考文献和期末设计报告结构适合电子信息、通信工程等专业学生用于课程报告参考、设计思路学习或答辩准备。已有198人学习下载可作为FPGA入门项目与数字系统设计的重要参考资料。1. 项目概述与整体设计思路1.1 核心需求解析一个被低估的综合练习接到“FPGA模拟电话计费器设计”这个课题很多人第一反应是“这有什么好做的”但实际动手之后会发现这个题目把FPGA开发里最常用、最基础、也最容易出问题的知识点全串起来了时钟分频、计数器设计、状态机、BCD码转换、动态扫描显示还有按键消抖和参数配置。可以说它比单纯跑一个LED流水灯或者数码管显示有营养得多又比做图像处理、PCIe这些大工程友好得多特别适合作为入门到进阶之间的过渡项目。这个项目的本质是用FPGA模拟传统电话计费系统的核心逻辑。说得直白一点就是实现“通话按时长计费”这个功能。你拿起电话触发开始信号系统开始按某个费率累加费用挂断电话触发结束信号时停止计费并显示费用总额。中间还要能处理费率切换、计费周期设置、甚至暂停计费等细节。那为什么要用FPGA来做用单片机也很容易实现同样的功能而且代码写起来可能更快。这里的关键在于FPGA的思路是“硬件并行”的计费逻辑、显示刷新、按键扫描是同时在工作互不阻塞。这和我们用CPU跑顺序程序是完全不同的思维模式。做完这个项目你对“时序逻辑”“状态转移”“模块间协同”的理解会有一个实质性的提升。1.2 方案选型Verilog 开发板 数码管的组合具体到实现方案我用的是最常见的组合开发环境Vivado我用的是2019.1版本其实ISE、Quartus也都行思路完全一致硬件平台Spartan-7或Artix-7系列开发板主时钟50MHz板载4位或6位共阳极数码管若干个按键和拨码开关设计语言Verilog HDL仿真工具Vivado自带的Simulation也可以用ModelSim反正仿真脚本是通用的选这个组合的理由很简单资料多、踩坑记录多、板子便宜。尤其是Xilinx的Vivado对初学者来说约束文件的写法、IP核的调用方式都有大量现成参考不像一些冷门工具卡半天还不知道问题出在哪。还有一个细节值得说它的参考价值才会最大化。第三个点也是我最想强调的代码风格从第一天就要规范。模块划分清晰、命名有意义、时序逻辑和组合逻辑分开写。这个项目规模不大如果代码写得乱凑合着也能跑通但这会给你后面的项目埋雷。尤其是多模块项目里信号满天飞的时候你会感激当初认真写的注释和规范的命名。2. 核心模块设计与原理解读2.1 系统时钟分频从50MHz到1Hz的“数学题”所有计费系统的第一步都是把板载的高频时钟降到一个可以跟“秒”挂钩的低频信号。以最常见的50MHz主时钟为例要实现1Hz的秒脉冲需要计数25,000,000个时钟周期。这里有个细节判断计满的边界条件,代码里写cnt 25000000 - 1还是cnt 24999999看起来都差不多但实际仿真结果会差一个周期。我习惯统一用“计数到设定值减一”的方式避免边界混淆。分频模块的输出通常有两种形式一种是产生占空比50%的方波另一种是产生“单周期脉冲”。在计费系统里我们几乎总是需要后者——一个只维持一个时钟周期的高电平脉冲用它来触发计数器加一。如果用了方波后面的计数器会在上升沿和下降沿各触发一次逻辑就乱了。这里放个核心代码片段// 秒脉冲生成模块 module clk_div( input clk, input rst_n, output reg s_pulse ); reg [24:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 25d0; s_pulse 1b0; end else if (cnt 25d24_999_999) begin cnt 25d0; s_pulse 1b1; end else begin cnt 25d0 cnt 1b1; // 等价于 cnt cnt 1b1 s_pulse 1b0; end end endmodule上面代码里我故意写了个cnt 25d0 cnt 1b1其实直接写cnt cnt 1b1就行。这个不做特别说明意思是想提醒大家写Verilog要留意数据位宽匹配25位宽的计数器加1综合工具会正确处理但你写成cnt cnt 1默认32位在某些老工具里会被警告有位宽截断养成好习惯以后可以少很多莫名其妙的警告。2.2 计费脉冲产生费率的本质是“一个脉冲代表多少钱”电话计费的核心概念是“计费脉冲”——也就是每隔固定的时间计费金额跳一个单位。这个“固定时间”就是计费周期而“单位金额”就是费率。传统电话系统里本地通话可能每3秒计1个脉冲长途可能是每1秒计1个脉冲这些参数早期是硬件设定的。在FPGA里实现逻辑上很简单以秒脉冲为基础再用一个计数器去数秒脉冲的个数数到设定的计费周期值就输出一个计费脉冲同时把周期计数器清零重来。这里存在一个“累计误差”问题如果你的周期计数是“计满清零”那么不会丢脉冲但如果是“比较相等后清零”一旦计数过程被中断比如有人按键暂停计费恢复后需要重新累计实际通话时长和计费时长就会不一致。我的做法是// 计费脉冲生成逻辑支持暂停 reg [31:0] cycle_cnt; wire billing_enable; // 计费使能信号1计费中 always (posedge s_pulse or negedge rst_n) begin if (!rst_n) cycle_cnt 32d0; else if (!billing_enable) cycle_cnt 32d0; // 暂停时清零周期计数 else if (cycle_cnt BILL_CYCLE - 1) begin cycle_cnt 32d0; bill_pulse 1b1; end else begin cycle_cnt cycle_cnt 1b1; bill_pulse 1b0; end end关键点是暂停计费时要同时暂停周期计数并清零这样恢复后从完整周期重新开始不会出现“暂停前已经数了2秒恢复后又数了2秒就计费”这种半截状态。BILL_CYCLE这个参数怎么定这就是可配置设计的关键。我把它做成了内部寄存器复位时有一个默认值比如等于5秒用户可以通过按键或拨码开关在运行时修改修改值通过串口打印出来或显示在数码管上。这个设计很有用因为你不需要重新综合烧录就能改变费率演示的时候效果非常直观。2.3 计费金额累加与BCD编码处理计费脉冲产生之后剩下的就是“数数”——每来一个计费脉冲费用累加一次。大多数人都会在这里踩一个坑直接用二进制计数器累加然后试图把二进制数转成数码管能显示的数。这个转换并不难但涉及一个叫“BCD码”的概念处理不好很容易出错。BCD码简单说就是用4位二进制表示一个十进制位。数码管有4位或6位每位显示0-9如果你累加到了100十进制在数码管上要显示“0100”或“0100”就需要把二进制数拆成千位、百位、十位、个位分别送给对应位。FPGA里最常用的转换方法是“移位加3算法”Double Dabble当然也可以用查找表或者直接用可综合的取模取余操作。但这里有一个更优雅也更贴合硬件思路的做法不要用二进制累加后再转换而是直接用一个BCD计数器累加。每一位是4位的BCD计数当低位的值到9时再加1就产生进位高位加1自己清零。这相当于把“十进制计数”直接在硬件上实现了天然就是各数码管位所对应的显示值完全绕开了二进制转BCD的步骤。// BCD码费用累加模块示例4位BCD最大显示9999 reg [3:0] bcd_0, bcd_1, bcd_2, bcd_3; always (posedge bill_pulse or negedge rst_n) begin if (!rst_n) begin bcd_0 4d0; bcd_1 4d0; bcd_2 4d0; bcd_3 4d0; end else if (bcd_0 4d9) begin bcd_0 4d0; if (bcd_1 4d9) begin bcd_1 4d0; if (bcd_2 4d9) begin bcd_2 4d0; if (bcd_3 4d9) bcd_3 4d0; // 溢出可以置一个标志位提示 else bcd_3 bcd_3 1b1; end else bcd_2 bcd_2 1b1; end else bcd_1 bcd_1 1b1; end else bcd_0 bcd_0 1b1; end这段嵌套的if看起来有点长但它就是“十进制进位”最直白的表达。实际项目里可以用for循环配合数组来写得更紧凑但逻辑上是一样的。还有一个细节每个计费脉冲对应的金额是1个货币单位还是多个货币单位如果你的费率不是1单位/脉冲可以再加一个乘法器或查表把脉冲数映射到金额。我在设计里把“费率”做成了可配置参数支持1倍、2倍、5倍、10倍用拨码开关选择效果相当好。2.4 状态机控制摘机、通话、挂机的流程管理计费系统不是计费脉冲一到就闷头累加那么简单。实际使用场景有明确的先后流程空闲待机→ 振铃/摘机 → 通话计费中 → 挂机结算 → 回到空闲。这个流程用状态机来描述最自然。我画状态机的思路IDLE空闲等待摘机信号对应一个按键按下。此阶段不计费、不累计显示当前费率或“0000”。TALKING通话计费摘机后进入计费使能打开计费脉冲正常运行金额累加。PAUSED暂停可选状态如果在通话中按下暂停键计费使能关闭但状态机保持在通话相关状态。恢复后继续计费。FINISH挂机结算挂机信号到来锁存最终计费金额停止计费脉冲。显示停留几秒后自动回到IDLE。状态机设计里最容易犯的错误是“漏了默认状态”。Verilog的case语句如果不写default综合后会生成一个锁存器行为会变得非常诡异。我自己的习惯是所有状态机必定写default: next_state IDLE;并配合(* full_case, parallel_case *)属性约束虽然这东西偶尔有争议但用好利大于弊。还有一个经验握手信号与状态切换的关系。摘机和挂机信号是按键产生的有抖动、有毛刺。如果直接用按键信号作为状态机的跳转条件很容易出现“按一下跳两个状态”的灵异现象。解决的方案有两个一是硬件上RC滤波二是在FPGA内部做边沿检测加消抖。前者要改板子不现实后者是软件方案可靠且改动小在下一节细说。2.5 显示驱动数码管动态扫描的细节数码管显示是很多初学者觉得“简单”但实际做起来又有很多猫腻的部分。6位数码管如果把每一位的段选信号都独立引出需要7x642根IO加小数点更多这在小型FPGA板子上根本不够用。所以几乎所有的开发板都用“动态扫描”方案所有数码管的段选信号共用一组IO再用位选信号轮流点亮某一位。动态扫描的原理就是“视觉暂留”——只要轮流点亮的频率足够高通常大于60Hz实际建议做到1kHz以上人眼看起来就像6位同时亮。实现时需要注意两个问题刷新频率不能太低也不能太高。太低会明显闪烁太高会影响每位能分到的点亮时间导致亮度不够。我实测下来6位数码管扫描频率1kHz左右效果最舒服也就是每位点亮时间约166微秒。这个时间远大于一个时钟周期20纳秒所以要用计数器分频产生扫描时钟而不是直接用在全局时钟里数位。位选切换和段选数据要同步。很多人写动态扫描是“先给段选再切位选”这会造成短暂的“串位”现象——前一位的残影和当前位的内容混在一起。正确做法是先把所有数据准备好位选切换之后再更新段选数据中间最好间隔一个时钟周期。更稳妥的做法是“先消隐再切换再送数据”。这里给一个推荐的扫描流程伪代码// 动态扫描控制流程 always (posedge clk_1k) begin // 第一步关掉所有位选 seg_en 4b0000; // 第二步切换当前要显示的位 case (scan_cnt) 2d0: begin digit_sel 3b001; disp_data bcd_0; end 2d1: begin digit_sel 3b010; disp_data bcd_1; end 2d2: begin digit_sel 3b100; disp_data bcd_2; end 2d3: begin digit_sel 3b000; disp_data bcd_3; end endcase // 第三步打开当前位的位选 seg_en digit_sel; end注意第二步里那个2d3: digit_sel 3b000;意思是第4个扫描周期不显示任何位留一个“空位”作为消隐间隔。这是消除串扰的土办法效果立竿见影。我一开始没加这个结果第3位和第4位之间总是有虚影调了半天才发现是切换瞬间段选数据串位了。3. 工程实现与完整实操流程3.1 工程文件结构与模块划分一个清晰的工程结构能让你在后期的调试和修改中节省大量时间。我这个示例工程的目录如下billing_meter/ ├── rtl/ # 所有RTL源码 │ ├── top_billing.v # 顶层模块例化所有子模块 │ ├── clk_div.v # 时钟分频模块 │ ├── key_debounce.v # 按键消抖模块 │ ├── billing_fsm.v # 计费状态机 │ ├── bcd_counter.v # BCD码费用累加器 │ ├── pulse_gen.v # 计费脉冲产生器 │ └── seg_display.v # 数码管动态扫描模块 ├── sim/ # 仿真文件 │ ├── tb_top_billing.v # 顶层模块仿真 │ ├── tb_bcd_counter.v # 计数器独立仿真 │ └── tb_fsm.v # 状态机分支仿真 └── constraint/ # 约束文件 └── billing_meter.xdc这个结构里RTL目录按模块划分每个模块一个文件文件名和模块名保持一致方便查阅。仿真目录独立放避免综合时误将testbench当设计源文件加入工程。约束文件单独放是因为很多版本的Vivado在添加源文件的时候如果约束文件混在RTL文件列表里可能导致编译顺序问题。3.2 顶层模块的信号互联顶层模块是整个设计的“总装车间”所有子模块在这里例化信号在这里互联。我建议在写顶层模块之前先画一张信号连接表把每个子模块的输入输出列出来再决定怎么连。这能避免写到一半才发现某个信号没有来源、某个模块的复位顺序不对。顶层模块的关键信号包括信号名方向来源/去向作用clk_50m输入板载晶振系统主时钟rst_n输入复位按键全局低电平复位key_hook输入摘机/挂机按键通话状态切换key_pause输入暂停按键暂停/恢复计费sw_fee[1:0]输入拨码开关费率选择seg_data[7:0]输出数码管段选段码输出seg_sel[5:0]输出数码管位选位选输出顶层模块的核心就是例化并连线但有一点必须提醒顶层模块里不要写任何具体逻辑。我在实际工程中见过不少人图省事在顶层直接把某个小功能用assign实现结果后期调试时连信号查半天都找不到。规范做法是顶层只做连接、复位分配和时钟管理一切具体逻辑都封装在子模块里。3.3 按键消抖与边沿检测手感是调出来的按键消抖的话题老生常谈但这个项目里因为按键承担的是“摘机/挂机”“暂停/恢复”这种状态切换功能一旦消抖没做好按一次键可能触发两次切换整个系统的状态就乱了。我来说说实际的做法。最简单的写法是检测到按键按下后延时20ms再读一次电平如果还是按下状态就认为有效。这个延时可以用计数器实现。但更好的方案是“边沿检测消抖”一体在每次采样时把当前电平打两拍两级触发器同步然后用“前一拍为高、当前拍为低”的组合判定为下降沿对于低有效按键。配合20ms内的连续多次采样确认能完美消除抖动影响。// 两级同步边沿检测低有效按键 reg [1:0] key_r; always (posedge clk or negedge rst_n) begin if (!rst_n) key_r 2b11; else key_r {key_r[0], key_in}; end wire key_press (key_r[1] 1b1 key_r[0] 1b0);这段代码出来的是一个单周期的脉冲正好用来做状态机的跳转触发信号。但注意这个脉冲只有一个时钟周期宽如果你的状态机在另一个时钟域采样可能需要做跨时钟域处理。本项目所有模块都用同一个主时钟所以不存在这个问题。手感方面还有一个细节如果按键是“按下有效、松开也有效”的类型需要分别检测上升沿和下降沿。比如摘机用下降沿挂机可以用同一个按键但要求检测“长按”或者“次数”这就复杂了。我建议直接拆成两个不同的按键至少演示时逻辑清晰、不容易误触。3.4 约束文件时钟约束和引脚分配在Vivado里约束文件XDC承担的是“告诉工具你的引脚怎么定义、你的时钟频率是多少”的职责。初学者常常忽略约束直接跑综合布局布线结果生成的bitstream下载到板子上完全没反应却不知道问题出在约束缺失上。这里给出一个最基本的XDC模板# 主时钟约束 create_clock -period 20.000 -name clk [get_ports clk] # 复位按键 set_property PACKAGE_PIN A14 [get_ports rst_n] set_property IOSTANDARD LVCMOS33 [get_ports rst_n] # 摘机/挂机按键 set_property PACKAGE_PIN B14 [get_ports key_hook] set_property IOSTANDARD LVCMOS33 [get_ports key_hook] # 数码管段选和位选 set_property PACKAGE_PIN C1 [get_ports {seg_data[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {seg_data[0]}] # ... 其他引脚类似关于时钟约束我多说两句。50MHz时钟对应的周期是20ns但如果你在代码里写了异步逻辑比如用按键信号直接做时钟或复位Vivado的时序分析工具根本检查不出来因为异步路径默认不约束。这就是为什么我要强调“全局统一时钟、按键信号只做电平/边沿检测、不直接作为时钟或异步复位”——这个习惯能让你的设计干干净净地过时序检查。3.5 仿真验证不用上板就能确认逻辑正确整个设计里我认为最值得投入时间的是仿真验证。上板调试就像在没有仪表的情况下开飞机而仿真就是你提前检查仪表盘的唯一手段。我在这个项目里花在写testbench上的时间比写RTL还多但正因如此上板后几乎一次通过没有遇到玄学问题。一个基础的testbench流程module tb_billing_top; reg clk; reg rst_n; reg key_hook; reg key_pause; reg [1:0] sw_fee; wire [7:0] seg_data; wire [5:0] seg_sel; // 时钟生成50MHz initial clk 0; always #10 clk ~clk; // 复位和按键激励 initial begin rst_n 0; key_hook 1; key_pause 1; sw_fee 2b00; #100; rst_n 1; // 模拟摘机 #5000 key_hook 0; #100 key_hook 1; // 模拟暂停 #20000 key_pause 0; #100 key_pause 1; #30000 $finish; end // 例化被测模块 billing_top uut ( .clk(clk), .rst_n(rst_n), .key_hook(key_hook), .key_pause(key_pause), .sw_fee(sw_fee), .seg_data(seg_data), .seg_sel(seg_sel) ); endmodule注意几个细节一是测试激励里的按键操作要留足抖动时间不要按下后马上释放要给消抖逻辑反应的时间二是仿真里要把计费周期参数调小比如改成2个秒脉冲就计费而不是5个否则仿真时间会非常长三是观察信号时重点看状态机的状态跳转和BCD计数器的进位行为而不是盯着数码管的最终输出。仿真跑通后还有一个重要步骤是“覆盖率检查”你的testbench有没有覆盖到暂停、恢复、费率切换、溢出这几个场景如果没覆盖到上板后极有可能在某个边角场景暴露问题。我的习惯是在每条testbench用例里加$display打印关键信号的变化把整个流程用文本记录下来对照预期行为逐条核对。4. 常见问题与调试干货4.1 现象数码管乱跳、数字闪得像跑马灯这是动态扫描类项目最经典的问题。原因大多数出在扫描时序上——位选切换和段选数据更新不同步或者扫描频率太低。我的排错顺序是查扫描频率用计数器把扫描时钟分频到1kHz左右不要直接用系统时钟去轮询数码管位。在示波器或逻辑分析仪上查看位选信号的频率是否稳定。查位选和段选波形相对关系位选从第0位切换到第1位时段选数据应该已经准备好。如果段选数据更新滞后了半个周期就会看到残影。查共阴共阳设置开发板手册写的是共阳极还是共阴极直接决定段码表是取反还是不取反。这个查错了显示的数字就是乱的但很多人第一反应是代码问题排查半天结果发现是最基础的电平关系反了。4.2 现象按一下按键状态跳了两个状态这是消抖没做好的典型表现。按键的机械抖动时间通常在5-20ms之间如果不做处理FPGA会在这个窗口内看到多次电平跳变。边沿检测只做了两级同步还不够必须加上“确认按下”的逻辑。解决办法主要有两种延时确认法检测到变化后启动一个20ms的计数器计数结束后再读一次电平确认。多次采样法以1ms为间隔连续采样20次要求其中至少有18次为同一电平才认为按键状态发生变化。我推荐第二种因为它不阻塞系统——延时确认法里那20ms如果放在主循环中会造成其他模块卡顿。而多次采样法只是多消耗一些寄存器资源对现代FPGA来说完全不是问题。4.3 现象计费金额偶尔多跳一次或少跳一次这个问题非常隐蔽通常和跨时钟域或脉冲宽度有关。比如计费脉冲被做成一个持续多个周期的电平信号而BCD计数器在每个上升沿都判定为有效导致一次计费多跳了几次。排查思路确认计费脉冲信号是“单周期脉冲”还是“持续电平”。如果是后者要么在计数器上加边沿检测要么改分频电路让它产生真正的单周期脉冲。检查秒脉冲到计费脉冲之间的逻辑链路确保每个秒脉冲最多只产生一个计费脉冲。用仿真可以很容易验证。检查暂停逻辑恢复计费时周期计数器是清零还是继续如果保留旧值就会出现“暂停前数了3秒恢复后数2秒就计费”的情况导致每次暂停后费用跳变。4.4 现象仿真一切正常上板后完全不工作仿真通过但上板失败的案例我见过太多了。最常见的原因是时钟约束缺失或者引脚约束写错。很多人一开始为了快速看到现象草草写了两个引脚约束就下载bitstream结果某些引脚被分配到不支持LVCMOS33电平标准的bank上或者时钟引脚写错导致FPGA内部时钟树没有接上。上板前的自查清单[ ] XDC里是否定义了主时钟约束create_clock写了吗[ ] 所有用到的引脚是否都有PACKAGE_PIN和IOSTANDARD约束[ ] 复位按键是高有效还是低有效XDC和代码里是否一致[ ] 数码管的共阳共阴设置和段码表是否匹配[ ] 下载时是否选了正确的bit文件如果你开了增量编译有时bit文件没更新4.5 实战心得仿真时间太长的处理技巧计费系统牵涉到秒脉冲仿真时不可能真的跑几秒时间去看效果。一个非常实用的技巧是把分频系数和计费周期做成模块的parameter仿真时通过defparam或直接改例化时的参数来覆盖默认值。// 在testbench里把分频系数改小加速仿真 defparam uut.clk_div_inst.MAX_CNT 25d50; // 仿真时用50个周期代替2500万个这样仿真中1秒钟在真实世界对应的是几千个时钟周期跑起来非常快同时验证的时序逻辑和真实情况完全一致。上板时保持默认的大参数即可。5. 扩展方向与个人体会这个项目做完之后如果你想继续深入有几个方向可以玩出花来串口通信把每次通话的起止时间、时长、费率、费用通过UART发送到电脑上位机做成一个完整的“计费管理后台”。这需要增加一个UART收发模块但代码量不大难度适中。实时钟RTC在计费器里加一个实时钟模块按时间段设置不同费率比如夜间半价这需要处理比较复杂的日历计数逻辑但对逻辑设计能力的锻炼非常有效。存费扣费功能做一个预付费系统用户先充入金额通话时实时扣减余额不足时强制挂机。这就涉及减法逻辑、欠费状态判断和告警输出商业场景的味道一下就出来了。换一个IP核数码管显示改成LCD1602或OLED显示内容更丰富但驱动时序又会带来新的挑战。我个人在实际操作中最大的收获是FPGA设计的思路和软件不太一样。写软件时可以随意调用函数、随时打印调试信息但写FPGA每个模块都在“同时跑”你必须在设计阶段就把信号流想清楚仿真时把所有可能场景覆盖到位上板才自信。这次计费器项目虽然不大但状态机、时序约束、模块化设计这些核心技巧都练到了。最后再分享一个小技巧状态机的状态编码不要用默认的连续0/1/2/3改成独热码One-Hot。独热码每个状态只有一位为1状态译码逻辑简单而且在看波形的时候一眼就能看出当前状态调试体验极好。代价是多用几个寄存器对这个小项目来说完全无所谓。如果你想让数字逻辑更省资源再用二进制编码也不迟——但在学习和调试阶段优先选“看起来舒服”的方案真的能让你少很多折腾。本文还有配套的精品资源点击获取
返回列表