使用 C++23 从零实现 RISC-V 模拟器(2):内存和总线

news/发布时间2024/5/15 2:30:10

👉🏻 文章汇总「从零实现模拟器、操作系统、数据库、编译器…」:https://okaitserrj.feishu.cn/docx/R4tCdkEbsoFGnuxbho4cgW2Yntc

内存和总线

上一部分将内存全部放到了 CPU 里面,总线的概念是隐含着的。这一部分将内存拆分出来,再引入总线的概念,CPU 通过总线连接内存。

完整代码可以查看这个分支:https://github.com/weijiew/crvemu/tree/lab2-memory

实际上可以直接看代码,文章作为补充,这部分内容很简单。后续内容并没有完全将代码的所有修改列出来,建议快速浏览下面的内容有一个整体的认识后再结合代码学习。

1. CPU、内存和总线之间的关系

下面展示了 CPU、内存和总线之间的关系:

                    +-----+| CPU |+-----+|+-------+-------+|       |       |控制总线  数据总线 地址总线|       |       |v       v       v+--------------------------------+|             总线               |+--------------------------------+|       |v       v+-----+       +-----+| 内存 |<---->| I/O  |+-----+       +-----+

在这个简化模型中:

  • CPU:作为计算中心,它执行程序代码,处理数据。

  • 总线:分为控制总线、数据总线和地址总线,连接 CPU 和内存以及 I/O 设备。

    • 控制总线:CPU 通过它发送控制信号,如读写请求。
    • 数据总线:实际数据在 CPU 和内存之间的传输通道。
    • 地址总线:指定数据来源或目标位置的内存地址。
  • 内存:存储指令和数据,供 CPU 直接访问。

这个表示强调了 CPU 通过不同类型的总线与内存进行通信的方式,体现了它们之间的关系。

2. 内存 Dram

上一节已经实现的部分中内存是放在了 CPU 中,下面要将内存单独拆分出来作为一个类名为 Dram 的类(“Dynamic Random Access Memory” 动态随机访问内存)。随后再实现一个类名为 bus 的类来表示总线, CPU 通过总线 bus 读写内存 Dram ,接下来先实现 Dram 。

在实现之前要先定义几个参数来表示从内存中哪里开始读取,在 Qemu 中不是从物理地址 0 开始读取的,而是定义了一个具体的数字,下面的内容会详细讲解。

2.1 参数

上面的代码中涉及到了一些参数还没有定义,接下来定义一下参数。

// param.cpp
#include <cstddef> // 引入定义 std::size_t 的头文件// 定义DRAM的基地址
constexpr std::size_t DRAM_BASE = 0x8000'0000;// 定义DRAM的大小,128MB
constexpr std::size_t DRAM_SIZE = 1024 * 1024 * 128;// 定义DRAM的结束地址
constexpr std::size_t DRAM_END = DRAM_SIZE + DRAM_BASE - 1;

这三个参数是在计算机内存管理上下文中定义的,用于指定特定内存区域(在本例中是 DRAM,即动态随机访问内存)的基本属性。

  • DRAM_BASE 定义了 DRAM 内存区域的起始物理地址。

qemu 中定义了这个变量,这个地址是一个十六进制数,从 0x8000'0000 处开始执行,即内存区域的开始点。

  • DRAM_SIZE 定义了 DRAM 区域的总大小。

这个变量指定了从DRAM_BASE开始,可以用于存储数据的内存量。这个大小是以字节为单位的,对于内存大小的计算通常使用字节作为基本单位。DRAM_SIZE被定义为1024 * 1024 * 128字节,即 128MB。这是通过计算 1024 字节(1KB)乘以 1024(即 1MB)再乘以 128 得到的,即 DRAM 区域有 128MB 的存储容量。

  • DRAM_END定义了 DRAM 内存区域的结束地址。

基于DRAM_BASEDRAM_SIZE计算得出,指出了 DRAM 区域的最后一个字节的地址。这个地址用于界定 DRAM 区域的范围,对于确定内存访问是否越界很有帮助。

总结来说,这三个参数共同定义了 DRAM 内存区域的物理位置和大小,是计算机内存管理的基本组成部分。通过这些参数,操作系统和应用程序可以正确地定位和管理内存资源。

下面是涉及到现代 C++ 语法层面的解释:

  1. 使用std::size_t替换uint64_t用于表示大小

虽然在上述代码中使用uint64_t对于定义 DRAM 大小和地址范围是合适的,但在 C++中,表示大小或基于内存的索引时通常推荐使用std::size_t。这是因为std::size_t是一个无符号整数类型,其大小是为了能够安全地表示对象的大小,以及对象最大可能的索引,这样可以增强代码的可移植性和安全性。

  1. 使用constexpr确保编译时常量

代码已经正确使用了constexpr来声明编译时常量,这是现代 C++推荐的做法,因为它可以在编译时而不是运行时解析这些值,从而提高效率。没有需要修改的地方。关于 constexpr 可以进一步阅读这篇文章。

  1. 使用单引号(')作为数字分隔符

这个特性自 C++14 起被引入,允许开发者在数字字面量中加入单引号来分隔数字,使得长数字序列更容易被阅读。对于DRAM_BASE的定义,我们可以这样改写来增加其可读性:

constexpr std::size_t DRAM_BASE = 0x8000'0000;

这里,0x8000'00000x80000000在编译时是完全相同的,但加入分隔符后,数字更易于阅读,尤其是对于较长的十六进制或十进制数值。这种写法没有改变原有的数值,只是使得数值的表示更为友好。使用这种方式,你可以使代码更加清晰和易于维护。

2.2 实现 Dram

接下来讲解如何实现 Dram ,下面是一个最简的形式,简单来说用一个 vector 来表示内存,Dram 初始化的时候需要将指令写入内存中。

// dram.cpp
// 定义一个名为Dram的类,用于模拟DRAM(动态随机访问内存)的行为。
class Dram {
public:// 类的构造函数,接受一个包含机器码(即初始化代码)的vector作为参数。Dram(const std::vector<uint8_t>& code) {// 将dram成员变量的大小调整为DRAM_SIZE,并将所有元素初始化为0。// 这里DRAM_SIZE应该是一个在类外部定义的常量,表示DRAM的总容量(字节数)。dram.resize(DRAM_SIZE, 0); // 使用0初始化DRAM。// 将传入的code(机器码)复制到dram向量的开始位置。// std::copy是标准库算法,用于复制一个范围内的元素到另一个范围。// code.begin()和code.end()分别指向传入vector的开始和结束,指定了要复制的数据范围。// dram.begin()指定了目标范围的开始位置。std::copy(code.begin(), code.end(), dram.begin());}private:// 类的私有成员变量,用std::vector<uint8_t>表示DRAM存储的数据。// uint8_t是8位无符号整数类型,代表DRAM中每个存储单元可以存储的数据范围(0-255)。// 使用vector是因为它是一个动态数组,可以灵活地调整大小,并提供随机访问能力。std::vector<uint8_t> dram;
};

接下来添加loadstore成员函数,这些函数将模拟从 DRAM 加载和向 DRAM 存储数据的行为。

2.3 实现 Dram Load 方法

接下来要实现 Dram Load 方法,即从内存中读取指定长度的数据,输入参数为 addr 表示内存地址,size 表示需要读取的长度。目前 size 只能读取 8 位、16 位、32 位或 64 位 。

内存用 vector 来表示,其中一个位置表示 8 bit 所以需要计算 size 对应的比特数,即读取 vector 中多少个位置。随后使用 | 运算符将读取到的数据拼接起来。

下面是具体的代码:

class Dram {
public:// ...// 模拟从DRAM加载数据uint64_t load(uint64_t addr, uint64_t size) {if (size != 8 && size != 16 && size != 32 && size != 64) {throw std::runtime_error("LoadAccessFault");}uint64_t nbytes = size / 8;std::size_t index = (addr - DRAM_BASE);if (index + nbytes > dram.size()) {throw std::out_of_range("Address out of range");}uint64_t value = 0;for (uint64_t i = 0; i < nbytes; ++i) {value |= static_cast<uint64_t>(dram[index + i]) << (i * 8);}return value;}// ...private:std::vector<uint8_t> dram;};

2.4 实现 Dram store 方法

这部分实现写入内存的方法,输入参数需要给定读取对应的内存地址 addr ,待读取的长度 size 和返回值 value 。

和之前读取的方法类似,依旧是计算出来对应的索引然后将数据拼接起来。

class Dram {
public:// ...// 模拟向DRAM存储数据void store(uint64_t addr, uint64_t size, uint64_t value) {if (size != 8 && size != 16 && size != 32 && size != 64) {throw std::runtime_error("StoreAMOAccessFault");}uint64_t nbytes = size / 8;std::size_t index = (addr - DRAM_BASE);if (index + nbytes > dram.size()) {throw std::out_of_range("Address out of range");}for (uint64_t i = 0; i < nbytes; ++i) {dram[index + i] = (value >> (i * 8)) & 0xFF;}}private:std::vector<uint8_t> dram;
};

3. 总线 Bus

Bus 是用来将不同的设备衔接起来,用于在不同组件之间传输数据的通信系统。总线在计算机架构中起到了重要的桥梁作用,连接了各个硬件组件,如处理器、内存、输入/输出设备等。

目前只需要将内存 Dram 连接起来即可,下面是 bus 头文件的定义:

// bus.h
class Bus {
public:Bus(const std::vector<uint8_t>& code);uint64_t load(uint64_t addr, uint64_t size);void store(uint64_t addr, uint64_t size, uint64_t value);private:Dram dram;
};

其中 load 用于同 Dram 交互读取数据,而 store 用于写入数据。接下来实现 load 和 store 方法。

3.1 Bus load store

下面是代码是对 Dram 的包装,首先要检验地址是否合法随后调用 Dram 的方法,反之报错。

Bus::Bus(const std::vector<uint8_t>& code) : dram(code) {}uint64_t Bus::load(uint64_t addr, uint64_t size) {if (addr >= DRAM_BASE && addr <= DRAM_END) {return dram.load(addr, size);} else {throw std::runtime_error("LoadAccessFault at address " + std::to_string(addr));}
}void Bus::store(uint64_t addr, uint64_t size, uint64_t value) {if (addr >= DRAM_BASE && addr <= DRAM_END) {dram.store(addr, size, value);} else {throw std::runtime_error("StoreAMOAccessFault at address " + std::to_string(addr));}
}

4. CPU

上面已经将 Dram、Bus 剥离出来的,接下来需要修改 cpu.cpp 部分的代码,在其中增加 Bus 成员变量,通过 bus 调用 dram 进行读写。

随后删除 std::vector<uint8_t> dram; 成员变量,再提供对应的 store 和 load 方法同 dram 读写。

class Cpu {
public:// ... 其他Bus bus;uint64_t load(uint64_t addr, uint64_t size);void store(uint64_t addr, uint64_t size, uint64_t value);uint32_t fetch();
};

4.1 load 和 store

接下来实现 load 方法:

uint64_t Cpu::load(uint64_t addr, uint64_t size) {try {return bus.load(addr, size);} catch (const Exception& e) {std::cerr << "Exception load: " << e << std::endl;}
}

直接调去 bus 即可,两个参数分别为对应的地址和要读取数据的长度。

store 同上

void Cpu::store(uint64_t addr, uint64_t size, uint64_t value) {try {bus.store(addr, size, value);} catch (const Exception& e) {std::cerr << "Exception store: " << e << std::endl;}
}

4.2 fetch

fetch 即获取 32 位长度的指令。

uint32_t Cpu::fetch() {try {bus.load(pc, 32);} catch (const Exception& e) {std::cerr << "Exception fetch: " << e << std::endl;}
}

目前先解析 32 位,后续再进一步扩展。

不是所有的 RISC-V 指令都是固定的 32 位长度。RISC-V(Reduced Instruction Set Computing - V)是一种基于精简指令集(RISC)的开放标准架构,它提供了多种指令长度的选项,以适应不同的需求。

RISC-V 支持的指令长度包括 32 位、64 位和 128 位。最常见的是 RV32I、RV64I 和 RV128I,它们分别表示 32 位、64 位和 128 位的整数基本指令集。

例如,RV32I 指令是固定长度为 32 位的整数指令集,而 RV64I 则是 64 位的整数指令集。此外,RISC-V 还提供了扩展指令集,如 M 扩展用于整数乘法和除法,A 扩展用于原子操作,F 和 D 扩展用于浮点运算等。

总的来说,RISC-V 的灵活性使得它可以适应不同的应用领域,并且可以选择不同长度的指令集来平衡性能和资源的需求。

5. main

接下来更新 main 函数,读取指令的二进制形式随后执行。

int main(int argc, char* argv[]) {if (argc != 2) {std::cout << "Usage:\n"<< "- ./program_name <filename>\n";return 0;}std::ifstream file(argv[1], std::ios::binary);if (!file) {std::cerr << "Cannot open file: " << argv[1] << std::endl;return 1;}std::vector<uint8_t> code(std::istreambuf_iterator<char>(file), {});Cpu cpu(code); // 假设Cpu类的构造函数接受指令代码的vectortry {while (true) {uint32_t inst = cpu.fetch();auto new_pc = cpu.execute(inst);if (new_pc.has_value()) {cpu.pc = new_pc.value();} else {break;}}} catch (const Exception& e) {std::cerr << "Exception main: " << e << std::endl;}// 使用cpu对象进行操作cpu.dump_registers(); // 打印寄存器状态cpu.dump_pc();return 0;
}

将汇编编译为二进制的形式

$ riscv64-unknown-elf-gcc -Wl,-Ttext=0x0 -nostdlib -o add-addi add-addi.s
$ riscv64-unknown-elf-objcopy -O binary add-addi add-addi.bin

编译并执行指令,运行并测试是否正确:

mkdir -p build && cd build && cmake .. && make && ./crvemu ../add-addi.bin

6. 测试

此外本节内容引入了单元测试,将上面手动测试的过程封装为函数:

$ riscv64-unknown-elf-gcc -Wl,-Ttext=0x0 -nostdlib -o add-addi add-addi.s
$ riscv64-unknown-elf-objcopy -O binary add-addi add-addi.bin

下面是最终的单元测试:

// 消除警告: warning: cannot find entry symbol _start; defaulting to 0000000000000000
const std::string start = ".global _start \n _start:";// Test addi instruction
TEST(RVTests, TestAddi) {std::string code = start + "addi x31, x0, 42 \n";Cpu cpu = rv_helper(code, "test_addi", 1);EXPECT_EQ(cpu.regs[31], 42) << "Error: x31 should be 42 after ADDI instruction";
}// Test add instruction
TEST(RVTests, TestAdd) {std::string code = ".global _start \n _start:""addi x2, x0, 10 \n"   // 将 10 加载到 x2 中"addi x3, x0, 20 \n"   // 将 20 加载到 x3 中"add x1, x2, x3 \n";  // x1 = x2 + x3Cpu cpu = rv_helper(code, "test_add", 3);// 验证 x1 的值是否正确EXPECT_EQ(cpu.regs[1], 30) << "Error: x1 should be the result of ADD instruction";
}

5.1 rv_helper

通过 rv_helper 函数实现了将字符串转为汇编、二进制再放入 CPU 中执行。

三个参数分别为汇编代码的字符串形式,测试对应的名称,待测试的指令个数。

Cpu rv_helper(const std::string& code, const std::string& testname, size_t n_clock) {std::string filename = testname + ".s";// 创建并写入汇编文件std::ofstream file(filename);if (!file.is_open()) {throw std::runtime_error("Failed to create assembly file.");}file << code;file.close();// 生成目标文件和二进制文件generate_rv_obj(filename.c_str());generate_rv_binary(testname.c_str());// 读取二进制文件内容std::string binFilename = testname + ".bin";std::ifstream file_bin(binFilename, std::ios::binary);if (!file_bin.is_open()) {throw std::runtime_error("Failed to open binary file.");}std::vector<uint8_t> binaryCode((std::istreambuf_iterator<char>(file_bin)), std::istreambuf_iterator<char>());// 初始化CPU并执行指令Cpu cpu(binaryCode);for (size_t i = 0; i < n_clock; ++i) {try {uint64_t inst = cpu.fetch();auto new_pc = cpu.execute(inst);if (new_pc.has_value()) {cpu.pc = new_pc.value();} else {break;}} catch (const std::exception& e) {std::cerr << "CPU execution error: " << e.what() << std::endl;break;}}return cpu;
}

5.2 generate_rv_obj

此函数为 riscv64-unknown-elf-gcc -Wl,-Ttext=0x0 -nostdlib -o add-addi add-addi.s 对应的处理过程:

void generate_rv_obj(const std::string& assembly) {// 使用C++的字符串处理能力来获取不含扩展名的文件名size_t dotPos = assembly.find_last_of(".");std::string baseName = (dotPos == std::string::npos) ? assembly : assembly.substr(0, dotPos);std::string command = "riscv64-unknown-elf-gcc -Wl,-Ttext=0x0 -nostdlib -o " + baseName + " " + assembly;// 执行命令int result = std::system(command.c_str());// 检查命令执行结果if (result != 0) {std::cerr << "Failed to generate RV object from assembly: " << assembly << std::endl;}
}

5.2 generate_rv_obj

此函数为 riscv64-unknown-elf-gcc -Wl,-Ttext=0x0 -nostdlib -o add-addi add-addi.s 对应的处理过程:

void generate_rv_obj(const std::string& assembly) {// 使用C++的字符串处理能力来获取不含扩展名的文件名size_t dotPos = assembly.find_last_of(".");std::string baseName = (dotPos == std::string::npos) ? assembly : assembly.substr(0, dotPos);std::string command = "riscv64-unknown-elf-gcc -Wl,-Ttext=0x0 -nostdlib -o " + baseName + " " + assembly;// 执行命令int result = std::system(command.c_str());// 检查命令执行结果if (result != 0) {std::cerr << "Failed to generate RV object from assembly: " << assembly << std::endl;}
}

5.3 generate_rv_binary

此函数为 riscv64-unknown-elf-objcopy -O binary add-addi add-addi.bin 对应的处理过程:

void generate_rv_binary(const std::string& obj) {// 构建llvm-objcopy命令行字符串std::string command = "riscv64-unknown-elf-objcopy -O binary " + obj + " " + obj + ".bin";// 执行命令int result = std::system(command.c_str());// 检查命令执行结果if (result != 0) {std::cerr << "Failed to generate RV binary from object: " << obj << std::endl;}
}

运行并测试是否正确:

mkdir -p build && cd build && cmake .. && make && ./crvemu ../add-addi.bin
~/crvemu/build$ ./crvemu ../add-addi.bin
--------------------------------------------------------------------------------
x0(zero) = 0000000000000000 000x1(ra) = 0000000000000000 000x2(sp) = 0000000007ffffff 000x3(gp) = 0000000000000000
000x4(tp) = 0000000000000000 000x5(t0) = 0000000000000000 000x6(t1) = 0000000000000000 000x7(t2) = 0000000000000000
000x8(s0) = 0000000000000000 000x9(s1) = 0000000000000000 000xa(a0) = 0000000000000000 000xb(a1) = 0000000000000000
000xc(a2) = 0000000000000000 000xd(a3) = 0000000000000000 000xe(a4) = 0000000000000000 000xf(a5) = 0000000000000000
000x10(a6) = 0000000000000000 000x11(a7) = 0000000000000000 000x12(s2) = 0000000000000000 000x13(s3) = 0000000000000000
000x14(s4) = 0000000000000000 000x15(s5) = 0000000000000000 000x16(s6) = 0000000000000000 000x17(s7) = 0000000000000000
000x18(s8) = 0000000000000000 000x19(s9) = 0000000000000000 000x1a(s10) = 0000000000000000 000x1b(s11) = 0000000000000000
000x1c(t3) = 0000000000000000 000x1d(t4) = 0000000000000005 000x1e(t5) = 0000000000000025 000x1f(t6) = 000000000000002a

6. 总结

综上,这一章节将 dram 拆分出来作为一个单独的类,为了链接 dram 又引入了 bus 。并且将手动编译的过程改成函数,避免了手动执行,后续可以很方便的测试更多的指令。

下一节会将解析指令的过程单独拆分为一个类,然后进一步的解析更多的指令。

👉🏻 文章汇总「从零实现模拟器、操作系统、数据库、编译器…」:https://okaitserrj.feishu.cn/docx/R4tCdkEbsoFGnuxbho4cgW2Yntc

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.bcls.cn/TXuY/157.shtml

如若内容造成侵权/违法违规/事实不符,请联系编程老四网进行投诉反馈email:xxxxxxxx@qq.com,一经查实,立即删除!

相关文章

Spring MVC(基于 Spring4.x)基础学习

一、SpringMVC概述 二、SpringMVC的HelloWorld 三、使用RequestMapping映射请求 四、映射请求参数&请求头 五、处理模型数据 六、视图和视图解析器 七、RESTful CRUD 八、SpringMVC表单标签&处理静态资源 九、数据转换&数据格式化&数据校验 十、处理JSON:使用…

Django学习全纪录:Django开发环境的搭建

导言 对于Django&#xff0c;它是Python的一个开发框架&#xff0c;之前系统地学习过。遗憾的是&#xff0c;对于一些遇到的问题&#xff0c;没有及时地记录下来。因此&#xff0c;我将它重新捡起&#xff0c;进行学习和实践。从搭建环境开始&#xff0c;重新去学习它&#xff…

django中的中间件

在Django中&#xff0c;中间件&#xff08;Middleware&#xff09;是一个轻量级的、底层的“插件”系统&#xff0c;用于全局地修改Django的输入或输出。每个中间件组件都负责执行一些特定的任务&#xff0c;比如检查用户是否登录、处理日志、GZIP压缩等。Django的中间件提供了…

Xubuntu16.04系统中修改系统语言和系统时间

1.修改系统语言 问题&#xff1a;下图显示系统语言不对 查看系统中可用的所有区域设置的命令 locale -a修改/etc/default/locale文件 修改后如下&#xff1a; # File generated by update-locale LANG"en_US.UTF-8" LANGUAGE"en_US:en"LANG"en_US…

STM32CubeMx+FreeRTOS+Clion运用事件组开发按键

文章目录 1、事件组2、范例2.1 功能2.2 步骤生成代码配置编写 API 函数介绍创建删除设置事件标志位等待事件标志位 3、参考文章 1、事件组 一个事件标志组有多个事件位&#xff0c;每个事件位表示了一个事件的标志。 比如我们用事件标志组的bit0表示事件A、bit1表示事件B、bit…

清华AutoGPT:掀起AI新浪潮,与GPT4.0一较高下

引言&#xff1a; 随着人工智能技术的飞速发展&#xff0c;自然语言处理&#xff08;NLP&#xff09;领域迎来了一个又一个突破。最近&#xff0c;清华大学研发的AutoGPT成为了业界的焦点。这款AI模型以其出色的性能&#xff0c;展现了中国在AI领域的强大实力。 目录 引言&…

DOC主题 WordPress博客、文库、资讯主题

主题专为博客、自媒体、资讯类的网站设计开发&#xff0c;适合做博客、文库、帮助中心的主题。 演示站&#xff1a;做好服务 - 服务器故障、网站故障、宝塔问题快速排查与修复 截图 代码非常简练&#xff0c;主题下载地址&#xff1a;DOC主题.zip

数据结构——线性表

逻辑结构——线性表 1.线性表的定义&#xff08;逻辑结构&#xff09; 要点&#xff1a; 相同数据类型有限序列 几个概念&#xff1a; 是线性表中的“第i个”元素线性表中的位序 是表头元素&#xff1b;是表尾元素。 除第一个元素外&#xff0c;每个元素有且仅有一个直接前驱&…

第4讲 小程序首页实现

首页 create.vue <template><view class"vote_type"><view class"vote_tip_wrap"><text class"type_tip">请选择投票类型</text><!-- <text class"share">&#xe739;分享给朋友</text&g…

相机图像质量研究(21)常见问题总结:CMOS期间对成像的影响--隔行扫描/逐行扫描

系列文章目录 相机图像质量研究(1)Camera成像流程介绍 相机图像质量研究(2)ISP专用平台调优介绍 相机图像质量研究(3)图像质量测试介绍 相机图像质量研究(4)常见问题总结&#xff1a;光学结构对成像的影响--焦距 相机图像质量研究(5)常见问题总结&#xff1a;光学结构对成…

树和堆的精讲

&#x1d649;&#x1d65e;&#x1d658;&#x1d65a;!!&#x1f44f;&#x1f3fb;‧✧̣̥̇‧✦&#x1f44f;&#x1f3fb;‧✧̣̥̇‧✦ &#x1f44f;&#x1f3fb;‧✧̣̥̇:Solitary_walk ⸝⋆ ━━━┓ - 个性标签 - &#xff1a;来于“云”的“羽球人”。…

《Go 简易速速上手小册》第1章:Go 语言基础(2024 最新版)

文章目录 1.1 Go 语言的安装与环境配置1.1.1 基础知识讲解案例 Demo&#xff1a;简单的 Go 程序 1.1.2 重点案例&#xff1a;搭建一个 Go Web 服务准备工作步骤 1&#xff1a;创建项目目录步骤 2&#xff1a;编写 Web 服务代码步骤 3&#xff1a;运行你的 Web 服务步骤 4&#…

【开源】JAVA+Vue.js实现天然气工程运维系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统角色分类2.2 核心功能2.2.1 流程 12.2.2 流程 22.3 各角色功能2.3.1 系统管理员功能2.3.2 用户服务部功能2.3.3 分公司&#xff08;施工单位&#xff09;功能2.3.3.1 技术员角色功能2.3.3.2 材料员角色功能 2.3.4 安…

沁恒CH32V30X学习笔记01--创建工程

资料下载 https://www.wch.cn/products/CH32V307.html? 下载完成后安装MounRiver Studio(MRS) 创建工程 修改时钟144M printf重定向 修改外部晶振频率位置 添加自定义文件 添加目录

动态头部:统一目标检测头部与注意力

论文地址:https://arxiv.org/pdf/2106.08322.pdf ai阅读论文_论文速读_论文阅读软件-网易有道速读 创新点是什么? 这篇文档的创新点是提出了一种统一的方法&#xff0c;将对象检测头和注意力机制结合起来。作者在文中提出了一种称为Dynamic Head的方法&#xff0c;通过引入…

掌握Go并发:Go语言并发编程深度解析

&#x1f3f7;️个人主页&#xff1a;鼠鼠我捏&#xff0c;要死了捏的主页 &#x1f3f7;️系列专栏&#xff1a;Golang全栈-专栏 &#x1f3f7;️个人学习笔记&#xff0c;若有缺误&#xff0c;欢迎评论区指正 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&…

LabVIEW智能温度监控系统

LabVIEW智能环境监测系统 介绍了一个基于LabVIEW的智能环境监测系统的开发过程。该系统在实时监测和分析环境参数&#xff0c;如温度、湿度、气体浓度等&#xff0c;以提供精确的数据支持&#xff0c;确保环境安全与健康。通过高效的数据处理和友好的用户界面&#xff0c;系统…

pytest 框架自动化测试

随笔记录 目录 1. 安装 2. 安装pytest 相关插件 2.1 准备阶段 2.2 安装 2.3 验证安装成功 3. pytest测试用例的运行方式 3.1 主函数模式 3.1.1 主函数执行指定文件 3.1.2 主函数执行指定模块 3.1.3 主函数执行某个文件中的某个类、方法、函数 3.1.4 主函数执行生…

Mysql Day06

sql优化 插入数据 大批量插入数据 主键顺序插入性能高于乱序插入 load data local infile /root/load_user_100w_sort.sql into table tb_user fields terminated by , lines terminated by \n ; 主键优化 这个黄色的都是一个一个Page 主键乱序插入之后会变成1-3-2&#x…

2.18 C++ day6

思维导图 以下是一个简单的比喻&#xff0c;将多态概念与生活中的实际情况相联系&#xff1a; 比喻&#xff1a;动物园的讲解员和动物表演 想象一下你去了一家动物园&#xff0c;看到了许多不同种类的动物&#xff0c;如狮子、大象、猴子等。现在&#xff0c;动物园里有一位讲…
推荐文章