【SCPI学习】STM32与LWIP实现SCPI命令解析

文章目录

  • 一、参考教程
  • 二、背景
  • 三、SCPI简介
    • [3.1 什么是SCPI](#3.1 什么是SCPI)
    • [3.2 基本概念](#3.2 基本概念)
      • [3.2.1 命令类型:](#3.2.1 命令类型:)
      • [3.2.2 语法知识:](#3.2.2 语法知识:)
  • 四、移植
    • [4.1 移植目的](#4.1 移植目的)
    • [4.2 源代码](#4.2 源代码)
    • [4.3 移植到 STM32F407 + LWIP raw API 的总体思路](#4.3 移植到 STM32F407 + LWIP raw API 的总体思路)
      • [4.3.1 核心思想:](#4.3.1 核心思想:)
      • [4.3.2 主要步骤:](#4.3.2 主要步骤:)
    • [4.4 移植步骤](#4.4 移植步骤)
      • [4.4.1 添加库文件](#4.4.1 添加库文件)
      • [4.4.2 添加 `scpi_server`文件](#4.4.2 添加 scpi_server文件)
        • (1)数据流层次
        • [(2)TCP客户端初始化`void scpi_server_init(void)`,监听接收](#(2)TCP客户端初始化void scpi_server_init(void),监听接收)
        • [(3) 写TCP回调函数` scpi_tcp_accept`,在接收到数据后运行`scpi_tcp_recv`](#(3) 写TCP回调函数 scpi_tcp_accept,在接收到数据后运行scpi_tcp_recv)
        • (4)`scpi_tcp_recv`函数将TCP数据交给SCPI解析器
        • [(5)`SCPI_Write` 函数,SCPI响应数据的TCP发送函数](#(5)SCPI_Write 函数,SCPI响应数据的TCP发送函数)
        • (6)配置宏定义
          • [**1. TCP Keepalive参数**](#1. TCP Keepalive参数)
          • [**2. 端口定义**](#2. 端口定义)
        • (7)完整代码
          • [1. scpi_server.c](#1. scpi_server.c)
          • [2. scpi_server.h](#2. scpi_server.h)
      • [4.4.3 添加`scpi-def` SCPI命令定义文件](#4.4.3 添加scpi-def SCPI命令定义文件)
        • [**(1) 自定义测试命令**](#(1) 自定义测试命令)
        • [**(2)SCPI命令表 - 核心部分**](#(2)SCPI命令表 - 核心部分)
          • [**1. IEEE 488.2强制命令**](#1. IEEE 488.2强制命令)
          • [**2. SCPI必需命令**](#2. SCPI必需命令)
          • [**3. 自定义命令(最重要的部分)**](#3. 自定义命令(最重要的部分))
          • [**4. 特殊命令**](#4. 特殊命令)
          • [**5. 命令表结束标志**](#5. 命令表结束标志)
        • (3)SCPI接口结构体
        • **(4)全局变量**
          • [**1. 输入缓冲区**](#1. 输入缓冲区)
          • [**2. 错误队列**](#2. 错误队列)
          • [**3. SCPI上下文**](#3. SCPI上下文)
        • (5)完整代码
          • [1. scpi-def.c](#1. scpi-def.c)
          • [2. scpi-def.h](#2. scpi-def.h)
    • **五、如何添加自定义命令**
    • **六、SCPI命令模式语法**
      • [**1. 基本模式**](#1. 基本模式)
      • [**2. 可选参数**](#2. 可选参数)
      • [**3. 设置命令**](#3. 设置命令)
  • 七、实验结果
    • [7.1 连接](#7.1 连接)
    • [7.2 发送SCPI命令](#7.2 发送SCPI命令)

一、参考教程

简介

  1. SCPI是什么?SCPI指令简介
  2. 仪器仪表控制:基础篇 SCPI简介

移植

  1. SCPI解析器
  2. SCPI解析器开源项目

二、背景

先前我已经配置好了LWIP,并且能够成功ping通,接下来是在这个基础上添加SCPI解析器,使我的工程能通过网线读懂接受到SCPI命令,并且能正常回应。

前情:
【LWIP学习】dp83848与STM32f407re的接线并实现ping通

三、SCPI简介

3.1 什么是SCPI

SCPI(Standard Commands for Programmable Instruments)语言是一种广泛使用的指令集,用于控制和管理测试设备。SCPI命令基于ASCII文本,易于理解和实现,使得不同厂家的设备能够通过统一的命令进行控制。

3.2 基本概念

3.2.1 命令类型:

设置命令(Set Commands) :用于设置仪器的状态或值,例如 VOLT 20 设置电压为20伏。
查询命令(Query Commands) :用于从仪器获取信息,通常以问号(?)结尾,例如VOLT?用来查询当前电压值。
通用命令 :SCPI还定义了一系列通用命令,适用于大多数仪器,如*IDN?用于查询仪器的身份信息。

3.2.2 语法知识:

SCPI(Standard Commands for Programmable Instruments)语言的语法惯例是标准化的,以确保在不同设备和制造商之间的一致性和兼容性。以下是SCPI命令语法的主要特点和惯例:

  1. 命令结构
    SCPI命令的结构通常遵循一种层次式的设计,其中包括多个层次的命令词,通过冒号(:)分隔。例如:

    c 复制代码
    MEASure:VOLTage:DC?
  • MEASure 是一个子系统,表示测量功能的开始。
  • VOLTage 是一个子系统,指定测量的电气参数类型。- -
  • DC 是一个子系统或修饰符,指定电压的直流类型。
  • ? 表示这是一个查询命令。
  1. 命令大小写

    SCPI命令是不区分大小写的。然而,惯例是使用大写字母来提高可读性,并在文档中突出显示命令词。例如,measure:voltage:dc?MEASURE:VOLTAGE:DC? 在功能上是等价的。

  2. 简写

    SCPI标准允许命令词的简写,只要这种简写在给定的上下文中是唯一的。例如,MEASure:VOLTage:DC? 可以缩写为 MEAS:VOLT:DC?,前提是没有其他命令词以相同的缩写开始。

  3. 参数

    命令可能需要一个或多个参数。参数紧跟在命令之后,用空格分隔。参数可以是数字、字符串或布尔值。例如:

    c 复制代码
    SOURce:VOLTage 20

    这里,20是一个参数,表示将电压设置为20伏。

  4. 分隔符

    冒号(:):用于分隔不同层级的命令词。

    分号(;):用于在单个指令字符串中分隔多个命令,允许在一行中发送多个命令。例如:

    c 复制代码
    *RST; *IDN?

    这会先重置仪器,然后查询仪器的身份信息。

  5. 查询和设置

    查询命令:通常以问号(?)结尾,用于从仪器获取信息或状态。例如,VOLT? 查询当前设置的电压值。

    设置命令:不以问号结尾,用于配置仪器的设置或发送控制命令。例如,VOLT 10 设置电压为10伏。

  6. 通用命令

    SCPI还定义了一组通用命令,这些命令在大多数SCPI兼容设备上都是相同的。例如:
    *IDN?:查询仪器的身份。
    *RST:重置仪器到其默认状态。
    *CLS:清除所有的状态和错误信息。

  7. 错误处理

    SCPI设备通常包含一个错误队列,用于记录执行命令过程中发生的错误。使用命令如SYSTem:ERRor?可以查询和清除错误队列。

这些语法惯例和命令结构确保了SCPI命令的易学性、可读性和跨设备的一致性,使得设备和系统的编程与控制变得直观和标准化。

四、移植

4.1 移植目的

让我的stm32能识别SCPI命令并且能正确回复。

4.2 源代码

用于移植的代码是 SCPI解析器开源项目

该仓库是 libscpi:一个开源的 SCPI(Standard Commands for Programmable Instruments)解析库。

  • 它的职责是:把收到的字节流解析成 SCPI 命令/查询、解析参数/表达式/单位、维护错误队列、并把命令分发到用户实现的回调(命令处理函数)。
  • 使用者需要在平台层实现的东西(接口):
    • scpi_interface_t 中的回调(例如 write、error、control、flush、reset 等)用于 I/O 和控制/错误处理。
    • 定义 scpi_command_t 命令表,给每个命令模式绑定 handler(handler 使用 SCPI_Result* 等接口返回数据)。
  • 仓库里有许多 example(examples/common, examples/test-* 等),可做模板。
    仓库中有很多关键类型与接口如:scpi_interface_t、scpi_commands、SCPI_Init、SCPI_Input 等。

4.3 移植到 STM32F407 + LWIP raw API 的总体思路

4.3.1 核心思想:

把 LWIP raw 接收到的字节直接喂给 SCPI_Input,SCPI 在解析过程中通过你实现的 SCPI_Write 把回复写回对应的 TCP 连接。

4.3.2 主要步骤:

  1. 将 libscpi 源代码(libscpi/src/.c + libscpi/inc/)加入 STM32 工程,调整 include 路径。
  2. 在工程中实现一个 scpi 命令定义文件(参考 examples/common/scpi-def.c),至少包含 IEEE 必需命令(*IDN?等)或其他命令。
  3. 在系统启动时或 TCP accept 时为每个连接(或全局)创建并初始化 scpi_t:
  • SCPI_Init(&ctx, scpi_commands, &scpi_interface, units, idn1, idn2, idn3, idn4, input_buffer, input_buffer_len, error_queue, error_queue_size);
  • 推荐为每个并发连接分配独立 scpi_input_buffer、error_queue 和 scpi_t(否则需要加锁)。
  1. 使用 LWIP raw API 建立 TCP 监听(port 5025 常用),在 accept 回调里为新连接建立 per-connection 结构并设置 recv 回调。
  2. recv 回调里:
  • 从 pbuf 中取出数据:SCPI_Input(&conn->scpi_ctx, (const char*)p->payload, p->len);
  • 调用 tcp_recved(pcb, p->len);
  • 释放 pbuf。
  1. 实现 SCPI_Write 回调使其把 data 写回对应的 tcp_pcb:通常用 tcp_write(pcb, data, len, TCP_WRITE_FLAG_COPY) 然后 tcp_output(pcb);返回已写字节数。
  2. 注意:SCPI 的 handler 里不要做长时间阻塞(否则会阻塞 TCP/IP 回调线程);如果需要做较慢任务,应把任务投递到工作线程/队列中,然后立刻返回。

4.4 移植步骤

4.4.1 添加库文件

  1. 打开下载的工程,复制里面的libscpi文件夹,添加到自己的工程
  2. 将scpi库加入工程,运行后不报错。


    这样就把scpi库成功添加了

4.4.2 添加 scpi_server文件

SCPI命令服务器,通过TCP/IP网络接收和处理SCPI命令。

(1)数据流层次
复制代码
TCP客户端 → TCP接收 → SCPI解析器 → 命令执行 → 响应 → TCP发送
(2)TCP客户端初始化void scpi_server_init(void),监听接收
  1. 初始化SCPI解析器
  2. 使用pcb = tcp_new();创建TCP控制块
  3. tcp_bind(pcb, NULL, SCPI_DEVICE_PORT);绑定端口
  4. tcp_listen(pcb)启动监听
  5. tcp_accept(server_state.listen_pcb, scpi_tcp_accept);当监听到了接收数据后运行scpi_tcp_accept
c 复制代码
/**
 * @brief Initialize SCPI TCP server
 */
void scpi_server_init(void) {
    err_t err;
    struct tcp_pcb *pcb;
    
    // Initialize SCPI parser
    SCPI_Init(&scpi_context,
              scpi_commands,
              &scpi_interface,
              scpi_units_def,
              SCPI_IDN1, SCPI_IDN2, SCPI_IDN3, SCPI_IDN4,
              scpi_input_buffer, SCPI_INPUT_BUFFER_LENGTH,
              scpi_error_queue_data, SCPI_ERROR_QUEUE_SIZE);
    
    // Set user context
    scpi_context.user_context = &server_state;
    
    // Create listening PCB
    pcb = tcp_new();
    if (pcb == NULL) {
        return; // Failed to create PCB
    }
    
    // Bind to port
    err = tcp_bind(pcb, NULL, SCPI_DEVICE_PORT);
    if (err != ERR_OK) {
        tcp_close(pcb);
        return; // Failed to bind
    }
    
    // Listen for connections
    server_state.listen_pcb = tcp_listen(pcb);
    if (server_state.listen_pcb == NULL) {
        tcp_close(pcb);
        return; // Failed to listen
    }
    
    // Set accept callback
    tcp_accept(server_state.listen_pcb, scpi_tcp_accept);
    
    // Initialize server state
    server_state.client_pcb = NULL;
    server_state.connected = 0;
}
(3) 写TCP回调函数 scpi_tcp_accept,在接收到数据后运行scpi_tcp_recv

主要负责连接管理

复制代码
// 接收回调(模板)
err_t my_tcp_recv(void *arg, struct tcp_pcb *pcb, 
                  struct pbuf *p, err_t err) {
    // 1. 处理空指针(连接关闭)
    // 2. 检查错误
    // 3. 处理数据
    // 4. 确认接收
    // 5. 释放pbuf
    // 6. 返回状态
}
c 复制代码
/**
 * @brief TCP accept callback (new connection)
 */
static err_t scpi_tcp_accept(void *arg, struct tcp_pcb *newpcb, err_t err) {
    (void)arg;
    (void)err;
    
    if (err != ERR_OK || newpcb == NULL) {
        return ERR_VAL;
    }
    
    // Reject if already connected
    if (server_state.connected && server_state.client_pcb != NULL) {
        tcp_abort(newpcb);// 拒绝新连接
        return ERR_ABRT;
    }
    
    // Accept new connection
    server_state.client_pcb = newpcb;
    server_state.connected = 1;
    
    // Set TCP callbacks
    tcp_arg(newpcb, newpcb); // 作用:设置TCP控制块的用户参数,这个参数会在回调函数中传递回来
    tcp_recv(newpcb, scpi_tcp_recv);// 作用:设置接收数据回调函数,当TCP接收到数据时,会自动调用scpi_tcp_recv
    tcp_err(newpcb, scpi_tcp_err);// 作用:设置错误回调函数,当TCP连接出错时会自动调用
    
    // Set TCP options
    tcp_nagle_disable(newpcb); // Disable Nagle algorithm for low latency 禁用Nagle算法
    
    // Notify connection
    SCPI_Event_DeviceConnected(&scpi_context, newpcb);//连接时事件通知,目前是空的
    
    return ERR_OK;
}
(4)scpi_tcp_recv函数将TCP数据交给SCPI解析器

SCPI_Input(&scpi_context, (char *)q->payload, q->len); : 将TCP数据交给SCPI解析器
tcp_recved()进行TCP确认
pbuf_free()释放内存

c 复制代码
/**
 * @brief TCP receive callback 作用:接收TCP数据并传递给SCPI解析器
 */
static err_t scpi_tcp_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) {
    (void)arg;
    
    if (p == NULL) {
        // Connection closed 连接关闭的情况
        SCPI_Event_DeviceDisconnected(&scpi_context, pcb);
        server_state.client_pcb = NULL;
        server_state.connected = 0;
        tcp_close(pcb);
        return ERR_OK;
    }
    
    if (err != ERR_OK) {
        // Error occurred 错误处理
        pbuf_free(p);
        return err;
    }
    
    // Process received data 处理接收的数据
    // Extract all data from pbuf chain
    struct pbuf *q = p;
    while (q != NULL) {
        // Send data to SCPI parser 关键:将TCP数据交给SCPI解析器
        SCPI_Input(&scpi_context, (char *)q->payload, q->len);
        q = q->next;
    }
    
    // Acknowledge received data 确认接收(TCP滑动窗口)
    tcp_recved(pcb, p->tot_len);
    pbuf_free(p); // 必须释放!
    
    return ERR_OK;
}
(5)SCPI_Write 函数,SCPI响应数据的TCP发送函数

当SCPI解析器需要发送响应数据(如测量结果、设备信息等)时,会调用这个函数,将数据通过TCP发送给客户端。

检查连接状态,使用tcp_write循环发送确保完整数据,最后调用tcp_output()刷新缓冲区

c 复制代码
/**
 * @brief SCPI write function - sends data over TCP
 */
size_t SCPI_Write(scpi_t * context, const char * data, size_t len) {
    (void)context;
    
   // 检查TCP连接是否有效    
    if (server_state.client_pcb != NULL && server_state.connected) {
        err_t err;
        size_t sent = 0;
        
        // 循环发送直到所有数据发送完或出错
        while (sent < len) { 
            err = tcp_write(server_state.client_pcb, data + sent, len - sent, TCP_WRITE_FLAG_COPY);
            if (err == ERR_OK) {
                sent += len - sent;
            } else if (err == ERR_MEM) {
                // Buffer full, try to output
                tcp_output(server_state.client_pcb);
                // Retry later
                break;
            } else {
                // Error occurred
                break;
            }
        }
         
        // 强制TCP输出缓冲数据
        // Try to send buffered data
        if (sent > 0) {
            tcp_output(server_state.client_pcb);
        }
        
        return sent;
    }
    return 0;
}
(6)配置宏定义
1. TCP Keepalive参数
c 复制代码
#define SCPI_KEEP_IDLE    2000 // (ms) 空闲等待时间
#define SCPI_KEEP_INTVL   1000 // (ms) 重试间隔
#define SCPI_KEEP_CNT        4 // 重试次数

作用:TCP保活机制,检测死连接。

复制代码
工作流程:
1. 连接空闲2秒后 → 开始发送Keepalive探测包
2. 如果没响应 → 每隔1秒重试一次
3. 连续4次失败 → 认为连接断开
2. 端口定义
c 复制代码
#define SCPI_DEVICE_PORT  5025 // scpi-raw标准端口

作用

  • 5025是SCPI-over-TCP的标准端口
  • 类似于HTTP的80端口
  • 客户端默认会连接这个端口
(7)完整代码
1. scpi_server.c
c 复制代码
/*-
 * BSD 2-Clause License
 *
 * Copyright (c) 2012-2018, Jan Breuer
 * All rights reserved.
 */

/**
 * @file   scpi_server.c
 * @brief  TCP/IP SCPI Server using LWIP Raw TCP API (bare metal)
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "scpi/scpi.h"
#include "scpi/units.h"
#include "scpi-def.h"
#include "scpi_server.h"

#include "lwip/tcp.h"
#include "lwip/tcpip.h"
#include "lwip/inet.h"
#include "lwip/ip_addr.h"
#include "lwip/ip4_addr.h"

/* SCPI server state */
typedef struct {
    struct tcp_pcb *listen_pcb;
    struct tcp_pcb *client_pcb;
    uint8_t connected;
} scpi_server_state_t;

static scpi_server_state_t server_state = {0};

/* TCP receive buffer for SCPI commands - not needed, SCPI has its own buffer */

/**
 * @brief SCPI write function - sends data over TCP
 */
size_t SCPI_Write(scpi_t * context, const char * data, size_t len) {
    (void)context;
    
    if (server_state.client_pcb != NULL && server_state.connected) {
        err_t err;
        size_t sent = 0;
        
        while (sent < len) { //循环发送
            err = tcp_write(server_state.client_pcb, data + sent, len - sent, TCP_WRITE_FLAG_COPY);
            if (err == ERR_OK) {
                sent += len - sent;
            } else if (err == ERR_MEM) {
                // Buffer full, try to output
                tcp_output(server_state.client_pcb);
                // Retry later
                break;
            } else {
                // Error occurred
                break;
            }
        }
        
        // Try to send buffered data
        if (sent > 0) {
            tcp_output(server_state.client_pcb);
        }
        
        return sent;
    }
    return 0;
}

/**
 * @brief SCPI flush function
 */
scpi_result_t SCPI_Flush(scpi_t * context) {
    (void)context;
    
    if (server_state.client_pcb != NULL && server_state.connected) {
        tcp_output(server_state.client_pcb);
    }
    return SCPI_RES_OK;
}

/**
 * @brief SCPI error function
 */
int SCPI_Error(scpi_t * context, int_fast16_t err) {
    (void)context;
    
    if (err != 0) {
        /* New error */
        SCPI_Event_ErrorIndicatorOn(context, err);
    } else {
        /* No more errors in the queue */
        SCPI_Event_ErrorIndicatorOff(context, err);
    }
    return 0;
}

/**
 * @brief SCPI control function (for SRQ, etc.)
 */
scpi_result_t SCPI_Control(scpi_t * context, scpi_ctrl_name_t ctrl, scpi_reg_val_t val) {
    (void)context;
    
    if (SCPI_CTRL_SRQ == ctrl) {
        // Service request - can be implemented if needed
    }
    return SCPI_RES_OK;
}

/**
 * @brief SCPI reset function
 */
scpi_result_t __attribute__((weak)) SCPI_Reset(scpi_t * context) {
    (void)context;
    return SCPI_RES_OK;
}

/**
 * @brief System communication TCP/IP control query
 */
scpi_result_t SCPI_SystemCommTcpipControlQ(scpi_t * context) {
    (void)context;
    // Control port not implemented in this simple version
    SCPI_ResultInt(context, 0);
    return SCPI_RES_OK;
}

/**
 * @brief TCP error callback
 */
static void scpi_tcp_err(void *arg) {
    (void)arg;
    struct tcp_pcb *pcb = (struct tcp_pcb *)arg;
    
    if (pcb == server_state.client_pcb) {
        SCPI_Event_DeviceDisconnected(&scpi_context, pcb);
        server_state.client_pcb = NULL;
        server_state.connected = 0;
        //recv_buffer_idx = 0;
    }
}

/**
 * @brief TCP receive callback 作用:接收TCP数据并传递给SCPI解析器
 */
static err_t scpi_tcp_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) {
    (void)arg;
    
    if (p == NULL) {
        // Connection closed 连接关闭的情况
        SCPI_Event_DeviceDisconnected(&scpi_context, pcb);
        server_state.client_pcb = NULL;
        server_state.connected = 0;
        tcp_close(pcb);
        return ERR_OK;
    }
    
    if (err != ERR_OK) {
        // Error occurred 错误处理
        pbuf_free(p);
        return err;
    }
    
    // Process received data 处理接收的数据
    // Extract all data from pbuf chain
    struct pbuf *q = p;
    while (q != NULL) {
        // Send data to SCPI parser 关键:将TCP数据交给SCPI解析器
        SCPI_Input(&scpi_context, (char *)q->payload, q->len);
        q = q->next;
    }
    
    // Acknowledge received data 确认接收(TCP滑动窗口)
    tcp_recved(pcb, p->tot_len);
    pbuf_free(p); // 必须释放!
    
    return ERR_OK;
}

/**
 * @brief TCP accept callback (new connection)
 */
static err_t scpi_tcp_accept(void *arg, struct tcp_pcb *newpcb, err_t err) {
    (void)arg;
    (void)err;
    
    if (err != ERR_OK || newpcb == NULL) {
        return ERR_VAL;
    }
    
    // Reject if already connected
    if (server_state.connected && server_state.client_pcb != NULL) {
        tcp_abort(newpcb);// 拒绝新连接
        return ERR_ABRT;
    }
    
    // Accept new connection
    server_state.client_pcb = newpcb;
    server_state.connected = 1;
    
    // Set TCP callbacks
    tcp_arg(newpcb, newpcb); // 作用:设置TCP控制块的用户参数,这个参数会在回调函数中传递回来
    tcp_recv(newpcb, scpi_tcp_recv);// 作用:设置接收数据回调函数,当TCP接收到数据时,会自动调用scpi_tcp_recv
    tcp_err(newpcb, scpi_tcp_err);// 作用:设置错误回调函数,当TCP连接出错时会自动调用
    
    // Set TCP options
    tcp_nagle_disable(newpcb); // Disable Nagle algorithm for low latency 禁用Nagle算法
    
    // Notify connection
    SCPI_Event_DeviceConnected(&scpi_context, newpcb);//连接时事件通知,目前是空的
    
    return ERR_OK;
}

/**
 * @brief Default event handlers (weak symbols, can be overridden)
 */
void __attribute__((weak)) SCPI_Event_DeviceConnected(scpi_t * context, struct tcp_pcb * pcb) {
    (void)context;
    (void)pcb;
    // Can add LED indication or logging here
}

void __attribute__((weak)) SCPI_Event_DeviceDisconnected(scpi_t * context, struct tcp_pcb * pcb) {
    (void)context;
    (void)pcb;
    // Can add LED indication or logging here
}

void __attribute__((weak)) SCPI_Event_ErrorIndicatorOn(scpi_t * context, int_fast16_t err) {
    (void)context;
    // Can add error indication here
    (void)err;
}

void __attribute__((weak)) SCPI_Event_ErrorIndicatorOff(scpi_t * context, int_fast16_t err) {
    (void)context;
    (void)err;
    // Can add error indication here
}

/**
 * @brief Initialize SCPI TCP server
 */
void scpi_server_init(void) {
    err_t err;
    struct tcp_pcb *pcb;
    
    // Initialize SCPI parser
    SCPI_Init(&scpi_context,
              scpi_commands,
              &scpi_interface,
              scpi_units_def,
              SCPI_IDN1, SCPI_IDN2, SCPI_IDN3, SCPI_IDN4,
              scpi_input_buffer, SCPI_INPUT_BUFFER_LENGTH,
              scpi_error_queue_data, SCPI_ERROR_QUEUE_SIZE);
    
    // Set user context
    scpi_context.user_context = &server_state;
    
    // Create listening PCB
    pcb = tcp_new();
    if (pcb == NULL) {
        return; // Failed to create PCB
    }
    
    // Bind to port
    err = tcp_bind(pcb, NULL, SCPI_DEVICE_PORT);
    if (err != ERR_OK) {
        tcp_close(pcb);
        return; // Failed to bind
    }
    
    // Listen for connections
    server_state.listen_pcb = tcp_listen(pcb);
    if (server_state.listen_pcb == NULL) {
        tcp_close(pcb);
        return; // Failed to listen
    }
    
    // Set accept callback
    tcp_accept(server_state.listen_pcb, scpi_tcp_accept);
    
    // Initialize server state
    server_state.client_pcb = NULL;
    server_state.connected = 0;
}

/**
 * @brief Process SCPI server (should be called periodically in main loop)
 */
void scpi_server_process(void) {
    // Process timeout for SCPI parser
    SCPI_Input(&scpi_context, NULL, 0);
    
    // LWIP timers are handled by sys_check_timeouts() in main loop
}
2. scpi_server.h
c 复制代码
/*-
 * BSD 2-Clause License
 *
 * Copyright (c) 2012-2018, Jan Breuer
 * All rights reserved.
 */

#ifndef _SCPI_SERVER_H_
#define _SCPI_SERVER_H_
#ifdef __cplusplus
extern "C" {
#endif

#define SCPI_KEEP_IDLE    2000 // (ms) keepalive quiet time after last TCP packet
#define SCPI_KEEP_INTVL   1000 // (ms) keepalive repeat interval
#define SCPI_KEEP_CNT        4 // Retry count before terminating connection

#define SCPI_DEVICE_PORT  5025 // scpi-raw standard port

#include <stdint.h>
#include "lwip/tcp.h"
#include "scpi/types.h"

/**
 * @brief Initialize SCPI TCP server
 * This function should be called after LWIP initialization
 */
void scpi_server_init(void);

/**
 * @brief Process SCPI server (should be called in main loop)
 */
void scpi_server_process(void);

// optional event handlers
void SCPI_Event_DeviceConnected(scpi_t * context, struct tcp_pcb * pcb);
void SCPI_Event_DeviceDisconnected(scpi_t * context, struct tcp_pcb * pcb);
void SCPI_Event_ErrorIndicatorOn(scpi_t * context, int_fast16_t err);
void SCPI_Event_ErrorIndicatorOff(scpi_t * context, int_fast16_t err);

#ifdef __cplusplus
}
#endif
#endif /* _SCPI_SERVER_H_ */

4.4.3 添加scpi-def SCPI命令定义文件

SCPI命令定义文件,是SCPI服务器的核心配置文件,定义了所有可用的SCPI命令。

(1) 自定义测试命令
c 复制代码
static scpi_result_t My_CoreTstQ(scpi_t * context) {
    SCPI_ResultInt32(context, 0);
    return SCPI_RES_OK;
}

作用

  • *TST?是IEEE 488.2标准命令(自检)
  • 返回0表示设备正常
  • 用户可以修改这个函数执行真正的自检逻辑
(2)SCPI命令表 - 核心部分
1. IEEE 488.2强制命令
c 复制代码
{ .pattern = "*CLS", .callback = SCPI_CoreCls,},      // 清除状态
{ .pattern = "*ESE", .callback = SCPI_CoreEse,},      // 设置事件状态使能
{ .pattern = "*IDN?", .callback = SCPI_CoreIdnQ,},    // 查询设备标识
{ .pattern = "*RST", .callback = SCPI_CoreRst,},      // 设备复位

必须实现:这些是标准仪器必须支持的命令。

2. SCPI必需命令
c 复制代码
{.pattern = "SYSTem:ERRor[:NEXT]?", .callback = SCPI_SystemErrorNextQ,},
{.pattern = "SYSTem:VERSion?", .callback = SCPI_SystemVersionQ,},
{.pattern = "STATus:PRESet", .callback = SCPI_StatusPreset,},

作用:提供错误查询、版本查询、状态复位等功能。

3. 自定义命令(最重要的部分)
c 复制代码
/* Custom commands for your instrument - Add your commands here */
/* Example:
{.pattern = "MEASure:VOLTage:DC?", .callback = Your_MeasureVoltageDcQ,},
*/

这是添加自己命令的地方

4. 特殊命令
c 复制代码
{.pattern = "SYSTem:COMMunication:TCPIP:CONTROL?", .callback = SCPI_SystemCommTcpipControlQ,},

作用:查询TCP/IP控制端口状态。

5. 命令表结束标志
c 复制代码
SCPI_CMD_LIST_END

作用:标记命令表结束,这是一个宏定义。

(3)SCPI接口结构体
c 复制代码
scpi_interface_t scpi_interface = {
    .error = SCPI_Error,      // 错误处理函数
    .write = SCPI_Write,      // 数据发送函数(TCP发送)
    .control = SCPI_Control,  // 控制函数(SRQ等)
    .flush = SCPI_Flush,      // 刷新输出缓冲区
    .reset = SCPI_Reset,      // 设备复位函数
};

作用:将用户实现的函数注册到SCPI库中。

(4)全局变量
1. 输入缓冲区
c 复制代码
char scpi_input_buffer[SCPI_INPUT_BUFFER_LENGTH];

作用:存储从TCP接收的SCPI命令。

  • 大小在scpi-def.h中定义,例如#define SCPI_INPUT_BUFFER_LENGTH 256
2. 错误队列
c 复制代码
scpi_error_t scpi_error_queue_data[SCPI_ERROR_QUEUE_SIZE];

作用 :存储发生的错误,可以通过SYSTem:ERRor?查询。

3. SCPI上下文
c 复制代码
scpi_t scpi_context;

作用:SCPI解析器的全局上下文,包含所有状态信息。

(5)完整代码
1. scpi-def.c
c 复制代码
/*-
 * BSD 2-Clause License
 *
 * Copyright (c) 2012-2018, Jan Breuer
 * All rights reserved.
 */

/**
 * @file   scpi-def.c
 * @brief  SCPI parser command definitions
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "scpi/scpi.h"
#include "scpi-def.h"

/**
 * Reimplement IEEE488.2 *TST?
 *
 * Result should be 0 if everything is ok
 * Result should be 1 if something goes wrong
 *
 * Return SCPI_RES_OK
 */
static scpi_result_t My_CoreTstQ(scpi_t * context) {
    SCPI_ResultInt32(context, 0);
    return SCPI_RES_OK;
}

const scpi_command_t scpi_commands[] = {
    /* IEEE Mandated Commands (SCPI std V1999.0 4.1.1) */
    { .pattern = "*CLS", .callback = SCPI_CoreCls,},// 清除状态
    { .pattern = "*ESE", .callback = SCPI_CoreEse,},// 设置事件状态使能
    { .pattern = "*ESE?", .callback = SCPI_CoreEseQ,},
    { .pattern = "*ESR?", .callback = SCPI_CoreEsrQ,},
    { .pattern = "*IDN?", .callback = SCPI_CoreIdnQ,},// 查询设备标识
    { .pattern = "*OPC", .callback = SCPI_CoreOpc,},
    { .pattern = "*OPC?", .callback = SCPI_CoreOpcQ,},
    { .pattern = "*RST", .callback = SCPI_CoreRst,},// 设备复位
    { .pattern = "*SRE", .callback = SCPI_CoreSre,},
    { .pattern = "*SRE?", .callback = SCPI_CoreSreQ,},
    { .pattern = "*STB?", .callback = SCPI_CoreStbQ,},
    { .pattern = "*TST?", .callback = My_CoreTstQ,},
    { .pattern = "*WAI", .callback = SCPI_CoreWai,},

    /* Required SCPI commands (SCPI std V1999.0 4.2.1) */
    {.pattern = "SYSTem:ERRor[:NEXT]?", .callback = SCPI_SystemErrorNextQ,},
    {.pattern = "SYSTem:ERRor:COUNt?", .callback = SCPI_SystemErrorCountQ,},
    {.pattern = "SYSTem:VERSion?", .callback = SCPI_SystemVersionQ,},

    {.pattern = "STATus:QUEStionable[:EVENt]?", .callback = SCPI_StatusQuestionableEventQ,},
    {.pattern = "STATus:QUEStionable:ENABle", .callback = SCPI_StatusQuestionableEnable,},
    {.pattern = "STATus:QUEStionable:ENABle?", .callback = SCPI_StatusQuestionableEnableQ,},

    {.pattern = "STATus:PRESet", .callback = SCPI_StatusPreset,},

    /* Custom commands for your instrument - Add your commands here */
    /* Example:
    {.pattern = "MEASure:VOLTage:DC?", .callback = Your_MeasureVoltageDcQ,},
    */

    {.pattern = "SYSTem:COMMunication:TCPIP:CONTROL?", .callback = SCPI_SystemCommTcpipControlQ,},

    SCPI_CMD_LIST_END
};

scpi_interface_t scpi_interface = {
    .error = SCPI_Error,
    .write = SCPI_Write,
    .control = SCPI_Control,
    .flush = SCPI_Flush,
    .reset = SCPI_Reset,
};

char scpi_input_buffer[SCPI_INPUT_BUFFER_LENGTH];
scpi_error_t scpi_error_queue_data[SCPI_ERROR_QUEUE_SIZE];

scpi_t scpi_context;
2. scpi-def.h
c 复制代码
/*-
 * BSD 2-Clause License
 *
 * Copyright (c) 2012-2018, Jan Breuer
 * All rights reserved.
 */

#ifndef __SCPI_DEF_H_
#define __SCPI_DEF_H_
#ifdef __cplusplus
extern "C" {
#endif

#include "scpi/scpi.h"

#define SCPI_INPUT_BUFFER_LENGTH 256
#define SCPI_ERROR_QUEUE_SIZE 17
#define SCPI_IDN1 "MANUFACTURE"
#define SCPI_IDN2 "INSTR2013"
#define SCPI_IDN3 NULL
#define SCPI_IDN4 "01-02"

extern const scpi_command_t scpi_commands[];
extern scpi_interface_t scpi_interface;
extern char scpi_input_buffer[];
extern scpi_error_t scpi_error_queue_data[];
extern scpi_t scpi_context;

size_t SCPI_Write(scpi_t * context, const char * data, size_t len);
int SCPI_Error(scpi_t * context, int_fast16_t err);
scpi_result_t SCPI_Control(scpi_t * context, scpi_ctrl_name_t ctrl, scpi_reg_val_t val);
scpi_result_t SCPI_Reset(scpi_t * context);
scpi_result_t SCPI_Flush(scpi_t * context);

scpi_result_t SCPI_SystemCommTcpipControlQ(scpi_t * context);

#ifdef __cplusplus
}
#endif
#endif /* __SCPI_DEF_H_ */

五、如何添加自定义命令

步骤1:定义命令处理函数

c 复制代码
// 在scpi-def.c中添加函数
static scpi_result_t MY_MEAS_VOLTAGE_Q(scpi_t * context) {
    float voltage;
    
    // 1. 读取实际电压(硬件相关)
    voltage = read_adc_voltage();
    
    // 2. 返回结果
    SCPI_ResultFloat(context, voltage);
    
    return SCPI_RES_OK;
}

static scpi_result_t MY_OUTPUT_ON(scpi_t * context) {
    // 解析参数
    scpi_bool_t enable;
    
    if (!SCPI_ParamBool(context, &enable, TRUE)) {
        return SCPI_RES_ERR;
    }
    
    // 控制硬件
    if (enable) {
        gpio_set(OUTPUT_PIN, HIGH);
    } else {
        gpio_set(OUTPUT_PIN, LOW);
    }
    
    return SCPI_RES_OK;
}

步骤2:添加到命令表

c 复制代码
const scpi_command_t scpi_commands[] = {
    // ... 原有命令 ...
    
    /* 自定义命令 */
    {.pattern = "MEASure:VOLTage?", .callback = MY_MEAS_VOLTAGE_Q,},
    {.pattern = "OUTPut:STATe", .callback = MY_OUTPUT_ON,},
    {.pattern = "OUTPut:STATe?", .callback = MY_OUTPUT_QUERY,},
    
    SCPI_CMD_LIST_END
};

步骤3:实现查询命令

c 复制代码
static scpi_result_t MY_OUTPUT_QUERY(scpi_t * context) {
    // 读取硬件状态
    uint8_t state = gpio_get(OUTPUT_PIN);
    
    // 返回结果(1表示ON,0表示OFF)
    SCPI_ResultBool(context, state);
    
    return SCPI_RES_OK;
}

六、SCPI命令模式语法

1. 基本模式

c 复制代码
{ .pattern = "*IDN?", .callback = SCPI_CoreIdnQ,}
// 命令:*IDN?
// 作用:查询设备标识

2. 可选参数

c 复制代码
{ .pattern = "SYSTem:ERRor[:NEXT]?", .callback = SCPI_SystemErrorNextQ,}
// 命令可以是:
//   SYSTEM:ERROR?     查询下一个错误
//   SYSTEM:ERROR:NEXT? 同上(可选部分)

3. 设置命令

c 复制代码
{ .pattern = "*ESE", .callback = SCPI_CoreEse,}
// 命令:*ESE 123
// 作用:设置事件状态使能寄存器

七、实验结果

7.1 连接

使用野火多功能调试助手,配置端口ip等,打开监听,详细见前文

7.2 发送SCPI命令

能收到服务器端的回复

相关推荐
豆沙沙包?2 小时前
2026年--Lc342-841. 钥匙和房间(图 - 广度优先搜索)--java版
java·算法·宽度优先
小范馆2 小时前
STM32F03C8T6通过AT指令获取天气API
前端·javascript·stm32
Emilin Amy2 小时前
【C++】【STL算法】那些STL算法替代的循环
开发语言·c++·算法·ros1/2
Hcoco_me2 小时前
大模型面试题74:在使用GRPO训练LLM时,训练数据有什么要求?
人工智能·深度学习·算法·机器学习·chatgpt·机器人
天赐学c语言2 小时前
1.16 - 二叉树的中序遍历 && 动态多态的实现原理
数据结构·c++·算法·leecode
CQ_YM2 小时前
51单片机(4)
单片机·嵌入式硬件·51单片机
jh10_2 小时前
嵌入式硬件day1
嵌入式硬件
sin_hielo2 小时前
leetcode 2975
数据结构·算法·leetcode
java修仙传3 小时前
力扣hot100:跳跃游戏
算法·leetcode·游戏