当前位置: 首页 > news >正文

STM32串口中文乱码终极解决方案:从编码原理到工程实践

1. 项目概述:从“Hello World”到“你好,世界”的坎坷之路

在嵌入式开发的世界里,串口通信(UART)几乎是每个开发者接触的第一个外设。它就像单片机的“嘴巴”和“耳朵”,是我们与芯片对话、调试程序、传输数据最直接、最基础的通道。使用STM32CubeMX这个强大的图形化配置工具,配置一个串口并实现基本的收发功能,对很多新手来说,可能只是几分钟的事情。你按照教程,勾选USART1,设置波特率115200,生成代码,然后在main.cwhile(1)循环里加上一句HAL_UART_Transmit(&huart1, (uint8_t*)"Hello World\r\n", 13, 1000);,接上USB转串口线,打开串口助手,一气呵成地看到了“Hello World”。这一刻,你感觉自己已经掌握了串口通信的精髓。

然而,当你信心满满地将“Hello World”换成“你好,世界”,准备向世界宣告你的中文能力时,串口助手屏幕上弹出的却是一堆莫名其妙的“锟斤拷烫烫烫”或者“????”。这一刻的困惑和挫败感,相信很多朋友都深有体会。这不仅仅是几个字符显示错误的问题,它背后牵扯到的是计算机底层字符编码的百年纷争、编译器处理源文件的方式、以及单片机运行时内存数据的真实面貌。搞不清楚这个问题,你的嵌入式系统就无法正确处理任何非ASCII字符,无论是中文菜单、传感器中文名称还是包含中文的JSON数据包,都会变成一堆乱码,严重影响产品的可用性和专业性。

因此,本篇实战教程将深入这个看似简单却陷阱重重的“中文乱码”问题。我们将不仅仅停留在如何使用STM32CubeMX配置串口,更要刨根问底,弄清楚为什么在Keil、IAR或者STM32CubeIDE中写下的中文,到了串口另一端就面目全非。我会带你从字符编码的源头(ASCII、GB2312、UTF-8)开始,经过编译器编译环节,再到单片机内存中的存储,最后通过串口字节流发送出去,完整地走一遍数据链路,把每一个可能出错的环节都揪出来,并提供经过实测的、一劳永逸的解决方案。无论你是正在被此问题困扰的初学者,还是希望深入理解嵌入式系统中文处理机制的开发者,这篇内容都将为你提供清晰的路径和实用的工具。

2. 核心乱码根源深度剖析:一条数据的“奇幻漂流”

要根治乱码,必须像侦探一样,追踪字符串从你键入代码到在串口助手上显示的完整旅程。这个过程任何一个环节的编码不一致,都会导致最终的乱码。我们主要面对的是“中文乱码”,所以焦点就在“多字节字符”的表示上。

2.1 字符编码:世界的“语言地图”

计算机只认识0和1,字符(尤其是中文这样庞大的字符集)需要一套映射规则来与二进制对应,这就是字符编码。

  • ASCII(美国信息交换标准代码):老祖宗,只用1个字节(8位)中的低7位,定义了128个字符,包括英文大小写、数字、标点和一些控制符。它是所有编码的基础,但其范围仅限于拉丁字母,无力表示中文。
  • GB2312 / GBK / GB18030:这是中文世界为了解决“汉字进入计算机”而制定的国家标准。它们属于“多字节字符集”(MBCS)。例如,一个汉字在GBK编码中通常由2个字节表示。这套编码在中国大陆的Windows系统历史版本中曾是默认的。关键点在于:不同的汉字,其对应的两个字节的值,可能与某些语言(如拉丁语系)中的两个连续单字节字符“撞车”,产生歧义。
  • Unicode(统一码):一个雄心勃勃的计划,旨在为全世界所有字符提供一个唯一的数字编号(称为“码点”),比如“你”的码点是U+4F60,“好”是U+597D。Unicode本身只是一个字符集,不是具体的编码方案。
  • UTF-8:Unicode最流行的一种“实现方式”或“编码方案”。它是一种变长编码,完美兼容ASCII。对于ASCII字符(码点小于128),它用1个字节表示,和ASCII码一模一样。对于中文等字符,通常用3个字节表示。例如,“你好”的UTF-8编码是E4 BD A0(你)和E5 A5 BD(好)。

注意:乱码的根源,绝大多数情况下是编码与解码的不匹配。即:数据以A编码方式(如UTF-8)被存储或发送,却以B编码方式(如GBK)被解读或显示。

2.2 旅程第一站:你的源代码文件是什么编码?

当你用Keil MDK、IAR Embedded Workbench或STM32CubeIDE写下printf(“你好”)时,第一个问题就是:你这个.c.h源文件本身,是以什么编码格式保存在硬盘上的?

  • 默认陷阱:许多老版本的IDE(尤其是Keil MDK),其内置编辑器默认保存文件的编码可能是ANSI。在中文Windows系统下,“ANSI”通常指代的就是GBK编码。如果你的源代码文件是GBK编码,里面的“你好”两个字在文件里就是用两个双字节GBK码存储的。
  • 现代IDE:像STM32CubeIDE、Visual Studio Code等现代工具,更倾向于默认使用UTF-8编码。这本身是好事,但需要整个工具链保持一致。

实操检查与设置(以Keil uVision 5为例)

  1. 用记事本或Notepad++打开你的源文件。
  2. 在Notepad++中,查看右下角状态栏,会显示“UTF-8-BOM”、“ANSI(GBK)”等。
  3. 在Keil中,虽然设置选项不直观,但你可以通过“Edit -> Configuration -> Editor”查看编码设置,更可靠的方法是统一用外部编辑器(如Notepad++)将文件转换为目标编码后,再在Keil中使用。

我的心得:我强烈建议将整个项目的所有源代码文件统一保存为UTF-8无BOM格式。这是与网络、现代操作系统兼容性最好的选择。你可以在Notepad++中使用“编码 -> 转为UTF-8无BOM编码”进行批量转换。

2.3 旅程第二站:编译器如何看待这些中文?

编译器在编译你的源代码时,它会读取源文件中的字节流。对于字符串常量“你好”,编译器需要决定将哪一串字节数据放入最终的可执行文件中。

  • 关键标志:在Keil和IAR中,有一个至关重要的编译器选项:--encoding--multibyte_chars。这个选项告诉编译器:“请你把我源文件里的多字节字符串(比如中文),当作XXX编码来处理”。
  • 经典错误配置:你的源文件是UTF-8编码(“你好”占6个字节),但编译器选项设置的是“GBK”或默认的“ANSI”。那么,编译器会试图把E4 BD A0 E5 A5 BD这6个字节当作GBK编码去“解读”。GBK解码器看到E4BD,它可能认为这是一个GBK汉字,但E4 BD在GBK码表中可能对应一个完全不同的生僻字甚至非法序列,而A0等字节也可能被单独解释为控制字符。这导致编译器生成到内存中的字符串数据已经是错乱的。
  • ARM Compiler (AC6) 示例:在Keil的Options for Target -> C/C++ (AC6)选项卡中,Language / C++部分有一个Multibyte Support选项。如果你使用UTF-8源文件,这里应该保持默认或选择支持宽字符。更底层的控制可以通过Misc Controls添加--locale=english等,但核心是保持源文件与编译器认知一致。对于GCC(STM32CubeIDE所用),通常默认能较好处理UTF-8,但也要注意-finput-charset-fexec-charset参数(通常不需要手动设置)。

2.4 旅程第三站:单片机内存里的数据到底是什么?

经过(可能出错的)编译后,字符串常量被储存在单片机的Flash只读存储区。我们可以通过调试器来窥视内存,这是判断问题出在哪一步的“终极手段”。

  1. 在Keil中编译并进入调试模式。
  2. Watch窗口或Memory窗口,找到你的字符串变量或直接查看常量地址。
  3. 例如,对于char str[] = “你好”;,查看str地址开始的内存内容。
    • 如果一切正确(UTF-8源文件+编译器正确识别):你应该看到连续的6个字节:E4 BD A0 E5 A5 BD
    • 如果出现乱码(如源文件是GBK但编译器按UTF-8理解,或反之):你可能会看到像C4 E3 BA C3(这是“你好”的GBK编码)或者其他毫无规律的字节序列。

这一步是分水岭。如果内存中的数据已经是错误的,那么后面无论串口发送多么准确,显示都必然是乱码。所以,我们的首要目标是确保内存中的数据是正确的UTF-8或你期望的编码字节序列。

2.5 旅程终点站:串口助手,你用什么“字典”来解读?

假设现在单片机内存中的数据是正确的UTF-8字节序列E4 BD A0 E5 A5 BD。我们通过HAL_UART_Transmit函数,忠实地将这6个字节依次发送出去。

数据通过串口线到达PC,你的串口助手软件(如Putty、SecureCRT、MobaXterm或国产的XCOM、SSCOM)收到了这6个字节。现在,轮到串口助手软件来决定如何显示它们。它必须知道这串字节代表什么编码,才能调用正确的“字典”(解码器)将其还原为字符。

  • 串口助手的编码设置:几乎所有串口助手都有一个“编码”或“字符集”设置选项。常见选项有:ANSI(GBK)、UTF-8、ASCII等。
  • 匹配!:如果串口助手设置为“UTF-8”,它收到E4 BD A0,会识别出这是一个UTF-8三字节字符,解码后显示为“你”。完美!
  • 失配!:如果串口助手设置为“ANSI(GBK)”,它会将E4 BD A0中的E4BD尝试组合成一个GBK汉字。E4BD在GBK码表中对应的是“閮”字,而A0是一个GBK中的空格类字符。于是你可能会看到“閮 宄”之类的乱码。这就是经典的“编码发送端是UTF-8,接收解码端是GBK”导致的乱码。

3. 基于STM32CubeMX的完整解决方案与实操

理论分析完毕,我们进入实战环节。我们的目标是:在STM32平台上,实现串口稳定、正确地输出中文字符。这里以STM32F103C8T6(BluePill板)和STM32CubeMX + Keil MDK环境为例,方案完全适用于其他系列和IDE。

3.1 步骤一:STM32CubeMX工程配置

  1. 新建工程:选择你的MCU型号(STM32F103C8)。
  2. 配置串口
    • Pinout & Configuration选项卡中,找到USART1
    • 将模式设置为Asynchronous(异步通信)。
    • 查看右侧的Configuration标签,进入Parameter Settings
    • 设置Baud Rate为115200(常用),Word Length为8 Bits,Parity为None,Stop Bits为1。
    • 其他参数保持默认。此时,PA9和PA10引脚会自动被配置为USART1_TX和USART1_RX。
  3. 配置时钟树:根据你的晶振频率(BluePill板通常外部晶振8MHz),使用CubeMX的时钟配置工具,将系统时钟(HCLK)配置到最高72MHz,并确保USART1的时钟源(通常来自APB2总线)被正确使能且频率准确。准确的时钟是波特率正确的基石。
  4. 生成代码
    • Project Manager选项卡,设置项目名称、路径,选择Toolchain / IDEMDK-ARM V5
    • 关键步骤:在Project Manager -> Code Generator部分,我强烈建议勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”。这会将USART1的初始化代码独立到usart.cusart.h中,让代码结构更清晰。
    • 点击GENERATE CODE

3.2 步骤二:Keil工程关键设置(治本之策)

生成了代码,用Keil打开项目。接下来是解决乱码问题的核心步骤。

  1. 统一源代码文件编码为UTF-8无BOM

    • 关闭Keil。
    • 使用Notepad++打开项目目录下所有你自己的.c.h文件(Src/Inc/文件夹里的)。
    • 在Notepad++中,依次点击【编码】->【转为UTF-8无BOM编码】,然后保存。
    • 也可以使用Notepad++的“在文件中查找”功能,选择所有文件,然后进行批量转换。
  2. 配置编译器编码选项(针对ARM Compiler 5)

    • 在Keil中,点击魔术棒按钮Options for Target
    • 进入C/C++选项卡。
    • Misc Controls输入框中,添加以下参数:
      --locale=english
      这个参数告诉编译器使用英语本地环境处理宽字符和多字节字符,对于UTF-8源文件,这通常是一个安全的设置,能避免编译器对多字节序列进行错误的转换。对于纯ASCII和UTF-8,此设置基本够用。
    • 更彻底的方案:如果你明确知道自己在处理UTF-8,并且不希望编译器做任何转换,可以尝试添加--no_multibyte_chars。但这可能会影响宽字符wchar_t的使用,需谨慎。
  3. 配置编译器编码选项(针对ARM Compiler 6)

    • 如果你使用更新的AC6编译器,配置更简单。
    • Options for Target -> C/C++ (AC6)选项卡。
    • Language / C++部分,确保Multibyte Support选项是启用的(默认通常是)。对于UTF-8源文件,这个默认设置通常能正确工作。
    • 同样,可以在Misc Controls中添加-finput-charset=UTF-8来显式指定源文件输入字符集为UTF-8。

我的心得:经过大量项目实践,我最推荐且最稳定的组合是:源代码UTF-8无BOM + AC6编译器默认设置。这个组合在跨平台(Windows/Linux)、跨工具(Keil/IAR/CMake)时兼容性最好,几乎不会出现编码问题。对于AC5,--locale=english是一个有效的缓解措施。

3.3 步骤三:编写并验证测试代码

现在,我们在main.c的用户代码区添加测试程序。

/* 在main函数开始处,添加一个测试字符串 */ char test_str[] = "STM32串口通信测试:你好,世界!\r\n"; /* 在while(1)循环之前,发送一次 */ HAL_UART_Transmit(&huart1, (uint8_t*)test_str, strlen(test_str), 1000); /* 你也可以使用重定向后的printf,但需要先初始化 */ #include <stdio.h> #ifdef __GNUC__ #define PUTCHAR_PROTOTYPE int __io_putchar(int ch) #else #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f) #endif PUTCHAR_PROTOTYPE { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; } /* 然后在需要的地方使用printf */ printf("使用printf输出:温度:25℃\r\n");

代码解析

  • 我们定义了一个字符串数组test_str,它包含中文。
  • 使用HAL_UART_Transmit直接发送其字节内容。strlen会自动计算字符串长度(注意:对于UTF-8中文,一个汉字长度为3,strlen计算的是字节数,不是字符数,这在这里是正确的)。
  • 我们也展示了printf重定向的方法,这在调试中非常方便。重定向的本质就是将标准库的putchar函数指向我们的串口发送函数。

3.4 步骤四:硬件连接与串口助手设置

  1. 硬件连接:将STM32开发板的USART1_TX(PA9)引脚连接到USB转串口模块的RX引脚,USART1_RX(PA10)连接到USB转串口模块的TX引脚。GND互连。将USB转串口模块插入电脑。
  2. 识别端口:在Windows设备管理器中查看新增的COM口(如COM3)。
  3. 串口助手设置(至关重要!)
    • 打开你选择的串口助手(这里以功能强大的MobaXterm为例,Putty、SecureCRT类似)。
    • 选择正确的COM口,波特率115200,数据位8,停止位1,无校验位。
    • 找到并设置字符编码。在MobaXterm的串口会话窗口中,右键选择“Change terminal settings”,在“Terminal”或“Advanced”选项卡中,将“Character encoding”设置为“UTF-8”
    • 在XCOM或SSCOM中,通常有一个显式的“编码”或“字符集”下拉框,同样选择“UTF-8”
    • 如果软件没有UTF-8选项,只有“ANSI”,那么你可能需要回到步骤2,尝试将源代码转换为GBK编码,并相应调整编译器设置。但强烈建议使用支持UTF-8的终端软件

3.5 步骤五:编译、下载与现象验证

  1. 在Keil中编译工程,确保0错误,0警告。
  2. 将程序下载到STM32开发板。
  3. 打开设置好(UTF-8编码)的串口助手,连接COM口。
  4. 复位开发板。你应该在接收区清晰地看到:
    STM32串口通信测试:你好,世界! 使用printf输出:温度:25℃
    如果显示正确,恭喜你,大功告成!如果仍是乱码,请进入下一章的排查环节。

4. 系统性排查指南与进阶技巧

即使按照上述步骤操作,由于软件版本、环境差异,可能还会遇到问题。请按照以下流程进行系统性排查,这套方法能解决99%的串口中文乱码问题。

4.1 排查流程图与步骤

你可以遵循以下顺序进行诊断:

  1. 检查串口助手编码设置:这是最快、最常被忽略的一步。确保其设置为UTF-8。尝试切换为ANSI(GBK)看看乱码是否变化,如果变化,则证明是编码不匹配。
  2. 检查内存数据(终极验证)
    • 在Keil调试模式下,在Watch窗口添加test_str变量,或者右键test_str选择“Add to Memory Window”。
    • Memory窗口中,查看该地址的数据。你应该看到连续的十六进制值。将“你好”的UTF-8编码(E4 BD A0 E5 A5 BD)和GBK编码(C4 E3 BA C3)记在纸上,与内存中的数据对比。
    • 如果内存数据是E4 BD A0 E5 A5 BD:说明源代码和编译器处理正确,问题一定在串口助手的编码设置上。
    • 如果内存数据是C4 E3 BA C3:说明你的源代码可能是GBK编码,且编译器也按GBK处理了。此时,你需要将串口助手编码改为“ANSI”或“GBK”来匹配。
    • 如果内存数据是其他杂乱无章的值:说明编译环节出了问题。源头是源代码文件编码编译器编码设置不匹配。回到章节3.2,重新确认并统一。
  3. 检查编译器预处理后的文件
    • 在Keil的Options for Target -> C/C++中,勾选“Preprocessor Symbols Only”或“Preprocess to a File”,然后只编译这一个文件。
    • 查看生成的.i预处理文件。搜索你的中文字符串,看它在被编译器真正处理前变成了什么。这能帮你确认编译器“看到”的源文件内容。
  4. 尝试最简测试
    • 创建一个全新的、简单的测试工程,只做串口打印中文这一件事。排除其他复杂代码(如中断、DMA)的干扰。
    • 使用字节数组直接发送UTF-8编码值,绕过编译器的字符串处理。
      // “你好”的UTF-8编码 uint8_t utf8_hello[] = {0xE4, 0xBD, 0xA0, 0xE5, 0xA5, 0xBD, ‘\r’, ‘\n’, ‘\0’}; HAL_UART_Transmit(&huart1, utf8_hello, sizeof(utf8_hello)-1, 1000);
    • 如果这样发送,在UTF-8终端上能正确显示“你好”,则100%确定是编译器/源文件编码问题。如果还是乱码,则检查硬件连接、波特率(用示波器或逻辑分析仪看波形)等基础通信问题。

4.2 常见问题速查表

问题现象可能原因解决方案
中文显示为“锟斤拷”、“烫烫烫”通常是因为内存中出现了0xEF 0xBF 0xBD(UTF-8的替换字符�)的重复,根源可能是编译器将GBK汉字错误解码。检查并统一源文件编码和编译器设置为UTF-8。
中文显示为“????”终端或解码器无法识别接收到的字节序列,将其替换为问号。常见于UTF-8数据被用ASCII或单字节解码器解读。将串口助手编码设置为UTF-8。
中文显示为其他不认识的汉字(如“閮宄”)编码解码不匹配的典型表现。例如,UTF-8数据被用GBK解码。确保发送端(内存数据)和接收端(终端设置)编码一致。
只有部分中文乱码,英文数字正常字符串混合了不同编码方式的内容,或者在传输过程中某个字节丢失/错误(奇偶校验或波特率不准)。检查整个字符串的编码一致性;检查硬件连接和波特率容错性(尝试降低波特率)。
使用printf乱码但直接HAL_UART_Transmit正常printf重定向可能使用了宽字符版本,或者标准库内部有字符转换。确保printf重定向函数(fputc)是单字节发送。避免在printf中使用宽字符字符串(L”中文”)。
换行符\r\n显示为乱码字符终端软件可能将0x0D0x0A这两个控制字符以某种编码(如GBK)解读成了汉字的一部分。这同样是终端编码设置错误的有力佐证。坚持将终端设置为UTF-8。

4.3 进阶技巧与最佳实践

  1. 为项目建立编码规范:在团队协作或大型项目中,在README或项目规范中明确写明“本项目所有源代码文件均采用UTF-8无BOM编码”。这能从根本上避免混乱。
  2. 使用转义序列或资源文件处理复杂文本:对于界面菜单、提示信息等大量文本,不建议直接硬编码在.c文件中。可以考虑:
    • 使用const char *数组集中管理。
    • 更高级的做法是将文本存放在外部SPI Flash或SD卡中,以文件形式存储,系统运行时读取。文件本身明确编码(如UTF-8),在读取后统一处理。
  3. JSON与网络通信中的中文:当你的STM32作为HTTP客户端或MQTT客户端,需要发送包含中文的JSON数据时,务必确保整个数据包是UTF-8编码。许多JSON解析库(如cJSON)默认期望输入是UTF-8。在代码中构造JSON字符串时,就应使用UTF-8编码的中文。
  4. 调试信息国际化:如果你的产品可能需要支持多语言,那么从项目初期就规划好字符串资源的管理方式(比如使用索引号,运行时根据语言选择不同的字符串表),并统一使用UTF-8作为资源文件的编码格式。
  5. 终端软件推荐
    • MobaXterm:功能强大,内置多种编码支持,会话设置可保存,是嵌入式工程师的利器。
    • Putty:轻量、经典,编码设置在Window -> Translation里。
    • VS Code + 串口插件:如果你喜欢在VS Code中开发,可以安装串口监视插件,同样需要注意设置编码。

解决中文乱码的过程,本质上是一次对计算机系统如何表示和处理文本的深度理解。它强迫你关注从源码到二进制,再到通信传输的每一个细节。一旦你掌握了这套排查方法和“统一UTF-8”的原则,今后无论遇到蓝牙、Wi-Fi模块的通信,还是LCD屏显示中文,抑或是与云平台的数据交互,你都能从容应对,确保你的嵌入式设备能够清晰、准确地“说”好每一句话。

http://www.jsqmd.com/news/1393534/

相关文章:

  • Bow与RxSwift集成教程:响应式编程与函数式的碰撞
  • 2026年唐山3-12岁儿童数字化思维训练机构盘点 宁贤培训可圈可点 - 拜了拜了
  • 2026年云南做设计施工软装一体化的全案设计机构选哪个好:十空筑造深耕行业效果出众 - 优企甄选
  • 同一个画面颜色总不对?用 OpenColorIO-Configs 这套 ACES 颜色管理配置,三步解决跨软件色差
  • 2026山东工业废水处理设备厂家公司横向测评:舍科赛斯凭啥排第一 - 品牌报告
  • 4步快速部署DeepTutor:从零搭建你的个性化AI学习伴侣完整指南
  • 西安装修公司工期容易拖吗?签约前先问什么 - 科技焦点
  • 为什么你的 Fight Club 5e 总缺内容?FightClub5eXML 零基础完整配置指南
  • express-cdn环境配置全解析:开发模式vs生产模式的关键差异
  • 2026年AI模版官网建设公司甄选攻略:9家靠谱服务商推荐、选型标准与合作避坑全指南 - 商业大观
  • DepthSplat 实战指南:如何用 12 张照片跑通高斯溅射场景重建与深度估计
  • 用户体验细节设计:从加载反馈到表单交互的避坑指南
  • AMD显卡也能开DLSS?DLSS-Enabler免费解锁超采样,帧率最高提升50%
  • 2026年长春市佳森教育单招培训班 吉林考生高职升学全程服务指南 - 爱说大实话121
  • 混合P2P分发架构实践:解决大文件下载瓶颈
  • mdBook 安装零门槛指南:三种方式让文档站点生成器快速就位
  • 2026年唐山3-8岁幼儿思维启蒙机构挑选攻略 宁贤培训等优质品牌梳理 - 拜了拜了
  • GitSuggest源码解读:文本清洗与令牌化技术实现
  • 深入解析Apollo tools_platform:自动驾驶工具链的架构设计与工程实践
  • 2026杭州注册公司推荐:初创老板代办避坑实用攻略 - 商业新知
  • 3步告别B站弹幕刷屏:pakku.js免费神器安装与实战指南
  • GNU Emacs 从入门到进阶:一篇吃透这个可编程编辑器的完整实战指南
  • 看美剧学英语总半途而废?这6个功能让DashPlayer帮你把“追剧“变成“精学“
  • Claude AI在得物数仓的深度集成实践:从SQL开发到数据治理
  • 不开游戏,也能把《流放之路》的角色算得明明白白——Path of Building 免费离线 Build 规划器上手指南
  • 图智能AI模型快速上手:10分钟跑通GraphGPT,让大模型看懂图数据
  • pico框架核心优势解析:为何它比Viola-Jones快3倍且无需图像预处理?
  • Transformer FFN激活函数演进:从ReLU到SwiGLU的工程实践与选择
  • 已使用金蝶云星空的企业如何选择OA系统 - 企业IT选型笔记
  • I wrote a free, story-driven Python book – The Python Codex