程序异常处理

Copyright © Quectel Wireless Solutions Co., Ltd. 2026. All rights reserved.


功能概述

在应用开发中,及时识别并处理运行时错误对程序健壮性至关重要。UniRTOS SDK中的错误分为两类:

  • 可恢复错误:通过函数返回值(错误码枚举)表示,调用方检查后可选择重试、降级或向上传递。

  • 不可恢复错误
    通过 QOSA_ASSERT 触发的断言失败。
    CPU异常:非法内存访问、非法指令。
    系统级检查:看门狗超时、缓存访问错误、堆栈溢出、堆栈粉碎、堆栈损坏等。

本文介绍针对可恢复错误的处理机制与常用模式。

错误码

UniRTOS SDK中的大多数函数以功能组件专属枚举类型返回错误码,成功时统一返回值为 0(各功能组件定义为 QOSA_XXX_SUCCESS = 0QOSA_XXX_OK = 0)。

每个子功能组件的错误枚举均遵循统一的32位编码规则:

[31         16] [15          0]
  组件标识码       具体错误序号

高16位为组件ID,由 qosa_def.h 中的 qosa_base_component_e 枚举定义;低16位为该功能组件内的顺序错误号。每个功能组件通过自身的 ERRCODE_BASE 宏将两者合并,以UART为例:

#define QOSA_UART_ERRCODE_BASE  (QOSA_COMPONENT_API_UART << 16)  // 0x80120000

typedef enum {
    QOSA_UART_SUCCESS           = 0,
    QOSA_UART_EXECUTE_ERR       = 1 | QOSA_UART_ERRCODE_BASE,   // 0x80120001
    QOSA_UART_MEM_ADDR_NULL_ERR,                                // 0x80120002
    QOSA_UART_INVALID_PARAM_ERR,                                // 0x80120003
    QOSA_UART_OPEN_REPEAT_ERR,                                  // 0x80120004
    QOSA_UART_NOT_OPEN_ERR,                                     // 0x80120005
} qosa_uart_error_e;

此外,SDK在 qosa_common_error_e 中定义了多功能组件的通用错误,基类组件码为 QOSA_COMPONENT_COMMON(0x8000),具体定义如下:

/*! common error code */
typedef enum
{
    QOSA_OK = 0,
    QOSA_ERROR_GENERAL = 1 | (QOSA_COMPONENT_COMMON << 16), /*!< generic error,this error is only returned when a special exception occurs */
    QOSA_ERROR_NO_MEMORY,                                   /*!< memory malloc failed */
    QOSA_ERROR_PARAM_INVALID,                               /*!< Parameter input error */
    QOSA_ERROR_PARAM_IS_NULL,                               /*!< Parameter is empty error */
    QOSA_ERROR_TIMEOUT,                                     /*!< Operation timeout error */
    QOSA_ERROR_NO_RESOURCE,                                 /*!< No resources */
    QOSA_ERROR_VALUE_INVALID,                               /*!< Invalid value */
    QOSA_ERROR_OPERATION,                                   /*!< Invalid operation */
    QOSA_ERROR_INVALID_SIZE,                                /*!< Invalid size */
    QOSA_ERROR_REQUEST,                                     /*!< Invalid request */
    QOSA_ERROR_NO_SPACE,                                    /*!< Queue space full*/
    QOSA_ERROR_DELETED,                                     /*!< Delete error */
    QOSA_ERROR_UNSUPPORT,                                   /*!< Operation not supported */
    QOSA_ERROR_DUPLICATE_NAME,                              /*!< Duplicate name */
} qosa_common_error_e; 

各功能组件ID的完整列表见 qosa_def.h 中的 qosa_base_component_e 枚举。常用组件ID速查:

组件宏

对应值

对应功能

QOSA_COMPONENT_API_ADC

0x8011

ADC采样

QOSA_COMPONENT_API_UART

0x8012

串口通信

QOSA_COMPONENT_BSP_GPIO

0x8013

GPIO控制

QOSA_COMPONENT_API_I2C

0x8016

I2C总线

QOSA_COMPONENT_API_SPI

0x8018

SPI总线

QOSA_COMPONENT_API_CAMERA

0x8019

摄像头

QOSA_COMPONENT_API_USB

0x8020

USB

QOSA_COMPONENT_MQTT

0x8110

MQTT客户端

QOSA_COMPONENT_FOTA

0x8112

固件升级

错误码解析

32位错误码本身不直观,可按以下方式手动拆解定位:

uint32_t err = 0x80160003;
uint16_t component = (err >> 16) & 0xFFFF;  // 0x8016 → QOSA_COMPONENT_API_I2C
uint16_t errnum    = err & 0xFFFF;           // 0x0003 → QOSA_I2C_INIT_ERR(第3个错误)

日志输出时建议同时打印十六进制原始值与调用位置,便于离线分析:

qosa_i2c_error_e ret = qosa_i2c_init(I2C_PORT_0, &cfg);
if (ret != QOSA_I2C_SUCCESS) {
    QLOGE("qosa_i2c_init failed: 0x%08X", (unsigned int)ret);
    return ret;
}

备注

部分HAL接口(如 qosa_usb_get_vbus_stateqosa_usb_get_connect_state)返回的不是错误码枚举,而是状态枚举,其中 -1 表示硬件不支持(如 QOSA_USB_VBUS_NOT_SUPPORT),0 表示未连接,1 表示已连接。处理此类接口时须与普通错误码区分对待,不能简单地与 SUCCESS 比较。

QOSA_ASSERT宏

QOSA_ASSERT 的作用与 assert 类似,但会在触发时自动记录函数名、行号,然后中止程序执行:

// qosa_def.h
#define QOSA_ASSERT(a)  qosa_assert(__func__, __LINE__, a)

适用场景:验证程序内部不变量——即在正常情况下绝不应为假的条件。

QOSA_ASSERT(ctx != NULL);           // 内部上下文指针不应为NULL
QOSA_ASSERT(buf_size <= MAX_BUF);   // 缓冲区大小不应超出设计上限

备注

QOSA_ASSERT 触发后程序将中止,属于不可恢复行为,不得用于处理外部输入校验或普通API返回值。示例代码中使用 QOSA_ASSERT 包裹API调用,目的是使示例更简洁,并不代表应用开发的最佳实践。

推荐检查写法

UniRTOS SDK应用代码中推荐使用以下两种写法。

立即返回(无资源需释放)

适用于函数执行路径上没有需要清理的资源时:

#define QOS_LOG_TAG "MY_APP"

qosa_uart_error_e my_uart_setup(void)
{
    qosa_uart_error_e ret;

    ret = qosa_uart_open(UART_PORT_0, &open_cfg);
    if (ret != QOSA_UART_SUCCESS) {
        QLOGE("uart open failed: 0x%08X", (unsigned int)ret);
        return ret;
    }

    ret = qosa_uart_set_baud_rate(UART_PORT_0, 115200);
    if (ret != QOSA_UART_SUCCESS) {
        QLOGE("uart set baud failed: 0x%08X", (unsigned int)ret);
        qosa_uart_close(UART_PORT_0);
        return ret;
    }

    return QOSA_UART_SUCCESS;
}

goto清理(有资源需释放)

适用于函数中途分配了内存、打开了设备或持有了锁等需要统一释放的场景:

qosa_i2c_error_e sensor_init(void)
{
    qosa_i2c_error_e ret = QOSA_I2C_SUCCESS;
    uint8_t *buf = NULL;

    buf = qosa_malloc(BUF_SIZE);
    if (buf == NULL) {
        QLOGE("malloc failed");
        return QOSA_ERROR_NO_MEMORY;
    }

    ret = qosa_i2c_init(I2C_PORT_0, &i2c_cfg);
    if (ret != QOSA_I2C_SUCCESS) {
        QLOGE("i2c init failed: 0x%08X", (unsigned int)ret);
        goto cleanup;
    }

    ret = qosa_i2c_write(I2C_PORT_0, SENSOR_ADDR, buf, BUF_SIZE);
    if (ret != QOSA_I2C_SUCCESS) {
        QLOGE("i2c write failed: 0x%08X", (unsigned int)ret);
        goto cleanup;
    }

cleanup:
    qosa_free(buf);
    return ret;
}

错误处理模式

重试恢复

部分错误属于瞬态故障(总线繁忙、队列满、短暂超时),可在延迟后重试有限次数:

#define MAX_RETRY 3

qosa_i2c_error_e ret;
int retry = 0;
do {
    ret = qosa_i2c_write(I2C_PORT_0, addr, buf, len);
    if (ret == QOSA_I2C_SUCCESS) break;
    qosa_msleep(10);
} while (++retry < MAX_RETRY);

if (ret != QOSA_I2C_SUCCESS) {
    QLOGE("I2C write failed after %d retries: 0x%08X", MAX_RETRY, (unsigned int)ret);
    return ret;
}

向上传递

中间件或驱动层通常不适合自行决策错误的处置方式,应在释放已持有的资源后,将原始错误码返回给上层:

uint8_t *pkt = qosa_malloc(PKT_SIZE);
if (pkt == NULL) {
    return QOSA_ERROR_NO_MEMORY;
}

qosa_uart_error_e ret = qosa_uart_write(UART_PORT_0, pkt, PKT_SIZE, TIMEOUT_MS);
if (ret != QOSA_UART_SUCCESS) {
    qosa_free(pkt);
    return ret;   // 原始错误码透传给调用方
}
qosa_free(pkt);
return QOSA_UART_SUCCESS;

能力探测与降级

NOT_SUPPORT 类错误码(如 QOSA_USB_VBUS_NOT_SUPPORTQOSA_USB_STATE_NOT_SUPPORT)表示当前硬件不具备该能力,属于静态事实而非运行时故障。应在初始化阶段探测能力并决定功能路径,而非每次调用都处理这个返回值:

// 初始化时一次性探测VBUS检测能力
static bool s_vbus_supported = false;

void usb_feature_init(void)
{
    if (qosa_usb_get_vbus_state() != QOSA_USB_VBUS_NOT_SUPPORT) {
        s_vbus_supported = true;
        qosa_usb_bind_vbus_cb(vbus_event_cb);
        QLOGI("VBUS detection enabled");
    } else {
        QLOGW("VBUS pin not available, falling back to connect state polling");
    }
}

转为不可恢复错误

在初始化阶段,某些API的失败意味着后续所有逻辑都无法正常运行,此时可以合理地使用 QOSA_ASSERT 使程序在调试阶段快速暴露问题:

// 适用于开发阶段,初始化时的硬性依赖
void app_init(void)
{
    qosa_uart_error_e ret = qosa_uart_open(LOG_UART_PORT, &log_cfg);
    QOSA_ASSERT(ret == QOSA_UART_SUCCESS);

    // 继续初始化其他功能组件...
}

对中间件或可选功能组件,不建议在失败时直接断言,应返回错误码让应用层自行决策。

日志输出

SDK提供按级别分类的日志宏,自动附带函数名和行号。每个源文件顶部须定义 QOS_LOG_TAG 用于过滤:

#define QOS_LOG_TAG  "MY_MODULE"

QLOGE(fmt, ...)   // ERROR:不可恢复错误
QLOGW(fmt, ...)   // WARN :可恢复异常
QLOGI(fmt, ...)   // INFO :关键流程节点
QLOGD(fmt, ...)   // DEBUG:详细调试信息
QLOGV(fmt, ...)   // VERBOSE:高频/低优先级信息

通过 qosa_log_control_set 按位控制输出通道:

说明

bit0

QOSA_LOG_BIT_MASTER_ENABLE

日志总开关

bit1

QOSA_LOG_BIT_DEBUG

调试串口

bit2

QOSA_LOG_BIT_USB

USB虚拟串口

bit3

QOSA_LOG_BIT_SDCARD

SD卡文件

bit4

QOSA_LOG_BIT_FLASH

内置Flash文件

量产固件建议仅保留 MASTER_ENABLE + DEBUG,关闭USB和Flash输出,降低I/O开销。