☰
Arduino驱动STM32F103指南者串口通信实战指南
2026/10/9 12:07:32 网站建设 项目流程

1. 项目缘起与整体设计思路

1.1 为什么要在 Arduino 环境下折腾 STM32F103

很多人第一次接触 STM32F103 指南者这块板子,都是被它“国产经典入门板”的名头吸引来的。板载 STM32F103VET6,72MHz 主频,512KB Flash,64KB SRAM,外设齐全,价格又便宜,拿来学串口通信再合适不过。但问题也来了:官方推荐的工具链是 Keil MDK 或者 STM32CubeIDE,注册、配置、烧录一套流程下来,对刚入门的朋友并不友好。

Arduino 生态的好处在于“开箱即用”。它的 API 高度封装,Serial.begin()、Serial.print()这种写法几乎零学习成本,特别适合快速验证串口通信逻辑。所以这个项目的核心思路就是:用 Arduino 的编程体验,去驱动 STM32F103 的硬件串口,既保留 STM32 的性能和外设优势,又降低开发门槛。

具体来说,这个项目要解决三个问题:第一,让 Arduino IDE 能够识别并编译 STM32F103 指南者;第二,配置好硬件串口(USART1)的引脚和波特率;第三,实现稳定的收发通信,包括与电脑串口助手、与其他单片机、甚至与 Unity 等上位机软件的交互。

适合谁来参考?如果你手头正好有一块 STM32F103 指南者,又不想被 Keil 的复杂配置劝退,或者你想用 Arduino 快速做一个串口通信的小项目,那这篇内容就是为你准备的。哪怕你之前只玩过 Arduino UNO,跟着走也能跑通。

1.2 方案选型:为什么不用 HAL 库直接写

这里得说清楚一个关键选择。STM32F103 在 Arduino 环境下有两种主流玩法:一种是直接用 STM32duino 官方核心(基于 HAL 库封装),另一种是用 Roger Clark 的 Arduino_STM32 核心(基于标准外设库)。我实测下来,对于指南者这块板子,STM32duino 官方核心更稳,因为它对 F103 系列的支持已经非常成熟,串口、定时器、DMA 都有现成封装。

那为什么不直接用 STM32CubeMX 生成 HAL 库代码?因为那样就脱离了 Arduino 的语境。Arduino 的核心价值在于“快速迭代”,你改一行代码,点一下上传,几秒钟就能看到结果。而 CubeMX + Keil 的流程,光是改个波特率就要重新生成代码、重新编译、重新烧录,效率差太多。当然,如果你要做复杂的 DMA 接收或者定时器触发串口发送,那确实需要回到 HAL 库层面去精细控制,这个后面会讲到。

还有一个细节:指南者的串口引脚。STM32F103VET6 的 USART1 默认引脚是 PA9(TX)和 PA10(RX)。但指南者板子上,PA9 和 PA10 可能被其他外设占用,或者引到了特定的排针位置。我踩过的坑是:直接按默认引脚接线,结果串口助手收不到任何数据。后来查原理图才发现,板子上有跳线帽或者复用设计,需要确认实际引出的引脚。所以第一步一定是确认硬件连接,而不是急着写代码。

1.3 整体架构与数据流

整个项目的架构可以拆成三层:硬件层、固件层、上位机层。

硬件层就是 STM32F103 指南者、USB 转 TTL 模块(比如 CH340 或 CP2102)、杜邦线。注意,指南者板载了 USB 转串口芯片,但那个串口通常连的是 USART1,而且经过了电平转换。如果你要用其他串口(比如 USART2),就得外接 USB 转 TTL 模块。

固件层就是 Arduino sketch。核心是HardwareSerial对象,STM32duino 核心默认把Serial映射到 USART1。你可以用Serial1、Serial2等访问其他串口。数据流是:STM32 通过 USART1 发送数据 -> 板载 USB 转串口芯片 -> USB 线 -> 电脑。接收则反过来。

上位机层可以是 Arduino IDE 自带的串口监视器,也可以是专门的串口助手(如 SSCOM、XCOM),甚至是用 C# 写的 Unity 串口通信程序。这里的关键是波特率、数据位、停止位、校验位必须完全一致。我习惯用 115200 波特率,8 数据位,1 停止位,无校验,这是最通用的配置。

注意:STM32F103 的 USART1 挂在 APB2 总线上,最高时钟 72MHz。波特率的计算是72MHz / (16 * 波特率),所以 115200 对应的分频系数是 39.0625,实际会取 39,误差约 0.16%,完全在容忍范围内。但如果你用 9600 这种低波特率,误差会更小,通信更稳。

2. 核心细节解析与实操要点

2.1 环境搭建:从零配置 Arduino IDE 支持 STM32F103

第一步,安装 Arduino IDE。版本建议用 1.8.x 或者 2.x 都行,但 2.x 的界面变化较大,有些第三方核心的兼容性还在完善中。我目前用的是 1.8.19,稳定。

第二步,添加 STM32duino 的板管理器网址。打开“文件 -> 首选项”,在“附加开发板管理器网址”里填入:

https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json

然后打开“工具 -> 开发板 -> 开发板管理器”,搜索“STM32”,找到“STM32 MCU based boards”,点击安装。这个过程会下载几百 MB 的编译工具链,网速慢的话耐心等。

第三步,安装完成后,在开发板列表里选择“Generic STM32F1 series”,然后根据指南者的具体型号选择“STM32F103VE”。注意,指南者用的是 VET6,所以选 VE 没错。如果选错了型号,编译能过但烧录会失败。

第四步,烧录方式。指南者板载了 ST-Link 或者串口下载电路?我手头这块是串口下载,所以要在“Upload method”里选“Serial”。但串口下载需要手动进入 Bootloader 模式:把 BOOT0 跳线帽接到 1,BOOT1 接到 0,按一下复位键,然后点上传。上传完成后,再把 BOOT0 接回 0,再复位,程序才会正常运行。这个步骤很繁琐,但没办法,串口下载就是这样。

如果你有 ST-Link 调试器,那就简单多了。在“Upload method”里选“STLink”,直接点上传,自动复位运行。我后来买了一个 ST-Link V2,几十块钱,效率提升巨大。所以强烈建议:如果打算长期玩 STM32,一定要配一个 ST-Link。

2.2 串口初始化:波特率、引脚与模式配置

在 Arduino 环境下,串口初始化就一行代码:

Serial.begin(115200);

但这行代码背后做了很多事情。STM32duino 核心会自动配置 USART1 的时钟、引脚复用、波特率寄存器。默认引脚是 PA9 和 PA10。如果你要用其他引脚,比如 PA2 和 PA3(USART2),那就用Serial2.begin(115200)。

这里有个细节:STM32F103 的串口引脚是 5V 容忍的吗?PA9 和 PA10 是 5V 容忍的,但其他一些引脚不是。如果你外接 5V 的 USB 转 TTL 模块,一定要确认引脚是否支持 5V。指南者板载的 USB 转串口芯片输出的是 3.3V 电平,所以直接连没问题。但如果你用外部模块,最好选 3.3V 电平的,或者加电平转换电路。

提示:STM32F103 的 USART1 是挂在 APB2 上的,而 USART2、USART3 挂在 APB1 上。APB2 的最高时钟是 72MHz,APB1 是 36MHz。所以 USART1 的波特率上限更高,可以到 4.5Mbps,而 USART2/3 最高 2.25Mbps。一般用 115200 就够了。

2.3 数据收发:print、write 与 read 的区别

Arduino 的串口 API 里,Serial.print()和Serial.write()是两个最容易混淆的函数。print()会把数据转换成 ASCII 字符串发送,比如Serial.print(65)发送的是字符 '6' 和 '5',也就是 0x36 和 0x35。而Serial.write(65)直接发送一个字节 0x41,也就是字符 'A'。

这个区别在跟其他设备通信时特别重要。比如你跟一个只认二进制协议的模块通信,用print()就会出错。我刚开始玩的时候,用Serial.print()发送一个十六进制命令,结果对方完全没反应,后来才发现发送的是 ASCII 码。

接收方面,Serial.read()返回一个字节,Serial.available()返回缓冲区里的字节数。典型的接收循环:

void loop() { if (Serial.available() > 0) { char c = Serial.read(); // 处理字符 } }

但要注意,Serial.read()一次只读一个字节,如果数据量大,最好用Serial.readBytes()或者Serial.readString()。不过readString()会阻塞,不推荐在实时性要求高的场景用。

2.4 电平转换与硬件连接要点

STM32F103 的 IO 电平是 3.3V,而很多外设是 5V 的。如果你直接把 5V 的信号接到 STM32 的 RX 引脚,可能会烧坏芯片。所以电平转换是必须的。

最简单的电平转换方案是电阻分压。比如 5V 信号经过一个 1kΩ 和一个 2kΩ 的电阻分压,得到 3.3V。但这种方法只适合低速信号,高速下波形会变差。更好的方案是用专用的电平转换芯片,比如 TXS0108E 或者 74LVC245。

指南者板子上已经集成了 USB 转串口芯片,而且做了电平转换,所以直接用板载 USB 口就行。但如果你要用其他串口,比如 USART2 接一个 5V 的 GPS 模块,那就得自己加电平转换。

注意:STM32F103 的 BOOT0 和 BOOT1 引脚在启动时决定从哪启动。BOOT0=0 从 Flash 启动,BOOT0=1 从系统存储器启动(用于串口下载)。所以正常运行时 BOOT0 必须接地。

3. 实操过程与核心环节实现

3.1 硬件连接清单与接线步骤

先列一下我用的硬件:

  • STM32F103 指南者开发板一块
  • USB 转 TTL 模块一个(CH340G,3.3V 电平)
  • 杜邦线若干
  • 电脑一台

接线步骤:

  1. 确认指南者的 BOOT0 跳线帽接在 0 位置(GND)。
  2. 用杜邦线连接 USB 转 TTL 模块的 TXD 到指南者的 PA10(RX),RXD 到 PA9(TX),GND 到 GND。注意 TX 和 RX 要交叉连接。
  3. 如果指南者板载了 USB 转串口,可以直接用 USB 线连接电脑,不需要外接模块。但板载的串口通常只连了 USART1,而且可能占用了 PA9 和 PA10。
  4. 检查电源:指南者可以通过 USB 供电,也可以用外部 5V 或 3.3V 供电。如果同时接了两个电源,可能会冲突。

我实测下来,板载 USB 口最方便,一根线搞定供电和通信。但如果你要同时用 USART1 和 USART2,那就得外接模块。

3.2 第一个串口程序:Hello World 与回环测试

先写一个最简单的发送程序:

void setup() { Serial.begin(115200); while (!Serial) { // 等待串口初始化,STM32 上其实不需要 } Serial.println("Hello from STM32F103!"); } void loop() { Serial.println("Tick"); delay(1000); }

上传后,打开串口监视器,波特率选 115200,应该能看到每秒打印一次 "Tick"。如果乱码,检查波特率是否一致。如果没反应,检查接线和 BOOT0 跳线。

接下来做回环测试,也就是把收到的数据原样发回去:

void loop() { if (Serial.available() > 0) { char c = Serial.read(); Serial.print("Received: "); Serial.println(c); } }

在串口监视器里输入一个字符,应该能看到 "Received: x"。这个测试能验证收发都正常。

3.3 与上位机通信:C# 串口助手与 Unity 对接

如果你要用 C# 写上位机,核心是System.IO.Ports.SerialPort类。初始化:

SerialPort port = new SerialPort("COM3", 115200, Parity.None, 8, StopBits.One); port.Open(); port.DataReceived += (sender, e) => { string data = port.ReadLine(); Console.WriteLine(data); };

注意,C# 的ReadLine()会阻塞直到收到换行符,所以 STM32 那边要用println()发送,末尾带\n。

Unity 串口通信稍微麻烦一点,因为 Unity 的 Mono 运行时对串口的支持有限。我试过用System.IO.Ports,在 Windows 上能跑,但在某些版本会报错。后来改用了一个开源插件,或者直接用 C# 的异步读取。关键点是:不要在 Unity 主线程里做阻塞读取,否则画面会卡死。用Thread或者Task在后台读,然后通过消息队列把数据传给主线程。

提示:Unity 串口通信的波特率不要设太高,115200 就够了。太高的话,Unity 的帧率会受影响。另外,记得在OnDestroy()里关闭串口,否则下次运行会报“端口被占用”。

3.4 进阶:DMA 接收与首帧丢失问题

STM32F103 的串口 DMA 接收是个好东西,可以大大减轻 CPU 负担。但在 Arduino 环境下,DMA 的配置比较麻烦,因为 STM32duino 核心没有直接暴露 DMA API。你需要直接操作寄存器,或者用 HAL 库的函数。

我遇到的一个典型问题是:DMA 接收首帧丢失。原因是 DMA 通道没有及时使能,或者串口接收中断没有正确配置。解决方法是在初始化时先清空接收缓冲区,再使能 DMA。具体代码:

// 伪代码,需要根据 HAL 库调整 HAL_UART_Receive_DMA(&huart1, rxBuffer, BUFFER_SIZE); __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

然后在空闲中断里处理数据。这个方案比较复杂,如果你只是做简单的串口通信,不建议一开始就上 DMA。先用中断接收,稳定了再考虑 DMA。

3.5 参数计算:波特率误差与时钟配置

前面提到波特率的计算,这里详细说一下。STM32F103 的 USART1 时钟是 72MHz,波特率寄存器USART_BRR的值是72MHz / (16 * 波特率)。比如 115200:

72,000,000 / (16 * 115200) = 39.0625

取整数部分 39,小数部分 0.0625 * 16 = 1,所以 BRR = 0x271。实际波特率是72MHz / (16 * 39.0625) = 115200,误差为 0。但如果取 39,实际波特率是72MHz / (16 * 39) = 115384.6,误差 0.16%。这个误差在 10 位数据帧下,累积误差不到 2%,完全没问题。

如果你用 9600 波特率:

72,000,000 / (16 * 9600) = 468.75

取 468,实际波特率72MHz / (16 * 468) = 9615.4,误差 0.16%。同样很小。

所以 STM32F103 的串口波特率精度很高,不用担心。

4. 常见问题与排查技巧实录

4.1 串口无输出或乱码的排查流程

这是最常见的问题。我整理了一个排查表:

现象可能原因解决方法
完全无输出接线错误检查 TX/RX 是否交叉,GND 是否共地
完全无输出BOOT0 跳线错误确保 BOOT0 接 GND,复位后运行
完全无输出串口被占用关闭其他串口软件,重新插拔 USB
乱码波特率不匹配检查两边波特率是否一致
乱码时钟配置错误检查 STM32duino 的时钟源设置
部分乱码电平不匹配检查是否用了 5V 电平直接接 3.3V 引脚
数据丢失缓冲区溢出降低发送频率,或增大缓冲区

我踩过最坑的一次是:串口助手能收到数据,但全是乱码。查了半天,发现是 STM32duino 核心的时钟配置默认用了内部 RC 振荡器,而不是外部晶振。指南者板子上有 8MHz 晶振,但核心默认没启用。解决方法是在variant文件里修改时钟配置,或者用Serial.begin()之前手动配置 RCC。后来我换了一个核心版本,问题就消失了。

4.2 上传失败与 Bootloader 问题

串口下载时,经常遇到“上传失败”或者“无法连接到 Bootloader”。原因通常是:

  • BOOT0 没有接高电平
  • 复位键没有按对时机
  • 串口被其他程序占用
  • USB 线质量差,数据线不是数据线

我的经验是:先拔掉 USB,把 BOOT0 接 1,插上 USB,按一下复位,然后立刻点上传。上传成功后,拔掉 USB,把 BOOT0 接回 0,再插上 USB。这个流程虽然繁琐,但成功率很高。

如果你有 ST-Link,那就完全不用管 BOOT0,直接点上传就行。所以再次强调:ST-Link 是必备工具。

4.3 与 Unity/C# 通信时的典型故障

Unity 串口通信最常见的问题是“端口被占用”。因为 Unity 编辑器在停止运行后,有时候不会释放串口。解决方法是在OnApplicationQuit()里强制关闭串口,或者用try-catch包裹打开操作。

另一个问题是数据解析错误。比如 STM32 发送的是二进制数据,而 C# 用ReadLine()读取,就会一直阻塞。这时候要用ReadExisting()或者ReadByte()。我一般会在协议里加一个帧头和帧尾,比如0xAA 0x55开头,0x0D 0x0A结尾,这样解析起来更可靠。

提示:如果 Unity 里串口读取导致卡顿,可以把读取操作放在单独的线程里,然后用ConcurrentQueue把数据传给主线程。这样既不影响帧率,又能实时处理数据。

4.4 电源与干扰问题

STM32F103 指南者用 USB 供电时,如果同时驱动多个外设,可能会供电不足。表现是串口数据断断续续,或者芯片频繁复位。解决方法是用外部 5V 电源供电,或者加一个大的滤波电容。

另外,如果串口线太长,或者周围有电机、继电器等干扰源,数据也会出错。我试过用屏蔽线,效果明显好很多。如果条件允许,尽量缩短串口线长度,或者用光耦隔离。

4.5 常见问题速查表

问题排查方向快速解决
编译报错找不到板子板管理器网址错误重新添加 STM32duino 网址
上传时提示“无法打开端口”串口被占用关闭串口监视器和其他软件
程序运行一次就死机看门狗未喂狗检查是否启用了看门狗
串口数据偶尔丢失中断优先级冲突调整 NVIC 优先级
DMA 接收首帧丢失DMA 使能时机不对先清空缓冲区再使能 DMA
波特率高于 115200 出错时钟精度不够降低波特率或改用外部晶振

5. 个人实操心得与扩展思路

5.1 几个让我少走弯路的习惯

第一个习惯:每次改代码前先备份。Arduino 的 sketch 没有版本控制,改错了想回退很麻烦。我后来用 Git 管理,每个功能点提交一次,清晰很多。

第二个习惯:串口打印调试信息。STM32 没有屏幕,串口就是唯一的调试窗口。我会在关键代码处加Serial.println("Step 1"),这样一旦卡住,立刻知道在哪一步。

第三个习惯:用逻辑分析仪抓波形。如果串口通信不稳定,用逻辑分析仪看一下 TX 和 RX 的波形,能快速判断是硬件问题还是软件问题。一个几十块钱的 8 通道逻辑分析仪,比万用表好用多了。

5.2 这个项目还能怎么扩展

串口通信只是基础,往上可以做很多事情。比如:

  • 用串口接收 GPS 模块的 NMEA 数据,解析经纬度
  • 用串口控制舵机、电机驱动器
  • 用串口和 ESP8266 通信,实现物联网功能
  • 用串口和 OpenMV 通信,做视觉识别
  • 用串口和 PLC 通信,做工业控制

我最近在做一个项目,用 STM32F103 的串口接收微波炉控制面板的按键数据,然后通过另一个串口发送给显示屏。整个系统用 Arduino 框架开发,两天就跑通了。如果换成 Keil + HAL 库,估计得一周。

5.3 关于 STM32F103 最小系统的选型建议

如果你还没有板子,想买一个 STM32F103 最小系统,我建议注意几点:

  • 确认晶振是 8MHz,有些便宜板子用的是 12MHz,时钟配置不一样
  • 确认有 BOOT0 和 BOOT1 跳线,方便串口下载
  • 确认 USB 转串口芯片是 CH340 或 CP2102,驱动好装
  • 确认引出了所有 IO 口,方便扩展

指南者这块板子在这些方面做得不错,但价格比普通最小系统贵一点。如果预算有限,买个普通的最小系统板也行,就是接线麻烦点。

5.4 最后分享一个串口调试小技巧

如果你在调试串口时,不确定对方发的是什么数据,可以用一个“十六进制显示”的串口助手。比如 SSCOM 就有这个功能。把显示模式切成 HEX,就能看到原始字节。我经常用这个功能来抓协议帧,比看 ASCII 码直观多了。

另外,如果你要发送十六进制数据,在 Arduino 里可以这样写:

byte cmd[] = {0xAA, 0x55, 0x01, 0x02, 0x0D, 0x0A}; Serial.write(cmd, sizeof(cmd));

这样发送的就是纯二进制,不会经过 ASCII 转换。接收端用 HEX 模式看,一目了然。

这个项目虽然简单,但涉及的知识点不少:硬件接线、电平转换、时钟配置、波特率计算、中断处理、上位机通信。每一个环节都有坑,但踩过之后,你对串口通信的理解会深很多。我到现在还在用这套方案做各种小项目,稳定、快速、够用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询