30-ESP32-S3-WIFI篇-01 FreeRTOS

news2024/12/23 12:27:56

ESP32-S3-WIFI-FreeRTOS 任务

介绍

FreeRTOS

FreeRTOS是一个开源的实时操作系统(RTOS)内核,以模块化的方式与ESP-IDF集成。这意味着所有的ESP-IDF应用程序和各种ESP-IDF组件都建立在FreeRTOS框架之上。FreeRTOS内核已经被移植到ESP芯片的所有CPU架构上,包括Xtensa和RISC-V。

在ESP32中,WIFI的操作与FreeRTOS的多任务紧密相连,并且彼此之间存在相互依赖的关系。实际上,FreeRTOS作为一种用于实时操作系统的开源软件,它提供了任务调度和管理的功能。而ESP32作为一款集成了WIFI功能的芯片,充分利用了FreeRTOS的多任务处理机制来实现同时处理多个WIFI连接和数据传输的能力。因此,了解和熟悉FreeRTOS的多任务编程模型对于有效地操作和管理ESP32的WIFI功能至关重要。通过合理的任务分配和调度,能够充分利用ESP32的资源,提高WIFI的性能和稳定性。同时,合理处理WIFI任务与其他任务的并行执行,能够确保系统的整体响应性和吞吐量。

显示当前所有的任务

在开始前让我们先看看不进行任何操作,ESP3都有哪些任务

首先在 menuconfig中修改打开下面👇设置
在这里插入图片描述
然后运行以下程序

/**
 * @brief 显示当前所有的任务
 * 
 */
void task_list(){
    char ptrTaskList[250];

    vTaskList(ptrTaskList);
    printf("********************************************\n");
    printf("Task          State     Prio    Stack     Num\n");
    printf("********************************************\n");
    printf(ptrTaskList);
    printf("********************************************\n");

}

void app_main(void){
    task_list();
}

效果如下
在这里插入图片描述
以下是将FreeRTOS任务列表用表格表示的结果:

任务名称状态优先级剩余堆栈空间 (字节)任务编号功能描述
mainX120364应用主任务,通常用于启动其他任务和初始化系统。
IDLE1R08126空闲任务之一,通常在系统空闲时执行,一般进行喂狗。
IDLE0R010085空闲任务之二,通常在系统空闲时执行,一般进行喂狗。
esp_timerB2233523定时器服务任务,处理系统定时器事件。
ipc1S245242进程间通信任务之一,用于处理高优先级的IPC操作。
Tmr SvcB113207定时器服务任务,管理应用程序定时器回调函数的执行。
ipc0S245121进程间通信任务之二,用于处理高优先级的IPC操作。

各个字段含义:

  • 任务名称 (Task): 任务的名称。
  • 状态 (State): 任务的状态。
    • X: 未知状态
    • R: 运行中 (Running)
    • B: 阻塞中 (Blocked)
    • S: 挂起 (Suspended)
  • 优先级 (Prio): 任务的优先级,数值越大优先级越高。
  • 剩余堆栈空间 (Stack): 任务剩余的堆栈空间大小,以字节为单位。
  • 任务编号 (Num): 任务的编号。

带有WIFI功能时的任务

我们将WIFI扫描的功能加入到app_main

#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

/**
 * @brief WIFI扫描
 * 
 */
void wifi_scan(){

    ESP_LOGI("WIFI", "0. 初始化NVS存储");
    ESP_ERROR_CHECK(nvs_flash_init());                   // 对NVS默认的区域进行初始化

    ESP_LOGI("WIFI", "1. WIFI 初始化阶段");
    esp_netif_init();                    // 1.1 创建一个 LwIP 核心任务
    esp_event_loop_create_default();             // 1.2 创建一个系统事件任务
    esp_netif_create_default_wifi_sta(); // 1.3.1 创建有 TCP/IP 堆栈的默认网络接口实例

    wifi_init_config_t wifi_config = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&wifi_config);             // 1.3.2 创建 Wi-Fi 驱动程序任务

    ESP_LOGI("WIFI", "2. WIFI 配置阶段");
    esp_wifi_set_mode(WIFI_MODE_STA);       // 2 将 Wi-Fi 模式配置为 station

    ESP_LOGI("WIFI", "3. WIFI 启动阶段");
    esp_wifi_start();                        // 3.1 启动 Wi-Fi 驱动程序

    ESP_LOGI("WIFI", "4. WIFI 扫描");

    //在所有信道中扫描全部 AP(前端)
    wifi_country_t country_config = {
        .cc = "CN",
        .schan = 1,
        .nchan = 13,
    };
    esp_wifi_set_country(&country_config); // 4.1 扫描配置国家代码

    wifi_scan_config_t scan_config = {
        .show_hidden = true               // 显示隐藏
    };
    esp_wifi_scan_start(&scan_config,true); // 4.2 配置扫描信息 true表示当这个任务执行的时候,回进入阻塞状态等待扫描

    //得到扫描的AP数量
    uint16_t ap_num =0;
    ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(&ap_num)); 
    ESP_LOGI("WIFI","AP Count : %d",ap_num);

    //获取具体的AP信息
    uint16_t max_aps=20;
    wifi_ap_record_t ap_records[max_aps];
    memset(ap_records,0,sizeof(ap_records));

    uint16_t aps_count =max_aps;
    ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&aps_count,ap_records));

    //打印信息
    ESP_LOGI("WIFI","AP Count: %d",aps_count);
    printf("%30s %3s %3s %3s\n","SSID","频道","强度","MAC地址");

    for(int i=0;i<aps_count;i++){
        printf("%30s %4d %4d %02X-%02X-%02X-%02X-%02X-%02X\n", 
       ap_records[i].ssid, 
       ap_records[i].primary, 
       ap_records[i].rssi, 
       ap_records[i].bssid[0], 
       ap_records[i].bssid[1], 
       ap_records[i].bssid[2], 
       ap_records[i].bssid[3], 
       ap_records[i].bssid[4], 
       ap_records[i].bssid[5]);
    }
}

/**
 * @brief 显示当前所有的任务
 * 
 */
void task_list(){
    char ptrTaskList[250];

    vTaskList(ptrTaskList);
    printf("********************************************\n");
    printf("Task          State     Prio    Stack     Num\n");
    printf("********************************************\n");
    printf(ptrTaskList);
    printf("********************************************\n");

}

void app_main(void)
{
    wifi_scan();
    task_list();
    for(;;){
        vTaskDelay(1000/portTICK_PERIOD_MS);
    }
}

在这里插入图片描述
可见怎加了WIFI扫描后任务列表中增加了以下任务:

  1. tiT: 优先级18,状态为阻塞 (B),堆栈空间为2436字节,任务编号为8。
  2. sys_evt: 优先级20,状态为阻塞 (B),堆栈空间为1588字节,任务编号为9。
  3. wifi: 优先级23,状态为运行 ®,堆栈空间为4384字节,任务编号为10。

这些任务对应的就是,ESP-IDF编程文档中关于WIFI编程流程的图片,对应关系如下👇。

在这里插入图片描述
而对于App task任务在上图1.4步骤创建

创建App task方法如下

void app_task(void* pt){
    ESP_LOGI("app_task","App Task创建成功");
    vTaskDelete(NULL);
}
xTaskCreate(app_task,"App Task",1024*12,NULL,1,NULL);   //1.4 创建app_task任务

五个任务分别是什么

到现在五个任务都有了,它们分别执行的是什么功能呢

任务功能描述

  1. Main task::主任务通常负责系统初始化和启动其他任务。在Wi-Fi连接过程中的主要作用是初始化并启动Wi-Fi连

  2. App task:应用任务负责应用程序的主要逻辑处理,例如处理用户请求、数据处理等。在Wi-Fi连接过程中的主要作用是处理Wi-Fi连接状态的变化并相应地通知其他任务。

  3. Event task:事件任务负责处理系统和应用程序的事件,在Wi-Fi连接过程中主要负责处理Wi-Fi事件,作为中间媒介促使两个任务沟通,例如连接成功、断开连接、获取IP地址等。
    在这里插入图片描述

    例如,在启动阶段,“主任务”(Main task)启动了Wi-Fi的激活。一旦Wi-Fi任务成功启动,它会向"事件任务"(Event task)发送一条消息,指示WIFI_STA已启用。收到此信息后,事件任务会向应用程序任务(App task)提供反馈,通知它WIFI_STA已启用。

  4. LwIP task:LwIP任务负责TCP/IP协议栈的处理,包括网络数据包的发送和接收。在Wi-Fi连接过程中,LwIP任务负责网络数据的处理和传输。

  5. Wi-Fi task:Wi-Fi任务负责Wi-Fi的管理和控制,包括扫描可用网络、连接到指定网络、处理Wi-Fi事件等。

事件循环库

事件循环库使组件能够声明事件,允许其他组件注册处理程序(即在事件发生时执行的代码片段)。此时,无需直接涉及应用程序,松散耦合组件也能够在其他组件状态变化时附加所需的行为。此外,通过将代码执行序列化,在指定的任务中运行事件循环库,可以简化事件处理程序,实现更高效的事件处理。

那么如何在代码中捕获上图的1,2的信息呢

我们修改代码如下

#include "esp_event.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// 定义事件处理程序
void run_on_event(void* handler_arg, esp_event_base_t base, int32_t id, void* event_data)
{
    // 事件处理程序逻辑
    ESP_LOGE("event_handle", "事件处理程序 base: %s, id: %d", base, id);
}

// 应用任务
void app_task(void* pt) {
    ESP_LOGI("app_task", "App Task创建成功");

    // 注册事件处理程序
    esp_err_t err = esp_event_handler_register(ESP_EVENT_ANY_BASE, ESP_EVENT_ANY_ID, run_on_event, NULL);
    if (err != ESP_OK) {
        ESP_LOGE("app_task", "事件处理程序注册失败: %s", esp_err_to_name(err));
    } else {
        ESP_LOGI("app_task", "事件处理程序注册成功");
    }

    // 删除任务
    ESP_LOGI("app_task", "删除App Task");
    vTaskDelete(NULL);
}

// 主函数
void app_main(void) {
    // 创建应用任务
    xTaskCreate(app_task, "app_task", 2048, NULL, 5, NULL);
}

我们定义了一个事件处理程序 run_on_event 和一个任务 app_task,该任务中使用 esp_event_handler_register 注册和处理各种系统和应用程序事件,从而实现事件驱动的编程模型,在事件处理程序 run_on_event中接收所有事件并打印事件的基类型 (base) 和事件ID (id)。简单来说app_task任务中的esp_event_handler_register 指的是要听Event task任务中的信息(Event task就像一个传话筒,作任务间信息传递的媒介),下面是各参数的含义

  • ESP_EVENT_ANY_BASE表示我要听WIFI_Event(注册处理所有事件基类型的事件),

  • ESP_EVENT_ANY_ID:表示注册处理所有事件ID的事件。

  • run_on_event:这是事件处理程序函数,当注册的事件发生时,将调用该函数。

  • NULL:这是传递给事件处理程序的参数,在本例中未使用。

esp_event_handler_register 函数,代码将 run_on_event 注册为一个通用事件处理程序,使其能够处理所有类型和所有ID的事件。这意味着,无论何种事件发生,run_on_event 都会被调用并处理该事件。

🚨需要注意的事情是指重要的事件,比如Wi-Fi成功连接到接入点。当引用事件时,应该使用由两部分组成的标识符。事件循环是连接事件和事件处理程序之间的桥梁,事件源可以通过使用事件循环库提供的API将事件发布到事件循环中 而esp_event_handler_register函数就是默认事件循环 API 。注册到事件循环的事件处理程序会对特定类型的事件做出响应。

可能说到这里还是很晕,我们结合流程图来看,刚刚我们在App task任务中使用esp_event_handler_register,相当于告诉Event task(Event loop),我要听谁的信息,哪些信息,我们要听的是Wi-Fi task(Wi-Fi_Event或者叫base)的所有信息,而在启动阶段我们可以听到下图1️⃣这个信息,而WIFI_EVENT_STA_START这个信息会传给run_on_event这个函数,用int32_t id这个参数来接收。那么在上面代码run_on_eventESP_LOGE("event_handle", "事件处理程序 base: %s, id: %d", base, id);这行代码就会打印WIFI_EVENT_STA_START所对应的ID
在这里插入图片描述
🚨注意在事件处理程序中尽量避免执行大量程序,只做简短的信息操作处理。

WIFI的初始化,配置,启动之后,就要开始进行WIFI扫描流程如下

在这里插入图片描述
扫描操作会在WIFI_task任务中完成,完成后会在Event_task发送已经完成的信息,我们修改程序如下,在esp_event_handler_register捕捉到对应信息时,进行对应的处理,例如我们在捕捉到WIFI_EVENT_STA_START信息时表明WIFI已经启动,此时进行WIFI的扫描操作,在捕捉到WIFI_EVENT_SCAN_DONE信息时表明扫描完成,此时进行AP的打印操作。

/**
 * @brief WIFI 扫描
 * 
 */
void wifi_scan_task(void* pt){
    ESP_LOGI("WIFI", "4. WIFI 扫描");

    //在所有信道中扫描全部 AP(前端)
    wifi_country_t country_config = {
        .cc = "CN",
        .schan = 1,
        .nchan = 13,
    };
    esp_wifi_set_country(&country_config); // 4.1 扫描配置国家代码

    wifi_scan_config_t scan_config = {
        .show_hidden = true               // 显示隐藏
    };
    esp_wifi_scan_start(&scan_config,true); // 4.2 配置扫描信息 true表示当这个任务执行的时候,回进入阻塞状态等待扫描
    vTaskDelete(NULL);
}

/**
 * @brief 显示扫描的ap信息
 * 
 * @param pd 
 */
void wifi_show_task(void* pd){
    //得到扫描的AP数量
    uint16_t ap_num =0;
    ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(&ap_num)); 
    ESP_LOGI("WIFI","AP Count : %d",ap_num);

    //获取具体的AP信息
    uint16_t max_aps=20;
    wifi_ap_record_t ap_records[max_aps];
    memset(ap_records,0,sizeof(ap_records));

    uint16_t aps_count =max_aps;
    ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&aps_count,ap_records));

    //打印信息
    ESP_LOGI("WIFI","AP Count: %d",aps_count);
    printf("%30s %3s %3s %3s\n","SSID","频道","强度","MAC地址");

    for(int i=0;i<aps_count;i++){
        printf("%30s %4d %4d %02X-%02X-%02X-%02X-%02X-%02X\n", 
       ap_records[i].ssid, 
       ap_records[i].primary, 
       ap_records[i].rssi, 
       ap_records[i].bssid[0], 
       ap_records[i].bssid[1], 
       ap_records[i].bssid[2], 
       ap_records[i].bssid[3], 
       ap_records[i].bssid[4], 
       ap_records[i].bssid[5]);
    }
    vTaskDelete(NULL);
}

// 定义事件处理程序
void run_on_event(void* handler_arg, esp_event_base_t base, int32_t id, void* event_data)
{
    // 事件处理程序逻辑
    //ESP_LOGE("event_handle","事件处理程序 base: %s , id: %d",base,id);

    switch(id){
    case WIFI_EVENT_STA_START:
        ESP_LOGE("EVENT_HANDLE","WIFI_EVENT_STA_START");
         xTaskCreate(wifi_scan_task,"WIFI_scan Task",1024*12,NULL,1,NULL);
        break;
    case WIFI_EVENT_SCAN_DONE:
        ESP_LOGE("EVENT_HANDLE","WIFI_EVENT_SCAN_DONE");
        xTaskCreate(wifi_show_task,"WIFI_show Task",1024*12,NULL,1,NULL);

    default:
    }
}
/**
 * @brief 用户自定义app_task任务用于捕捉wifi_task的消息
 * 
 * @param pt 
 */
void app_task(void* pt){
    ESP_LOGI("app_task","App Task创建成功");

    esp_event_handler_register(ESP_EVENT_ANY_BASE,ESP_EVENT_ANY_ID,run_on_event,NULL);
    vTaskDelete(NULL);
}

总结

参考资料
ESP-IDF 编程指南 事件循环库
ESP-IDF 编程指南 Wi-Fi 驱动程序
FreeRTOS 任务 - 乐鑫 ESP32 物联网开发框架 ESP-IDF 开发入门 - 孤独的二进制出品

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1718788.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C/C++|智能指针的shared_from_this和enable_shared_from_this

参考博客(或者叫摘抄的博客&#xff0c;本博客只做为个人学习使用)&#xff1a; 施磊老师牛逼 深入掌握C智能指针 C智能指针的enable_shared_from_this和shared_from_this机制 文章目录 再探 shared_ptr错误一修改错误一错误二修改错误二 enable_shared_from_this和shared_from…

web项目规范配置(husky、eslint、lint-staged、commit)

背景&#xff1a; 团队开发为了保证提交代码格式统一&#xff0c;通常在进行代码提交的时候对暂存区代码进行校验&#xff0c;如没有通过eslint(本例使用eslint)校验&#xff0c;则不能提交到远端。 安装依赖 husky 、eslint 、prettier 、lint-staged npm install husky e…

光环云携手火山引擎共推全栈AI服务,赋能千行百业智能化转型,助力新质生产力发展

5月15日&#xff0c;2024春季火山引擎FORCE原动力大会在北京举办。作为智算云网综合服务提供商&#xff0c;光环云受邀出席大会&#xff0c;与火山引擎共同探索大模型时代下行业发展的新趋势。 会上&#xff0c;光环云数据有限公司正式与火山引擎签署生态伙伴合作协议&#xf…

算法与数据结构:红黑树

ACM大牛带你玩转算法与数据结构-课程资料 本笔记属于船说系列课程之一&#xff0c;课程链接&#xff1a; 哔哩哔哩_bilibilihttps://www.bilibili.com/cheese/play/ep66799?csourceprivate_space_class_null&spm_id_from333.999.0.0 你也可以选择购买『船说系列课程-年度会…

计算机图形学入门04:视图变换

1.MVP变换 将虚拟场景中的模型投影到屏幕上&#xff0c;也就是二维平面上&#xff0c;需要分三个变换。 1.首先需要知道模型的位置&#xff0c;也就是前面提到的基本变换&#xff0c;像缩放、平移&#xff0c;旋转&#xff0c;也称为模型(Model)变换。 2.然后需要知道从…

精选免费在线工具与资源推荐20240531

精选免费在线工具与资源推荐 引言 在互联网高速发展的今天&#xff0c;我们身处一个信息爆炸的时代。为了更好地应对工作和学习中的挑战&#xff0c;我们时常需要借助各种工具和资源来提高效率。幸运的是&#xff0c;网络上存在着大量免费且高效的在线工具和资源&#xff0c;…

告别低效提问:掌握BARD技巧,让AI成为你的智能助手!

今天只聊一个主题&#xff1a;提示词 Prompt。 说到提示词&#xff0c;大家可能都看过GPT的高级示例&#xff0c;那些几百字的提示词&#xff0c;写起来确实不容易。 那么&#xff0c;如何写出同样效果的提示词呢&#xff1f; 有没有什么公式或者系统学习的方法&#xff1f;…

HackTheBox-Machines--Nibbles

Nibbles 测试过程 1 信息收集 NMAP 80 端口 网站出了打印出“Hello world&#xff01;”外&#xff0c;无其他可利用信息&#xff0c;但是查看网页源代码时&#xff0c;发现存在一个 /nibbleblog 文件夹 检查了 http://10.129.140.63/nibbleblog/ &#xff0c;发现了 /index.p…

windows系统配置dns加快访问github 实用教程一(图文保姆级教程)

第一步、打开网页 https://tool.lu/ip IP地址查询 - 在线工具 输入www.github.com 或者github.com 点击网页查询按钮, 获取对应github网站对应的ip 完整操作步骤如上图所示,可以很清晰的看到github网站的ip显示地区是美国也就是说该网站服务器是在国外, 这也就是为什么我们在…

JUC总结2

synchronized锁 synchronized底层原理 当使用synchronized时&#xff0c;不需要自己编写代码进行上锁和上锁的操作&#xff0c;因为JVM帮我们把相关操作完成了。 JVM采用了monitorenter和monitorexit指令进行同步的&#xff0c;前者指向同步代码开始的位置&#xff0c;后者指…

java——网络原理初识

T04BF &#x1f44b;专栏: 算法|JAVA|MySQL|C语言 &#x1faf5; 小比特 大梦想 目录 1.网络通信概念初识1.1 IP地址1.2端口号1.3协议1.3.1协议分层协议分层带来的好处主要有两个方面 1.3.2 TCP/IP五层 (或四层模型)1.3.3 协议的层和层之间是怎么配合工作的 1.网络通信概念初识…

探索气象数据的多维度三维可视化:PM2.5、风速与高度分析

探索气象数据的多维度可视化&#xff1a;PM2.5、风速与高度分析 摘要 在现代气象学中&#xff0c;数据可视化是理解复杂气象模式和趋势的关键工具。本文将介绍一种先进的数据可视化技术&#xff0c;它能够将PM2.5浓度、风速和高度等多维度数据以直观和动态的方式展现出来。 …

国产身份域管架构图集合(信创政策AD域替换必看)

几类典型架构 双机架构 单点单机房 集群架构 多点单机房 两地三中心架构 多点多机房 多地分布式架构 多点多机房 全栈信创方案架构&#xff0c;欢迎探讨交流~

emp.dll文件丢失要怎么解决?荒野大镖客emp.dll修复方法分享

软件运行过程中经常遇到各种技术问题&#xff0c;其中之一就是动态链接库&#xff08;DLL&#xff09;文件丢失的现象。DLL文件是Windows操作系统中一个重要的组件&#xff0c;它包含运行多个应用程序所需要的代码和数据。因此&#xff0c;一个丢失的DLL文件&#xff0c;如“em…

同城活动报名系统源码活动组局找搭子小程序Java源码全开源

活动流程图 管理端设置 1.系统操作 2.活动类型 可添加线上和线下活动,线上活动,比如游戏等,需要可以进入游戏,需要签到等; 线下活动,比如线下交友等, 3.活动管理 可给用户添加活动,给活动设置报名时间,活动开始时间等; 也可查看报名列表和签到列表 4.进行中的活动 等发起…

校园导航系统C++

制作一个简单的大学城导航系统&#xff0c;根据用户指定的起点和终点&#xff0c;求出最短路径长度以及具体路径。 项目要求&#xff1a; 1&#xff09;程序与数据相分离&#xff0c;地图中的所有数据都是从文件读入&#xff0c;而不是写在代码中 2&#xff09;最短路径算法…

热敏电阻的设计

热敏电阻(NTC)的作用&#xff1a;抑制开机时的浪涌电流。防止开机瞬间产生的浪涌电流损坏后面的元件。 取值依据:根据对开机的脉冲电流&#xff08;浪涌电流&#xff09;小于多少A&#xff1f; 由,这个U是指最大输入电压&#xff0c;I为要求的浪涌电流。 NTC是负温度系数的热…

设计模式23——状态模式

写文章的初心主要是用来帮助自己快速的回忆这个模式该怎么用&#xff0c;主要是下面的UML图可以起到大作用&#xff0c;在你学习过一遍以后可能会遗忘&#xff0c;忘记了不要紧&#xff0c;只要看一眼UML图就能想起来了。同时也请大家多多指教。 状态模式&#xff08;State&am…

打造高效上传体验:基于Kotlin的Android快速上传框架

1. 引言 在Android开发中&#xff0c;文件上传操作常常面临各种挑战&#xff0c;为此我开源了一个高效、易用的快速上传框架&#xff0c;助力开发者轻松实现文件上传功能。 GitHub项目地址: 点我 2. 框架特点概述 纯Kotlin编写&#xff1a;简洁、现代的编程语言。MVVM架构&a…

动态分配函数参数用二级指针的作用

文章目录 前言一、案例 前言 在一些情况下&#xff0c;我们需要在函数内部动态地分配内存来存储结构体&#xff0c;并且需要在函数外部访问该结构体。在这种情况下&#xff0c;可以使用二级指针作为函数参数来实现动态内存分配&#xff0c;并且在函数外部使用指针访问结构体。…