Delphi下并行端口控制步进电机的线程访问违例问题排查
问题现象
- 程序启动后4台步进电机未按预期运转
- 在
TLPTEngine.InitializeHardware过程中直接调用OutToLptThread[i].Execute时,触发访问违例异常:
Exception EAccessViolation in module MyProgram.exe at 001CE279. Access violation at address 005CE279 in module MyProgram.exe'. Read of address 00000000.
相关代码
uses SysUtils, Math, TraceLog, MDriveEngines, SyncObjs; const numOfEngines = 4; type // 控制电机的线程类 TOutToLptThread = class(TThread) private { Private declarations } protected procedure Execute; override; public LPTEngineNum: integer; // 引擎与线程编号 end; // 步进电机类 TLPTEngine = class(TEngine) public procedure InitializeHardware; override; procedure FinalizeHardware; override; procedure ApplyHardware; override; end; var RotateStep: array [1 .. numOfEngines] of Integer; // 步进电机旋转脉冲(LPT端口输出) RotDir: array [1 .. numOfEngines] of Integer; // 步进电机旋转方向(LPT端口输出) Time_out: array [1 .. numOfEngines] of Integer; // 步进电机脉冲延迟(控制转速) FSynchronizer: TCriticalSection; OutToLptThread: array[1..numOfEngines] of TOutToLptThread; // 控制4台引擎的线程数组 OutToLPTData: Byte; // 整合所有Dir和Step的字节数据,输出到LPT端口 procedure TOutToLptThread.Execute; procedure Delay_mcs(time_out: double); // 微秒级延迟 var iCounterPerSec: TLargeInteger; T1, T2: TLargeInteger; // 操作前后的计数器值 begin QueryPerformanceFrequency(iCounterPerSec); // 获取计数器频率 QueryPerformanceCounter(T1); // 记录操作开始时间 T2 := T1; while (T2 - T1) * 1000000 / iCounterPerSec < time_out do QueryPerformanceCounter(T2); // 记录操作结束时间 end; var PortAdr: word; begin PortAdr := $EEFC; // LPT端口地址 while true do begin OutToLPTData := RotDir[1] * 2 + RotDir[2] * 8 + RotDir[3] * 32 + RotDir[4] * 128; // 输出到LPT的数据 FSynchronizer.Enter; // 获取临界区 try Out32(PortAdr, OutToLPTData); finally FSynchronizer.Leave; // 释放临界区 end; Delay_mcs(Time_out[LPTEngineNum]); OutToLPTData := RotateStep[1] + RotateStep[2] * 4 + RotateStep[3] * 16 + RotateStep[4] * 64 + RotDir[1] * 2 + RotDir[2] * 8 + RotDir[3] * 32 + RotDir[4] * 128; // 输出到LPT的数据 FSynchronizer.Enter; // 获取临界区 try Out32(PortAdr, OutToLPTData); finally FSynchronizer.Leave; // 释放临界区 end; Delay_mcs(Time_out[LPTEngineNum]); end; end; procedure TLPTEngine.InitializeHardware; // 程序启动时调用 var i: integer; begin IsSoftwareControlEngine := true; for i := 1 to numOfEngines do begin RotateStep[i] := 1; // 步进电机初始旋转脉冲 RotDir[i] := 0; // 电机初始旋转方向 Time_out[i] := 500; // 步进电机脉冲延迟(微秒) OutToLptThread[i] := TOutToLptThread.Create(False); OutToLptThread[i].LPTEngineNum := i; // 线程编号对应引擎编号,用于Time_out[LPTEngineNum] OutToLptThread[i].Priority := tpNormal; // OutToLptThread[i].Execute; // 此行代码触发异常:"Exception EAccessViolation in module MyProgram.exe at 001CE279. Access violation at address 005CE279 in module MyProgram.exe'. Read of address 00000000." end; inherited; end; procedure TLPTEngine.ApplyHardware; // 速度值变化时调用 var i: Integer; begin i := AxisNum; // 引擎编号 if Velocity > 0 then // 旋转方向 begin RotDir[i] := 0; RotateStep[i] := 1 end else if Velocity < 0 then // 反向旋转 begin RotDir[i] := 1; RotateStep[i] := 1 end else begin RotDir[i] := 0; RotateStep[i] := 0 end; end;
问题根源
手动调用Execute方法
TThread的Execute是线程内部执行入口,由系统自动调度。手动调用时线程上下文未正确初始化,且FSynchronizer可能尚未创建,导致访问空指针触发异常。临界区未初始化
FSynchronizer作为全局临界区对象,未在程序启动时创建,线程执行时访问未初始化的对象,引发访问违例。线程启动顺序错误
创建线程时传入False会立即启动线程,但此时LPTEngineNum尚未赋值,线程执行Time_out[LPTEngineNum]时会访问非法内存或错误值。共享数据未同步
RotDir、RotateStep、Time_out等全局数组被主线程和多个工作线程同时读写,无同步保护,导致数据竞争,电机输出信号异常。全局OutToLPTData被多线程覆盖
所有线程直接修改全局的OutToLPTData,即使Out32调用在临界区内,但修改过程未同步,会导致多个线程互相覆盖数据,输出错误的LPT信号。速度变化时未更新延迟
ApplyHardware中仅设置方向和脉冲值,未根据速度更新Time_out[i],电机转速无法随速度调整,甚至可能因延迟错误导致不运转。
修复方案
- 初始化临界区
在单元初始化部分创建临界区,退出时销毁:
initialization FSynchronizer := TCriticalSection.Create; finalization FSynchronizer.Free;
删除手动调用Execute的代码
移除OutToLptThread[i].Execute,线程创建时传入False会自动启动,无需手动调用入口方法。调整线程创建流程
先创建挂起的线程,赋值LPTEngineNum后再启动,避免线程执行时访问未初始化的属性:
OutToLptThread[i] := TOutToLptThread.Create(True); // 挂起创建线程 OutToLptThread[i].LPTEngineNum := i; OutToLptThread[i].Priority := tpNormal; OutToLptThread[i].Resume; // 启动线程
- 同步共享数据的所有访问
主线程修改RotDir、RotateStep、Time_out时,同样进入临界区,避免数据竞争:
procedure TLPTEngine.ApplyHardware; var i: Integer; begin i := AxisNum; FSynchronizer.Enter; try if Velocity <> 0 then begin RotDir[i] := Ord(Velocity < 0); RotateStep[i] := 1; // 根据速度计算延迟,示例:速度越快延迟越小,需根据电机参数调整 Time_out[i] := Round(1000 / Abs(Velocity)); end else begin RotDir[i] := 0; RotateStep[i] := 0; Time_out[i] := 0; end; finally FSynchronizer.Leave; end; end;
- 修改OutToLPTData的使用方式
在临界区内完成共享数据读取和输出值计算,避免多线程覆盖:
procedure TOutToLptThread.Execute; // ... 保留Delay_mcs过程 ... var PortAdr: word; tempData: Byte; // 用局部变量存储计算结果,避免全局变量竞争 begin PortAdr := $EEFC; while true do begin FSynchronizer.Enter; try // 临界区内读取所有共享数据并计算输出值 tempData := RotDir[1] * 2 + RotDir[2] * 8 + RotDir[3] * 32 + RotDir[4] * 128; Out32(PortAdr, tempData); finally FSynchronizer.Leave; end; Delay_mcs(Time_out[LPTEngineNum]); FSynchronizer.Enter; try tempData := RotateStep[1] + RotateStep[2] * 4 + RotateStep[3] * 16 + RotateStep[4] * 64 + RotDir[1] * 2 + RotDir[2] * 8 + RotDir[3] * 32 + RotDir[4] * 128; Out32(PortAdr, tempData); finally FSynchronizer.Leave; end; Delay_mcs(Time_out[LPTEngineNum]); end; end;
内容的提问来源于stack exchange,提问作者Расул Барлыбаев

