BLE核心模块FS-QN9021模块开发-linux版

这段时间又参与了一个新的小项目,简单概括为蓝牙、智能、家居吧,虽然时间有点紧,还是希望能把这一些东西记录下来。


BLE

什么是BLE?参考这篇文章做如下总结。
中文名称为蓝牙低功耗。主要特点为低成本、超低功耗、短距离、标准接口和可互操作性强,并且工作在免许可的2.4GHz ISM射频段,需要支持蓝牙4.0(系统为Android4.3及以上)的主机设备才能与其连接。

目前生产BLE芯片的厂家主要有CSR、TI、Nodic和NXP(QN902x),各个厂家芯片对比如下图

s

从如上图对比可以看出,NXP的QN902x在功耗方面比CSR和TI更省电,在接收灵敏度和模式方面比Nodic的胜一筹,它的从设备相比其它几家可以连接的更多,共有8个,这也算是蓝牙4.0的一大特色吧,并且NXP的芯片已经过了MFI认证,直接能与苹果设备相连接,因为这种认证也是挺贵的。

因为BLE的低功耗、低成本及强大的处理能力,并且随着iPhone的设备支持蓝牙4.0,BLE的终端设备在我们的生活当中将会越来越多,在未来将会有爆发式增长。

QN902X是一款内核为M0的蓝牙BLE SOC芯片,其SDK对蓝牙BLE的profile都有实现,并提供源码,SDK也提供很多工具以便使用芯片,比如引脚配置,NVDS读写,串口USB Dongle(配合上位机可以调试各profile,没有手机也可以调试),ISP下载等,但QN902X不提供数据手册,所有的外设操作都以库的方式提供,SDK说明比较全面但全是英文的。

与NODRDIC的51822和TI的CC2540不同QN902X的架构是M0+ROM+FLASH+SRAM的方式,其中ROM放的是蓝牙协议和内部一个小的调度核,FLASH放的是用户程序和数据,RAM用于跑程序。其中ROM:96K,SRAM:64K,FLASH:64K/128K。因为QN902X程序是跑到SRAM中,所以它的深度睡眠电流比较大些。


环境搭建

老生长谈,开发一款产品,第一步当然是搭建开发环境咯。

wine安装

这里可知,keil不支持linux环境,所以必须自己想办法了。这是在linux环境下运行windows的程序,本身使用windows的请自动忽略这一步。具体请看官网/博文

1
2
3
4
5
6
7
8
~ $ sudo add-apt-repository ppa:ubuntu-wine/ppa
~ $ sudo apt-get update
# => 我的是64为的系统,所有安装64位版
~ $ sudo apt-get install wine1.8-amd64
# => 配置
~ $ winecfg
# => 中文路径:~/ .wine/drive_c/windows/Fonts/
~ $ winetricks corefonts

keil安装

首先得下载keil,可以自己去官网下载最新的,也可以直接点击mdk5.17下载地址,参考这篇博文并实践做如下记录。

1
2
# => 进入下载目录安装
~ $ wine mdk517.exe

提示:卸载程序可以用wine uninstaller
如编译有限制,下载破解注册机Keygen:http://pan.baidu.com/s/1hqGSRqs

安装完成后,会弹出来一个安装器件(pack installer)如下的界面,也就是说,你要用它来开发哪个芯片(此项目可以忽略,后面步骤导入DB)。
packin



或者打开keil界面会看到如下图标
packicon



要开发哪一款芯片,点击install即可,或者先网页端下载好在导入,具体参考上面提供的博文地址。

MCU DB库安装

下载Quintic最新的SDKQBlue1.3.7,安装:

1
~ $ wine QBlue-1.3.7.exe

会弹出窗口如下,点击安装即可,或者打开桌面QBlue里的QN9020DevDBforIDE工具安装。

sdk

keil使用

首先获取开源代码

1
2
~ $ cd
~ $ git clone git@bitbucket.org:T-Firefly/fireble.git

桌面打开keil,假如我们希望开始proxr工程:

  1. 在keil的Project菜单中选择Open Project…
  2. 弹出文件选择框中,打开/home/xxx/fireble/BLE/prj_proxr/keil/proxr.uvproj工程文件(linux可能在z磁盘里)
  3. 配置DB如下,如果没发现库,请重启软件

    dbset

  4. 编译代码,成功后如下,具体配置选项请参考这里

    compile

  5. 下载程序,下载的时候需要按复位键,如下载出错,可先下载到SRAM。

    dl

Tip:在ubuntu 上串口识别为ttyS0或ttyUSB0之类,在wine上识别不到,可用:

1
2
3
4
># =>将其该为小写的com1,如果不行,将其改为大写的COM1
>~ $ sudo ln -s /dev/ttyUSB0 ~/.wine/dosdevices/com1
>~ $ sudo usermod -a -G dialout $USER
>~ $ sudo chmod 777 ~/.wine/dosdevices/com1

即可在wine的应用程序使用串口


项目实践

点亮LED

对于新的芯片与开发板,从LED实验开始。首先下载该开发板的原理图对该LED部分的电路进行分析。

本程序非常简单,复制gpio demo代码

实现让开发板D1灯闪烁如下:

1
2
3
4
5
6
7
8
9
/* Set pin D1/P2_7 */
gpio_set_direction_field(GPIO_P27, (uint32_t)GPIO_OUTPUT);
while(1)
{
gpio_write_pin_field(GPIO_P27, GPIO_LOW);
delay(100000);
gpio_write_pin_field(GPIO_P27, GPIO_HIGH);
delay(100000);
}

程序流程:系统初始化–>GPIO配置–>各驱动模块初始化–>主循环实现功能
效果如下:

led

UART实验

串口通信可以用来打印数据,调试程序,有必要实验一下。
同样复制uart demo代码
改程序如下:

1
2
3
//Print out "Hello NXP!\n" thought uart.
uart_printf(QN_UART0, (uint8_t *)"Hello NXP!\n");
uart_printf(QN_UART0, (uint8_t *)"This is nephen's test!\n");

波特率设置为115200,现象如下:
uart_tst

PWM实验

由于这个项目会控制到电机什么的,所以pwm少不了,这个实验是通过pwm控制陶瓷蜂鸣器报警和呼吸灯。通过PWM方式调节脉冲频率和占空比,变换LED亮度,渐变亮度实现呼吸灯效果。FireBLE板载的贴片蜂鸣器是压电式陶瓷蜂鸣器,压电陶瓷蜂鸣器要想响起来,需要满足四个条件:多谐振荡器、压电蜂鸣片、阻抗匹配器及共鸣箱,压电蜂鸣片由锆钛酸铅或铌镁酸铅压电陶瓷材料制成,在陶瓷片的两面镀上银电极,经极化和老化处理后,再与黄铜片或不锈钢片粘在一起。板载的蜂鸣器缺少的只是多谢振荡器,这里我们用PWM来代替,输出1.5KHz-2.5KHz的方波信号推动压电蜂鸣片发声,频率在1.5KHz-2.5KHz才会响,太高太低都不响,直接加3.3V更不会响,直接加3.3V响的那是电磁式蜂鸣器,有源和无源。
首先看下电路连接

同样复制pwm代码,改写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int main (void)
{
SystemInit();

pwm_init(PWM_CH0);
pwm_io_config();
//P2.7 will output pwm wave with period for 1000us and pulse for 400us
pwm_config(PWM_CH0, PWM_PSCAL_DIV, PWM_COUNT_US(1000, PWM_PSCAL_DIV), PWM_COUNT_US(500, PWM_PSCAL_DIV));
pwm_enable(PWM_CH0, MASK_ENABLE);
pwm_io_dis_config();

pwm_init(PWM_CH1);
pwm_io_config();
//P2.6 will output pwm wave with period for 1000us and pulse for 500us
pwm_config(PWM_CH1, 119, PWM_COUNT_US(1000, 119), PWM_COUNT_US(500, 119));
pwm_enable(PWM_CH1, MASK_ENABLE);

while (1) /* Loop forever */
{
int i;
for(i=0;i<=1000;i++)
{
pwm_config(PWM_CH1, 119, PWM_COUNT_US(1000-i, 119), PWM_COUNT_US(500, 119));
if(i%2)
pwm_config(PWM_CH0, PWM_PSCAL_DIV, PWM_COUNT_US(1000-i, PWM_PSCAL_DIV), PWM_COUNT_US(500, PWM_PSCAL_DIV));
else
pwm_config(PWM_CH0, PWM_PSCAL_DIV, PWM_COUNT_US(i, PWM_PSCAL_DIV), PWM_COUNT_US(500, PWM_PSCAL_DIV));
delay(2000);
}
}
}

呼吸灯效果:

breath

按键广播

看到有些人对买的fireBLE按键广播不懂,其实刚开始我也是这样的,摸不着头脑,现在至少不会太迷糊,就给大家记录下吧。
首先按键按下属于gpio中断,找到中断初始化函数:SystemInit(); ——》
gpio_init(gpio_interrupt_callback); ——》
void gpio_interrupt_callback(enum gpio_pin pin) ——》
void usr_button1_cb(void) ——》
ke_evt_set(1UL << EVENT_BUTTON1_PRESS_ID); 找到EVENT_BUTTON1_PRESS_ID对应的事件,搜索
EVENT_BUTTON1_PRESS_ID,找到void usr_init(void)里的if(KE_EVENT_OK != ke_evt_callback_set(EVENT_BUTTON1_PRESS_ID, app_event_button1_press_handler))可知为app_event_button1_press_handler ——》
ke_timer_set(APP_KEY_SCAN_TIMER,TASK_APP,2); 同样通过搜索APP_KEY_SCAN_TIMER可知定时器事件为app_key_scan_timer_handler ——》
app_key_scan_timer_handler,这里进行adc采集,完成后由adc_read(&read_cfg, adc_key_value, KEY_SAMPLE_NUMBER, adc_test_cb);可知进入adc_test_cb,在这里设置了EVENT_ADC_KEY_SAMPLE_CMP_ID,搜索找到对应事件app_event_adc_key_sample_cmp_handler ——》
app_event_adc_key_sample_cmp_handler,判断按键朝哪个方向按,定时回调 ——》
app_key_process_timer_handler,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int app_key_process_timer_handler(ke_msg_id_t const msgid, void const *param,
ke_task_id_t const dest_id, ke_task_id_t const src_id)
{
ke_evt_clear(1UL << EVENT_ADC_KEY_SAMPLE_CMP_ID);
switch (key_value0)
{

case key_up:
if (APP_IDLE == ke_state_get(TASK_APP))
{

// start adv
//QPRINTF("you press up key\r\n!");
app_gap_adv_start_req(GAP_GEN_DISCOVERABLE | GAP_UND_CONNECTABLE,
app_env.adv_data, app_set_adv_data(GAP_GEN_DISCOVERABLE),
app_env.scanrsp_data, app_set_scan_rsp_data(app_get_local_service_flag()),
GAP_ADV_FAST_INTV1, GAP_ADV_FAST_INTV2);

这里一看就明白,向上按时,进入广播。其实在系统上电的时候,已经进行了一系列的gap建立连接的初始话,就差广播了,所以这里只要广播出去就能和别的蓝牙建立连接。

QTool使用及蓝牙了解

使用QBlueStudio中QTool工具进行蓝牙开发分析,可以方便的对各个蓝牙操作的过程进行细致的研究,并结合具体的源代码进行查看,能够更加深入的了解到蓝牙协议的实现过程,大部分的API接口在GAP和GATT。具体的使用文档请查看QBlueStudio里的Document。Linux里可以采取这种方法查看:地址见/home/username/.wine/drive_c/QBlue/QN9020/QBlue-1.3.7/Documents,然后使用evince命令打开。

在这之前,还需对蓝牙的一些术语做一个大概的了解,比如什么是master,slave,主机。客户端与服务器又是什么。GAP与GATT有什么区边。蓝牙各个协议层都有哪些分工。下面参考做一些归纳:

  1. BLE规范中定义了GAP(Generic Access Profile)和GATT(Generic Attribute)两个基本配置文件。
    a.协议中的GAP层负责设备访问模式和进程,包括设备发现,建立连接,终止连接。初始化安全特性和设备配置。
    b.GATT层用于已连接的蓝牙设备之间的数据通信。GATT通俗理解为用于主从机之间的客户端和服务器端的数据交互,以Attribute Table来体现。

  2. BLE低功耗蓝牙中有四种设备类型,Central主机,Peripheral从机,Observer观察者,Broadcaster广播者。通常Central主机,Peripheral从机一起使用,Observer观察者,Broadcaster广播者一起使用。Central和Peripheral连接交换数据,平时我们使用到的基本上是这种模式。而像多温度采集器,通常使用Observer和Broadcaster这种无连接形式。
    主机和从机是这样开机工作的:从机开始广播,然后主机扫描广播的从机,当从机收到主机的扫描请求后,会向主机发送扫描回应数据。然后主机发起连接,然后开始通讯。所以从机需要设置广播内容和扫描回应内容。这方面的代码可以查看App_gap.c和App_gap_task.c。

    • profile:可以理解为一种规范,一个标准的通信协议,profile存在于从机中。蓝牙组织规定了一系列的标准profile,如防丢计、心率计。每个profile中包含多个service,每个service代表从机的一种能力。
    • Service:可以理解为一种服务,在BLE从机里,通过有多个服务,例如电量信息服务、系统信息服务等。每个service又包含多个characteristic特征值。每个具体的characteristic特征值才是BLE通讯的主体,比如当前电量是80%。所以会通过电量的characteristic特征值特征值保存在从机的profile里,这样主机就可以通过这个characteristic来读取80%这个数。
    • characteristic:characteristic特征值,BLE主从机均是通过characteristic来实现,可以理解为一个标签,通过这个标签可以获取或者写入想要的内容。
    • UUID:统一标识吗,我们刚提到的characteristic和service,都需要一个唯一的UUID来标识。

      每个从机都会有个叫做profile的东西存在,不管自定义的还是标准的profile,他们都是由一系列的Service组成,然后每个service又包含多个characteristic,主机和从机之间的通信,均是通过characteristic来实现。
      BLE协议栈中传输数据分为两方面,一个GATT的client主动向service发送数据。另一个是GATT的service主动向client发送数据。即主从之前相互传数据。
      主机向从机发送数据,使用GATT_Write
      从机向主机发送数据,使用GATT_Notification

  3. GATT有Service和Client,Service作为服务器端,对GATT Client提供read/write接口,一般情况下,Central作为Client,Peripheral作为Service。所以主机会调用read/write来和作为Service端的Peripheral从机通讯。而Peripheral则通过notify的方式即调用GATT_Notifycation发起和主机通讯。

  4. 特征值声明值可以有五种属性:Read(可读) Write(可靠的可写,带响应) Write without resp(不可靠的可写,不带响应) Indicate(可靠通知,带响应) Notify(不可靠通知,不带响应)

更多请查看蓝牙设计问与答 /Android 蓝牙4.0 BLE 理解

BLE中主从机建立连接,到配对和绑定的过程如下图。

QPPS工程

在此之前,建议先了解一下Firefly的QPPS介绍/对QPPS profile中服务和特征实现的分析理解/对profile QPPS的分析理解。关于蓝牙的数据收发部分可以看Firefly的协议栈介绍。数据帧格式如下:

Tip:技术案例中串口透传案例,实现的是将蓝牙模组串口所接收到的数据透传到app,并且把app的数据通过蓝牙透传到串口输出,案例本身的实现是针对串口通信和蓝牙透传的结合。

  1. 其实BLE完成初始化的条件并不是进入main函数的while(1)中(系统在调度了几个消息之后才完成的初始化,前几次进入while(1)时初始化都是没完成的),真正完成初始化并且可以运行是在打印BLE is Ready这句话的地方。那个地方你可以看到只要开启QN_DEMO_AUTO宏定义就可以上电广播了。
  2. 设备名默认从NVDS中写入,但是软件也可以修改,可以用app_gap_set_devname_req修改设备名。
  3. 如果你非要用软件指定,参考广播内容设定,取消NVDS读取,直接利用app_gap_set_devname_req函数指定设备名,然后将QN_LOACL_NAME广播出去,那么设备名和广播中的设备名都会是QN_LOCAL_NAME了。

    具体实施如下

    修改自动广播,无需向上拨动按键:

    1
    2
    #=> App_config.h中,取消如下的注释即可
    #define QN_DEMO_AUTO 1

    在广播之前改变设备的名字,注意app_set_adv_data函数

    1
    2
    3
    4
    5
    6
    7
    8
    //Set remote device name
    app_gap_set_devname_req("nephen",6);
    // Created DB should has been finished by each profile service,
    // Start Adv mode automatically here
    app_gap_adv_start_req(GAP_GEN_DISCOVERABLE|GAP_UND_CONNECTABLE,
    app_env.adv_data, app_set_adv_data(GAP_GEN_DISCOVERABLE),
    app_env.scanrsp_data, app_set_scan_rsp_data(app_get_local_service_flag()),
    GAP_ADV_FAST_INTV1, GAP_ADV_FAST_INTV2);
  4. 接收手机app中9600可写属性发送的数据。
    由下面Qpps_task.c可知,当有数据到达蓝牙时,会触发gatt_write_cmd_ind_handler,同理,发送数据触发qpps_data_send_req_handler,得到通知触发gatt_notify_cmp_evt_handler,app层对应的api是app_qpps_data_send_cfm_handlerapp_qpps_data_ind_handlerapp_qpps_cfg_indntf_ind_handler,下文会提到,只会对这些做修改

    1
    2
    3
    4
    5
    6
    7
    /// Connected State handler definition.
    const struct ke_msg_handler qpps_connected[] =
    {
    {QPPS_DATA_SEND_REQ, (ke_msg_func_t) qpps_data_send_req_handler},
    {GATT_WRITE_CMD_IND, (ke_msg_func_t) gatt_write_cmd_ind_handler},
    {GATT_NOTIFY_CMP_EVT, (ke_msg_func_t) gatt_notify_cmp_evt_handler},
    };

    而在gatt_write_cmd_ind_handler函数里,会对QPPS_IDX_RX_DATA_VAL属性进行处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    else if (param->handle == (qpps_env.shdl + QPPS_IDX_RX_DATA_VAL))
    {
    if (param->length <= QPP_DATA_MAX_LEN)
    {
    //inform APP of configuration change
    struct qpps_data_val_ind * ind = KE_MSG_ALLOC_DYN(QPPS_DAVA_VAL_IND,
    qpps_env.appid,
    TASK_QPPS,
    qpps_data_val_ind, param->length);

    memcpy(&ind->conhdl, &(qpps_env.conhdl), sizeof(uint16_t));
    //Send received data to app value
    ind->length = param->length;
    memcpy(ind->data, param->value, param->length);

    ke_msg_send(ind);
    }
    else
    {
    status = QPPS_ERR_RX_DATA_EXCEED_MAX_LENGTH;
    }
    }

    再由上面的QPPS_DAVA_VAL_IND可知会跳到下面这个函数,在这里对接收到的数据进行处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    int app_qpps_data_ind_handler(ke_msg_id_t const msgid,
    struct qpps_data_val_ind *param,
    ke_task_id_t const dest_id,
    ke_task_id_t const src_id)
    {
    uint8_t i;
    if (param->length > 0)
    {
    QPRINTF("len=%d, I%02X", param->length, param->data[0]);
    QPRINTF("\r\n");
    QPRINTF("the receive data is :");
    for(i=0;i<param->length;i++)
    {
    QPRINTF("%x",param->data[i]);
    }
    }
    QPRINTF("\r\n");

    return (KE_MSG_CONSUMED);
    }

    当发送数据为29时,CuteCom蓝牙接收数据为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    QN BLE is ready.
    Set device name complete
    Advertising start.
    Connection with 1FDA7E9F8E30 result is 0x0.
    LTK request indication idx is 0, auth_req is 0.
    Start encryption complete, idx 0, status 0, key_size 0, sec_prop 1, bonded 0.
    Slave update success.
    Update parameter complete, interval: 0xc, latency: 0x0, sup to: 0x12c.
    len=1, I29
    the receive data is :29
  5. App收取蓝牙通知。通过点击app上5个特征的通知,能获取蓝牙模块的实时通知。Gatt层与通知相关的函数是qpps_data_send_req_handler,但我们只需要修改app开头的api即可完成相应的开发,现象及分析如下:
    当点击app上的通知时,会进入如下的函数,为了调试将其调整为如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    int app_qpps_cfg_indntf_ind_handler(ke_msg_id_t const msgid,
    struct qpps_cfg_indntf_ind *param,
    ke_task_id_t const dest_id,
    ke_task_id_t const src_id)
    {
    if (app_qpps_env->conhdl == param->conhdl)
    {
    QPRINTF("enter app_qpps_cfg_indntf_ind_handler");
    QPRINTF("\r\n");
    if (param->cfg_val == PRF_CLI_START_NTF)
    {
    QPRINTF("enter PRF_CLI_START_NTF");
    QPRINTF("\r\n");
    app_qpps_env->features |= (QPPS_VALUE_NTF_CFG << param->char_index);
    QPRINTF("app_qpps_env->features is %d, param->char_index is %d",app_qpps_env->features,param->char_index);
    QPRINTF("\r\n");
    // App send data if all of characteristic have been configured
    if (get_bit_num(app_qpps_env->features) == app_qpps_env->tx_char_num)//num is 5
    {
    QPRINTF("enter app_qpps_env->features");
    QPRINTF("\r\n");
    app_qpps_env->char_status = app_qpps_env->features;
    app_test_send_data(app_qpps_env->tx_char_num - 1);
    }
    }
    else
    {
    app_qpps_env->features &= ~(QPPS_VALUE_NTF_CFG << param->char_index);
    app_qpps_env->char_status &= ~(QPPS_VALUE_NTF_CFG << param->char_index);
    }
    }

    return (KE_MSG_CONSUMED);
    }

    其中,param->char_index代表第几个可通知特征,app_qpps_env->tx_char_num为特征的数量5,只有当所有特征都进入通知状态时蓝牙模块才会发数据到手机app。

    数据发送的过程见如下函数,这个函数中的数据val可以改为项目中的需求,如IO口状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    static void app_test_send_data(uint8_t max)
    {
    uint8_t cnt;

    QPRINTF("enter app_test_send_data");
    QPRINTF("\r\n");
    for (cnt = 0; (max != 0) && cnt < app_qpps_env->tx_char_num; cnt++)
    {
    if ((app_qpps_env->char_status >> cnt) & QPPS_VALUE_NTF_CFG)
    {
    static uint8_t val[] = {0, '0', '1', '2','3','4','5','6','7','8','9','8','7','6','5','4','3','2','1','0'};

    // Increment the first byte for test
    val[0]++;

    max--;
    // Allow next notify until confirmation received in this characteristic
    app_qpps_env->char_status &= ~(QPPS_VALUE_NTF_CFG << cnt);
    app_qpps_data_send(app_qpps_env->conhdl, cnt, sizeof(val), val);
    }
    }
    }

    数据发送后,有一个发送确认函数app_qpps_data_send_cfm_handler,发送成功后手机上看到的现象是这样的

至此完成的功能为,实时反应蓝牙通知信息,app发数据控制蓝牙模块。


QTool使用

QTool是一个运行在PC端的应用软件,允许用户启动两个BLE设备之间的连接,它能帮助用户分析BLE蓝牙。它是通过串口与BLE设备进行通信,通过ACI命令扮演网络处理器的作用。

打开pdf使用文档

1
2
~ $ cd /home/nephne/.wine/drive_c/QBlue/QN9020/QBlue-1.3.7/Documents
~ $ evince QBlue\ ISP\ Studio\ Manual\ v1.0.pdf

插入USB连接蓝牙模块,下载np_controller_B2.bin,然后打开QTool进入设备连接。关于界面说明见系统文档。

例如:点击Setting-Server-QPPS里的Create DB,然后进入Setting-Mode里点击Advertising,即可实现如上QPPS工程的效果。而在旁边的Local Device Traces窗口可以看到程序的大概运行过程。


参考文档

nephen wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!