Merge branch 'master' into csharp

This commit is contained in:
SikongJueluo 2025-05-20 18:14:00 +08:00
commit 970a537391
No known key found for this signature in database
71 changed files with 3842 additions and 95 deletions

10
package-lock.json generated
View File

@ -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",
@ -2587,6 +2588,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",

View File

@ -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",

BIN
public/doc/01/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 2.9 MiB

After

Width:  |  Height:  |  Size: 2.9 MiB

View File

Before

Width:  |  Height:  |  Size: 620 KiB

After

Width:  |  Height:  |  Size: 620 KiB

View File

Before

Width:  |  Height:  |  Size: 635 KiB

After

Width:  |  Height:  |  Size: 635 KiB

BIN
public/doc/02/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 821 KiB

After

Width:  |  Height:  |  Size: 821 KiB

View File

Before

Width:  |  Height:  |  Size: 434 KiB

After

Width:  |  Height:  |  Size: 434 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
public/doc/03/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

244
public/doc/03/doc.md Normal file
View File

@ -0,0 +1,244 @@
# 基础-3-数码管实验
---
在许多项目设计中,我们通常需要一些显示设备来显示我们需要的信息,可以选择的显示设备有很多,而数码管是使用最多,最简单的显示设备之一。
## 3.1 章节导读
本章将通过数码管驱动实验讲解FPGA数字系统中重要的"选通控制"概念。读者将学习到:
1. 数码管工作原理与动态扫描技术
2. 多路复用Multiplexing设计方法
3. 参数化模块设计技巧
4. 外设驱动时序规划
5. ASCII到段码的转换原理
实验将使用Verilog HDL实现一个支持8位数码管显示、包含字符动态滚动和选通控制的完整系统。
## 3.2 理论学习
### 3.2.1 数码管结构
- 7段数码管组成A-G段+DP小数点
- 共阳/共阴类型区分(本实验采用共阳型,低电平有效)
### 3.2.2 动态扫描原理
```
显示周期 = 刷新周期 × 数码管数量
人眼视觉暂留效应(>60Hz
扫描频率计算公式f_scan = f_clk / CLK_CYCLE
```
### 3.2.3 关键技术
- 时分复用:分时选通数码管
- 段码生成ASCII字符到七段码转换
- 消隐处理:消除切换时的视觉残留
### 3.2.4 设计指标
| 参数 | 值 | 说明 |
|-------|-----|-------------------|
| 位数 | 8 | 数码管数量 |
| 频率 | 200Hz | 单管刷新频率 |
| 分辨率 | 8bit | 段码控制(含小数点)|
## 3.2 实战演练
### 3.3.1 系统架构
```verilog
系统框图:
[Top模块] → [显示驱动模块] → [选通控制模块]
↖ ASCII数据生成 ↙ 时钟分频
```
### 3.3.2 模块设计
#### led_display_selector
```verilog
module led_display_selector #(
parameter NUM = 4,
parameter VALID_SIGNAL = 1'b0,
parameter CLK_CYCLE = 1000
)(
input wire clk,
input wire rstn,
input wire [NUM*8-1:0] led_in,
output reg [7:0] led_display_seg,//[DP,G,F,E,D,C,B,A]
output reg [NUM-1:0] led_display_sel
);
reg [31:0] clk_cnt;
always @(posedge clk or negedge rstn) begin
if (!rstn) clk_cnt <= 0;
else if(clk_cnt == CLK_CYCLE) clk_cnt <= 0;
else clk_cnt <= clk_cnt + 1;
end
wire seg_change = (clk_cnt == CLK_CYCLE) ? 1'b1 : 1'b0;
always @(posedge clk or negedge rstn) begin
if(!rstn) led_display_sel <= {{(NUM-1){~VALID_SIGNAL}}, VALID_SIGNAL};
else if (seg_change) led_display_sel <= {led_display_sel[NUM-2:0], led_display_sel[NUM-1]};
else led_display_sel <= led_display_sel;
end
integer i;
always @(*) begin
for(i=0;i<NUM;i=i+1) begin
if(led_display_sel[NUM-1-i] == VALID_SIGNAL)
led_display_seg = led_in[i*8 +: 8] ^ ({8{~VALID_SIGNAL}});
end
end
endmodule //led_display_ctrl
```
#### led_display_driver
```verilog
module led_display_driver(// 8个数码管显示阳极管在selector中已经做了阴阳处理
input wire clk,
input wire rstn,
input wire [8*8-1:0] assic_seg, //ASSIC coding
input wire [7:0] seg_point, //显示小数点
output wire [7:0] led_display_seg,
output wire [7:0] led_display_sel
);
reg [8*8-1:0] led_in;
integer i;
always @(*) begin
led_in = 0;
for(i=0;i<8;i=i+1) begin //led_in[i*8 +: 8] <---> assic_seg[i*8 +: 8]
case (assic_seg[i*8 +: 8])
"0": led_in[i*8 +: 8] = (8'h3f) | {seg_point[i],7'b0};
"1": led_in[i*8 +: 8] = (8'h06) | {seg_point[i],7'b0};
"2": led_in[i*8 +: 8] = (8'h5b) | {seg_point[i],7'b0};
"3": led_in[i*8 +: 8] = (8'h4f) | {seg_point[i],7'b0};
"4": led_in[i*8 +: 8] = (8'h66) | {seg_point[i],7'b0};
"5": led_in[i*8 +: 8] = (8'h6d) | {seg_point[i],7'b0};
"6": led_in[i*8 +: 8] = (8'h7d) | {seg_point[i],7'b0};
"7": led_in[i*8 +: 8] = (8'h07) | {seg_point[i],7'b0};
"8": led_in[i*8 +: 8] = (8'h7f) | {seg_point[i],7'b0};
"9": led_in[i*8 +: 8] = (8'h6f) | {seg_point[i],7'b0};
"A","a": led_in[i*8 +: 8] = (8'h77) | {seg_point[i],7'b0};
"B","b": led_in[i*8 +: 8] = (8'h7c) | {seg_point[i],7'b0};
"C","c": led_in[i*8 +: 8] = (8'h39) | {seg_point[i],7'b0};
"D","d": led_in[i*8 +: 8] = (8'h5e) | {seg_point[i],7'b0};
"E","e": led_in[i*8 +: 8] = (8'h79) | {seg_point[i],7'b0};
"F","f": led_in[i*8 +: 8] = (8'h71) | {seg_point[i],7'b0};
"G","g": led_in[i*8 +: 8] = (8'h3d) | {seg_point[i],7'b0};
"H","h": led_in[i*8 +: 8] = (8'h76) | {seg_point[i],7'b0};
"I","i": led_in[i*8 +: 8] = (8'h0f) | {seg_point[i],7'b0};
"J","j": led_in[i*8 +: 8] = (8'h0e) | {seg_point[i],7'b0};
"K","k": led_in[i*8 +: 8] = (8'h75) | {seg_point[i],7'b0};
"L","l": led_in[i*8 +: 8] = (8'h38) | {seg_point[i],7'b0};
"M","m": led_in[i*8 +: 8] = (8'h37) | {seg_point[i],7'b0};
"N","n": led_in[i*8 +: 8] = (8'h54) | {seg_point[i],7'b0};
"O","o": led_in[i*8 +: 8] = (8'h5c) | {seg_point[i],7'b0};
"P","p": led_in[i*8 +: 8] = (8'h73) | {seg_point[i],7'b0};
"Q","q": led_in[i*8 +: 8] = (8'h67) | {seg_point[i],7'b0};
"R","r": led_in[i*8 +: 8] = (8'h31) | {seg_point[i],7'b0};
"S","s": led_in[i*8 +: 8] = (8'h49) | {seg_point[i],7'b0};
"T","t": led_in[i*8 +: 8] = (8'h78) | {seg_point[i],7'b0};
"U","u": led_in[i*8 +: 8] = (8'h3e) | {seg_point[i],7'b0};
"V","v": led_in[i*8 +: 8] = (8'h1c) | {seg_point[i],7'b0};
"W","w": led_in[i*8 +: 8] = (8'h7e) | {seg_point[i],7'b0};
"X","x": led_in[i*8 +: 8] = (8'h64) | {seg_point[i],7'b0};
"Y","y": led_in[i*8 +: 8] = (8'h6e) | {seg_point[i],7'b0};
"Z","z": led_in[i*8 +: 8] = (8'h59) | {seg_point[i],7'b0};
" ": led_in[i*8 +: 8] = (8'h00) | {seg_point[i],7'b0};
"-": led_in[i*8 +: 8] = (8'h40) | {seg_point[i],7'b0};
"_": led_in[i*8 +: 8] = (8'h08) | {seg_point[i],7'b0};
"=": led_in[i*8 +: 8] = (8'h48) | {seg_point[i],7'b0};
"+": led_in[i*8 +: 8] = (8'h5c) | {seg_point[i],7'b0};
"(": led_in[i*8 +: 8] = (8'h39) | {seg_point[i],7'b0};
")": led_in[i*8 +: 8] = (8'h0F) | {seg_point[i],7'b0};
default: led_in[i*8 +: 8] = (8'h00) | {seg_point[i],7'b0};
endcase
end
end
led_display_selector #(
.NUM ( 8 ),
.VALID_SIGNAL ( 1'b0 ), //阳极管,低电平亮
.CLK_CYCLE ( 5000 ))
u_led_display_selector(
.clk ( clk ),
.rstn ( rstn ),
.led_in ( led_in ),
.led_display_seg ( led_display_seg ),
.led_display_sel ( led_display_sel )
);
endmodule //moduleName
```
#### led_display_top
```verilog
module led_diaplay_top(
//system io
input wire external_clk ,
input wire external_rstn,
//led display io
output wire [7:0] led_display_seg,
output wire [7:0] led_display_sel
);
reg [43*8-1:0] assic_seg;
reg [7:0] seg_point;
reg [31:0] clk_cnt;
always @(posedge external_clk or negedge external_rstn) begin
if(!external_rstn) clk_cnt <= 0;
else clk_cnt <= clk_cnt + 1;
end
always @(posedge external_clk or negedge external_rstn) begin
if(!external_rstn) begin
assic_seg <= "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ -_=+()";
seg_point <= 8'b00000001;
end else if({clk_cnt[24]==1'b1} && (clk_cnt[23:0]==25'b0))begin
assic_seg <= {assic_seg[8*43-8-1:0], assic_seg[8*43-1 -: 8]};
seg_point <= {seg_point[6:0], seg_point[7]};
end else begin
assic_seg <= assic_seg;
seg_point <= seg_point;
end
end
led_display_driver u_led_display_driver(
.clk ( external_clk ),
.rstn ( external_rstn ),
.assic_seg ( assic_seg[8*43-1 -: 8*8] ),
.seg_point ( seg_point ),
.led_display_seg ( led_display_seg ),
.led_display_sel ( led_display_sel )
);
endmodule //led_diaplay_top
```
### 3.3.3 上板验证步骤
1. 设置参数CLK_CYCLE=5000对应200Hz扫描频率
2. 绑定管脚:连接数码管段选/位选信号
3. 观察现象:字符"01234567"应稳定显示
4. 修改assic_seg初始值验证滚动功能
---
## 3.4 章末总结
**关键收获:**
1. 掌握动态扫描消除器件闪烁的原理
2. 理解参数化设计NUM/VALID_SIGNAL的优势
3. 学习时序控制中计数器的重要作用
4. 实践ASCII到硬件编码的转换方法
**设计亮点:**
- 支持阴阳极自动适配通过VALID_SIGNAL参数
- 字符环形缓冲区实现无缝滚动
- 参数化设计增强模块复用性
---
## 3.5 拓展训练
结合流水灯实验和数码管实验:数码管显示数字,标识出当前流水到了哪一个灯

BIN
public/doc/04/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

212
public/doc/04/doc.md Normal file
View File

@ -0,0 +1,212 @@
# 基础-4-矩阵键盘实验
## 4.1 章节导读
本章将介绍**矩阵键盘检测电路的设计与实现方法**通过Verilog HDL语言完成4×4矩阵键盘的扫描识别模块掌握**多键输入设备的行列扫描原理、消抖机制以及按键编码处理方式**。
矩阵键盘作为常见的人机交互接口之一广泛应用于嵌入式系统、数字电路和微控制器项目中。与独立按键不同矩阵键盘在节省IO资源的同时对扫描逻辑和时序处理提出了更高的要求。实验中我们将采用**逐行扫描法**,结合状态机与延时消抖手段,确保按键信息的准确采集。
## 4.2 理论学习
### 4.2.1 矩阵键盘结构
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/1.png"
alt="无法显示图片时显示的文字"
style="zoom:80%"/>
<br> <!--换行-->
图1.矩阵键盘原理图 <!--标题-->
</center>
</div>
实验板8个引脚分别连接矩阵键盘的KEY1~KEY8该矩阵键盘的原理如下将KEY1~KEY4脚设置为输出引脚KEY5~KEY8设置为输入引脚。以KEY1和KEY5为例当没有按键按下时KEY1和VCC之间是断路此时R1为上拉电阻电路几乎没有电流流过KEY5检测到的电压恰好是VCC为1。**所以按键不按下KEY5~KEY8检测到1。**
如果按键按下。此时KEY5~KEY8检测到的值与KEY1~KEY4的输出电压有关。以KEY1和KEY5为例如果KEY1输出为0按键1按下VCC和KEY1之间形成通路KEY5检测到0。但如果KEY1输出为1此时即使按键按下VCC和KEY1之间也几乎没有电流此时KEY5检测到高阻态也就是1。所以**如果行输出电平为0并且按键按下KEY5~8会检测到0如果行输出电平为1按键按下KEY5~8检测到1。**
现在我们看懂了原理图就可以开始设计verilog根据原理图我们知道只有行电平KEY1~4的输出电平为0时按键按下KEY5~8才会检测到0。那么我们可以用行扫描的逻辑设计
1. FPGA按顺序将1到4行中的一行输出为低电平其余3行为高电平或高阻态
2. FPGA逐个读取每列引脚KEY5~8的电平若某列为低电平则说明该行和该列交汇处的按键被按下。
3. 可以在没有按键按下时把所有行的输出电平都拉低直到有按键按下时重复1~2的步骤扫描。
## 4.2 实战演练
### 4.3.1 系统架构
``` verilog
系统框图:
[Top模块] = {矩阵键盘扫描模块 → 按键上升沿检测模块}
```
### 4.3.2 模块设计
根据上述原理,设计行扫描矩阵键盘检测模块如下:
#### matrix_key
```verilog
module matrix_key #(
parameter ROW_NUM = 4,
parameter COL_NUM = 4,
parameter DEBOUNCE_TIME = 2000,
parameter DELAY_TIME = 200
) (
input wire clk,
input wire rstn,
output reg [ROW_NUM-1:0] row,
input wire [COL_NUM-1:0] col,
output reg [ROW_NUM*COL_NUM-1:0] key_out
);
localparam ROW_ACTIVE = 1'b0; // 行有效电平
localparam ROW_INACTIVE = 1'b1; // 行无效电平
localparam COL_PRESSED = 1'b0; // 列按下电平
localparam COL_RELEASED = 1'b1; // 列释放电平
reg [ROW_NUM-1:0][COL_NUM-1:0] key; // 按键状态寄存器
reg [2:0] cu_st, nt_st;
localparam [2:0] ST_IDLE = 3'b001;
localparam [2:0] ST_SCAN = 3'b010;
localparam [2:0] ST_DEBOUNCE = 3'b100;
wire btn_pressed = ((|(~(col ^ {COL_NUM{COL_PRESSED}}))) && (cu_st == ST_IDLE)) || (key_out != 0); // 只要有一个按键按下btn_pressed为1
reg [31:0] delay_cnt; // 延时计数器
reg [31:0] debounce_cnt; // 消抖计数器
reg [ROW_NUM-1:0] row_cnt; // 行计数器
always @(posedge clk or negedge rstn) begin
if(!rstn) delay_cnt <= 0;
else if(cu_st == ST_SCAN) begin
if(delay_cnt == DELAY_TIME) delay_cnt <= 0;
else delay_cnt <= delay_cnt + 1;
end else delay_cnt <= 0;
end
always @(posedge clk or negedge rstn) begin
if(!rstn) row_cnt <= 0;
else if(cu_st == ST_SCAN) begin
if(delay_cnt == DELAY_TIME) row_cnt <= row_cnt + 1;
else row_cnt <= row_cnt;
end else row_cnt <= 0;
end
always @(posedge clk or negedge rstn) begin
if(!rstn) debounce_cnt <= 0;
else if(cu_st == ST_DEBOUNCE) begin
if(debounce_cnt == DEBOUNCE_TIME) debounce_cnt <= 0;
else debounce_cnt <= debounce_cnt + 1;
end else debounce_cnt <= 0;
end
/*
处理逻辑
ROW作为输出COL作为输入
1. ST_IDLE状态所有ROW都拉至有效电平
2. 若没有按键按下所有COL都为释放电平
3. 若有按键按下按下的按键所在的COL会变为按下电平
4. 进入ST_SCAN状态启动扫描ROW全部置为无效电平并逐次改变为有效电平。此时COL会都变成列释放电平
5. 如果某一个ROW行有效电平时COL变成了列按下电平则说明该ROW和COL交点的按键被按下
6. 每一行都扫描一遍。
7. 进入ST_DEBOUNCE状态所有ROW都拉至行有效电平在此期间不进行扫描。
8. DEBOUNCE时间到后进入IDLE状态。
*/
always @(posedge clk or negedge rstn) begin
if(!rstn) cu_st <= ST_IDLE;
else cu_st <= nt_st;
end
always @(*) begin
if(!rstn) nt_st <= ST_IDLE;
else case(cu_st)
ST_IDLE: begin
if(btn_pressed) nt_st <= ST_SCAN;
else nt_st <= ST_IDLE;
end
ST_SCAN: begin
if((delay_cnt == DELAY_TIME) && (row_cnt == ROW_NUM-1)) nt_st <= ST_DEBOUNCE;
else nt_st <= ST_SCAN;
end
ST_DEBOUNCE: begin
if(debounce_cnt == DEBOUNCE_TIME) nt_st <= ST_IDLE;
else nt_st <= ST_DEBOUNCE;
end
default: nt_st <= ST_IDLE;
endcase
end
integer i, j;
always @(posedge clk or negedge rstn) begin
if(!rstn) key <= 0;
else for(i=0; i<ROW_NUM; i=i+1)
for(j=0; j<COL_NUM; j=j+1)
if((cu_st == ST_SCAN) && (delay_cnt == DELAY_TIME) && (row_cnt == i)) key[i][j] <= (col[j] == COL_PRESSED)?(1'b1):(1'b0);
else key[i][j] <= key[i][j]; // 其他情况不变
end
always @(*) begin
for(i=0;i<ROW_NUM;i=i+1) begin
for(j=0;j<COL_NUM;j=j+1) begin
key_out[i*COL_NUM+j] <= key[i][j];
end
end
end
always @(posedge clk or negedge rstn) begin
if(!rstn) row <= {ROW_NUM{ROW_ACTIVE}};
else if(cu_st == ST_IDLE && nt_st == ST_SCAN) row <= {{(ROW_NUM-1){ROW_INACTIVE}}, ROW_ACTIVE};
else if(cu_st == ST_SCAN) begin
if(delay_cnt == DELAY_TIME) row <= {row[ROW_NUM-1:0],ROW_INACTIVE};
else row <= row;
end else row <= {ROW_NUM{ROW_ACTIVE}};
end
endmodule //matrix_key
```
为了能够观察到现象使用板载8个led和实验箱8个led进行显示按下矩阵键盘的按键对应led就会亮顶层文件如下所示
#### matrix_key_top
```verilog
module matrix_key_top(
//system io
input wire external_clk ,
input wire external_rstn,
input wire [ 3:0] col,
output wire [ 3:0] row,
output wire [15:0] led
);
wire [15:0] key_out;
assign led = key_out;
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 )
);
endmodule
```
### 4.3.3 上板验证步骤
1. 设置参数CLK_CYCLE=5000对应200Hz扫描频率
2. 绑定管脚连接led和矩阵键盘管脚
---
## 4.4 章末总结
**关键收获:**
1. 掌握矩阵键盘行扫描原理,能看懂原理图
3. 学习时序控制中计数器的重要作用
---
## 4.5 拓展训练
可以将数码管与矩阵键盘相结合

BIN
public/doc/04/images/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
public/doc/05/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

194
public/doc/05/doc.md Normal file
View File

@ -0,0 +1,194 @@
# 基础-5-PWM呼吸灯
## 5.1 章节导读
本章将实现 PWM脉宽调制呼吸灯效果即控制 LED 灯的亮度在一个周期内从暗到亮再从亮到暗,形成如人呼吸般的灯光变化。通过该实验可以掌握 PWM 占空比调节以及 FPGA 控制 LED 的基本方法。
## 5.2 理论学习
呼吸灯在我们的生活中很常见在电脑上多作为消息提醒指示灯而被广泛使用其效果是小灯在一段时间内从完全熄灭的状态逐渐变到最亮再在同样的时间段内逐渐达到完全熄灭的状态并循环往复。这种效果就像“呼吸”一样。而实现”呼吸“的方法就是PWM技术。
PWMPulse Width Modulation是一种常用的控制技术其核心思想是通过控制一个周期内信号为高电平的时间比例占空比来实现输出电压或亮度的变化。也就是说只要我们在小时间段内led灯的亮度依次增加然后依次减小即可实现”呼吸“的效果。
## 5.3 实战演练
### 5.3.1 实验目标
实现 LED 呼吸灯效果亮度逐渐变亮再逐渐变暗周而复始整体周期约为2秒视觉上更加自然流畅。
### 5.3.2 硬件资源
实验板提供 32 颗 LED 灯,本实验选用其中的 1 颗绿色 LED 进行 PWM 控制
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/1.png"
alt="无法显示图片时显示的文字"
style="zoom:30%"/>
<br> <!--换行-->
图1.LED扩展板 <!--标题-->
</center>
</div>
通过原理图可以得知本试验箱的LED灯为高电平时点亮。
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/2.png"
alt="无法显示图片时显示的文字"
style="zoom:40%"/>
<br> <!--换行-->
图2.LED扩展板原理图 <!--标题-->
</center>
</div>
### 5.3.3 程序设计
本模块的设计事实上是两个计数器所以肯定需要时钟信号sysclk也需要一个rstn复位信号同时需要一个IO口驱动LED。所以模块的端口如下表所示
| 端口名称 | 端口位宽 | 端口类型 |功能描述|
|:----------:|:----:|:----:|:--------------------:|
| sysclk | 1Bit | Input | 输入时钟频率27M |
| rstn | 1Bit | Input | 复位信号,低电平有效 |
| led | 1Bit | Output | LED控制信号 |
为了实现一个视觉上柔和自然的 LED 呼吸效果,我们设定完整的呼吸周期为 2 秒,即 LED 亮度在 1 秒内逐渐增强,接着在另 1 秒内逐渐减弱。整个过程由占空比duty cycle的变化来控制 PWM 输出的高电平持续时间。
在本设计中,使用实验板的 27MHz 系统时钟。为了获得合适的 PWM 控制精度,我们将一个 PWM 周期设定为 1ms这对应 27000 个时钟周期27M ÷ 1000。通过一个名为 `pwm_cnt` 的计数器来实现这一周期性计数,当 `pwm_cnt` 小于占空比 `duty` 的值时LED 输出高电平,从而控制亮度。
为了实现“呼吸”变化,我们再设计另一个计数器 `duty`,它每 1ms`pwm_cnt` 计满一次)更新一次。前 1000ms 内占空比逐渐增加,即 `duty` 每次增加从而输出高电平的时间逐步变长LED 亮度逐渐增强;后 1000ms 内占空比逐渐减小每次减小LED 亮度逐渐变弱。如此循环往复,即可实现 LED 的“柔和呼吸”效果。
那么,占空比 duty 的变化步长如何选择?考虑到:一个 1ms是 27000 个时钟如果我们希望1ms内led亮的时间为1us的倍数那么我们可以将27000分成1000份一份是27。如果duty的每次增减是27那么也就对应了led每次亮灭的时间增减了1us。也就是说当duty为27时led亮的时间为1us1ms过后duty变为54led亮的时间为2us以此类推当duty为27000时led亮满1ms。这样就实现了led亮的时间逐渐增加的效果。
模块的参考代码如下所示(`pwm.v`
```verilog
module pwm(
input wire sysclk, // 27MHz 系统时钟
input wire rstn, // 低有效复位
output wire led // PWM 控制LED输出
);
parameter PWM_PERIOD = 16'd27000;//1ms
// 单一PWM周期1ms
// duty上升的次数是1000次下降的次数也是1000次说明pwm的半周期是 1ms * 1000 = 1s
// pwm的一次全周期是 1s * 2 = 2s
reg [15:0] pwm_cnt;
reg [15:0] duty;
reg inc_dec_flag;//0表示duty+ 1表示duty-
//计数器1不断累加
always @(posedge sysclk or negedge rstn) begin
if (!rstn)
pwm_cnt <= 0;
else if (pwm_cnt < PWM_PERIOD - 1)
pwm_cnt <= pwm_cnt + 1;
else
pwm_cnt <= 0;
end
//计数器2控制占空比单一周期结束进行一次累加或者减
always @(posedge sysclk or negedge rstn) begin
if (!rstn)
duty <= 0;
else if (pwm_cnt == PWM_PERIOD - 1)begin
if(inc_dec_flag == 0)
duty <= duty + 27;
else
duty <= duty - 27;
end
else duty <= duty;
end
//加减的标志位,半周期结束后反转。
always @(posedge sysclk or negedge rstn) begin
if(~rstn)
inc_dec_flag <= 0;
else if(duty == PWM_PERIOD)
inc_dec_flag <= 1;
else if(duty == 0)
inc_dec_flag <= 0;
else
inc_dec_flag <= inc_dec_flag;
end
assign led = (pwm_cnt < duty) ? 1'b1 : 1'b0;
endmodule
```
### 5.3.4 仿真验证
为了验证模块功能,我们可以编写仿真模块,并将 `PWM_PERIOD` 等比例缩小为270以便快速验证。以下为仿真文件`pwm_tb.v`
```verilog
`timescale 1ns/1ns
module pwm_tb;
reg sysclk;
reg rstn;
wire led;
// 实例化待测试模块
pwm #(
.PWM_PERIOD(270)//为了减少仿真时间将单一pwm周期从27000等比例缩小为270
) pwm_inst (
.sysclk(sysclk),
.rstn(rstn),
.led(led)
);
// 产生系统时钟:周期约为 27Mhz
initial begin
sysclk = 0;
forever #(500/27) sysclk = ~sysclk;
end
// 初始化和复位过程
initial begin
// 初始化
rstn = 0;
#100; // 保持复位100ns
rstn = 1; // 释放复位
end
endmodule
```
同时为了便于仿真可以直接点击sim文件夹下hebav文件夹中的do.bat文件即可利用ModuleSim对模块进行仿真仿真波形如下
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/3.png"
alt="无法显示图片时显示的文字"
style="zoom:60%"/>
<br> <!--换行-->
图3.呼吸灯仿真波形(一) <!--标题-->
</center>
</div>
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/4.png"
alt="无法显示图片时显示的文字"
style="zoom:60%"/>
<br> <!--换行-->
图4.呼吸灯仿真波形(二) <!--标题-->
</center>
</div>
通过观察波形我们发现led输出为1的时间在逐步增加之后逐步减小duty的值从0增加到270后减小符合设计预期可以进行下一步上板验证。
### 5.3.5 上板验证
仿真验证通过后,即可进行上板测试。在实际使用时需要进行管脚约束。以下为参考端口与分配示例:
| 端口名称 | 信号类型 | 对应管脚 | 功能 |
| -------- | -------- | -------- | ------------------ |
| clk | Input | | 27MHz时钟 |
| rstn | Input | | 复位 |
| led | Output | | 输出PWM信号连接LED |
完成管脚绑定后生成 `.sbit` 文件,上传到实验平台后进行烧录,即可在摄像头画面中看到 LED 呼吸闪烁效果。
## 5.4 章末总结
本章我们学习了 PWM 控制的基本原理及其在 LED 呼吸灯上的应用同时通过不断改变PWM占空比方式使呼吸过程更加平滑自然。该方法不仅适用于视觉灯效控制还广泛应用于马达调速、音量控制等模拟量调节领域。你可以进一步尝试调整占空比范围、节奏速度甚至扩展到多个 LED 同步/异步呼吸控制,实现更加炫酷的视觉效果。

BIN
public/doc/05/images/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
public/doc/05/images/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

BIN
public/doc/05/images/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 KiB

BIN
public/doc/05/images/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
public/doc/06/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

650
public/doc/06/doc.md Normal file
View File

@ -0,0 +1,650 @@
# 基础-6-HDMI显示
## 6.1 章节导读
随着多媒体技术的快速发展高清显示已成为嵌入式系统与FPGA应用中不可或缺的一部分。HDMIHigh-Definition Multimedia Interface作为目前最主流的视频数字传输标准广泛应用于电视、显示器、笔记本、摄像头等各类终端设备中。相比传统的模拟VGA接口HDMI具有传输带宽高、支持音视频同步、无压缩信号传输等优点能更好地满足现代图像处理和显示系统的需求。
在FPGA开发中掌握HDMI显示技术不仅是实现图像/视频输出的基础能力更是后续图像识别、视频监控、图形用户界面GUI等复杂系统设计的前提。因此本实验以HDMI显示为核心内容带领大家从零开始构建一个完整的视频输出链路。通过配置显示参数、生成时序控制信号、输出RGB图像数据等关键步骤最终实现在HDMI接口上稳定输出画面。
在本次实验中我们将学习利用实验板的HDMI接口和MS7210芯片进行HDMI显示实验的设计。
## 6.2 理论学习
### 6.2.1 VGA时序
VGA显示是在行同步和帧同步场同步的信号同步下按照从上到下从左到右的顺序扫描到显示屏上。VGA扫描方式见下图所示
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/1.png"
alt="无法显示图片时显示的文字"
style="zoom:50%"/>
<br> <!--换行-->
图1.VGA扫描顺序 <!--标题-->
</center>
</div>
如上图所示每一帧图像都是从左上角开始逐行扫描形成所以规定最左上角的像素点为第一个像素点坐标是00以这个像素为起点向右x坐标逐渐增大向下y坐标逐渐增大重复若干次后扫描到右下角完成一帧图像的扫描扫描完成后进行图像消隐随后指针跳回左上角重新进行新一帧的扫描。
在扫描的过程中会对每一个像素点进行单独赋值使每个像素点显示对应色彩信息当扫描速度足够快加之人眼的视觉暂留特性我们会看到一幅完整的图片这就是VGA 显示的原理。
VGA显示除了要有像素点的信息还需要有行同步HSync和场同步VSync两个信号辅助显示。行同步信号规定了一行像素的开始与结束场同步信号规定了一帧图像的开始与结束。在VESA DMT 1.12版本的标准文档中给出的VGA时序图如下图所示
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/2.png"
alt="无法显示图片时显示的文字"
style="zoom:100%"/>
<br> <!--换行-->
图2.VGA标准时序 <!--标题-->
</center>
</div>
行同步时序如下图所示:
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/3.png"
alt="无法显示图片时显示的文字"
style="zoom:100%"/>
<br> <!--换行-->
图3.行同步时序 <!--标题-->
</center>
</div>
行同步的一个扫周期要经过6个部分分别是Sync同步、 Back Porch后沿、 Left Border左边框、 “Addressable” Video有效图像、 Right Border右边框、 Front Porch前沿这些过程的长度都是以像素为单位的也就是以像素时钟为单位例如Sync的值为96也就意味着Sync阶段要经历96个像素时钟。HSync信号会在Sync同步阶段拉高不同的芯片可能有不同标准以确定新一行的开始与上一行的结束。而完整的一行像素很多但有效的真正能显示在屏幕上的像素只有 “Addressable” Video有效图像部分的像素其他阶段的像素均无效无法显示在屏幕中。
场同步时序如下图所示:
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/4.png"
alt="无法显示图片时显示的文字"
style="zoom:100%"/>
<br> <!--换行-->
图4.场同步时序 <!--标题-->
</center>
</div>
场同步时序与行同步时序相同也是分为6个部分在Sync同步阶段拉高标志着一帧的结束和新一帧的开始其中像素只有在“Addressable” Video有效图像阶段才有效其他阶段均无效。而场同步信号的基本单位是行比如Sync的值为2也就意味着Sync同步阶段要经历两行。
那么我们将行同步和场同步信号结合起来,遍可以得到一帧图像的样貌,如下图所示:
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/5.png"
alt="无法显示图片时显示的文字"
style="zoom:100%"/>
<br> <!--换行-->
图5.一帧图像组成示意图 <!--标题-->
</center>
</div>
可以看到在行场同步信号构成了一个二维坐标系,原点在左上方,中间遍形成了一帧图像,而真正能显示在屏幕中的图像只有 “Addressable” Video有效图像部分。
现在我们知道了行同步和场同步都要经历6个部分那么这些部分的长度都是如何规定的呢VGA 行时序对行同步时间、 消隐时间、 行视频有效时间和行前肩时间有特定的规范, 场时序也是如此。 常用VGA 分辨率时序参数如下表所示:
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/6.png"
alt="无法显示图片时显示的文字"
style="zoom:100%"/>
<br> <!--换行-->
图6.常用VGA分辨率时序参数 <!--标题-->
</center>
</div>
### 6.2.2 MS7210芯片
MS7210是一款HD发送芯片支持4K@30Hz的视频3D传输格式。可以支持的最高分辨率高达4K@30Hz最高采样率达到300MHz。MS7210支持YUV和RGB 之间的色彩空间转换数字接口支持YUV以及RGB格式输入。MS7210的IIS接口以及S/PDIF 接口支持高清音频的传输其中S/PDIF接口既可以兼容IEC61937标准下的压缩音频传输同时还支持高比特音频HBR的传输在高比特音频HBR模式下音频采样率最高为768KHz。MS7210的IIC 地址可以根据SA引脚进行选择。当 SA引脚上拉到电源电压或者悬空时地址为 OxB2。当 SA 引脚连接到 GND 时地址为0x56。
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/7.png"
alt="无法显示图片时显示的文字"
style="zoom:100%"/>
<br> <!--换行-->
图7.MS7210芯片 <!--标题-->
</center>
</div>
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/8.png"
alt="无法显示图片时显示的文字"
style="zoom:50%"/>
<br> <!--换行-->
图8.MS7210功能框图 <!--标题-->
</center>
</div>
MS7210芯片可以通过IIC协议对内部寄存器进行配置有关芯片寄存器配置需要向芯片厂家进行申请。
## 6.3 实战演练
### 6.3.1实验目标
### 6.3.2硬件资源
实验板共有一个HDMI-OUT接口由MS7210驱动一个HDMI-IN接口由MS7200驱动。
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/9.png"
alt="无法显示图片时显示的文字"
style="zoom:30%"/>
<br> <!--换行-->
图9.板载HDMI芯片 <!--标题-->
</center>
</div>
实验箱配备一个小型HDMI显示器该显示器HDMI接口与HDMI-OUT接口连接图像可以显示在显示屏中通过摄像头可以在网站观察现象
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/xxx.png"
alt="实验箱显示器"
style="zoom:40%"/>
<br> <!--换行-->
图10.实验箱显示器 <!--标题-->
</center>
</div>
### 6.3.3程序设计
在设计程序时我们先对本实验工程有一个整体认知首先来看一下HDMI彩条显示实验的整体框图。
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/xxxx.png"
alt="实验箱显示器"
style="zoom:40%"/>
<br> <!--换行-->
图11.HDMI彩条显示整体框图 <!--标题-->
</center>
</div>
可见整个实验一共由好多个模块组成,下面是各个模块简介
| 模块名称 |功能描述| 备注 |
|:----:|:----:|:----:|
| hdmi_top | 顶层模块 ||
| ms7210_ctrl_iic_top | ms7210芯片配置和iic顶层模块 |参考小眼睛例程|
| ms7210_ctl | ms7210芯片配置和时序控制模块 |使用小眼睛例程|
| iic_dri | iic驱动模块 |使用小眼睛例程|
| vga_ctrl | vga时序信号生成模块 |参考野火例程|
| vga_pic | vga像素数据生成模块 |参考野火例程|
本次实验主要完成vga_ctrl和vga_pic模块的设计。
对于vga_ctrl模块我们主要完成hsyncvsync信号xy坐标数据有效rgb_valid信号的设计。经过我们前面的学习已经对vga时序有了一定的了解我们可以想象到这几个信号也只是一种计数器而已。
本实验要实现640x480的彩条显示相关参数如下所示
```Verilog
//parameter define
parameter H_SYNC = 10'd96 , //行同步
H_BACK = 10'd40 , //行时序后沿
H_LEFT = 10'd8 , //行时序左边框
H_VALID = 10'd640 , //行有效数据
H_RIGHT = 10'd8 , //行时序右边框
H_FRONT = 10'd8 , //行时序前沿
H_TOTAL = 10'd800 ; //行扫描周期
parameter V_SYNC = 10'd2 , //场同步
V_BACK = 10'd25 , //场时序后沿
V_TOP = 10'd8 , //场时序上边框
V_VALID = 10'd480 , //场有效数据
V_BOTTOM = 10'd8 , //场时序下边框
V_FRONT = 10'd2 , //场时序前沿
V_TOTAL = 10'd525 ; //场扫描周期
```
首先设计两个计数器`cnt_h`和`cnt_v`分别对像素和行进行计数,一个像素时钟过后`cnt_h`加一,一行过后`cnt_v`加一,扫描完一帧之后,计数器归零。
而其他的状态信号则可以根据计数器的计数进行设计。hsync信号只要`cnt_h < H_SYNC`就拉高vsync信号类似当计数到有效数据部分数据有效信号rgb_valid就可以拉高注意由于时序逻辑有一个时钟周期的反应时间所以xy的坐标变化比rgb_valid提前一个时钟周期参考代码如下所示
```verilog
`timescale 1ns/1ns
////////////////////////////////////////////////////////////////////////
// Author : EmbedFire
// 实验平台: 野火FPGA系列开发板
// 公司 : http://www.embedfire.com
// 论坛 : http://www.firebbs.cn
// 淘宝 : https://fire-stm32.taobao.com
////////////////////////////////////////////////////////////////////////
module vga_ctrl
(
input wire vga_clk , //输入工作时钟,频率25MHz
input wire sys_rst_n , //输入复位信号,低电平有效
input wire [15:0] pix_data , //输入像素点色彩信息
output wire [11:0] pix_x , //输出VGA有效显示区域像素点X轴坐标
output wire [11:0] pix_y , //输出VGA有效显示区域像素点Y轴坐标
output wire hsync , //输出行同步信号
output wire vsync , //输出场同步信号
output wire rgb_valid ,
output wire [15:0] rgb //输出像素点色彩信息
);
//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//parameter define
parameter H_SYNC = 10'd96 , //行同步
H_BACK = 10'd40 , //行时序后沿
H_LEFT = 10'd8 , //行时序左边框
H_VALID = 10'd640 , //行有效数据
H_RIGHT = 10'd8 , //行时序右边框
H_FRONT = 10'd8 , //行时序前沿
H_TOTAL = 10'd800 ; //行扫描周期
parameter V_SYNC = 10'd2 , //场同步
V_BACK = 10'd25 , //场时序后沿
V_TOP = 10'd8 , //场时序上边框
V_VALID = 10'd480 , //场有效数据
V_BOTTOM = 10'd8 , //场时序下边框
V_FRONT = 10'd2 , //场时序前沿
V_TOTAL = 10'd525 ; //场扫描周期
//wire define
wire pix_data_req ; //像素点色彩信息请求信号
//reg define
reg [11:0] cnt_h ; //行同步信号计数器
reg [11:0] cnt_v ; //场同步信号计数器
//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
//cnt_h:行同步信号计数器
always@(posedge vga_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_h <= 12'd0 ;
else if(cnt_h == H_TOTAL - 1'd1)
cnt_h <= 12'd0 ;
else
cnt_h <= cnt_h + 1'd1 ;
//hsync:行同步信号
assign hsync = (cnt_h <= H_SYNC - 1'd1) ? 1'b1 : 1'b0 ;
//cnt_v:场同步信号计数器
always@(posedge vga_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_v <= 12'd0 ;
else if((cnt_v == V_TOTAL - 1'd1) && (cnt_h == H_TOTAL-1'd1))
cnt_v <= 12'd0 ;
else if(cnt_h == H_TOTAL - 1'd1)
cnt_v <= cnt_v + 1'd1 ;
else
cnt_v <= cnt_v ;
//vsync:场同步信号
assign vsync = (cnt_v <= V_SYNC - 1'd1) ? 1'b1 : 1'b0 ;
//rgb_valid:VGA有效显示区域
assign rgb_valid = (((cnt_h >= H_SYNC + H_BACK + H_LEFT)
&& (cnt_h < H_SYNC + H_BACK + H_LEFT + H_VALID))
&&((cnt_v >= V_SYNC + V_BACK + V_TOP)
&& (cnt_v < V_SYNC + V_BACK + V_TOP + V_VALID)))
? 1'b1 : 1'b0;
//pix_data_req:像素点色彩信息请求信号,超前rgb_valid信号一个时钟周期
assign pix_data_req = (((cnt_h >= H_SYNC + H_BACK + H_LEFT - 1'b1)
&& (cnt_h < H_SYNC + H_BACK + H_LEFT + H_VALID - 1'b1))
&&((cnt_v >= V_SYNC + V_BACK + V_TOP)
&& (cnt_v < V_SYNC + V_BACK + V_TOP + V_VALID)))
? 1'b1 : 1'b0;
//pix_x,pix_y:VGA有效显示区域像素点坐标
assign pix_x = (pix_data_req == 1'b1)
? (cnt_h - (H_SYNC + H_BACK + H_LEFT - 1'b1)) : 12'hfff;
assign pix_y = (pix_data_req == 1'b1)
? (cnt_v - (V_SYNC + V_BACK + V_TOP)) : 12'hfff;
//rgb:输出像素点色彩信息
assign rgb = (rgb_valid == 1'b1) ? pix_data : 16'b0 ;
endmodule
```
对于vga_pic模块我们可以根据x坐标范围0~639分成十份每一份输出不同的颜色。参考代码如下所示
```verilog
`timescale 1ns/1ns
////////////////////////////////////////////////////////////////////////
// Author : EmbedFire
// 实验平台: 野火FPGA系列开发板
// 公司 : http://www.embedfire.com
// 论坛 : http://www.firebbs.cn
// 淘宝 : https://fire-stm32.taobao.com
////////////////////////////////////////////////////////////////////////
module vga_pic
(
input wire vga_clk , //输入工作时钟,频率25MHz
input wire sys_rst_n , //输入复位信号,低电平有效
input wire [11:0] pix_x , //输入VGA有效显示区域像素点X轴坐标
input wire [11:0] pix_y , //输入VGA有效显示区域像素点Y轴坐标
output reg [15:0] pix_data //输出像素点色彩信息
);
//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//parameter define
parameter H_VALID = 12'd640 , //行有效数据
V_VALID = 12'd480 ; //场有效数据
parameter RED = 16'hF800, //红色
ORANGE = 16'hFC00, //橙色
YELLOW = 16'hFFE0, //黄色
GREEN = 16'h07E0, //绿色
CYAN = 16'h07FF, //青色
BLUE = 16'h001F, //蓝色
PURPPLE = 16'hF81F, //紫色
BLACK = 16'h0000, //黑色
WHITE = 16'hFFFF, //白色
GRAY = 16'hD69A; //灰色
//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
//pix_data:输出像素点色彩信息,根据当前像素点坐标指定当前像素点颜色数据
always@(posedge vga_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
pix_data <= 16'd0;
else if((pix_x >= 0) && (pix_x < (H_VALID/10)*1))
pix_data <= RED;
else if((pix_x >= (H_VALID/10)*1) && (pix_x < (H_VALID/10)*2))
pix_data <= ORANGE;
else if((pix_x >= (H_VALID/10)*2) && (pix_x < (H_VALID/10)*3))
pix_data <= YELLOW;
else if((pix_x >= (H_VALID/10)*3) && (pix_x < (H_VALID/10)*4))
pix_data <= GREEN;
else if((pix_x >= (H_VALID/10)*4) && (pix_x < (H_VALID/10)*5))
pix_data <= CYAN;
else if((pix_x >= (H_VALID/10)*5) && (pix_x < (H_VALID/10)*6))
pix_data <= BLUE;
else if((pix_x >= (H_VALID/10)*6) && (pix_x < (H_VALID/10)*7))
pix_data <= PURPPLE;
else if((pix_x >= (H_VALID/10)*7) && (pix_x < (H_VALID/10)*8))
pix_data <= BLACK;
else if((pix_x >= (H_VALID/10)*8) && (pix_x < (H_VALID/10)*9))
pix_data <= WHITE;
else if((pix_x >= (H_VALID/10)*9) && (pix_x < H_VALID))
pix_data <= GRAY;
else
pix_data <= BLACK;
endmodule
```
在顶层模块我们首先要利用PLL ip核生成iic的驱动时钟进行初始化由于ms7210芯片的需要我们通过计数设置一个延迟复位信号由于我们的彩条颜色是按照RGB565格式生成的所以需要向RGB888进行转换只需要填0补位即可同时由于板载时钟是27M与25.175M相差不大,所以直接使用板载时钟作为像素时钟输出。然后我们将输出的行场同步信号,像素时钟,像素数据,像素数据有效信号等与模块相连接即可完成设计。顶层模块参考代码如下:
```Verilog
`timescale 1ns / 1ns
module hdmi_top(
input wire sys_clk ,// input system clock 50MHz
input rstn_in ,
output rstn_out ,
output hd_scl ,
inout hd_sda ,
output led_int ,
//hdmi_out
output pixclk_out ,//pixclk
output wire vs_out ,
output wire hs_out ,
output wire de_out ,
output wire [7:0] r_out ,
output wire [7:0] g_out ,
output wire [7:0] b_out
);
wire cfg_clk ;
wire locked ;
wire rstn ;
wire init_over ;
reg [15:0] rstn_1ms ;
//**********************************************//
//*****************MS7210初始化******************//
//**********************************************//
//**************仿真时不编译此部分***************//
`ifndef SIM
//初始化成功标志
assign led_int = init_over;
//生成10M IIC时钟
PLL u_pll (
.clkout0(cfg_clk), // output
.lock(locked), // output
.clkin1(sys_clk) // input
);
//ms7210初始化模块
ms7210_ctrl_iic_top ms7210_ctrl_iic_top_inst(
.clk ( cfg_clk ), //input clk,
.rst_n ( rstn_out ), //input rstn,
.init_over ( init_over ), //output init_over,
.iic_scl ( hd_scl ), //output iic_scl,
.iic_sda ( hd_sda ) //inout iic_sda
);
//延迟复位
always @(posedge cfg_clk)
begin
if(!locked)
rstn_1ms <= 16'd0;
else
begin
if(rstn_1ms == 16'h2710)
rstn_1ms <= rstn_1ms;
else
rstn_1ms <= rstn_1ms + 1'b1;
end
end
assign rstn_out = (rstn_1ms == 16'h2710) && rstn_in;
//**********************************************//
`else
assign led_int = 1;
assign rstn_out = rstn_in;
`endif
//**********************************************//
//**********************************************//
//**********************************************//
//**********************************************//
wire [15:0] rgb565;
wire [15:0] pix_data ;
wire [11:0] pix_x;
wire [11:0] pix_y;
//vga行场同步控制模块
vga_ctrl vga_ctrl_inst (
.vga_clk (sys_clk ),
.sys_rst_n (rstn_out ),
.pix_data (pix_data ),
.pix_x (pix_x ),
.pix_y (pix_y ),
.hsync (hs_out ),
.vsync (vs_out ),
.rgb_valid (de_out ),
.rgb (rgb565 )
);
//彩条数据生成模块
vga_pic vga_pic_inst (
.vga_clk (sys_clk ),
.sys_rst_n (rstn_out ),
.pix_x (pix_x ),
.pix_y (pix_y ),
.pix_data_out (pix_data )
);
//RGB565转RGB888
assign pixclk_out = sys_clk ;//直接使用27M时钟与25.175相差不大
assign r_out = {rgb565[15:11],3'b0};
assign g_out = {rgb565[10: 5],2'b0};
assign b_out = {rgb565[ 4: 0],3'b0};
endmodule
```
### 6.3.4仿真验证
由于仿真不需要对MS7210芯片进行初始化所以我们在top文件中加入条件编译指令并且在仿真文件中定义SIM宏那么就可以在仿真中不编译ms7210初始化相关代码只对vga时序进行仿真。我们只需要提供时钟和复位即可对模块进行仿真。仿真文件如下所示
```verilog
`timescale 1ns / 1ns
`define SIM
module hdmi_top_tb;
// Parameters
//Ports
reg sys_clk;
reg rstn_in;
wire rstn_out;
wire hd_scl;
wire hd_sda;
wire led_int;
wire pixclk_out;
wire vs_out;
wire hs_out;
wire de_out;
wire [7:0] r_out;
wire [7:0] g_out;
wire [7:0] b_out;
initial begin
sys_clk = 0;
rstn_in = 0;
#100
rstn_in = 1;
end
always #(500/27) sys_clk = ~sys_clk;
hdmi_top hdmi_top_inst (
.sys_clk(sys_clk),
.rstn_in(rstn_in),
.rstn_out(rstn_out),
.hd_scl(hd_scl),
.hd_sda(hd_sda),
.led_int(led_int),
.pixclk_out(pixclk_out),
.vs_out(vs_out),
.hs_out(hs_out),
.de_out(de_out),
.r_out(r_out),
.g_out(g_out),
.b_out(b_out)
);
endmodule
```
直接点击sim文件夹下hebav文件夹中的do.bat文件即可利用ModuleSim对模块进行仿真仿真波形如下
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/10.png"
alt="无法显示图片时显示的文字"
style="zoom:100%"/>
<br> <!--换行-->
图10.仿真波形(一) <!--标题-->
</center>
</div>
从上图我们可以发现vsync信号拉高了两个行同步信号的长度与设计相符
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/11.png"
alt="无法显示图片时显示的文字"
style="zoom:100%"/>
<br> <!--换行-->
图11.仿真波形(二) <!--标题-->
</center>
</div>
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/12.png"
alt="无法显示图片时显示的文字"
style="zoom:100%"/>
<br> <!--换行-->
图12.仿真波形(三) <!--标题-->
</center>
</div>
从图11和12中我们可以看到当cnt_h信号计数结束后会恢复0cnt_v会加一hsync信号会拉高96个像素时钟0~95cnt_h和hsync与设计相符。
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/13.png"
alt="无法显示图片时显示的文字"
style="zoom:100%"/>
<br> <!--换行-->
图13.仿真波形(四) <!--标题-->
</center>
</div>
如图13所示当cnt_h计数到H_SYNC + H_BACK + H_LEFT也就是144时rgb_valid拉高xy轴坐标比rgb_valid提前一个时钟周期以便pix_data准备好数据符合设计。
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/14.png"
alt="无法显示图片时显示的文字"
style="zoom:100%"/>
<br> <!--换行-->
图14.仿真波形(五) <!--标题-->
</center>
</div>
从每一行看每一行被分成了10个部分每部分像素数据分别对应不同颜色符合设计要求。可以进行下一步上板验证。
### 6.3.5上板验证
仿真已经通过,可以进行上板验证,上板前要先进行管脚约束。端口与对应管脚如下表所示:
| 端口名称 |信号类型| 对应管脚|功能|
|:----:|:----:|:----:|:----:|
| sysclk | Input | D18 | 27M时钟 |
| rstn_in | Input | C22 | 外部输入复位 |
| rstn_out | Output | G25 | 输出ms7210复位 |
| hd_scl | Output | K22 | iic SCL信号 |
| hd_sda | Output | K23 | iic SDA信号 |
| led_int | Output | A20 | 配置完成信号 |
| pixclk_out | Output | G25 | 像素时钟输出 |
| vs_out | Output | R21 | Vsync输出 |
| hs_out | Output | R20 | Hsync输出 |
| de_out | Output | N19 | RGB_valid输出 |
| r_out[0] | Output | N21 | RGB888输出 |
| r_out[1] | Output | L23 | RGB888输出 |
| r_out[2] | Output | L22 | RGB888输出 |
| r_out[3] | Output | L25 | RGB888输出 |
| r_out[4] | Output | L24 | RGB888输出 |
| r_out[5] | Output | K26 | RGB888输出 |
| r_out[6] | Output | K25 | RGB888输出 |
| r_out[7] | Output | P16 | RGB888输出 |
| g_out[0] | Output | T25 | RGB888输出 |
| g_out[1] | Output | P25 | RGB888输出 |
| g_out[2] | Output | R25 | RGB888输出 |
| g_out[3] | Output | P24 | RGB888输出 |
| g_out[4] | Output | P23 | RGB888输出 |
| g_out[5] | Output | N24 | RGB888输出 |
| g_out[6] | Output | N23 | RGB888输出 |
| g_out[7] | Output | N22 | RGB888输出 |
| b_out[0] | Output | P19 | RGB888输出 |
| b_out[1] | Output | P21 | RGB888输出 |
| b_out[2] | Output | P20 | RGB888输出 |
| b_out[3] | Output | M22 | RGB888输出 |
| b_out[4] | Output | M21 | RGB888输出 |
| b_out[5] | Output | N18 | RGB888输出 |
| b_out[6] | Output | R22 | RGB888输出 |
| b_out[7] | Output | T22 | RGB888输出 |
管脚分配可以直接编写.fdc文件也可以使用PDS内置的工具进行分配。完成管脚分配之后就可以生成sbit文件将文件提交到网站后点击烧录即可将sbit下载到实验板中在摄像头页面即可观察到显示屏中显示出彩条。
## 6.4 章末总结
本次实验主要学习VGA时序的相关知识并使用HD硬核进行HDMI显示感兴趣的同学可以尝试使用HDMI显示其他图像。

BIN
public/doc/06/images/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
public/doc/06/images/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
public/doc/06/images/11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

BIN
public/doc/06/images/12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

BIN
public/doc/06/images/13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 KiB

BIN
public/doc/06/images/14.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

BIN
public/doc/06/images/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 KiB

BIN
public/doc/06/images/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/doc/06/images/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
public/doc/06/images/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
public/doc/06/images/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
public/doc/06/images/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

BIN
public/doc/06/images/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
public/doc/06/images/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
public/doc/11/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

400
public/doc/11/doc.md Normal file
View File

@ -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状态重设密码
根据上述状态转移逻辑,我们可以画出状态转移图,状态转移图如下图所示:
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/1.png"
alt="无法显示图片时显示的文字"
style="zoom:100%"/>
<br> <!--换行-->
图1.状态转移图 <!--标题-->
</center>
</div>
我们已经了解了本次实验所使用的状态机那么如何使用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. **利用按键实现简易菜单系统**拓展状态机结构,允许通过矩阵键盘导航菜单,如“输入密码”、“查看状态”、“设置新密码”等。

BIN
public/doc/11/images/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

736
public/doc/12/doc.md Normal file

File diff suppressed because one or more lines are too long

BIN
public/doc/12/images/1.jfif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
public/doc/12/images/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
public/doc/12/images/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

BIN
public/doc/12/images/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

BIN
public/doc/12/images/5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

BIN
public/doc/12/images/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

BIN
public/doc/12/images/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

BIN
public/doc/12/images/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

BIN
public/doc/13/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 KiB

350
public/doc/13/doc.md Normal file
View File

@ -0,0 +1,350 @@
# 进阶-3-频率计
## 3.1 章节导读
本实验将基于实验平台设计并实现一个简易频率计用于测量输入信号的频率值并通过数码管进行实时显示。实验核心是掌握ADC模块的使用方法被测信号频率的获取方法及其在数字系统中的处理流程。
## 3.2 理论学习
### 3.2.1 ADC模块
实验平台有一块8bit高速ADDA模块其中ADC模块使用AD9280芯片支持最高32MSPS的速率模拟电压输入范围为-5~+5VADC模块可以根据输入电压的大小将其转换为0~2552的8次方的数值。模块有一个clk管脚和8个data管脚data的输入速率和驱动时钟有关给clk管脚的驱动时钟越快采样率越高data的输入速率越高。
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/1.png"
alt="无法显示图片时显示的文字"
style="zoom:30%"/>
<br> <!--换行-->
图1.ADDA模块示意图 <!--标题-->
</center>
</div>
### 3.2.2 数码管模块
数码管模块在前面基础实验3已经介绍过这里不再赘述。
## 3.3 实战演练
### 3.3.1实验目标
能够驱动板载ADC模块对ADC模块的输入数据进行测试计算输入信号的频率值并在数码管模块中显示。
### 3.3.2硬件资源
实验所需的信号源来自我们的实验平台实验平台集成一个以FPGA为基础的dds信号发生器该dds信号发生器可以输出频率可调的方波正弦波三角波锯齿波等用户可以在web平台使用并且改变输出波形和频率。
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/2.png"
alt="无法显示图片时显示的文字"
style="zoom:30%"/>
<br> <!--换行-->
图2.ADDA实验示意图 <!--标题-->
</center>
</div>
用户接收到信号源之后需自行设计逻辑处理数据并显示。
### 3.3.3程序设计
#### pulse_gen.v
首先用户接收到的是8bit波形数据要直接利用波形数据计算频率不是很方便计算频率数据我们只需要计算其脉冲的个数即可所以我们设计一个模块通过设计一个脉冲阈值trig_level高于阈值的就计算为一次脉冲输出一个周期的高电平方便后续模块计数模块代码如下
```verilog
module pulse_gen(
input rstn, //系统复位,低电平有效
input [7:0] trig_level,
input ad_clk, //AD9280驱动时钟
input [7:0] ad_data, //AD输入数据
output ad_pulse //输出的脉冲信号
);
//因为可能会有抖动,设置一个范围值避免反复触发
parameter THR_DATA = 3;
//reg define
reg pulse;
reg pulse_delay;
//*****************************************************
//** main code
//*****************************************************
assign ad_pulse = pulse & pulse_delay;
//根据触发电平将输入的AD采样值转换成高低电平
always @ (posedge ad_clk or negedge rstn)begin
if(!rstn)
pulse <= 1'b0;
else begin
if((trig_level >= THR_DATA) && (ad_data < trig_level - THR_DATA))
pulse <= 1'b0;
else if(ad_data > trig_level + THR_DATA)
pulse <= 1'b1;
end
end
//延时一个时钟周期,用于消除抖动
always @ (posedge ad_clk or negedge rstn)begin
if(!rstn)
pulse_delay <= 1'b0;
else
pulse_delay <= pulse;
end
endmodule
```
#### cymometer.v
下面根据pulse_gen信号生成的脉冲数据进行计数计算其频率。我们这里采用门控时钟法`clk_fs`(参考时钟)作为时间基准,测量 `clk_fx`(被测信号)的频率。
门控时钟法的原理很简单,也就是在一个**固定时间窗内**(即门控时间 `GATE_TIME`**计数被测时钟 clk_fx 的上升沿次数**,再结合参考时钟 `clk_fs` 的计数值,就可以算出频率:
$$
\text{频率} = \frac{\text{被测脉冲数量}}{\text{门控时间(秒)}} = \frac{fx\_cnt}{fs\_cnt / \text{CLK\_FS}} = \frac{\text{CLK\_FS} \times fx\_cnt}{fs\_cnt}
$$
| 步骤 | 描述 |
| ---- | ------------------------------------------------------------ |
| **1** | 使用 `clk_fx` 作为计数时钟,控制一个门控时间 `gate` 信号 |
| **2** | 当 `gate` 为高电平时,`fx_cnt_temp` 开始统计 `clk_fx` 的脉冲个数 |
| **3** | 同时将 `gate` 同步到参考时钟 `clk_fs`,并计数 `fs_cnt_temp`,记录 `gate` 高电平持续期间 `clk_fs` 的个数 |
| **4** | 一旦 `gate` 下降沿到来(通过打拍检测),将计数值冻结到 `fx_cnt``fs_cnt` 中 |
| **5** | 最后用上述表达式计算频率输出。 |
代码设计如下:
```verilog
module cymometer
#(parameter CLK_FS = 26'd50_000_000) // 基准时钟频率值
( //system clock
input clk_fs , // 基准时钟信号
input rstn , // 复位信号
//cymometer interface
input clk_fx , // 被测时钟信号
output reg [19:0] data_fx // 被测时钟频率输出
);
//parameter define
localparam MAX = 30; // 定义fs_cnt、fx_cnt的最大位宽
localparam GATE_TIME = 16'd2_000; // 门控时间设置
//reg define
reg gate ; // 门控信号
reg gate_fs ; // 同步到基准时钟的门控信号
reg gate_fs_r ; // 用于同步gate信号的寄存器
reg gate_fs_d0 ; // 用于采集基准时钟下gate下降沿
reg gate_fs_d1 ; //
reg gate_fx_d0 ; // 用于采集被测时钟下gate下降沿
reg gate_fx_d1 ; //
reg [ 58:0] data_fx_t ; //
reg [ 15:0] gate_cnt ; // 门控计数
reg [MAX-1:0] fs_cnt ; // 门控时间内基准时钟的计数值
reg [MAX-1:0] fs_cnt_temp ; // fs_cnt 临时值
reg [MAX-1:0] fx_cnt ; // 门控时间内被测时钟的计数值
reg [MAX-1:0] fx_cnt_temp ; // fx_cnt 临时值
//wire define
wire neg_gate_fs; // 基准时钟下门控信号下降沿
wire neg_gate_fx; // 被测时钟下门控信号下降沿
//*****************************************************
//** main code
//*****************************************************
//边沿检测,捕获信号下降沿
assign neg_gate_fs = gate_fs_d1 & (~gate_fs_d0);
assign neg_gate_fx = gate_fx_d1 & (~gate_fx_d0);
//门控信号计数器,使用被测时钟计数
always @(posedge clk_fx or negedge rstn) begin
if(!rstn)
gate_cnt <= 16'd0;
else if(gate_cnt == GATE_TIME + 5'd20)
gate_cnt <= 16'd0;
else
gate_cnt <= gate_cnt + 1'b1;
end
//门控信号拉高时间为GATE_TIME个实测时钟周期
always @(posedge clk_fx or negedge rstn) begin
if(!rstn)
gate <= 1'b0;
else if(gate_cnt < 4'd10)
gate <= 1'b0;
else if(gate_cnt < GATE_TIME + 4'd10)
gate <= 1'b1;
else if(gate_cnt <= GATE_TIME + 5'd20)
gate <= 1'b0;
else
gate <= 1'b0;
end
//将门控信号同步到基准时钟下
always @(posedge clk_fs or negedge rstn) begin
if(!rstn) begin
gate_fs_r <= 1'b0;
gate_fs <= 1'b0;
end
else begin
gate_fs_r <= gate;
gate_fs <= gate_fs_r;
end
end
//打拍采门控信号的下降沿(被测时钟下)
always @(posedge clk_fx or negedge rstn) begin
if(!rstn) begin
gate_fx_d0 <= 1'b0;
gate_fx_d1 <= 1'b0;
end
else begin
gate_fx_d0 <= gate;
gate_fx_d1 <= gate_fx_d0;
end
end
//打拍采门控信号的下降沿(基准时钟下)
always @(posedge clk_fs or negedge rstn) begin
if(!rstn) begin
gate_fs_d0 <= 1'b0;
gate_fs_d1 <= 1'b0;
end
else begin
gate_fs_d0 <= gate_fs;
gate_fs_d1 <= gate_fs_d0;
end
end
//门控时间内对被测时钟计数
always @(posedge clk_fx or negedge rstn) begin
if(!rstn) begin
fx_cnt_temp <= 32'd0;
fx_cnt <= 32'd0;
end
else if(gate)
fx_cnt_temp <= fx_cnt_temp + 1'b1;
else if(neg_gate_fx) begin
fx_cnt_temp <= 32'd0;
fx_cnt <= fx_cnt_temp;
end
end
//门控时间内对基准时钟计数
always @(posedge clk_fs or negedge rstn) begin
if(!rstn) begin
fs_cnt_temp <= 32'd0;
fs_cnt <= 32'd0;
end
else if(gate_fs)
fs_cnt_temp <= fs_cnt_temp + 1'b1;
else if(neg_gate_fs) begin
fs_cnt_temp <= 32'd0;
fs_cnt <= fs_cnt_temp;
end
end
//计算被测信号频率
always @(posedge clk_fs or negedge rstn) begin
if(!rstn) begin
data_fx_t <= 1'b0;
end
else if(gate_fs == 1'b0)
data_fx_t <= CLK_FS * fx_cnt ;
end
always @(posedge clk_fs or negedge rstn) begin
if(!rstn) begin
data_fx <= 20'd0;
end
else if(gate_fs == 1'b0)
data_fx <= data_fx_t / fs_cnt ;
end
endmodule
```
#### frequency_meter.v
由于之前基础实验设计过数码管显示模块本次实验不在赘述但因为数码管模块是输入ascii码进行显示的而现在输出频率数据是一个20bit的二进制数所以我们应该先想办法将二进制转成ascii码再连接数码管模块进行显示。BCD转ascii码通过查表的方式即可完成。但二进制转BCD码的算法不是特别简单之后会在基础实验部分讲解。
顶层模块代码如下:
```verilog
module frequency_meter(
input clk,
input rstn, // 复位信号
output ad_clk, // AD时钟
input [7:0] ad_data, // AD输入数据
output [7:0] led_display_seg,
output wire [7:0] led_display_sel
);
wire ad_pulse;
wire [19:0] data_fx;
wire [25:0] bcd;
wire [31:0] data_bcd;
wire [63:0] asciidata;
assign data_bcd = {6'b00,bcd};
//生成ad驱动时钟由于使用杜邦线连接ad_clk不要超过10M
PLL PLLinst(
.clkout0(ad_clk), // output 10M
.lock(),
.clkin1(clk) // input
);
pulse_gen pulse_gen_inst (
.rstn(rstn),
.trig_level(8'd128),
.ad_clk(ad_clk),
.ad_data(ad_data),
.ad_pulse(ad_pulse)
);
cymometer # (
.CLK_FS(32'd27_000_000)
)
cymometer_inst (
.clk_fs(clk),
.rstn(rstn),
.clk_fx(ad_pulse),
.data_fx(data_fx)
);
//二进制转bcd码模块
bin2bcd # (
.W(20)
)
bin2bcd_inst (
.bin(data_fx),
.bcd(bcd)
);
//4位BCD码转ascii模块例化8次使8个bcd同时输出ascii
genvar i;
generate
for (i = 0; i < 8; i = i + 1) begin : generate_module
bcd2ascii bcd2ascii_inst (
.bcd(data_bcd[i*4 +:4]),
.asciidata(asciidata[i*8 +: 8])
);
end
endgenerate
//数码管显示模块
led_display_driver led_display_driver_inst (
.clk(clk),
.rstn(rstn),
.assic_seg(asciidata),
.seg_point(8'b00000000),
.led_display_seg(led_display_seg),
.led_display_sel(led_display_sel)
);
endmodule
```
### 3.3.4仿真验证
### 3.3.5上板验证
## 3.4 章末总结

BIN
public/doc/13/images/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
public/doc/13/images/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
public/doc/13/images/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

View File

@ -0,0 +1,55 @@
using System.IO;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
namespace server.Controllers;
/// <summary>
/// 教程 API
/// </summary>
[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;
}
/// <summary>
/// 获取所有可用的教程目录
/// </summary>
/// <returns>教程目录列表</returns>
[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<string>() });
}
// 获取所有子目录
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 = "无法读取教程目录" });
}
}
}

View File

@ -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: '无法读取例程目录' });
}
}

View File

@ -1,30 +1,90 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import { marked } from 'marked';
import hljs from 'highlight.js';
//
import 'highlight.js/styles/github.css'; //
//
import { useThemeStore } from '@/stores/theme';
const props = defineProps({
content: {
type: String,
required: true
},
removeFirstH1: {
type: Boolean,
default: false
}
});
// 使
const themeStore = useThemeStore();
// 使 isDarkTheme
const isDarkMode = computed(() => themeStore.isDarkTheme());
//
watch(() => themeStore.currentTheme, () => {
//
updateCodeBlocksTheme();
});
//
const updateCodeBlocksTheme = () => {
//
// 使CSS
//
};
const renderedContent = computed(() => {
if (!props.content) return '<p>没有内容</p>';
let processedContent = props.content;
// marked
//
if (props.removeFirstH1) {
const lines = processedContent.split('\n');
const firstH1Index = lines.findIndex(line => line.startsWith('# '));
if (firstH1Index !== -1) {
processedContent = lines.slice(firstH1Index + 1).join('\n');
}
}
//
const renderer = new marked.Renderer();
marked.setOptions({
//
renderer.code = (code, incomingLanguage) => {
//
const language = incomingLanguage || 'plaintext';
//
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext';
//
const highlightedCode = hljs.highlight(code, { language: validLanguage }).value;
//
return `<pre class="hljs" data-language="${validLanguage}"><code class="language-${validLanguage}">${highlightedCode}</code></pre>`;
};
// marked
marked.use({
renderer: renderer,
gfm: true,
breaks: true
});
return marked(processedContent);
});
//
onMounted(() => {
updateCodeBlocksTheme();
});
</script>
<template>
<div class="markdown-content" v-html="renderedContent"></div>
<div class="markdown-content" :data-theme="themeStore.currentTheme" v-html="renderedContent"></div>
</template>
<style scoped>
@ -33,6 +93,8 @@ const renderedContent = computed(() => {
line-height: 1.6;
padding: 1rem 1.5rem;
max-width: 100%;
background-color: inherit; /* 继承父元素的背景色 */
height: 100%;
}
.markdown-content :deep(img) {
@ -45,47 +107,64 @@ const renderedContent = computed(() => {
}
.markdown-content :deep(h1) {
margin-top: 2rem;
margin-bottom: 1rem;
margin-top: 2.5rem;
margin-bottom: 1.5rem;
color: hsl(var(--bc));
font-weight: 700;
font-size: 2rem;
font-size: 2.2rem;
line-height: 1.3;
padding-bottom: 0.5rem;
border-bottom: 1px solid hsl(var(--b2));
padding-bottom: 0.7rem;
border-bottom: 2px solid hsl(var(--p) / 0.7);
text-shadow: 1px 1px 2px rgba(0,0,0,0.05);
}
.markdown-content :deep(h2) {
margin-top: 1.8rem;
margin-bottom: 0.8rem;
margin-top: 2rem;
margin-bottom: 1rem;
color: hsl(var(--bc));
font-weight: 600;
font-size: 1.5rem;
font-size: 1.7rem;
line-height: 1.4;
padding-left: 0.5rem;
border-left: 4px solid hsl(var(--p));
padding: 0.5rem 1rem;
border-left: 5px solid hsl(var(--p));
background: linear-gradient(to right, hsl(var(--b2) / 0.5), transparent);
border-radius: 0.3rem;
}
.markdown-content :deep(h3) {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
margin-top: 1.8rem;
margin-bottom: 0.9rem;
color: hsl(var(--bc));
font-weight: 600;
font-size: 1.25rem;
font-size: 1.4rem;
line-height: 1.4;
padding-left: 1rem;
border-left: 3px solid hsl(var(--s));
padding-top: 0.3rem;
padding-bottom: 0.3rem;
}
.markdown-content :deep(h4),
.markdown-content :deep(h5),
.markdown-content :deep(h6) {
margin-top: 1.2rem;
margin-bottom: 0.6rem;
margin-top: 1.5rem;
margin-bottom: 0.7rem;
color: hsl(var(--bc));
font-weight: 600;
font-size: 1.1rem;
font-size: 1.2rem;
line-height: 1.5;
padding-left: 1.5rem;
position: relative;
}
.markdown-content :deep(h4::before),
.markdown-content :deep(h5::before),
.markdown-content :deep(h6::before) {
content: '▶';
color: hsl(var(--p) / 0.7);
position: absolute;
left: 0.2rem;
font-size: 0.9em;
}
.markdown-content :deep(p) {
@ -97,21 +176,31 @@ const renderedContent = computed(() => {
.markdown-content :deep(ul),
.markdown-content :deep(ol) {
padding-left: 2em;
margin: 0.75rem 0;
padding-left: 2.5em;
margin: 1.25rem 0;
color: hsl(var(--bc) / 0.8);
background-color: hsl(var(--b1) / 0.3);
border-radius: 0.5rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
padding-right: 1rem;
border-left: 3px solid hsl(var(--p) / 0.7);
}
.markdown-content :deep(li) {
margin: 0.4rem 0;
margin: 0.5rem 0;
position: relative;
padding-left: 0.5rem;
}
.markdown-content :deep(ul ul),
.markdown-content :deep(ul ol),
.markdown-content :deep(ol ul),
.markdown-content :deep(ol ol) {
margin: 0.4rem 0 0.4rem 1rem;
margin: 0.5rem 0 0.5rem 0.5rem;
padding-left: 1.5rem;
border-left: 2px solid hsl(var(--s) / 0.5);
background-color: transparent;
}
.markdown-content :deep(ul) {
@ -134,45 +223,142 @@ const renderedContent = computed(() => {
color: hsl(var(--p));
}
.markdown-content :deep(ol li::marker) {
color: hsl(var(--s));
}
/* 代码块样式增强 - 响应主题 */
.markdown-content :deep(pre) {
background-color: hsl(var(--b3));
background-color: var(--code-bg, hsl(var(--b2)));
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
border: 1px solid hsl(var(--b2));
margin: 1rem 0;
margin: 1.5rem 0;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
position: relative;
color: var(--code-color, hsl(var(--bc)));
}
.markdown-content :deep(pre::before) {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, hsl(var(--p)), hsl(var(--s)));
border-radius: 0.5rem 0.5rem 0 0;
}
/* 代码语言标签 */
.markdown-content :deep(pre.hljs::after) {
content: attr(data-language);
position: absolute;
top: 0;
right: 0;
color: var(--code-label-color, hsl(var(--bc) / 0.7));
font-size: 0.75rem;
background-color: var(--code-label-bg, hsl(var(--b3)));
padding: 0.2rem 0.5rem;
border-radius: 0 0.3rem 0 0.3rem;
opacity: 0.8;
}
/* 内联代码样式 */
.markdown-content :deep(code) {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
background-color: hsl(var(--b3));
padding: 2px 0.5rem;
background-color: var(--inline-code-bg, hsl(var(--b3) / 0.7));
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.9em;
color: hsl(var(--p));
color: var(--inline-code-color, hsl(var(--p)));
border: 1px solid hsl(var(--b2) / 0.5);
}
/* 确保代码块内的代码不受内联代码样式影响 */
.markdown-content :deep(pre code) {
background-color: transparent;
padding: 0;
border: none;
color: inherit;
font-size: 0.95em;
line-height: 1.5;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
/* 为常见语言添加一些特殊的高亮效果 */
.markdown-content :deep(.hljs-keyword),
.markdown-content :deep(.hljs-tag),
.markdown-content :deep(.hljs-selector-tag) {
color: #cc99cd; /* 紫色,用于关键字 */
}
.markdown-content :deep(.hljs-string),
.markdown-content :deep(.hljs-regexp),
.markdown-content :deep(.hljs-template-tag) {
color: #7ec699; /* 绿色,用于字符串 */
}
.markdown-content :deep(.hljs-number),
.markdown-content :deep(.hljs-literal) {
color: #f08d49; /* 橙色,用于数字 */
}
.markdown-content :deep(.hljs-comment) {
color: #999999; /* 灰色,用于注释 */
font-style: italic;
}
.markdown-content :deep(.hljs-name),
.markdown-content :deep(.hljs-attribute),
.markdown-content :deep(.hljs-selector-id),
.markdown-content :deep(.hljs-selector-class) {
color: #e2777a; /* 红色用于HTML标签名和属性 */
}
.markdown-content :deep(.hljs-built_in),
.markdown-content :deep(.hljs-builtin-name) {
color: #6196cc; /* 蓝色,用于内置函数 */
}
.markdown-content :deep(.hljs-title),
.markdown-content :deep(.hljs-function) {
color: #f8c555; /* 金色,用于函数名和类名 */
}
.markdown-content :deep(table) {
border-collapse: collapse;
border-collapse: separate;
border-spacing: 0;
width: 100%;
margin: 1rem 0;
margin: 1.5rem 0;
background-color: hsl(var(--b1));
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
border: 1px solid hsl(var(--b2));
}
.markdown-content :deep(th),
.markdown-content :deep(td) {
border: 1px solid hsl(var(--b2));
padding: 0.75rem;
padding: 0.75rem 1rem;
text-align: left;
}
.markdown-content :deep(th) {
background-color: hsl(var(--b2));
font-weight: 500;
background-color: hsl(var(--p) / 0.15);
font-weight: 600;
color: hsl(var(--bc));
border-bottom: 2px solid hsl(var(--p) / 0.5);
}
.markdown-content :deep(tr:nth-child(even)) {
background-color: hsl(var(--b2) / 0.3);
}
.markdown-content :deep(tr:hover) {
background-color: hsl(var(--b2) / 0.5);
}
.markdown-content :deep(td) {
@ -180,12 +366,29 @@ const renderedContent = computed(() => {
}
.markdown-content :deep(blockquote) {
margin: 1rem 0;
padding: 0.5rem 1rem;
margin: 1.5rem 0;
padding: 1rem 1.5rem;
border-left: 4px solid hsl(var(--p));
background-color: hsl(var(--b2));
color: hsl(var(--bc) / 0.8);
background-color: hsl(var(--b2) / 0.3);
color: hsl(var(--bc) / 0.9);
font-style: italic;
border-radius: 0 0.5rem 0.5rem 0;
box-shadow: 0 2px 5px rgba(0,0,0,0.03);
position: relative;
}
.markdown-content :deep(blockquote::before) {
content: '"';
font-size: 2rem;
color: hsl(var(--p) / 0.3);
position: absolute;
top: 0.5rem;
left: 0.5rem;
font-family: serif;
}
.markdown-content :deep(blockquote p) {
margin-top: 0.5rem;
}
.markdown-content :deep(hr) {
@ -204,4 +407,245 @@ const renderedContent = computed(() => {
color: hsl(var(--pf));
text-decoration: underline;
}
/* 亮色主题下的代码样式 */
:root[data-theme="winter"] .markdown-content :deep(.hljs),
.markdown-content[data-theme="winter"] :deep(.hljs),
[data-theme="winter"] .markdown-content :deep(.hljs) {
--code-bg: #f5f7ff;
--code-color: #333;
--inline-code-bg: #f0f2fa;
--inline-code-color: #d32f2f;
--code-label-bg: #e6e9f5;
--code-label-color: #666;
}
/* 亮色主题下的语法高亮 */
:root[data-theme="winter"] .markdown-content :deep(.hljs-keyword),
.markdown-content[data-theme="winter"] :deep(.hljs-keyword),
[data-theme="winter"] .markdown-content :deep(.hljs-keyword),
:root[data-theme="winter"] .markdown-content :deep(.hljs-tag),
.markdown-content[data-theme="winter"] :deep(.hljs-tag),
[data-theme="winter"] .markdown-content :deep(.hljs-tag),
:root[data-theme="winter"] .markdown-content :deep(.hljs-selector-tag),
.markdown-content[data-theme="winter"] :deep(.hljs-selector-tag),
[data-theme="winter"] .markdown-content :deep(.hljs-selector-tag) {
color: #8959a8; /* 紫色,用于关键字 */
}
:root[data-theme="winter"] .markdown-content :deep(.hljs-string),
.markdown-content[data-theme="winter"] :deep(.hljs-string),
[data-theme="winter"] .markdown-content :deep(.hljs-string),
:root[data-theme="winter"] .markdown-content :deep(.hljs-regexp),
.markdown-content[data-theme="winter"] :deep(.hljs-regexp),
[data-theme="winter"] .markdown-content :deep(.hljs-regexp) {
color: #2e7d32; /* 绿色,用于字符串 */
}
:root[data-theme="winter"] .markdown-content :deep(.hljs-number),
.markdown-content[data-theme="winter"] :deep(.hljs-number),
[data-theme="winter"] .markdown-content :deep(.hljs-number),
:root[data-theme="winter"] .markdown-content :deep(.hljs-literal),
.markdown-content[data-theme="winter"] :deep(.hljs-literal),
[data-theme="winter"] .markdown-content :deep(.hljs-literal) {
color: #f5871f; /* 橙色,用于数字 */
}
:root[data-theme="winter"] .markdown-content :deep(.hljs-comment),
.markdown-content[data-theme="winter"] :deep(.hljs-comment),
[data-theme="winter"] .markdown-content :deep(.hljs-comment) {
color: #8e908c; /* 灰色,用于注释 */
font-style: italic;
}
:root[data-theme="winter"] .markdown-content :deep(.hljs-built_in),
.markdown-content[data-theme="winter"] :deep(.hljs-built_in),
[data-theme="winter"] .markdown-content :deep(.hljs-built_in) {
color: #3e999f; /* 青色,用于内置函数 */
}
:root[data-theme="winter"] .markdown-content :deep(.hljs-title),
.markdown-content[data-theme="winter"] :deep(.hljs-title),
[data-theme="winter"] .markdown-content :deep(.hljs-title),
:root[data-theme="winter"] .markdown-content :deep(.hljs-function),
.markdown-content[data-theme="winter"] :deep(.hljs-function),
[data-theme="winter"] .markdown-content :deep(.hljs-function) {
color: #4271ae; /* 蓝色,用于函数名 */
}
:root[data-theme="winter"] .markdown-content :deep(.hljs-name),
.markdown-content[data-theme="winter"] :deep(.hljs-name),
[data-theme="winter"] .markdown-content :deep(.hljs-name),
:root[data-theme="winter"] .markdown-content :deep(.hljs-attribute),
.markdown-content[data-theme="winter"] :deep(.hljs-attribute),
[data-theme="winter"] .markdown-content :deep(.hljs-attribute) {
color: #c82829; /* 红色用于HTML标签和属性 */
}
/* 暗黑主题下的代码样式 */
:root[data-theme="night"] .markdown-content :deep(.hljs),
.markdown-content[data-theme="night"] :deep(.hljs),
[data-theme="night"] .markdown-content :deep(.hljs) {
--code-bg: #1e1e2e;
--code-color: #f8f8f2;
--inline-code-bg: #282a36;
--inline-code-color: #ff79c6;
--code-label-bg: #282a36;
--code-label-color: #bd93f9;
}
/* 暗黑主题下的语法高亮 */
:root[data-theme="night"] .markdown-content :deep(.hljs-keyword),
.markdown-content[data-theme="night"] :deep(.hljs-keyword),
[data-theme="night"] .markdown-content :deep(.hljs-keyword),
:root[data-theme="night"] .markdown-content :deep(.hljs-tag),
.markdown-content[data-theme="night"] :deep(.hljs-tag),
[data-theme="night"] .markdown-content :deep(.hljs-tag),
:root[data-theme="night"] .markdown-content :deep(.hljs-selector-tag),
.markdown-content[data-theme="night"] :deep(.hljs-selector-tag),
[data-theme="night"] .markdown-content :deep(.hljs-selector-tag) {
color: #cc99cd; /* 紫色,用于关键字 */
}
:root[data-theme="night"] .markdown-content :deep(.hljs-string),
.markdown-content[data-theme="night"] :deep(.hljs-string),
[data-theme="night"] .markdown-content :deep(.hljs-string),
:root[data-theme="night"] .markdown-content :deep(.hljs-regexp),
.markdown-content[data-theme="night"] :deep(.hljs-regexp),
[data-theme="night"] .markdown-content :deep(.hljs-regexp) {
color: #7ec699; /* 绿色,用于字符串 */
}
:root[data-theme="night"] .markdown-content :deep(.hljs-number),
.markdown-content[data-theme="night"] :deep(.hljs-number),
[data-theme="night"] .markdown-content :deep(.hljs-number),
:root[data-theme="night"] .markdown-content :deep(.hljs-literal),
.markdown-content[data-theme="night"] :deep(.hljs-literal),
[data-theme="night"] .markdown-content :deep(.hljs-literal) {
color: #f08d49; /* 橙色,用于数字 */
}
:root[data-theme="night"] .markdown-content :deep(.hljs-comment),
.markdown-content[data-theme="night"] :deep(.hljs-comment),
[data-theme="night"] .markdown-content :deep(.hljs-comment) {
color: #999999; /* 灰色,用于注释 */
font-style: italic;
}
:root[data-theme="night"] .markdown-content :deep(.hljs-built_in),
.markdown-content[data-theme="night"] :deep(.hljs-built_in),
[data-theme="night"] .markdown-content :deep(.hljs-built_in) {
color: #6196cc; /* 蓝色,用于内置函数 */
}
:root[data-theme="night"] .markdown-content :deep(.hljs-title),
.markdown-content[data-theme="night"] :deep(.hljs-title),
[data-theme="night"] .markdown-content :deep(.hljs-title),
:root[data-theme="night"] .markdown-content :deep(.hljs-function),
.markdown-content[data-theme="night"] :deep(.hljs-function),
[data-theme="night"] .markdown-content :deep(.hljs-function) {
color: #f8c555; /* 金色,用于函数名 */
}
:root[data-theme="night"] .markdown-content :deep(.hljs-name),
.markdown-content[data-theme="night"] :deep(.hljs-name),
[data-theme="night"] .markdown-content :deep(.hljs-name),
:root[data-theme="night"] .markdown-content :deep(.hljs-attribute),
.markdown-content[data-theme="night"] :deep(.hljs-attribute),
[data-theme="night"] .markdown-content :deep(.hljs-attribute) {
color: #e2777a; /* 红色用于HTML标签和属性 */
}
.markdown-content :deep(table) {
border-collapse: separate;
border-spacing: 0;
width: 100%;
margin: 1.5rem 0;
background-color: hsl(var(--b1));
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
border: 1px solid hsl(var(--b2));
}
.markdown-content :deep(th),
.markdown-content :deep(td) {
border: 1px solid hsl(var(--b2));
padding: 0.75rem 1rem;
text-align: left;
}
.markdown-content :deep(th) {
background-color: hsl(var(--p) / 0.15);
font-weight: 600;
color: hsl(var(--bc));
border-bottom: 2px solid hsl(var(--p) / 0.5);
}
.markdown-content :deep(tr:nth-child(even)) {
background-color: hsl(var(--b2) / 0.3);
}
.markdown-content :deep(tr:hover) {
background-color: hsl(var(--b2) / 0.5);
}
.markdown-content :deep(td) {
color: hsl(var(--bc) / 0.8);
}
.markdown-content :deep(blockquote) {
margin: 1.5rem 0;
padding: 1rem 1.5rem;
border-left: 4px solid hsl(var(--p));
background-color: hsl(var(--b2) / 0.3);
color: hsl(var(--bc) / 0.9);
font-style: italic;
border-radius: 0 0.5rem 0.5rem 0;
box-shadow: 0 2px 5px rgba(0,0,0,0.03);
position: relative;
}
.markdown-content :deep(blockquote::before) {
content: '"';
font-size: 2rem;
color: hsl(var(--p) / 0.3);
position: absolute;
top: 0.5rem;
left: 0.5rem;
font-family: serif;
}
.markdown-content :deep(blockquote p) {
margin-top: 0.5rem;
}
.markdown-content :deep(hr) {
border: none;
border-top: 1px solid hsl(var(--b2));
margin: 1.5rem 0;
}
.markdown-content :deep(a) {
color: hsl(var(--p));
text-decoration: none;
transition: color 0.2s;
}
.markdown-content :deep(a:hover) {
color: hsl(var(--pf));
text-decoration: underline;
}
/* 暗黑模式下的代码高亮调整 */
@media (prefers-color-scheme: dark) {
.markdown-content :deep(pre) {
background-color: hsl(var(--b3));
border-color: hsl(var(--b1) / 0.7);
}
.markdown-content :deep(code) {
background-color: hsl(var(--b2) / 0.7);
}
}
</style>

View File

@ -49,6 +49,19 @@
测试功能
</router-link>
</li>
<li class="my-1 hover:translate-x-1 transition-all duration-300">
<router-link to="/markdown-test" class="text-base font-medium">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
Markdown测试
</router-link>
</li>
<li class="my-1 hover:translate-x-1 transition-all duration-300">
<a href="http://localhost:5000/swagger" target="_self" rel="noopener noreferrer"
class="text-base font-medium">

View File

@ -0,0 +1,305 @@
<template>
<div
class="tutorial-carousel relative"
@wheel.prevent="handleWheel"
@mouseenter="pauseAutoRotation"
@mouseleave="resumeAutoRotation"
> <!-- 例程卡片堆叠 -->
<div class="card-stack relative mx-auto">
<div
v-for="(tutorial, index) in tutorials"
:key="index"
class="tutorial-card absolute transition-all duration-500 ease-in-out rounded-2xl shadow-2xl border-4 border-base-300 overflow-hidden"
:class="getCardClass(index)"
:style="getCardStyle(index)"
@click="handleCardClick(index, tutorial.id)"
>
<!-- 卡片内容 -->
<div class="relative">
<!-- 图片 --> <img
:src="tutorial.thumbnail || `https://placehold.co/600x400?text=${tutorial.title}`"
class="w-full object-contain"
:alt="tutorial.title"
style="width: 600px; height: 400px;"
/>
<!-- 卡片蒙层 -->
<div
class="absolute inset-0 bg-primary opacity-20 transition-opacity duration-300"
:class="{'opacity-10': index === currentIndex}"
></div>
<!-- 标题覆盖层 -->
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-base-300 to-transparent">
<h3 class="text-lg font-bold text-base-content">{{ tutorial.title }}</h3>
<p class="text-sm opacity-80 truncate">{{ tutorial.description }}</p>
</div>
</div>
</div>
</div>
<!-- 导航指示器 -->
<div class="indicators flex justify-center gap-2 mt-4">
<button
v-for="(_, index) in tutorials"
:key="index"
@click="setActiveCard(index)"
class="w-3 h-3 rounded-full transition-all duration-300"
:class="index === currentIndex ? 'bg-primary scale-125' : 'bg-base-300'"
></button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
//
interface Tutorial {
id: string;
title: string;
description: string;
thumbnail?: string;
docPath: string;
}
// Props
const props = defineProps<{
autoRotationInterval?: number;
}>();
//
const autoRotationInterval = props.autoRotationInterval || 5000; // 5
//
const tutorials = ref<Tutorial[]>([]);
const currentIndex = ref(0);
const router = useRouter();
let autoRotationTimer: number | null = null;
//
const handleCardClick = (index: number, tutorialId: string) => {
if (index === currentIndex.value) {
goToTutorial(tutorialId);
} else {
setActiveCard(index);
}
};
// public/doc
onMounted(async () => {
try {
// API
let tutorialIds: string[] = [];
try {
const response = await fetch('/api/tutorial');
if (response.ok) {
const data = await response.json();
tutorialIds = data.tutorials || [];
}
} catch (error) {
console.warn('无法从API获取教程目录使用默认值:', error);
}
// API使
if (tutorialIds.length === 0) {
console.log('使用默认教程列表');
tutorialIds = ['01', '02', '03', '04', '05', '06', '11', '12', '13']; //
} else {
console.log('使用API获取的教程列表:', tutorialIds);
}
//
const tutorialPromises = tutorialIds.map(async (id) => {
// doc.md
let title = `例程 ${id}`;
let description = "点击加载此例程";
let thumbnail = `/doc/${id}/cover.png`; // 使
try {
//
const response = await fetch(`/doc/${id}/doc.md`);
if (response.ok) {
const text = await response.text();
// Markdown
const titleMatch = text.match(/^#\s+(.+)$/m);
if (titleMatch && titleMatch[1]) {
title = titleMatch[1].trim();
}
//
const descMatch = text.match(/\n\n([^#\n][^\n]+)/);
if (descMatch && descMatch[1]) {
description = descMatch[1].substring(0, 100).trim();
if (description.length === 100) description += '...';
}
}
} catch (error) {
console.warn(`无法读取例程${id}的文档内容:`, error);
}
return {
id,
title,
description,
thumbnail,
docPath: `/doc/${id}/doc.md`
};
});
tutorials.value = await Promise.all(tutorialPromises);
//
startAutoRotation();
} catch (error) {
console.error('加载例程失败:', error);
}
});
//
onUnmounted(() => {
if (autoRotationTimer) {
clearInterval(autoRotationTimer);
}
});
//
const handleWheel = (event: WheelEvent) => {
if (event.deltaY > 0) {
nextCard();
} else {
prevCard();
}
};
//
const nextCard = () => {
currentIndex.value = (currentIndex.value + 1) % tutorials.value.length;
};
//
const prevCard = () => {
currentIndex.value = (currentIndex.value - 1 + tutorials.value.length) % tutorials.value.length;
};
//
const setActiveCard = (index: number) => {
currentIndex.value = index;
};
//
const startAutoRotation = () => {
autoRotationTimer = window.setInterval(() => {
nextCard();
}, autoRotationInterval);
};
//
const pauseAutoRotation = () => {
if (autoRotationTimer) {
clearInterval(autoRotationTimer);
autoRotationTimer = null;
}
};
//
const resumeAutoRotation = () => {
if (!autoRotationTimer) {
startAutoRotation();
}
};
//
const goToTutorial = (tutorialId: string) => {
// query
router.push({
path: '/project',
query: { tutorial: tutorialId }
});
};
//
const getCardClass = (index: number) => {
const isActive = index === currentIndex.value;
const isPrev = (index === currentIndex.value - 1) || (currentIndex.value === 0 && index === tutorials.value.length - 1);
const isNext = (index === currentIndex.value + 1) || (currentIndex.value === tutorials.value.length - 1 && index === 0);
return {
'z-30': isActive,
'z-20': isPrev || isNext,
'z-10': !isActive && !isPrev && !isNext,
'hover:scale-105': isActive,
'cursor-pointer': true
};
};
const getCardStyle = (index: number) => {
const isActive = index === currentIndex.value;
const isPrev = (index === currentIndex.value - 1) || (currentIndex.value === 0 && index === tutorials.value.length - 1);
const isNext = (index === currentIndex.value + 1) || (currentIndex.value === tutorials.value.length - 1 && index === 0);
//
let style = {
transform: 'scale(1) translateY(0) rotate(0deg)',
opacity: '1',
filter: 'blur(0)'
};
//
if (isActive) {
return style;
}
//
if (isPrev) {
style.transform = 'scale(0.85) translateY(-10%) rotate(-5deg)';
style.opacity = '0.7';
style.filter = 'blur(1px)';
return style;
}
//
if (isNext) {
style.transform = 'scale(0.85) translateY(10%) rotate(5deg)';
style.opacity = '0.7';
style.filter = 'blur(1px)';
return style;
}
//
style.transform = 'scale(0.7) translateY(0) rotate(0deg)';
style.opacity = '0.4';
style.filter = 'blur(2px)';
return style;
}
</script>
<style scoped>
.tutorial-carousel {
width: 100%;
height: 500px;
perspective: 1000px;
display: flex;
flex-direction: column;
align-items: center;
}
.card-stack {
width: 600px;
height: 440px;
position: relative;
transform-style: preserve-3d;
}
.tutorial-card {
width: 600px;
height: 400px;
background-color: hsl(var(--b2));
will-change: transform, opacity;
}
.tutorial-card:hover {
box-shadow: 0 0 15px rgba(var(--p), 0.5);
}
</style>

View File

@ -78,6 +78,8 @@ const pinRefs = ref<Record<string, any>>({});
interface SevenSegmentDisplayProps {
size?: number;
color?: string;
AFTERGLOW_BUFFER_SIZE?: number; //
AFTERGLOW_UPDATE_INTERVAL?: number; //
pins?: {
pinId: string;
constraint: string;
@ -90,6 +92,8 @@ interface SevenSegmentDisplayProps {
const props = withDefaults(defineProps<SevenSegmentDisplayProps>(), {
size: 1,
color: "red",
AFTERGLOW_BUFFER_SIZE: 1, // 100
AFTERGLOW_UPDATE_INTERVAL: 1, // 2
cathodeType: "common", //
pins: () => [
{ pinId: "a", constraint: "", x: 10, y: 170 }, // a
@ -121,7 +125,7 @@ watch(
{ deep: true },
);
//
//
const segmentStates = ref({
a: false,
b: false,
@ -133,59 +137,127 @@ const segmentStates = ref({
dp: false,
});
//
// -
const afterglowBuffers = ref<Record<string, boolean[]>>({
a: [],
b: [],
c: [],
d: [],
e: [],
f: [],
g: [],
dp: [],
});
//
let updateIntervalTimer: number | null = null;
// - true
function isSegmentActive(
segment: "a" | "b" | "c" | "d" | "e" | "f" | "g" | "dp",
): boolean {
return segmentStates.value[segment];
return segmentStates.value[segment] || afterglowBuffers.value[segment].some(state => state);
}
//
function updateSegmentStates() {
// COM
const comPin = props.pins.find(p => p.pinId === "COM");
let comActive = true;
if (comPin && comPin.constraint) {
const comState = getConstraintState(comPin.constraint);
if (props.cathodeType === "anode") {
// anodeCOM
comActive = comState !== "high";
}
//
}
// COM
if (comActive) {
updateAfterglowBuffers();
}
for (const pin of props.pins) {
if (["a", "b", "c", "d", "e", "f", "g", "dp"].includes(pin.pinId)) {
// constraint
if (!pin.constraint) {
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] =
false;
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = false;
continue;
}
const pinState = getConstraintState(pin.constraint);
// /
let newState: boolean;
if (props.cathodeType === "common") {
// :
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] =
pinState === "high";
newState = pinState === "high";
} else {
// :
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] =
pinState === "low";
newState = pinState === "low";
}
//
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = newState && comActive;
}
}
}
// -
function updateAfterglowBuffers() {
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
const typedSegmentId = segmentId as keyof typeof segmentStates.value;
const currentState = segmentStates.value[typedSegmentId];
//
afterglowBuffers.value[segmentId].unshift(currentState);
//
if (afterglowBuffers.value[segmentId].length > props.AFTERGLOW_BUFFER_SIZE) {
afterglowBuffers.value[segmentId].pop();
}
}
}
//
function startAfterglowUpdates() {
if (updateIntervalTimer) return;
updateIntervalTimer = window.setInterval(() => {
updateSegmentStates();
}, props.AFTERGLOW_UPDATE_INTERVAL);
}
//
function stopAfterglowUpdates() {
if (updateIntervalTimer) {
window.clearInterval(updateIntervalTimer);
updateIntervalTimer = null;
}
}
//
function onConstraintChange(constraint: string, level: string) {
const affectedPin = props.pins.find((pin) => pin.constraint === constraint);
if (
affectedPin &&
["a", "b", "c", "d", "e", "f", "g", "dp"].includes(affectedPin.pinId)
) {
if (affectedPin) {
updateSegmentStates();
}
}
//
onMounted(() => {
//
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
afterglowBuffers.value[segmentId] = Array(props.AFTERGLOW_BUFFER_SIZE).fill(false);
}
updateSegmentStates();
onConstraintStateChange(onConstraintChange);
startAfterglowUpdates();
});
onUnmounted(() => {
//
stopAfterglowUpdates();
});
//

View File

@ -1,23 +1,22 @@
import { createWebHistory, createRouter } from "vue-router";
import LoginView from "../views/LoginView.vue";
import UserView from "../views/UserView.vue";
import TestView from "../views/TestView.vue";
import ProjectView from "../views/ProjectView.vue";
import HomeView from "@/views/HomeView.vue";
import AdminView from "@/views/AdminView.vue";
const routes = [
{ path: "/", name: "Home", component: HomeView },
{ path: "/login", name: "Login", component: LoginView },
{ path: "/user", name: "User", component: UserView },
{ path: "/test", name: "Test", component: TestView },
{ path: "/project", name: "Project", component: ProjectView },
{ path: "/admin", name: "Admin", component: AdminView },
];
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue'
import LabView from '../views/LabView.vue'
import ProjectView from '../views/ProjectView.vue'
import TestView from '../views/TestView.vue'
import UserView from '../views/UserView.vue'
import AdminView from '../views/AdminView.vue'
const router = createRouter({
history: createWebHistory(),
routes,
});
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{path: '/', name: 'home', component: HomeView},
{path: '/login', name: 'login', component: LoginView},
{path: '/lab/:id',name: 'lab', component: LabView},
{path: '/project',name: 'project',component: ProjectView},
{path: '/test', name: 'test', component: TestView},
{path: '/user', name: 'user', component: UserView},
{path: '/admin', name: 'admin', component: AdminView}]
})
export default router;
export default router

View File

@ -1,14 +1,9 @@
<template>
<div class="bg-base-200 min-h-screen">
<main class="hero min-h-screen bg-base-200">
<div class="hero-content flex-col lg:flex-row-reverse gap-8 lg:gap-12 py-10 px-4">
<!-- 图片容器 -->
<div
class="image-container relative w-full max-w-sm hover:scale-105 hover:-rotate-1 transition-transform duration-500 ease-in-out">
<img src="https://placehold.co/600x400"
class="w-full rounded-2xl shadow-2xl border-4 border-base-300 transition-shadow duration-300 hover:shadow-primary" />
<!-- 这里使用relative定位限制覆盖层只在图片容器内 -->
<div class="absolute inset-0 bg-primary opacity-10 rounded-2xl pointer-events-none"></div>
<div class="hero-content flex-col xl:flex-row-reverse gap-8 xl:gap-12 py-10 px-4"> <!-- 例程轮播容器 -->
<div class="w-full flex justify-center" style="min-width: 650px;">
<TutorialCarousel :autoRotationInterval="3000" />
</div>
<!-- 内容容器 -->
<div class="content-container max-w-md lg:max-w-2xl transform transition-all duration-500 ease-in-out">
@ -93,6 +88,7 @@
<script lang="ts" setup>
import "@/router";
import TutorialCarousel from "@/components/TutorialCarousel.vue";
</script>
<style scoped lang="postcss">

View File

@ -26,9 +26,7 @@
<div
class="resizer bg-base-100 hover:bg-primary hover:opacity-70 active:bg-primary active:opacity-90 transition-colors"
@mousedown="startResize"
></div>
<!-- 右侧编辑区域 -->
></div> <!-- 右侧编辑区域 -->
<div
class="bg-base-200 h-full overflow-hidden flex flex-col"
:style="{ width: 100 - leftPanelWidth + '%' }"
@ -41,10 +39,9 @@
:componentConfig="selectedComponentConfig"
@updateProp="updateComponentProp"
@updateDirectProp="updateComponentDirectProp"
/>
<div
/> <div
v-else
class="doc-panel overflow-y-auto bg-base-100 rounded-md h-full"
class="doc-panel overflow-y-auto h-full"
>
<MarkdownRenderer :content="documentContent" />
</div>
@ -82,20 +79,57 @@ import {
const showDocPanel = ref(false);
const documentContent = ref("");
//
import { useRoute } from 'vue-router';
const route = useRoute();
//
async function toggleDocPanel() {
showDocPanel.value = !showDocPanel.value;
//
if (showDocPanel.value) {
const response = await fetch("/doc/01_water_led/water_led.md");
documentContent.value = (await response.text()).replace(
/.\/images/gi,
"/doc/01_water_led/images",
);
await loadDocumentContent();
}
}
//
async function loadDocumentContent() {
try {
// ID
const tutorialId = route.query.tutorial as string || '02'; // 02
//
let docPath = `/doc/${tutorialId}/doc.md`;
// 线 02_key
// 使
if (!tutorialId.includes('_')) {
docPath = `/doc/${tutorialId}/doc.md`;
}
//
const response = await fetch(docPath);
if (!response.ok) {
throw new Error(`Failed to load document: ${response.status}`);
}
//
documentContent.value = (await response.text())
.replace(/.\/images/gi, `/doc/${tutorialId}/images`);
} catch (error) {
console.error('加载文档失败:', error);
documentContent.value = '# 文档加载失败\n\n无法加载请求的文档。'; }
}
//
onMounted(async () => {
if (route.query.tutorial) {
showDocPanel.value = true;
await loadDocumentContent();
}
});
// --- ---
const showComponentsMenu = ref(false);
const diagramData = ref<DiagramData>({
@ -807,8 +841,10 @@ body {
/* 文档面板样式 */
.doc-panel {
padding: 1.5rem;
max-width: 800px;
margin: 0 auto;
max-width: 100%;
margin: 0;
background-color: transparent; /* 使用透明背景 */
border: none; /* 确保没有边框 */
}
/* 文档切换按钮样式 */