yopen GPIO demo
从任务注册到中断上报的完整链路
摘要
demo/demo_gpio.c(527 行)实现了一个完整的 GPIO 控制与中断上报示例,覆盖嵌入式开发的三个基础环节:串口命令接收 、GPIO 配置与控制 、外部中断主动上报。
本文按源码执行顺序解析其实现,依次说明任务注册、初始化流程、消息分发机制、中断注册参数、AT 协议设计、命令解析与中断上报,并在末尾给出该示例的已知缺陷与工程化改进建议。
目标读者:已具备 C 语言与 RTOS 基础的嵌入式开发者。
1. 功能与接口概览
| 项目 | 内容 |
|---|---|
| 源文件 | demo/demo_gpio.c |
| 任务注册 | demo/demo_entry.c:63 |
| 命令入口 | USB 虚拟串口(YOPEN_USB_PORT_COM) |
| 核心机制 | 消息队列 + 双回调(串口回调 / 中断回调) |
| 外部依赖 | yopen_os.h、yopen_gpio.h、yopen_uart.h、yopen_debug.h |
功能:通过串口 AT 指令配置任意 GPIO 为输出或输入中断模式;输入模式下引脚电平变化时,设备主动上报当前电平。
支持的指令:
text
AT+GPIO=<port>,<mode>[,<level>]
| 参数 | 取值 |
|---|---|
port |
0 ~ 38(对应 GPIO_0 ~ GPIO_38) |
mode |
0 输出 / 1 输入·边沿中断 / 2 输入·电平中断 / 3 读取电平 |
level |
输出:1 高、0 低;输入:1 上拉、0 下拉、缺省高阻 |
响应格式:
| 输出 | 含义 |
|---|---|
\r\nOK\r\n |
配置成功 |
\r\nERROR\r\n |
配置失败 |
\r\n+GPIO LVL <n>\r\n |
电平查询结果 |
\r\n+GPIO INT <n>\r\n |
中断上报(URC,非请求响应) |
2. 任务注册
任务通过 application_init 宏在编译期注册,由框架在启动阶段创建:
c
application_init(yopen_gpio_demo_task, "GPIO", 12, 4, NULL);
| 参数 | 含义 |
|---|---|
app_entry |
任务入口函数 |
app_name |
任务名 |
stack_size_kib |
栈大小(KB),宏内换算为字节 |
startup_prio |
启动优先级 |
arvg |
传入任务的参数 |
宏展开后生成一个 appRegItem_t 结构体并放入注册段:
c
#define application_init(app_entry, app_name, stack_size_kib, startup_prio, arvg) \
appRegItem_t _regAppItem_##app_entry = \
{app_entry, app_name, stack_size_kib*1024, startup_prio, arvg}
注意 :demo_entry.c:63 处该语句默认被注释,需取消注释后重新编译。
栈空间评估 :本任务局部变量包含 char data[64]、char command[64],调用链涉及 sprintf、parse_at_command、demo_gpio_init 等。12 KB 为安全余量。若任务内引入浮点格式化或大尺寸局部数组,须同步上调。
3. 任务结构与初始化
3.1 整体骨架
c
void yopen_gpio_demo_task(void * arg)
{
int ret;
gpio_msg_s msg;
char data[64];
yopen_rtos_task_sleep_ms(5000); // 阶段一:等待系统稳定
yopen_gpio_set_voltage(gpio_vol); // 阶段二:配置电压域
ret = yopen_rtos_queue_create(&s_queue, sizeof(gpio_msg_s), 50);
msg = GPIO_MSG_GPIO_CTRL;
yopen_rtos_queue_release(s_queue, sizeof(msg), (uint8*)&msg, YOPEN_NO_WAIT);
demo_uart_init(); // 阶段三:初始化串口
while(1) { /* 消息分发 */ } // 阶段四:常驻循环
}
结构与常规 RTOS 任务一致:顺序初始化 + 常驻事件循环。
3.2 延时 5000 ms
等待底层(射频、电源、时钟)初始化完成。若应用不依赖底层状态,可缩短或移除;保留该延时有助于规避上电阶段的偶发配置失败。
3.3 配置 IO 电压域
c
static yopen_Lvl gpio_vol = Vol_1_80V; // demo_gpio.c:36
...
yopen_gpio_set_voltage(gpio_vol);
IO 电压域决定引脚的逻辑电平阈值,必须在操作 GPIO 前设置。电压域选型须与硬件设计一致:等级不匹配会导致电平识别失败,过高则有损坏外设的风险。
3.4 创建消息队列
c
ret = yopen_rtos_queue_create(&s_queue, sizeof(gpio_msg_s), 50);
| 参数 | 值 | 说明 |
|---|---|---|
&s_queue |
全局句柄 | 队列句柄存储位置 |
sizeof(gpio_msg_s) |
4 字节 | 单条消息长度 |
50 |
50 | 队列深度 |
消息类型定义(demo_gpio.c:10):
c
typedef enum
{
GPIO_MSG_GPIO_CTRL, // 执行 GPIO 配置
GPIO_MSG_GPIO_INIT, // 处理中断上报
GPIO_MSG_UART_RECV, // 处理串口数据
} gpio_msg_s;
设计特征:消息体仅携带事件类型,不含参数。业务参数通过文件级静态变量传递:
c
static int s_gpio_port = -1; // 目标 GPIO
static int s_gpio_mode = GPIO_OUTPUT_LVL_HIGH; // 工作模式
static int s_gpio_int_trig_mode = EDGE_BOTH; // 中断触发方式
该设计节省内存与拷贝开销,但引入了全局状态耦合。工程实践中建议将参数封装进消息结构体,随队列一并投递。
3.5 上电自检
c
msg = GPIO_MSG_GPIO_CTRL;
yopen_rtos_queue_release(s_queue, sizeof(msg), (uint8*)&msg, YOPEN_NO_WAIT);
任务在进入循环前向自身投递一条配置消息,使"配置 GPIO"这一动作统一收敛到 GPIO_MSG_GPIO_CTRL 分支处理。无论配置请求来自上电自检还是串口指令,均走同一路径。
yopen_rtos_queue_release() 参数:
| 参数 | 说明 |
|---|---|
| 队列句柄 | 目标队列 |
sizeof(msg) |
消息长度 |
(uint8*)&msg |
数据地址 |
YOPEN_NO_WAIT |
非阻塞;队列满时立即返回失败 |
YOPEN_NO_WAIT 保证投递不阻塞,队列满时立即返回失败。该标志在通知/中断上下文中为必需项,理由见第 6.1、7.1 节。
3.6 串口初始化
c
void demo_uart_init(void)
{
yopen_uart_config_s uart_config = {0};
uart_config.baudrate = YOPEN_UART_BAUD_115200;
uart_config.data_bit = YOPEN_UART_DATABIT_8;
uart_config.stop_bit = YOPEN_UART_STOP_1;
uart_config.parity_bit = YOPEN_UART_PARITY_NONE;
uart_config.flow_ctrl = YOPEN_FC_NONE;
uart_config.isPrintfPort = TRUE;
switch(s_uart_port)
{
case YOPEN_UART_PORT_0:
yopen_pin_set_func(YOPEN_PIN_GPIO6, 1); // TX
yopen_pin_set_func(YOPEN_PIN_GPIO7, 1); // RX
break;
/* UART1 / UART2 类似 */
}
yopen_uart_close(s_uart_port);
yopen_uart_set_dcbconfig(s_uart_port, &uart_config);
yopen_uart_open(s_uart_port);
yopen_uart_register_cb(s_uart_port, _uart_notify_cb);
}
初始化序列:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 填充 yopen_uart_config_s |
115200-8-N-1,无流控 |
| 2 | yopen_pin_set_func() |
配置引脚复用为 UART 功能 |
| 3 | yopen_uart_close() |
关闭,避免重复打开报错 |
| 4 | yopen_uart_set_dcbconfig() |
下发配置参数 |
| 5 | yopen_uart_open() |
打开串口 |
| 6 | yopen_uart_register_cb() |
注册数据到达回调 |
步骤 2 不可省略。 引脚复位后默认功能并非 UART,未配置复用即使物理连线正确也无法通信。
串口选择 (demo_gpio.c:44):
c
static int s_uart_port = YOPEN_USB_PORT_COM;
默认使用 USB 虚拟串口,通过 USB 连接即可收发指令,无需额外接线。
缓冲区规格 :yopen_uart_write() 与 yopen_uart_read() 均为非阻塞接口;TX 缓冲区 4096 字节,RX 缓冲区 512 字节。发送未完成时调用写接口返回 YOPEN_UART_BUSY。
4. 主循环与消息分发
c
while(1)
{
static int gpio = -1;
ret = yopen_rtos_queue_wait(s_queue, (uint8*)&msg, sizeof(msg), YOPEN_WAIT_FOREVER);
DEMO_GPIO_TRACE("yopen_rtos_queue_wait msg %d", msg);
switch(msg)
{
case GPIO_MSG_GPIO_CTRL: { ... } break;
case GPIO_MSG_GPIO_INIT: { ... } break;
case GPIO_MSG_UART_RECV: { ... } break;
}
}
yopen_rtos_queue_wait() 以 YOPEN_WAIT_FOREVER 阻塞等待,任务在无事件时不消耗 CPU,避免轮询开销。
static int gpio 的作用 :static 保证变量仅初始化一次并跨循环迭代保留值,用于记录上一次配置的 GPIO 端口号(见 5.1 节)。
类型一致性 :消息以枚举类型直接投递,依赖 enum 与 int 等宽(通常 4 字节)。若改用 uint8_t 等类型,sizeof 变化将导致收发长度不匹配。
5. GPIO 配置与中断注册
5.1 配置入口
c
case GPIO_MSG_GPIO_CTRL:
{
gpio = s_gpio_port;
if (gpio != -1) // 非首次配置,先释放旧配置
{
yopen_gpio_deinit(gpio);
}
demo_pin_init();
ret = demo_gpio_init();
if(gpio != -1) // 非首次配置,返回响应
{
if(ret) sprintf(data, "%s", "\r\nOK\r\n");
else sprintf(data, "%s", "\r\nERROR\r\n");
demo_uart_write(data);
}
yopen_rtos_task_sleep_ms(100);
}
break;
两处 if (gpio != -1) 承担同一职责:
- 上电自检时
s_gpio_port为初始值-1,无需deinit旧配置; - 同时不返回响应,避免设备上电后主动输出
OK干扰上位机。
gpio 变量在配置前赋值,与 s_gpio_port 分离,用于区分"本次是否由指令触发"。
5.2 引脚复用配置
demo_pin_init()(demo_gpio.c:190)执行两项操作。
AON 域上电
c
#ifdef TYPE_EC718M
if (s_gpio_port >= GPIO_20 && s_gpio_port <= GPIO_28)
yopen_aon_power_on();
#else
if (s_gpio_port >= GPIO_10 && s_gpio_port <= GPIO_16)
yopen_aon_power_on();
#endif
AON(Always-On)域在主电源关闭后仍保持供电。不同芯片型号的 AON 引脚范围不同,更换平台时须同步修改。
引脚功能复用
c
switch(s_gpio_port)
{
case GPIO_0: yopen_pin_set_func(YOPEN_PIN_GPIO0, 0); return;
case GPIO_1: yopen_pin_set_func(YOPEN_PIN_GPIO1, 0); return;
/* ... 至 GPIO_38 ... */
}
yopen_pin_set_func(pin, func_sel) 的 func_sel 为复用功能号,0 表示 GPIO 功能。
注意 #else 分支中 GPIO_17 ~ GPIO_20 使用功能号 4 而非 0------同一物理引脚在不同芯片上的复用表存在差异,须以芯片手册为准。
5.3 GPIO 参数生成
demo_gpio_init()(demo_gpio.c:263)将 s_gpio_mode 映射为四个初始化参数:
c
yopen_GpioDir gpio_dir = GPIO_OUTPUT;
yopen_PullMode gpio_pull = FORCE_PULL_NONE;
yopen_LvlMode gpio_lvl = LVL_LOW;
yopen_TriggerMode gpio_trigger = EDGE_TRIGGER;
bool int_flag = false;
映射关系:
s_gpio_mode |
方向 | 上下拉 | 触发方式 | 注册中断 |
|---|---|---|---|---|
GPIO_OUTPUT_LVL_HIGH (0) |
输出 | 无 | --- | 否 |
GPIO_OUTPUT_LVL_LOW (1) |
输出 | 无 | --- | 否 |
GPIO_INPUT_EDGE_INT_PULL_NONE (2) |
输入 | 无 | 边沿 | 是 |
GPIO_INPUT_EDGE_INT_PULL_UP (3) |
输入 | 上拉 | 边沿 | 是 |
GPIO_INPUT_EDGE_INT_PULL_DOWN (4) |
输入 | 下拉 | 边沿 | 是 |
GPIO_INPUT_LEVEL_INT_PULL_NONE (5) |
输入 | 无 | 电平 | 是 |
GPIO_INPUT_LEVEL_INT_PULL_UP (6) |
输入 | 上拉 | 电平 | 是 |
GPIO_INPUT_LEVEL_INT_PULL_DOWN (7) |
输入 | 下拉 | 电平 | 是 |
枚举值 0/1 对应输出,2/3/4 对应输入+边沿中断,5/6/7 对应输入+电平中断------仅三组行为,差异在上下拉配置。
5.4 初始化与中断注册
c
ret |= yopen_gpio_init(s_gpio_port, gpio_dir, gpio_pull, gpio_lvl);
if (int_flag)
{
ret |= yopen_int_register(s_gpio_port, gpio_trigger, DEBOUNCE_DIS,
s_gpio_int_trig_mode, gpio_pull, demo_gpio_cb, (void *)s_gpio_port);
ret |= yopen_int_enable(s_gpio_port);
}
yopen_int_register() 参数说明:
| # | 参数 | 当前值 | 说明 |
|---|---|---|---|
| 1 | 引脚号 | s_gpio_port |
监听目标 |
| 2 | 触发类型 | gpio_trigger |
EDGE_TRIGGER / LEVEL_TRIGGER |
| 3 | 防抖 | DEBOUNCE_DIS |
关闭防抖;机械触点应用建议开启 |
| 4 | 边沿方向 | EDGE_BOTH |
双沿触发;可选单沿 |
| 5 | 上下拉 | gpio_pull |
避免悬空引脚误触发 |
| 6 | 回调函数 | demo_gpio_cb |
中断服务入口 |
| 7 | 回调参数 | (void *)s_gpio_port |
见下 |
回调参数传递 :注册时将整型 s_gpio_port 强转为 void *,回调内再转回整型:
c
yopen_int_register(..., (void *)s_gpio_port); // 注册:int → void*
int gpio = (int)param; // 回调:void* → int
该写法省去额外的内存分配,但依赖指针与 int 等宽,可移植性受限。规范做法是传递结构体指针。
返回值采用 |= 累积,任一环节失败均记录到 ret,由调用方统一判断。
6. 命令解析链路
6.1 串口回调:仅投递事件
c
static void _uart_notify_cb(uint32 ind_type, yopen_uart_port_number_e port, uint32 size)
{
gpio_msg_s msg;
switch(ind_type)
{
case YOPEN_UART_RX_RECV_DATA_IND:
{
msg = GPIO_MSG_UART_RECV;
yopen_rtos_queue_release(s_queue, sizeof(msg), (uint8*)&msg, YOPEN_NO_WAIT);
break;
}
default:
break;
}
}
回调中不读取串口数据,原因有二:
- 回调运行于系统通知上下文,栈空间受限且不可阻塞,此处执行解析、格式化输出存在风险;
yopen_uart_read()为非阻塞接口,数据保存在 RX 缓冲区,由任务后续读取不会丢失。
使用 YOPEN_NO_WAIT 的作用:队列满时丢弃本次事件而非阻塞回调。在通知上下文阻塞会导致事件积压甚至系统异常。
6.2 数据读取与解析
c
case GPIO_MSG_UART_RECV:
{
char command[64];
int param1, param2, param3 = 0Xff;
memset(data, 0, sizeof(data));
int read_len = yopen_uart_read(s_uart_port, (unsigned char *)data, sizeof(data));
if (read_len > 2)
{
for (int i = 0; i < 2; i++) // 去除行尾 \r
{
if (data[read_len-i] == 0xd)
data[read_len-i] = 0;
}
parse_at_command(data, command, ¶m1, ¶m2, ¶m3);
s_gpio_port = param1;
/* 依据 param2 / param3 设置 s_gpio_mode */
msg = GPIO_MSG_GPIO_CTRL;
yopen_rtos_queue_release(s_queue, sizeof(msg), (uint8*)&msg, YOPEN_NO_WAIT);
}
break;
}
处理流程:读取数据 → 去除行尾 → 解析 → 写入全局状态 → 投递配置消息。
解析与执行分离:解析阶段只更新 s_gpio_port / s_gpio_mode,实际配置统一由 GPIO_MSG_GPIO_CTRL 分支执行。该分层使不同来源的配置请求(串口、按键、网络)复用同一执行路径。
6.3 模式映射
param2 / param3 到 s_gpio_mode 的映射:
param2 |
param3 |
s_gpio_mode |
|---|---|---|
0 输出 |
非 0 | GPIO_OUTPUT_LVL_HIGH |
0 |
GPIO_OUTPUT_LVL_LOW |
|
1 边沿中断 |
1 |
..._EDGE_..._PULL_UP |
0xff 缺省 |
..._EDGE_..._PULL_NONE |
|
0 |
..._EDGE_..._PULL_DOWN |
|
2 电平中断 |
1 |
..._LEVEL_..._PULL_UP |
0xff 缺省 |
..._LEVEL_..._PULL_NONE |
|
0 |
..._LEVEL_..._PULL_DOWN |
|
3 读电平 |
--- | 不进入配置流程,直接返回结果 |
0xff 为缺省参数哨兵值,用于区分"参数为 0"与"参数未提供"。
6.4 指令解析实现
parse_at_command()(demo_gpio.c:60)将 "AT+GPIO=2,0,1" 拆分为命令名与三个参数。
命令名提取
c
const char* equal_sign = strchr(input, '=');
if (equal_sign == NULL) { return -1; }
int command_length = equal_sign - input; // 指针相减得到长度
strncpy(command, input, command_length);
command[command_length] = '\0'; // 手动补结束符
指针相减用于计算区间长度。strncpy() 不保证写入结束符,必须手动补充,否则后续字符串操作越界读取。
第一参数提取
c
const char* params_start = equal_sign + 1;
const char* comma = strchr(params_start, ',');
if (comma == NULL) { return -1; }
int param1_length = comma - params_start;
char param1_str[param1_length + 1];
strncpy(param1_str, params_start, param1_length);
param1_str[param1_length] = '\0';
*param1 = atoi(param1_str);
第二参数提取
c
const char* param2_start = comma + 1;
*param2 = atoi(param2_start);
直接对 "0,1" 调用 atoi(),依赖其"遇非数字字符停止解析"的行为,得到 0。该写法正确但隐式,可读性较差。
第三参数(可选)
c
comma = strchr(param2_start, ',');
if (comma)
*param3 = atoi(comma + 1);
else
*param3 = 0xff; // 缺省哨兵
7. 中断上报链路
7.1 中断回调
c
static void demo_gpio_cb(void *param)
{
int gpio = (int)param;
gpio_msg_s msg;
yopen_int_disable(gpio); // 立即关闭本引脚中断
msg = GPIO_MSG_GPIO_INIT;
yopen_rtos_queue_release(s_queue, sizeof(msg), (uint8*)&msg, YOPEN_NO_WAIT);
}
进入回调后立即调用 yopen_int_disable(),原因:
- 中断上下文不可执行耗时操作,须尽快返回;
- 电平触发模式下,只要电平保持有效,中断将持续触发。未关闭将导致队列被快速填满(队列深度仅 50)。
7.2 上报处理
c
case GPIO_MSG_GPIO_INIT:
{
yopen_LvlMode gpio_lvl;
yopen_rtos_task_sleep_ms(50); // 等待电平稳定
yopen_gpio_get_level(s_gpio_port, &gpio_lvl);
yopen_int_enable(s_gpio_port); // 重新使能中断
memset(data, 0, sizeof(data));
sprintf(data, "\r\n+GPIO INT %d\r\n", gpio_lvl);
demo_uart_write(data);
}
break;
| 步骤 | 操作 | 目的 |
|---|---|---|
| 1 | 延时 50 ms | 等待触点抖动或线路振铃衰减 |
| 2 | 读取电平 | 获取稳定状态 |
| 3 | 重新使能中断 | 恢复监听 |
| 4 | 串口输出 | 上报(\r\n 包裹,符合 AT 规范) |
"关闭中断 → 处理数据 → 重新使能"是中断处理的通用范式,适用于 GPIO、UART、DMA 等各类中断源。
8. 完整时序
8.1 指令下发
主任务 s_queue _uart_notify_cb 上位机 主任务 s_queue _uart_notify_cb 上位机 #mermaid-svg-mGgJT5eNaRoMRb2u{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mGgJT5eNaRoMRb2u .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mGgJT5eNaRoMRb2u .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mGgJT5eNaRoMRb2u .error-icon{fill:#552222;}#mermaid-svg-mGgJT5eNaRoMRb2u .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mGgJT5eNaRoMRb2u .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mGgJT5eNaRoMRb2u .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mGgJT5eNaRoMRb2u .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mGgJT5eNaRoMRb2u .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mGgJT5eNaRoMRb2u .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mGgJT5eNaRoMRb2u .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mGgJT5eNaRoMRb2u .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mGgJT5eNaRoMRb2u .marker.cross{stroke:#333333;}#mermaid-svg-mGgJT5eNaRoMRb2u svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mGgJT5eNaRoMRb2u p{margin:0;}#mermaid-svg-mGgJT5eNaRoMRb2u .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mGgJT5eNaRoMRb2u text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-mGgJT5eNaRoMRb2u .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-mGgJT5eNaRoMRb2u .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-mGgJT5eNaRoMRb2u .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-mGgJT5eNaRoMRb2u .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-mGgJT5eNaRoMRb2u #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-mGgJT5eNaRoMRb2u .sequenceNumber{fill:white;}#mermaid-svg-mGgJT5eNaRoMRb2u #sequencenumber{fill:#333;}#mermaid-svg-mGgJT5eNaRoMRb2u #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-mGgJT5eNaRoMRb2u .messageText{fill:#333;stroke:none;}#mermaid-svg-mGgJT5eNaRoMRb2u .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mGgJT5eNaRoMRb2u .labelText,#mermaid-svg-mGgJT5eNaRoMRb2u .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-mGgJT5eNaRoMRb2u .loopText,#mermaid-svg-mGgJT5eNaRoMRb2u .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-mGgJT5eNaRoMRb2u .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-mGgJT5eNaRoMRb2u .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-mGgJT5eNaRoMRb2u .noteText,#mermaid-svg-mGgJT5eNaRoMRb2u .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-mGgJT5eNaRoMRb2u .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mGgJT5eNaRoMRb2u .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mGgJT5eNaRoMRb2u .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mGgJT5eNaRoMRb2u .actorPopupMenu{position:absolute;}#mermaid-svg-mGgJT5eNaRoMRb2u .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-mGgJT5eNaRoMRb2u .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mGgJT5eNaRoMRb2u .actor-man circle,#mermaid-svg-mGgJT5eNaRoMRb2u line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-mGgJT5eNaRoMRb2u :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不读取数据,仅投递事件 "AT+GPIO=2,0,1\r"GPIO_MSG_UART_RECVqueue_wait 返回uart_read → 去行尾 → parse_at_commands_gpio_port=2, s_gpio_mode=OUTPUT_HIGHGPIO_MSG_GPIO_CTRLqueue_wait 返回deinit(2) → demo_pin_init → demo_gpio_init"\r\nOK\r\n"
8.2 中断上报
主任务 s_queue demo_gpio_cb GPIO 引脚 上位机 主任务 s_queue demo_gpio_cb GPIO 引脚 上位机 #mermaid-svg-0MgX3cvg2fQBPaNA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0MgX3cvg2fQBPaNA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0MgX3cvg2fQBPaNA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0MgX3cvg2fQBPaNA .error-icon{fill:#552222;}#mermaid-svg-0MgX3cvg2fQBPaNA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0MgX3cvg2fQBPaNA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0MgX3cvg2fQBPaNA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0MgX3cvg2fQBPaNA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0MgX3cvg2fQBPaNA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0MgX3cvg2fQBPaNA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0MgX3cvg2fQBPaNA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0MgX3cvg2fQBPaNA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0MgX3cvg2fQBPaNA .marker.cross{stroke:#333333;}#mermaid-svg-0MgX3cvg2fQBPaNA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0MgX3cvg2fQBPaNA p{margin:0;}#mermaid-svg-0MgX3cvg2fQBPaNA .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0MgX3cvg2fQBPaNA text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-0MgX3cvg2fQBPaNA .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0MgX3cvg2fQBPaNA .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-0MgX3cvg2fQBPaNA .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-0MgX3cvg2fQBPaNA .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-0MgX3cvg2fQBPaNA #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-0MgX3cvg2fQBPaNA .sequenceNumber{fill:white;}#mermaid-svg-0MgX3cvg2fQBPaNA #sequencenumber{fill:#333;}#mermaid-svg-0MgX3cvg2fQBPaNA #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-0MgX3cvg2fQBPaNA .messageText{fill:#333;stroke:none;}#mermaid-svg-0MgX3cvg2fQBPaNA .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0MgX3cvg2fQBPaNA .labelText,#mermaid-svg-0MgX3cvg2fQBPaNA .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-0MgX3cvg2fQBPaNA .loopText,#mermaid-svg-0MgX3cvg2fQBPaNA .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-0MgX3cvg2fQBPaNA .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0MgX3cvg2fQBPaNA .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-0MgX3cvg2fQBPaNA .noteText,#mermaid-svg-0MgX3cvg2fQBPaNA .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-0MgX3cvg2fQBPaNA .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0MgX3cvg2fQBPaNA .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0MgX3cvg2fQBPaNA .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0MgX3cvg2fQBPaNA .actorPopupMenu{position:absolute;}#mermaid-svg-0MgX3cvg2fQBPaNA .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-0MgX3cvg2fQBPaNA .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0MgX3cvg2fQBPaNA .actor-man circle,#mermaid-svg-0MgX3cvg2fQBPaNA line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-0MgX3cvg2fQBPaNA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 电平变化 yopen_int_disable(5) "AT+GPIO=5,1,1\r""\r\nOK\r\n"触发中断GPIO_MSG_GPIO_INITqueue_wait 返回sleep(50) → get_level → int_enable(5)"\r\n+GPIO INT 1\r\n"
9. 已知缺陷与改进建议
该示例定位为教学代码,投入生产环境前需处理以下问题。
9.1 缺陷清单
| # | 位置 | 问题 | 风险 |
|---|---|---|---|
| 1 | demo_gpio.c:99 |
变长数组 char param1_str[param1_length + 1],长度由串口输入决定 |
超长指令可导致栈溢出 |
| 2 | demo_gpio.c:459 |
data[read_len-i] 下标越界:i=0 时访问 data[read_len],有效下标上界为 read_len-1;应为 data[read_len-1-i] |
读取缓冲区外一字节;读满 64 字节时确为越界 |
| 3 | GPIO_MSG_UART_RECV |
假设单次 read 对应一条完整指令 |
串口为字节流,指令可能被拆分,导致解析失败 |
| 4 | GPIO_MSG_UART_RECV |
单次仅读取 64 字节,RX 缓冲为 512 字节 | 剩余数据滞留,形成残留片段 |
| 5 | demo_gpio.c:463 |
parse_at_command 返回值未检查 |
解析失败时 param1 = -1 赋给 s_gpio_port;且因 gpio == -1,不返回任何响应,上位机持续等待 |
| 6 | demo_gpio.c:60 |
command 解析后未校验 |
发送 AT+FOO=2,0,1 同样生效 |
| 7 | demo_gpio.c:465 |
解析成功即更新 s_gpio_port |
后续初始化失败时全局状态已污染,无法回退 |
| 8 | GPIO_MSG_GPIO_INIT |
上报内容不含引脚号 | 多路中断无法区分来源 |
9.2 改进方向
栈安全:以定长缓冲区限制输入长度,替代变长数组:
c
#define AT_LINE_MAX 128
static char s_line[AT_LINE_MAX];
/* 超出长度直接丢弃并返回 ERROR,不做部分截断 */
if (len >= sizeof(s_line)) { return -1; }
指令完整性 :维护行缓冲,以 \r 作为指令结束标志,而非假定单次读取即完整指令:
c
static uint8_t s_rxbuf[AT_LINE_MAX];
static int s_rxlen = 0;
static void at_feed_byte(uint8_t c)
{
if (s_rxlen >= (int)sizeof(s_rxbuf)) { s_rxlen = 0; return; } // 溢出保护
s_rxbuf[s_rxlen++] = c;
if (c == '\r' || c == '\n')
{
if (s_rxlen > 1)
{
s_rxbuf[s_rxlen - 1] = '\0';
at_dispatch((char*)s_rxbuf); // 完整指令,分发
}
s_rxlen = 0;
}
}
参数校验 :以 strtol 替代 atoi,可区分"解析失败"与"值为 0":
c
char *end = NULL;
long v = strtol(s, &end, 10);
if (end == s || *end != '\0' || v < 0 || v > 38)
{
/* 参数非法,返回 ERROR */
}
end == s 表示未解析出数字,*end != '\0' 表示存在尾部非法字符。
命令分发:以命令表替代条件分支,新增指令无需修改解析逻辑:
c
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
typedef struct {
const char *name;
int (*handler)(const char *params, char *resp, int resp_len);
} at_cmd_t;
static char s_resp[128];
static const at_cmd_t g_at_cmds[] = {
{ "AT+GPIO", at_gpio_handler },
{ "AT+VER", at_ver_handler },
};
static void at_dispatch(const char *line)
{
for (int i = 0; i < (int)ARRAY_SIZE(g_at_cmds); i++)
{
size_t n = strlen(g_at_cmds[i].name);
if (strncmp(line, g_at_cmds[i].name, n) == 0)
{
g_at_cmds[i].handler(line + n, s_resp, sizeof(s_resp));
demo_uart_write(s_resp);
return;
}
}
demo_uart_write("\r\nERROR\r\n"); /* 未匹配命令明确报错 */
}
9.3 概念澄清
本示例中的 AT 指令为应用层自定义文本协议,非芯片或框架提供的能力。
api/ 目录未提供 AT 命令注册接口;其中的 yopen_ril_send_atcmd() 用于向 4G 模组发送 AT 指令,方向相反。因此指令格式可自由定义,但解析逻辑须自行实现。
10. 设计要点总结
- 任务结构 :顺序初始化 +
while(1)阻塞于queue_wait; - 并发模型:串口与中断回调仅投递事件,业务处理集中主任务,避免在回调上下文执行耗时操作;
- 配置流程 :先
deinit释放旧配置,再配置引脚复用,最后初始化并注册中断; - 命令流程:数据读取 → 解析 → 更新状态 → 投递配置事件,解析与执行分离;
- 中断流程:关闭中断 → 投递事件 → 重新使能 → 读取电平 → 串口上报。
该模式具有普适性 :ADC、PWM、I2C、UART 透传等外设的实现均遵循相同结构------参数配置 → 外设使能 → 注册回调 → 回调投递 → 任务处理。
附录 A:指令速查
text
AT+GPIO=<port>,<mode>[,<level>]
port : 0 ~ 38
mode : 0 = 输出
1 = 输入,边沿中断
2 = 输入,电平中断
3 = 读取当前电平
level: 输出 1 = 高电平, 0 = 低电平
输入 1 = 上拉, 0 = 下拉, 缺省 = 高阻
响应:
\r\nOK\r\n 配置成功
\r\nERROR\r\n 配置失败
\r\n+GPIO LVL <n>\r\n 电平查询结果
\r\n+GPIO INT <n>\r\n 中断上报(URC)
附录 B:API 索引
| API | 功能 | 章节 |
|---|---|---|
application_init() |
任务注册宏 | 2 |
yopen_rtos_task_sleep_ms() |
任务延时 | 3.2 / 7.2 |
yopen_gpio_set_voltage() |
设置 IO 电压域 | 3.3 |
yopen_rtos_queue_create() |
创建消息队列 | 3.4 |
yopen_rtos_queue_release() |
投递消息(非阻塞) | 3.5 / 6.1 / 7.1 |
yopen_rtos_queue_wait() |
等待消息 | 4 |
yopen_pin_set_func() |
引脚复用配置 | 3.6 / 5.2 |
yopen_aon_power_on() |
AON 域上电 | 5.2 |
yopen_gpio_init() |
GPIO 初始化 | 5.4 |
yopen_gpio_deinit() |
GPIO 反初始化 | 5.1 |
yopen_int_register() |
中断注册 | 5.4 |
yopen_int_enable() / yopen_int_disable() |
中断使能 / 关闭 | 5.4 / 7 |
yopen_gpio_get_level() |
读取电平 | 6.3 / 7.2 |
yopen_uart_read() / yopen_uart_write() |
串口读写(非阻塞) | 3.6 / 6.2 |
yopen_uart_register_cb() |
注册串口回调 | 3.6 |