Blue Pill 上的隔离实验,最值得先弄明白的不是代码怎么写,而是你做的隔离到底是电源隔离、信号隔离,还是数据逻辑隔离。这几种隔离经常被混在一起,实际踩坑以后才会发现:不同隔离的目标、器件和验证方式完全不一样。我的一次真实经历是在一块 Blue Pill 和电机驱动板共地之后,通信接口直接冒烟,从那以后,我在这块 STM32F103C8T6 最小系统板上做任何外部连接,都会先把隔离层想清楚。这篇文章是围绕 Blue Pill 做隔离实验的一整套记录,包括光耦隔离、隔离电源、隔离串口与 CAN、软件任务隔离,以及最后用 Isolation Forest 对采集数据做异常筛查的完整链路。
1. 先弄清楚 Blue Pill 上的隔离到底隔离什么
1.1 Blue Pill 是一块没有硬件 MPU 的常规开发板
Blue Pill 使用的是 STM32F103C8T6,这是一颗 Cortex-M3 内核的单片机,最高主频 72MHz,内置 64KB Flash 和 20KB RAM。板子本身非常便宜,引脚也基本都引出来了,很多嵌入式入门者都用它做点灯、串口、ADC、CAN 等实验。
但要特别记住一点:STM32F103 没有硬件 MPU,也就是内存保护单元。这意味着它不能像 STM32F4、STM32F7 那样,在硬件层面给代码画一块“禁止访问”的内存区域。很多人以为跑一个 FreeRTOS 就能实现任务隔离,严格来说不是这样。没有 MPU 的情况下,一个任务完全可以越界改写另一个任务的堆栈、全局变量,甚至系统内核数据。系统当时可能不报错,但一个随机中断或者一段野指针就能让整机崩溃。
所以,在 Blue Pill 上做隔离实验,首先要把“隔离”的边界搞清楚。硬件上能做的隔离,主要是外部电路层:电源隔离、信号隔离、总线隔离。软件上能做的隔离,主要是逻辑隔离:通过队列、临界区、任务约定来降低数据竞争和越界访问的风险。这两种隔离不能互相替代。
1.2 隔离不是简单“断开地线”
很多第一次接触隔离的人,会认为隔离就是把两边的地线分开,不连在一起就行。这个理解不够准确。隔离的完整目的是建立明确的能量边界和信息边界,具体包括四层:
- 电源隔离:给隔离侧提供独立电源,避免地环路和共模干扰。
- 信号隔离:通过光耦、数字隔离器、隔离收发器等器件传输信号,不让两侧电气直接连通。
- 逻辑隔离:在代码里限制任务间直接访问,通过队列、信号量、事件组等方式传递数据。
- 数据隔离:对采集结果做异常检测,把异常数据从正常数据里分离出来,比如 Isolation Forest 这类的算法。
其实这四层是一个递进关系。电源不通,光耦后的信号再干净也可能因为地电位差损坏器件;只隔离电源但信号线直接连接,也等于没有隔离。所以实验顺序我建议固定为:电源隔离 -> 信号隔离 -> 逻辑隔离 -> 数据异常隔离。后面每个实验都是按这个顺序写的。
2. 实验环境搭建:板卡资源和材料清单
2.1 Blue Pill 的关键资源
做隔离实验之前,先确认手里 Blue Pill 的版本。常见蓝色 PCB 板载一颗 8MHz 晶振,也有部分版本是 16MHz 晶振,这会影响串口波特率。建议先通过一个简单的串口回环测试确认板子能正常工作,再开始接隔离模块。
Blue Pill 引出的关键外设包括:
- 3 个 USART(USART1 在 PA9/PA10,USART2 在 PA2/PA3,USART3 在 PB10/PB11)
- 2 个 SPI、2 个 I2C
- 1 个 CAN(PB8/PB9)
- 10 个 ADC 输入通道
- 多个 PWM 输出
在隔离实验中,我主要用到 USART、CAN、ADC 和 GPIO。这些外设足够覆盖大部分传感器采集和现场通信场景。
2.2 隔离实验需要的模块和工具
下面是我搭建这套环境时用的材料清单,你可以根据自己的需求增减。
| 材料 | 用途 | 说明 |
|---|---|---|
| Blue Pill 开发板 | 主控 | STM32F103C8T6,逻辑电平 3.3V |
| USB-TTL 模块 | 程序烧录、串口打印 | 建议使用带隔离的 USB-TTL,避免调试过程中引入地环路 |
| 光耦 PC817 或 6N137 | GPIO 输入信号隔离 | PC817 适合低速率信号,6N137 适合脉冲和高速信号 |
| 隔离 DC-DC 模块 | 隔离侧供电 | 常见型号有 B0505S,输入 5V,输出 5V,输入输出地不连通 |
| 数字隔离器 ADuM1201、ISO7221 | 隔离 UART 信号 | 双通道,可隔离 TX/RX |
| 隔离 CAN 收发器 ISO1050 | 隔离 CAN 总线 | 内部集成隔离,总线侧和 MCU 侧不共地 |
| 非隔离 CAN 收发器 TJA1050 | 对比测试 | 用于观察非隔离方案的不足 |
| 面包板、杜邦线、电阻、万用表 | 电路搭建、测试 | 必备工具 |
| 示波器 | 观察波形质量 | 没有示波器时可以用串口数据是否稳定作为判断依据 |
工具方面,万用表是必须的。用来确认两个地之间是否完全隔离,以及光耦输入侧和输出侧的电压是否正常。示波器不是必须,但如果要做 CAN 或者高速 UART 通信,有示波器会直观很多。
环境的搭建顺序很重要。先给 Blue Pill 单独供电,烧录一个最简单的闪灯程序,确认开发板正常。然后接 USB-TTL,确认串口能打印。最后再接隔离模块。不要一上来就把所有模块都接上去,否则出了问题很难定位是电源、接线、光耦、隔离器还是代码问题。
3. 实验一:用光耦实现 GPIO 输入隔离
3.1 电路连接和器件选型
光耦隔离是最常见、成本最低的信号隔离方式。原理是把电信号转换成光信号,再转换成另一侧的电信号。两侧之间只有光路,没有电气连接。
我用的测试场景是:外部一个 5V 的传感器信号需要输入到 Blue Pill 的 GPIO,但外部电源和 Blue Pill 的 GND 不共用。为了安全,我用 PC817 做隔离。
典型接线方式如下:
- PC817 输入侧:
- 正极:外部 5V 信号通过一个限流电阻接过来。
- 负极:接外部 GND。
- PC817 输出侧:
- 集电极(C):接 Blue Pill 的 3.3V。
- 发射极(E):接 Blue Pill 的 GND。
- 同时集电极与 MCU GPIO 之间接一个上拉电阻,或者把 GPIO 设为上拉输入。
这里要注意限流电阻的计算。PC817 输入侧是一个红外发光二极管,正常工作电流一般取 5mA 到 10mA。外部信号如果是 5V,二极管压降约 1.2V,那限流电阻大概取 (5V - 1.2V) / 5mA = 760Ω,实际可以选 680Ω 或 1kΩ。如果外部信号是 12V 或 24V,电阻值要重新算,不能直接照搬。
输出侧上拉电阻取 4.7kΩ 到 10kΩ 都可以。上拉电阻太小会增大功耗,太大会导致上升沿变慢,影响信号频率。低速开关量信号用 10kΩ 更稳定。
接线时我踩过一个坑:光耦的二极管极性接反。PC817 的输入侧如果反向接入,输出侧永远不会有变化。所以接好以后第一件事不是写代码,而是用万用表量一下输入侧两端电压,5V 信号输入时应该能看到约 1V 左右的压降。
3.2 STM32 侧代码与验证方法
光耦输出侧接到 Blue Pill 的 GPIO,我用的是 STM32 HAL 库,把 PB0 配置为输入模式,开启上拉。
GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);主循环里读取这个引脚,并打印电平值。
while (1) { GPIO_PinState state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0); if (state == GPIO_PIN_RESET) { // 光耦导通,外部信号有效 HAL_UART_Transmit(&huart1, (uint8_t *)"ON\n", 3, 100); } else { HAL_UART_Transmit(&huart1, (uint8_t *)"OFF\n", 4, 100); } HAL_Delay(50); }为什么光耦导通时读取到低电平?因为输出侧是集电极开路结构。光耦内部三极管导通后,集电极和发射极之间接近短路,GPIO 被拉到 GND,所以读到低电平。光耦断开时,GPIO 通过内部上拉变成高电平。
验证方法也比较简单:在外部信号侧给 0V 和 5V 交替输入,观察串口输出是否对应切换。如果一直不变,先检查外部信号是否真的送到了光耦输入侧,再检查输出侧上拉和 GPIO 配置。
这个实验完成以后,你就得到了一路隔离的开关量输入。如果信号频率比较高,比如 PWM 方波,PC817 的响应速度会跟不上,可以考虑换成 6N137,但 6N137 的输入侧驱动电流和输出侧上拉要求不完全一样,需要单独看器件手册。
4. 实验二:隔离 UART 和隔离 CAN 通信
4.1 用数字隔离器搭 UART
光耦适合低频开关量,但要用在 UART 通信上,PC817 这种慢光耦就不太合适了。UART 的波特率即使只有 9600,也需要信号在上升沿和下降沿有足够的速度。我建议使用数字隔离器,比如 ADuM1201、ISO7221 这类双通道器件。
数字隔离器和光耦不一样,它内部没有发光二极管,而是通过电容或磁耦合传递数字信号。延迟低、传输速率高,有些型号能做到几十 Mbps。对于 Blue Pill 的 UART 最常见 115200 波特率来说,绰绰有余。
接线方式如下:
- 发送端 Blue Pill 的 TX -> 隔离器输入通道 1 -> 输出通道 1 -> 接收端 Blue Pill 的 RX
- 接收端 Blue Pill 的 TX -> 隔离器输入通道 2 -> 输出通道 2 -> 发送端 Blue Pill 的 RX
注意:隔离器的输入侧和输出侧必须使用完全独立的两组电源。两侧 GND 不能连在一起。如果你把两侧的 GND 短接了,那隔离就失去了意义。
准备好以后,用一个最普通的串口程序就能验证。发送端周期性发送一个字符串,接收端收到以后打印出来。
// 发送端 uint8_t msg[] = "hello isolator\n"; HAL_UART_Transmit(&huart1, msg, strlen((char *)msg), 100); HAL_Delay(100);// 接收端 uint8_t rx_buf[64] = {0}; HAL_UART_Receive(&huart1, rx_buf, 14, 1000);如果接收端出现乱码、丢字节,先不要怀疑隔离器。首先要确认两边的波特率是否一致,然后再检查 USB-TTL 的地、蓝板晶振频率、串口线是否接触良好。数字隔离器如果接线正确,它对串口波形的影响在工程上是可以忽略的。
4.2 隔离 CAN 收发器测试
CAN 是工业设备里最常见的总线。Blue Pill 本身带一路 bxCAN 控制器,但 CAN 控制器只能输出 TX 和 RX 信号,真正接到总线上的需要 CAN 收发器,比如 TJA1050。非隔离方案里,TJA1050 的 GND 直接和 MCU 的 GND 相连。当两个 CAN 节点之间的距离比较远时,两个节点的地电位可能有差异,这个差异会直接加在收发器上,严重时会导致收发器损坏。
隔离 CAN 收发器 ISO1050 的作用,就是把 MCU 侧和总线侧隔开。接线时,MCU 的 CAN_TX / CAN_RX 接到 ISO1050 的输入侧,ISO1050 的输出侧连接 CANH 和 CANL,输出侧的供电和地完全独立。
CAN 总线两端各接一个 120Ω 终端电阻。如果只有一个节点,可以把 CAN_TX 和 CAN_RX 短接,做自发自收测试。
我使用 HAL 库的 bxCAN 外设时,会先做最小初始化。注意 STM32F103 的 CAN 引脚默认映射在 PB8(RX)和 PB9(TX)。
CAN_HandleTypeDef hcan; hcan.Instance = CAN1; hcan.Init.Prescaler = 4; hcan.Init.Mode = CAN_MODE_NORMAL; hcan.Init.SyncJumpWidth = CAN_SJW_1TQ; hcan.Init.TimeSeg1 = CAN_BS1_13TQ; hcan.Init.TimeSeg2 = CAN_BS2_2TQ; hcan.Init.TimeTriggeredMode = DISABLE; hcan.Init.AutoBBusOffRecovery = ENABLE; hcan.Init.AutoWakeUp = DISABLE; hcan.Init.AutoRetransmission = ENABLE; hcan.Init.ReceiveFifoLocked = DISABLE; hcan.Init.TransmitFifoPriority = DISABLE; HAL_CAN_Init(&hcan);CAN 的波特率由 APB1 时钟和 Prescaler 决定,不同开发板的晶振配置会影响最终波特率。如果两个节点通不上,先查波特率,再查终端电阻,最后查隔离收发器的供电和地。
隔离 CAN 的实验做完以后,你就能理解为什么工业现场倾向于使用隔离收发器。当两台设备的供电来源不同,或者存在电机启停带来的地电位波动时,隔离 CAN 的成功率明显更高。非隔离方案在调试桌上可能正常,一到现场就可能出现偶发错误。
5. 实验三:没有 MPU,怎么在软件层做隔离
5.1 为什么 Blue Pill 上不要谈硬件任务隔离
前面已经提到,STM32F103 没有 MPU。在 FreeRTOS 或者其他 RTOS 中创建多个任务,并不等于这些任务之间有了硬件隔离。实际上,任何一个任务都能通过指针访问任意地址,包括别的任务的堆栈、全局变量、外设寄存器。一旦任务 A 越界写坏了任务 B 的堆栈,系统可能在一个随机位置崩溃。
所以,在 Blue Pill 上做软件隔离,目标不是“防止越界”,而是“减少越界的可能,并降低越界后的影响”。最实用的做法是:
- 任务之间不要共享全局缓冲区。
- 需要传递数据时,使用 RTOS 提供的队列、信号量、事件组。
- 必须共享变量时,用临界区保护。
- 保持每个任务的堆栈容错一部分余量,但不能依赖这个余量。
这套方法没有硬件保护强,但这是 Blue Pill 这类中低端 MCU 上最现实的软件隔离方案。
5.2 FreeRTOS 队列示例
以最常见的“采集-处理”两个任务为例。采集任务读取 ADC,处理任务负责滤波和通信。如果两个任务直接访问同一个全局变量,读写的时序很难保证。用队列以后,采集任务只负责发送,处理任务只负责接收,数据在队列内部完成复制。
#include "FreeRTOS.h" #include "task.h" #include "queue.h" typedef struct { uint16_t adc_value; uint32_t timestamp; } sensor_data_t; QueueHandle_t sensor_queue; void vSensorTask(void *arg) { sensor_data_t data; for (;;) { data.adc_value = read_adc(); data.timestamp = HAL_GetTick(); xQueueSend(sensor_queue, &data, pdMS_TO_TICKS(10)); vTaskDelay(pdMS_TO_TICKS(100)); } } void vProcessTask(void *arg) { sensor_data_t data; for (;;) { if (xQueueReceive(sensor_queue, &data, portMAX_DELAY) == pdPASS) { process_data(data.adc_value, data.timestamp); } } }队列本身实现了临界区保护,生产者和消费者之间不需要关心对方的状态。这个模式在 Blue Pill 上非常实用,也容易复用。如果只有 20KB RAM,队列长度和数据体大小不要设得太大,一个保存 5 条到 10 条数据的队列已经够用。
另一个需要注意的问题是临界区。如果要保护一段共享变量,可以用taskENTER_CRITICAL()和taskEXIT_CRITICAL(),但不要在里面做延时、打印或者浮点运算。关中断时间太长,会影响实时性,还可能让看门狗误触发。优先用队列,而不是关中断,这是我在实际项目里总结出来的经验。
6. 实验四:用 Isolation Forest 检查隔离数据中的异常
6.1 采集数据并传到 PC
硬件隔离做完以后,数据应该变得相对干净。但“相对干净”也要有验证手段。我一般会把 Blue Pill 采集到的传感器电压通过隔离 UART 发送到 PC,保存成 CSV 文件,然后用 Python 做一次 Isolated Forest 异常检测。
先看 Blue Pill 侧发送代码。采集一个 ADC 引脚的电压,计算成实际电压值,加上时间戳,一行一行通过串口发出去。
while (1) { HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK) { uint16_t adc_value = HAL_ADC_GetValue(&hadc1); float voltage = adc_value * 3.3f / 4096.0f; char buf[64]; snprintf(buf, sizeof(buf), "%.3f\n", voltage); HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 100); } HAL_Delay(100); }PC 侧用 pyserial 读取。
import serial ser = serial.Serial('COM7', 115200, timeout=1) with open('samples.csv', 'w') as f: f.write('value\n') while True: line = ser.readline().decode('utf-8', errors='ignore').strip() if line: f.write(line + '\n')采集时间可以持续几分钟,数据量不要太多,几千条足够。采集完成后关闭串口,再进入分析步骤。
6.2 Isolation Forest 的 Python 实现与判读
Isolation Forest 是一种基于“隔离”思想的异常检测算法。它的核心逻辑是随机选择特征和切分点,把样本空间不断切分。正常数据往往需要更多次切分才能被隔离,而异常数据因为分布孤立,通常很少几次切分就会被分出来。
用 scikit-learn 实现非常简单。
import pandas as pd from sklearn.ensemble import IsolationForest df = pd.read_csv('samples.csv') values = df[['value']].values clf = IsolationForest(contamination=0.01, random_state=0) df['anomaly'] = clf.fit_predict(values) anomalies = df[df['anomaly'] == -1] print(anomalies)fit_predict返回值为 1 表示正常,-1 表示异常。contamination参数表示你预计异常数据大概占多少比例。这里我取 0.01,也就是大约 1% 的异常率。如果数据本身很稳定,异常率可以设得更低。
需要注意的是,这个算法不能代替硬件隔离。它的作用是当数据出现异常时,从统计上告诉你“这段数据有问题”。如果隔离电路正常,数据基本都集中在很小的波动范围内,异常点很少。如果经常出现异常点,说明可能存在地环路、共模干扰、电源纹波或者光耦工作不稳定。不要急着调contamination参数去掩盖异常,应该回到硬件上去查问题。
7. 实验中的常见坑和排查顺序
7.1 电源隔离的坑
电源隔离是整个实验的基础。如果隔离电源出了问题,后面的光耦、隔离器、CAN 收发器都可能不稳定。
我遇到过的常见情况有三种:
- 隔离模块空载时输出电压偏高,带载以后慢慢恢复正常。
- 万用表量隔离两侧地时,蜂鸣器会响,但其实是隔离模块内部的寄生电容导致的,并不是地真正连在一起。
- 把隔离模块输出侧的 GND 和输入侧的 GND 接到了一起,导致两个电源系统被强制共地。
排查电源问题时,先用万用表确认两侧地没有直流导通。然后给隔离侧接一个假负载,比如一个 1kΩ 电阻,再测量电压。不要只用蜂鸣器档判断隔离状态,最好用电压档对比两侧电压值。
7.2 信号隔离和通信的排查
通信实验出问题时,按下面顺序排查:
- 先看 MCU 和模块之间是否有公共点没有接好,比如 GND、VCC、使能脚。
- 再看波特率、引脚复用时序是否错误。
- 用串口打印关键节点电平,确认光耦、隔离器输出侧有没有正确翻转。
- 用示波器观察波形,重点看上升沿是否平滑、信号幅度是否达到 MCU 的 VIH/VIL 要求。
- 最后再怀疑隔离器本身。
在 Blue Pill 上,很多问题不是隔离导致的,而是外部引脚配置错误、时钟配置不对、USB-TTL 的 GND 没有连接等。建议每次只改一个变量,比如先不接隔离器直接串口连接,确认正常后再加入隔离器。这样能快速定位是哪一层引入的问题。
8. 后续实验建议
这套实验做完后,你还可以继续扩展:
- 用数字隔离器搭建隔离 SPI,让 Blue Pill 和外部 ADC 芯片通信。
- 用两片 Blue Pill 搭配一对小功率隔离电源,组成无公共地的无线透传实验。
- 换带 MPU 的 STM32F4 开发板,重新做一套 memory protection 隔离,对比 Blue Pill 与 STM32F4 在可靠性上的差异。
- 在 Isolation Forest 里加入多维度特征,比如电压、电流、温度,做更完整的异常检测模型。
我个人更建议先把单层隔离做扎实,再去做组合实验。Blue Pill 本身虽然便宜,但它能让你用很低的成本理解工业隔离的核心逻辑:隔离电源负责隔离能量,光耦和数字隔离器负责隔离信号,队列和临界区负责隔离代码逻辑,Isolation Forest 负责隔离异常数据。把这几层都做通,再面对更高端的项目时,才会知道每一步应该在哪里设防。