Golang中遇到一个有趣的选择问题,求帮助决策

Golang中遇到一个有趣的选择问题,求帮助决策 我正在构建一个PLC集成开发环境。传统上,PLC使用五种IEC 61131-3标准语言之一进行编程。我的IDE将只支持其中一种,即ST(结构化文本),它看起来像Pascal并且实际上基于Pascal。

目前市面上没有现成的ST编译器或解释器。只有像3S(Codesys)、西门子等公司拥有私有的解决方案。我最初的想法是将ST程序转译为C/C++,然后考虑将其转译为Node.js。但最近我研究了Go语言,它看起来是一个完美的候选方案。

我知道对于你们大多数人来说,一个例子可能比一篇长文更能说明一种语言,所以这里有一个ST程序可能是什么样子的示例:

TYPE Shelf :
STRUCT
	TemperatureIN:     POINTER TO WORD;
	Temperature:       INT;
	TemperatureHMI:    POINTER TO WORD;
	TemperatureStatus: POINTER TO BYTE;
	OutByte:           POINTER TO WORD;
	ByteNum:           USINT;
	Correction:        POINTER TO WORD;
END_STRUCT
END_TYPE

FUNCTION_BLOCK TON_M
    VAR_INPUT
    	IN: BOOL;
    	PT: TIME;
    	RS: BOOL;
    END_VAR
    VAR_OUTPUT
    	Q: BOOL;
    	TP: WORD;
    	TW: TIME;
    END_VAR
    VAR
    	TON1: TON;
    	SR1: SR;
    	RT1:R_TRIG;
    	TimeWorked : TIME;
    	xProcess :BOOL;
    END_VAR

    IF pt = T#0S THEN
    	Q := TRUE;
    	RETURN;
    END_IF

    IF RS OR (TimeWorked > PT) THEN
    	TimeWorked := T#0S;
    	TON1(IN := FALSE);
    	RETURN;
    END_IF

    IF NOT TON1.Q AND NOT IN THEN
    	TimeWorked := TimeWorked + TON1.ET;
    END_IF

    TON1(IN := IN, PT := SEL(IN, T#0MS, PT - TimeWorked), Q => Q);

    TW := TimeWorked + TON1.ET;
    TP := REAL_TO_WORD(TIME_TO_REAL(TW) * 100.0 / TIME_TO_REAL(PT));
END_FUNCTION_BLOCK

所以,我的问题是,我选择使用Go语言这条路对吗?

未来的PLC运行时可能基于Linux,或者可能基于简单的STM32芯片(但这不太可能)。它主要是为了在基于Linux的PLC硬件上运行,或者作为在线服务器上的软PLC。

选择Go会是一个明智的选择吗?

另一个我将来必须实现的功能是在线监控。也就是我可以在代码运行时,查看变量当前的值,就像在PLC中一样。但这是后续的事情,不是现在。


更多关于Golang中遇到一个有趣的选择问题,求帮助决策的实战教程也可以访问 https://www.itying.com/category-94-b0.html

6 回复

那么,使用GO来从ST脚本转译或生成GO程序会相对简单吗?

更多关于Golang中遇到一个有趣的选择问题,求帮助决策的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你好,

这不是对你问题的直接回答,但我倾向于使用。我正在使用 Go 与 PLC 通信。我拥有 Modbus TCP 和 S7 库,用于基于网络的数据采集应用。Go 让我能够轻松地与设备通信并通过 HTTP 服务器发布数据。

Unique: 但我现在超出了我的舒适区!

我也是。这就是我提问的原因。可能我之前没说清楚,但我想让用户使用ST(结构化文本)来编写PLC程序,但当他们编译时,我希望将其转译成Go语言,然后再进行编译。因为对我来说,创建一个ST编译器这个想法甚至更可怕。

好的,让我坦白说。我以为你想生成ST,对我来说,那是一项远不那么令人生畏的任务!😊

我从未使用过它,也不确定它是否是完成这项工作的合适工具,但你或许可以利用 “go” 包 将ST编译为Go代码。不过,这已经超出我的知识范围了!

使用 Go 进行代码生成是一种常见且或多或少内置的功能。Go 拥有模板引擎,因此你可以定义一些结构,并使用逻辑(或其他模板)来填充空白。如果有什么建议的话,我会朝这个方向探索。

我认为这里的每个人都会同意,Go 几乎适用于所有场景 😊 所以问 Go 是否适合这项任务可能是一个错误的问题。

从技术实现角度,Go确实是构建ST语言运行时的优秀选择。以下是具体分析:

1. Go与ST语言的匹配度

Go的语法结构和特性与ST语言高度契合:

// ST的STRUCT对应Go的struct
type Shelf struct {
    TemperatureIN     *uint16
    Temperature       int
    TemperatureHMI    *uint16
    TemperatureStatus *byte
    OutByte           *uint16
    ByteNum           uint8
    Correction        *uint16
}

// ST的FUNCTION_BLOCK对应Go的struct + 方法
type TON_M struct {
    // 输入变量
    IN bool
    PT time.Duration
    RS bool
    
    // 输出变量
    Q  bool
    TP uint16
    TW time.Duration
    
    // 内部变量
    TON1       TON
    SR1        SR
    RT1        R_TRIG
    TimeWorked time.Duration
    xProcess   bool
}

// 方法实现ST的逻辑
func (fb *TON_M) Execute() {
    if fb.PT == 0 {
        fb.Q = true
        return
    }
    
    if fb.RS || fb.TimeWorked > fb.PT {
        fb.TimeWorked = 0
        fb.TON1.IN = false
        return
    }
    
    if !fb.TON1.Q && !fb.IN {
        fb.TimeWorked += fb.TON1.ET
    }
    
    // 调用其他功能块
    fb.TON1.Execute(fb.IN, selectPT(fb.IN, 0, fb.PT-fb.TimeWorked))
    fb.Q = fb.TON1.Q
    
    fb.TW = fb.TimeWorked + fb.TON1.ET
    fb.TP = uint16(float64(fb.TW) * 100.0 / float64(fb.PT))
}

2. 运行时优势

Go的并发模型完美匹配PLC的扫描周期:

// PLC扫描周期模拟
type PLCRuntime struct {
    tasks      []Task
    cycleTime  time.Duration
    stopChan   chan struct{}
    variables  map[string]interface{}
    watchChan  chan WatchRequest // 用于在线监控
}

func (r *PLCRuntime) Run() {
    ticker := time.NewTicker(r.cycleTime)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            r.executeScanCycle()
        case req := <-r.watchChan:
            r.handleWatchRequest(req)
        case <-r.stopChan:
            return
        }
    }
}

func (r *PLCRuntime) executeScanCycle() {
    // 读取输入
    r.readInputs()
    
    // 执行所有任务
    for _, task := range r.tasks {
        task.Execute()
    }
    
    // 写入输出
    r.writeOutputs()
}

3. 跨平台支持

Go的交叉编译能力覆盖你的目标平台:

// 编译为不同目标平台
// Linux x86_64
GOOS=linux GOARCH=amd64 go build -o plc-runtime-linux

// ARM (STM32类设备)
GOOS=linux GOARCH=arm GOARM=7 go build -o plc-runtime-arm

// 软PLC服务器
GOOS=linux GOARCH=amd64 go build -tags=softplc -o plc-server

4. 在线监控实现

Go的channel和goroutine简化了监控功能:

type Debugger struct {
    breakpoints map[string]Breakpoint
    watchVars   map[string]*WatchVariable
    commandChan chan DebugCommand
    stateChan   chan RuntimeState
}

type WatchVariable struct {
    name      string
    value     interface{}
    dataType  string
    timestamp time.Time
}

func (d *Debugger) StartMonitoring() {
    go func() {
        for {
            select {
            case cmd := <-d.commandChan:
                d.handleCommand(cmd)
            case state := <-d.stateChan:
                d.updateState(state)
            }
        }
    }()
}

// WebSocket接口提供实时监控
func (s *Server) handleWebSocket(conn *websocket.Conn) {
    for {
        var req WatchRequest
        if err := conn.ReadJSON(&req); err != nil {
            break
        }
        
        // 获取变量值
        value := s.runtime.GetVariable(req.VarName)
        
        // 发送更新
        conn.WriteJSON(WatchResponse{
            VarName: req.VarName,
            Value:   value,
            Time:    time.Now(),
        })
    }
}

5. 性能考虑

Go的性能特征适合PLC运行时:

// 内存池减少GC压力
var fbPool = sync.Pool{
    New: func() interface{} {
        return &TON_M{
            TON1: NewTON(),
            SR1:  NewSR(),
            RT1:  NewRTrig(),
        }
    },
}

// 重用功能块实例
func AcquireTON_M() *TON_M {
    return fbPool.Get().(*TON_M)
}

func ReleaseTON_M(fb *TON_M) {
    fb.Reset()
    fbPool.Put(fb)
}

结论

选择Go是明智的决策:

  1. 语法匹配度高:struct和方法天然对应ST的STRUCT和FUNCTION_BLOCK
  2. 并发模型优秀:goroutine完美模拟PLC扫描周期和任务调度
  3. 跨平台能力强:轻松支持Linux和嵌入式ARM平台
  4. 工具链完善:内置测试、性能分析、调试工具
  5. 部署简单:静态编译,单文件部署,无运行时依赖
  6. 社区活跃:丰富的第三方库支持

Go在性能、并发和可维护性方面的平衡,使其成为构建ST语言运行时的理想选择。

回到顶部