绪论
IEEE 488 (GP-IB) 是由惠普研发的 HP-IB 衍生的电测仪器事实通讯和互联规范,USB TMC USB488 类实现了多种IEEE 488所有的特性。我们将在 ESP32 上实现:
- 完整的SCPI支持
- SR1 – 支持服务请求
- DT1 – 支持设备触发
- RL0 – 不支持远程/本地模式操作,设计上仪器采用队列接受控制指令,无需在两个模式间切换 (TinyUSB也不支持)
以 IEEE 488.1 Capabilities表示,就是AH1,C0,DC1,DT1,L2,PP0,RL0,SR1,SH1,T6
库的准备
由于 esp_tinyusb 只支持常用几个设备类的快速部署,我们这里使用原生 tinyusb 开发,具体步骤参见:https://docs.espressif.com/projects/esp-iot-solution/zh_CN/latest/usb/usb_overview/tinyusb_development_guide.html 将 tinyusb 作为 idf 管理下的组件引入即可。
随后,在main目录下创建目录tusb 创建 tinyusb 配置文件 tusb_config.h 和 usb 描述符文件 usb_descriptor.c 。在 tusb_config.h 中如下配置启用 USB488并配置有关基础设施(配置 CFG_TUSB_DEBUG 为 3 可以获得最大化调试日志):
#ifndef CFG_TUSB_OS
#define CFG_TUSB_OS OPT_OS_FREERTOS
#endif
#ifndef ESP_PLATFORM
#define ESP_PLATFORM 1
#endif
#ifndef CFG_TUSB_DEBUG
#define CFG_TUSB_DEBUG 0
#endif
#define CFG_TUSB_OS_INC_PATH freertos/
#define CFG_TUSB_DEBUG_PRINTF esp_rom_printf
// Enable Device stack
#define CFG_TUD_ENABLED       1
#define CFG_TUD_USBTMC                1
#define CFG_TUD_USBTMC_ENABLE_INT_EP  1
#define CFG_TUD_USBTMC_ENABLE_488     1usb_descriptor.c 可以采用 tinyusb 示例目录中的模板,复制即可。在 capabilities 响应中回复如下结构体:
static usbtmc_response_capabilities_488_t const
    tud_usbtmc_app_capabilities = { .USBTMC_status          = USBTMC_STATUS_SUCCESS,
                                    .bcdUSBTMC              = USBTMC_VERSION,
                                    .bmIntfcCapabilities    = { .listenOnly             = 0,
                                                                .talkOnly               = 0,
                                                                .supportsIndicatorPulse = 1 },
                                    .bmDevCapabilities      = { .canEndBulkInOnTermChar = 1 },
                                    .bcdUSB488              = USBTMC_488_VERSION,
                                    .bmIntfcCapabilities488 = { .supportsTrigger     = 1,
                                                                .supportsREN_GTL_LLO = 1,
                                                                .is488_2             = 1 },
                                    .bmDevCapabilities488   = {
                                          .SCPI = 1, // 支持 SCPI
                                          .SR1  = 1, // 支持 SRQ 请求
                                          .RL1  = 0, // 不支持 本地/远程切换
                                          .DT1  = 1, // 支持设备触发
                                    } };scpi 解析器和状态管理使用 scpi-parser 库 https://github.com/j123b567/scpi-parser 。由于该库的构建系统是 Makefile ,所以使用一个自定义的 CMakeLists.txt 使其可以被 CMake 构建系统识别,并可以被 idf 注册为一个组件。内容如下:
cmake_minimum_required(VERSION 3.5)
file(GLOB LIBSCPI_SOURCES
    "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c"
    "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp"
)
idf_component_register(SRCS "${LIBSCPI_SOURCES}"
    INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/inc
)随后注册为一个依赖:
libscpi:
    path: ../thirdparty/scpi-parser/libscpi库的应用
//
// 实现四个 USB 状态函数,若为便携式仪器编程,可以考虑利用该函数节能
//
void tud_mount_cb(void)
{
    ESP_LOGI(TAG, "USB Mounted");
}
void tud_umount_cb(void)
{
    ESP_LOGI(TAG, "USB Unmounted");
}
void tud_suspend_cb(bool remote_wakeup_en)
{
    ESP_LOGI(TAG, "USB Suspend");
}
void tud_resume_cb(void)
{
    ESP_LOGI(TAG, "USB Resume");
}
//
// 实现 Bulk IN/OUT 传输中止和其他辅助函数,由于我们维护了一个介于 SCPI 和 USB 之间的输出缓冲区
// 所以无需额外操作
//
bool tud_usbtmc_initiate_abort_bulk_in_cb(uint8_t* tmcResult)
{
    ESP_LOGD(TAG, "bulk in request is aborting");
    *tmcResult = USBTMC_STATUS_SUCCESS;
    return true;
}
bool tud_usbtmc_check_abort_bulk_in_cb(usbtmc_check_abort_bulk_rsp_t* rsp)
{
    ESP_LOGD(TAG, "bulk in request is aborted");
    tud_usbtmc_start_bus_read();
    return true;
}
bool tud_usbtmc_initiate_abort_bulk_out_cb(uint8_t* tmcResult)
{
    ESP_LOGD(TAG, "bulk out request is aborting");
    *tmcResult = USBTMC_STATUS_SUCCESS;
    return true;
}
bool tud_usbtmc_check_abort_bulk_out_cb(usbtmc_check_abort_bulk_rsp_t* rsp)
{
    ESP_LOGD(TAG, "bulk out request is aborted");
    tud_usbtmc_start_bus_read();
    return true;
}
void tud_usbtmc_bulkIn_clearFeature_cb(void)
{
}
void tud_usbtmc_bulkOut_clearFeature_cb(void)
{
    tud_usbtmc_start_bus_read();
}
//
// 实现设备状态清除,暂时不实现 SCPI 层的清除
//
bool tud_usbtmc_initiate_clear_cb(uint8_t* tmcResult)
{
    ESP_LOGD(TAG, "device status is clearing");
    *tmcResult = USBTMC_STATUS_SUCCESS;
    return true;
}
bool tud_usbtmc_check_clear_cb(usbtmc_get_clear_status_rsp_t* rsp)
{
    ESP_LOGD(TAG, "device status is cleared");
    rsp->USBTMC_status           = USBTMC_STATUS_SUCCESS;
    rsp->bmClear.BulkInFifoBytes = 0u;
    return true;
}
//
// 实现状态字节读取、指示灯闪烁和触发器
//
uint8_t tud_usbtmc_get_stb_cb(uint8_t* tmcResult)
{
    return SCPI_RegGet(&scpi_context, SCPI_REG_STB); // 直接转发到 SCPI 层
}
bool tud_usbtmc_msg_trigger_cb(usbtmc_msg_generic_t* msg)
{
    (void)msg;
    SCPI_Control(&scpi_context, SCPI_CTRL_GET, 0);
    return true;
}
bool tud_usbtmc_indicator_pulse_cb(tusb_control_request_t const* msg, uint8_t* tmcResult)
{
    (void)msg; // To Be Done
    *tmcResult = USBTMC_STATUS_SUCCESS;
    return true;
}
//
// 实现数据接收,由于输入缓冲区由 SCPI Parser 自行管理,仅需直接传入,在接收到换行符后开始解析
//
bool tud_usbtmc_msg_data_cb(void* data, size_t len, bool transfer_complete)
{
    ESP_LOGD(TAG, "received %u bytes, transfer_complete=%d", (unsigned)len, transfer_complete);
    SCPI_Input(&scpi_context, data, len);
    tud_usbtmc_start_bus_read();
    return true;
}
//
// 实现数据传送请求,主机请求设备传回数据
//
bool tud_usbtmc_msgBulkIn_request_cb(usbtmc_msg_request_dev_dep_in const* request)
{
    uint8_t stb = SCPI_RegGet(&scpi_context, SCPI_REG_STB);
    if (stb & STB_MAV) // 检查输出缓冲区(手动管理) 是否有合法数据
    {
        return tud_usbtmc_transmit_dev_msg_data(
            message_out_buffer, message_out_buffer_ptr - message_out_buffer, true, false);
    }
    else
    {
        return false; // TMC 规范要求回复 NAK
    }
}
//
// 实现 SCPI 层回调
//
size_t SCPI_Write(scpi_t* context, const char* data, size_t len)
{
    (void)context;
    if (SCPI_RegGet(&scpi_context, SCPI_REG_STB) & STB_MAV)
    {
        return 0;
    }
    if ((len + (size_t)(message_out_buffer_ptr - message_out_buffer)) > MESSAGE_OUT_BUFFER_SIZE)
    {
        ESP_LOGE(TAG, "SCPI_Write is overflowing the buffer");
        return 0;
    }
    memcpy(message_out_buffer_ptr, data, len);
    message_out_buffer_ptr += len;
    return len;
}
scpi_result_t SCPI_Flush(scpi_t* context)
{
    (void)context;
    SCPI_RegSetBits(&scpi_context, SCPI_REG_STB, STB_MAV); // 消息复制完毕,设置可用位
    return SCPI_RES_OK;
}
int SCPI_Error(scpi_t* context, int_fast16_t err)
{
    (void)context;
    ESP_LOGE(TAG, "**ERROR: %d, \"%s\"", (int16_t)err, SCPI_ErrorTranslate(err));
    return 0;
}
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)
    {
        ESP_LOGI(TAG, "**SRQ: 0x%X (%d)", val, val);
    }
    else if (ctrl == SCPI_CTRL_LLO || ctrl == SCPI_CTRL_SDC)
    {
        ESP_LOGI(TAG, "device cleared");
    }
    else
    {
        ESP_LOGI(TAG, "**CTRL %02x: 0x%X (%d)", ctrl, val, val);
    }
    return SCPI_RES_OK;
}
scpi_result_t SCPI_Reset(scpi_t* context)
{
    (void)context;
    ESP_LOGI(TAG, "**Reset");
    return SCPI_RES_OK;
}随后,按照 scpi-parser common_c 范例实现你的 SCPI 子例程,实现整个 SCPI 指令集。额外的,您可以使用 SCPI_ErrorPushEx 在出现设备错误的时候往 SCPI 子系统发送这个错误。整个初始化实现如下:(需要包含 #include <esp_private/usb_phy.h> 这一私有头文件)
    usb_phy_handle_t phy_handle;
    usb_phy_config_t phy_conf = {
        .controller = USB_PHY_CTRL_OTG,
        .otg_mode   = USB_OTG_MODE_DEVICE,
        .target     = USB_PHY_TARGET_INT,
    };
    ESP_ERROR_CHECK(usb_new_phy(&phy_conf, &phy_handle));
    tusb_rhport_init_t dev_init = { .role = TUSB_ROLE_DEVICE, .speed = TUSB_SPEED_AUTO };
    tusb_init(0, &dev_init);
    SCPI_Init(
        &scpi_context,
        scpi_commands,
        &scpi_interface,
        scpi_units_def,
        SYSTEM_MANUFACTURE,
        SYSTEM_NAME,
        SYSTEM_SERIAL,
        SYSTEM_VERSION,
        message_in_buffer,
        MESSAGE_IN_BUFFER_SIZE,
        scpi_error_queue_data,
        ERROR_QUEUE_SIZE);
    scpi_context.user_context = pvParameter; // actually the queue handler
    while (1)
    {
        // 在这里轮询通讯队列,检查错误中断
        tud_task(); // 注意:该函数是阻塞的!
    }至此,USB488 已然实现完毕,打开设备管理器就可以看到一个 IVI 设备(需要先安装 VISA 驱动,如 Keysight IO Suite 和 NI-VISA)。打开 NI-MAX 并查询 *IDN?,结果如下:


附 USB TMC 规范供参考:
Alan. 2025/09/25
 
			