Realtek:ble profile架构概览
1 bluetooth 蓝牙协议
什么是蓝牙呢?蓝牙是频率为2.4GHz的特高频无线通讯标准;按协议制定的时间将其分为两种类型;
1.1 经典蓝牙BT:以点对点方式创建一对一通信;使用蓝牙3.0标准协议;
1.2 低功耗蓝牙BLE :以广播(一对多)和网格(多对多)等通信; 使用蓝牙4.0标准协议,兼容3.0;
2 ( C/S client server )架构
c/s架构通信的双方总是由client客户端的主机发起,server服务端的从机响应;gatt协议使用c/s架构;
2.1 client客户端: 使用数据服务的设备,即主机设备;
2.2 server服务端:提供数据服务的设备,即从机设备; 数据服务:即service服务;
2.3 gatt协议传输
attribute指令在发送出去之后,会一直等待直到收到回复的ack包,直到超时或断开ble;杜绝了空中丢包的可能性;
attribute指令可以选择request/ack的模式,也可以选择request/respond的模式;
3 蓝牙协议栈架构
蓝牙协议栈主要分为三层;
其中GAP协议后文介绍了其广播包部分,其中GATT协议后文介绍了其attribute table部分;application层等价于profile协议部分;
3.1 controller层
由芯片厂编写的硬件层,包括射频电路,将HCI数据按空中包的格式调制解调的链路层,以及规范通信的hci接口;
3.2 host层
由各公司配置的驱动层,用于连接硬件与应用层的通信,需要配置的主要是配置gatt和gap;
3.2.1 GAP generic access profile 通用连接协议
当从机还未建立连接的时候,通过GAP协议单向向外广播数据,该协议通常由芯片产商写好;
从机广播间隔约20ms-10s,主机扫描间隔约2.5ms-10s;广播间隔越长越省电,同时也不容易被扫描到;
3.2.2 GATT generic attribute profile 通用属性协议
当从机和主机建立一对一连接,通过GATT协议进行通信,该协议结构即后文的attribute profile;
GATT连接属于一对一连接,从机和主机建立连接之后,就会停止向外广播使得对其他设备不可见;当设备断开后设备又开始广播;
3.3 application层
由各公司编写的的应用层,每个代码功能都可视为profile;蓝牙联盟也定义了相关功能的标准profiles,看情况使用;
4 广播数据
4.1 广播包 adv_data[ ]
广播包的主要作用是用来广播蓝牙设备名称,以及广播部分需要广播的类型数据;
广播包的数据单元可以切分为[ length、type、data ]的格式进行广播,广播包的数据长度最大31bytes;
4.1.1 type类型
//gap_le_types.h 广播包type类型 #define GAP_ADTYPE_FLAGS 0x01 //!< The Flags data type contains one bit Boolean flags. Please reference @ref ADV_TYPE_FLAGS for details. #define GAP_ADTYPE_16BIT_MORE 0x02 //!< Service: More 16-bit UUIDs available #define GAP_ADTYPE_16BIT_COMPLETE 0x03 //!< Service: Complete list of 16-bit UUIDs #define GAP_ADTYPE_32BIT_MORE 0x04 //!< Service: More 32-bit UUIDs available #define GAP_ADTYPE_32BIT_COMPLETE 0x05 //!< Service: Complete list of 32-bit UUIDs #define GAP_ADTYPE_128BIT_MORE 0x06 //!< Service: More 128-bit UUIDs available #define GAP_ADTYPE_128BIT_COMPLETE 0x07 //!< Service: Complete list of 128-bit UUIDs #define GAP_ADTYPE_LOCAL_NAME_SHORT 0x08 //!< Shortened local name #define GAP_ADTYPE_LOCAL_NAME_COMPLETE 0x09 //!< Complete local name #define GAP_ADTYPE_POWER_LEVEL 0x0A //!< TX Power Level: 0xXX: -127 to +127 dBm #define GAP_ADTYPE_OOB_CLASS_OF_DEVICE 0x0D //!< Simple Pairing OOB Tag: Class of device (3 octets) #define GAP_ADTYPE_OOB_SIMPLE_PAIRING_HASHC 0x0E //!< Simple Pairing OOB Tag: Simple Pairing Hash C (16 octets) #define GAP_ADTYPE_OOB_SIMPLE_PAIRING_RANDR 0x0F //!< Simple Pairing OOB Tag: Simple Pairing Randomizer R (16 octets) #define GAP_ADTYPE_SM_TK 0x10 //!< Security Manager TK Value #define GAP_ADTYPE_SM_OOB_FLAG 0x11 //!< Secutiry Manager OOB Flags #define GAP_ADTYPE_SLAVE_CONN_INTERVAL_RANGE 0x12 //!< Min and Max values of the connection interval (2 octets Min, 2 octets Max) // (0xFFFF indicates no conn interval min or max) #define GAP_ADTYPE_SIGNED_DATA 0x13 //!< Signed Data field #define GAP_ADTYPE_SERVICES_LIST_16BIT 0x14 //!< Service Solicitation: list of 16-bit Service UUIDs #define GAP_ADTYPE_SERVICES_LIST_128BIT 0x15 //!< Service Solicitation: list of 128-bit Service UUIDs #define GAP_ADTYPE_SERVICE_DATA 0x16 //!< Service Data #define GAP_ADTYPE_PUBLIC_TGT_ADDR 0x17 //!< Public Target Address #define GAP_ADTYPE_RANDOM_TGT_ADDR 0x18 //!< Random Target Address #define GAP_ADTYPE_APPEARANCE 0x19 //!< Appearance #define GAP_ADTYPE_ADV_INTERVAL 0x1A //!< Advertising Interval #define GAP_ADTYPE_LE_BT_ADDR 0x1B //!< LE Bluetooth Device Address #define GAP_ADTYPE_LE_ROLE 0x1C //!< LE Role #define GAP_ADTYPE_SP_HASH_C256 0x1D //!< Simple Pairing Hash C-256 #define GAP_ADTYPE_SP_RAND_R256 0x1E //!< Simple Pairing Randomizer R-256 #define GAP_ADTYPE_LIST_32BIT_SILI 0x1F //!< List of 32-bit Service Solicitation UUIDs #define GAP_ADTYPE_SERVICE_DATA_32BIT 0x20 //!< Service Data - 32-bit UUID #define GAP_ADTYPE_SERVICE_DATA_128BIT 0x21 //!< Service Data - 128-bit UUID #define GAP_ADTYPE_SC_CONF_VALUE 0x22 //!< LE Secure Connections Confirmation Value #define GAP_ADTYPE_SC_RAND_VALUE 0x23 //!< LE Secure Connections Random Value #define GAP_ADTYPE_URI 0x24 //!< URI #define GAP_ADTYPE_INDOOR_POSITION 0x25 //!< Indoor Positioning #define GAP_ADTYPE_TRANSPORT_DISCOVERY_DATA 0x26 //!< Transport Discovery Data #define GAP_ADTYPE_LE_SUPPORTED_FEATURES 0x27 //!< LE Supported Features #define GAP_ADTYPE_CHAN_MAP_UPDATE_IND 0x28 //!< Channel Map Update Indication #define GAP_ADTYPE_MESH_PB_ADV 0x29 //!< Mesh Pb-Adv #define GAP_ADTYPE_MESH_PACKET 0x2A //!< Mesh Packet #define GAP_ADTYPE_MESH_BEACON 0x2B //!< Mesh Beacon #define GAP_ADTYPE_3D_INFO_DATA 0x3D //!< 3D Information Data #define GAP_ADTYPE_MANUFACTURER_SPECIFIC 0xFF //!< Manufacturer Specific Data: first 2 octets contain the Company Identifier Code // followed by the additional manufacturer specific data // type=ADV_TYPE_FLAGS,data如下: #define GAP_ADTYPE_FLAGS_LIMITED 0x01 //!< Discovery Mode: LE Limited Discoverable Mode #define GAP_ADTYPE_FLAGS_GENERAL 0x02 //!< Discovery Mode: LE General Discoverable Mode #define GAP_ADTYPE_FLAGS_BREDR_NOT_SUPPORTED 0x04 //!< Discovery Mode: BR/EDR Not Supported #define GAP_ADTYPE_FLAGS_SIMULTANEOUS_LE_BREDR_CONTROLLER 0x08 //!< Discovery Mode: Simultaneous LE and BR/EDR Controller Supported #define GAP_ADTYPE_FLAGS_SIMULTANEOUS_LE_BREDR_HOST 0x10 //!< Discovery Mode: Simultaneous LE and BR/EDR Host Supported
4.1.2 广播包
// GAP Advertisement data (max size = 31 bytes,best kept short to conserve power while advertisting) SHM_DATA_SECTION uint8_t adv_data[] ={ /* Core spec. Vol. 3, Part C, Chapter 18 */ /* Flags */ /* place holder for Local Name, filled by BT stack. if not present */ /* BT stack appends Local Name. */ 0x02, //length GAP_ADTYPE_FLAGS, //type GAP_ADTYPE_FLAGS_GENERAL , //data // 0x03, //length // GAP_ADTYPE_16BIT_MORE, //type:16bit uuid // 0x59, //data // 0xD4, 0x0F //length 0x09, //type:Complete local name 'C', 'M', 'B', '5', '9', '0', '9', '6', '8', '-', 'X', 'X', 'X', 'X',//data 0x08, //length 0xFF, //type:Manufacture specified data 'L','B', //data:company identifier 0x20,0x15,0x09,0x14,0x14, //data:mac addr };
4.2 广播回复包
//main.c GAP - SCAN RSP data (max size = 31 bytes) 数据格式同广播包一样 SHM_DATA_SECTION static uint8_t scan_rsp_data[] ={ 0x0F, //length 0x09, //type 'C', 'M', 'B', '5', '9', '0', '9', '6', '8', '-', 'X', 'X', 'X', 'X',//data };
4.3 广播包初始化
//main.c void app_le_gap_init(void){ //... 广播包adv_data[] 和广播回复包scan_rsp_data[]的初始化 le_adv_set_param(GAP_PARAM_ADV_DATA, sizeof(adv_data), (void *)adv_data); le_adv_set_param(GAP_PARAM_SCAN_RSP_DATA, sizeof(scan_rsp_data), (void *)scan_rsp_data); } //main.c static void app_bt_gap_init(void){ //... app_le_gap_init(); } //src\app\watch\watch_app\main.c int main(void){ //... app_bt_gap_init(); }
5 ATT attribute 协议
5.1 attribute 数据段
attribute数据段格式如下,是后续提供给gatt使用的服务数据源;gatt将其归纳为gatt的格式同应用层交互;
attribute handle:由蓝牙底层生成,是ATT PDU(protocol data unit)的句柄,不是代码中的attrib_index;
属性句柄虽然也起到标识和管理作用,但不是通道索引序号,这属性句柄体现在什么代码中呢?先放着;
5.2 attribute 结构体
5.2.1 attribute 结构体
//gatt.h //brief GATT attribute definition. profile中通过 type_value 来区分通道的,具体逻辑边界先放着; typedef struct { uint16_t flags; // flags uint8_t type_value[2 + 14]; // UUID charecteristic 特征值属性 uint16_t value_len; // Length of value void *p_value_context; // service data addr uint32_t permissions; // 属性权限,可以组合属性; } T_ATTRIB_APPL;
5.2.2 flags 属性
// flags:标识service的其他属性,比如UUID长度、支持广播、CCCD使能、指示等;不同蓝牙厂商的功能应该会有所不同; #define ATTRIB_FLAG_VOID 0x0000 /**< Attribute value neither supplied by application nor included following 16bit UUID. Attribute value is pointed by p_value_context and value_len shall be set to the length of attribute value. */ #define ATTRIB_FLAG_UUID_128BIT 0x0001 /**< Attribute uses 128 bit UUID */ #define ATTRIB_FLAG_VALUE_INCL 0x0002 /**< Attribute value is included following 16 bit UUID */ #define ATTRIB_FLAG_VALUE_APPL 0x0004 /**< Application has to supply write value */ #define ATTRIB_FLAG_ASCII_Z 0x0008 /**< Attribute value is ASCII_Z string */ #define ATTRIB_FLAG_CCCD_APPL 0x0010 /**< Application will be informed about CCCD value is changed */ #define ATTRIB_FLAG_CCCD_NO_FILTER 0x0020 /**< Application will be informed about CCCD value when CCCD is write by client, no matter it is changed or not */ #define ATTRIB_FLAG_INCLUDE_MULTI 0x0040 #define ATTRIB_FLAG_LE 0x0800 /**< Used only for primary service declaration attributes if GATT over BLE is supported */ #define ATTRIB_FLAG_BREDR 0x0400 /**< Used only for primary service declaration attributes if GATT over BR/EDR is supported */
5.2.3 permissions 属性
//permissions:标识service的访问属性,比如读写,加密读写、认证读写、签名读写 #define GATT_PERM_NONE 0x00 #define GATT_PERM_ALL 0x01 /**< bits 0..1 (rd), 4..5 (wr), 8..9 (notif/ind) */ #define GATT_PERM_AUTHEN_REQ 0x02 #define GATT_PERM_AUTHEN_MITM_REQ 0x03 #define GATT_PERM_AUTHOR_REQ 0x04 /**< bits 2 (rd), 6 (wr), 10 (notif/ind) */ #define GATT_PERM_ENCRYPTED_REQ 0x08 /**< bits 3 (rd), 7 (wr), 11 (notif/ind) */ #define GATT_PERM_AUTHEN_SC_REQ 0x00010000 #define GATT_PERM_READ GATT_PERM_ALL #define GATT_PERM_READ_AUTHEN_GET(x) (x & 0x03) #define GATT_PERM_WRITE (GATT_PERM_ALL << 4) #define GATT_PERM_WRITE_AUTHEN_GET(x) ((x >> 4) & 0x03) #define GATT_PERM_NOTIF_IND (GATT_PERM_ALL << 8) #define GATT_PERM_NOTIF_IND_AUTHEN_GET(x) ((x >> 8) & 0x03) //... //flags和permissions具有关联性,没有因果性;比如ATTRIB_FLAG_VALUE_APPL flag会有notify permission; //上面这句话真是总结的太准确了,我真是小聪明;
5.2.4 characteristic属性
//这里还有一个相关概念老是在网文中出现的,那就是characteristic特征值属性; //是用于描述characteristic的访问权限;说句大白话就是用于描述特征值的permissions的;为什么要费这劲再定义呢?先放着; //write属性: 该从机可以被写入 //read属性: 该从机可以被读取 //notify属性: 该从机可以主动发送通知消息; //authenticated属性: 该从机需要验证才能读写 //虽然不是蓝牙规范的标准用法,但是notify通知也可以用来实现service指令传输;
5.2.5 uuid 通用唯一标识码
UUID: universally unique identifier 通用唯一标识码,通过不同的uuid来区分不同的service服务;
蓝牙技术联盟定义基本的UUID标识码如右:0000xxxx-0000-1000-8000-00805F9B34FB;
蓝牙技术联盟定义基本的UUID标识码中xxxx为属性的uuid;使用uuid标识service的characteristic属性;
蓝牙技术联盟定义基本的UUID标识码只是官方提供的,实际上多数公司都会重新定义自己的UUID特征值通道;
蓝牙广播的中uuid格式:xxxx,0000xxxx,0000xxxx-0000-1000-8000-00805F9B34FB;2bytes、4bytes、16bytes;
5.3 attribute table
//wristband_private_service.c const T_ATTRIB_APPL bwps_service_tbl[] ={ { // attrib_index 0 (ATTRIB_FLAG_VOID | ATTRIB_FLAG_LE), //flag:0x0800, 仅用于primary service; { LO_WORD(GATT_UUID_PRIMARY_SERVICE), //uuid:0x2800 HI_WORD(GATT_UUID_PRIMARY_SERVICE), }, UUID_16BIT_SIZE, //valueLength (void *)GATT_UUID16_BWPS, //pvalue:&[0x59,0xd4] //这个是service的uuid,后面的几个都是这个service的characteristic; //添加service就是重新添加一个当前attrib_index和后面几个特征值的attrib_index; GATT_PERM_READ //permission }, { // attrib_index 1 ATTRIB_FLAG_VALUE_INCL, //flag:0x0002, 使用16bit uuid { LO_WORD(GATT_UUID_CHARACTERISTIC), //uuid:0x2803 HI_WORD(GATT_UUID_CHARACTERISTIC), GATT_CHAR_PROP_WRITE|GATT_CHAR_PROP_WRITE_NO_RSP, //??? }, 1, //valueLength NULL, //pvalue GATT_PERM_READ //permission }, { // attrib_index 2 ATTRIB_FLAG_VALUE_APPL, //flag:0x0004, 支持写数据 { LO_WORD(BLE_UUID_CM_TX_CHARACTERISTIC), //uuid:0x0013 HI_WORD(BLE_UUID_CM_TX_CHARACTERISTIC), }, 0, //valueLength NULL, //pvalue GATT_PERM_WRITE //permission }, // attrib_index 3 ... { // attrib_index 4 ATTRIB_FLAG_VALUE_APPL, //flag:0x0004, 支持写数据 { LO_WORD(BLE_UUID_CM_RXN_CHARACTERISTIC), //uuid:0x0014 HI_WORD(BLE_UUID_CM_RXN_CHARACTERISTIC), }, 0, //valueLength NULL, //pvalue GATT_PERM_NOTIF_IND //permission }, { // attrib_index 5 (ATTRIB_FLAG_VALUE_INCL | ATTRIB_FLAG_CCCD_APPL), { LO_WORD(GATT_UUID_CHAR_CLIENT_CONFIG), //uuid:0x2902 HI_WORD(GATT_UUID_CHAR_CLIENT_CONFIG), LO_WORD(GATT_CLIENT_CHAR_CONFIG_DEFAULT), HI_WORD(GATT_CLIENT_CHAR_CONFIG_DEFAULT) }, 2, //valueLength NULL, //pvalue (GATT_PERM_READ | GATT_PERM_WRITE) //permission }, // attrib_index 6 ... { // attrib_index 7 ATTRIB_FLAG_VALUE_APPL, //flag { LO_WORD(BLE_UUID_CM_RXC_CHARACTERISTIC), //uuid:0x0015 HI_WORD(BLE_UUID_CM_RXC_CHARACTERISTIC), }, 0, //valueLength NULL, //pvalue GATT_PERM_NOTIF_IND //permission }, }
6 profile
蓝牙规范冗余繁杂,所以蓝牙设备通常只实现所需协议;这部分协议好比蓝牙规范的剪影,所以称为profile;
每个attribute_table可视为一个profile协议,每个profile协议中包含多个service服务;每个service服务中包含多个characteristic特征值服务;
这些 service 和 characteristic 体现在bwps_service_tbl 中都是一条service服务,都有自己的attrib_index id;是并列存在而非包含的关系;
6.1 私有协议 profile 举例
对于当前sdk而言,其中包含了LB私有profile、CM私有profile,两个私有协议的profile;
此处以LB私有profile为例来分析,将实现代码与profile定义对应上;
私有profile协议的特征值service为ffb0,ffb0 service的特征值有ffb1,ffb2,ffb3,ffb4,ffb6;
6.2 私有协议 profile 流程
profile是如何通过service和characteristics来接收数据服务的呢?
通信由提前约定好service和characteristics,在bwps_service_tbl中为其添加对应的 T_ATTRIB_APPL attrib_index数据服务;
设备接收到数据之后,characteristic会调用对应permissions的回调函数;
回调函数中通过判断attrib_index来区分如何传递服务数据的何去何从;
根据代码推断出来的逻辑大致如上;那么是否可以根据特征值来反推attrib_index呢?它们通过permissions联系起来没法反推;
6.3 常见 service
除去传输数据的service attribute之外,还要一些数据服务由协议栈实现,概述部分如下,具体先放着;
6.3.1 uuid 1800 service attribute
GAP Generic Access服务,为必须service,包含characteristic:2a00、2a01、2a04、2a06;
包含device name;appearance;PPCP;CAR等characteristic;由GAP协议层实现的;
6.3.2 uuid 1801 service attribute
GATT Generic attribute,为必须service,包含characteristic:2a05、2902、2b3a;
包含service change等characteristic;由GAP协议层实现的;
6.3.3 uuid 2902 characteristic attribute;
CCCD使能:server允许发送notify或indicate类型的characteristic给client;
CCCD禁能:server禁止发送notify或indicate类型的characteristic给client;
所以如果蓝牙有notify或者indicate特征值的功能,那么就应该添加CCCD 特征值attribute
7 realtek sdk ble service初始化
以下是瑞昱sdk中对gatt协议的初始化逻辑流程,本文则是对如下代码进行的内容扩展;
//main.c int main(void){ //... app_ble_service_init(); } //app_ble_service.c //注册4个gatt service服务,然后返回注册id,通过service id来标识不同的服务; //这个需要看数据手册和sdk文档,相关资料我都没有也不知道怎么找,先放着; //先以sdk处理的profile数据源的service为例继续往下走; void app_ble_service_init(void){ server_init(4); wristband_ser_id = bwps_service_add_service((void *)app_profile_callback);//sdk data src profile service; ota_ser_id = ota_add_service((void *)app_profile_callback); //xx? profile service dfu_ser_id = dfu_add_service((void *)app_profile_callback); //xx? profile service hid_kb_ser_id = hids_add_service((void *)app_profile_callback); //xx? profile serveice //理论上这里应该给每个service都注册自己的回调函数的,但是下面用同一个回调函数来注册;所以所有service都在这个模块中处理了; server_register_app_cb(app_profile_callback); client_init(2); ancs_init(1); ams_init(1); } //wristband_private_service.c //seivice的attribute table >> bwps_service_tbl[] const T_ATTRIB_APPL bwps_service_tbl[] ={ //...前文4.3小节attribute table } //seivice的回调函数 >> bwps_service_cbs const T_FUN_GATT_SERVICE_CBS bwps_service_cbs = { bwps_service_attr_read_cb, // Read callback function pointer bwps_service_attr_write_cb, // Write callback function pointer bwps_service_cccd_update_cb // CCCD update callback function pointer //*** 重写这三个函数作为应用层profile数据的处理开始;*** }; //wristband_private_service.c //service map>> bwps_service_tbl[]、bwps_service_cbs; //所以这里的回调函数应该是由蓝牙底层驱动中断来触发的回调处理; uint8_t bwps_service_add_service(void *app_profile_callback){ uint8_t bwps_service_id; server_add_service(&bwps_service_id, (uint8_t *)bwps_service_tbl, sizeof(bwps_service_tbl), bwps_service_cbs); pfn_wristband_service_cb = (P_FUN_SERVER_GENERAL_CB)app_profile_callback; return bwps_service_id; } //wristband_private_service.c //回调系列之 write callback sample; T_APP_RESULT bwps_service_attr_write_cb(uint8_t conn_id, T_SERVER_ID service_id, uint16_t attrib_index, T_WRITE_TYPE write_type, uint16_t length, uint8_t *p_value, P_FUN_WRITE_IND_POST_PROC *p_write_ind_post_proc) { //这个attrib_index是attrib_table中的序号,没有直接对应上,推导可知; switch(attrib_index){ case GATT_ATTRIB_INDEX_BG_DATA: memcpy(receive_temp + 1, p_value, length); //...大数据接收 break; case GATT_ATTRIB_INDEX_AT_DATA: memcpy(receive_at_temp + 1, p_value, length); //...at指令接收,at指令设置完毕之后直接往通道回复,没有发送msg; break; } } //数据接收完毕后,send cmd msg queue to l1send_queue_handle; //另起一应用层task,阻塞等待 recv msg queue from l1send_queue_handle; //因为使用的是阻塞等待,所以没收到msg的话task就会一直被挂起,这样就不会占用资源,这个构思挺巧的;
7.1 接收数据后续的应用层task处理逻辑补充,即profile具体代码;
//sdk\LW107-10701\src\app\watch\watch_app\main.c int main(void){ //... task_init(); } static void task_init(void){ //... communicate_task_init(); } //sdk\LW107-10701\src\app\watch\communicate\communicate_task.c //l1send_task线程创建; void communicate_task_init(){ os_task_create(&l1send_task_handle, "l1send", l1send_task, 0, L1SEND_TASK_STACK_SIZE, L1SEND_TASK_PRIORITY); } //l1send_task线程:创建了消息队列 l1send_queue_handle; void l1send_task(void *pvParameters){ //参数名称:os_msg_queue_create(pp_handle, p_name, msg_num, msg_size); os_msg_queue_create(&l1send_queue_handle, "l1sendQ", 0x10, sizeof(uint16_t)); while (true){ //while(true)搭配永久阻塞等待,觉得哪里怪怪的,但是又十分自洽。。。==! if (os_msg_recv(l1send_queue_handle, &event, 0xFFFFFFFF) == true){ switch(event){ case CMD_xx: cp_xx(); //cp_xx()处理完数据后,调用sendRespondPacket(NB_RET_CMD rs); break; //... } } } }
8 小节
本文结构由上至下为循序渐进,从下往上查阅则方便追踪;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具