文章目录
- 一、参考教程
- 二、背景
- 三、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-defSCPI命令定义文件) -
- [**(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命令)
一、参考教程
简介
移植
二、背景
先前我已经配置好了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命令语法的主要特点和惯例:
-
命令结构
SCPI命令的结构通常遵循一种层次式的设计,其中包括多个层次的命令词,通过冒号(:)分隔。例如:cMEASure:VOLTage:DC?
MEASure是一个子系统,表示测量功能的开始。VOLTage是一个子系统,指定测量的电气参数类型。- -DC是一个子系统或修饰符,指定电压的直流类型。?表示这是一个查询命令。
-
命令大小写
SCPI命令是不区分大小写的。然而,惯例是使用大写字母来提高可读性,并在文档中突出显示命令词。例如,
measure:voltage:dc?和MEASURE:VOLTAGE:DC?在功能上是等价的。 -
简写
SCPI标准允许命令词的简写,只要这种简写在给定的上下文中是唯一的。例如,
MEASure:VOLTage:DC?可以缩写为MEAS:VOLT:DC?,前提是没有其他命令词以相同的缩写开始。 -
参数
命令可能需要一个或多个参数。参数紧跟在命令之后,用空格分隔。参数可以是数字、字符串或布尔值。例如:
cSOURce:VOLTage 20这里,
20是一个参数,表示将电压设置为20伏。 -
分隔符
冒号(
:):用于分隔不同层级的命令词。分号(
;):用于在单个指令字符串中分隔多个命令,允许在一行中发送多个命令。例如:c*RST; *IDN?这会先重置仪器,然后查询仪器的身份信息。
-
查询和设置
查询命令:通常以问号(?)结尾,用于从仪器获取信息或状态。例如,VOLT? 查询当前设置的电压值。
设置命令:不以问号结尾,用于配置仪器的设置或发送控制命令。例如,VOLT 10 设置电压为10伏。
-
通用命令
SCPI还定义了一组通用命令,这些命令在大多数SCPI兼容设备上都是相同的。例如:
*IDN?:查询仪器的身份。
*RST:重置仪器到其默认状态。
*CLS:清除所有的状态和错误信息。 -
错误处理
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 主要步骤:
- 将 libscpi 源代码(libscpi/src/.c + libscpi/inc/)加入 STM32 工程,调整 include 路径。
- 在工程中实现一个 scpi 命令定义文件(参考 examples/common/scpi-def.c),至少包含 IEEE 必需命令(*IDN?等)或其他命令。
- 在系统启动时或 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(否则需要加锁)。
- 使用 LWIP raw API 建立 TCP 监听(port 5025 常用),在 accept 回调里为新连接建立 per-connection 结构并设置 recv 回调。
- recv 回调里:
- 从 pbuf 中取出数据:SCPI_Input(&conn->scpi_ctx, (const char*)p->payload, p->len);
- 调用 tcp_recved(pcb, p->len);
- 释放 pbuf。
- 实现 SCPI_Write 回调使其把 data 写回对应的 tcp_pcb:通常用 tcp_write(pcb, data, len, TCP_WRITE_FLAG_COPY) 然后 tcp_output(pcb);返回已写字节数。
- 注意:SCPI 的 handler 里不要做长时间阻塞(否则会阻塞 TCP/IP 回调线程);如果需要做较慢任务,应把任务投递到工作线程/队列中,然后立刻返回。
4.4 移植步骤
4.4.1 添加库文件
- 打开下载的工程,复制里面的
libscpi文件夹,添加到自己的工程

- 将scpi库加入工程,运行后不报错。


这样就把scpi库成功添加了
4.4.2 添加 scpi_server文件
SCPI命令服务器,通过TCP/IP网络接收和处理SCPI命令。
(1)数据流层次
TCP客户端 → TCP接收 → SCPI解析器 → 命令执行 → 响应 → TCP发送
(2)TCP客户端初始化void scpi_server_init(void),监听接收
- 初始化SCPI解析器
- 使用
pcb = tcp_new();创建TCP控制块 tcp_bind(pcb, NULL, SCPI_DEVICE_PORT);绑定端口tcp_listen(pcb)启动监听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命令
能收到服务器端的回复
