diff --git a/package-lock.json b/package-lock.json index 0640e94..9dcaef7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@svgdotjs/svg.js": "^3.2.4", "@types/lodash": "^4.17.16", "async-mutex": "^0.5.0", + "highlight.js": "^11.11.1", "lodash": "^4.17.21", "log-symbols": "^7.0.0", "marked": "^12.0.0", @@ -2586,6 +2587,15 @@ "he": "bin/he" } }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/hookable": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", diff --git a/package.json b/package.json index 16d2349..ac7b8d0 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@svgdotjs/svg.js": "^3.2.4", "@types/lodash": "^4.17.16", "async-mutex": "^0.5.0", + "highlight.js": "^11.11.1", "lodash": "^4.17.21", "log-symbols": "^7.0.0", "marked": "^12.0.0", diff --git a/public/doc/01_water_led/water_led.md b/public/doc/01/doc.md similarity index 100% rename from public/doc/01_water_led/water_led.md rename to public/doc/01/doc.md diff --git a/public/doc/01_water_led/images/1.png b/public/doc/01/images/1.png similarity index 100% rename from public/doc/01_water_led/images/1.png rename to public/doc/01/images/1.png diff --git a/public/doc/01_water_led/images/2.png b/public/doc/01/images/2.png similarity index 100% rename from public/doc/01_water_led/images/2.png rename to public/doc/01/images/2.png diff --git a/public/doc/01_water_led/images/3.png b/public/doc/01/images/3.png similarity index 100% rename from public/doc/01_water_led/images/3.png rename to public/doc/01/images/3.png diff --git a/public/doc/01_water_led/images/4.png b/public/doc/01/images/4.png similarity index 100% rename from public/doc/01_water_led/images/4.png rename to public/doc/01/images/4.png diff --git a/public/doc/02_key/key.md b/public/doc/02/doc.md similarity index 100% rename from public/doc/02_key/key.md rename to public/doc/02/doc.md diff --git a/public/doc/02_key/images/1.png b/public/doc/02/images/1.png similarity index 100% rename from public/doc/02_key/images/1.png rename to public/doc/02/images/1.png diff --git a/public/doc/02_key/images/2.png b/public/doc/02/images/2.png similarity index 100% rename from public/doc/02_key/images/2.png rename to public/doc/02/images/2.png diff --git a/public/doc/02_key/images/3.png b/public/doc/02/images/3.png similarity index 100% rename from public/doc/02_key/images/3.png rename to public/doc/02/images/3.png diff --git a/public/doc/02_key/images/4.png b/public/doc/02/images/4.png similarity index 100% rename from public/doc/02_key/images/4.png rename to public/doc/02/images/4.png diff --git a/public/doc/02_key/images/5.png b/public/doc/02/images/5.png similarity index 100% rename from public/doc/02_key/images/5.png rename to public/doc/02/images/5.png diff --git a/public/doc/02_key/images/6.png b/public/doc/02/images/6.png similarity index 100% rename from public/doc/02_key/images/6.png rename to public/doc/02/images/6.png diff --git a/public/doc/02_key/images/7.png b/public/doc/02/images/7.png similarity index 100% rename from public/doc/02_key/images/7.png rename to public/doc/02/images/7.png diff --git a/public/doc/11/doc.md b/public/doc/11/doc.md new file mode 100644 index 0000000..fefa7c7 --- /dev/null +++ b/public/doc/11/doc.md @@ -0,0 +1,400 @@ +# 进阶-1-密码锁实验 + +## 1.1 章节导读 +本章作为进阶的第一个实验,主要学习状态机的写法和使用,同时联系前面所学的数码管和矩阵键盘,完成一个密码锁的设计。 + + +## 1.2 理论学习 +### 1.2.1 FSM状态机 + +在数字逻辑设计中,**有限状态机(FSM, Finite State Machine)**是一种根据输入和当前状态决定下一个状态和输出的模型,广泛用于顺序逻辑电路的控制部分。 + +在本实验中,我们将使用 FSM 构建密码锁的控制逻辑,用于管理**按键输入过程、密码比对、开锁显示、错误处理等多个步骤**。 + +FSM 通常包含以下几个组成部分: + +- **状态定义(State)**:用来描述系统当前所处的逻辑阶段。例如:待输入、输入中、校验中、成功、失败等。 + +- **状态转移条件(Transition)**:根据输入信号(如按键、定时器、复位)从一个状态跳转到另一个状态。 + +- **输出控制(Output)**:每个状态下系统应有的行为,比如更新数码管、检测密码、拉高开锁信号等。 + +常见的 FSM 类型包括: + +- **Moore 状态机:**输出只与当前状态有关,结构更稳定; +- **Mealy 状态机:**输出与当前状态和输入有关,反应更灵敏。 + +在本例中我们要设计一个状态机去对密码锁进行控制。首先我们应该先给密码锁分一下他会处于什么状态,每个状态有什么输出(本例中将密码锁设计成下述4个状态): + +1. SETUP状态:该状态下可以设置4位密码,输入4位数字后按#键设置密码有效,*清空设置,数码管输出4位数字输入 +2. LOCK状态:锁定状态,可以输入密码解锁,按#确定,*键清空输入,数码管输出4位数字输入 +3. ERROR状态:如果输入密码错误,或者操作错误,进入此状态,数码管输出ERROR +4. UNLOCK状态:解锁状态,可以按*重设密码,也可以按#重新锁定,数码管输出UNLOCK + +然后确定状态之间如何进行转移: + +1. SETUP状态:输入4位数字后按#键设置密码有效,有效后进入LOCK状态 +2. LOCK状态:输入密码,按#确定后如果密码正确进入UNLOCK状态,如果错误进入ERROR状态 +3. ERROR状态:按下任意按键后进入LOCK状态 +4. UNLOCK状态:按下#键进入LOCK状态,按*键进入SETUP状态重设密码 + +根据上述状态转移逻辑,我们可以画出状态转移图,状态转移图如下图所示: + +
+
+ 无法显示图片时显示的文字 +
+ 图1.状态转移图 +
+
+ +我们已经了解了本次实验所使用的状态机,那么如何使用verilog编写状态机呢?主要有三种方法,分别是:三段式状态机,二段式状态机,一段式状态机。 + +三段式状态机写法如下: + +- 状态机第一段,时序逻辑,非阻塞赋值,传递寄存器的状态。 +- 状态机第二段,组合逻辑,阻塞赋值,根据当前状态和当前输入,确定下一个状态机的状态。 +- 状态机第三代,时序逻辑,非阻塞赋值,因为是 Mealy 型状态机,根据当前状态和当前输入,确定输出信号。 + +二段式状态机将三段式状态机二三段糅合在一起,一段式状态机则将三段式状态机三段融合。推荐使用三段式状态机,只有在状态转移逻辑非常简单,状态很少时会采用一段式状态机。 + +### 1.2.2 数码管 + +见基础实验3 + +### 1.2.3 矩阵键盘 + +见基础实验4 + +## 1.3 实战演练 +### 1.3.1 系统架构 +``` verilog +系统框图: +``` + +### 1.3.2 模块设计 +首先是密码锁状态机逻辑,本例采用三段式状态机写法。代码如下: + +#### password_lock + +```verilog +module password_lock( + input wire clk, + input wire rstn, + input wire [15:0] key_trigger, + output reg [8*8-1:0] assic_seg, + output wire [7:0] seg_point +); + +/* + K00 K01 K02 K03 | 1 2 3 A + | + K04 K05 K06 K07 | 4 5 6 B + | + K08 K09 K10 K11 | 7 8 9 C + | + K12 K13 K14 K15 | * 0 # D +*/ + +/* +密码锁状态机设定: +1. SETUP状态 :设置密码,按*清空输入,按#确认输入进入LOCK状态,不足4位#键无效 +2. LOCK状态 :锁定状态,按*清空输入,按#确认输入,不足4位#键无效,密码正确解锁,错误则进入ERROR状态 +3. ERROR状态 :密码错误状态,按任意键返回LCOK状态 +4. UNLOCK状态:解锁状态,按*重设密码,按#重新锁定,其余键无效 + +1-D键为输入 +*为清空之前的输入 +#为确认输入 + +*/ + +wire flag_setup_password; +wire flag_input_pass; +wire flag_input_confirm; +wire flag_error_return; +wire flag_relock; +wire flag_reset; + +localparam [2:0] ST_SETUP = 3'b001; +localparam [2:0] ST_LOCK = 3'b010; +localparam [2:0] ST_ERROR = 3'b100; +localparam [2:0] ST_UNLOCK = 3'b101; + +reg [2:0] cu_st, nt_st; +reg [4*4-1:0] password, input_password; +reg [2:0] input_num; + +assign flag_setup_password = (cu_st == ST_SETUP) && (key_trigger[14]) && (input_num == 3'b100); +assign flag_input_confirm = (cu_st == ST_LOCK) && (key_trigger[14]) && (input_num == 3'b100); +assign flag_input_pass = (cu_st == ST_LOCK) && (password == input_password) && (input_num == 3'b100); +assign flag_error_return = (cu_st == ST_ERROR) && (|key_trigger); +assign flag_relock = (cu_st == ST_UNLOCK) && (key_trigger[14]); +assign flag_reset = (cu_st == ST_UNLOCK) && (key_trigger[12]); +//状态机第一段,传递寄存器状态 +always @(posedge clk or negedge rstn) begin + if(~rstn) cu_st <= ST_SETUP; + else cu_st <= nt_st; +end +//状态机第二段,确定下一个状态机状态 +always @(*) begin + case(cu_st) + ST_SETUP : nt_st <= (flag_setup_password)?(ST_LOCK):(ST_SETUP); + ST_LOCK : nt_st <= (flag_input_confirm)?((flag_input_pass)?(ST_UNLOCK):(ST_ERROR)):(ST_LOCK); + ST_ERROR : nt_st <= (flag_error_return)?(ST_LOCK):(ST_ERROR); + ST_UNLOCK: nt_st <= (flag_relock)?(ST_LOCK):((flag_reset)?(ST_SETUP):(ST_UNLOCK)); + default : nt_st <= ST_SETUP; + endcase +end +//状态机第三段,根据状态和输入确定输出,这里由于信号较多,分了多个always块,也可以用case语句写在同一个always块中 +always @(posedge clk or negedge rstn) begin + if(~rstn) password <= 0; + else if((cu_st == ST_SETUP) && (input_num != 3'b100)) begin + if(key_trigger[00]) password <= {password[0+:3*4], 4'h1}; + else if(key_trigger[01]) password <= {password[0+:3*4], 4'h2}; + else if(key_trigger[02]) password <= {password[0+:3*4], 4'h3}; + else if(key_trigger[03]) password <= {password[0+:3*4], 4'hA}; + else if(key_trigger[04]) password <= {password[0+:3*4], 4'h4}; + else if(key_trigger[05]) password <= {password[0+:3*4], 4'h5}; + else if(key_trigger[06]) password <= {password[0+:3*4], 4'h6}; + else if(key_trigger[07]) password <= {password[0+:3*4], 4'hB}; + else if(key_trigger[08]) password <= {password[0+:3*4], 4'h7}; + else if(key_trigger[09]) password <= {password[0+:3*4], 4'h8}; + else if(key_trigger[10]) password <= {password[0+:3*4], 4'h9}; + else if(key_trigger[11]) password <= {password[0+:3*4], 4'hC}; + else if(key_trigger[12]) password <= 0; + else if(key_trigger[13]) password <= {password[0+:3*4], 4'h0}; + else if(key_trigger[14]) password <= password; + else if(key_trigger[15]) password <= {password[0+:3*4], 4'hD}; + else password <= password; + end else password <= password; +end + +always @(posedge clk or negedge rstn) begin + if(~rstn) input_password <= 0; + else if(cu_st == ST_LOCK) begin + if(input_num == 3'b100) input_password <= input_password; + else if(key_trigger[00]) input_password <= {input_password[0+:3*4], 4'h1}; + else if(key_trigger[01]) input_password <= {input_password[0+:3*4], 4'h2}; + else if(key_trigger[02]) input_password <= {input_password[0+:3*4], 4'h3}; + else if(key_trigger[03]) input_password <= {input_password[0+:3*4], 4'hA}; + else if(key_trigger[04]) input_password <= {input_password[0+:3*4], 4'h4}; + else if(key_trigger[05]) input_password <= {input_password[0+:3*4], 4'h5}; + else if(key_trigger[06]) input_password <= {input_password[0+:3*4], 4'h6}; + else if(key_trigger[07]) input_password <= {input_password[0+:3*4], 4'hB}; + else if(key_trigger[08]) input_password <= {input_password[0+:3*4], 4'h7}; + else if(key_trigger[09]) input_password <= {input_password[0+:3*4], 4'h8}; + else if(key_trigger[10]) input_password <= {input_password[0+:3*4], 4'h9}; + else if(key_trigger[11]) input_password <= {input_password[0+:3*4], 4'hC}; + else if(key_trigger[12]) input_password <= 0; + else if(key_trigger[13]) input_password <= {input_password[0+:3*4], 4'h0}; + else if(key_trigger[14]) input_password <= input_password; + else if(key_trigger[15]) input_password <= {input_password[0+:3*4], 4'hD}; + else input_password <= input_password; + end else input_password <= 0; +end + +always @(posedge clk or negedge rstn) begin + if(~rstn) input_num <= 0; + else if(cu_st == ST_SETUP || cu_st == ST_LOCK) begin + if(flag_setup_password || flag_input_confirm) input_num <= 0; + else if(key_trigger[00] || key_trigger[01] || key_trigger[02] || key_trigger[03] || + key_trigger[04] || key_trigger[05] || key_trigger[06] || key_trigger[07] || + key_trigger[08] || key_trigger[09] || key_trigger[10] || key_trigger[11] || + key_trigger[13] || key_trigger[15]) + input_num <= (input_num < 3'b100)?(input_num + 1):(input_num); + else if(key_trigger[12]) input_num <= 0; + else input_num <= input_num; + end else input_num <= 0; +end + +assign seg_point = 8'b0; +always @(posedge clk or negedge rstn) begin + if(~rstn) assic_seg <= "12345678"; + else case(cu_st) + ST_SETUP :begin + assic_seg[0+:8] <= "-"; + assic_seg[8+:8] <= "-"; + assic_seg[16+:8] <= (input_num > 0)?(hex2assic(password[0+:4])):("_"); + assic_seg[24+:8] <= (input_num > 1)?(hex2assic(password[4+:4])):("_"); + assic_seg[32+:8] <= (input_num > 2)?(hex2assic(password[8+:4])):("_"); + assic_seg[40+:8] <= (input_num > 3)?(hex2assic(password[12+:4])):("_"); + assic_seg[48+:8] <= "-"; + assic_seg[56+:8] <= "-"; + end + ST_LOCK :begin + assic_seg[0+:8] <= "="; + assic_seg[8+:8] <= "="; + assic_seg[16+:8] <= (input_num > 0)?(hex2assic(input_password[0+:4])):("-"); + assic_seg[24+:8] <= (input_num > 1)?(hex2assic(input_password[4+:4])):("-"); + assic_seg[32+:8] <= (input_num > 2)?(hex2assic(input_password[8+:4])):("-"); + assic_seg[40+:8] <= (input_num > 3)?(hex2assic(input_password[12+:4])):("-"); + assic_seg[48+:8] <= "="; + assic_seg[56+:8] <= "="; + end + ST_ERROR : assic_seg <= " ERROR "; + ST_UNLOCK: assic_seg <= " unlock "; + default : assic_seg <= "12345678"; + endcase +end + + +function [7:0] hex2assic; + input [3:0] hex; + case(hex) + 4'h0: hex2assic = "0"; // 0 + 4'h1: hex2assic = "1"; // 1 + 4'h2: hex2assic = "2"; // 2 + 4'h3: hex2assic = "3"; // 3 + 4'h4: hex2assic = "4"; // 4 + 4'h5: hex2assic = "5"; // 5 + 4'h6: hex2assic = "6"; // 6 + 4'h7: hex2assic = "7"; // 7 + 4'h8: hex2assic = "8"; // 8 + 4'h9: hex2assic = "9"; // 9 + 4'hA: hex2assic = "A"; // A + 4'hB: hex2assic = "B"; // B + 4'hC: hex2assic = "C"; // C + 4'hD: hex2assic = "D"; // D + 4'hE: hex2assic = "E"; // E + 4'hF: hex2assic = "F"; // F + default: hex2assic = " "; + endcase +endfunction + +endmodule //password_lock +``` + +矩阵键盘行扫描模块在前面基础实验已经介绍过,但这次实验还需要为矩阵键盘添加按键上升沿检测模块,代码如下: + +#### matrix_key_trigger + +```verilog +module matrix_key_trigger( + input wire clk, + input wire rstn, + input wire [15:0] key, + output wire [15:0] key_trigger +); + +// 按键上升沿捕获模块 + +reg [15:0] key_d; // 上一时钟周期的按键状态 +reg [15:0] key_d2; // 上两时钟周期的按键状态 + +assign key_trigger = (key_d) & (~key_d2); + +always @(posedge clk or negedge rstn) begin + if (!rstn) begin + key_d <= 0; + key_d2 <= 0; + end else begin + key_d <= key; + key_d2 <= key_d; + end +end + +endmodule //matrix_key_decode +``` + +至于数码管模块,为了方便,在led_display_driver模块添加了参数定义,并未进行其他修改。 + +最后将几个模块例化在顶层,将端口相连接,代码如下所示: + +#### password_lock_top + +```verilog + +module password_lock_top #( + parameter VALID_SIGNAL = 1'b0, + parameter CLK_CYCLE = 5000 +)( +//system io +input wire external_clk , +input wire external_rstn, + +output wire [7:0] led_display_seg, +output wire [7:0] led_display_sel, + +input wire [3:0] col, +output wire [3:0] row +); + +wire [15:0] key_out; +wire [15:0] key_trigger; +wire [8*8-1:0] assic_seg; +wire [7:0] seg_point; + +led_display_driver #( + .VALID_SIGNAL (VALID_SIGNAL), + .CLK_CYCLE (CLK_CYCLE) +)u_led_display_driver( + .clk ( external_clk ), + .rstn ( external_rstn ), + .assic_seg ( assic_seg ), + .seg_point ( seg_point ), + .led_display_seg ( led_display_seg ), + .led_display_sel ( led_display_sel ) +); + +matrix_key #( + .ROW_NUM ( 4 ), + .COL_NUM ( 4 ), + .DEBOUNCE_TIME ( 10000 ), + .DELAY_TIME ( 2000 )) +u_matrix_key( + .clk ( external_clk ), + .rstn ( external_rstn ), + .row ( row ), + .col ( col ), + .key_out ( key_out ) +); + +matrix_key_trigger u_matrix_key_trigger( + .clk ( external_clk ), + .rstn ( external_rstn), + .key ( key_out ), + .key_trigger ( key_trigger ) +); + +password_lock u_password_lock( + .clk ( external_clk ), + .rstn ( external_rstn), + .key_trigger ( key_trigger ), + .assic_seg ( assic_seg ), + .seg_point ( seg_point ) +); + +endmodule //led_diaplay_top + +``` + +### 1.3.3 上板验证步骤 + +--- + +可以直接将矩阵键盘,数码管的管脚约束文件中的约束复制到本次实验的管脚约束文件中。 + +将生成的sbit文件烧录好后,即可使用网页界面的虚拟按键进行使用。 + +## 1.4 章末总结 + +本章通过设计一个简易密码锁系统,综合运用了前面基础实验中学习的**矩阵键盘扫描**、**数码管显示**等知识,并引入了**有限状态机(FSM)**的设计方法,完成了一个具有较强工程实用性的综合实验。 + +通过本实验,你应该掌握了以下几点核心能力: + +- 理解并运用 状态机进行系统流程控制; +- 将多个功能模块(键盘、数码管、比较器)整合为一个完整系统; +- 设计基于状态的控制逻辑,实现密码输入、校验、反馈显示等功能; +- 理解数字电路系统中控制与数据路径的分离思想。 + +密码锁系统虽然逻辑简单,但已经具备了完整嵌入式控制系统的基本结构,是后续更复杂项目设计的重要基础。 + +## 1.5 拓展训练 + +为了进一步加深对本实验内容的理解,并锻炼系统设计与工程实现能力,你可以尝试完成以下拓展任务: + +1. **增加防爆破机制**:限定密码错误尝试次数,例如连续三次错误后锁定一段时间,并在数码管上提示“Err”。 +2. **利用按键实现简易菜单系统**拓展状态机结构,允许通过矩阵键盘导航菜单,如“输入密码”、“查看状态”、“设置新密码”等。 diff --git a/public/doc/11/images/1.png b/public/doc/11/images/1.png new file mode 100644 index 0000000..c226fe5 Binary files /dev/null and b/public/doc/11/images/1.png differ diff --git a/public/doc/11/images/UDP.png b/public/doc/11/images/UDP.png new file mode 100644 index 0000000..02293d5 Binary files /dev/null and b/public/doc/11/images/UDP.png differ diff --git a/server/src/TutorialController.cs b/server/src/TutorialController.cs new file mode 100644 index 0000000..f5ffb01 --- /dev/null +++ b/server/src/TutorialController.cs @@ -0,0 +1,55 @@ +using System.IO; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; + +namespace server.Controllers; + +/// +/// 教程 API +/// +[ApiController] +[Route("api/[controller]")] +public class TutorialController : ControllerBase +{ + private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + private readonly IWebHostEnvironment _environment; + + public TutorialController(IWebHostEnvironment environment) + { + _environment = environment; + } + + /// + /// 获取所有可用的教程目录 + /// + /// 教程目录列表 + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult GetTutorials() + { + try + { + // 获取文档目录 + string docPath = Path.Combine(_environment.WebRootPath, "doc"); + + if (!Directory.Exists(docPath)) + { + return Ok(new { tutorials = new List() }); + } + + // 获取所有子目录 + var directories = Directory.GetDirectories(docPath) + .Select(Path.GetFileName) + .Where(dir => !string.IsNullOrEmpty(dir)) + .ToList(); + + return Ok(new { tutorials = directories }); + } + catch (Exception ex) + { + logger.Error(ex, "获取教程目录失败"); + return StatusCode(500, new { error = "无法读取教程目录" }); + } + } +} diff --git a/server/src/TutorialService.ts b/server/src/TutorialService.ts new file mode 100644 index 0000000..6dd953d --- /dev/null +++ b/server/src/TutorialService.ts @@ -0,0 +1,30 @@ +// 此接口提供获取例程目录服务 +// GET /api/tutorials 返回所有可用的例程目录 + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { Request, Response } from 'express'; + +// 获取当前文件的目录 +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const publicDir = path.resolve(__dirname, '../public'); + +export function getTutorials(req: Request, res: Response) { + try { + const docDir = path.join(publicDir, 'doc'); + + // 读取doc目录下的所有文件夹 + const entries = fs.readdirSync(docDir, { withFileTypes: true }); + const dirs = entries + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name); + + // 返回文件夹列表 + res.json({ tutorials: dirs }); + } catch (error) { + console.error('获取例程目录失败:', error); + res.status(500).json({ error: '无法读取例程目录' }); + } +} diff --git a/src/components/MarkdownRenderer.vue b/src/components/MarkdownRenderer.vue index 8e631a2..4015a80 100644 --- a/src/components/MarkdownRenderer.vue +++ b/src/components/MarkdownRenderer.vue @@ -1,26 +1,67 @@