热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

计算机组成原理,硬件模拟语言,Vivado仿真

工具链-ChiselGTKWave本次综合实验中我采用了UCBerkeley的Chisel3作为硬件描述的工具。Chisel相比Verilog,有如下优势

工具链 - Chisel + GTKWave

本次综合实验中我采用了 UC Berkeley 的 Chisel3 作为硬件描述的工具。

Chisel 相比 Verilog,有如下优势:


  1. Verilog 在设计之初只是硬件模拟语言,其中很多语法不适合硬件综合;Chisel 对此有仔细区分
  2. Verilog 的一些语法概念在如何映射到硬件实现方面是非常不直观的,或一不小心就会导致非常低效的电路结构(比如锁存器)
  3. Verilog 缺少现代编程语言的机制,诸如 OOP/FP,类型推导和反射
  4. Verilog 缺乏规模化构建的机制,比如「成批链接」I/O 端口的语法和好用的 function 机制
  5. Verilog 的可扩展性大大弱于 Scala

Chisel 是基于 Scala 语言设计构建的。 Scala 是一个以 Java 为基础的「Scalable Language」,在执行前会被翻译为 Class 文件,最终在 JVM 中运行。

Chisel 经过 Firrtl 中间层后,会被翻译为 Verilog 代码,然后再用 Vivado 导入综合/实现/烧写。


配置工具链

主要参考 https://github.com/ucb-bar/chisel-tutorial.git。

在 ArchLinux 下只需要 pacman -Sy sbt scala 安装 sbt 构建工具和 scala; Chisel 的相关代码会在第一次使用引用 Chisel 的 sbt 项目时由 sbt 经由 Maven 构建系统下载。下载可能需要网络加速服务,或切换镜像(比较困难,因为硬写在了 sbtjar 包中了),因为 Maven 主镜像非常缓慢。


从模板开始

为了方便大家熟悉 Chisel,Chisel 项目准备了 chisel-template 项目。用 git clone https://github.com/freechipsproject/chisel-template 即可 clone 到本地。
http://www.biyezuopin.vip
目录结构大致如下所示:

.
├── build.sbt # sbt 构建文件
├── project # sbt 生成的文件,无需关心
│ ├── build.properties
│ └── plugins.sbt
├── README.md # template 的介绍
├── scalastyle-config.xml
├── scalastyle-test-config.xml
└── src # 源码目录├── main # 一般用来放 Design Sources│ └── scala│ └── gcd│ └── GCD.scala└── test # 一般用来放 Testbench└── scala└── gcd├── GCDMain.scala└── GCDUnitTest.scala8 directories, 9 files

这个目录结构和 sbt 的构建有关系,更详细的介绍请参见 sbt 的手册,大概是 src 下面每一个目录都是一个子的 sbt 构建单位(项目),可以写独立的 build.sbt 的那种。


从哪里获得帮助

我主要用到了四个方面的帮助


  1. 关于 Scala 的:参考 Spec,谷歌和 Scala 自己的 doc
  2. 关于 Chisel 的:
    1. 一般的用法直接参见 https://github.com/freechipsproject/chisel3.wiki 和 https://github.com/ucb-bar/chisel-tutorial
    2. 特别的用法要去看源码(在 https://github.com/freechipsproject/chisel3/)和 API 文档(在 https://chisel.eecs.berkeley.edu/api/3.0.0/chisel3/ )
      1. 比如说 ListLookup/BitPad 和某些 deprecated 的用法(比如支持 BitPad 的类似 MuxCase 的调用 MuxLookup)
  3. 关于 sbt 的:参考 sbt 的文档即可
  4. 代码风格和复用的设计思路:RISCV Mini 项目( freechipsproject @ Github)

调试

一般采用 Chisel 的 PeekPokeTester 的 poke(置数)和 step(前进 n 个时钟周期),在运行的时候加上 --generate-vcd-output on 选项,然后用 GTKWave 打开生成的 vcd 文件。此法调试和 Vivado 体验近似。


[NEW!] 我的源码结构

.
├── ALU.anno.json
├── ALU.fir
├── ALU.v
├── BlackBoxCGROM.v
├── black_box_verilog_files.f
├── build.sbt
├── CPU.anno.json
├── CPU.fir
├── CPU.v
├── DDU.anno.json
├── DDU.fir
├── DDU.v
├── project
├── scalastyle-config.xml
├── scalastyle-test-config.xml
├── src
│ ├── main
│ │ └── scala # 除新加入的 .scala,剩余基本为原 CPU 的代码
│ │ ├── ALU.scala
│ │ ├── Control.scala
│ │ ├── CPU.scala # 加入了几条新指令
│ │ ├── Datapath.scala # 加入了几条新指令
│ │ ├── DDU.scala
│ │ ├── Instr.scala
│ │ ├── Mem.scala
│ │ ├── MMap.scala # 进行程序存储器,UART Cell 和 Textmode 显存的内存地址转换
│ │ ├── RegFile.scala
│ │ ├── TestField.scala
│ │ └── VGA # VGA 相关代码
│ │ ├── Counter.scala # VGACounter 类,用来便捷构建 reset + carry 的计数器
│ │ ├── VGACore.scala # VGA 核心模块
│ │ └── VGA.scala # 字模 ROM <&#61;&#61;> VGACore <&#61;&#61;> 显存
│ └── test
│ ├── resources # 资源文件
│ │ ├── BlackBoxCGROM.1.v # 使用 Block RAM IP 核的 CGROM Blackbox 模型
│ │ ├── BlackBoxCGROM.v # 使用 Dist RAM IP 核的 CGROM Blackbox 模型
│ │ ├── build.sh # 根据 util.c 构建内存 COE&#xff08;仅有效载荷部分&#xff09;的构建脚本
│ │ ├── inst_rom.coe # Deprecated
│ │ ├── inst_rom.S # Deprecated
│ │ ├── linker.ld # *链接器脚本
│ │ ├── main.asm # Deprecated&#xff0c;下同
│ │ ├── main_prog.asm
│ │ ├── main_prog.txt
│ │ ├── main.S
│ │ ├── mips1.asm
│ │ ├── NOTE.md
│ │ ├── start.o
│ │ ├── start.S # 用来设置栈地址的汇编代码&#xff0c;必须保证紧跟 main_loop# &#xff08;如果把 main_loop 写在 util.c 的最前面&#xff0c;一般是可以的&#xff09;# 尽管这样&#xff0c;还是应该想办法把这里的行为改进。# &#xff08;主要的困难&#xff1a;在这里添加 j main_loop 会生成额外的 .pic 段&#xff0c;为什么&#xff1f;&#xff09;
│ │ ├── test_jal_jr.asm # 用来测试 jal 和 jr 指令是否工作正常的汇编代码
│ │ ├── test_jal_jr.txt
│ │ ├── util.1.c
│ │ ├── util.c # util.c&#xff0c;CPU 运行的主程序
│ │ ├── util.c.S
│ │ ├── util.elf
│ │ ├── util_final.bin # 进行 objcopy&#xff0c;剔除所有符号后的 util_final.elf
│ │ ├── util_final.elf # 和 start.S 一同编译链接后的 util.c&#xff0c;详情参见 build.sh
│ │ ├── util_final.elf.objdump.d # 反汇编后的 util_final.elf
│ │ ├── util.o
│ │ └── xxd_c4_util_final_bin.txt # 利用 xxd 输出二进制后的 util_final.bin
│ └── scala
│ ├── ALU.scala
│ ├── CPU.scala
│ ├── DDU.scala
│ ├── RegFile.scala
│ ├── TestField.scala
│ └── VGA.scala # VGA Testbench
├── target
└── test_run_dir

构建命令

http://www.biyezuopin.vip
例&#xff1a;在与 build.sbt 同层的目录下运行 sbt 后&#xff1a;


  • test:runMain ddu.DDUGen 用于生成 DDU 的 Verilog 代码
  • test:runMain ddu.DDUTest --generate-vcd-output on 用于运行 DDU &#43; CPU Testbench 并生成 VCD 波形文件&#xff0c;可以用 GTKWave 打开
  • test:runMain multicpu.CPUTest --generate-vcd-output on 运行 CPU Testbench
  • test:run 可以查看所有可以运行的 main classes

下面为 test:run 的示例&#xff1a;

sbt:MultiCycleCpu> test:run
[warn] Multiple main classes detected. Run &#39;show discoveredMainClasses&#39; to see the listMultiple main classes detected, select one to run:[1] ddu.DDUGen[2] ddu.DDUTest[3] gcd.GCDMain[4] gcd.GCDRepl[5] multicpu.ALUGen[6] multicpu.ALUTest[7] multicpu.CPUGen[8] multicpu.CPUTest[9] multicpu.RegFileGen[10] multicpu.RegFileTest[11] testfield.TestFieldGen[12] testfield.TestFieldTestEnter number:

构建生成的内容一般在 test_run_dir&#xff0c;ALUGen/DDUGen 等生成的一般在项目主目录下。


Chisel 的问题


  1. 不支持内存初值
    • 因为 Chisel 的硬件描述完全面向综合&#xff0c;而可以赋初值的内存本质上是个高级功能
  2. 对 IP 核只能用 Verilog Blackbox 封装起来&#xff0c;到 Vivado Simulator 仿真
    • 不是个大问题&#xff0c;但是还是诸多不便&#xff08;如果必须用 IP 核的话&#xff09;
  3. Chisel 的 poke&#xff08;置数&#xff09;操作总是慢半个时钟周期&#xff0c;以及 iotester 执行效率和 Vivado Simulator 一样感人
    • 没找到解决方法&#xff0c;并且 RISCV-Mini 项目为了解决内存模拟的问题用的 Verilator C&#43;&#43; 写的模拟插件

Vivado 项目和 top.v

DDU 缺乏时钟分频&#xff0c;所以在 Vivado 中写一个 top.v 用来实例化 Clocking Wizard IP 核&#xff0c;同时把 xdc 处理好。


MIPS C &#43; 汇编混合编程

利用 C 语言可以极大的简化计算地址和想指令的苦恼。

本次设计采用 mipsel-linux-gnu-gcc 进行编译。在 Baremetal 环境编译需要注意以下事项&#xff1a;


  • 开启 -nostdlib -ffreestanding -static 选项
  • 因为我没有实现流水线&#xff0c;所以 MIPS 的分支延迟指令会有问题
    • 利用 -fno-delayed-branch -Wa,-O0 来禁用之&#xff08;参见 这篇 StackOverflow&#xff09;
  • 在链接的时候要用自定义的链接脚本&#xff08;linker.ld&#xff09;&#xff0c;并且在链接后用 objcopy 把符号裁掉
    • 这部分详见代码和 build.sh

逻辑设计

扩展的指令&#xff1a;


  • JALJR - 用来实现 C 的过程调用和返回
  • LUI - C 编译器经常使用 LUI 和 LW/SW 指令用来实现装入/写出
  • ADDIU SLTIU 等带 U 的指令 - C 编译器经常使用此版本&#xff0c;避免异常&#xff08;虽然我也没实现&#xff09;
  • SLL - C 编译器利用移位和加法来进行乘以常数的运算


拓扑关系&#xff1a;

UART RX 通过内存映射 IO 的形式&#xff0c;连接到 CPU&#xff1b;CPU 轮询 UART Cell 地址&#xff08;0xFF00&#xff09;&#xff0c;检测到数据有效后&#xff0c;取出数据并且写入显存&#xff08;0x3000~0x4F40&#xff09;&#xff1b;显存和 CGROM&#xff08;Character Generation ROM&#xff09;一起来显示对应字母。




  1. UART Rx 通过检测下降沿开始移位&#xff0c;在 CLK_PER_BITS/2 时间后检测是否仍为低电平&#xff08;此时应该是 Start Bit 的一半&#xff09;

  2. 如果仍为低电平&#xff0c;则开始接收&#xff0c;否则认为是错误数据&#xff08;毛刺等&#xff09;&#xff0c;放弃&#xff0c;否则进入 &#xff08;3&#xff09;

  3. 每隔 CLK_PER_BITS 采样&#xff0c;共八次&#xff0c;最后再等待 CLK_PER_BITS/2 后进入等待 CPU 取走的状态

    • 在此状态&#xff0c;Valid 为高&#xff1b;不停检测&#xff0c;如果 Ready 为高&#xff0c;则进入可以接收下一个字节的模式&#xff08;1&#xff09;

Memory Cell 如下&#xff1a;

// &#43;----------------------------------------------&#43;
// | 31 | 30 | 29 ... 8 | 7 ... 0 |
// | READY | VALID | don&#39;t care | Serial data |
// &#43;----------------------------------------------&#43;

核心代码&#xff08;相对于 Lab5 的增加&#xff09;


C 主程序 - util.c

int cursor_h &#61; 0;
int cursor_v &#61; 0;void putchar(int ch);
int getchar();
void clear_scr();
void backspace();void main_loop() {cursor_h &#61; 0;cursor_v &#61; 0;int ch;//clear_scr();putchar(&#39;>&#39; | 0xF00);for (;;) {// putchar(&#39;_&#39; | 0x700);ch &#61; getchar();if (ch &#61;&#61; &#39;\n&#39; || ch &#61;&#61; &#39;\r&#39;) {cursor_h &#61; 0;cursor_v &#43;&#61; 1;} else if (ch &#61;&#61; 8) {// Backspaceif (cursor_h &#61;&#61; 0) {cursor_h &#61; 24;cursor_v -&#61; 1;putchar(&#39; &#39; | 0xF00);cursor_h &#61; 24;cursor_v -&#61; 1;} else {cursor_h -&#61; 1;putchar(&#39; &#39; | 0xF00);cursor_h -&#61; 1;}} else if (ch &#61;&#61; 27) {for (int i &#61; 0; i < 4000; i&#43;&#43;) {putchar(&#39; &#39; | 0xF00);}cursor_h &#61; 0;cursor_v &#61; 0;} else {putchar(ch | 0xF00);}}
}void putchar(int ch) {volatile int *vram_offset &#61; 0x3000;vram_offset[cursor_v * 80 &#43; cursor_h] &#61; ch;cursor_h&#43;&#43;;if (cursor_h &#61;&#61; 80) {cursor_h &#61; 0;cursor_v&#43;&#43;;if (cursor_v &#61;&#61; 25) {cursor_v &#61; 0;}}
}int getchar() {// uart offsetvolatile int *uart_offset &#61; 0xFF00;int valid_mask &#61; 0x40000000;int byte_mask &#61; 0x000000ff;while ((*uart_offset & valid_mask) &#61;&#61; 0) {// Ready; do nothing//__asm__("nop");}int ret &#61; byte_mask & *uart_offset;*uart_offset &#61; (1 << 31);return ret;
}

VGACore.scala

可以看到&#xff0c;此处利用 object VGAConfig 有效减少硬编码&#xff0c;实现优雅的参数化。

// Thanks to iBug for its VGA Sources.package vgaimport chisel3._
import chisel3.util._/* hd: Horizontal Visible Area* hf: Horizontal Front Porch* hs: Horizontal Sync Pulse* hb: Horizontal Back Porch* vd: Vertical Visible Area* vf: Vertical Front Porch* vs: Vertical Sync Pulse* vb: Vertical Back Porch*/object VGAConfig {val config &#61; Map(// | hd | hf | hs | hb | vd | vf | vs | vb |"640x480&#64;60" -> List( 640 , 16 , 96 , 48 , 480 , 10 , 2 , 31 ),// Not widely supported, at 85Hz"720x400&#64;85" -> List( 720 , 36 , 72 , 108 , 400 , 1 , 3 , 42 ),"720x400&#64;70" -> List( 720 , 15 , 108 , 51 , 400 , 11 , 2 , 32 ),"800x600&#64;60" -> List( 800 , 40 , 128 , 88 , 600 , 1 , 4 , 23 ),"800x600&#64;72" -> List( 800 , 56 , 120 , 64 , 600 , 37 , 6 , 23 ),"1024x768&#64;60" -> List( 1024 , 24 , 136 , 160 , 768 , 3 , 6 , 29 ))val refresh_freq &#61; Map("640x480&#64;60" -> 25175000, // 25.175 MHz Pixel Freq"720x400&#64;85" -> 35500000, // 35.500 MHz Pixel Freq"720x400&#64;70" -> 28322000,"800x600&#64;60" -> 40000000, // 40.000 MHz Pixel Freq"800x600&#64;72" -> 50000000, // 50.000 MHz Pixel Freq"1024x768&#64;60" -> 65000000 // 65.000 MHz Pixel Freq)val mode_selected &#61; "720x400&#64;70"
}class VGASig extends Bundle {val r &#61; Output(UInt(4.W))val g &#61; Output(UInt(4.W))val b &#61; Output(UInt(4.W))val hsync &#61; Output(Bool())val vsync &#61; Output(Bool())
}class VGACore extends Module {val io &#61; IO(new Bundle {val row &#61; Output(UInt(32.W))val col &#61; Output(UInt(32.W))val ready &#61; Output(Bool())// Indicate that in next cycle// ready will assertval pre_ready &#61; Output(Bool())val color &#61; Input(UInt(12.W))val sig &#61; new VGASig()})val cfg_list &#61; VGAConfig.config(VGAConfig.mode_selected)val hd &#61; cfg_list(0).U(32.W) // Must have this, or compr will be buggyval hf &#61; cfg_list(1).U(32.W) // for h_tick >&#61; hs &#43; hb it&#39;ll do val hs &#61; cfg_list(2).U(32.W)val hb &#61; cfg_list(3).U(32.W)val vd &#61; cfg_list(4).U(32.W)val vf &#61; cfg_list(5).U(32.W)val vs &#61; cfg_list(6).U(32.W)val vb &#61; cfg_list(7).U(32.W)val h_tick &#61; RegInit(0.U(32.W))val v_tick &#61; RegInit(0.U(32.W))val h_max &#61; hs &#43; hb &#43; hd &#43; hfval v_max &#61; vs &#43; vb &#43; vd &#43; vf// Layered counterwhen (h_tick >&#61; h_max - 1.U) {h_tick :&#61; 0.Uwhen (v_tick >&#61; v_max - 1.U) {v_tick :&#61; 0.U} .otherwise {v_tick :&#61; v_tick &#43; 1.U}} .otherwise {h_tick :&#61; h_tick &#43; 1.U}// 0 ~ hs&#43;hb-1 &#61; total hs&#43;hb cyclesio.ready :&#61; (h_tick >&#61; hs &#43; hb) && (h_tick < hs &#43; hb &#43; hd) && (v_tick >&#61; vs &#43; vb) && (v_tick < vs &#43; vb &#43; vd)io.pre_ready :&#61; (h_tick >&#61; hs &#43; hb - 1.U) && (h_tick < hs &#43; hb &#43; hd - 1.U) && (v_tick >&#61; vs &#43; vb - 1.U) && (v_tick < vs &#43; vb &#43; vd - 1.U)io.sig.r :&#61; Mux(io.ready, io.color(3,0), 0.U)io.sig.g :&#61; Mux(io.ready, io.color(7,4), 0.U)io.sig.b :&#61; Mux(io.ready, io.color(11,8), 0.U)io.col :&#61; h_tick - hs - hb // BUG!!io.row :&#61; v_tick - vs - vbio.sig.hsync :&#61; h_tick >&#61; hsio.sig.vsync :&#61; v_tick >&#61; vs
}

内存映射模块 MMap.scala

package multicpuimport chisel3._
import chisel3.util._import vga._// // Datapath perspective
// class MemPort extends Bundle {
// val mem_rdata &#61; Input(UInt(32.W))
// val mem_addr &#61; Output(UInt(32.W))
// val mem_wdata &#61; Output(UInt(32.W))
// val mem_wen &#61; Output(Bool())// // Extra reading port for debugging
// val mem_addr2 &#61; Output(UInt(32.W))
// val mem_rdata2 &#61; Input(UInt(32.W))
// }class MemCellPort extends Bundle {val mem_rdata &#61; Input(UInt(32.W))val mem_wdata &#61; Output(UInt(32.W))val mem_wen &#61; Output(Bool())
}class VRamPort extends Bundle {val mem_addr &#61; Output(UInt(32.W))val mem_wdata &#61; Output(UInt(16.W))val mem_wen &#61; Output(Bool())
}
// 0 ~ 3FF(1023) : Prog Mem, 1024 bytes (as 128 Words)
// 3000(12288) ~ 4F40(20288) : Disp Mem, 80*25 words (but upper 2 byte is always zero)
// FF00(65280) : UART Mem
class MMap extends Module {val io &#61; IO(new Bundle {// MMap -> Datapathval mmap_port &#61; Flipped(new MemPort)// Mem -> MMapval mem_port &#61; new MemPort// Uart -> MMapval uart_port &#61; new MemCellPort// VRam -> MMapval vram_port &#61; new VRamPort})val addr_in_mem &#61; ((io.mmap_port.mem_addr >&#61; 0.U) && (io.mmap_port.mem_addr < 1024.U))val addr_in_uart &#61; (io.mmap_port.mem_addr &#61;&#61;&#61; 65280.U)val addr_in_vram &#61; (io.mmap_port.mem_addr >&#61; 12288.U) && (io.mmap_port.mem_addr < 20289.U)io.mmap_port.mem_rdata :&#61; MuxCase(0.U, Seq(addr_in_mem -> io.mem_port.mem_rdata,addr_in_uart -> io.uart_port.mem_rdata,addr_in_vram -> 0.U))//io.mem_port.mem_rdataio.mem_port.mem_addr :&#61; io.mmap_port.mem_addrio.mem_port.mem_wdata :&#61; io.mmap_port.mem_wdataio.mem_port.mem_wen :&#61; (io.mmap_port.mem_wen && addr_in_mem)io.mem_port.mem_addr2 :&#61; io.mmap_port.mem_addr2io.mmap_port.mem_rdata2 :&#61; io.mem_port.mem_rdata2//io.uart_port.mem_rdataio.uart_port.mem_wdata :&#61; io.mmap_port.mem_wdataio.uart_port.mem_wen :&#61; (io.mmap_port.mem_wen && addr_in_uart)io.vram_port.mem_addr :&#61; (io.mmap_port.mem_addr - 12288.U) >> 2io.vram_port.mem_wdata :&#61; io.mmap_port.mem_wdata(15,0)io.vram_port.mem_wen :&#61; (io.mmap_port.mem_wen && addr_in_vram)
}

其余未尽事宜请参见压缩包内源码。


资源占用


仿真结果

上面是全部导到 Vivado 后的仿真结果。限于篇幅&#xff0c;下面 VideoSys 和 CPU Sig 没有截出。


下载结果

请参见同压缩包下的视频&#xff0c;非常清晰的展现了功能。


实验总结


注意事项


  1. Vivado 生成某些查找表结构的时候非常慢&#xff08;至少 O(n^3)&#xff09;&#xff1b;诊断这种问题的时候&#xff0c;就应该尝试注释代码 &#43; 记录时间&#xff0c;会得到很直观的认识。
    • 然后就要用 IP 核&#xff08;如 Dist/Block ROM&#xff09;替换&#xff0c;或者单独拎出来进行 Per Module Synthesis&#xff08;可以保存综合结果&#xff09;&#xff1b;详情可以参见《Vivado 从此开始》一书。

推荐阅读
  • 2020年9月15日,Oracle正式发布了最新的JDK 15版本。本次更新带来了许多新特性,包括隐藏类、EdDSA签名算法、模式匹配、记录类、封闭类和文本块等。 ... [详细]
  • 本文详细介绍了如何在 Ubuntu 14.04 系统上搭建仅使用 CPU 的 Caffe 深度学习框架,包括环境准备、依赖安装及编译过程。 ... [详细]
  • 2023年1月28日网络安全热点
    涵盖最新的网络安全动态,包括OpenSSH和WordPress的安全更新、VirtualBox提权漏洞、以及谷歌推出的新证书验证机制等内容。 ... [详细]
  • 页面预渲染适用于主要包含静态内容的页面。对于依赖大量API调用的动态页面,建议采用SSR(服务器端渲染),如Nuxt等框架。更多优化策略可参见:https://github.com/HaoChuan9421/vue-cli3-optimization ... [详细]
  • 本文旨在探讨Swift中的Closure与Objective-C中的Block之间的区别与联系,通过定义、使用方式以及外部变量捕获等方面的比较,帮助开发者更好地理解这两种机制的特点及应用场景。 ... [详细]
  • WebBenchmark:强大的Web API性能测试工具
    本文介绍了一款名为WebBenchmark的Web API性能测试工具,该工具不仅支持HTTP和HTTPS服务的测试,还提供了丰富的功能来帮助开发者进行高效的性能评估。 ... [详细]
  • 本文探讨了Python类型注解使用率低下的原因,主要归结于历史背景和投资回报率(ROI)的考量。文章不仅分析了类型注解的实际效用,还回顾了Python类型注解的发展历程。 ... [详细]
  • 本文详细介绍了Android系统的四层架构,包括应用程序层、应用框架层、库与Android运行时层以及Linux内核层,并提供了如何关闭Android系统的步骤。 ... [详细]
  • 在Java开发中,保护代码安全是一个重要的课题。由于Java字节码容易被反编译,因此使用代码混淆工具如ProGuard变得尤为重要。本文将详细介绍如何使用ProGuard进行代码混淆,以及其基本原理和常见问题。 ... [详细]
  • 解决Jenkins编译过程中ERROR: Failed to Parse POMs的问题
    在使用Jenkins进行自动化构建时,有时会遇到“ERROR: Failed to parse POMs”的错误。本文将详细分析该问题的原因,并提供有效的解决方案。 ... [详细]
  • 目录预备知识导包构建数据集神经网络结构训练测试精度可视化计算模型精度损失可视化输出网络结构信息训练神经网络定义参数载入数据载入神经网络结构、损失及优化训练及测试损失、精度可视化qu ... [详细]
  • 本文详细介绍了如何在 Linux 系统上安装 JDK 1.8、MySQL 和 Redis,并提供了相应的环境配置和验证步骤。 ... [详细]
  • 本文回顾了作者在求职阿里和腾讯实习生过程中,从最初的迷茫到最后成功获得Offer的心路历程。文中不仅分享了个人的面试经历,还提供了宝贵的面试准备建议和技巧。 ... [详细]
  • 默认情况下,Git 使用 Nano 编辑器进行提交信息的编辑,但如果您更喜欢使用 Vim,可以通过简单的配置更改来实现这一变化。本文将指导您如何通过修改全局配置文件来设置 Vim 作为默认的 Git 提交编辑器。 ... [详细]
  • 我的读书清单(持续更新)201705311.《一千零一夜》2006(四五年级)2.《中华上下五千年》2008(初一)3.《鲁滨孙漂流记》2008(初二)4.《钢铁是怎样炼成的》20 ... [详细]
author-avatar
佳鈺佳琴欣怡
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有