Compare commits
58 Commits
c39f688115
...
refactor
| Author | SHA1 | Date | |
|---|---|---|---|
| de28471f87 | |||
| 3a292c0a98 | |||
| 91b00a977c | |||
| c5ce246caf | |||
| c3bd61ed51 | |||
| 14d8499f77 | |||
| d18cf82813 | |||
| f1e2dbd9d8 | |||
| 262c5e4003 | |||
| fbd13f8f2f | |||
| 6cf7ef02ac | |||
| 8207c37e12 | |||
| db71681bdf | |||
| 2270022bbe | |||
| dcadb97a7f | |||
| 1538bb9d07 | |||
| f340c86a41 | |||
| b6fb7e05fa | |||
| e0db12e0eb | |||
| dc64a65702 | |||
| 46621fdb40 | |||
| 970a537391 | |||
| 3883cd8304 | |||
| 2aa2f1dc37 | |||
| 1bdcb672ab | |||
| 7b1d1a5e87 | |||
| 5bb011e685 | |||
| e3826f0ff6 | |||
| f8163be98b | |||
|
|
ea7c2d425a | ||
| e4791b41a8 | |||
| bea1c7e5ae | |||
|
|
b6d8612e8c | ||
| b68d8eaf11 | |||
| 390ce8250d | |||
|
|
09b8f676ba | ||
|
|
cc83133a6c | ||
|
|
6d640e8049 | ||
| 07eb606324 | |||
|
|
8eefed92a8 | ||
|
|
8f2f90ef1d | ||
| 487e7c114a | |||
| 7f37514dfa | |||
| 0b0b4acb17 | |||
|
|
0bd3b42840 | ||
| 068576b60b | |||
| d754a881d7 | |||
| a6ac728cf1 | |||
| ba6ec73b84 | |||
| 5042bf8ce5 | |||
| 2a3ef1ea7d | |||
| 4885447c2b | |||
| 351a230107 | |||
| 7aff4f3f02 | |||
| c7907b4253 | |||
| 3fb59af2dd | |||
| 9b2ee8ad46 | |||
|
|
7dd5e2189f |
1
.gitignore
vendored
@@ -9,6 +9,7 @@ lerna-debug.log*
|
|||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.vscode
|
||||||
dist
|
dist
|
||||||
**/wwwroot
|
**/wwwroot
|
||||||
dist-ssr
|
dist-ssr
|
||||||
|
|||||||
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "server/src/BsdlParser"]
|
|
||||||
path = server/src/BsdlParser
|
|
||||||
url = git@github.com:SikongJueluo/python-bsdl-parser.git
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
|
||||||
isSelfContained := "false"
|
isSelfContained := "false"
|
||||||
|
|
||||||
@_show-dir:
|
@_show-dir:
|
||||||
@@ -12,16 +13,20 @@ 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:
|
update:
|
||||||
npm install
|
npm install
|
||||||
cd server && dotnet restore
|
dotnet restore ./server/server.csproj
|
||||||
git submodule update --init --remote --recursive
|
git submodule update --init --remote --recursive
|
||||||
|
|
||||||
# 生成Restful API到网页客户端
|
# 生成Restful API到网页客户端
|
||||||
gen-api:
|
gen-api:
|
||||||
npm run gen-api
|
npm run gen-api
|
||||||
|
|
||||||
|
gen-api-from-server:
|
||||||
|
npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts
|
||||||
|
|
||||||
# 构建服务器,包含win与linux平台
|
# 构建服务器,包含win与linux平台
|
||||||
[working-directory: "server"]
|
[working-directory: "server"]
|
||||||
build-server self-contained=isSelfContained: _show-dir
|
build-server self-contained=isSelfContained: _show-dir
|
||||||
@@ -43,7 +48,7 @@ dev: dev-server
|
|||||||
|
|
||||||
# 测试服务器
|
# 测试服务器
|
||||||
dev-server: _show-dir
|
dev-server: _show-dir
|
||||||
cd server && dotnet run --watch
|
dotnet run --watch --project ./server/server.csproj
|
||||||
|
|
||||||
# 运行网页客户端
|
# 运行网页客户端
|
||||||
dev-web:
|
dev-web:
|
||||||
|
|||||||
56
components.d.ts
vendored
Normal 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
@@ -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",
|
||||||
|
|||||||
712
package-lock.json
generated
@@ -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",
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -16,15 +16,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": {
|
||||||
@@ -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"
|
||||||
|
|||||||
314
public/EquipmentTemplates/MatrixKey.json
Normal 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": []
|
||||||
|
}
|
||||||
BIN
public/doc/01/cover.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
175
public/doc/01/doc.md
Normal 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 = 8’b0000_0001,当经过0.5s后,也就是cnt等于13_499_999的时候,第一个led灭,第二个led亮起,也就是led = 8‘b0000_0010。同理,再过0.5s,led = 8’b0000_0100,再过0.5s,led = 8‘b0000_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
|
After Width: | Height: | Size: 1.2 MiB |
BIN
public/doc/01/images/2.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
public/doc/01/images/3.png
Normal file
|
After Width: | Height: | Size: 620 KiB |
BIN
public/doc/01/images/4.png
Normal file
|
After Width: | Height: | Size: 635 KiB |
BIN
public/doc/02/cover.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
258
public/doc/02/doc.md
Normal 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_flag),led会加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`的值增加到`8‘hFF`时,认为按键按下,`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`计数到`8’hFF`时,更新`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
|
After Width: | Height: | Size: 821 KiB |
BIN
public/doc/02/images/2.png
Normal file
|
After Width: | Height: | Size: 434 KiB |
BIN
public/doc/02/images/3.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/doc/02/images/4.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
public/doc/02/images/5.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/doc/02/images/6.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
public/doc/02/images/7.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
public/doc/03/cover.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
244
public/doc/03/doc.md
Normal 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
|
After Width: | Height: | Size: 37 KiB |
212
public/doc/04/doc.md
Normal 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
|
After Width: | Height: | Size: 85 KiB |
BIN
public/doc/05/cover.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
194
public/doc/05/doc.md
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
# 基础-5-PWM呼吸灯
|
||||||
|
|
||||||
|
## 5.1 章节导读
|
||||||
|
|
||||||
|
本章将实现 PWM(脉宽调制)呼吸灯效果,即控制 LED 灯的亮度在一个周期内从暗到亮再从亮到暗,形成如人呼吸般的灯光变化。通过该实验可以掌握 PWM 占空比调节以及 FPGA 控制 LED 的基本方法。
|
||||||
|
|
||||||
|
## 5.2 理论学习
|
||||||
|
|
||||||
|
呼吸灯在我们的生活中很常见,在电脑上多作为消息提醒指示灯而被广泛使用,其效果是小灯在一段时间内从完全熄灭的状态逐渐变到最亮,再在同样的时间段内逐渐达到完全熄灭的状态,并循环往复。这种效果就像“呼吸”一样。而实现”呼吸“的方法就是PWM技术。
|
||||||
|
|
||||||
|
PWM(Pulse 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亮的时间为1us,1ms过后,duty变为54,led亮的时间为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
|
After Width: | Height: | Size: 1.2 MiB |
BIN
public/doc/05/images/2.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
public/doc/05/images/3.png
Normal file
|
After Width: | Height: | Size: 858 KiB |
BIN
public/doc/05/images/4.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/doc/06/cover.png
Normal file
|
After Width: | Height: | Size: 377 KiB |
650
public/doc/06/doc.md
Normal file
@@ -0,0 +1,650 @@
|
|||||||
|
# 基础-6-HDMI显示
|
||||||
|
|
||||||
|
## 6.1 章节导读
|
||||||
|
|
||||||
|
随着多媒体技术的快速发展,高清显示已成为嵌入式系统与FPGA应用中不可或缺的一部分。HDMI(High-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>
|
||||||
|
|
||||||
|
如上图所示,每一帧图像都是从左上角开始,逐行扫描形成,所以规定最左上角的像素点为第一个像素点,坐标是(0,0),以这个像素为起点,向右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模块,我们主要完成hsync,vsync信号,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信号计数结束后会恢复0,cnt_v会加一,hsync信号会拉高96个像素时钟(0~95)cnt_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
|
After Width: | Height: | Size: 39 KiB |
BIN
public/doc/06/images/10.png
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
public/doc/06/images/11.png
Normal file
|
After Width: | Height: | Size: 620 KiB |
BIN
public/doc/06/images/12.png
Normal file
|
After Width: | Height: | Size: 614 KiB |
BIN
public/doc/06/images/13.png
Normal file
|
After Width: | Height: | Size: 942 KiB |
BIN
public/doc/06/images/14.png
Normal file
|
After Width: | Height: | Size: 543 KiB |
BIN
public/doc/06/images/2.png
Normal file
|
After Width: | Height: | Size: 745 KiB |
BIN
public/doc/06/images/3.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/doc/06/images/4.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/doc/06/images/5.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
public/doc/06/images/6.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
public/doc/06/images/7.png
Normal file
|
After Width: | Height: | Size: 203 KiB |
BIN
public/doc/06/images/8.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
public/doc/06/images/9.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
public/doc/11/cover.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
400
public/doc/11/doc.md
Normal 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
|
After Width: | Height: | Size: 21 KiB |
BIN
public/doc/11/images/UDP.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
736
public/doc/12/doc.md
Normal file
BIN
public/doc/12/images/1.jfif
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
public/doc/12/images/2.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
public/doc/12/images/3.jpg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
BIN
public/doc/12/images/4.png
Normal file
|
After Width: | Height: | Size: 653 KiB |
BIN
public/doc/12/images/5.jpg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
BIN
public/doc/12/images/6.png
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
BIN
public/doc/12/images/7.png
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
BIN
public/doc/12/images/8.png
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
public/doc/13/cover.png
Normal file
|
After Width: | Height: | Size: 848 KiB |
350
public/doc/13/doc.md
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
# 进阶-3-频率计
|
||||||
|
|
||||||
|
## 3.1 章节导读
|
||||||
|
|
||||||
|
本实验将基于实验平台设计并实现一个简易频率计,用于测量输入信号的频率值,并通过数码管进行实时显示。实验核心是掌握ADC模块的使用方法,被测信号频率的获取方法及其在数字系统中的处理流程。
|
||||||
|
|
||||||
|
## 3.2 理论学习
|
||||||
|
|
||||||
|
### 3.2.1 ADC模块
|
||||||
|
|
||||||
|
实验平台有一块8bit高速ADDA模块,其中ADC模块使用AD9280芯片,支持最高32MSPS的速率,模拟电压输入范围为-5~+5V,ADC模块可以根据输入电压的大小将其转换为0~255(2的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
|
After Width: | Height: | Size: 57 KiB |
BIN
public/doc/13/images/1.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
public/doc/13/images/2.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
1
server/.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
obj
|
obj
|
||||||
bin
|
bin
|
||||||
bitstream
|
bitstream
|
||||||
|
bsdl
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ try
|
|||||||
{
|
{
|
||||||
options.AddPolicy("Users", policy => policy
|
options.AddPolicy("Users", policy => policy
|
||||||
.AllowAnyOrigin()
|
.AllowAnyOrigin()
|
||||||
|
.AllowAnyHeader()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,12 +101,6 @@ try
|
|||||||
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "assets")),
|
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "assets")),
|
||||||
RequestPath = "/assets"
|
RequestPath = "/assets"
|
||||||
});
|
});
|
||||||
// Public Files
|
|
||||||
app.UseStaticFiles(new StaticFileOptions
|
|
||||||
{
|
|
||||||
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "EquipmentTemplates")),
|
|
||||||
RequestPath = "/public/EquipmentTemplates"
|
|
||||||
});
|
|
||||||
// Log Files
|
// Log Files
|
||||||
if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "log")))
|
if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "log")))
|
||||||
{
|
{
|
||||||
|
|||||||
5124
server/boundary_registers.json
Normal file
@@ -25,7 +25,6 @@
|
|||||||
<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="pythonnet" Version="3.0.5" />
|
|
||||||
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
|
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
193
server/src/BsdlParser.cs
Normal 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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections;
|
||||||
using DotNext;
|
using DotNext;
|
||||||
|
|
||||||
namespace Common
|
namespace Common
|
||||||
@@ -273,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>
|
||||||
/// 字符串转二进制字节数组
|
/// 字符串转二进制字节数组
|
||||||
|
|||||||
@@ -1,833 +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 ID Code
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="address"> 设备地址 </param>
|
|
||||||
/// <param name="port"> 设备端口 </param>
|
|
||||||
[HttpGet("GetDeviceIDCode")]
|
|
||||||
[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>
|
|
||||||
/// <param name="portNum">[TODO:parameter]</param>
|
|
||||||
/// <returns>[TODO:return]</returns>
|
|
||||||
[HttpPost("BoundaryScan")]
|
|
||||||
[EnableCors("Users")]
|
|
||||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
|
|
||||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
|
||||||
public async ValueTask<IResult> BoundaryScan(string address, int port, int portNum)
|
|
||||||
{
|
|
||||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
|
||||||
var ret = await jtagCtrl.BoundaryScan(portNum);
|
|
||||||
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>
|
|
||||||
/// 远程更新
|
|
||||||
/// </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")]
|
|
||||||
[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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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 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 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 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <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];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
30
server/src/Controllers/BsdlParserController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
108
server/src/Controllers/DDSController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
69
server/src/Controllers/DataController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
278
server/src/Controllers/JtagController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
101
server/src/Controllers/MatrixKeyController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
46
server/src/Controllers/PowerController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
292
server/src/Controllers/RemoteUpdateController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
133
server/src/Controllers/UDPController.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections;
|
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;
|
||||||
@@ -27,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>
|
||||||
@@ -771,14 +776,14 @@ public class Jtag
|
|||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// [TODO:description]
|
/// 边界扫描
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="portNum">[TODO:parameter]</param>
|
/// <returns>返回所有引脚边界扫描结果</returns>
|
||||||
/// <returns>[TODO:return]</returns>
|
public async ValueTask<Result<BitArray>> BoundaryScan()
|
||||||
public async ValueTask<Result<BitArray>> BoundaryScan(int portNum)
|
|
||||||
{
|
{
|
||||||
if (portNum <= 0)
|
var paser = new BsdlParser.Parser();
|
||||||
return new(new ArgumentException("The number of port couldn't be negative", nameof(portNum)));
|
var portNum = paser.GetBoundaryRegsNum().Value;
|
||||||
|
logger.Debug($"Get boundar scan registers number: {portNum}");
|
||||||
|
|
||||||
// Clear Data
|
// Clear Data
|
||||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||||
@@ -810,6 +815,53 @@ public class Jtag
|
|||||||
|
|
||||||
var byteArray = Common.Number.UInt32ArrayToBytes(retData.Value);
|
var byteArray = Common.Number.UInt32ArrayToBytes(retData.Value);
|
||||||
if (!byteArray.IsSuccessful) return new(byteArray.Error);
|
if (!byteArray.IsSuccessful) return new(byteArray.Error);
|
||||||
return new BitArray(byteArray.Value);
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using DotNext;
|
using DotNext;
|
||||||
|
|
||||||
namespace DDSClient;
|
namespace Peripherals.DDSClient;
|
||||||
|
|
||||||
static class DDSAddr
|
static class DDSAddr
|
||||||
{
|
{
|
||||||
89
server/src/Peripherals/MatrixKeyClient.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
server/src/Peripherals/PowerClient.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
55
server/src/TutorialController.cs
Normal 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 = "无法读取教程目录" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
server/src/TutorialService.ts
Normal 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: '无法读取例程目录' });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
2746
src/APIClient.ts
10
src/assets/base.css
Normal 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,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 {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useDialogStore } from "@/stores/dialog";
|
import { useDialogStore } from "@/stores/dialog";
|
||||||
const dialog = useDialogStore();
|
const dialog = useDialogStore();
|
||||||
|
dialog.enableDialog = true;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="postcss">
|
<style scoped lang="postcss">
|
||||||
|
|||||||
4
src/components/InjectKeys.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import type { InjectionKey, Ref } from "vue";
|
||||||
|
|
||||||
|
export const CanvasCurrentSelectedComponentID = Symbol() as InjectionKey<Ref<string | null>>
|
||||||
|
|
||||||
@@ -181,7 +181,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, shallowRef, onMounted } from "vue";
|
import { ref, computed, shallowRef, onMounted } from "vue";
|
||||||
import motherboardSvg from "../components/equipments/svg/motherboard.svg";
|
import motherboardSvg from "@/components/equipments/svg/motherboard.svg";
|
||||||
|
import buttonSvg from "@/components//equipments/svg/button.svg";
|
||||||
|
|
||||||
// Props 定义
|
// Props 定义
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -219,6 +220,7 @@ const availableComponents = [
|
|||||||
{ type: "SMA", name: "SMA连接器" },
|
{ type: "SMA", name: "SMA连接器" },
|
||||||
{ type: "MotherBoard", name: "主板" },
|
{ type: "MotherBoard", name: "主板" },
|
||||||
{ type: "PG2L100H_FBG676", name: "PG2L100H FBG676芯片" },
|
{ type: "PG2L100H_FBG676", name: "PG2L100H FBG676芯片" },
|
||||||
|
{ type: "BaseBoard", name: "通用底板" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// --- 可用虚拟外设列表 ---
|
// --- 可用虚拟外设列表 ---
|
||||||
@@ -230,9 +232,16 @@ const availableTemplates = ref([
|
|||||||
name: "PG2L100H 基础开发板",
|
name: "PG2L100H 基础开发板",
|
||||||
id: "PG2L100H_Pango100pro",
|
id: "PG2L100H_Pango100pro",
|
||||||
description: "包含主板和两个LED的基本设置",
|
description: "包含主板和两个LED的基本设置",
|
||||||
path: "/public/EquipmentTemplates/PG2L100H_Pango100pro.json",
|
path: "/EquipmentTemplates/PG2L100H_Pango100pro.json",
|
||||||
thumbnailUrl: motherboardSvg,
|
thumbnailUrl: motherboardSvg,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "矩阵键盘",
|
||||||
|
id: "MatrixKey",
|
||||||
|
description: "包含4x4,共16个按键的矩阵键盘",
|
||||||
|
path: "/EquipmentTemplates/MatrixKey.json",
|
||||||
|
thumbnailUrl: buttonSvg,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 显示/隐藏组件菜单
|
// 显示/隐藏组件菜单
|
||||||
@@ -249,7 +258,7 @@ async function loadComponentModule(type: string) {
|
|||||||
if (!componentModules.value[type]) {
|
if (!componentModules.value[type]) {
|
||||||
try {
|
try {
|
||||||
// 假设组件都在 src/components/equipments/ 目录下,且文件名与 type 相同
|
// 假设组件都在 src/components/equipments/ 目录下,且文件名与 type 相同
|
||||||
const module = await import(`../components/equipments/${type}.vue`);
|
const module = await import(`@/components/equipments/${type}.vue`);
|
||||||
|
|
||||||
// 将模块添加到缓存中
|
// 将模块添加到缓存中
|
||||||
componentModules.value = {
|
componentModules.value = {
|
||||||
@@ -372,6 +381,7 @@ async function addTemplate(template: any) {
|
|||||||
id: template.id,
|
id: template.id,
|
||||||
name: template.name,
|
name: template.name,
|
||||||
template: templateData,
|
template: templateData,
|
||||||
|
capsPage: template.capsPage
|
||||||
});
|
});
|
||||||
|
|
||||||
// 关闭菜单
|
// 关闭菜单
|
||||||
646
src/components/LabCanvas/composable/componentManager.ts
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
import { ref, shallowRef, computed } from "vue";
|
||||||
|
import { createInjectionState } from "@vueuse/core";
|
||||||
|
import type { DiagramData, DiagramPart } from "./diagramManager";
|
||||||
|
import type { PropertyConfig } from "@/components/equipments/componentConfig";
|
||||||
|
import {
|
||||||
|
generatePropertyConfigs,
|
||||||
|
generatePropsFromDefault,
|
||||||
|
generatePropsFromAttrs,
|
||||||
|
} from "@/components/equipments/componentConfig";
|
||||||
|
|
||||||
|
// 存储动态导入的组件模块
|
||||||
|
interface ComponentModule {
|
||||||
|
default: any;
|
||||||
|
getDefaultProps?: () => Record<string, any>;
|
||||||
|
config?: {
|
||||||
|
props?: Array<PropertyConfig>;
|
||||||
|
};
|
||||||
|
__esModule?: boolean; // 添加 __esModule 属性
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义组件管理器的状态和方法
|
||||||
|
const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||||
|
() => {
|
||||||
|
// --- 状态管理 ---
|
||||||
|
const componentModules = ref<Record<string, ComponentModule>>({});
|
||||||
|
const selectedComponentId = ref<string | null>(null);
|
||||||
|
const selectedComponentConfig = shallowRef<{ props: PropertyConfig[] } | null>(null);
|
||||||
|
const diagramCanvas = ref<any>(null);
|
||||||
|
const componentRefs = ref<Record<string, any>>({});
|
||||||
|
|
||||||
|
// 计算当前选中的组件数据
|
||||||
|
const selectedComponentData = computed(() => {
|
||||||
|
if (!diagramCanvas.value || !selectedComponentId.value) return null;
|
||||||
|
|
||||||
|
const canvasInstance = diagramCanvas.value as any;
|
||||||
|
if (canvasInstance && canvasInstance.getDiagramData) {
|
||||||
|
const data = canvasInstance.getDiagramData();
|
||||||
|
return data.parts.find((p: DiagramPart) => p.id === selectedComponentId.value) || null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- 组件模块管理 ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态加载组件模块
|
||||||
|
*/
|
||||||
|
async function loadComponentModule(type: string) {
|
||||||
|
console.log(`尝试加载组件模块: ${type}`);
|
||||||
|
console.log(`当前已加载的模块:`, Object.keys(componentModules.value));
|
||||||
|
|
||||||
|
if (!componentModules.value[type]) {
|
||||||
|
try {
|
||||||
|
console.log(`正在动态导入模块: @/components/equipments/${type}.vue`);
|
||||||
|
const module = await import(`@/components/equipments/${type}.vue`);
|
||||||
|
console.log(`成功导入模块 ${type}:`, module);
|
||||||
|
|
||||||
|
// 直接设置新的对象引用以触发响应性
|
||||||
|
componentModules.value = {
|
||||||
|
...componentModules.value,
|
||||||
|
[type]: module,
|
||||||
|
};
|
||||||
|
console.log(`模块 ${type} 已添加到 componentModules`);
|
||||||
|
console.log(`更新后的模块列表:`, Object.keys(componentModules.value));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load component module ${type}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`模块 ${type} 已经存在`);
|
||||||
|
}
|
||||||
|
return componentModules.value[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预加载所有组件模块
|
||||||
|
*/
|
||||||
|
async function preloadComponentModules(componentTypes: string[]) {
|
||||||
|
console.log("Preloading component modules:", componentTypes);
|
||||||
|
await Promise.all(
|
||||||
|
componentTypes.map((type) => loadComponentModule(type))
|
||||||
|
);
|
||||||
|
console.log("All component modules loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 组件操作 ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加新组件到画布
|
||||||
|
*/
|
||||||
|
async function addComponent(componentData: {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
props: Record<string, any>;
|
||||||
|
}) {
|
||||||
|
console.log("=== 开始添加组件 ===");
|
||||||
|
console.log("组件数据:", componentData);
|
||||||
|
|
||||||
|
const canvasInstance = diagramCanvas.value as any;
|
||||||
|
if (!canvasInstance) {
|
||||||
|
console.error("没有可用的画布实例");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预加载组件模块,确保组件能正常渲染
|
||||||
|
console.log(`预加载组件模块: ${componentData.type}`);
|
||||||
|
const componentModule = await loadComponentModule(componentData.type);
|
||||||
|
if (!componentModule) {
|
||||||
|
console.error(`无法加载组件模块: ${componentData.type}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`组件模块加载成功: ${componentData.type}`, componentModule);
|
||||||
|
|
||||||
|
// 获取画布位置信息
|
||||||
|
let position = { x: 100, y: 100 };
|
||||||
|
let scale = 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (canvasInstance.getCanvasPosition && canvasInstance.getScale) {
|
||||||
|
position = canvasInstance.getCanvasPosition();
|
||||||
|
scale = canvasInstance.getScale();
|
||||||
|
|
||||||
|
const canvasContainer = canvasInstance.$el as HTMLElement;
|
||||||
|
if (canvasContainer) {
|
||||||
|
const viewportWidth = canvasContainer.clientWidth;
|
||||||
|
const viewportHeight = canvasContainer.clientHeight;
|
||||||
|
|
||||||
|
position.x = (viewportWidth / 2 - position.x) / scale;
|
||||||
|
position.y = (viewportHeight / 2 - position.y) / scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取画布位置时出错:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加随机偏移
|
||||||
|
const offsetX = Math.floor(Math.random() * 100) - 50;
|
||||||
|
const offsetY = Math.floor(Math.random() * 100) - 50;
|
||||||
|
|
||||||
|
// 获取组件能力页面
|
||||||
|
let capsPage = null;
|
||||||
|
if (
|
||||||
|
componentModule &&
|
||||||
|
componentModule.default &&
|
||||||
|
typeof componentModule.default.getCapabilities === "function"
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
capsPage = componentModule.default.getCapabilities();
|
||||||
|
console.log(`获取到${componentData.type}组件的能力页面`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`获取${componentData.type}组件能力页面失败:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新组件
|
||||||
|
const newComponent: DiagramPart = {
|
||||||
|
id: `component-${Date.now()}`,
|
||||||
|
type: componentData.type,
|
||||||
|
x: Math.round(position.x + offsetX),
|
||||||
|
y: Math.round(position.y + offsetY),
|
||||||
|
attrs: componentData.props,
|
||||||
|
rotate: 0,
|
||||||
|
group: "",
|
||||||
|
positionlock: false,
|
||||||
|
hidepins: true,
|
||||||
|
isOn: true,
|
||||||
|
index: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 通过画布实例添加组件
|
||||||
|
if (canvasInstance.getDiagramData && canvasInstance.updateDiagramDataDirectly) {
|
||||||
|
const currentData = canvasInstance.getDiagramData();
|
||||||
|
currentData.parts.push(newComponent);
|
||||||
|
|
||||||
|
// 使用 updateDiagramDataDirectly 避免触发加载状态
|
||||||
|
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||||
|
|
||||||
|
console.log("组件添加完成:", newComponent);
|
||||||
|
|
||||||
|
// 等待Vue的下一个tick,确保组件模块已经更新
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加模板到画布
|
||||||
|
*/
|
||||||
|
async function addTemplate(templateData: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
template: any;
|
||||||
|
}) {
|
||||||
|
console.log("添加模板:", templateData);
|
||||||
|
|
||||||
|
const canvasInstance = diagramCanvas.value as any;
|
||||||
|
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||||
|
console.error("没有可用的画布实例添加模板");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentData = canvasInstance.getDiagramData();
|
||||||
|
console.log("=== 当前图表组件数量:", currentData.parts.length);
|
||||||
|
|
||||||
|
// 生成唯一ID前缀
|
||||||
|
const idPrefix = `template-${Date.now()}-`;
|
||||||
|
|
||||||
|
if (templateData.template?.parts) {
|
||||||
|
// 获取视口中心位置
|
||||||
|
let viewportCenter = { x: 300, y: 200 };
|
||||||
|
try {
|
||||||
|
if (canvasInstance.getCanvasPosition && canvasInstance.getScale) {
|
||||||
|
const position = canvasInstance.getCanvasPosition();
|
||||||
|
const scale = canvasInstance.getScale();
|
||||||
|
const canvasContainer = canvasInstance.$el as HTMLElement;
|
||||||
|
|
||||||
|
if (canvasContainer) {
|
||||||
|
const viewportWidth = canvasContainer.clientWidth;
|
||||||
|
const viewportHeight = canvasContainer.clientHeight;
|
||||||
|
viewportCenter.x = (viewportWidth / 2 - position.x) / scale;
|
||||||
|
viewportCenter.y = (viewportHeight / 2 - position.y) / scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取视口中心位置时出错:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainPart = templateData.template.parts[0];
|
||||||
|
|
||||||
|
// 创建新组件
|
||||||
|
const newParts = await Promise.all(
|
||||||
|
templateData.template.parts.map(async (part: any) => {
|
||||||
|
const newPart = JSON.parse(JSON.stringify(part));
|
||||||
|
newPart.id = `${idPrefix}${part.id}`;
|
||||||
|
|
||||||
|
// 加载组件模块并获取能力页面
|
||||||
|
try {
|
||||||
|
const componentModule = await loadComponentModule(part.type);
|
||||||
|
if (
|
||||||
|
componentModule?.default &&
|
||||||
|
typeof componentModule.default.getCapabilities === "function"
|
||||||
|
) {
|
||||||
|
newPart.capsPage = componentModule.default.getCapabilities();
|
||||||
|
console.log(`加载模板组件${part.type}组件的能力页面成功`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`加载模板组件${part.type}的能力页面失败:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算新位置
|
||||||
|
if (typeof newPart.x === "number" && typeof newPart.y === "number") {
|
||||||
|
const relativeX = part.x - mainPart.x;
|
||||||
|
const relativeY = part.y - mainPart.y;
|
||||||
|
newPart.x = viewportCenter.x + relativeX;
|
||||||
|
newPart.y = viewportCenter.y + relativeY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPart;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
currentData.parts.push(...newParts);
|
||||||
|
|
||||||
|
// 处理连接关系
|
||||||
|
if (templateData.template.connections) {
|
||||||
|
const idMap: Record<string, string> = {};
|
||||||
|
templateData.template.parts.forEach((part: any) => {
|
||||||
|
idMap[part.id] = `${idPrefix}${part.id}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const newConnections = templateData.template.connections.map((conn: any) => {
|
||||||
|
if (Array.isArray(conn)) {
|
||||||
|
const [from, to, type, path] = conn;
|
||||||
|
const fromParts = from.split(":");
|
||||||
|
const toParts = to.split(":");
|
||||||
|
|
||||||
|
if (fromParts.length === 2 && toParts.length === 2) {
|
||||||
|
const fromComponentId = fromParts[0];
|
||||||
|
const fromPinId = fromParts[1];
|
||||||
|
const toComponentId = toParts[0];
|
||||||
|
const toPinId = toParts[1];
|
||||||
|
|
||||||
|
const newFrom = `${idMap[fromComponentId] || fromComponentId}:${fromPinId}`;
|
||||||
|
const newTo = `${idMap[toComponentId] || toComponentId}:${toPinId}`;
|
||||||
|
|
||||||
|
return [newFrom, newTo, type, path];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return conn;
|
||||||
|
});
|
||||||
|
|
||||||
|
currentData.connections.push(...newConnections);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||||
|
console.log("=== 更新图表数据完成,新组件数量:", currentData.parts.length);
|
||||||
|
|
||||||
|
return { success: true, message: `已添加 ${templateData.name} 模板` };
|
||||||
|
} else {
|
||||||
|
console.error("模板格式错误,缺少parts数组");
|
||||||
|
return { success: false, message: "模板格式错误" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除组件
|
||||||
|
*/
|
||||||
|
function deleteComponent(componentId: string) {
|
||||||
|
const canvasInstance = diagramCanvas.value as any;
|
||||||
|
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentData = canvasInstance.getDiagramData();
|
||||||
|
const component = currentData.parts.find((p: DiagramPart) => p.id === componentId);
|
||||||
|
|
||||||
|
if (!component) return;
|
||||||
|
|
||||||
|
const componentsToDelete: string[] = [componentId];
|
||||||
|
|
||||||
|
// 处理组件组
|
||||||
|
if (component.group && component.group !== "") {
|
||||||
|
const groupMembers = currentData.parts.filter(
|
||||||
|
(p: DiagramPart) => p.group === component.group && p.id !== componentId
|
||||||
|
);
|
||||||
|
componentsToDelete.push(...groupMembers.map((p: DiagramPart) => p.id));
|
||||||
|
console.log(`删除组件 ${componentId} 及其组 ${component.group} 中的 ${groupMembers.length} 个组件`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除组件
|
||||||
|
currentData.parts = currentData.parts.filter(
|
||||||
|
(p: DiagramPart) => !componentsToDelete.includes(p.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 删除相关连接
|
||||||
|
currentData.connections = currentData.connections.filter((connection: any) => {
|
||||||
|
for (const id of componentsToDelete) {
|
||||||
|
if (connection[0].startsWith(`${id}:`) || connection[1].startsWith(`${id}:`)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清除选中状态
|
||||||
|
if (selectedComponentId.value && componentsToDelete.includes(selectedComponentId.value)) {
|
||||||
|
selectedComponentId.value = null;
|
||||||
|
selectedComponentConfig.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选中组件
|
||||||
|
*/
|
||||||
|
async function selectComponent(componentData: DiagramPart | null) {
|
||||||
|
selectedComponentId.value = componentData ? componentData.id : null;
|
||||||
|
selectedComponentConfig.value = null;
|
||||||
|
|
||||||
|
if (componentData) {
|
||||||
|
const moduleRef = await loadComponentModule(componentData.type);
|
||||||
|
|
||||||
|
if (moduleRef) {
|
||||||
|
try {
|
||||||
|
const propConfigs: PropertyConfig[] = [];
|
||||||
|
const addedProps = new Set<string>();
|
||||||
|
|
||||||
|
// 从 getDefaultProps 方法获取默认配置
|
||||||
|
if (typeof moduleRef.getDefaultProps === "function") {
|
||||||
|
const defaultProps = moduleRef.getDefaultProps();
|
||||||
|
const defaultPropConfigs = generatePropsFromDefault(defaultProps);
|
||||||
|
defaultPropConfigs.forEach((config) => {
|
||||||
|
propConfigs.push(config);
|
||||||
|
addedProps.add(config.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加组件直接属性
|
||||||
|
const directPropConfigs = generatePropertyConfigs(componentData);
|
||||||
|
const newDirectProps = directPropConfigs.filter(
|
||||||
|
(config) => !addedProps.has(config.name)
|
||||||
|
);
|
||||||
|
propConfigs.push(...newDirectProps);
|
||||||
|
|
||||||
|
// 添加 attrs 中的属性
|
||||||
|
if (componentData.attrs) {
|
||||||
|
const attrPropConfigs = generatePropsFromAttrs(componentData.attrs);
|
||||||
|
attrPropConfigs.forEach((attrConfig) => {
|
||||||
|
const existingIndex = propConfigs.findIndex(
|
||||||
|
(p) => p.name === attrConfig.name
|
||||||
|
);
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
propConfigs[existingIndex] = attrConfig;
|
||||||
|
} else {
|
||||||
|
propConfigs.push(attrConfig);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedComponentConfig.value = { props: propConfigs };
|
||||||
|
console.log(`Built config for ${componentData.type}:`, selectedComponentConfig.value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error building config for ${componentData.type}:`, error);
|
||||||
|
selectedComponentConfig.value = { props: [] };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Module for component ${componentData.type} not found.`);
|
||||||
|
selectedComponentConfig.value = { props: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新组件属性
|
||||||
|
*/
|
||||||
|
function updateComponentProp(componentId: string, propName: string, value: any) {
|
||||||
|
const canvasInstance = diagramCanvas.value as any;
|
||||||
|
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||||
|
console.error("没有可用的画布实例进行属性更新");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查值格式
|
||||||
|
if (value !== null && typeof value === "object" && "value" in value) {
|
||||||
|
value = value.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentData = canvasInstance.getDiagramData();
|
||||||
|
const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
|
||||||
|
|
||||||
|
if (part) {
|
||||||
|
if (propName in part) {
|
||||||
|
(part as any)[propName] = value;
|
||||||
|
} else {
|
||||||
|
if (!part.attrs) {
|
||||||
|
part.attrs = {};
|
||||||
|
}
|
||||||
|
part.attrs[propName] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||||
|
console.log(`更新组件${componentId}的属性${propName}为:`, value, typeof value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新组件直接属性
|
||||||
|
*/
|
||||||
|
function updateComponentDirectProp(componentId: string, propName: string, value: any) {
|
||||||
|
const canvasInstance = diagramCanvas.value as any;
|
||||||
|
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||||
|
console.error("没有可用的画布实例进行属性更新");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentData = canvasInstance.getDiagramData();
|
||||||
|
const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
|
||||||
|
|
||||||
|
if (part) {
|
||||||
|
(part as any)[propName] = value;
|
||||||
|
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||||
|
console.log(`更新组件${componentId}的直接属性${propName}为:`, value, typeof value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动组件
|
||||||
|
*/
|
||||||
|
function moveComponent(moveData: { id: string; x: number; y: number }) {
|
||||||
|
const canvasInstance = diagramCanvas.value as any;
|
||||||
|
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentData = canvasInstance.getDiagramData();
|
||||||
|
const part = currentData.parts.find((p: DiagramPart) => p.id === moveData.id);
|
||||||
|
if (part) {
|
||||||
|
part.x = moveData.x;
|
||||||
|
part.y = moveData.y;
|
||||||
|
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置画布实例引用
|
||||||
|
*/
|
||||||
|
function setCanvasRef(canvasRef: any) {
|
||||||
|
diagramCanvas.value = canvasRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置组件DOM引用
|
||||||
|
*/
|
||||||
|
function setComponentRef(componentId: string, el: any) {
|
||||||
|
if (el) {
|
||||||
|
componentRefs.value[componentId] = el;
|
||||||
|
} else {
|
||||||
|
delete componentRefs.value[componentId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取组件DOM引用
|
||||||
|
*/
|
||||||
|
function getComponentRef(componentId: string) {
|
||||||
|
return componentRefs.value[componentId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前图表数据
|
||||||
|
*/
|
||||||
|
function getDiagramData() {
|
||||||
|
const canvasInstance = diagramCanvas.value;
|
||||||
|
if (canvasInstance && canvasInstance.getDiagramData) {
|
||||||
|
return canvasInstance.getDiagramData();
|
||||||
|
}
|
||||||
|
return { parts: [], connections: [], version: 1, author: "admin", editor: "me" };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新图表数据
|
||||||
|
*/
|
||||||
|
function updateDiagramData(data: any) {
|
||||||
|
const canvasInstance = diagramCanvas.value;
|
||||||
|
if (canvasInstance && canvasInstance.updateDiagramDataDirectly) {
|
||||||
|
canvasInstance.updateDiagramDataDirectly(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取画布位置和缩放信息
|
||||||
|
*/
|
||||||
|
function getCanvasInfo() {
|
||||||
|
const canvasInstance = diagramCanvas.value;
|
||||||
|
if (!canvasInstance) return { position: { x: 0, y: 0 }, scale: 1 };
|
||||||
|
|
||||||
|
const position = canvasInstance.getCanvasPosition ? canvasInstance.getCanvasPosition() : { x: 0, y: 0 };
|
||||||
|
const scale = canvasInstance.getScale ? canvasInstance.getScale() : 1;
|
||||||
|
|
||||||
|
return { position, scale };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示通知
|
||||||
|
*/
|
||||||
|
function showToast(message: string, type: "success" | "error" | "info" = "info") {
|
||||||
|
const canvasInstance = diagramCanvas.value;
|
||||||
|
if (canvasInstance && canvasInstance.showToast) {
|
||||||
|
canvasInstance.showToast(message, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取组件定义
|
||||||
|
*/
|
||||||
|
function getComponentDefinition(type: string) {
|
||||||
|
const module = componentModules.value[type];
|
||||||
|
|
||||||
|
if (!module) {
|
||||||
|
console.warn(`No module found for component type: ${type}`);
|
||||||
|
// 尝试异步加载组件模块
|
||||||
|
loadComponentModule(type);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保我们返回一个有效的组件定义
|
||||||
|
if (module.default) {
|
||||||
|
return module.default;
|
||||||
|
} else if (module.__esModule && module.default) {
|
||||||
|
// 有时 Vue 的动态导入会将默认导出包装在 __esModule 属性下
|
||||||
|
return module.default;
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
`Module for ${type} found but default export is missing`,
|
||||||
|
module,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 准备组件属性
|
||||||
|
*/
|
||||||
|
function prepareComponentProps(
|
||||||
|
attrs: Record<string, any>,
|
||||||
|
componentId?: string,
|
||||||
|
): Record<string, any> {
|
||||||
|
const result: Record<string, any> = { ...attrs };
|
||||||
|
if (componentId) {
|
||||||
|
result.componentId = componentId;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化组件管理器
|
||||||
|
*/
|
||||||
|
async function initialize() {
|
||||||
|
const canvasInstance = diagramCanvas.value as any;
|
||||||
|
if (canvasInstance?.getDiagramData) {
|
||||||
|
const diagramData = canvasInstance.getDiagramData();
|
||||||
|
|
||||||
|
// 收集所有组件类型
|
||||||
|
const componentTypes = new Set<string>();
|
||||||
|
diagramData.parts.forEach((part: DiagramPart) => {
|
||||||
|
componentTypes.add(part.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 预加载组件模块
|
||||||
|
await preloadComponentModules(Array.from(componentTypes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
componentModules,
|
||||||
|
selectedComponentId,
|
||||||
|
selectedComponentData,
|
||||||
|
selectedComponentConfig,
|
||||||
|
componentRefs,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
loadComponentModule,
|
||||||
|
preloadComponentModules,
|
||||||
|
addComponent,
|
||||||
|
addTemplate,
|
||||||
|
deleteComponent,
|
||||||
|
selectComponent,
|
||||||
|
updateComponentProp,
|
||||||
|
updateComponentDirectProp,
|
||||||
|
moveComponent,
|
||||||
|
setCanvasRef,
|
||||||
|
setComponentRef,
|
||||||
|
getComponentRef,
|
||||||
|
getDiagramData,
|
||||||
|
updateDiagramData,
|
||||||
|
getCanvasInfo,
|
||||||
|
showToast,
|
||||||
|
getComponentDefinition,
|
||||||
|
prepareComponentProps,
|
||||||
|
initialize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export { useProvideComponentManager, useComponentManager };
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { JSX } from "vue/jsx-runtime";
|
|
||||||
|
|
||||||
// 定义 diagram.json 的类型结构
|
// 定义 diagram.json 的类型结构
|
||||||
export interface DiagramData {
|
export interface DiagramData {
|
||||||
version: number;
|
version: number;
|
||||||
@@ -17,7 +15,6 @@ export interface DiagramPart {
|
|||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
attrs: Record<string, any>;
|
attrs: Record<string, any>;
|
||||||
capsPage?: JSX.Element;
|
|
||||||
rotate: number;
|
rotate: number;
|
||||||
group: string;
|
group: string;
|
||||||
positionlock: boolean;
|
positionlock: boolean;
|
||||||