You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;

问题根源

  1. 手动调用Execute方法
    TThread的Execute是线程内部执行入口,由系统自动调度。手动调用时线程上下文未正确初始化,且FSynchronizer可能尚未创建,导致访问空指针触发异常。

  2. 临界区未初始化
    FSynchronizer作为全局临界区对象,未在程序启动时创建,线程执行时访问未初始化的对象,引发访问违例。

  3. 线程启动顺序错误
    创建线程时传入False会立即启动线程,但此时LPTEngineNum尚未赋值,线程执行Time_out[LPTEngineNum]时会访问非法内存或错误值。

  4. 共享数据未同步
    RotDir、RotateStep、Time_out等全局数组被主线程和多个工作线程同时读写,无同步保护,导致数据竞争,电机输出信号异常。

  5. 全局OutToLPTData被多线程覆盖
    所有线程直接修改全局的OutToLPTData,即使Out32调用在临界区内,但修改过程未同步,会导致多个线程互相覆盖数据,输出错误的LPT信号。

  6. 速度变化时未更新延迟
    ApplyHardware中仅设置方向和脉冲值,未根据速度更新Time_out[i],电机转速无法随速度调整,甚至可能因延迟错误导致不运转。

修复方案

  1. 初始化临界区
    在单元初始化部分创建临界区,退出时销毁:
initialization
  FSynchronizer := TCriticalSection.Create;
finalization
  FSynchronizer.Free;
  1. 删除手动调用Execute的代码
    移除OutToLptThread[i].Execute,线程创建时传入False会自动启动,无需手动调用入口方法。

  2. 调整线程创建流程
    先创建挂起的线程,赋值LPTEngineNum后再启动,避免线程执行时访问未初始化的属性:

OutToLptThread[i] := TOutToLptThread.Create(True); // 挂起创建线程
OutToLptThread[i].LPTEngineNum := i;
OutToLptThread[i].Priority := tpNormal;
OutToLptThread[i].Resume; // 启动线程
  1. 同步共享数据的所有访问
    主线程修改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;
  1. 修改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,提问作者Расул Барлыбаев

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 14:47:01