75 Commits

Author SHA1 Message Date
de28471f87 rm:去除konva 2025-07-09 17:05:29 +08:00
3a292c0a98 refactor:持续解耦合 2025-07-09 16:32:17 +08:00
91b00a977c refactor: 给Canvas解耦合 2025-07-09 15:55:49 +08:00
c5ce246caf refactor: 重构projectview页面结构 2025-07-09 14:42:29 +08:00
c3bd61ed51 refactor: 重构canvas 2025-07-03 21:56:58 +08:00
14d8499f77 refactor: try to rewrite component manager 2025-07-02 21:16:18 +08:00
d18cf82813 feat: right mouse down to drag canvas 2025-07-02 20:27:35 +08:00
f1e2dbd9d8 feat: add components drawer 2025-07-01 21:08:58 +08:00
262c5e4003 fix: select not work 2025-07-01 20:12:39 +08:00
fbd13f8f2f fix: select rect wrong after zoom in / out 2025-06-30 21:48:41 +08:00
6cf7ef02ac feat: add resizer and add zoom in / out for canvas 2025-06-30 21:39:08 +08:00
8207c37e12 fix: box select failed 2025-06-14 10:58:00 +08:00
db71681bdf try to fix box select 2025-06-13 22:05:09 +08:00
2270022bbe feat: auto hide rect bounding when not hover 2025-06-13 21:05:44 +08:00
dcadb97a7f fix: correct calculate rect bounding 2025-06-13 20:08:46 +08:00
1538bb9d07 add boarder 2025-06-12 21:49:23 +08:00
f340c86a41 feat: add rect select but have some problems 2025-06-11 21:25:15 +08:00
b6fb7e05fa refactor: rewrite basic canvas using konva 2025-06-06 21:05:46 +08:00
e0db12e0eb update flake and add konva 2025-06-06 20:28:17 +08:00
dc64a65702 feat: add power control 2025-05-20 19:11:29 +08:00
46621fdb40 fix: jam when doc panel open 2025-05-20 18:34:44 +08:00
970a537391 Merge branch 'master' into csharp 2025-05-20 18:14:00 +08:00
3883cd8304 fix: matrix key not work 2025-05-20 18:13:52 +08:00
2aa2f1dc37 fix: matrix key not work 2025-05-20 18:13:52 +08:00
1bdcb672ab feat: frontend add virtual matrix key 2025-05-20 18:13:52 +08:00
7b1d1a5e87 fix: backend change matrix key api to post to fix set failed 2025-05-20 18:13:52 +08:00
5bb011e685 feag: backend add matrix key peripheral with its web api 2025-05-20 18:13:52 +08:00
e3826f0ff6 feat: add logger to udpclient 2025-05-20 18:13:52 +08:00
f8163be98b fix: matrix key not work 2025-05-20 18:11:04 +08:00
alivender
ea7c2d425a add: more exp 2025-05-20 18:04:25 +08:00
e4791b41a8 fix: matrix key not work 2025-05-20 17:23:00 +08:00
bea1c7e5ae feat: frontend add virtual matrix key 2025-05-20 17:09:57 +08:00
alivender
b6d8612e8c feat: optimize segment 2025-05-20 16:25:02 +08:00
b68d8eaf11 fix: backend change matrix key api to post to fix set failed 2025-05-20 11:21:12 +08:00
390ce8250d feag: backend add matrix key peripheral with its web api 2025-05-20 11:14:00 +08:00
alivender
09b8f676ba add: more exp markdown 2025-05-20 10:51:38 +08:00
alivender
cc83133a6c feat: code block dark mode 2025-05-20 10:15:42 +08:00
alivender
6d640e8049 add: home select exp 2025-05-20 09:35:29 +08:00
07eb606324 feat: add logger to udpclient 2025-05-20 09:34:26 +08:00
alivender
8eefed92a8 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab into dpp 2025-05-19 23:08:38 +08:00
alivender
8f2f90ef1d add: segment afterglow effect 2025-05-19 23:08:22 +08:00
487e7c114a chore: fix build failure
refactor: delete /public/equiments api
2025-05-19 22:29:28 +08:00
7f37514dfa fix: marked don't render images 2025-05-19 21:56:30 +08:00
0b0b4acb17 chore: remove all package 2025-05-19 21:42:30 +08:00
alivender
0bd3b42840 feat: add markdown 2025-05-19 21:40:55 +08:00
068576b60b Merge branch 'csharp' 2025-05-19 21:19:55 +08:00
d754a881d7 feat: frontend add set jtag frequency 2025-05-19 21:18:22 +08:00
a6ac728cf1 fix: boundary scan could not save 2025-05-19 18:47:15 +08:00
ba6ec73b84 fix: frontend upload bitstream failed 2025-05-19 15:56:23 +08:00
5042bf8ce5 feat: frontend add boundary scan 2025-05-19 13:30:06 +08:00
2a3ef1ea7d style: pretty frontend chip vue
feat: fronten add boundary scan to APIClient
2025-05-18 23:33:08 +08:00
4885447c2b feat: backend finish boundary scan web api 2025-05-18 23:17:56 +08:00
351a230107 feat: add bsdl parser 2025-05-18 21:52:44 +08:00
7aff4f3f02 fix: Component awlays reset 2025-05-17 17:32:16 +08:00
c7907b4253 fix: Componnent will reset when select of drag 2025-05-17 15:49:54 +08:00
3fb59af2dd fix: DDS and drag will reset conponent props 2025-05-17 15:09:59 +08:00
9b2ee8ad46 fix: DDS couldn't apply 2025-05-17 13:10:00 +08:00
alivender
7dd5e2189f add: markdown viewer 2025-05-16 20:35:43 +08:00
c39f688115 fix: mother board reactive problem 2025-05-16 16:38:57 +08:00
1eded97c76 fix: remote update failed and template not found 2025-05-15 20:23:48 +08:00
00ce79fa7b feat: add remote update function 2025-05-14 21:22:20 +08:00
alivender
48ae3b5975 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab into dpp 2025-05-14 19:45:10 +08:00
alivender
21197887cd add: AdminView and remote update view 2025-05-14 19:44:39 +08:00
f0fa32aeaa fix: jtag download failed 2025-05-14 19:40:37 +08:00
9657aacf83 feat: restore upload jtag bitstream 2025-05-13 21:52:58 +08:00
eea03f5bc8 feat: upload and download bitstream from the component of project view 2025-05-13 18:14:57 +08:00
eae67d04d4 fix: make boundary scan work 2025-05-13 13:57:57 +08:00
1102bba40d fix: jtag download failed 2025-05-13 13:19:26 +08:00
099d44663d Merge branch 'csharp' 2025-05-13 13:00:37 +08:00
74d40d25e2 feat: add swagger page 2025-05-12 21:56:41 +08:00
8699a568d1 feat: add jtag boundary scan 2025-05-12 21:56:17 +08:00
9eb3acb94c feat: setup spa proxy when develop backend server 2025-05-12 21:08:08 +08:00
257d63e63d feat: asp serve frontend static files 2025-05-12 20:05:26 +08:00
020674a277 feat: change test view to basic jtag upload and download page 2025-05-09 21:44:51 +08:00
10918a997c add submodule: python bsdl parser 2025-05-08 21:54:35 +08:00
143 changed files with 21590 additions and 4642 deletions

2
.gitignore vendored
View File

@@ -9,7 +9,9 @@ lerna-debug.log*
node_modules node_modules
.DS_Store .DS_Store
.vscode
dist dist
**/wwwroot
dist-ssr dist-ssr
coverage coverage
*.local *.local

View File

@@ -1,3 +1,6 @@
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
isSelfContained := "false"
@_show-dir: @_show-dir:
echo "Current Working Directory:" echo "Current Working Directory:"
pwd pwd
@@ -10,25 +13,45 @@ clean:
rm -rf "server.test/bin" rm -rf "server.test/bin"
rm -rf "server.test/obj" rm -rf "server.test/obj"
rm -rf "dist" rm -rf "dist"
rm -rf "wwwroot"
update:
npm install
dotnet restore ./server/server.csproj
git submodule update --init --remote --recursive
# 生成Restful API到网页客户端 # 生成Restful API到网页客户端
gen-api: gen-api:
cd server && dotnet run & npm run gen-api
gen-api-from-server:
npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts
pkill server
# 构建服务器包含win与linux平台 # 构建服务器包含win与linux平台
[working-directory: "server"] [working-directory: "server"]
build-server: _show-dir build-server self-contained=isSelfContained: _show-dir
dotnet publish --self-contained false -t:PublishAllRids dotnet publish --self-contained {{self-contained}} -t:PublishAllRids
npm run build
rsync -avz --delete ../wwwroot/ ./bin/Release/net9.0/linux-x64/publish/wwwroot/
rsync -avz --delete ../wwwroot/ ./bin/Release/net9.0/win-x64/publish/wwwroot/
# 运行服务器 run: run-server
[working-directory: "server"]
run-server: _show-dir run-server: (build-server "true")
dotnet run ./server/bin/Release/net9.0/linux-x64/publish/server
run-web:
npm run build
npm run preview
dev: dev-server
# 测试服务器
dev-server: _show-dir
dotnet run --watch --project ./server/server.csproj
# 运行网页客户端 # 运行网页客户端
run-web: dev-web:
npm run dev npm run dev
# 运行测试用例测试服务器 # 运行测试用例测试服务器

56
components.d.ts vendored Normal file
View File

@@ -0,0 +1,56 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
BaseBoard: typeof import('./src/components/equipments/BaseBoard.vue')['default']
Canvas: typeof import('./src/components/Canvas.vue')['default']
CollapsibleSection: typeof import('./src/components/CollapsibleSection.vue')['default']
ComponentSelector: typeof import('./src/components/LabCanvas/ComponentSelector.vue')['default']
DDR: typeof import('./src/components/equipments/DDR.vue')['default']
DDS: typeof import('./src/components/equipments/DDS.vue')['default']
DDSPropertyEditor: typeof import('./src/components/equipments/DDSPropertyEditor.vue')['default']
DiagramCanvas: typeof import('./src/components/LabCanvas/DiagramCanvas.vue')['default']
Dialog: typeof import('./src/components/Dialog.vue')['default']
ETH: typeof import('./src/components/equipments/ETH.vue')['default']
HDMI: typeof import('./src/components/equipments/HDMI.vue')['default']
LabCanvas: typeof import('./src/components/LabCanvasNew/LabCanvas.vue')['default']
LabCanvasNew: typeof import('./src/components/LabCanvas/LabCanvasNew.vue')['default']
LabComponentsDrawer: typeof import('./src/components/LabCanvasNew/LabComponentsDrawer.vue')['default']
LabComponentsDrawerNew: typeof import('./src/components/LabCanvas/LabComponentsDrawerNew.vue')['default']
LoginCard: typeof import('./src/components/LoginCard.vue')['default']
MarkdownRenderer: typeof import('./src/components/MarkdownRenderer.vue')['default']
MechanicalButton: typeof import('./src/components/equipments/MechanicalButton.vue')['default']
MotherBoard: typeof import('./src/components/equipments/MotherBoard.vue')['default']
MotherBoardCaps: typeof import('./src/components/equipments/MotherBoardCaps.vue')['default']
Navbar: typeof import('./src/components/Navbar.vue')['default']
PG2L100H_FBG676: typeof import('./src/components/equipments/PG2L100H_FBG676.vue')['default']
Pin: typeof import('./src/components/equipments/Pin.vue')['default']
PopButton: typeof import('./src/components/PopButton.vue')['default']
PropertyEditor: typeof import('./src/components/PropertyEditor.vue')['default']
PropertyPanel: typeof import('./src/components/PropertyPanel.vue')['default']
RekaSplitterGroup: typeof import('reka-ui')['SplitterGroup']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SD: typeof import('./src/components/equipments/SD.vue')['default']
SevenSegmentDisplay: typeof import('./src/components/equipments/SevenSegmentDisplay.vue')['default']
SFP: typeof import('./src/components/equipments/SFP.vue')['default']
Sidebar: typeof import('./src/components/Sidebar.vue')['default']
SMA: typeof import('./src/components/equipments/SMA.vue')['default']
SMT_LED: typeof import('./src/components/equipments/SMT_LED.vue')['default']
SplitterGroup: typeof import('reka-ui')['SplitterGroup']
SplitterPanel: typeof import('reka-ui')['SplitterPanel']
SplitterResizeHandle: typeof import('reka-ui')['SplitterResizeHandle']
Switch: typeof import('./src/components/equipments/Switch.vue')['default']
ThemeControlButton: typeof import('./src/components/ThemeControlButton.vue')['default']
ThemeControlToggle: typeof import('./src/components/ThemeControlToggle.vue')['default']
TutorialCarousel: typeof import('./src/components/TutorialCarousel.vue')['default']
UploadCard: typeof import('./src/components/UploadCard.vue')['default']
Wire: typeof import('./src/components/equipments/Wire.vue')['default']
}
}

10
flake.lock generated
View File

@@ -2,12 +2,12 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1741246872, "lastModified": 1748929857,
"narHash": "sha256-Q6pMP4a9ed636qilcYX8XUguvKl/0/LGXhHcRI91p0U=", "narHash": "sha256-lcZQ8RhsmhsK8u7LIFsJhsLh/pzR9yZ8yqpTzyGdj+Q=",
"rev": "10069ef4cf863633f57238f179a0297de84bd8d3", "rev": "c2a03962b8e24e669fb37b7df10e7c79531ff1a4",
"revCount": 763342, "revCount": 810143,
"type": "tarball", "type": "tarball",
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.763342%2Brev-10069ef4cf863633f57238f179a0297de84bd8d3/01956ed4-f66c-7a87-98e4-b7e58f4aa591/source.tar.gz" "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.810143%2Brev-c2a03962b8e24e669fb37b7df10e7c79531ff1a4/01973914-8b42-7168-9ee2-4d6ea6946695/source.tar.gz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",

722
package-lock.json generated
View File

@@ -10,15 +10,21 @@
"dependencies": { "dependencies": {
"@svgdotjs/svg.js": "^3.2.4", "@svgdotjs/svg.js": "^3.2.4",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@vueuse/core": "^13.5.0",
"async-mutex": "^0.5.0",
"highlight.js": "^11.11.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"log-symbols": "^7.0.0", "log-symbols": "^7.0.0",
"lucide-vue-next": "^0.525.0",
"marked": "^12.0.0",
"mathjs": "^14.4.0", "mathjs": "^14.4.0",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"tinypool": "^1.0.2", "reka-ui": "^2.3.1",
"ts-log": "^2.2.7", "ts-log": "^2.2.7",
"ts-results-es": "^5.0.1", "ts-results-es": "^5.0.1",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "4", "vue-router": "4",
"yocto-queue": "^1.2.1",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
@@ -35,6 +41,7 @@
"postcss": "^8.5.3", "postcss": "^8.5.3",
"tailwindcss": "^4.0.12", "tailwindcss": "^4.0.12",
"typescript": "~5.7.3", "typescript": "~5.7.3",
"unplugin-vue-components": "^28.8.0",
"vite": "^6.1.0", "vite": "^6.1.0",
"vite-plugin-vue-devtools": "^7.7.2", "vite-plugin-vue-devtools": "^7.7.2",
"vue-tsc": "^2.2.2" "vue-tsc": "^2.2.2"
@@ -956,6 +963,86 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@floating-ui/core": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz",
"integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz",
"integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.2",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@floating-ui/vue": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.7.tgz",
"integrity": "sha512-idmAtbAIigGXN2SI5gItiXYBYtNfDTP9yIiObxgu13dgtG7ARCHlNfnR29GxP4LI4o13oiwsJ8wVgghj1lNqcw==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.7.2",
"@floating-ui/utils": "^0.2.10",
"vue-demi": ">=0.13.0"
}
},
"node_modules/@floating-ui/vue/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@internationalized/date": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.8.2.tgz",
"integrity": "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@internationalized/number": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.3.tgz",
"integrity": "sha512-p+Zh1sb6EfrfVaS86jlHGQ9HA66fJhV9x5LiE5vCbZtXEHAuhcmUZUdZ4WrFpUBfNalr2OkAJI5AcKEQF+Lebw==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8", "version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
@@ -1348,6 +1435,15 @@
"url": "https://github.com/sponsors/Fuzzyma" "url": "https://github.com/sponsors/Fuzzyma"
} }
}, },
"node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@tailwindcss/node": { "node_modules/@tailwindcss/node": {
"version": "4.1.4", "version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.4.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.4.tgz",
@@ -1616,6 +1712,32 @@
"tailwindcss": "4.1.4" "tailwindcss": "4.1.4"
} }
}, },
"node_modules/@tanstack/virtual-core": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/vue-virtual": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.12.tgz",
"integrity": "sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"vue": "^2.7.0 || ^3.0.0"
}
},
"node_modules/@tsconfig/node22": { "node_modules/@tsconfig/node22": {
"version": "22.0.1", "version": "22.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.1.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.1.tgz",
@@ -1646,6 +1768,12 @@
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/web-bluetooth": {
"version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "5.2.3", "version": "5.2.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz",
@@ -1986,6 +2114,69 @@
} }
} }
}, },
"node_modules/@vueuse/core": {
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.5.0.tgz",
"integrity": "sha512-wV7z0eUpifKmvmN78UBZX8T7lMW53Nrk6JP5+6hbzrB9+cJ3jr//hUlhl9TZO/03bUkMK6gGkQpqOPWoabr72g==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "13.5.0",
"@vueuse/shared": "13.5.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@vueuse/core/node_modules/@vueuse/shared": {
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.5.0.tgz",
"integrity": "sha512-K7GrQIxJ/ANtucxIXbQlUHdB0TPA8c+q5i+zbrjxuhJCnJ9GtBg75sBSnvmLSxHKPg2Yo8w62PWksl9kwH0Q8g==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@vueuse/metadata": {
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.5.0.tgz",
"integrity": "sha512-euhItU3b0SqXxSy8u1XHxUCdQ8M++bsRs+TYhOLDU/OykS7KvJnyIFfep0XM5WjIFry9uAPlVSjmVHiqeshmkw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "12.8.2",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz",
"integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==",
"license": "MIT",
"dependencies": {
"vue": "^3.5.13"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/alien-signals": { "node_modules/alien-signals": {
"version": "1.0.13", "version": "1.0.13",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
@@ -2006,6 +2197,54 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/anymatch/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/aria-hidden": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/async-mutex": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
"integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.21", "version": "10.4.21",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
@@ -2051,6 +2290,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/birpc": { "node_modules/birpc": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz", "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz",
@@ -2061,15 +2313,28 @@
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
} }
}, },
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.4", "version": "4.24.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
@@ -2140,6 +2405,31 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/complex.js": { "node_modules/complex.js": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz", "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz",
@@ -2153,6 +2443,13 @@
"url": "https://github.com/sponsors/rawify" "url": "https://github.com/sponsors/rawify"
} }
}, },
"node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
"dev": true,
"license": "MIT"
},
"node_modules/convert-source-map": { "node_modules/convert-source-map": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -2237,9 +2534,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2303,6 +2600,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
@@ -2446,6 +2749,13 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1" "url": "https://github.com/sindresorhus/execa?sponsor=1"
} }
}, },
"node_modules/exsolve": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
"dev": true,
"license": "MIT"
},
"node_modules/fdir": { "node_modules/fdir": {
"version": "6.4.4", "version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
@@ -2477,6 +2787,19 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fraction.js": { "node_modules/fraction.js": {
"version": "4.3.7", "version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -2548,6 +2871,19 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/globals": { "node_modules/globals": {
"version": "11.12.0", "version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -2575,6 +2911,15 @@
"he": "bin/he" "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": { "node_modules/hookable": {
"version": "5.5.3", "version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
@@ -2591,6 +2936,19 @@
"node": ">=18.18.0" "node": ">=18.18.0"
} }
}, },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-docker": { "node_modules/is-docker": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
@@ -2607,6 +2965,29 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-inside-container": { "node_modules/is-inside-container": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
@@ -2626,6 +3007,16 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/is-plain-obj": { "node_modules/is-plain-obj": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
@@ -3020,6 +3411,24 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/local-pkg": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz",
"integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"mlly": "^1.7.4",
"pkg-types": "^2.0.1",
"quansync": "^0.2.8"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -3052,6 +3461,15 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/lucide-vue-next": {
"version": "0.525.0",
"resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.525.0.tgz",
"integrity": "sha512-Xf8+x8B2DrnGDV/rxylS+KBp2FIe6ljwDn2JsGTZZvXIfhmm/q+nv8RuGO1OyoMjOVkkz7CqtUqJfwtFPRbB2w==",
"license": "ISC",
"peerDependencies": {
"vue": ">=3.0.1"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.17", "version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
@@ -3061,6 +3479,18 @@
"@jridgewell/sourcemap-codec": "^1.5.0" "@jridgewell/sourcemap-codec": "^1.5.0"
} }
}, },
"node_modules/marked": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mathjs": { "node_modules/mathjs": {
"version": "14.4.0", "version": "14.4.0",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.4.0.tgz", "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.4.0.tgz",
@@ -3128,6 +3558,38 @@
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/mlly": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
"integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.14.0",
"pathe": "^2.0.1",
"pkg-types": "^1.3.0",
"ufo": "^1.5.4"
}
},
"node_modules/mlly/node_modules/confbox": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
"dev": true,
"license": "MIT"
},
"node_modules/mlly/node_modules/pkg-types": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"confbox": "^0.1.8",
"mlly": "^1.7.4",
"pathe": "^2.0.1"
}
},
"node_modules/mrmime": { "node_modules/mrmime": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -3177,6 +3639,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/normalize-range": { "node_modules/normalize-range": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
@@ -3267,6 +3739,12 @@
"npm": ">=3.10.8" "npm": ">=3.10.8"
} }
}, },
"node_modules/ohash": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"license": "MIT"
},
"node_modules/open": { "node_modules/open": {
"version": "10.1.1", "version": "10.1.1",
"resolved": "https://registry.npmjs.org/open/-/open-10.1.1.tgz", "resolved": "https://registry.npmjs.org/open/-/open-10.1.1.tgz",
@@ -3382,6 +3860,18 @@
} }
} }
}, },
"node_modules/pkg-types": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.1.tgz",
"integrity": "sha512-eY0QFb6eSwc9+0d/5D2lFFUq+A3n3QNGSy/X2Nvp+6MfzGw2u6EbA7S80actgjY1lkvvI0pqB+a4hioMh443Ew==",
"dev": true,
"license": "MIT",
"dependencies": {
"confbox": "^0.2.2",
"exsolve": "^1.0.7",
"pathe": "^2.0.3"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.3", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
@@ -3433,6 +3923,23 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/quansync": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
"integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/antfu"
},
{
"type": "individual",
"url": "https://github.com/sponsors/sxzz"
}
],
"license": "MIT"
},
"node_modules/read-package-json-fast": { "node_modules/read-package-json-fast": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz", "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz",
@@ -3447,6 +3954,77 @@
"node": "^18.17.0 || >=20.5.0" "node": "^18.17.0 || >=20.5.0"
} }
}, },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/readdirp/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/reka-ui": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.3.1.tgz",
"integrity": "sha512-2SjGeybd7jvD8EQUkzjgg7GdOQdf4cTwdVMq/lDNTMqneUFNnryGO43dg8WaM/jaG9QpSCZBvstfBFWlDdb2Zg==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.6.13",
"@floating-ui/vue": "^1.1.6",
"@internationalized/date": "^3.5.0",
"@internationalized/number": "^3.5.0",
"@tanstack/vue-virtual": "^3.12.0",
"@vueuse/core": "^12.5.0",
"@vueuse/shared": "^12.5.0",
"aria-hidden": "^1.2.4",
"defu": "^6.1.4",
"ohash": "^2.0.11"
},
"peerDependencies": {
"vue": ">= 3.2.0"
}
},
"node_modules/reka-ui/node_modules/@vueuse/core": {
"version": "12.8.2",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz",
"integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "12.8.2",
"@vueuse/shared": "12.8.2",
"vue": "^3.5.13"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/reka-ui/node_modules/@vueuse/metadata": {
"version": "12.8.2",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz",
"integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/rfdc": { "node_modules/rfdc": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
@@ -3653,9 +4231,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.13", "version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -3669,13 +4247,17 @@
"url": "https://github.com/sponsors/SuperchupuDev" "url": "https://github.com/sponsors/SuperchupuDev"
} }
}, },
"node_modules/tinypool": { "node_modules/to-regex-range": {
"version": "1.0.2", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": { "engines": {
"node": "^18.0.0 || >=20.0.0" "node": ">=8.0"
} }
}, },
"node_modules/totalist": { "node_modules/totalist": {
@@ -3700,6 +4282,12 @@
"integrity": "sha512-HjX/7HxQe2bXkbp8pHTjy4Ir9eHIDnDDsLDphhGqy6I9iZ/vD4QXWEIlrVRZsEX+kS2jIiiF/mnl0nKnPTiYFw==", "integrity": "sha512-HjX/7HxQe2bXkbp8pHTjy4Ir9eHIDnDDsLDphhGqy6I9iZ/vD4QXWEIlrVRZsEX+kS2jIiiF/mnl0nKnPTiYFw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typed-function": { "node_modules/typed-function": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz", "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz",
@@ -3723,6 +4311,13 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/ufo": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
"dev": true,
"license": "MIT"
},
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@@ -3753,6 +4348,74 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/unplugin": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.5.tgz",
"integrity": "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.14.1",
"picomatch": "^4.0.2",
"webpack-virtual-modules": "^0.6.2"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/unplugin-utils": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.2.4.tgz",
"integrity": "sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pathe": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=18.12.0"
},
"funding": {
"url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/unplugin-vue-components": {
"version": "28.8.0",
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-28.8.0.tgz",
"integrity": "sha512-2Q6ZongpoQzuXDK0ZsVzMoshH0MWZQ1pzVL538G7oIDKRTVzHjppBDS8aB99SADGHN3lpGU7frraCG6yWNoL5Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^3.6.0",
"debug": "^4.4.1",
"local-pkg": "^1.1.1",
"magic-string": "^0.30.17",
"mlly": "^1.7.4",
"tinyglobby": "^0.2.14",
"unplugin": "^2.3.5",
"unplugin-utils": "^0.2.4"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@babel/parser": "^7.15.8",
"@nuxt/kit": "^3.2.2 || ^4.0.0",
"vue": "2 || 3"
},
"peerDependenciesMeta": {
"@babel/parser": {
"optional": true
},
"@nuxt/kit": {
"optional": true
}
}
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
@@ -3785,18 +4448,18 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.3.2", "version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.3", "fdir": "^6.4.4",
"picomatch": "^4.0.2", "picomatch": "^4.0.2",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"rollup": "^4.34.9", "rollup": "^4.34.9",
"tinyglobby": "^0.2.12" "tinyglobby": "^0.2.13"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
@@ -4013,6 +4676,13 @@
"typescript": ">=5.0.0" "typescript": ">=5.0.0"
} }
}, },
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"dev": true,
"license": "MIT"
},
"node_modules/which": { "node_modules/which": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
@@ -4036,6 +4706,18 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/yocto-queue": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz",
"integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==",
"license": "MIT",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yoctocolors": { "node_modules/yoctocolors": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz",

View File

@@ -4,27 +4,33 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host",
"build": "run-p type-check \"build-only {@}\" --", "build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview", "preview": "vite preview",
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --build", "type-check": "vue-tsc --build",
"pregen-api": "cd server && dotnet run &", "pregen-api": "cd server && dotnet run --property:Configuration=Release &",
"gen-api": "npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts", "gen-api": "npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts",
"postgen-api": "pkill server" "postgen-api": "pkill server"
}, },
"dependencies": { "dependencies": {
"@svgdotjs/svg.js": "^3.2.4", "@svgdotjs/svg.js": "^3.2.4",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@vueuse/core": "^13.5.0",
"async-mutex": "^0.5.0",
"highlight.js": "^11.11.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"log-symbols": "^7.0.0", "log-symbols": "^7.0.0",
"lucide-vue-next": "^0.525.0",
"marked": "^12.0.0",
"mathjs": "^14.4.0", "mathjs": "^14.4.0",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"tinypool": "^1.0.2", "reka-ui": "^2.3.1",
"ts-log": "^2.2.7", "ts-log": "^2.2.7",
"ts-results-es": "^5.0.1", "ts-results-es": "^5.0.1",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "4", "vue-router": "4",
"yocto-queue": "^1.2.1",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
@@ -41,6 +47,7 @@
"postcss": "^8.5.3", "postcss": "^8.5.3",
"tailwindcss": "^4.0.12", "tailwindcss": "^4.0.12",
"typescript": "~5.7.3", "typescript": "~5.7.3",
"unplugin-vue-components": "^28.8.0",
"vite": "^6.1.0", "vite": "^6.1.0",
"vite-plugin-vue-devtools": "^7.7.2", "vite-plugin-vue-devtools": "^7.7.2",
"vue-tsc": "^2.2.2" "vue-tsc": "^2.2.2"

View File

@@ -0,0 +1,314 @@
{
"version": 1,
"author": "template",
"editor": "system",
"parts": [
{
"id": "board",
"type": "BaseBoard",
"x": 0,
"y": 0,
"attrs": {
"size": 1.2,
"width": 400,
"height": 400,
"roundCorner": 20
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": false,
"isOn": true,
"index": 0
},
{
"id": "key_0_0",
"type": "MechanicalButton",
"x": 50,
"y": 50,
"attrs": {
"size": 0.5,
"bindKey": "1",
"bindMatrixKey": "0",
"pins": []
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": true,
"isOn": false,
"index": 0
},
{
"id": "key_0_1",
"type": "MechanicalButton",
"x": 150,
"y": 50,
"attrs": {
"size": 0.5,
"bindKey": "2",
"bindMatrixKey": "1",
"pins": []
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": true,
"isOn": false,
"index": 1
},
{
"id": "key_0_2",
"type": "MechanicalButton",
"x": 250,
"y": 50,
"attrs": {
"size": 0.5,
"bindKey": "3",
"bindMatrixKey": "2",
"pins": []
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": true,
"isOn": false,
"index": 2
},
{
"id": "key_0_3",
"type": "MechanicalButton",
"x": 350,
"y": 50,
"attrs": {
"size": 0.5,
"bindKey": "A",
"bindMatrixKey": "3",
"pins": []
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": true,
"isOn": false,
"index": 3
},
{
"id": "key_1_0",
"type": "MechanicalButton",
"x": 50,
"y": 150,
"attrs": {
"size": 0.5,
"bindKey": "4",
"bindMatrixKey": "4",
"pins": []
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": true,
"isOn": false,
"index": 4
},
{
"id": "key_1_1",
"type": "MechanicalButton",
"x": 150,
"y": 150,
"attrs": {
"size": 0.5,
"bindKey": "5",
"bindMatrixKey": "5",
"pins": []
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": true,
"isOn": false,
"index": 5
},
{
"id": "key_1_2",
"type": "MechanicalButton",
"x": 250,
"y": 150,
"attrs": {
"size": 0.5,
"bindKey": "6",
"bindMatrixKey": "6",
"pins": []
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": true,
"isOn": false,
"index": 6
},
{
"id": "key_1_3",
"type": "MechanicalButton",
"x": 350,
"y": 150,
"attrs": {
"size": 0.5,
"bindKey": "B",
"bindMatrixKey": "7",
"pins": []
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": true,
"isOn": false,
"index": 7
},
{
"id": "key_2_0",
"type": "MechanicalButton",
"x": 50,
"y": 250,
"attrs": {
"size": 0.5,
"bindKey": "7",
"bindMatrixKey": "8",
"pins": []
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": true,
"isOn": false,
"index": 8
},
{
"id": "key_2_1",
"type": "MechanicalButton",
"x": 150,
"y": 250,
"attrs": {
"size": 0.5,
"bindKey": "8",
"bindMatrixKey": "9",
"pins": []
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": true,
"isOn": false,
"index": 9
},
{
"id": "key_2_2",
"type": "MechanicalButton",
"x": 250,
"y": 250,
"attrs": {
"size": 0.5,
"bindKey": "9",
"bindMatrixKey": "10",
"pins": []
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": true,
"isOn": false,
"index": 10
},
{
"id": "key_2_3",
"type": "MechanicalButton",
"x": 350,
"y": 250,
"attrs": {
"size": 0.5,
"bindKey": "C",
"bindMatrixKey": "11",
"pins": []
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": true,
"isOn": false,
"index": 11
},
{
"id": "key_3_0",
"type": "MechanicalButton",
"x": 50,
"y": 350,
"attrs": {
"size": 0.5,
"bindKey": "*",
"bindMatrixKey": "12",
"pins": []
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": true,
"isOn": false,
"index": 12
},
{
"id": "key_3_1",
"type": "MechanicalButton",
"x": 150,
"y": 350,
"attrs": {
"size": 0.5,
"bindKey": "0",
"bindMatrixKey": "13",
"pins": []
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": true,
"isOn": false,
"index": 13
},
{
"id": "key_3_2",
"type": "MechanicalButton",
"x": 250,
"y": 350,
"attrs": {
"size": 0.5,
"bindKey": "#",
"bindMatrixKey": "14",
"pins": []
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": true,
"isOn": false,
"index": 14
},
{
"id": "key_3_3",
"type": "MechanicalButton",
"x": 350,
"y": 350,
"attrs": {
"size": 0.5,
"bindKey": "D",
"bindMatrixKey": "15",
"pins": []
},
"rotate": 0,
"group": "MatrixKeypad",
"positionlock": false,
"hidepins": true,
"isOn": false,
"index": 15
}
],
"connections": []
}

File diff suppressed because it is too large Load Diff

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

175
public/doc/01/doc.md Normal file
View File

@@ -0,0 +1,175 @@
# 基础-1-流水灯
## 1.1 章节导读
流水灯实验作为基础实验的第一个实验是非常合适的本章我们利用试验箱中的LED进行点亮LED并实现流水灯的功能。
## 1.2 理论学习
相信大家之前肯定接触过单片机等设备而学习这些设备的第一个实验例程往往都是点亮一个LED。本次实验在点亮LED的基础上另LED灯依次闪亮循环不止实现“流水”的功能。其原理是依次控制连接到LED的IO口的电平高低让LED的闪亮间隔为0.5s,以实现流水灯的效果。
## 1.3 实战演练
### 1.3.1实验目标
依次点亮实验板中的8个LED灯两灯点亮间隔为0.5s每次点亮持续0.5s,实现流水灯效果。
### 1.3.2硬件资源
实验板上有0~31共32个LED灯的资源每4个LED灯为一组分别是绿黄四种颜色本次实验使用8个LED进行验证。
<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>
### 1.3.3程序设计
流水灯的设计与分频器计数器的逻辑相似只是多了LED灯的点亮部分。为了实现计数器肯定需要时钟信号sysclk也需要一个复位信号rstn同时为了驱动LED需要8个IO口。所以模块的端口如下表所示
| 端口名称 | 端口位宽 | 端口类型 |功能描述
|:----------:|:----:|:----:|:--------------------:|
| sysclk | 1Bit | Input | 输入时钟频率27M |
| rstn | 1Bit | Input | 复位信号,低电平有效 |
| led | 8Bit | Output | LED控制信号 |
为了使灯点亮0.5s我们应该设计一个计数器或者是分频器先将板载27M高频时钟降速。在27M时钟下计数0.5s需要计数器计数13_500_000个数也就是计数器从0开始计数到13_499_999。所以我们定义一个寄存器cnt每一次时钟上升沿cnt就加1当计数到13_499_999时led的状态改变同时cnt归零重新开始计数。
为了实现8个led流水的效果我们将0定义为led灭1表示亮初始状态led = 8b0000_0001当经过0.5s后也就是cnt等于13_499_999的时候第一个led灭第二个led亮起也就是led = 8b0000_0010。同理再过0.5sled = 8b0000_0100再过0.5sled = 8b0000_1000以此类推。
根据上面的规律我们很容易发现led的流水是靠1的移位来实现的也就是最基本的左移(<<)和右移(>>)运算符去实现。在这里我们需要向左移位并且每次只需要移动1位。模块的参考代码waterled_top.v如下所示
```verilog
module waterled_top(
input sysclk, //27MHz system clock
input rstn, //active low reset
output [7:0] led
);
parameter CNT_MAX = 32'd13_499_999;
reg [7:0] led_reg;
reg [31:0] cnt;
//cnt 当cnt == CNT_MAX时变为0计数0.5秒
always @(posedge sysclk) begin
if (!rstn)
cnt <= 0;
else if (cnt < CNT_MAX)
cnt <= cnt + 1;
else
cnt <= 0;
end
//led_reg 当cnt == CNT_MAX时左移一位。
always @(posedge sysclk) begin
if (!rstn)
led_reg <= 8'b0000_0001;
else if (led_reg == 8'b1000_0000 && cnt == CNT_MAX)//led7亮0.5s后重回led0
led_reg <= 8'b0000_0001;
else if (cnt == CNT_MAX) //0.5s后左移
led_reg <= led_reg << 1;
else
led_reg <= led_reg;
end
//led
assign led = led_reg;
endmodule
```
### 1.3.4仿真验证
为上述模块编写仿真模块参考代码waterled_top_tb.v如下
```verilog
`timescale 1ns/1ns
module waterled_top_tb;
reg sysclk;
reg rstn;
wire [7:0] led;
// 实例化待测试模块
waterled_top #(
.CNT_MAX(32'd100)//为了加快仿真速度将模块内部CNT_MAX由13_499_999变为1000
)uut (
.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
```
为了加速仿真我们在仿真文件中另CNT_MAX的值为100。同时为了便于仿真可以直接点击sim文件夹下hebav文件夹中的do.bat文件即可利用ModuleSim对模块进行仿真仿真波形如下
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/3.png"
alt="无法显示图片时显示的文字"
style="zoom:70%"/>
<br> <!--换行-->
图3.流水灯仿真波形(一) <!--标题-->
</center>
</div>
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/4.png"
alt="无法显示图片时显示的文字"
style="zoom:70%"/>
<br> <!--换行-->
图4.流水灯仿真波形(二) <!--标题-->
</center>
</div>
从图3我们可以看到端口信号led的值经过一定时间之后就进行了左移并且在图4中我们也可以发现当cnt的值等于CNT_MAX的时候led进行左移与我们设计的目标相符合可以进行下一步上板验证了。
### 1.3.5上板验证
仿真已经通过,可以进行上板验证,上板前要先进行管脚约束。端口与对应管脚如下表所示:
| 端口名称 |信号类型| 对应管脚|功能
|:----:|:----:|:----:|:----:|
| sysclk | Input | | 时钟 |
| rstn | Input | | 复位 |
| led[0] | Output | | LED |
| led[1] | Output | | LED |
| led[2] | Output | | LED |
| led[3] | Output | | LED |
| led[4] | Output | | LED |
| led[5] | Output | | LED |
| led[6] | Output | | LED |
| led[7] | Output | | LED |
管脚分配可以直接编写.fdc文件也可以使用PDS内置的工具进行分配。
完成管脚分配之后就可以生成sbit文件将文件提交到网站后点击烧录即可将sbit下载到实验板中在摄像头页面即可观察到流水灯的现象。
## 1.4 章末总结
本次实验主要学习使用左移(<<)和右移(>>)运算符实现移位,但实际应用中也可以使用位拼接({})进行更加复杂的移位操作,各位同学可以尝试学习。

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

258
public/doc/02/doc.md Normal file
View File

@@ -0,0 +1,258 @@
# 基础-2-按键检测与消抖
## 2.1 章节导读
在数字电路中,按键是最常用的人机交互输入方式。然而,机械式按键在按下或释放过程中会产生抖动信号,直接读取会引起误触发。本章我们将实现一个可靠的按键检测模块,完成信号的消抖和下降沿检测,以便为更复杂的模块如状态机切换、模式转换等提供稳定的触发信号。
## 2.2 理论学习
由于机械结构的限制按键在触发的一瞬间其接触点会发生数次抖动导致输出信号在0和1之间反复跳变。这种现象称为“抖动”。为避免系统错误响应需要对按键信号进行“消抖”处理。
常见的软件消抖方法包括定时器延时,而在软件中通常使用计数器。在本实验中,采用对输入信号进行采样判断,当其状态发生变化时开始计数,若持续稳定一定时长后,才认为按键真正改变。
在此基础上,若需检测按键的“按下事件”,则还需进一步提取其上升沿(或下降沿)作为一个单周期的“有效触发”信号。
## 2.3 实战演练
### 2.3.1 实验目标
实现一个具有消抖功能的按键检测模块,并进一步提取其下降沿触发信号,输出一个单时钟周期宽度的 `btn_flag` 信号用于后级逻辑判断。同时为了使实验现象更加明显设置8位的IO输出连接led当检测到 `btn_flag` 信号后8位信号`led`会自加1。
### 2.3.2 硬件资源
本实验使用试验箱上普通按键输入资源,输入信号经过电平转换后进入 FPGA 芯片,输出信号可连接状态指示灯以观察效果。
根据原理图可知实验板的按键按下是低电平,不按为高电平。
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/1.png"
alt="无法显示图片时显示的文字"
style="zoom:30%"/>
<br> <!--换行-->
图1.实验板的按键资源 <!--标题-->
</center>
</div>
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/2.png"
alt="实验板按键原理图"
style="zoom:60%"/>
<br> <!--换行-->
图2.实验板按键原理图 <!--标题-->
</center>
</div>
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/xx.png"
alt="数字孪生"
style="zoom:60%"/>
<br> <!--换行-->
图3.远程实验界面按键 <!--标题-->
</center>
</div>
### 2.3.3 程序设计
为了实现稳定的按键检测逻辑,设计流程如下:
1. 对输入 `btn` 进行采样,形成 `btn_temp`
2. 若检测到 `btn_temp` 与当前 `btn` 状态不一致,则开始计数;
3. 若计数器 `cnt` 达到设定阈值如255则认为按键状态稳定更新 `btn_ggle`
4. 实验板的按键按下是低电平,不按为高电平。所以对 `btn_ggle` 打两拍形成 `btn_flag_d0``btn_flag_d1`,再判断其下降沿,输出一个时钟周期的`btn_flag`
5. 检测到信号`btn_flag`后,信号`led <= led + 1`
该模块的参考代码如下(`btn_ggle.v`
```verilog
module btn_ggle(
input wire clk,
input wire rstn,
input wire btn,
output wire btn_flag,
output reg [7:0] led
);
reg btn_ggle;
reg btn_flag_d0,btn_flag_d1;
reg [7:0] cnt;
reg btn_temp;
//检测按键状态
always @(posedge clk) btn_temp <= btn;
//按键状态改变时开始计数
always @(posedge clk) begin
if(~rstn) cnt <= 0;
else if(btn_temp != btn) cnt <= 1;
else if(cnt != 0) cnt <= cnt + 1;
else cnt <= 0;
end
//计数到255时认为按键值稳定
always @(posedge clk) begin
if(~rstn) btn_ggle <= btn;
else if(cnt == 8'hFF) btn_ggle <= btn_temp;
else btn_ggle <= btn_ggle;
end
//对btn_ggle信号延迟打拍
always @(posedge clk) begin
if(~rstn) begin
btn_flag_d0 <= 0;
btn_flag_d1 <= 0;
end
else begin
btn_flag_d0 <= btn_ggle;
btn_flag_d1 <= btn_flag_d0;
end
end
//btn_flag检测btn_ggle的下降沿
assign btn_flag = ~btn_flag_d0 && btn_flag_d1;
//检测到按键按下的标志位btn_flagled会加1
always @(posedge clk) begin
if(~rstn) led <= 0;
else if(btn_flag) led <= led + 1;
else led <= led;
end
endmodule
```
### 2.3.4 仿真验证
为验证功能的正确性,设计测试平台(`btn_ggle_tb.v`),代码如下:
```verilog
`timescale 1ns/1ns
module btn_ggle_tb;
reg clk;
reg rstn;
reg btn;
wire btn_flag;
wire [7:0] led;
btn_ggle btn_ggle_inst (
.clk(clk),
.rstn(rstn),
.btn(btn),
.btn_flag(btn_flag),
.led(led)
);
// 27MHz 时钟周期约为 37.037ns取37ns近似
always #(500/27) clk = ~clk; // 半周期18.5ns ≈ 27MHz
initial begin
// 初始化
clk = 0;
rstn = 0;
btn = 1; // 按键默认未按下,高电平有效
// 释放复位
#200;
rstn = 1;
// 模拟带抖动的按下过程
#1000 btn = 0;
#100 btn = 1; // 抖动
#100 btn = 0;
#100 btn = 1;
#100 btn = 0;
// 稳定按下
#100000 btn = 0;
// 模拟抖动松开过程
#300000 btn = 1;
#100 btn = 0;
#100 btn = 1;
#100 btn = 0;
#100 btn = 1;
// 稳定松开
#100000 btn = 1;
// 第二次按下
#300000 btn = 0;
#100000 btn = 0;
#300000 $finish;
end
endmodule
```
利用ModuleSim进行仿真部分仿真波形如下图所示
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/3.png"
alt="仿真波形(一)"
style="zoom:60%"/>
<br> <!--换行-->
图4.仿真波形(一) <!--标题-->
</center>
</div>
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/4.png"
alt="仿真波形(二)"
style="zoom:60%"/>
<br> <!--换行-->
图5.仿真波形(二) <!--标题-->
</center>
</div>
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/5.png"
alt="仿真波形(三)"
style="zoom:60%"/>
<br> <!--换行-->
图6.仿真波形(三) <!--标题-->
</center>
</div>
从仿真波形二和三中我们可以看到当我们模拟按键按下1 ----> 0当按键抖动`btn`在0和1之间来回跳转`cnt`的值会变回1重新开始计数直到按键稳定按下`btn`的值稳定不变为0`cnt`稳定增加,当`cnt`的值增加到`8hFF`时,认为按键按下,`btn_ggle`存储此时的按键状态,同时`btn_flag`检测到下降沿,拉高一个时钟周期。`led`信号也加一。
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/6.png"
alt="仿真波形(四)"
style="zoom:60%"/>
<br> <!--换行-->
图7.仿真波形(四) <!--标题-->
</center>
</div>
<div> <!--块级封装-->
<center> <!--将图片和文字居中-->
<img src="./images/7.png"
alt="仿真波形(五)"
style="zoom:60%"/>
<br> <!--换行-->
图8.仿真波形(五) <!--标题-->
</center>
</div>
从波形三和四中我们可以看到当模拟按键抬起时0 ----> 1按键的抖动也会使`cnt`重新计数,直到稳定,`cnt`计数到`8hFF`时,更新`btn_ggle`,由于按键是抬起,`btn_flag`不变,`led`不变。
### 2.3.5 上板验证
完成仿真后,可进行上板验证。端口连接如下表所示:
| 端口名称 | 类型 | 管脚 |说明 |
| -------- | ------ | ------ | ---------- |
| clk | Input | | 27MHz 时钟 |
| rstn | Input | | 低电平复位 |
| btn | Input | | 外部按钮 |
| btn_flag | Output | | 上升沿标志 |
| led[0] | Output | | 驱动led |
| led[1] | Output | | 驱动led |
| led[2] | Output | | 驱动led |
| led[3] | Output | | 驱动led |
| led[4] | Output | | 驱动led |
| led[5] | Output | | 驱动led |
| led[6] | Output | | 驱动led |
| led[7] | Output | | 驱动led |
`.sbit`文件上传至平台并下载到实验板多次按下按键观察led灯跳转如果按下1次按键led只跳转一次那么说明达成实验目标。
## 2.4 章末总结
本实验通过一个典型的按键检测例子,介绍了数字系统中常用的消抖和边沿检测方法,掌握了如何利用计数器和触发器组合进行抖动抑制与事件捕捉。在更复杂的设计中,这类基础模块可作为控制逻辑的可靠触发信号源,具有广泛应用价值。

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

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

Binary file not shown.

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

1
server/.gitignore vendored
View File

@@ -1,4 +1,5 @@
obj obj
bin bin
bitstream bitstream
bsdl

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.FileProviders;
using Newtonsoft.Json; using Newtonsoft.Json;
using NLog; using NLog;
using NLog.Web; using NLog.Web;
@@ -9,7 +10,6 @@ var logger = NLog.LogManager.Setup()
.GetCurrentClassLogger(); .GetCurrentClassLogger();
logger.Debug("Init Main..."); logger.Debug("Init Main...");
try try
{ {
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -48,6 +48,13 @@ try
); );
}); });
} }
builder.Services.AddCors(options =>
{
options.AddPolicy("Users", policy => policy
.AllowAnyOrigin()
.AllowAnyHeader()
);
});
// Add Swagger // Add Swagger
builder.Services.AddControllers(); builder.Services.AddControllers();
@@ -78,34 +85,51 @@ try
// Application Settings // Application Settings
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
// app.UseExceptionHandler(new ExceptionHandlerOptions() if (!app.Environment.IsDevelopment())
// { {
// AllowStatusCode404Response = true, // app.UseExceptionHandler("/Home/Error");
// ExceptionHandlingPath = "/error" // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
// }); app.UseHsts();
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts(); // Serve static files
logger.Info($"Use Static Files : {Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")}");
app.UseDefaultFiles();
app.UseStaticFiles(); // Serves files from wwwroot by default
// Assets Files
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "assets")),
RequestPath = "/assets"
});
// Log Files
if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "log")))
{
Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), "log"));
}
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "log")),
RequestPath = "/log"
});
app.MapFallbackToFile("index.html");
}
// Add logs
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting(); app.UseRouting();
app.UseCors(); app.UseCors();
app.UseAuthorization(); app.UseAuthorization();
// if (app.Environment.IsDevelopment()) // Swagger
// {
app.UseOpenApi(); app.UseOpenApi();
app.UseSwaggerUi(); app.UseSwaggerUi();
// }
// Router
app.MapControllers();
// Setup Program // Setup Program
MsgBus.Init(); MsgBus.Init();
// Router app.Run();
// API Get
app.MapGet("/", () => Results.Redirect("/swagger"));
app.MapControllers();
app.Run("http://localhost:5000");
} }
catch (Exception exception) catch (Exception exception)
{ {

View File

@@ -5,18 +5,20 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"applicationUrl": "http://localhost:5188", "applicationUrl": "http://localhost:5000",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
} }
}, },
"https": { "https": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"applicationUrl": "https://localhost:7070;http://localhost:5188", "applicationUrl": "https://localhost:7278;http://localhost:5000",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="PublishAllRids.targets" /> <Import Project="PublishAllRids.xml" />
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<RuntimeIdentifiers>win-x64;linux-x64;</RuntimeIdentifiers> <RuntimeIdentifiers>win-x64;linux-x64;</RuntimeIdentifiers>
<PublishSingleFile>true</PublishSingleFile> <PublishSingleFile>true</PublishSingleFile>
</PropertyGroup> <SpaRoot>../</SpaRoot>
<SpaProxyServerUrl>http://localhost:5173</SpaProxyServerUrl>
<SpaProxyLaunchCommand>npm run dev</SpaProxyLaunchCommand>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DotNext" Version="5.19.1" /> <PackageReference Include="DotNext" Version="5.19.1" />
<PackageReference Include="DotNext.Threading" Version="5.19.1" /> <PackageReference Include="DotNext.Threading" Version="5.19.1" />
<PackageReference Include="Honoo.IO.Hashing.Crc" Version="1.3.3" /> <PackageReference Include="Honoo.IO.Hashing.Crc" Version="1.3.3" />
<PackageReference Include="linq2db.AspNet" Version="5.4.1" /> <PackageReference Include="linq2db.AspNet" Version="5.4.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.4" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="9.0.4" />
<PackageReference Include="Microsoft.OpenApi" Version="1.6.23" /> <PackageReference Include="Microsoft.OpenApi" Version="1.6.23" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.4.0" /> <PackageReference Include="NLog" Version="5.4.0" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.4.0" /> <PackageReference Include="NLog.Web.AspNetCore" Version="5.4.0" />
<PackageReference Include="NSwag.AspNetCore" Version="14.3.0" /> <PackageReference Include="NSwag.AspNetCore" Version="14.3.0" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" /> <PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
</ItemGroup> </ItemGroup>
</Project> </Project>

193
server/src/BsdlParser.cs Normal file
View File

@@ -0,0 +1,193 @@
using DotNext;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BsdlParser;
/// <summary>
/// [TODO:description]
/// </summary>
public class BoundaryScanRegs
{
/// <summary>
/// [TODO:description]
/// </summary>
public class CellEntry
{
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("cell_number")]
[JsonRequired]
public int CellNumber { get; set; }
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("cell_name")]
[JsonRequired]
public string CellName { get; set; } = "UnknownCellName";
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("port_id")]
public string? PortID { get; set; }
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("function")]
[JsonRequired]
public string? Function { get; set; }
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("safe_bit")]
[JsonRequired]
public string? SafeBit { get; set; }
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("ccell")]
public string? CCell { get; set; }
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("disabel_value")]
public string? DisableValue { get; set; }
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("disabel_result")]
public string? DisableResult { get; set; }
}
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("register_length")]
[JsonRequired]
public int RegisterLength { get; set; }
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("registers")]
[JsonRequired]
public CellEntry[] Registers { get; set; } = new CellEntry[] { };
}
/// <summary>
/// [TODO:description]
/// </summary>
public class Parser
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private const string BOUNDARY_REGS_DESP = "boundary_registers.json";
/// <summary>
/// [TODO:description]
/// </summary>
public JObject BoundaryRegsDesp { get; }
/// <summary>
/// [TODO:description]
/// </summary>
/// <returns>[TODO:return]</returns>
public Parser()
{
var filePath = Path.Combine(Environment.CurrentDirectory, BOUNDARY_REGS_DESP);
if (!Path.Exists(filePath))
throw new Exception($"Counld not find boundary_registers.json in {filePath}");
this.BoundaryRegsDesp = JObject.Parse(File.ReadAllText(filePath));
}
/// <summary>
/// [TODO:description]
/// </summary>
public Optional<int> GetBoundaryRegsNum()
{
var ret = this.BoundaryRegsDesp["register_length"];
if (ret is null) return new();
return Convert.ToInt32(ret);
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <returns>[TODO:return]</returns>
public Optional<List<BoundaryScanRegs.CellEntry>> GetBoundaryPorts()
{
var registers = this.BoundaryRegsDesp["registers"]?.ToList();
if (registers is null) return new();
var cellList = new List<BoundaryScanRegs.CellEntry>();
foreach (var item in registers)
{
var cell = item.ToObject<BoundaryScanRegs.CellEntry>();
if (cell is null) return new();
cellList.Add(cell);
}
return cellList;
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <returns>[TODO:return]</returns>
public Optional<List<BoundaryScanRegs.CellEntry>> GetBoundaryLogicalPorts()
{
var registers = this.BoundaryRegsDesp["registers"]?.ToList().Where((item) =>
{
return item["port_id"] is not null;
});
if (registers is null) return new();
var cellList = new List<BoundaryScanRegs.CellEntry>();
foreach (var item in registers)
{
var cell = item.ToObject<BoundaryScanRegs.CellEntry>();
if (cell is null) return new();
cellList.Add(cell);
}
return cellList;
}
// public Result<string> GetLogicalPorts()
// {
// using (Py.GIL())
// {
// using (PyModule scope = Py.CreateScope())
// {
// string code = $@"
// bsdl_parser = BsdlParser({this.filePath})
// result = json.dumps(bsdl_parser.GetLogicPortDesp(), indent=2)
// ";
//
// var localVariables = new PyDict();
// scope.Exec(code, localVariables);
// if (!localVariables.HasKey("result"))
// return new(new Exception($"PythonNet doesn't has result from dict: {localVariables}"));
//
// var result = localVariables.GetItem("result");
// if (result is null)
// return new(new Exception($"PythonNet get null from dict: {localVariables}"));
//
// var resultString = result.ToString();
// if (resultString is null)
// return new(new Exception($"Pythonnet convert PyObject to string failed :{result}"));
// return resultString;
// }
// }
// }
}

View File

@@ -1,3 +1,4 @@
using System.Collections;
using DotNext; using DotNext;
namespace Common namespace Common
@@ -41,6 +42,7 @@ namespace Common
0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef, 0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef,
0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff 0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff
}; };
/// <summary> /// <summary>
/// 整数转成二进制字节数组 /// 整数转成二进制字节数组
/// </summary> /// </summary>
@@ -166,6 +168,26 @@ namespace Common
} }
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="uintArray">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public static Result<byte[]> UInt32ArrayToBytes(UInt32[] uintArray)
{
byte[] byteArray = new byte[uintArray.Length * 4];
try
{
Buffer.BlockCopy(uintArray, 0, byteArray, 0, uintArray.Length * 4);
return byteArray;
}
catch (Exception error)
{
return new(error);
}
}
/// <summary> /// <summary>
/// 比特合并成二进制字节 /// 比特合并成二进制字节
@@ -252,6 +274,21 @@ namespace Common
return ((srcBits >> location) & ((UInt32)0b1)) == 1; return ((srcBits >> location) & ((UInt32)0b1)) == 1;
} }
/// <summary>
/// 将BitArray转化为32bits无符号整型
/// </summary>
/// <param name="bits">BitArray比特数组</param>
/// <returns>32bits无符号整型</returns>
public static Result<UInt32> BitsToNumber(BitArray bits)
{
if (bits.Length > 32)
throw new ArgumentException("Argument length shall be at most 32 bits.");
var array = new UInt32[1];
bits.CopyTo(array, 0);
return array[0];
}
/// <summary> /// <summary>
/// 字符串转二进制字节数组 /// 字符串转二进制字节数组

View File

@@ -1,683 +0,0 @@
using System.Net;
using Common;
using DotNext;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using WebProtocol;
namespace server.Controllers;
/// <summary>
/// UDP API
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class UDPController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private const string LOCALHOST = "127.0.0.1";
/// <summary>
/// 页面
/// </summary>
[HttpGet]
public string Index()
{
return "This is UDP Controller";
}
/// <summary>
/// 发送字符串
/// </summary>
/// <param name="address">IPV4 或者 IPV6 地址</param>
/// <param name="port">设备端口号</param>
/// <param name="text">发送的文本</param>
/// <response code="200">发送成功</response>
/// <response code="500">发送失败</response>
[HttpPost("SendString")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SendString(string address = LOCALHOST, int port = 1234, string text = "Hello Server!")
{
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
var ret = await UDPClientPool.SendStringAsync(endPoint, [text]);
if (ret) { return TypedResults.Ok(); }
else { return TypedResults.InternalServerError(); }
}
/// <summary>
/// 发送二进制数据
/// </summary>
/// <param name="address" example="127.0.0.1">IPV4 或者 IPV6 地址</param>
/// <param name="port" example="1234">设备端口号</param>
/// <param name="bytes" example="FFFFAAAA">16进制文本</param>
[HttpPost("SendBytes")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SendBytes(string address, int port, string bytes)
{
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
var ret = await UDPClientPool.SendBytesAsync(endPoint, Number.StringToBytes(bytes));
if (ret) { return TypedResults.Ok(); }
else { return TypedResults.InternalServerError(); }
}
/// <summary>
/// 发送地址包
/// </summary>
/// <param name="address">IP地址</param>
/// <param name="port">UDP 端口号</param>
/// <param name="opts">地址包选项</param>
[HttpPost("SendAddrPackage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SendAddrPackage(
string address,
int port,
[FromBody] SendAddrPackOptions opts)
{
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
var ret = await UDPClientPool.SendAddrPackAsync(endPoint, new WebProtocol.SendAddrPackage(opts));
if (ret) { return TypedResults.Ok(); }
else { return TypedResults.InternalServerError(); }
}
/// <summary>
/// 发送数据包
/// </summary>
/// <param name="address">IP地址</param>
/// <param name="port">UDP 端口号</param>
/// <param name="data">16进制数据</param>
[HttpPost("SendDataPackage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SendDataPackage(string address, int port, string data)
{
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
var ret = await UDPClientPool.SendDataPackAsync(endPoint,
new WebProtocol.SendDataPackage(Number.StringToBytes(data)));
if (ret) { return TypedResults.Ok(); }
else { return TypedResults.InternalServerError(); }
}
/// <summary>
/// 获取指定IP地址接受的数据列表
/// </summary>
/// <param name="address">IP地址</param>
[HttpGet("GetRecvDataArray")]
[ProducesResponseType(typeof(List<UDPData>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> GetRecvDataArray(string address)
{
var ret = await MsgBus.UDPServer.GetDataArrayAsync(address);
if (ret.HasValue)
{
var dataJson = JsonConvert.SerializeObject(ret.Value);
logger.Debug($"Get Receive Successfully: {dataJson}");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Debug("Get Receive Failed");
return TypedResults.InternalServerError();
}
}
}
/// <summary>
/// Jtag API
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class JtagController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private const string BITSTREAM_PATH = "bitstream/Jtag";
/// <summary>
/// 页面
/// </summary>
[HttpGet]
public string Index()
{
return "This is Jtag Controller";
}
/// <summary>
/// 执行一个Jtag命令
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="port"> 设备端口 </param>
/// <param name="hexDevAddr"> 16进制设备目的地址(Jtag) </param>
/// <param name="hexCmd"> 16进制命令 </param>
[HttpPost("RunCommand")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> RunCommand(string address, int port, string hexDevAddr, string hexCmd)
{
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.WriteFIFO(Convert.ToUInt32(hexDevAddr, 16), Convert.ToUInt32(hexCmd, 16));
if (ret.IsSuccessful) { return TypedResults.Ok(ret.Value); }
else { return TypedResults.InternalServerError(ret.Error); }
}
/// <summary>
/// 获取Jtag ID Code
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="port"> 设备端口 </param>
[HttpGet("GetDeviceIDCode")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> GetDeviceIDCode(string address, int port)
{
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.ReadIDCode();
if (ret.IsSuccessful)
{
logger.Info($"Get device {address} ID code: 0x{ret.Value:X4}");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
/// <summary>
/// 获取状态寄存器
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="port"> 设备端口 </param>
[HttpGet("ReadStatusReg")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> ReadStatusReg(string address, int port)
{
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.ReadStatusReg();
if (ret.IsSuccessful)
{
var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0'));
var decodeValue = new JtagClient.JtagStatusReg(ret.Value);
logger.Info($"Read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}");
return TypedResults.Ok(new
{
original = ret.Value,
binaryValue,
decodeValue,
});
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
/// <summary>
/// 上传比特流文件
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="file">比特流文件</param>
[HttpPost("UploadBitstream")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async ValueTask<IResult> UploadBitstream(string address, IFormFile file)
{
if (file == null || file.Length == 0)
return TypedResults.BadRequest("未选择文件");
// 生成安全的文件名(避免路径遍历攻击)
var fileName = Path.GetRandomFileName();
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
// 如果存在文件,则删除原文件再上传
if (Directory.Exists(uploadsFolder))
{
Directory.Delete(uploadsFolder, true);
}
Directory.CreateDirectory(uploadsFolder);
var filePath = Path.Combine(uploadsFolder, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
logger.Info($"Device {address} Upload Bitstream Successfully");
return TypedResults.Ok("Bitstream Upload Successfully");
}
/// <summary>
/// 通过Jtag下载比特流文件
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="port"> 设备端口 </param>
[HttpPost("DownloadBitstream")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> DownloadBitstream(string address, int port)
{
// 检查文件
var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
if (!Directory.Exists(fileDir))
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
try
{
// 读取文件
var filePath = Directory.GetFiles(fileDir)[0];
using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
{
if (fileStream is null || fileStream.Length <= 0)
return TypedResults.BadRequest("Wrong bitstream, Please upload it again");
// 定义缓冲区大小: 32KB
byte[] buffer = new byte[32 * 1024];
byte[] revBuffer = new byte[32 * 1024];
long totalBytesRead = 0;
// 使用异步流读取文件
using (var memoryStream = new MemoryStream())
{
int bytesRead;
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
// 反转 32bits
var retBuffer = Common.Number.ReverseBytes(buffer, 4);
if (!retBuffer.IsSuccessful)
return TypedResults.InternalServerError(retBuffer.Error);
revBuffer = retBuffer.Value;
await memoryStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesRead += bytesRead;
}
// 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存)
var fileBytes = memoryStream.ToArray();
// 下载比特流
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.DownloadBitstream(fileBytes);
if (ret.IsSuccessful)
{
logger.Info($"Device {address} dowload bitstream successfully");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
}
}
catch (Exception error)
{
return TypedResults.InternalServerError(error);
}
finally
{
}
}
}
/// <summary>
/// 远程更新
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class RemoteUpdater : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private const string BITSTREAM_PATH = "bitstream/RemoteUpdate";
/// <summary>
/// 上传远程更新比特流文件
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="goldenBitream">黄金比特流文件</param>
/// <param name="bitstream1">比特流文件1</param>
/// <param name="bitstream2">比特流文件2</param>
/// <param name="bitstream3">比特流文件3</param>
/// <returns>上传结果</returns>
[HttpPost("UploadBitstream")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async ValueTask<IResult> UploadBitstreams(
string address,
IFormFile? goldenBitream,
IFormFile? bitstream1,
IFormFile? bitstream2,
IFormFile? bitstream3)
{
if ((goldenBitream is null || goldenBitream.Length == 0) &&
(bitstream1 is null || bitstream1.Length == 0) &&
(bitstream2 is null || bitstream2.Length == 0) &&
(bitstream3 is null || bitstream3.Length == 0))
return TypedResults.BadRequest("未选择文件");
// 生成安全的文件名(避免路径遍历攻击)
var fileName = Path.GetRandomFileName();
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
// 如果存在文件,则删除原文件再上传
if (Directory.Exists(uploadsFolder))
{
Directory.Delete(uploadsFolder, true);
}
Directory.CreateDirectory(uploadsFolder);
for (int bitstreamNum = 0; bitstreamNum < 4; bitstreamNum++)
{
IFormFile file;
if (bitstreamNum == 0 && goldenBitream is not null)
file = goldenBitream;
else if (bitstreamNum == 1 && bitstream1 is not null)
file = bitstream1;
else if (bitstreamNum == 2 && bitstream2 is not null)
file = bitstream2;
else if (bitstreamNum == 3 && bitstream3 is not null)
file = bitstream3;
else continue;
var fileFolder = Path.Combine(uploadsFolder, bitstreamNum.ToString());
Directory.CreateDirectory(fileFolder);
var filePath = Path.Combine(fileFolder, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
}
logger.Info($"Device {address} Upload Bitstream Successfully");
return TypedResults.Ok("Bitstream Upload Successfully");
}
private async ValueTask<Result<byte[]>> ProcessBitstream(string filePath)
{
using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
{
if (fileStream is null || fileStream.Length <= 0)
return new(new ArgumentException("Wrong bitstream path"));
// 定义缓冲区大小: 32KB
byte[] buffer = new byte[32 * 1024];
byte[] revBuffer = new byte[32 * 1024];
long totalBytesRead = 0;
// 使用异步流读取文件
using (var memoryStream = new MemoryStream())
{
int bytesRead;
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
// 反转 32bits
var retBuffer = Common.Number.ReverseBytes(buffer, 4);
if (!retBuffer.IsSuccessful)
return new(retBuffer.Error);
revBuffer = retBuffer.Value;
await memoryStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesRead += bytesRead;
}
// 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存)
var restStreamLen = memoryStream.Length % (4 * 1024);
if (restStreamLen != 0)
{
var appendLen = ((int)(4 * 1024 - restStreamLen));
var bytesAppend = new byte[appendLen];
Array.Fill<byte>(bytesAppend, 0xFF);
await memoryStream.WriteAsync(bytesAppend, 0, appendLen);
}
return new(memoryStream.ToArray());
}
}
}
/// <summary>
/// 远程更新单个比特流文件
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="port"> 设备端口 </param>
/// <param name="bitstreamNum"> 比特流位号 </param>
[HttpPost("DownloadBitstream")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> UpdateBitstream(string address, int port, int bitstreamNum)
{
// 检查文件
var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}/{bitstreamNum}");
if (!Directory.Exists(fileDir))
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
try
{
// 读取文件
var filePath = Directory.GetFiles(fileDir)[0];
var fileBytes = await ProcessBitstream(filePath);
if (!fileBytes.IsSuccessful) return TypedResults.InternalServerError(fileBytes.Error);
// 下载比特流
var remoteUpdater = new RemoteUpdate.RemoteUpdateClient(address, port);
var ret = await remoteUpdater.UpdateBitstream(bitstreamNum, fileBytes.Value);
if (ret.IsSuccessful)
{
logger.Info($"Device {address} Update bitstream successfully");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
catch (Exception error)
{
return TypedResults.InternalServerError(error);
}
}
/// <summary>
/// 下载多个比特流文件
/// </summary>
/// <param name="address">设备地址</param>
/// <param name="port">设备端口</param>
/// <param name="bitstreamNum">比特流编号</param>
/// <returns>总共上传比特流的数量</returns>
[HttpPost("DownloadMultiBitstreams")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> DownloadMultiBitstreams(string address, int port, int? bitstreamNum)
{
// 检查文件
var bitstreamsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
if (!Directory.Exists(bitstreamsFolder))
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
try
{
var bitstreams = new List<byte[]?>() { null, null, null, null };
int cnt = 0; // 上传比特流数量
for (int i = 0; i < 4; i++)
{
var bitstreamDir = Path.Combine(bitstreamsFolder, i.ToString());
if (!Directory.Exists(bitstreamDir))
continue;
cnt++;
// 读取文件
var filePath = Directory.GetFiles(bitstreamDir)[0];
var fileBytes = await ProcessBitstream(filePath);
if (!fileBytes.IsSuccessful) return TypedResults.InternalServerError(fileBytes.Error);
bitstreams[i] = fileBytes.Value;
}
// 下载比特流
var remoteUpdater = new RemoteUpdate.RemoteUpdateClient(address, port);
{
var ret = await remoteUpdater.UploadBitstreams(bitstreams[0], bitstreams[1], bitstreams[2], bitstreams[3]);
if (!ret.IsSuccessful) return TypedResults.InternalServerError(ret.Error);
if (!ret.Value) return TypedResults.InternalServerError("Upload MultiBitstreams failed");
}
if (bitstreamNum is not null)
{
var ret = await remoteUpdater.HotResetBitstream(bitstreamNum ?? 0);
if (!ret.IsSuccessful) return TypedResults.InternalServerError(ret.Error);
if (!ret.Value) return TypedResults.InternalServerError("Hot reset failed");
}
return TypedResults.Ok(cnt);
}
catch (Exception error)
{
return TypedResults.InternalServerError(error);
}
}
/// <summary>
/// 热复位比特流文件
/// </summary>
/// <param name="address">设备地址</param>
/// <param name="port">设备端口</param>
/// <param name="bitstreamNum">比特流编号</param>
/// <returns>操作结果</returns>
[HttpPost("HotResetBitstream")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> HotResetBitstream(string address, int port, int bitstreamNum)
{
var remoteUpdater = new RemoteUpdate.RemoteUpdateClient(address, port);
var ret = await remoteUpdater.HotResetBitstream(bitstreamNum);
if (ret.IsSuccessful)
{
logger.Info($"Device {address}即可 Update bitstream successfully");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
}
/// <summary>
/// 数据控制器
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class Data : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// 创建数据库表
/// </summary>
/// <returns>插入的记录数</returns>
[EnableCors("Development")]
[HttpPost("CreateTable")]
public IResult CreateTables()
{
using var db = new Database.AppDataConnection();
db.CreateAllTables();
return TypedResults.Ok();
}
/// <summary>
/// 删除数据库表
/// </summary>
/// <returns>插入的记录数</returns>
[EnableCors("Development")]
[HttpDelete("DropTables")]
public IResult DropTables()
{
using var db = new Database.AppDataConnection();
db.DropAllTables();
return TypedResults.Ok();
}
/// <summary>
/// 获取所有用户
/// </summary>
/// <returns>用户列表</returns>
[HttpGet("AllUsers")]
public IResult AllUsers()
{
using var db = new Database.AppDataConnection();
var ret = db.User.ToList();
return TypedResults.Ok(ret);
}
/// <summary>
/// 注册新用户
/// </summary>
/// <param name="name">用户名</param>
/// <returns>操作结果</returns>
[HttpPost("SignUpUser")]
public IResult SignUpUser(string name)
{
if (name.Length > 255)
return TypedResults.BadRequest("Name Couln't over 255 characters");
using var db = new Database.AppDataConnection();
var ret = db.AddUser(name);
return TypedResults.Ok(ret);
}
}
/// <summary>
/// 日志控制器
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class Log : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// 日志文件路径
/// </summary>
private readonly string _logFilePath = Directory.GetFiles(Directory.GetCurrentDirectory())[0];
}

View File

@@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace server.Controllers;
/// <summary>
/// [TODO:description]
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class BsdlParserController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// [TODO:description]
/// </summary>
/// <returns>[TODO:return]</returns>
[EnableCors("Development")]
[HttpGet("GetBoundaryLogicalPorts")]
public IResult GetBoundaryLogicalPorts()
{
var parser = new BsdlParser.Parser();
var ret = parser.GetBoundaryLogicalPorts();
if (ret.IsNull) return TypedResults.InternalServerError("Get Null");
return TypedResults.Ok(ret.Value);
}
}

View File

@@ -0,0 +1,108 @@
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace server.Controllers;
/// <summary>
/// [TODO:description]
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class DDSController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="channelNum">[TODO:parameter]</param>
/// <param name="waveNum">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[HttpPost("SetWaveNum")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SetWaveNum(string address, int port, int channelNum, int waveNum)
{
var dds = new Peripherals.DDSClient.DDS(address, port);
var ret = await dds.SetWaveNum(channelNum, waveNum);
if (ret.IsSuccessful)
{
logger.Info($"Device {address} set output wave num successfully");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="channelNum">[TODO:parameter]</param>
/// <param name="waveNum">[TODO:parameter]</param>
/// <param name="step">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[HttpPost("SetFreq")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SetFreq(string address, int port, int channelNum, int waveNum, UInt32 step)
{
var dds = new Peripherals.DDSClient.DDS(address, port);
var ret = await dds.SetFreq(channelNum, waveNum, step);
if (ret.IsSuccessful)
{
logger.Info($"Device {address} set output freqency successfully");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="channelNum">[TODO:parameter]</param>
/// <param name="waveNum">[TODO:parameter]</param>
/// <param name="phase">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[HttpPost("SetPhase")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SetPhase(string address, int port, int channelNum, int waveNum, int phase)
{
var dds = new Peripherals.DDSClient.DDS(address, port);
var ret = await dds.SetPhase(channelNum, waveNum, phase);
if (ret.IsSuccessful)
{
logger.Info($"Device {address} set output phase successfully");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
}

View File

@@ -0,0 +1,69 @@
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace server.Controllers;
/// <summary>
/// 数据控制器
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class DataController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// 创建数据库表
/// </summary>
/// <returns>插入的记录数</returns>
[EnableCors("Development")]
[HttpPost("CreateTable")]
public IResult CreateTables()
{
using var db = new Database.AppDataConnection();
db.CreateAllTables();
return TypedResults.Ok();
}
/// <summary>
/// 删除数据库表
/// </summary>
/// <returns>插入的记录数</returns>
[EnableCors("Development")]
[HttpDelete("DropTables")]
public IResult DropTables()
{
using var db = new Database.AppDataConnection();
db.DropAllTables();
return TypedResults.Ok();
}
/// <summary>
/// 获取所有用户
/// </summary>
/// <returns>用户列表</returns>
[HttpGet("AllUsers")]
public IResult AllUsers()
{
using var db = new Database.AppDataConnection();
var ret = db.User.ToList();
return TypedResults.Ok(ret);
}
/// <summary>
/// 注册新用户
/// </summary>
/// <param name="name">用户名</param>
/// <returns>操作结果</returns>
[HttpPost("SignUpUser")]
public IResult SignUpUser(string name)
{
if (name.Length > 255)
return TypedResults.BadRequest("Name Couln't over 255 characters");
using var db = new Database.AppDataConnection();
var ret = db.AddUser(name);
return TypedResults.Ok(ret);
}
}

View File

@@ -0,0 +1,278 @@
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace server.Controllers;
/// <summary>
/// Jtag API
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class JtagController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private const string BITSTREAM_PATH = "bitstream/Jtag";
/// <summary>
/// 页面
/// </summary>
[HttpGet]
public string Index()
{
return "This is Jtag Controller";
}
/// <summary>
/// 获取Jtag ID Code
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="port"> 设备端口 </param>
[HttpGet("GetDeviceIDCode")]
[EnableCors("Users")]
[ProducesResponseType(typeof(uint), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> GetDeviceIDCode(string address, int port)
{
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.ReadIDCode();
if (ret.IsSuccessful)
{
logger.Info($"Get device {address} ID code: 0x{ret.Value:X4}");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
/// <summary>
/// 获取状态寄存器
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="port"> 设备端口 </param>
[HttpGet("ReadStatusReg")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> ReadStatusReg(string address, int port)
{
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.ReadStatusReg();
if (ret.IsSuccessful)
{
var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0'));
var decodeValue = new JtagClient.JtagStatusReg(ret.Value);
logger.Info($"Read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}");
return TypedResults.Ok(new
{
original = ret.Value,
binaryValue,
decodeValue,
});
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
/// <summary>
/// 上传比特流文件
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="file">比特流文件</param>
[HttpPost("UploadBitstream")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
public async ValueTask<IResult> UploadBitstream(string address, IFormFile file)
{
if (file == null || file.Length == 0)
return TypedResults.BadRequest("未选择文件");
// 生成安全的文件名(避免路径遍历攻击)
var fileName = Path.GetRandomFileName();
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
// 如果存在文件,则删除原文件再上传
if (Directory.Exists(uploadsFolder))
{
Directory.Delete(uploadsFolder, true);
}
Directory.CreateDirectory(uploadsFolder);
var filePath = Path.Combine(uploadsFolder, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
logger.Info($"Device {address} Upload Bitstream Successfully");
return TypedResults.Ok(true);
}
/// <summary>
/// 通过Jtag下载比特流文件
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="port"> 设备端口 </param>
[HttpPost("DownloadBitstream")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> DownloadBitstream(string address, int port)
{
// 检查文件
var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
if (!Directory.Exists(fileDir))
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
try
{
// 读取文件
var filePath = Directory.GetFiles(fileDir)[0];
using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
{
if (fileStream is null || fileStream.Length <= 0)
return TypedResults.BadRequest("Wrong bitstream, Please upload it again");
// 定义缓冲区大小: 32KB
byte[] buffer = new byte[32 * 1024];
byte[] revBuffer = new byte[32 * 1024];
long totalBytesRead = 0;
// 使用异步流读取文件
using (var memoryStream = new MemoryStream())
{
int bytesRead;
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
// 反转 32bits
var retBuffer = Common.Number.ReverseBytes(buffer, 4);
if (!retBuffer.IsSuccessful)
return TypedResults.InternalServerError(retBuffer.Error);
revBuffer = retBuffer.Value;
for (int i = 0; i < revBuffer.Length; i++)
{
revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]);
}
await memoryStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesRead += bytesRead;
}
// 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存)
var fileBytes = memoryStream.ToArray();
// 下载比特流
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.DownloadBitstream(fileBytes);
if (ret.IsSuccessful)
{
logger.Info($"Device {address} dowload bitstream successfully");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
}
}
catch (Exception error)
{
return TypedResults.InternalServerError(error);
}
finally
{
}
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[HttpPost("BoundaryScanAllPorts")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> BoundaryScanAllPorts(string address, int port)
{
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.BoundaryScan();
if (!ret.IsSuccessful)
{
if (ret.Error is ArgumentException)
return TypedResults.BadRequest(ret.Error);
else return TypedResults.InternalServerError(ret.Error);
}
return TypedResults.Ok(ret.Value);
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[HttpPost("BoundaryScanLogicalPorts")]
[EnableCors("Users")]
[ProducesResponseType(typeof(Dictionary<string, bool>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> BoundaryScanLogicalPorts(string address, int port)
{
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.BoundaryScanLogicalPorts();
if (!ret.IsSuccessful)
{
if (ret.Error is ArgumentException)
return TypedResults.BadRequest(ret.Error);
else return TypedResults.InternalServerError(ret.Error);
}
return TypedResults.Ok(ret.Value);
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="speed">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[HttpPost("SetSpeed")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SetSpeed(string address, int port, UInt32 speed)
{
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.SetSpeed(speed);
if (!ret.IsSuccessful)
{
if (ret.Error is ArgumentException)
return TypedResults.BadRequest(ret.Error);
else return TypedResults.InternalServerError(ret.Error);
}
return TypedResults.Ok(ret.Value);
}
}

View File

@@ -0,0 +1,101 @@
using System.Collections;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace server.Controllers;
/// <summary>
/// 矩阵键控制器,用于管理矩阵键的启用、禁用和状态设置
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class MatrixKeyController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// 启用矩阵键控制。
/// </summary>
/// <param name="address">设备的IP地址</param>
/// <param name="port">设备的端口号</param>
/// <returns>返回操作结果的状态码</returns>
[HttpPost("EnabelMatrixKey")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> EnabelMatrixKey(string address, int port)
{
var matrixKeyCtrl = new Peripherals.MatrixKeyClient.MatrixKey(address, port);
var ret = await matrixKeyCtrl.EnableControl();
if (ret.IsSuccessful)
{
logger.Info($"Enable device {address}:{port.ToString()} matrix key finished: {ret.Value}.");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
/// <summary>
/// 禁用矩阵键控制。
/// </summary>
/// <param name="address">设备的IP地址</param>
/// <param name="port">设备的端口号</param>
/// <returns>返回操作结果的状态码</returns>
[HttpPost("DisableMatrixKey")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> DisableMatrixKey(string address, int port)
{
var matrixKeyCtrl = new Peripherals.MatrixKeyClient.MatrixKey(address, port);
var ret = await matrixKeyCtrl.DisableControl();
if (ret.IsSuccessful)
{
logger.Info($"Disable device {address}:{port.ToString()} matrix key finished: {ret.Value}.");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
/// <summary>
/// 设置矩阵键的状态。
/// </summary>
/// <param name="address">设备的IP地址</param>
/// <param name="port">设备的端口号</param>
/// <param name="keyStates">矩阵键的状态数组长度应为16</param>
/// <returns>返回操作结果的状态码</returns>
[HttpPost("SetMatrixKeyStatus")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SetMatrixKeyStatus(string address, int port, [FromBody] bool[] keyStates)
{
if (keyStates.Length != 16)
return TypedResults.BadRequest($"The length of key states should be 16 instead of {keyStates.Length}");
var matrixKeyCtrl = new Peripherals.MatrixKeyClient.MatrixKey(address, port);
var ret = await matrixKeyCtrl.ControlKey(new BitArray(keyStates));
if (ret.IsSuccessful)
{
logger.Info($"Set device {address}:{port.ToString()} matrix key finished: {ret.Value}.");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
}

View File

@@ -0,0 +1,46 @@
using System.Collections;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace server.Controllers;
/// <summary>
/// 矩阵键控制器,用于管理矩阵键的启用、禁用和状态设置
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class PowerController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="enable">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[HttpPost("SetPowerOnOff")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SetPowerOnOff(string address, int port, bool enable)
{
var powerCtrl = new Peripherals.PowerClient.Power(address, port);
var ret = await powerCtrl.SetPowerOnOff(enable);
if (ret.IsSuccessful)
{
var powerStatus = enable ? "ON" : "OFF";
logger.Info($"Set device {address}:{port.ToString()} power {powerStatus} finished: {ret.Value}.");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
}

View File

@@ -0,0 +1,292 @@
using DotNext;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace server.Controllers;
/// <summary>
/// 远程更新
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class RemoteUpdateController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private const string BITSTREAM_PATH = "bitstream/RemoteUpdate";
/// <summary>
/// 上传远程更新比特流文件
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="goldenBitream">黄金比特流文件</param>
/// <param name="bitstream1">比特流文件1</param>
/// <param name="bitstream2">比特流文件2</param>
/// <param name="bitstream3">比特流文件3</param>
/// <returns>上传结果</returns>
[HttpPost("UploadBitstream")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
public async ValueTask<IResult> UploadBitstreams(
string address,
IFormFile? goldenBitream,
IFormFile? bitstream1,
IFormFile? bitstream2,
IFormFile? bitstream3)
{
if ((goldenBitream is null || goldenBitream.Length == 0) &&
(bitstream1 is null || bitstream1.Length == 0) &&
(bitstream2 is null || bitstream2.Length == 0) &&
(bitstream3 is null || bitstream3.Length == 0))
return TypedResults.BadRequest("未选择文件");
// 生成安全的文件名(避免路径遍历攻击)
var fileName = Path.GetRandomFileName();
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
// 如果存在文件,则删除原文件再上传
if (Directory.Exists(uploadsFolder))
{
Directory.Delete(uploadsFolder, true);
}
Directory.CreateDirectory(uploadsFolder);
for (int bitstreamNum = 0; bitstreamNum < 4; bitstreamNum++)
{
IFormFile file;
if (bitstreamNum == 0 && goldenBitream is not null)
file = goldenBitream;
else if (bitstreamNum == 1 && bitstream1 is not null)
file = bitstream1;
else if (bitstreamNum == 2 && bitstream2 is not null)
file = bitstream2;
else if (bitstreamNum == 3 && bitstream3 is not null)
file = bitstream3;
else continue;
var fileFolder = Path.Combine(uploadsFolder, bitstreamNum.ToString());
Directory.CreateDirectory(fileFolder);
var filePath = Path.Combine(fileFolder, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
}
logger.Info($"Device {address} Upload Bitstream Successfully");
return TypedResults.Ok(true);
}
private async ValueTask<Result<byte[]>> ProcessBitstream(string filePath)
{
using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
{
if (fileStream is null || fileStream.Length <= 0)
return new(new ArgumentException("Wrong bitstream path"));
// 定义缓冲区大小: 32KB
byte[] buffer = new byte[32 * 1024];
byte[] revBuffer = new byte[32 * 1024];
long totalBytesRead = 0;
// 使用异步流读取文件
using (var memoryStream = new MemoryStream())
{
int bytesRead;
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
// 反转 32bits
var retBuffer = Common.Number.ReverseBytes(buffer, 4);
if (!retBuffer.IsSuccessful)
return new(retBuffer.Error);
revBuffer = retBuffer.Value;
await memoryStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesRead += bytesRead;
}
// 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存)
var restStreamLen = memoryStream.Length % (4 * 1024);
if (restStreamLen != 0)
{
var appendLen = ((int)(4 * 1024 - restStreamLen));
var bytesAppend = new byte[appendLen];
Array.Fill<byte>(bytesAppend, 0xFF);
await memoryStream.WriteAsync(bytesAppend, 0, appendLen);
}
return new(memoryStream.ToArray());
}
}
}
/// <summary>
/// 远程更新单个比特流文件
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="port"> 设备端口 </param>
/// <param name="bitstreamNum"> 比特流位号 </param>
[HttpPost("DownloadBitstream")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> UpdateBitstream(string address, int port, int bitstreamNum)
{
// 检查文件
var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}/{bitstreamNum}");
if (!Directory.Exists(fileDir))
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
try
{
// 读取文件
var filePath = Directory.GetFiles(fileDir)[0];
var fileBytes = await ProcessBitstream(filePath);
if (!fileBytes.IsSuccessful) return TypedResults.InternalServerError(fileBytes.Error);
// 下载比特流
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
var ret = await remoteUpdater.UpdateBitstream(bitstreamNum, fileBytes.Value);
if (ret.IsSuccessful)
{
logger.Info($"Device {address} Update bitstream successfully");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
catch (Exception error)
{
return TypedResults.InternalServerError(error);
}
}
/// <summary>
/// 下载多个比特流文件
/// </summary>
/// <param name="address">设备地址</param>
/// <param name="port">设备端口</param>
/// <param name="bitstreamNum">比特流编号</param>
/// <returns>总共上传比特流的数量</returns>
[HttpPost("DownloadMultiBitstreams")]
[EnableCors("Users")]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> DownloadMultiBitstreams(string address, int port, int? bitstreamNum)
{
// 检查文件
var bitstreamsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
if (!Directory.Exists(bitstreamsFolder))
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
try
{
var bitstreams = new List<byte[]?>() { null, null, null, null };
int cnt = 0; // 上传比特流数量
for (int i = 0; i < 4; i++)
{
var bitstreamDir = Path.Combine(bitstreamsFolder, i.ToString());
if (!Directory.Exists(bitstreamDir))
continue;
cnt++;
// 读取文件
var filePath = Directory.GetFiles(bitstreamDir)[0];
var fileBytes = await ProcessBitstream(filePath);
if (!fileBytes.IsSuccessful) return TypedResults.InternalServerError(fileBytes.Error);
bitstreams[i] = fileBytes.Value;
}
// 下载比特流
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
{
var ret = await remoteUpdater.UploadBitstreams(bitstreams[0], bitstreams[1], bitstreams[2], bitstreams[3]);
if (!ret.IsSuccessful) return TypedResults.InternalServerError(ret.Error);
if (!ret.Value) return TypedResults.InternalServerError("Upload MultiBitstreams failed");
}
if (bitstreamNum is not null)
{
var ret = await remoteUpdater.HotResetBitstream(bitstreamNum ?? 0);
if (!ret.IsSuccessful) return TypedResults.InternalServerError(ret.Error);
if (!ret.Value) return TypedResults.InternalServerError("Hot reset failed");
}
return TypedResults.Ok(cnt);
}
catch (Exception error)
{
return TypedResults.InternalServerError(error);
}
}
/// <summary>
/// 热复位比特流文件
/// </summary>
/// <param name="address">设备地址</param>
/// <param name="port">设备端口</param>
/// <param name="bitstreamNum">比特流编号</param>
/// <returns>操作结果</returns>
[HttpPost("HotResetBitstream")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> HotResetBitstream(string address, int port, int bitstreamNum)
{
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
var ret = await remoteUpdater.HotResetBitstream(bitstreamNum);
if (ret.IsSuccessful)
{
logger.Info($"Device {address} Update bitstream successfully");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[HttpPost("GetFirmwareVersion")]
[EnableCors("Users")]
[ProducesResponseType(typeof(UInt32), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> GetFirmwareVersion(string address, int port)
{
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
var ret = await remoteUpdater.GetVersion();
if (ret.IsSuccessful)
{
logger.Info($"Device {address} get firmware version successfully");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
}

View File

@@ -0,0 +1,133 @@
using System.Net;
using Common;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using WebProtocol;
namespace server.Controllers;
/// <summary>
/// UDP API
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class UDPController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private const string LOCALHOST = "127.0.0.1";
/// <summary>
/// 页面
/// </summary>
[HttpGet]
public string Index()
{
return "This is UDP Controller";
}
/// <summary>
/// 发送字符串
/// </summary>
/// <param name="address">IPV4 或者 IPV6 地址</param>
/// <param name="port">设备端口号</param>
/// <param name="text">发送的文本</param>
/// <response code="200">发送成功</response>
/// <response code="500">发送失败</response>
[HttpPost("SendString")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SendString(string address = LOCALHOST, int port = 1234, string text = "Hello Server!")
{
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
var ret = await UDPClientPool.SendStringAsync(endPoint, [text]);
if (ret) { return TypedResults.Ok(); }
else { return TypedResults.InternalServerError(); }
}
/// <summary>
/// 发送二进制数据
/// </summary>
/// <param name="address" example="127.0.0.1">IPV4 或者 IPV6 地址</param>
/// <param name="port" example="1234">设备端口号</param>
/// <param name="bytes" example="FFFFAAAA">16进制文本</param>
[HttpPost("SendBytes")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SendBytes(string address, int port, string bytes)
{
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
var ret = await UDPClientPool.SendBytesAsync(endPoint, Number.StringToBytes(bytes));
if (ret) { return TypedResults.Ok(); }
else { return TypedResults.InternalServerError(); }
}
/// <summary>
/// 发送地址包
/// </summary>
/// <param name="address">IP地址</param>
/// <param name="port">UDP 端口号</param>
/// <param name="opts">地址包选项</param>
[HttpPost("SendAddrPackage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SendAddrPackage(
string address,
int port,
[FromBody] SendAddrPackOptions opts)
{
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
var ret = await UDPClientPool.SendAddrPackAsync(endPoint, new WebProtocol.SendAddrPackage(opts));
if (ret) { return TypedResults.Ok(); }
else { return TypedResults.InternalServerError(); }
}
/// <summary>
/// 发送数据包
/// </summary>
/// <param name="address">IP地址</param>
/// <param name="port">UDP 端口号</param>
/// <param name="data">16进制数据</param>
[HttpPost("SendDataPackage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SendDataPackage(string address, int port, string data)
{
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
var ret = await UDPClientPool.SendDataPackAsync(endPoint,
new WebProtocol.SendDataPackage(Number.StringToBytes(data)));
if (ret) { return TypedResults.Ok(); }
else { return TypedResults.InternalServerError(); }
}
/// <summary>
/// 获取指定IP地址接受的数据列表
/// </summary>
/// <param name="address">IP地址</param>
[HttpGet("GetRecvDataArray")]
[ProducesResponseType(typeof(List<UDPData>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> GetRecvDataArray(string address)
{
var ret = await MsgBus.UDPServer.GetDataArrayAsync(address);
if (ret.HasValue)
{
var dataJson = JsonConvert.SerializeObject(ret.Value);
logger.Debug($"Get Receive Successfully: {dataJson}");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Debug("Get Receive Failed");
return TypedResults.InternalServerError();
}
}
}

View File

@@ -1,4 +1,6 @@
using System.Collections;
using System.Net; using System.Net;
using BsdlParser;
using DotNext; using DotNext;
using Newtonsoft.Json; using Newtonsoft.Json;
using WebProtocol; using WebProtocol;
@@ -26,6 +28,10 @@ public static class JtagAddr
/// Jtag Write Command /// Jtag Write Command
/// </summary> /// </summary>
public const UInt32 WRITE_CMD = 0x10_00_00_03; public const UInt32 WRITE_CMD = 0x10_00_00_03;
/// <summary>
/// Jtag Speed Control
/// </summary>
public const UInt32 SPEED_CTRL = 0x10_00_00_04;
} }
/// <summary> /// <summary>
@@ -431,199 +437,44 @@ public class Jtag
return Convert.ToUInt32(Common.Number.BytesToUInt64(retPackOpts.Data).Value); return Convert.ToUInt32(Common.Number.BytesToUInt64(retPackOpts.Data).Value);
} }
/// <summary> async ValueTask<Result<bool>> WriteFIFO
/// 向指定的 JTAG 设备地址写入数据到 FIFO (UInt32 devAddr, UInt32 data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0)
/// </summary>
/// <param name="devAddr">目标设备地址</param>
/// <param name="data">要写入的数据</param>
/// <param name="delayMilliseconds">写入后的延迟时间(毫秒)</param>
/// <returns>包含接收数据包的异步结果</returns>
public async ValueTask<Result<RecvDataPackage>> WriteFIFO(UInt32 devAddr, UInt32 data, UInt32 delayMilliseconds = 0)
{ {
var ret = false;
var opts = new SendAddrPackOptions();
opts.BurstType = BurstType.FixedBurst;
opts.BurstLength = 0;
opts.CommandID = 0;
opts.Address = devAddr;
// Write Jtag State Register
opts.IsWrite = true;
ret = await UDPClientPool.SendAddrPackAsync(ep, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send 1st address package failed!"));
// Send Data Package
ret = await UDPClientPool.SendDataPackAsync(ep,
new SendDataPackage(Common.Number.NumberToBytes(data, 4).Value));
if (!ret) return new(new Exception("Send data package failed!"));
// Check Msg Bus
if (!MsgBus.IsRunning)
return new(new Exception("Message bus not working!"));
// Wait for Write Ack
var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(address, port);
if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error);
else if (!udpWriteAck.Value.IsSuccessful)
return new(new Exception("Send address package failed"));
// Delay some time before read register
await Task.Delay(TimeSpan.FromMilliseconds(delayMilliseconds));
// Read Jtag State Register
opts.IsWrite = false;
opts.Address = JtagAddr.STATE;
ret = await UDPClientPool.SendAddrPackAsync(ep, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send 2rd Address Package Failed!"));
// Wait for Read Data
var udpDataResp = await MsgBus.UDPServer.WaitForDataAsync(address, port);
if (!udpDataResp.IsSuccessful) return new(udpDataResp.Error);
else if (!udpDataResp.Value.IsSuccessful)
return new(new Exception("Send address package failed"));
return udpDataResp.Value;
}
async ValueTask<Result<RecvDataPackage>> WriteFIFO(UInt32 devAddr, byte[] dataArray, UInt32 delayMilliseconds = 0)
{
var ret = false;
var opts = new SendAddrPackOptions();
opts.BurstType = BurstType.FixedBurst;
opts.CommandID = 0;
opts.Address = devAddr;
// Check Msg Bus
if (!MsgBus.IsRunning)
return new(new Exception("Message bus not working!"));
var writeTimes = dataArray.Length / (256 * (32 / 8)) + 1;
for (var i = 0; i < writeTimes; i++)
{ {
// Sperate Data Array var ret = await UDPClientPool.WriteAddr(this.ep, devAddr, data, this.timeout);
var isLastData = i == writeTimes - 1; if (!ret.IsSuccessful) return new(ret.Error);
var sendDataArray = if (!ret.Value) return new(new Exception("Write FIFO failed"));
isLastData ?
dataArray[(i * (256 * (32 / 8)))..] :
dataArray[(i * (256 * (32 / 8)))..((i + 1) * (256 * (32 / 8)))];
// Write Jtag State Register
opts.IsWrite = true;
opts.BurstLength = ((byte)(sendDataArray.Length / 4 - 1));
ret = await UDPClientPool.SendAddrPackAsync(ep, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send 1st address package failed!"));
// Send Data Package
ret = await UDPClientPool.SendDataPackAsync(ep, new SendDataPackage(sendDataArray));
if (!ret) return new(new Exception("Send data package failed!"));
// Wait for Write Ack
var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(address, port);
if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error);
else if (!udpWriteAck.Value.IsSuccessful)
return new(new Exception("Send address package failed"));
} }
// Delay some time before read register // Delay some time before read register
await Task.Delay(TimeSpan.FromMilliseconds(delayMilliseconds)); await Task.Delay(TimeSpan.FromMilliseconds(delayMilliseconds));
// Read Jtag State Register {
opts.IsWrite = false; var ret = await UDPClientPool.ReadAddrWithWait(this.ep, JtagAddr.STATE, result, resultMask, this.timeout);
opts.BurstLength = 0; if (!ret.IsSuccessful) return new(ret.Error);
opts.Address = JtagAddr.STATE; return ret.Value;
ret = await UDPClientPool.SendAddrPackAsync(ep, new SendAddrPackage(opts)); }
if (!ret) return new(new Exception("Send 2rd Address Package Failed!"));
// Wait for Read Data
var udpDataResp = await MsgBus.UDPServer.WaitForDataAsync(address, port);
if (!udpDataResp.IsSuccessful) return new(udpDataResp.Error);
else if (!udpDataResp.Value.IsSuccessful)
return new(new Exception("Send address package failed"));
return udpDataResp.Value;
}
async ValueTask<Result<bool>> WriteFIFO
(UInt32 devAddr, UInt32 data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0)
{
var ret = false;
var retPack = await WriteFIFO(devAddr, data, delayMilliseconds);
if (!retPack.IsSuccessful) return new(retPack.Error);
if (retPack.Value.Options.Data is null)
return new(new Exception($"Data is Null, package: {retPack.Value.Options.ToString()}"));
var retPackLen = retPack.Value.Options.Data.Length;
if (retPackLen != 4)
return new(new Exception($"RecvDataPackage BodyData Length not Equal to 4: Total {retPackLen} bytes"));
if (Common.Number.BitsCheck(
Common.Number.BytesToUInt64(retPack.Value.Options.Data).Value, result, resultMask))
ret = true;
return ret;
} }
async ValueTask<Result<bool>> WriteFIFO async ValueTask<Result<bool>> WriteFIFO
(UInt32 devAddr, byte[] data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0) (UInt32 devAddr, byte[] data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0)
{
var ret = false;
var retPack = await WriteFIFO(devAddr, data, delayMilliseconds);
if (retPack.Value.Options.Data is null)
return new(new Exception($"Data is Null, package: {retPack.Value.Options.ToString()}"));
var retPackLen = retPack.Value.Options.Data.Length;
if (retPackLen != 4)
return new(new Exception($"RecvDataPackage BodyData Length not Equal to 4: Total {retPackLen} bytes"));
if (Common.Number.BitsCheck(
Common.Number.BytesToUInt64(retPack.Value.Options.Data).Value, result, resultMask))
ret = true;
return ret;
}
async ValueTask<Result<bool>> WaitForWriteFIFO
(UInt32 devAddr, byte[] data, UInt32 result,
UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 timeout = 10_000, UInt32 cycle = 500)
{ {
{ {
var wrRet = await WriteFIFO(devAddr, data, result, resultMask); var ret = await UDPClientPool.WriteAddr(this.ep, devAddr, data, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
if (!wrRet.IsSuccessful) return new(wrRet.Error); if (!ret.Value) return new(new Exception("Write FIFO failed"));
if (wrRet.Value) return true;
} }
// Wait some time // Delay some time before read register
var ret = false; await Task.Delay(TimeSpan.FromMilliseconds(delayMilliseconds));
var startTime = DateTime.Now;
var isTimeout = false;
var timeleft = TimeSpan.FromMilliseconds(timeout);
while (!isTimeout)
{ {
// Check whether timeout var ret = await UDPClientPool.ReadAddrWithWait(this.ep, JtagAddr.STATE, result, resultMask, this.timeout);
var elapsed = DateTime.Now - startTime; if (!ret.IsSuccessful) return new(ret.Error);
isTimeout = elapsed >= TimeSpan.FromMilliseconds(timeout); return ret.Value;
if (isTimeout) break;
timeleft = TimeSpan.FromMilliseconds(timeout) - elapsed;
// Check FIFO
var retPack = await ReadFIFO(JtagAddr.STATE);
if (Common.Number.BitsCheck(retPack.Value, result, resultMask))
{
ret = true;
break;
}
// Wait
await Task.Delay(TimeSpan.FromMilliseconds(cycle));
} }
return ret;
} }
/// <summary> /// <summary>
/// 清除所有 JTAG 寄存器 /// 清除所有 JTAG 寄存器
/// </summary> /// </summary>
@@ -717,26 +568,25 @@ public class Jtag
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Write CMD_JTAG_LOAD_DR_CAREI Failed")); else if (!ret.Value) return new(new Exception("Write CMD_JTAG_LOAD_DR_CAREI Failed"));
} }
{ {
var ret = await WaitForWriteFIFO( var ret = await WriteFIFO(
JtagAddr.WRITE_DATA, JtagAddr.WRITE_DATA,
bytesArray, 0x01_00_00_00, bytesArray, 0x01_00_00_00,
JtagState.CMD_EXEC_FINISH, JtagState.CMD_EXEC_FINISH);
timeout, cycle);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Write Data Failed")); return ret.Value;
} }
return true;
} }
async ValueTask<Result<UInt32>> LoadDRCareOutput(UInt32 bytesLen) async ValueTask<Result<UInt32>> LoadDRCareOutput(UInt32 UInt32Num)
{ {
if (bytesLen > Math.Pow(2, 28)) return new(new Exception("Length is over 2^(28 - 3)")); if (UInt32Num > Math.Pow(2, 23)) return new(new Exception("Length is over 2^(28 - 5)"));
var ret = await WriteFIFO( var ret = await WriteFIFO(
JtagAddr.WRITE_CMD, JtagAddr.WRITE_CMD,
Common.Number.MultiBitsToNumber(JtagCmd.CMD_JTAG_LOAD_DR_CAREO, JtagCmd.LEN_CMD_JTAG, 8 * bytesLen, 28).Value, Common.Number.MultiBitsToNumber(JtagCmd.CMD_JTAG_LOAD_DR_CAREO, JtagCmd.LEN_CMD_JTAG, 32 * UInt32Num, 28).Value,
0x01_00_00_00, JtagState.CMD_EXEC_FINISH); 0x01_00_00_00, JtagState.CMD_EXEC_FINISH);
if (ret.Value) if (ret.Value)
@@ -745,6 +595,31 @@ public class Jtag
return new(new Exception("LoadDRCareo Failed!")); return new(new Exception("LoadDRCareo Failed!"));
} }
async ValueTask<Result<UInt32[]>> LoadDRCareOutputArray(UInt32 UInt32Num)
{
if (UInt32Num > Math.Pow(2, 23)) return new(new Exception("Length is over 2^(28 - 5)"));
var ret = await WriteFIFO(
JtagAddr.WRITE_CMD,
Common.Number.MultiBitsToNumber(JtagCmd.CMD_JTAG_LOAD_DR_CAREO, JtagCmd.LEN_CMD_JTAG, 32 * UInt32Num, 28).Value,
JtagState.CMD_EXEC_FINISH, JtagState.CMD_EXEC_FINISH);
if (ret.Value)
{
var array = new UInt32[UInt32Num];
for (int i = 0; i < UInt32Num; i++)
{
var retData = await ReadFIFO(JtagAddr.READ_DATA);
if (!retData.IsSuccessful)
return new(new Exception("Read FIFO failed when Load DR"));
array[i] = retData.Value;
}
return array;
}
else
return new(new Exception("LoadDRCareo Failed!"));
}
/// <summary> /// <summary>
/// 读取 JTAG 设备的 ID 代码 /// 读取 JTAG 设备的 ID 代码
/// </summary> /// </summary>
@@ -774,7 +649,7 @@ public class Jtag
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Clear Write Registers Failed")); else if (!ret.Value) return new(new Exception("Jtag Clear Write Registers Failed"));
var retData = await LoadDRCareOutput(4); var retData = await LoadDRCareOutput(1);
if (!retData.IsSuccessful) if (!retData.IsSuccessful)
{ {
return new(new Exception("Get ID Code Failed")); return new(new Exception("Get ID Code Failed"));
@@ -812,11 +687,9 @@ public class Jtag
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Clear Write Registers Failed")); else if (!ret.Value) return new(new Exception("Jtag Clear Write Registers Failed"));
var retData = await LoadDRCareOutput(4); var retData = await LoadDRCareOutput(1);
if (!retData.IsSuccessful) if (!retData.IsSuccessful)
{
return new(new Exception("Read Status Reg Failed")); return new(new Exception("Read Status Reg Failed"));
}
return retData.Value; return retData.Value;
} }
@@ -901,4 +774,94 @@ public class Jtag
return true; return true;
} }
/// <summary>
/// 边界扫描
/// </summary>
/// <returns>返回所有引脚边界扫描结果</returns>
public async ValueTask<Result<BitArray>> BoundaryScan()
{
var paser = new BsdlParser.Parser();
var portNum = paser.GetBoundaryRegsNum().Value;
logger.Debug($"Get boundar scan registers number: {portNum}");
// Clear Data
await MsgBus.UDPServer.ClearUDPData(this.address);
logger.Trace($"Clear up udp server {this.address} receive data");
Result<bool> ret;
ret = await CloseTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
ret = await RunTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
logger.Trace("Jtag initialize");
ret = await ExecRDCmd(JtagCmd.JTAG_DR_SAMPLE);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_SAMPLE Failed"));
var retData = await LoadDRCareOutputArray(((uint)(portNum % 32 == 0 ? portNum / 32 : portNum / 32 + 1)));
if (!retData.IsSuccessful) return new(retData.Error);
ret = await CloseTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
var byteArray = Common.Number.UInt32ArrayToBytes(retData.Value);
if (!byteArray.IsSuccessful) return new(byteArray.Error);
var bitArray = new BitArray(byteArray.Value);
if (bitArray is null)
return new(new Exception($"Convert to BitArray failed"));
bitArray.Length = portNum;
return bitArray;
}
/// <summary>
/// 执行边界扫描并返回逻辑端口的状态
/// </summary>
/// <returns>包含逻辑端口状态的字典键为端口ID值为布尔状态</returns>
public async ValueTask<Result<Dictionary<string, bool>>> BoundaryScanLogicalPorts()
{
var bitArray = await BoundaryScan();
if (!bitArray.IsSuccessful) return new(bitArray.Error);
var paser = new BsdlParser.Parser();
var cellList = paser.GetBoundaryLogicalPorts();
if (cellList.IsNull) return new(new Exception("Get boundary logical ports failed"));
var portStatus = new Dictionary<string, bool>();
foreach (var cell in cellList.Value)
{
portStatus.Add(cell.PortID ?? "UnknownPortID", bitArray.Value[cell.CellNumber]);
}
return portStatus;
}
/// <summary>
/// 设置JTAG的运行速度。
/// </summary>
/// <param name="speed">运行速度值。</param>
/// <returns>指示操作是否成功的异步结果。</returns>
public async ValueTask<Result<bool>> SetSpeed(UInt32 speed)
{
// Clear Data
await MsgBus.UDPServer.ClearUDPData(this.address);
logger.Trace($"Clear up udp server {this.address} receive data");
var ret = await WriteFIFO(
JtagAddr.SPEED_CTRL, (speed << 16) | speed,
JtagState.CMD_EXEC_FINISH, JtagState.CMD_EXEC_FINISH);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
}
} }

View File

@@ -0,0 +1,170 @@
using System.Net;
using DotNext;
namespace Peripherals.DDSClient;
static class DDSAddr
{
/*地址定义以2路输出4波形存储为例
00 R/W CHANNEL0 wave_sel
01 R/W CHANNEL0 STORE0 freq_ctrl
02 R/W CHANNEL0 STORE1 freq_ctrl
03 R/W CHANNEL0 STORE2 freq_ctrl
04 R/W CHANNEL0 STORE3 freq_ctrl
05 R/W CHANNEL0 STORE0 phase_ctrl
06 R/W CHANNEL0 STORE1 phase_ctrl
07 R/W CHANNEL0 STORE2 phase_ctrl
08 R/W CHANNEL0 STORE3 phase_ctrl
09 R/W CHANNEL0 dds_wr_enable
0A WO CHANNEL0 data
10 R/W CHANNEL1 wave_sel
11 R/W CHANNEL1 STORE0 freq_ctrl
12 R/W CHANNEL1 STORE1 freq_ctrl
13 R/W CHANNEL1 STORE2 freq_ctrl
14 R/W CHANNEL1 STORE3 freq_ctrl
15 R/W CHANNEL1 STORE0 phase_ctrl
16 R/W CHANNEL1 STORE1 phase_ctrl
17 R/W CHANNEL1 STORE2 phase_ctrl
18 R/W CHANNEL1 STORE3 phase_ctrl
19 R/W CHANNEL1 dds_wr_enable
1A WO CHANNEL1 data
*/
public const UInt32 Base = 0x4000_0000;
public static readonly ChannelAddr[] Channel = { new ChannelAddr(0x00), new ChannelAddr(0x10) };
public class ChannelAddr
{
private readonly UInt32 Offset;
public readonly UInt32 WaveSelect;
public readonly UInt32[] FreqCtrl;
public readonly UInt32[] PhaseCtrl;
public readonly UInt32 WriteEnable;
public readonly UInt32 WriteData;
public ChannelAddr(UInt32 offset)
{
this.Offset = offset;
this.WaveSelect = Base + Offset + 0x00;
this.FreqCtrl = new UInt32[]
{
Base + offset + 0x01,
Base + offset + 0x02,
Base + offset + 0x03,
Base + offset + 0x04
};
this.PhaseCtrl = new UInt32[] {
Base + Offset + 0x05,
Base + Offset + 0x06,
Base + Offset + 0x07,
Base + Offset + 0x08
};
this.WriteEnable = Base + Offset + 0x09;
this.WriteData = Base + Offset + 0x0A;
}
}
}
/// <summary>
/// [TODO:description]
/// </summary>
public class DDS
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 2000;
readonly int port;
readonly string address;
private IPEndPoint ep;
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="timeout">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public DDS(string address, int port, int timeout = 2000)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.timeout = timeout;
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="channelNum">[TODO:parameter]</param>
/// <param name="waveNum">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public async ValueTask<Result<bool>> SetWaveNum(int channelNum, int waveNum)
{
if (channelNum < 0 || channelNum > 1) return new(new ArgumentException(
$"Channel number should be 0 ~ 1 instead of {channelNum}", nameof(channelNum)));
if (waveNum < 0 || waveNum > 3) return new(new ArgumentException(
$"Wave number should be 0 ~ 3 instead of {waveNum}", nameof(waveNum)));
await MsgBus.UDPServer.ClearUDPData(this.address);
logger.Trace("Clear udp data finished");
var ret = await UDPClientPool.WriteAddr(
this.ep, DDSAddr.Channel[channelNum].WaveSelect, (UInt32)waveNum, this.timeout);
if (!ret.IsSuccessful)
return new(ret.Error);
return ret.Value;
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="channelNum">[TODO:parameter]</param>
/// <param name="waveNum">[TODO:parameter]</param>
/// <param name="step">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public async ValueTask<Result<bool>> SetFreq(int channelNum, int waveNum, UInt32 step)
{
if (channelNum < 0 || channelNum > 1) return new(new ArgumentException(
$"Channel number should be 0 ~ 1 instead of {channelNum}", nameof(channelNum)));
if (waveNum < 0 || waveNum > 3) return new(new ArgumentException(
$"Wave number should be 0 ~ 3 instead of {waveNum}", nameof(waveNum)));
await MsgBus.UDPServer.ClearUDPData(this.address);
logger.Trace("Clear udp data finished");
var ret = await UDPClientPool.WriteAddr(
this.ep, DDSAddr.Channel[channelNum].FreqCtrl[waveNum], step, this.timeout);
if (!ret.IsSuccessful)
return new(ret.Error);
return ret.Value;
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="channelNum">[TODO:parameter]</param>
/// <param name="waveNum">[TODO:parameter]</param>
/// <param name="phase">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public async ValueTask<Result<bool>> SetPhase(int channelNum, int waveNum, int phase)
{
if (channelNum < 0 || channelNum > 1) return new(new ArgumentException(
$"Channel number should be 0 ~ 1 instead of {channelNum}", nameof(channelNum)));
if (waveNum < 0 || waveNum > 3) return new(new ArgumentException(
$"Wave number should be 0 ~ 3 instead of {waveNum}", nameof(waveNum)));
if (phase < 0 || phase > 4096) return new(new ArgumentException(
$"Phase should be 0 ~ 4096 instead of {phase}", nameof(phase)));
await MsgBus.UDPServer.ClearUDPData(this.address);
logger.Trace("Clear udp data finished");
var ret = await UDPClientPool.WriteAddr(
this.ep, DDSAddr.Channel[channelNum].PhaseCtrl[waveNum], (UInt32)phase, this.timeout);
if (!ret.IsSuccessful)
return new(ret.Error);
return ret.Value;
}
}

View File

@@ -0,0 +1,89 @@
using System.Collections;
using System.Net;
using DotNext;
namespace Peripherals.MatrixKeyClient;
class MatrixKeyAddr
{
public const UInt32 BASE = 0x10_00_00_00;
public const UInt32 KEY_ENABLE = BASE + 5;
public const UInt32 KEY_CTRL = BASE + 6;
}
/// <summary>
/// 矩阵键盘外设类,用于控制和管理矩阵键盘的功能。
/// </summary>
public class MatrixKey
{
readonly int timeout;
readonly int port;
readonly string address;
private IPEndPoint ep;
/// <summary>
/// 构造函数,用于初始化矩阵键盘外设实例。
/// </summary>
/// <param name="address">设备的IP地址</param>
/// <param name="port">设备的端口号</param>
/// <param name="timeout">操作的超时时间毫秒默认为1000</param>
/// <returns>无返回值。</returns>
public MatrixKey(string address, int port, int timeout = 1000)
{
this.address = address;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.timeout = timeout;
}
/// <summary>
/// 启用矩阵键盘的控制功能。
/// </summary>
/// <returns>返回一个包含操作结果的异步任务</returns>
public async ValueTask<Result<bool>> EnableControl()
{
if (MsgBus.IsRunning)
await MsgBus.UDPServer.ClearUDPData(this.address);
else return new(new Exception("Message Bus not work!"));
var ret = await UDPClientPool.WriteAddr(this.ep, MatrixKeyAddr.KEY_ENABLE, 1, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
}
/// <summary>
/// 禁用矩阵键盘的控制功能。
/// </summary>
/// <returns>返回一个包含操作结果的异步任务</returns>
public async ValueTask<Result<bool>> DisableControl()
{
if (MsgBus.IsRunning)
await MsgBus.UDPServer.ClearUDPData(this.address);
else return new(new Exception("Message Bus not work!"));
var ret = await UDPClientPool.WriteAddr(this.ep, MatrixKeyAddr.KEY_ENABLE, 0, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
}
/// <summary>
/// 控制矩阵键盘的按键状态。
/// </summary>
/// <param name="keyStates">表示按键状态的位数组长度必须为16</param>
/// <returns>返回一个包含操作结果的异步任务</returns>
public async ValueTask<Result<bool>> ControlKey(BitArray keyStates)
{
if (MsgBus.IsRunning)
await MsgBus.UDPServer.ClearUDPData(this.address);
else return new(new Exception("Message Bus not work!"));
if (keyStates.Length != 16) return new(new ArgumentException(
$"The number of key should be 16 instead of {keyStates.Length}", nameof(keyStates)));
var ret = await UDPClientPool.WriteAddr(
this.ep, MatrixKeyAddr.KEY_CTRL, Common.Number.BitsToNumber(keyStates).Value, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
}
}

View File

@@ -0,0 +1,56 @@
using System.Net;
using DotNext;
namespace Peripherals.PowerClient;
class PowerAddr
{
public const UInt32 Base = 0x10_00_00_00;
public const UInt32 PowerCtrl = Base + 7;
}
/// <summary>
/// [TODO:description]
/// </summary>
public class Power
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout;
readonly int port;
readonly string address;
private IPEndPoint ep;
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="timeout">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public Power(string address, int port, int timeout = 1000)
{
this.address = address;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.timeout = timeout;
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="enable">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public async ValueTask<Result<bool>> SetPowerOnOff(bool enable)
{
if (MsgBus.IsRunning)
await MsgBus.UDPServer.ClearUDPData(this.address);
else return new(new Exception("Message Bus not work!"));
var ret = await UDPClientPool.WriteAddr(this.ep, PowerAddr.PowerCtrl, Convert.ToUInt32(enable), this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
}
}

View File

@@ -1,8 +1,8 @@
using System.Net; using System.Net;
using DotNext; using DotNext;
namespace RemoteUpdate; namespace RemoteUpdateClient;
static class RemoteUpdateClientAddr static class RemoteUpdaterAddr
{ {
public const UInt32 Base = 0x20_00_00_00; public const UInt32 Base = 0x20_00_00_00;
@@ -91,7 +91,7 @@ static class FlashAddr
/// <summary> /// <summary>
/// [TODO:description] /// [TODO:description]
/// </summary> /// </summary>
public class RemoteUpdateClient public class RemoteUpdater
{ {
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@@ -112,7 +112,7 @@ public class RemoteUpdateClient
/// <param name="timeout">[TODO:parameter]</param> /// <param name="timeout">[TODO:parameter]</param>
/// <param name="timeoutForWait">[TODO:parameter]</param> /// <param name="timeoutForWait">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns> /// <returns>[TODO:return]</returns>
public RemoteUpdateClient(string address, int port, int timeout = 2000, int timeoutForWait = 60 * 1000) public RemoteUpdater(string address, int port, int timeout = 2000, int timeoutForWait = 60 * 1000)
{ {
if (timeout < 0) if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout)); throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
@@ -142,7 +142,7 @@ public class RemoteUpdateClient
{ {
var ret = await UDPClientPool.WriteAddr( var ret = await UDPClientPool.WriteAddr(
this.ep, RemoteUpdateClientAddr.WriteCtrl, this.ep, RemoteUpdaterAddr.WriteCtrl,
Convert.ToUInt32((writeSectorNum << 16) | (1 << 15) | Convert.ToInt32(flashAddr / 4096)), this.timeout); Convert.ToUInt32((writeSectorNum << 16) | (1 << 15) | Convert.ToInt32(flashAddr / 4096)), this.timeout);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Enable write flash failed")); if (!ret.Value) return new(new Exception("Enable write flash failed"));
@@ -150,7 +150,7 @@ public class RemoteUpdateClient
{ {
var ret = await UDPClientPool.ReadAddrWithWait( var ret = await UDPClientPool.ReadAddrWithWait(
this.ep, RemoteUpdateClientAddr.WriteSign, this.ep, RemoteUpdaterAddr.WriteSign,
0x00_00_00_01, 0x00_00_00_01, this.timeoutForWait); 0x00_00_00_01, 0x00_00_00_01, this.timeoutForWait);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception( if (!ret.Value) return new(new Exception(
@@ -158,14 +158,14 @@ public class RemoteUpdateClient
} }
{ {
var ret = await UDPClientPool.WriteAddr(this.ep, RemoteUpdateClientAddr.WriteFIFO, bytesData, this.timeout); var ret = await UDPClientPool.WriteAddr(this.ep, RemoteUpdaterAddr.WriteFIFO, bytesData, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Send data to flash failed")); if (!ret.Value) return new(new Exception("Send data to flash failed"));
} }
{ {
var ret = await UDPClientPool.ReadAddrWithWait( var ret = await UDPClientPool.ReadAddrWithWait(
this.ep, RemoteUpdateClientAddr.WriteSign, this.ep, RemoteUpdaterAddr.WriteSign,
0x00_00_01_00, 0x00_00_01_00, this.timeoutForWait); 0x00_00_01_00, 0x00_00_01_00, this.timeoutForWait);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value; return ret.Value;
@@ -314,14 +314,14 @@ public class RemoteUpdateClient
private async ValueTask<Result<bool>> CheckBitstreamCRC(int bitstreamNum, int bitstreamLen, UInt32 checkSum) private async ValueTask<Result<bool>> CheckBitstreamCRC(int bitstreamNum, int bitstreamLen, UInt32 checkSum)
{ {
{ {
var ret = await UDPClientPool.WriteAddr(this.ep, RemoteUpdateClientAddr.ReadCtrl2, 0x00_00_00_00, this.timeout); var ret = await UDPClientPool.WriteAddr(this.ep, RemoteUpdaterAddr.ReadCtrl2, 0x00_00_00_00, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Write read control 2 failed")); if (!ret.Value) return new(new Exception("Write read control 2 failed"));
} }
{ {
var ret = await UDPClientPool.WriteAddr( var ret = await UDPClientPool.WriteAddr(
this.ep, RemoteUpdateClientAddr.ReadCtrl1, this.ep, RemoteUpdaterAddr.ReadCtrl1,
Convert.ToUInt32((bitstreamLen << 16) | (1 << 15) | Convert.ToInt32(FlashAddr.Bitstream[bitstreamNum] / 4096)), Convert.ToUInt32((bitstreamLen << 16) | (1 << 15) | Convert.ToInt32(FlashAddr.Bitstream[bitstreamNum] / 4096)),
this.timeout); this.timeout);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
@@ -330,7 +330,7 @@ public class RemoteUpdateClient
{ {
var ret = await UDPClientPool.ReadAddrWithWait( var ret = await UDPClientPool.ReadAddrWithWait(
this.ep, RemoteUpdateClientAddr.ReadSign, this.ep, RemoteUpdaterAddr.ReadSign,
0x00_00_01_00, 0x00_00_01_00, this.timeoutForWait); 0x00_00_01_00, 0x00_00_01_00, this.timeoutForWait);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception( if (!ret.Value) return new(new Exception(
@@ -338,7 +338,7 @@ public class RemoteUpdateClient
} }
{ {
var ret = await UDPClientPool.ReadAddr(this.ep, RemoteUpdateClientAddr.ReadCRC, this.timeout); var ret = await UDPClientPool.ReadAddr(this.ep, RemoteUpdaterAddr.ReadCRC, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
var bytes = ret.Value.Options.Data; var bytes = ret.Value.Options.Data;
@@ -368,7 +368,7 @@ public class RemoteUpdateClient
$"Bitsteam num should be 0 ~ 3 for HotRest, but given {bitstreamNum}", nameof(bitstreamNum))); $"Bitsteam num should be 0 ~ 3 for HotRest, but given {bitstreamNum}", nameof(bitstreamNum)));
var ret = await UDPClientPool.WriteAddr( var ret = await UDPClientPool.WriteAddr(
this.ep, RemoteUpdateClientAddr.HotResetCtrl, this.ep, RemoteUpdaterAddr.HotResetCtrl,
((FlashAddr.Bitstream[bitstreamNum] << 8) | 1), this.timeout); ((FlashAddr.Bitstream[bitstreamNum] << 8) | 1), this.timeout);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value; return ret.Value;
@@ -532,4 +532,23 @@ public class RemoteUpdateClient
} }
} }
/// <summary>
/// [TODO:description]
/// </summary>
/// <returns>[TODO:return]</returns>
public async ValueTask<Result<UInt32>> GetVersion()
{
await MsgBus.UDPServer.ClearUDPData(this.address);
logger.Trace("Clear udp data finished");
{
var ret = await UDPClientPool.ReadAddr(this.ep, RemoteUpdaterAddr.Version, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
var retData = ret.Value.Options.Data;
if (retData is null || retData.Length != 4) return new(new Exception("Failed to read remote update firmware version"));
var version = Common.Number.BytesToUInt32(retData);
return version;
}
}
} }

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

@@ -9,6 +9,8 @@ using WebProtocol;
/// </summary> /// </summary>
public class UDPClientPool public class UDPClientPool
{ {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private static IPAddress localhost = IPAddress.Parse("127.0.0.1"); private static IPAddress localhost = IPAddress.Parse("127.0.0.1");
/// <summary> /// <summary>
@@ -58,6 +60,9 @@ public class UDPClientPool
var sendLen = socket.SendTo(buf, endPoint); var sendLen = socket.SendTo(buf, endPoint);
socket.Close(); socket.Close();
logger.Debug($"UDP socket send bytes to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
logger.Debug($" Original Data: {BitConverter.ToString(buf).Replace("-", " ")}");
if (sendLen == buf.Length) { return true; } if (sendLen == buf.Length) { return true; }
else { return false; } else { return false; }
} }
@@ -86,6 +91,10 @@ public class UDPClientPool
var sendLen = socket.SendTo(sendBytes, endPoint); var sendLen = socket.SendTo(sendBytes, endPoint);
socket.Close(); socket.Close();
logger.Debug($"UDP socket send address package to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
logger.Debug($" Original Data: {BitConverter.ToString(pkg.ToBytes()).Replace("-", " ")}");
logger.Debug($" Decoded Data: {pkg.ToString()}");
if (sendLen == sendBytes.Length) { return true; } if (sendLen == sendBytes.Length) { return true; }
else { return false; } else { return false; }
} }
@@ -115,6 +124,9 @@ public class UDPClientPool
var sendLen = socket.SendTo(sendBytes, endPoint); var sendLen = socket.SendTo(sendBytes, endPoint);
socket.Close(); socket.Close();
logger.Debug($"UDP socket send data package to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
logger.Debug($" Original Data: {BitConverter.ToString(pkg.ToBytes()).Replace("-", " ")}");
if (sendLen == sendBytes.Length) { return true; } if (sendLen == sendBytes.Length) { return true; }
else { return false; } else { return false; }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,37 @@
<script setup lang="ts"> <script setup lang="ts">
import Navbar from "./components/Navbar.vue"; import Navbar from "./components/Navbar.vue";
import { ref, provide, onMounted } from "vue"; import Dialog from "./components/Dialog.vue";
import { ref, provide, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
// 主题切换状态管理 // 主题切换状态管理
const isDarkMode = ref(window.matchMedia('(prefers-color-scheme: dark)').matches); const isDarkMode = ref(
window.matchMedia("(prefers-color-scheme: dark)").matches,
);
// 初始化主题设置 // 初始化主题设置
onMounted(() => { onMounted(() => {
// 应用初始主题 // 应用初始主题
applyTheme(); applyTheme();
// 监听系统主题变化 // 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { window
// 跟随系统变化 .matchMedia("(prefers-color-scheme: dark)")
isDarkMode.value = e.matches; .addEventListener("change", (e) => {
applyTheme(); // 跟随系统变化
}); isDarkMode.value = e.matches;
applyTheme();
});
}); });
// 应用主题到文档 // 应用主题到文档
const applyTheme = () => { const applyTheme = () => {
document.documentElement.setAttribute('data-theme', isDarkMode.value ? 'night' : 'winter'); document.documentElement.setAttribute(
"data-theme",
isDarkMode.value ? "night" : "winter",
);
}; };
// 切换主题 // 切换主题
@@ -30,9 +41,13 @@ const toggleTheme = () => {
}; };
// 提供主题状态和切换方法给子组件 // 提供主题状态和切换方法给子组件
provide('theme', { provide("theme", {
isDarkMode, isDarkMode,
toggleTheme toggleTheme,
});
const currentRoutePath = computed(() => {
return router.currentRoute.value.path;
}); });
</script> </script>
@@ -40,15 +55,18 @@ provide('theme', {
<div> <div>
<header class="relative"> <header class="relative">
<Navbar></Navbar> <Navbar></Navbar>
<Dialog></Dialog>
</header> </header>
<main> <main>
<RouterView /> <RouterView />
</main> <footer class="footer footer-center p-4 bg-base-300 text-base-content"> </main>
<footer v-if="currentRoutePath != '/project'" class="footer footer-center p-4 bg-base-300 text-base-content">
<div> <div>
<p>Copyright © 2023 - All right reserved by OurEDA</p> <p>Copyright © 2023 - All right reserved by OurEDA</p>
</div> </div>
</footer> </div> </footer>
</div>
</template> </template>
<style scoped> <style scoped>

10
src/assets/base.css Normal file
View File

@@ -0,0 +1,10 @@
@import "tailwindcss";
@plugin "daisyui" {
themes: winter --default, night --prefersdark;
}
@custom-variant dark (&:where([data-theme=night], [data-theme=night] *));
@custom-variant light (&:where([data-theme=winter], [data-theme=winter] *));

1
src/assets/doc.svg Normal file
View File

@@ -0,0 +1 @@
<svg t="1747137150839" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5910" width="200" height="200"><path d="M224 831.936V192.096L223.808 192H576v159.936c0 35.328 28.736 64.064 64.064 64.064h159.712c0.032 0.512 0.224 1.184 0.224 1.664L800.256 832 224 831.936zM757.664 352L640 351.936V224.128L757.664 352z m76.064-11.872l-163.872-178.08C651.712 142.336 619.264 128 592.672 128H223.808A64.032 64.032 0 0 0 160 192.096v639.84A64 64 0 0 0 223.744 896h576.512A64 64 0 0 0 864 831.872V417.664c0-25.856-12.736-58.464-30.272-77.536z" fill="#3E3A39" p-id="5911"></path><path d="M640 512h-256a32 32 0 0 0 0 64h256a32 32 0 0 0 0-64M640 672h-256a32 32 0 0 0 0 64h256a32 32 0 0 0 0-64" fill="#3E3A39" p-id="5912"></path></svg>

After

Width:  |  Height:  |  Size: 759 B

View File

@@ -1,11 +1,4 @@
@import "tailwindcss"; @import "base.css";
@plugin "daisyui" {
themes: winter --default, night --prefersdark;
}
@custom-variant dark (&:where([data-theme=night], [data-theme=night] *));
@custom-variant light (&:where([data-theme=winter], [data-theme=winter] *));
/* 禁止所有图像和SVG选择 */ /* 禁止所有图像和SVG选择 */
img, svg { img, svg {

1
src/assets/refresh.svg Normal file
View File

@@ -0,0 +1 @@
<svg t="1747136937808" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4865" width="200" height="200"><path d="M894.481158 505.727133c0 49.589418-9.711176 97.705276-28.867468 143.007041-18.501376 43.74634-44.98454 83.031065-78.712713 116.759237-33.728172 33.728172-73.012897 60.211337-116.759237 78.712713-45.311998 19.156292-93.417623 28.877701-143.007041 28.877701s-97.695043-9.721409-142.996808-28.877701c-43.756573-18.501376-83.031065-44.98454-116.76947-78.712713-33.728172-33.728172-60.211337-73.012897-78.712713-116.759237-19.156292-45.301765-28.867468-93.417623-28.867468-143.007041 0-49.579185 9.711176-97.695043 28.867468-142.996808 18.501376-43.74634 44.98454-83.031065 78.712713-116.759237 33.738405-33.728172 73.012897-60.211337 116.76947-78.712713 45.301765-19.166525 93.40739-28.877701 142.996808-28.877701 52.925397 0 104.008842 11.010775 151.827941 32.745798 46.192042 20.977777 86.909395 50.79692 121.016191 88.608084 4.389984 4.860704 8.646937 9.854439 12.781094 14.97097l0-136.263453c0-11.307533 9.168824-20.466124 20.466124-20.466124 11.307533 0 20.466124 9.15859 20.466124 20.466124l0 183.64253c0 5.433756-2.148943 10.632151-5.986341 14.46955-3.847631 3.837398-9.046027 5.996574-14.479783 5.996574l-183.64253-0.020466c-11.307533 0-20.466124-9.168824-20.466124-20.466124 0-11.307533 9.168824-20.466124 20.466124-20.466124l132.293025 0.020466c-3.960195-4.952802-8.063653-9.782807-12.289907-14.479783-30.320563-33.605376-66.514903-60.098773-107.549481-78.753645-42.467207-19.289322-87.850837-29.072129-134.902456-29.072129-87.195921 0-169.172981 33.9533-230.816946 95.597265-61.654198 61.654198-95.597265 143.621025-95.597265 230.816946s33.943067 169.172981 95.597265 230.816946c61.643965 61.654198 143.621025 95.607498 230.816946 95.607498s169.172981-33.9533 230.816946-95.607498c61.654198-61.643965 95.597265-143.621025 95.597265-230.816946 0-11.2973 9.168824-20.466124 20.466124-20.466124C885.322567 485.261009 894.481158 494.429833 894.481158 505.727133z" p-id="4866"></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -3,35 +3,39 @@
interface Props { interface Props {
title: string; title: string;
isExpanded?: boolean; isExpanded?: boolean;
status?: 'default' | 'success' | 'error'; status?: "default" | "success" | "error";
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
isExpanded: false, isExpanded: false,
status: 'default' status: "default",
}); });
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:isExpanded', value: boolean): void (e: "update:isExpanded", value: boolean): void;
}>(); }>();
// 切换展开/收起状态 // 切换展开/收起状态
const toggleExpand = () => { const toggleExpand = () => {
emit('update:isExpanded', !props.isExpanded); emit("update:isExpanded", !props.isExpanded);
}; };
// 动画处理函数 // 动画处理函数
const enter = (element: Element, done: () => void) => { const enter = (element: Element, done: () => void) => {
if (element instanceof HTMLElement) { if (element instanceof HTMLElement) {
const height = element.scrollHeight; const height = element.scrollHeight;
element.style.height = '0px'; element.style.height = "0px";
// 触发重绘 // 触发重绘
element.offsetHeight; element.offsetHeight;
element.style.height = height + 'px'; element.style.height = height + "px";
element.addEventListener('transitionend', () => { element.addEventListener(
done(); "transitionend",
}, { once: true }); () => {
done();
},
{ once: true },
);
} else { } else {
done(); done();
} }
@@ -39,21 +43,25 @@ const enter = (element: Element, done: () => void) => {
const afterEnter = (element: Element) => { const afterEnter = (element: Element) => {
if (element instanceof HTMLElement) { if (element instanceof HTMLElement) {
element.style.height = 'auto'; element.style.height = "auto";
} }
}; };
const leave = (element: Element, done: () => void) => { const leave = (element: Element, done: () => void) => {
if (element instanceof HTMLElement) { if (element instanceof HTMLElement) {
const height = element.scrollHeight; const height = element.scrollHeight;
element.style.height = height + 'px'; element.style.height = height + "px";
// 触发重绘 // 触发重绘
element.offsetHeight; element.offsetHeight;
element.style.height = '0px'; element.style.height = "0px";
element.addEventListener('transitionend', () => { element.addEventListener(
done(); "transitionend",
}, { once: true }); () => {
done();
},
{ once: true },
);
} else { } else {
done(); done();
} }
@@ -61,17 +69,12 @@ const leave = (element: Element, done: () => void) => {
</script> </script>
<template> <template>
<div class="section" :class="[`status-${status}`]"> <div class="section m-4 shadow-xl" :class="[`status-${status}`]">
<div class="section-header" @click="toggleExpand"> <div class="section-header bg-primary text-primary-content" @click="toggleExpand">
<h2>{{ title }}</h2> <h2>{{ title }}</h2>
<span class="expand-icon" :class="{ 'is-expanded': isExpanded }"></span> <span class="expand-icon" :class="{ 'is-expanded': isExpanded }"></span>
</div> </div>
<transition <transition name="collapse" @enter="enter" @after-enter="afterEnter" @leave="leave">
name="collapse"
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
>
<div v-show="isExpanded" class="section-content"> <div v-show="isExpanded" class="section-content">
<div class="section-inner"> <div class="section-inner">
<slot></slot> <slot></slot>
@@ -88,7 +91,6 @@ const leave = (element: Element, done: () => void) => {
border-radius: var(--radius-md, 0.375rem); border-radius: var(--radius-md, 0.375rem);
overflow: hidden; overflow: hidden;
background-color: hsl(var(--b1)); background-color: hsl(var(--b1));
box-shadow: var(--shadow-sm, 0 1px 2px 0 rgba(0, 0, 0, 0.05));
transition: all var(--transition-normal, 0.3s); transition: all var(--transition-normal, 0.3s);
} }
@@ -119,26 +121,58 @@ const leave = (element: Element, done: () => void) => {
} }
@keyframes borderPulseSuccess { @keyframes borderPulseSuccess {
0% { border-color: hsl(var(--b3)); box-shadow: 0px 0px 0px transparent;} 0% {
50% { border-color: hsl(var(--su)); box-shadow: 0px 0px 5px hsl(var(--su));} border-color: hsl(var(--b3));
100% { border-color: hsl(var(--su)); box-shadow: 0px 0px 3px hsl(var(--su));} box-shadow: 0px 0px 0px transparent;
}
50% {
border-color: hsl(var(--su));
box-shadow: 0px 0px 5px hsl(var(--su));
}
100% {
border-color: hsl(var(--su));
box-shadow: 0px 0px 3px hsl(var(--su));
}
} }
@keyframes borderPulseError { @keyframes borderPulseError {
0% { border-color: hsl(var(--b3)); box-shadow: 0px 0px 0px transparent;} 0% {
50% { border-color: hsl(var(--er)); box-shadow: 0px 0px 5px hsl(var(--er));} border-color: hsl(var(--b3));
100% { border-color: hsl(var(--er)); box-shadow: 0px 0px 3px hsl(var(--er));} box-shadow: 0px 0px 0px transparent;
}
50% {
border-color: hsl(var(--er));
box-shadow: 0px 0px 5px hsl(var(--er));
}
100% {
border-color: hsl(var(--er));
box-shadow: 0px 0px 3px hsl(var(--er));
}
} }
@keyframes borderPulseInfo { @keyframes borderPulseInfo {
0% { border-color: hsl(var(--b3)); box-shadow: 0px 0px 0px transparent;} 0% {
50% { border-color: hsl(var(--in)); box-shadow: 0px 0px 5px hsl(var(--in));} border-color: hsl(var(--b3));
100% { border-color: hsl(var(--in)); box-shadow: 0px 0px 3px hsl(var(--in));} box-shadow: 0px 0px 0px transparent;
}
50% {
border-color: hsl(var(--in));
box-shadow: 0px 0px 5px hsl(var(--in));
}
100% {
border-color: hsl(var(--in));
box-shadow: 0px 0px 3px hsl(var(--in));
}
} }
.section-header { .section-header {
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 0.75rem); padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 0.75rem);
background-color: hsl(var(--b1));
cursor: pointer; cursor: pointer;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -148,10 +182,6 @@ const leave = (element: Element, done: () => void) => {
transition: all var(--transition-normal, 0.3s); transition: all var(--transition-normal, 0.3s);
} }
.section-header:hover {
background-color: hsl(var(--b2));
}
.section-header h2 { .section-header h2 {
margin: 0; margin: 0;
font-size: 1.1em; font-size: 1.1em;
@@ -188,10 +218,12 @@ const leave = (element: Element, done: () => void) => {
} }
.content-wrapper { .content-wrapper {
overflow: visible; /* 允许内容溢出 */ overflow: visible;
/* 允许内容溢出 */
} }
.card-body { .card-body {
overflow: visible; /* 允许内容溢出 */ overflow: visible;
/* 允许内容溢出 */
} }
</style> </style>

Some files were not shown because too many files have changed in this diff Show More