最近在工业自动化项目中经常遇到需要低成本PLC解决方案的场景。传统PLC虽然稳定可靠但价格较高且扩展性有限。基于STM32F103自主设计的PLC开发板正好填补了这一空白既能满足基本的工业控制需求又具备极高的性价比和灵活性。本文将完整分享基于STM32F103的PLC开发板设计全过程包含开关量输入输出、模拟量输入采集、两路步进电机驱动等核心功能。无论你是嵌入式初学者还是工业自动化工程师都能通过本文掌握从硬件设计到软件编程的完整开发流程。1. PLC开发板整体设计框架1.1 什么是基于STM32的PLC开发板传统PLC可编程逻辑控制器是工业控制领域的核心设备负责执行逻辑运算、顺序控制、定时计数等操作。基于STM32F103设计的PLC开发板实际上是用通用微控制器实现PLC的核心功能既保留了PLC的编程习惯和控制逻辑又具备了MCU的灵活性和低成本优势。这种设计方案特别适合中小型自动化项目、教学实验设备以及定制化控制需求。STM32F103作为一款经典的ARM Cortex-M3内核微控制器具有丰富的外设资源和强大的处理能力完全能够胜任大多数工业控制场景。1.2 系统架构设计整个PLC开发板的系统架构分为以下几个核心模块主控模块STM32F103C8T6最小系统包含晶振电路、复位电路、调试接口开关量输入模块光耦隔离的数字量输入通道支持24V工业标准电平开关量输出模块继电器或MOSFET输出带隔离保护模拟量输入模块ADC采集电路支持0-10V或4-20mA标准信号步进电机驱动模块两路步进电机驱动采用A4988或DRV8825驱动芯片通信模块RS485、CAN或以太网通信接口电源模块多路电源转换为不同模块提供稳定供电这种模块化设计使得每个功能单元相对独立便于调试和维护也方便根据实际需求进行功能裁剪。1.3 硬件选型考量在选择具体元器件时需要综合考虑成本、性能、可靠性等因素主控芯片选择STM32F103C8T6的原因72MHz主频性能足够处理多路IO和电机控制64KB Flash、20KB RAM资源充足丰富的定时器资源特别适合PWM生成和电机控制2个ADC12位精度支持多通道模拟量采集成本低廉开发资料丰富隔离器件选择工业环境存在各种干扰光耦隔离是保证系统稳定性的关键。开关量输入采用PC817等常见光耦输出根据负载功率选择继电器或MOSFET光耦方案。ADC基准源选择模拟量采集的精度很大程度上取决于基准电压源的稳定性。建议使用TL431或专用基准芯片而不是直接使用LDO输出的3.3V作为基准。2. 硬件电路设计详解2.1 STM32F103最小系统设计STM32F103最小系统是整个开发板的核心设计时需要注意以下几个关键点电源电路设计// 电源拓扑结构 220V AC → 变压器 → 整流桥 → 滤波电容 → LM2596(24V转5V) → AMS1117(5V转3.3V)3.3V电源需要为STM32核心、外围芯片供电要保证足够的电流余量。建议使用500mA以上的LDO或DC-DC器件并在电源入口处增加TVS管防止浪涌冲击。时钟电路设计主晶振8MHz为PLL提供基准频率RTC晶振32.768kHz可选配在晶振引脚附近放置负载电容容值根据晶振要求选择通常20-22pF调试接口设计 标准的SWD调试接口VCC、GND、SWDIO、SWCLK占用IO少且调试效率高。建议增加复位引脚连接便于远程调试。2.2 开关量输入电路设计工业现场的开关量信号通常是24V电平而STM32的GPIO只能耐受3.3V电压因此需要电平转换和隔离保护。光耦隔离输入电路24V现场信号 → 限流电阻(2.2kΩ) → PC817光耦 → 上拉电阻(10kΩ) → STM32 GPIO每个输入通道都需要独立的隔离设计防止现场信号之间的串扰。输入状态指示灯可以方便调试通常在每个输入通道增加LED指示。防抖处理 机械开关会产生抖动需要在硬件或软件层面进行处理。硬件防抖可以使用RC滤波电路软件防抖可以通过延时采样实现。2.3 模拟量输入电路设计模拟量输入模块用于采集传感器信号如温度、压力、流量等模拟量信号。信号调理电路 工业标准模拟信号通常是0-10V电压或4-20mA电流需要转换为STM32 ADC可接受的0-3.3V范围。对于电压信号0-10V输入 → 电阻分压(10V→3.3V) → 电压跟随器 → RC滤波 → STM32 ADC对于电流信号4-20mA输入 → 精密采样电阻(250Ω) → 运放调理 → 电压跟随器 → STM32 ADCADC配置要点 STM32F103的ADC为12位分辨率最大采样率1MHz。在多通道采集时需要合理配置采样顺序和采样时间。2.4 步进电机驱动电路设计两路步进电机驱动采用A4988或DRV8825驱动芯片这两种芯片都内置了细分驱动逻辑简化了控制难度。A4988驱动电路STM32 GPIO → A4988(STEP、DIR、ENABLE) → 步进电机每个A4988需要以下控制信号STEP脉冲信号每个脉冲电机走一步DIR方向控制信号ENABLE使能信号MS1、MS2、MS3细分设置引脚电源设计 电机驱动需要独立的电源供电与数字电路电源隔离。大容量电解电容1000uF以上靠近驱动芯片放置提供瞬时大电流。3. 软件开发环境搭建3.1 开发工具链配置STM32开发可以使用多种工具链推荐使用以下组合IDE选择STM32CubeIDE官方免费IDE集成度高适合初学者Keil MDK功能强大调试方便商业软件IAR Embedded Workbench编译效率高商业软件编译器配置 无论使用哪种IDE都需要正确配置芯片型号、时钟频率、调试接口等参数。建议在项目初期就建立完整的工程模板。3.2 STM32CubeMX配置STM32CubeMX是ST官方提供的图形化配置工具可以快速生成初始化代码时钟树配置HSE8MHz外部晶振PLL倍频8MHz * 9 72MHz系统时钟72MHzAPB1总线时钟36MHzAPB2总线时钟72MHzGPIO配置 根据硬件设计配置每个引脚的功能开关量输入GPIO输入模式上拉或下拉根据电路设计选择开关量输出GPIO推挽输出高速模式ADC通道模拟输入模式步进电机控制GPIO输出模式外设配置ADC12位分辨率连续扫描模式使能DMA传输定时器用于生成步进电机脉冲PWM模式USART用于调试信息输出和通信3.3 工程目录结构建立清晰的工程目录结构有利于代码维护Project/ ├── Core/ │ ├── Inc/ // 头文件 │ ├── Src/ // 源文件 │ └── Startup.s // 启动文件 ├── Drivers/ │ ├── CMSIS/ // Cortex-M内核支持 │ └── STM32F1xx_HAL_Driver/ // HAL库 ├── Middlewares/ // 中间件 ├── PLC/ │ ├── plc_io.c/h // IO处理模块 │ ├── plc_adc.c/h // ADC处理模块 │ ├── plc_motor.c/h // 电机控制模块 │ └── plc_logic.c/h // 逻辑处理模块 └── README.md4. 核心功能模块实现4.1 开关量输入处理开关量输入处理需要考虑防抖、滤波、状态检测等功能。输入扫描函数// plc_io.h #define DI_CHANNELS 8 // 8路开关量输入 typedef struct { uint8_t current_state; // 当前状态 uint8_t last_state; // 上次状态 uint8_t filter_count; // 滤波计数器 uint32_t press_duration; // 按下持续时间 } DI_Channel_t; extern DI_Channel_t di_channels[DI_CHANNELS]; void PLC_DI_Init(void); void PLC_DI_Scan(void); uint8_t PLC_DI_GetState(uint8_t channel); uint8_t PLC_DI_GetRisingEdge(uint8_t channel); uint8_t PLC_DI_GetFallingEdge(uint8_t channel);输入扫描实现// plc_io.c void PLC_DI_Scan(void) { for (int i 0; i DI_CHANNELS; i) { // 读取GPIO状态 uint8_t raw_state HAL_GPIO_ReadPin(DI_PORT[i], DI_PIN[i]); // 软件防抖滤波 if (raw_state di_channels[i].current_state) { if (di_channels[i].filter_count DEBOUNCE_COUNT) { di_channels[i].filter_count; } } else { di_channels[i].filter_count 0; di_channels[i].current_state raw_state; } // 状态稳定后更新 if (di_channels[i].filter_count DEBOUNCE_COUNT) { di_channels[i].last_state di_channels[i].current_state; } // 记录按下时间 if (di_channels[i].current_state PRESSED_STATE) { di_channels[i].press_duration; } else { di_channels[i].press_duration 0; } } }4.2 模拟量采集处理模拟量采集需要配置ADC和DMA实现多通道连续采集。ADC配置代码// plc_adc.c ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_adc1; uint16_t adc_buffer[AI_CHANNELS]; // ADC采样缓冲区 float ai_values[AI_CHANNELS]; // 转换后的工程值 void PLC_AI_Init(void) { // ADC配置 hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_ENABLE; hadc1.Init.ContinuousConvMode ENABLE; hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion AI_CHANNELS; HAL_ADC_Init(hadc1); // ADC通道配置 ADC_ChannelConfTypeDef sConfig {0}; for (int i 0; i AI_CHANNELS; i) { sConfig.Channel ai_channels[i]; // 通道号 sConfig.Rank i 1; sConfig.SamplingTime ADC_SAMPLETIME_55CYCLES5; HAL_ADC_ConfigChannel(hadc1, sConfig); } // DMA配置 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, AI_CHANNELS); } // ADC转换完成回调函数 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { for (int i 0; i AI_CHANNELS; i) { // 将ADC原始值转换为工程值 ai_values[i] (adc_buffer[i] * 3.3f / 4095.0f) * scale_factor[i] offset[i]; } }4.3 步进电机控制步进电机控制需要精确的脉冲时序控制使用定时器产生PWM脉冲。步进电机控制结构// plc_motor.h typedef struct { uint32_t target_position; // 目标位置脉冲数 uint32_t current_position; // 当前位置 uint32_t pulse_delay; // 脉冲间隔控制速度 uint8_t direction; // 方向 uint8_t enabled; // 使能状态 uint8_t moving; // 运动状态 } StepperMotor_t; extern StepperMotor_t motor[2]; // 两路步进电机 void PLC_Motor_Init(void); void PLC_Motor_Move(uint8_t motor_id, int32_t steps, uint32_t speed); void PLC_Motor_Stop(uint8_t motor_id); void PLC_Motor_Update(void);电机控制实现// plc_motor.c void PLC_Motor_Update(void) { for (int i 0; i 2; i) { if (!motor[i].enabled || !motor[i].moving) { continue; } static uint32_t last_pulse_time[2] {0}; uint32_t current_time HAL_GetTick(); // 检查是否到达脉冲时间 if (current_time - last_pulse_time[i] motor[i].pulse_delay) { // 生成一个脉冲 HAL_GPIO_WritePin(STEP_PORT[i], STEP_PIN[i], GPIO_PIN_SET); delay_us(5); // 脉冲宽度 HAL_GPIO_WritePin(STEP_PORT[i], STEP_PIN[i], GPIO_PIN_RESET); // 更新位置 if (motor[i].direction) { motor[i].current_position; } else { motor[i].current_position--; } last_pulse_time[i] current_time; // 检查是否到达目标位置 if (motor[i].current_position motor[i].target_position) { motor[i].moving 0; } } } } void PLC_Motor_Move(uint8_t motor_id, int32_t steps, uint32_t speed) { if (motor_id 2) return; motor[motor_id].target_position motor[motor_id].current_position steps; motor[motor_id].direction (steps 0) ? 1 : 0; motor[motor_id].pulse_delay 1000 / speed; // 速度转换为脉冲间隔 motor[motor_id].moving 1; }4.4 PLC逻辑处理框架仿照传统PLC的扫描周期机制实现循环扫描执行逻辑。主循环框架// main.c int main(void) { // 硬件初始化 HAL_Init(); SystemClock_Config(); PLC_IO_Init(); PLC_AI_Init(); PLC_Motor_Init(); // PLC逻辑初始化 PLC_Logic_Init(); while (1) { // 输入采样阶段 PLC_DI_Scan(); PLC_AI_Update(); // 逻辑运算阶段 PLC_Logic_Run(); // 输出刷新阶段 PLC_DO_Update(); PLC_Motor_Update(); // 通信处理 PLC_Comm_Process(); // 看门狗喂狗 HAL_IWDG_Refresh(); // 延时控制扫描周期 HAL_Delay(10); // 100Hz扫描频率 } }5. 高级功能实现5.1 通信功能扩展工业设备通常需要通信功能实现远程监控和控制。RS485通信实现// plc_comm.c UART_HandleTypeDef huart2; void PLC_Comm_Init(void) { huart2.Instance USART2; huart2.Init.BaudRate 9600; huart2.Init.WordLength UART_WORDLENGTH_8B; huart2.Init.StopBits UART_STOPBITS_1; huart2.Init.Parity UART_PARITY_NONE; huart2.Init.Mode UART_MODE_TX_RX; huart2.Init.HwFlowCtl UART_HWCONTROL_NONE; HAL_UART_Init(huart2); } // Modbus RTU从站协议处理 void PLC_Modbus_Process(void) { uint8_t rx_buffer[256]; uint8_t tx_buffer[256]; // 接收Modbus帧 if (HAL_UART_Receive(huart2, rx_buffer, 8, 100) HAL_OK) { // 解析Modbus指令 uint8_t slave_addr rx_buffer[0]; uint8_t function_code rx_buffer[1]; uint16_t start_addr (rx_buffer[2] 8) | rx_buffer[3]; uint16_t quantity (rx_buffer[4] 8) | rx_buffer[5]; // 处理不同功能码 switch (function_code) { case 0x01: // 读线圈 PLC_Modbus_ReadCoils(tx_buffer, start_addr, quantity); break; case 0x03: // 读保持寄存器 PLC_Modbus_ReadRegisters(tx_buffer, start_addr, quantity); break; case 0x05: // 写单个线圈 PLC_Modbus_WriteCoil(start_addr, rx_buffer[4] 0xFF); break; case 0x06: // 写单个寄存器 PLC_Modbus_WriteRegister(start_addr, (rx_buffer[4] 8) | rx_buffer[5]); break; } // 发送响应帧 HAL_UART_Transmit(huart2, tx_buffer, tx_buffer[2] 3, 1000); } }5.2 故障诊断与保护工业设备需要完善的故障诊断和保护机制。系统监控功能// plc_monitor.c typedef struct { uint32_t run_time; // 运行时间 uint16_t scan_count; // 扫描次数 uint8_t diag_code; // 诊断代码 float cpu_usage; // CPU使用率 float temperature; // 芯片温度 } SystemMonitor_t; void PLC_Monitor_Init(void) { // 启用看门狗 IWDG_HandleTypeDef hiwdg; hiwdg.Instance IWDG; hiwdg.Init.Prescaler IWDG_PRESCALER_32; hiwdg.Init.Reload 0x0FFF; HAL_IWDG_Init(hiwdg); } void PLC_Monitor_Update(void) { static uint32_t last_tick 0; uint32_t current_tick HAL_GetTick(); uint32_t cycle_time current_tick - last_tick; // 计算CPU使用率 system_monitor.cpu_usage (cycle_time * 100.0) / TARGET_SCAN_CYCLE; // 检查系统状态 if (system_monitor.cpu_usage 90.0) { system_monitor.diag_code DIAG_CPU_OVERLOAD; } // 更新运行时间 system_monitor.run_time current_tick / 1000; system_monitor.scan_count; last_tick current_tick; }5.3 数据存储与掉电保护重要参数需要掉电保护使用STM32的Flash模拟EEPROM。参数存储实现// plc_storage.c #define PARAM_FLASH_ADDR 0x0800F000 // Flash最后一页 typedef struct { uint16_t magic; // 魔数用于验证数据有效性 uint32_t system_config; // 系统配置 float ai_calibration[8]; // AI校准参数 uint32_t motor_position[2]; // 电机位置 uint16_t crc16; // 校验和 } SystemParams_t; void PLC_Param_Save(SystemParams_t* params) { // 计算CRC params-crc16 PLC_CRC16((uint8_t*)params, sizeof(SystemParams_t) - 2); // 解锁Flash HAL_FLASH_Unlock(); // 擦除Flash页 FLASH_EraseInitTypeDef erase; erase.TypeErase FLASH_TYPEERASE_PAGES; erase.PageAddress PARAM_FLASH_ADDR; erase.NbPages 1; uint32_t error 0; HAL_FLASHEx_Erase(erase, error); // 写入数据 uint64_t* data_ptr (uint64_t*)params; for (int i 0; i sizeof(SystemParams_t) / 8; i) { HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, PARAM_FLASH_ADDR i * 8, data_ptr[i]); } HAL_FLASH_Lock(); } SystemParams_t* PLC_Param_Load(void) { SystemParams_t* params (SystemParams_t*)PARAM_FLASH_ADDR; // 验证魔数和CRC if (params-magic 0x55AA params-crc16 PLC_CRC16((uint8_t*)params, sizeof(SystemParams_t) - 2)) { return params; } return NULL; // 数据无效 }6. 调试与测试方法6.1 硬件调试技巧硬件调试是项目成功的关键需要系统性的方法。电源调试先不插主芯片测量各电源点电压是否正常检查3.3V、5V、24V电源的纹波和稳定性使用示波器观察电源上电时序和掉电保持时间信号调试使用逻辑分析仪抓取GPIO信号时序检查光耦输入输出的响应时间验证ADC采集的精度和线性度电机驱动调试先不接电机用示波器检查脉冲波形逐步增加电机负载观察电流变化测试不同细分设置下的运行平滑度6.2 软件调试工具充分利用STM32的调试功能提高开发效率。SWD调试配置// 在IDE中配置调试参数 // - 调试器ST-LINK // - 接口SWD // - 时钟速度4MHz // - 复位模式硬件复位printf重定向// 重定向printf到串口 int _write(int file, char *ptr, int len) { HAL_UART_Transmit(huart2, (uint8_t*)ptr, len, 1000); return len; } // 使用示例 printf(系统启动完成版本%s\r\n, FW_VERSION); printf(ADC采样值%d电压%.2fV\r\n, adc_value, voltage);调试宏定义// debug.h #ifdef DEBUG #define DBG_PRINT(fmt, ...) printf([DEBUG] fmt, ##__VA_ARGS__) #define DBG_HEX(data, len) PLC_Debug_HexDump(data, len) #else #define DBG_PRINT(fmt, ...) #define DBG_HEX(data, len) #endif void PLC_Debug_HexDump(uint8_t* data, uint16_t len) { for (int i 0; i len; i) { printf(%02X , data[i]); if ((i 1) % 16 0) printf(\r\n); } printf(\r\n); }6.3 功能测试用例建立完整的测试用例确保每个功能正常。IO测试用例void PLC_Test_IO(void) { printf(开始IO测试...\r\n); // 测试开关量输入 for (int i 0; i DI_CHANNELS; i) { printf(DI%d状态%d\r\n, i, PLC_DI_GetState(i)); } // 测试开关量输出 for (int i 0; i DO_CHANNELS; i) { PLC_DO_SetState(i, 1); HAL_Delay(100); printf(DO%d设置成功\r\n, i); PLC_DO_SetState(i, 0); } }ADC测试用例void PLC_Test_ADC(void) { printf(开始ADC测试...\r\n); // 测试各通道ADC值 for (int i 0; i AI_CHANNELS; i) { float voltage ai_values[i]; printf(AI%d原始值%d电压%.3fV\r\n, i, adc_buffer[i], voltage); } // 线性度测试 printf(ADC线性度测试\r\n); for (int volt 0; volt 10; volt) { // 施加标准电压读取ADC值 printf(标准电压%dV测量电压%.3fV\r\n, volt, measured_voltage); } }7. 常见问题与解决方案7.1 硬件常见问题电源问题现象系统不稳定随机复位原因电源纹波过大或功率不足解决增加滤波电容检查电源芯片散热确保功率余量信号干扰现象开关量误动作ADC采样跳动原因工业环境电磁干扰解决加强屏蔽增加RC滤波优化布线电机驱动问题现象电机抖动、失步、驱动芯片发热原因电流设置不当、细分配置错误、散热不良解决调整驱动电流合理设置细分加强散热7.2 软件常见问题ADC采样不准// 常见原因和解决方案 // 1. 参考电压不稳定 // 解决使用外部基准电压源 // 2. 采样时间不足 // 解决增加ADC采样时间 // 3. 电源噪声 // 解决优化PCB布局增加滤波 hadc1.Init.SamplingTime ADC_SAMPLETIME_239CYCLES5; // 增加采样时间电机控制失步// 失步原因分析 // 1. 脉冲频率过高 // 解决降低脉冲频率特别是启动时 // 2. 加速度设置不合理 // 解决实现加减速控制算法 // 3. 负载过大 // 解决检查电机选型是否合适 // 加减速控制实现 void PLC_Motor_SmoothMove(uint8_t motor_id, int32_t steps, uint32_t max_speed) { // 梯形加减速算法 uint32_t acceleration_steps steps / 4; for (uint32_t speed 100; speed max_speed; speed 10) { PLC_Motor_Move(motor_id, acceleration_steps, speed); } // ... 匀速段和减速段 }通信异常现象Modbus通信超时、数据错误原因波特率不匹配、线路干扰、协议处理错误解决检查波特率设置添加通信超时处理完善协议解析7.3 系统稳定性优化看门狗应用// 独立看门狗配置 void IWDG_Config(void) { hiwdg.Instance IWDG; hiwdg.Init.Prescaler IWDG_PRESCALER_32; // 32分频 hiwdg.Init.Reload 0x0FFF; // 约1.6s超时 hiwdg.Init.Window 0x0FFF; HAL_IWDG_Init(hiwdg); } // 在合适的位置喂狗 void Task_Scheduler(void) { while (1) { PLC_DI_Scan(); HAL_IWDG_Refresh(); // 喂狗 PLC_Logic_Run(); HAL_IWDG_Refresh(); // ... 其他任务 } }错误处理机制// 全局错误代码定义 typedef enum { ERROR_NONE 0, ERROR_ADC_OVERRUN, ERROR_MOTOR_OVERLOAD, ERROR_COMM_TIMEOUT, ERROR_PARAM_INVALID } ErrorCode_t; // 错误处理函数 void PLC_Error_Handle(ErrorCode_t error) { static ErrorCode_t last_error ERROR_NONE; if (error ! last_error) { printf(错误代码%d时间%lu\r\n, error, HAL_GetTick()); // 根据错误等级采取不同措施 switch (error) { case ERROR_MOTOR_OVERLOAD: PLC_Motor_Stop(ALL_MOTORS); break; case ERROR_ADC_OVERRUN: // 重新初始化ADC HAL_ADC_Stop_DMA(hadc1); HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, AI_CHANNELS); break; } last_error error; } }8. 项目应用与扩展8.1 典型应用场景基于STM32F103的PLC开发板可以应用于多种工业场景小型自动化设备包装机械、灌装设备、输送线控制替代传统继电器控制系统实现简单的顺序控制和逻辑运算环境监控系统温湿度、压力、流量监测数据采集和报警输出通过通信接口上传数据教学实验平台PLC原理教学工业通信协议学习电机控制技术实践8.2 功能扩展方向通信扩展增加Ethernet接口支持Modbus TCP添加CAN总线接口用于汽车电子集成WiFi或4G模块实现远程监控功能增强增加更多模拟量输出通道支持PID控制算法添加触摸屏人机界面可靠性提升双机热备设计安全继电器电路EMC等级提升8.3 产业化考虑成本优化元器件选型国产化PCB布局优化减少面积批量化生产降成本标准化设计遵循IEC 61131-3编程标准模块化硬件设计统一的通信协议本文详细介绍了基于STM32F103的PLC开发板从硬件设计到软件实现的完整流程。通过实际项目验证这种设计方案在成本、性能和灵活性方面都具有明显优势特别适合中小型自动化项目和教学应用。