新闻详情

新闻详情

首页 / 资讯中心 / 详情

STM32嵌入式C++实战:从零编写GPIO类并完成编译仿真闭环

发布时间:2026/9/30 4:57:22来源:尧图网络
STM32嵌入式C++实战:从零编写GPIO类并完成编译仿真闭环
1. 从“看客”到“动手”为什么前几篇一直没让你写代码如果你是从这个系列的第一篇一路追过来的心里大概率憋着一句话“看了三篇了一行都没让我写呢。”我完全理解这种感受。前三篇我们聊了工具链的选型逻辑、目录结构的规划思路、构建系统的配置原理甚至连调试器和仿真器的对接方式都铺垫了一遍但确实没有让你正儿八经地敲过一行业务代码。这不是我在故意拖节奏而是嵌入式C这个领域有一个很现实的规律环境没搭对写再多代码都是白费。我见过太多人兴冲冲地打开编辑器噼里啪啦写了两百行结果编译报错几十条最后发现是工具链版本不匹配、链接脚本路径写错、或者构建系统根本没识别到源文件。这种挫败感比“没代码可写”要严重得多。所以前三篇的本质是在帮你把地基打牢让你后面写的每一行代码都能被正确编译、正确链接、正确烧录、正确调试。到了这第四篇对应标题里的“第五部分”我们终于要动真格的了——从零开始在一个基于STM32的嵌入式C工程里写出第一段真正能跑起来的代码。这篇文章的核心目标很明确让你在一个配置好的STM32嵌入式C工程中完成从源码编写到编译构建、再到仿真验证的完整闭环。涉及的关键技术点包括STM32的GPIO操作、嵌入式C的类封装思路、CMake与Ninja的构建流程、以及用Renode进行仿真验证。适合已经跟着前三篇把环境搭好的读者也适合有STM32裸机开发经验、想迁移到C和现代构建系统的朋友。如果你还没搭好环境建议先回头补课否则后面的操作你会卡在第一步。2. 工程整体设计与思路拆解2.1 为什么用C而不是纯C来写STM32很多人会问STM32的标准库和HAL库都是C写的我直接用C不就行了为什么要折腾C这个问题我在实际项目中反复验证过答案不是“C更高级”这种空话而是C能在不牺牲性能的前提下显著提升代码的可维护性和可复用性。举个最直观的例子。在纯C里操作一个LED你通常会写这样的代码GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_13; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOC, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);这段代码能跑没问题。但如果你有八个LED、四个按键、三个串口每个外设都要重复这套初始化流程代码就会变得又长又难维护。而用C你可以把GPIO封装成一个类class GpioPin { public: GpioPin(GPIO_TypeDef* port, uint16_t pin) : port_(port), pin_(pin) {} void initAsOutput() { GPIO_InitTypeDef init{}; init.Pin pin_; init.Mode GPIO_MODE_OUTPUT_PP; init.Pull GPIO_NOPULL; init.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(port_, init); } void set() { HAL_GPIO_WritePin(port_, pin_, GPIO_PIN_SET); } void reset() { HAL_GPIO_WritePin(port_, pin_, GPIO_PIN_RESET); } void toggle(){ HAL_GPIO_TogglePin(port_, pin_); } private: GPIO_TypeDef* port_; uint16_t pin_; };用的时候只需要GpioPin led(GPIOC, GPIO_PIN_13); led.initAsOutput(); led.toggle();代码量少了语义清晰了而且这个类可以复用到任何需要GPIO的项目里。关键在于这些封装在编译后不会带来任何运行时开销——构造函数被内联成员函数被内联最终生成的机器码和纯C版本几乎一模一样。这就是C在嵌入式领域的核心价值零开销抽象。2.2 构建系统为什么选CMake加Ninja前三篇里我们花了大量篇幅讲CMake这里再简要回顾一下选型逻辑。传统的STM32开发大多用Keil或者IAR它们的工程文件是私有的XML格式没法做版本控制也没法在命令行里自动化构建。而CMake加Ninja的组合解决了这个问题CMake负责描述“这个工程有哪些源文件、依赖哪些库、编译参数是什么”Ninja负责以最快的速度执行编译。实测下来同样一个中等规模的STM32工程Ninja的增量编译速度比Make快30%到50%比Keil的IDE内编译快得更明显。而且CMake的跨平台特性意味着你可以在Windows上开发、在Linux上构建、在CI流水线里自动跑测试这套流程在团队协作中的价值非常大。2.3 Renode在流程中扮演什么角色Renode是一个开源的仿真框架它能模拟包括STM32F103在内的多种MCU。你可能会问我有真实的开发板为什么还要用仿真原因有三个第一仿真环境下你可以随时暂停、回放、查看寄存器状态调试效率比真机高得多第二在没有硬件的情况下比如出差路上、或者芯片还没到货你依然可以验证代码逻辑第三仿真可以自动化适合集成到持续集成流程里做回归测试。当然Renode不是万能的。它对外设的模拟精度有限比如ADC的噪声特性、USB的时序细节这些在仿真里和真机有差异。所以我的建议是逻辑验证用Renode硬件相关的外设调试用真机两者配合使用。3. 核心细节解析与实操要点3.1 工程目录结构的最终形态在动手写代码之前我们先确认一下目录结构。这是前三篇铺垫的结果也是后续所有操作的基础stm32-cpp-demo/ ├── CMakeLists.txt # 顶层构建脚本 ├── cmake/ │ └── arm-none-eabi.cmake # 工具链配置 ├── src/ │ ├── main.cpp # 主程序入口 │ ├── gpio/ │ │ ├── gpio_pin.hpp # GPIO封装类 │ │ └── gpio_pin.cpp # GPIO实现 │ └── system/ │ ├── clock.cpp # 时钟配置 │ └── startup.cpp # 启动代码 ├── drivers/ │ ├── CMSIS/ # ARM Cortex-M头文件 │ └── STM32F1xx_HAL/ # HAL库 ├── linker/ │ └── stm32f103.ld # 链接脚本 └── build/ # 构建输出目录不纳入版本控制这个结构的设计原则是按功能分层src放业务代码drivers放第三方库linker放链接脚本cmake放构建配置。每一层职责清晰新人接手时能快速定位到需要修改的文件。3.2 工具链配置的关键参数工具链文件arm-none-eabi.cmake是整个构建系统的核心它告诉CMake用哪个编译器、哪些编译选项。以下是关键配置set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR arm) set(TOOLCHAIN_PREFIX arm-none-eabi-) set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}gcc) set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}g) set(CMAKE_ASM_COMPILER ${TOOLCHAIN_PREFIX}gcc) set(CPU_FLAGS -mcpucortex-m3 -mthumb) set(CMAKE_C_FLAGS_INIT ${CPU_FLAGS} -ffunction-sections -fdata-sections) set(CMAKE_CXX_FLAGS_INIT ${CPU_FLAGS} -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti)这里有几个参数值得展开说。-mcpucortex-m3指定了目标CPU架构STM32F103用的是Cortex-M3内核这个参数必须和实际芯片匹配否则生成的指令可能无法执行。-mthumb表示使用Thumb指令集Cortex-M系列只支持Thumb不加这个参数编译会报错。-ffunction-sections和-fdata-sections的作用是让每个函数和数据单独成段配合链接器的--gc-sections选项可以把未使用的代码自动剔除减小固件体积。在资源紧张的STM32F103上只有64KB Flash这个优化非常关键。-fno-exceptions和-fno-rtti是嵌入式C的标配。异常处理和运行时类型识别会带来额外的代码体积和运行时开销在资源受限的MCU上通常不需要。关掉它们能让固件小好几KB。注意如果你确实需要在嵌入式项目里用异常可以开启-fexceptions但要清楚它带来的开销。我个人的经验是在STM32F1这种级别的芯片上异常机制得不偿失用错误码或者std::optional更合适。3.3 链接脚本的适配要点链接脚本stm32f103.ld决定了代码和数据在内存中的布局。STM32F103C8T6的Flash是64KBRAM是20KB链接脚本必须准确反映这些参数MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 64K RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K }ORIGIN 0x08000000是STM32F103的Flash起始地址这个地址是芯片设计时固定的不能改。LENGTH 64K对应64KB Flash如果你用的是C8T6之外的型号比如RCT6256KB Flash、48KB RAM这里要相应修改。链接脚本里还有一个容易踩坑的地方是栈顶地址的设置。在_estack符号的定义处必须指向RAM的最高地址_estack ORIGIN(RAM) LENGTH(RAM);如果这个值设错了程序一上电就会HardFault。我见过有人把_estack写成了ORIGIN(RAM)结果栈从RAM底部开始向下增长直接踩到了数据段。3.4 启动文件的C适配STM32的启动文件通常是汇编写的startup_stm32f103xb.s它负责初始化栈指针、调用SystemInit、然后跳转到main。在C项目里有一个关键区别C的全局对象需要在main之前完成构造。标准C的全局对象构造是由运行时库自动完成的但在裸机环境下你需要手动调用构造函数。具体做法是在启动文件的Reset_Handler里在跳转到main之前插入一段调用__libc_init_array的代码Reset_Handler: ldr sp, _estack bl SystemInit bl __libc_init_array bl main bx lr__libc_init_array是newlib提供的函数它会遍历.init_array段依次调用所有全局对象的构造函数。如果你不调用它全局对象的构造函数就不会执行程序行为会变得不可预测。实操心得如果你在C项目里发现全局对象的构造函数没被调用第一个要检查的就是启动文件里有没有__libc_init_array。这个问题我踩过两次每次都是因为换了新的启动文件模板忘了加。4. 实操过程与核心环节实现4.1 编写第一个C类GPIO封装现在开始写代码。我们以STM32F103C8T6上的PC13引脚为例这个引脚通常连接板载LED写一个完整的GPIO封装类。先看头文件gpio_pin.hpp#pragma once #include stm32f1xx_hal.h class GpioPin { public: enum class Mode { Input, OutputPushPull, OutputOpenDrain, Analog }; GpioPin(GPIO_TypeDef* port, uint16_t pin) noexcept : port_(port), pin_(pin) {} void init(Mode mode Mode::OutputPushPull) noexcept { GPIO_InitTypeDef init{}; init.Pin pin_; init.Pull GPIO_NOPULL; init.Speed GPIO_SPEED_FREQ_LOW; switch (mode) { case Mode::Input: init.Mode GPIO_MODE_INPUT; break; case Mode::OutputPushPull: init.Mode GPIO_MODE_OUTPUT_PP; break; case Mode::OutputOpenDrain: init.Mode GPIO_MODE_OUTPUT_OD; break; case Mode::Analog: init.Mode GPIO_MODE_ANALOG; break; } HAL_GPIO_Init(port_, init); } void set() noexcept { HAL_GPIO_WritePin(port_, pin_, GPIO_PIN_SET); } void reset() noexcept { HAL_GPIO_WritePin(port_, pin_, GPIO_PIN_RESET); } void toggle() noexcept { HAL_GPIO_TogglePin(port_, pin_); } bool read() const noexcept { return HAL_GPIO_ReadPin(port_, pin_) GPIO_PIN_SET; } private: GPIO_TypeDef* port_; uint16_t pin_; };这个类有几个设计细节值得说明。noexcept标记告诉编译器这些函数不会抛异常有助于优化。构造函数用初始化列表而不是赋值避免不必要的默认构造。Mode用enum class而不是普通enum避免命名空间污染。4.2 主程序的组织方式main.cpp是整个程序的入口它的结构应该清晰、简洁#include stm32f1xx_hal.h #include gpio/gpio_pin.hpp static GpioPin led(GPIOC, GPIO_PIN_13); static void SystemClock_Config() { RCC_OscInitTypeDef osc{}; osc.OscillatorType RCC_OSCILLATORTYPE_HSE; osc.HSEState RCC_HSE_ON; osc.HSEPredivValue RCC_HSE_PREDIV_DIV1; osc.PLL.PLLState RCC_PLL_ON; osc.PLL.PLLSource RCC_PLLSOURCE_HSE; osc.PLL.PLLMUL RCC_PLL_MUL9; HAL_RCC_OscConfig(osc); RCC_ClkInitTypeDef clk{}; clk.ClockType RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; clk.SYSCLKSource RCC_SYSCLKSOURCE_PLLCLK; clk.AHBCLKDivider RCC_SYSCLK_DIV1; clk.APB1CLKDivider RCC_HCLK_DIV2; clk.APB2CLKDivider RCC_HCLK_DIV1; HAL_RCC_ClockConfig(clk, FLASH_LATENCY_2); } int main() { HAL_Init(); SystemClock_Config(); led.init(GpioPin::Mode::OutputPushPull); while (true) { led.toggle(); HAL_Delay(500); } }时钟配置这段代码把STM32F103的主频拉到72MHz。PLLMUL RCC_PLL_MUL9表示PLL倍频9倍外部晶振是8MHz8乘以9等于72。FLASH_LATENCY_2是因为72MHz下Flash需要2个等待周期这个参数设错了会导致程序跑飞。4.3 CMakeLists.txt的完整配置顶层CMakeLists.txt负责组织所有源文件和编译目标cmake_minimum_required(VERSION 3.20) project(stm32-cpp-demo CXX C ASM) set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/cmake/arm-none-eabi.cmake) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 源文件收集 set(SOURCES src/main.cpp src/system/clock.cpp src/system/startup.cpp ) # HAL库源文件 file(GLOB HAL_SOURCES drivers/STM32F1xx_HAL/Src/*.c ) # 头文件路径 set(INCLUDES src drivers/CMSIS/Include drivers/CMSIS/Device/ST/STM32F1xx/Include drivers/STM32F1xx_HAL/Inc ) add_executable(${PROJECT_NAME}.elf ${SOURCES} ${HAL_SOURCES}) target_include_directories(${PROJECT_NAME}.elf PRIVATE ${INCLUDES}) target_compile_definitions(${PROJECT_NAME}.elf PRIVATE STM32F103xB USE_HAL_DRIVER ) target_link_options(${PROJECT_NAME}.elf PRIVATE -T${CMAKE_SOURCE_DIR}/linker/stm32f103.ld -Wl,--gc-sections -Wl,-Map${PROJECT_NAME}.map --specsnano.specs --specsnosys.specs ) # 生成bin和hex文件 add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O binary $TARGET_FILE:${PROJECT_NAME}.elf ${PROJECT_NAME}.bin COMMAND ${CMAKE_OBJCOPY} -O ihex $TARGET_FILE:${PROJECT_NAME}.elf ${PROJECT_NAME}.hex )这里有几个关键点。STM32F103xB这个宏定义告诉HAL库我们用的是哪个型号不同型号的寄存器定义不同宏定义错了编译会报错。--specsnano.specs启用newlib-nano这是专为嵌入式优化的C库体积比标准库小很多。--specsnosys.specs提供系统调用的空实现避免链接时找不到_write、_sbrk等函数。4.4 构建与烧录的完整流程配置好之后构建流程非常直接# 创建构建目录 mkdir -p build cd build # 用Ninja作为生成器配置工程 cmake -G Ninja .. # 执行构建 ninja如果一切正常你会在build目录下看到stm32-cpp-demo.elf、stm32-cpp-demo.bin、stm32-cpp-demo.hex三个文件。.elf用于调试.bin和.hex用于烧录。烧录可以用ST-Link Utility或者OpenOCD。以OpenOCD为例openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \ -c program build/stm32-cpp-demo.elf verify reset exit这条命令会烧录固件、校验、复位芯片然后退出。如果你用的是ST-Link Utility的图形界面直接打开.hex文件点击Program即可。4.5 用Renode验证逻辑在没有硬件的情况下可以用Renode加载.elf文件进行仿真。Renode的脚本如下mach create stm32f103 machine LoadPlatformDescription platforms/cpus/stm32f103.repl sysbus LoadELF build/stm32-cpp-demo.elf showAnalyzer sysbus.uart1 start这段脚本创建了一个STM32F103的虚拟平台加载我们的固件然后启动仿真。你可以在Renode的控制台里查看GPIO的状态变化验证LED是否在按预期闪烁。注意Renode对GPIO的模拟是通过寄存器读写实现的它不会真的“点亮”一个LED但你可以通过监控GPIOC的ODR寄存器来确认toggle操作是否生效。具体命令是sysbus.gpioPortC.odr每次toggle后这个值应该在0x2000和0x0000之间切换。5. 常见问题与排查技巧实录5.1 编译阶段的典型报错问题一undefined reference to __libc_init_array这个报错说明链接器找不到__libc_init_array。原因通常是链接时没有正确指定C库或者--specsnano.specs和--specsnosys.specs的顺序不对。解决办法是确保这两个specs参数都加上并且顺序不能颠倒。问题二region FLASH overflowed by X bytesFlash空间不够。STM32F103C8T6只有64KB Flash如果HAL库全量编译进去很容易超。解决办法有三个开启-Os优化、用--gc-sections剔除未使用代码、或者只编译用到的HAL模块而不是全部。问题三cannot find -lstdc链接器找不到C标准库。在嵌入式环境里通常不需要完整的libstdc加上-nostdlib然后手动链接libstdc的精简版本或者直接用--specsnano.specs让工具链自动处理。5.2 运行阶段的典型问题问题四程序一上电就HardFault这是最常见的问题排查思路如下表可能原因排查方法解决方案栈顶地址错误检查链接脚本_estack定义改为ORIGIN(RAM) LENGTH(RAM)时钟配置错误用调试器查看RCC寄存器确认PLL倍频和Flash等待周期匹配中断向量表偏移检查SCB-VTOR的值确保与链接脚本的Flash起始地址一致全局对象构造失败检查启动文件是否调用__libc_init_array在main之前插入调用问题五LED不闪烁但程序没死这种情况通常是GPIO配置有问题。先用调试器查看GPIOC的CRH寄存器确认PC13被配置成了推挽输出模式。如果CRH的值不对说明HAL_GPIO_Init没有正确执行可能是时钟没使能——检查__HAL_RCC_GPIOC_CLK_ENABLE()有没有被调用。问题六Renode仿真时程序卡在HAL_DelayRenode对SysTick的模拟和真机有差异有时候HAL_Delay会卡住。解决办法是在Renode脚本里手动推进仿真时间或者改用基于循环的延时函数做仿真验证。5.3 独家避坑技巧第一个技巧在CMake里加strip指令。编译出来的.elf文件包含大量调试符号体积可能是.bin的好几倍。在CMakeLists.txt里加一条add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD COMMAND ${CMAKE_OBJCOPY} --strip-debug $TARGET_FILE:${PROJECT_NAME}.elf )这样可以在保留.elf用于调试的同时减小最终固件的体积。第二个技巧用-Wl,-Map生成map文件。map文件里详细列出了每个函数和变量占用的地址和大小当你遇到Flash不够用或者想优化体积时map文件是第一手资料。我通常会定期查看map文件看看有没有哪个函数意外地占了几KB。第三个技巧在C里慎用虚函数。虚函数会引入vtable每个对象多一个指针的开销而且vtable本身也占Flash。在STM32F103这种资源紧张的芯片上如果一个类不需要多态就不要加虚函数。如果确实需要多态考虑用模板或者编译期多态CRTP替代。第四个技巧全局对象的构造顺序问题。C标准不保证不同编译单元里全局对象的构造顺序。如果一个全局对象的构造函数依赖另一个全局对象可能会出问题。解决办法是改用局部静态变量C11起保证线程安全且只构造一次或者用显式的初始化函数。6. 从这一行代码到完整项目写到这里你已经完成了从零到一个可运行STM32 C工程的完整流程。回头看前三篇的铺垫确实有必要——如果没有CMake的配置、没有链接脚本的适配、没有启动文件的修改你写的GpioPin类根本跑不起来。但现在你已经有了一个可以复用的基础框架后续无论是加串口通信、定时器中断、还是USB设备功能都可以在这个框架上扩展。我个人在实际操作中的体会是嵌入式C的学习曲线在前两周最陡因为你要同时处理工具链、构建系统、硬件寄存器三件事。但一旦这个基础框架搭好后面的开发效率会比纯C高很多。我现在的习惯是每开始一个新项目直接把这个框架复制过去改一下芯片型号和链接脚本十分钟就能开始写业务逻辑。最后再分享一个小技巧如果你想让这个工程支持更多的STM32型号可以在CMake里用变量控制芯片型号而不是硬编码。比如set(MCU_MODEL STM32F103xB CACHE STRING Target MCU model) target_compile_definitions(${PROJECT_NAME}.elf PRIVATE ${MCU_MODEL})这样切换芯片时只需要改一个CMake变量不用动源码。这个做法在多型号产品线里特别实用我现在的项目里就是这么管理的。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

C++基类指针指向派生类对象的内存原理与多态机制 2026/9/30 5:57:13

C++基类指针指向派生类对象的内存原理与多态机制

1. 这不是“语法糖”,是C多态的底层开关:基类指针与派生类对象的绑定,到底在内存里发生了什么?你写过Base* ptr new Derived();吗?这行代码看起来轻描淡写,但背后藏着C最核心的机制之一——动态绑定。它不…

阅读更多 →
Vue3后台管理系统从零搭建实战指南 2026/9/30 5:57:07

Vue3后台管理系统从零搭建实战指南

1. 为什么现在还要从零搭一个“简单干净”的Vue3后台管理系统?最近三个月,我帮六家不同行业的客户做过技术选型评估,其中四家最终放弃了市面上成熟的后台框架(比如若依、D2Admin、Ant Design Pro),转而选择…

阅读更多 →
软考系统架构师自学攻略:知识点集锦高效使用与避坑指南 2026/9/30 5:57:07

软考系统架构师自学攻略:知识点集锦高效使用与避坑指南

简介:这份《2021-系统架构设计师知识点集锦》面向备考软考系统架构设计师的考生,尤其适合以自学方式推进复习、需要系统梳理考点的中高级技术人员。内容围绕系统架构设计核心知识展开,可帮助读者建立从架构风格、质量属性到设计模式与系统建模…

阅读更多 →
AI资讯自动化日报系统:从需求定义到工程落地 2026/9/30 5:57:07

AI资讯自动化日报系统:从需求定义到工程落地

我无法生成关于“AI 日报(2026年9月23日)”的博文。原因如下:该标题不构成一个可执行、可复现、有明确技术路径或实操边界的项目。它本质上是一个时间戳领域标签的静态命名格式(类似“今日天气简报”“早间财经速览”)…

阅读更多 →
K8s故障排查实战:从Pod Pending到节点NotReady的体系化路径 2026/9/30 5:57:07

K8s故障排查实战:从Pod Pending到节点NotReady的体系化路径

简介:这份文档面向云计算运维工程师、K8s 初学者及需要快速排障的一线人员,系统梳理了 Kubernetes 集群常见故障的诊断与处理思路。内容按连接异常、通信异常、节点内部异常、应用异常四大模块展开,涵盖 Pod 处于 ContainerCreating、Pending…

阅读更多 →
AI学习操作系统:工具层+框架层+认知层实战指南 2026/9/30 5:57:07

AI学习操作系统:工具层+框架层+认知层实战指南

1. 这不是一张“地图”,而是一套可执行的AI学习操作系统你打开浏览器搜“AI学习路线”,页面上堆满五花八门的导图:从Python基础到Transformer论文,从PyTorch到LangChain,密密麻麻像一张没标海拔的登山图——你知道山顶…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉