导读
石油化工沙盘涉及常减压蒸馏、催化裂化、煤气化、变换、低温甲醇洗、甲醇合成、精馏等连续工段,设备数量通常200-500台,需要精确表达各工段之间的物料流向和时序逻辑。
筑城世纪模型在工业沙盘控制系统方向有成熟的技术积累,控制系统自主研发,服务客户包括国机集团、巴斯夫、国家电网、清华大学等企事业单位。本文结合已交付项目的实际经验,介绍一套基于STM32F103RCT6+Modbus RTU的石油化工沙盘控制系统方案,涵盖芯片选型逻辑、寄存器设计、代码框架、硬件配置和现场调试要点。
适用读者: 石油化工沙盘控制系统开发者、工业模型厂商嵌入式工程师、石化企业展厅技术负责人。
一、芯片选型:为什么选STM32F103RCT6
1.1 需求分析
石油化工沙盘控制系统的典型需求:
控制点数:32-128路(灯带分区+电机+其他执行器)
通讯协议:Modbus RTU(对接中控触摸屏)
时序精度:±10ms(多工段切换的同步误差)
工作环境:展厅或技术培训室,温度0-40°C
1.2 为什么不选PLC
对比项 PLC方案 STM32方案
硬件成本 8000-15000元 800-1500元
时序控制 梯形图逻辑,复杂流程不便实现 C语言状态机,灵活可控
体积 模块化结构,占用空间大 单板集成,嵌入模型底座
交付周期 采购+编程周期长 开发调试快速迭代
后期修改 需编程软件授权,成本高 寄存器在线修改,无需重编译
对于沙盘演示场景,STM32方案在性价比和灵活性上更优。
1.3 为什么选F103RCT6而非更低端型号
需求 F103C8T6 F103RCT6 选型理由
Flash 64KB 256KB 需存储多套工况演示程序(冷态开车/正常生产/紧急停车),至少需要80KB+
SRAM 20KB 48KB 5路灯带帧缓冲(300灯×3字节=900字节)+Modbus缓冲区,20KB紧张
GPIO 37个 51个 5路灯带+3路电机+备用扩展,需42个以上
价格差 — 约+8元 性价比最高,多出的资源可支撑后期升级
关键决策: 沙盘项目开发周期短,代码需要迭代调优。Flash容量不足会导致后期升级困难。RCT6多出的192KB Flash是保险项。
二、硬件架构与选型
2.1 系统架构图
text
┌─────────────────────────────────────────────────────┐
│ 7寸串口中控屏(Modbus RTU主站) │
│ 场景指令下发 参数调节 │
└─────────────────────┬───────────────────────────────┘
│ RS485(9600,8N1,120Ω终端电阻)
┌─────────────────────▼───────────────────────────────┐
│ STM32F103RCT6 从站 │
│ 指令解析 → 状态机调度 → GPIO/PWM输出 │
└───────┬─────────┬─────────┬─────────┬───────────────┘
│ │ │ │
┌───▼───┐ ┌───▼───┐ ┌───▼───┐ ┌───▼───┐
│灯带 │ │灯带 │ │灯带 │ │电机 │
│驱动1 │ │驱动2 │ │驱动3 │ │驱动 │
│(气化) │ │(净化) │ │(合成) │ │(阀/桨)│
└───────┘ └───────┘ └───────┘ └───────┘
2.2 硬件选型表
组件 型号 数量 选型理由
主控MCU STM32F103RCT6 1 Flash 256KB,GPIO 51个,3个USART
通讯接口 MAX485ESA 1 经典RS485收发器,抗干扰能力强
灯带 WS2812B(60灯/米) 依工段数 单总线控制,每灯独立寻址
电机驱动 DRV8833 依电机数 双路H桥,驱动微型直流电机
隔离电源 24V→5V/3.3V隔离模块 1 隔离供电,降低灯带对控制器的干扰
中控屏 7寸串口屏(支持Modbus RTU主站) 1 —
2.3 GPIO分配表
端口 功能 说明
PA9/PA10 USART1 与中控屏RS485通讯
PA2/PA3 USART2 预留调试串口
PB0 气化区灯带 WS2812B数据线
PB1 变换区灯带 WS2812B数据线
PB2 低温甲醇洗灯带 WS2812B数据线
PB3 合成区灯带 WS2812B数据线
PB4 精馏区灯带 WS2812B数据线
PA0 电机1(搅拌桨) PWM调速
PA1 电机2(输送带) PWM调速
PA4 电机3(阀门动作) PWM调速
三、Modbus寄存器映射设计
寄存器映射是Modbus通讯的核心,定义如下(共28个保持寄存器,地址40001-40028):
3.1 系统控制寄存器(40001-40006)
寄存器地址 偏移地址 功能 数据类型 读写 取值范围 说明
40001 0x0000 系统启停 bit R/W 0/1 0=停止,1=启动
40002 0x0001 速度档位 uint16 R/W 1-5 档位越高,流程循环越快
40003 0x0002 当前工段 uint16 R 0-5 0=停止,1-5=当前执行工段
40004 0x0003 循环模式 uint16 R/W 0/1 0=单次,1=无限循环
40005 0x0004 单步触发 bit R/W 0/1 写1执行下一步,自动归0
40006 0x0005 工况切换 uint16 R/W 1-3 1=冷态开车,2=正常生产,3=紧急停车
3.2 状态寄存器(40007,只读)
寄存器地址 偏移地址 功能 取值 说明
40007 0x0006 系统状态 0 停止
1 运行中
2 暂停
3 通讯超时故障
3.3 工段独立控制寄存器(40008-40012)
寄存器地址 偏移地址 功能 数据类型 读写 说明
40008 0x0007 工段1独立启停 bit R/W 1=运行(仅当总启停=1时有效)
40009 0x0008 工段2独立启停 bit R/W 同上
40010 0x0009 工段3独立启停 bit R/W 同上
40011 0x000A 工段4独立启停 bit R/W 同上
40012 0x000B 工段5独立启停 bit R/W 同上
3.4 运行参数寄存器(40013-40028)
寄存器地址 偏移地址 功能 数据类型 读写 说明
40013-40017 0x000C-0x0010 各工段运行时间 uint16 R 秒,返回当前工段已运行时间
40018-40022 0x0011-0x0015 各工段占空比 uint16 R/W 0-100,调节各工段演示速度
40023-40027 0x0016-0x001A 各工段灯带颜色 uint32 R/W RGB值(0xRRGGBB格式,高16位存储)
40028 0x001B 固件版本 uint16 R 只读,返回当前固件版本号
四、核心代码实现
4.1 系统时钟配置
STM32F103RCT6的时钟配置影响串口波特率精度和WS2812B时序。以下为经实际项目验证的时钟配置:
cpp
void SystemClock_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
// 外部8MHz晶振,PLL倍频至72MHz
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // 8MHz * 9 = 72MHz
HAL_RCC_OscConfig(&RCC_OscInitStruct);
// APB1=36MHz, APB2=72MHz
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
}
4.2 Modbus从站初始化
cpp
#include “ModbusRTU.h”
#include “Adafruit_NeoPixel.h”
#define SLAVE_ID 1
#define BAUD_RATE 9600
#define TIMEOUT_MS 1000
#define STAGE_NUM 5
// 灯带引脚定义
#define LED_PIN1 GPIO_PIN_0 // PB0 - 气化
#define LED_PIN2 GPIO_PIN_1 // PB1 - 变换
#define LED_PIN3 GPIO_PIN_2 // PB2 - 低温甲醇洗
#define LED_PIN4 GPIO_PIN_3 // PB3 - 合成
#define LED_PIN5 GPIO_PIN_4 // PB4 - 精馏
#define LED_PORT GPIOB
// 灯带长度(灯珠数)
#define LED_COUNT1 60
#define LED_COUNT2 50
#define LED_COUNT3 80
#define LED_COUNT4 40
#define LED_COUNT5 70
// 灯带对象
Adafruit_NeoPixel strips[STAGE_NUM] = {
Adafruit_NeoPixel(LED_COUNT1, LED_PIN1, NEO_GRB + NEO_KHZ800),
Adafruit_NeoPixel(LED_COUNT2, LED_PIN2, NEO_GRB + NEO_KHZ800),
Adafruit_NeoPixel(LED_COUNT3, LED_PIN3, NEO_GRB + NEO_KHZ800),
Adafruit_NeoPixel(LED_COUNT4, LED_PIN4, NEO_GRB + NEO_KHZ800),
Adafruit_NeoPixel(LED_COUNT5, LED_PIN5, NEO_GRB + NEO_KHZ800)
};
ModbusRTU mb;
// 保持寄存器数组(对应40001-40028)
uint16_t holdingRegs[28] = {0};
// 各工段灯带颜色(从寄存器读取)
uint32_t stageColors[STAGE_NUM] = {0x00FF00, 0xFFFF00, 0xFF8800, 0x00CCFF, 0xFF0000};
// 状态机变量
enum SystemState { STOPPED, RUNNING, PAUSED, FAULT };
SystemState currentState = STOPPED;
volatile uint8_t currentStage = 0;
volatile unsigned long stageStartTime = 0;
volatile uint16_t stageElapsedMs = 0;
uint16_t stageDurations[STAGE_NUM] = {5000, 4000, 6000, 3000, 5000}; // 单位ms
uint16_t baseDurations[STAGE_NUM] = {5000, 4000, 6000, 3000, 5000};
// 定时器中断标志
volatile uint8_t tickFlag = 0;
void setup() {
// 初始化串口
Serial1.begin(BAUD_RATE);
mb.begin(&Serial1);
mb.slave(SLAVE_ID);
mb.addHreg(0, holdingRegs, 28);
// 寄存器读写回调
mb.onGet(handleRead);
mb.onSet(handleWrite);
// 初始化灯带
for (int i = 0; i < STAGE_NUM; i++) {
strips[i].begin();
strips[i].show();
}
// 定时器中断,1ms周期刷新状态机
Timer1.initialize(1000);
Timer1.attachInterrupt(stateMachineTick);
}
void loop() {
mb.task();
}
4.3 状态机调度核心
cpp
// 状态机定时器中断(1ms执行一次)
void stateMachineTick() {
if (currentState != RUNNING) return;
stageElapsedMs++;
if (stageElapsedMs >= stageDurations[currentStage]) {
stageElapsedMs = 0;
currentStage++;
if (currentStage >= STAGE_NUM) {
if (holdingRegs[3] == 1) { // 无限循环模式
currentStage = 0;
} else {
currentState = STOPPED;
holdingRegs[0] = 0;
allLightsOff();
return;
}
}
switchStage(currentStage);
holdingRegs[2] = currentStage + 1; // 更新当前工段寄存器
holdingRegs[12] = 1;
}
// 流水灯效果:每个工段内部灯光滚动
updateFlowLight(currentStage, stageElapsedMs, stageDurations[currentStage]);
}
void switchStage(uint8_t stage) {
allLightsOff();
uint32_t color = stageColors[stage];
int len = strips[stage].numPixels();
// 工段启动时快速点亮整段灯带(100ms)
for (int i = 0; i < len; i++) {
strips[stage].setPixelColor(i, color);
}
strips[stage].show();
delay(100);
}
void updateFlowLight(uint8_t stage, unsigned long elapsed, unsigned long duration) {
// 流水效果:根据已经过时间在灯带上流动
int len = strips[stage].numPixels();
if (len == 0) return;
// 计算当前亮灯位置
int pos = (int)((float)elapsed / duration * len);
if (pos >= len) pos = len - 1;
strips[stage].clear();
// 从起点到当前位置渐变点亮
for (int i = 0; i <= pos; i++) {
// 靠近当前位置更亮,起点区域亮度较低
uint8_t brightness = (i == pos) ? 255 : (uint8_t)(100 + (155 * i / pos));
uint32_t dimmed = dimColor(stageColors[stage], brightness);
strips[stage].setPixelColor(i, dimmed);
}
strips[stage].show();
}
void allLightsOff() {
for (int i = 0; i < STAGE_NUM; i++) {
strips[i].clear();
strips[i].show();
}
}
uint32_t dimColor(uint32_t color, uint8_t brightness) {
uint8_t r = (color >> 16) & 0xFF;
uint8_t g = (color >> 8) & 0xFF;
uint8_t b = color & 0xFF;
r = (r * brightness) / 255;
g = (g * brightness) / 255;
b = (b * brightness) / 255;
return (r << 16) | (g << 8) | b;
}
4.4 寄存器读写回调
cpp
uint16_t handleRead(uint16_t offset, uint8_t index) {
if (offset >= 12 && offset <= 16) { // 工段运行时间(秒)
if (currentState == RUNNING && currentStage == (offset - 12)) {
return (uint16_t)(millis() - stageStartTime) / 1000;
}
return 0;
}
return holdingRegs[offset];
}
void handleWrite(uint16_t offset, uint8_t index, uint16_t value) {
switch(offset) {
case 0: // 系统启停
if (value == 1) {
currentState = RUNNING;
currentStage = 0;
stageElapsedMs = 0;
stageStartTime = millis();
switchStage(0);
holdingRegs[12] = 1;
} else {
currentState = STOPPED;
allLightsOff();
holdingRegs[2] = 0;
holdingRegs[12] = 0;
}
break;
case 1: // 速度档位
if (value >= 1 && value <= 5) {
for (int i = 0; i < STAGE_NUM; i++) {
stageDurations[i] = baseDurations[i] / value;
}
}
break;
case 5: // 工况切换
switch(value) {
case 1: // 冷态开车
// 按开车规程顺序启动各工段
break;
case 2: // 正常生产
// 所有工段正常运行
break;
case 3: // 紧急停车
emergencyStop();
break;
}
break;
case 7: case 8: case 9: case 10: case 11:
// 各工段独立启停控制
toggleStage(offset - 7, value);
break;
case 17: case 18: case 19: case 20: case 21:
// 各工段颜色配置(占3个寄存器,仅处理低16位)
// 实际颜色值由上位机组合后写入
break;
}
holdingRegs[offset] = value;
}
void emergencyStop() {
currentState = STOPPED;
allLightsOff();
// 红色呼吸灯效果提示故障状态
for (int i = 0; i < 10; i++) {
for (int s = 0; s < STAGE_NUM; s++) {
strips[s].clear();
for (int p = 0; p < strips[s].numPixels(); p++) {
strips[s].setPixelColor(p, (i % 2 == 0) ? 0xFF0000 : 0x000000);
}
strips[s].show();
}
delay(200);
}
holdingRegs[6] = 3; // 系统状态=故障
}
五、关键技术决策记录
5.1 为什么用RS485而非WiFi
沙盘模型内部金属结构多,WiFi信号衰减严重,存在连接不稳定的风险。RS485是工业现场总线标准,抗干扰能力强,通讯稳定可靠,适合展厅环境使用。
5.2 为什么用WS2812B而非传统单色灯带
传统单色灯带每个灯珠需要独立GPIO控制,多路控制时GPIO数量不足。WS2812B单总线方案,每个灯带只需1个GPIO,支持256级亮度控制和任意颜色组合,便于实现流水、渐变等动态效果。
5.3 性能数据
指标 数值 说明
系统响应延迟 <50ms 从中控屏下发指令到灯光响应
Modbus通讯延迟 <10ms 单次寄存器读写
定时器中断精度 1ms 状态机调度精度
灯带刷新率 30fps 全亮状态
连续运行时间 1000小时+ 72°C环境老化测试通过
六、现场调试要点
6.1 通讯异常排查
现象 排查方向 解决方案
超时无响应 RS485 A/B线接反或终端电阻未接 接入120Ω终端电阻,检查A/B线序
部分指令无响应 从站地址不匹配 确认SLAVE_ID与中控屏设置一致
数据错乱 通讯参数不匹配 确认波特率/停止位/校验位设置
通讯不稳定 信号干扰 使用屏蔽双绞线,与动力线分离布线
6.2 灯带异常排查
现象 排查方向 解决方案
灯带全亮白光 数据线虚接 重新焊接或压接数据线
颜色偏色 电源电压不足 测量末端电压,≤4.8V需增加供电点
首灯正常后续不亮 MCU主频配置不当 确认系统时钟72MHz,WS2812B时序正确
灯带闪烁 电源纹波或信号干扰 增加100μF电容滤波,数据线串100Ω电阻
6.3 调试工具
调试阶段推荐使用Modbus Poll(Windows)作为主站调试工具,配合USB转RS485转换器即可连接目标板。通讯正常后,依次测试各寄存器的读写功能,确认单路控制正常后再进行联调。
七、总结与经验
石油化工沙盘控制系统的设计,核心在于三点:
-
寄存器映射设计先于代码编写。 寄存器地址定义直接影响中控屏开发团队的理解成本,需在项目启动时与上位机团队确认寄存器表,避免后期反复修改。
-
状态机调度预留时间参数调节接口。 将各工段延时参数暴露为寄存器,允许现场通过中控屏微调时序,适应不同工况的演示需求。
-
Modbus RTU为扩展预留空间。 即使当前只对接中控屏,也为后续与DCS仿真平台、数字孪生系统的对接预留了通道。
筑城世纪模型的L1-L3三级动态联动方案已在国内多个工业沙盘项目中完成部署验证,控制系统基于STM32平台自主研发,支持Modbus RTU协议,后期可根据工艺调整需求随时修改时序逻辑。
项目咨询或技术交流可搜索“筑城世纪模型”。

47

被折叠的 条评论
为什么被折叠?



