翻译原文链接:https://lupyuen.github.io/articles/wifi
文章翻译总结:文章分析了BL602的WiFi演示固件源码,涵盖了WiFi事件处理、启动WiFi固件任务、连接WiFi热点等过程,通过GitHub代码搜索,找到了与BL602 WiFi驱动匹配的开源代码,包括RivieraWaves UMAC和LMAC代码、WiFi认证代码以及部分物理层代码,深入分析了BL602的WiFi驱动工作流程,并找到了大量可参考的开源代码。
文章相关标签:#RivieraWaves、#FreeRTOS、#物理层、#GitHub Search、#UMAC&LMAC
逆向WiFi驱动板 RISC-V BL602
[TOC]
今天,我们对 BL602 RISC-V 系统芯片上的 WiFi 驱动程序进行逆向工程,并了解内部发生的情况……在找到的(不完整)驱动程序源代码的指导下进行。
为什么要对 BL602 W(See this non-BL602 example)iFi 驱动程序进行逆向工程?
- 教育:掌握在BL602(一种芯片型号)上WiFi数据包的传输和接收过程。
- 故障排除:当WiFi驱动程序出现问题时,我们应该能够定位并解决问题。(或许还能进行修复!)
- 审计:确保WiFi数据包的传输和接收既正确又安全。(可参考非BL602的相关例子)
- 替换:未来,我们可能用开源驱动程序(如Openwifi)替换现有的闭源WiFi驱动程序
https://twitter.com/Yu_Wei_Wu/status/1406940637773979655?s=19
接下来开始一起阅读并开始学习逆向工程吧。
一、BL602 WiFi 固件演示demo
让我们一起来研究BL602物联网软件开发套件中的BL602 WiFi演示固件源代码:bl602_demo_wifi
在演示固件中,我们将执行以下操作:
- 注册处理WiFi事件的事件处理程序
- 启动控制BL602 WiFi固件的任务
- 启动管理WiFi连接状态的任务
- 连接到WiFi**接入点**
- 发送HTTP请求
1.1 注册WiFi事件处理
固件启动时,我们将注册一个处理WiFi事件的回调函数:main.c
// 启动时调用以初始化驱动程序并运行事件循环
static void aos_loop_proc(void *pvParameters) {
// 省略部分: 初始化驱动程序
...
// 为WiFi事件注册回调函数
aos_register_event_filter(
EV_WIFI, // Event Type
event_cb_wifi_event, // Event Callback Function
NULL); // Event Callback Argument
// 启动WiFi网络堆栈
cmd_stack_wifi(NULL, 0, 0, NULL);
// 运行事件循环
aos_loop_run();
}
(我们稍后会讨论event_cb_wifi_event变量) 启动代码中,通过调用cmd_stack_wifi来启动WiFi网络堆栈。 现在,让我们深入了解其内部实现…
1.2 启动WiFi固件管理
在cmd_stack_wifi函数中,我们按照如下方式启动WiFi固件任务:main.c
// 启动WiFi网络堆栈
static void cmd_stack_wifi(char *buf, int len, int argc, char **argv) {
// Check whether WiFi Networking is already started
static uint8_t stack_wifi_init = 0;
if (1 == stack_wifi_init) { return; } // Already started
stack_wifi_init = 1;
// 启动WiFi固件任务 (FreeRTOS)
hal_wifi_start_firmware_task();
// 发布WiFi事件以启动WiFi管理器任务
aos_post_event(
EV_WIFI, // Event Type
CODE_WIFI_ON_INIT_DONE, // Event Code
0); // Event Argument
}
(本文稍后将讨论hal_wifi_start_firmware_task函数) 任务启动后,我们触发WiFi事件CODE_WIFI_ON_INIT_DONE,以便启动WiFi管理任务。 现在,让我们探究WiFi事件处理程序的内部工作原理…
1.3 启动WiFi管理任务
这里是处理WiFi事件的方法:main.c
// WiFi事件的回调功能
static void event_cb_wifi_event(input_event_t *event, void *private_data) {
// 处理WiFi事件
switch (event->code) {
// 由cmd_stack_wifi发布以启动Wi-Fi管理器任务
case CODE_WIFI_ON_INIT_DONE:
// 启动WiFi管理器任务 (FreeRTOS)
wifi_mgmr_start_background(&conf);
break;
// 省略部分: 处理其他WiFi事件
当接收到WiFi事件CODE_WIFI_ON_INIT_DONE时,我们会调用wifi_mgmr_start_background启动WiFi管理任务(在FreeRTOS中)。 wifi_mgmr_start_background函数是由BL602 WiFi驱动程序提供的。(请参考源代码)
1.4 连接到WiFi网络
既然我们已经启动了WiFi固件任务和WiFi管理任务这两个后台任务,接下来让我们连接到一个WiFi网络! 通过演示固件,我们可以输入特定的命令来连接到WiFi**接入点**…
wifi_sta_connect YOUR_WIFI_SSID YOUR_WIFI_PASSWORD
这里是wifi_sta_connect命令的实现方法:main.c
// 连接到WiFi接入点
static void wifi_sta_connect(char *ssid, char *password) {
// 启用WiFi客户端
wifi_interface_t wifi_interface
= wifi_mgmr_sta_enable();
// 连接到WiFi接入点
wifi_mgmr_sta_connect(
wifi_interface, // WiFi Interface
ssid, // SSID
password, // Password
NULL, // PMK
NULL, // MAC Address
0, // Band
0); // Frequency
}
我们通过调用BL602 WiFi驱动程序提供的wifi_mgmr_sta_enable函数来激活WiFi客户端功能。
(“STA”代表“WiFi站点”,也就是WiFi客户端)
接着,我们通过调用BL602 WiFi驱动程序中的wifi_mgmr_sta_connect函数来连接到WiFi**接入点**。
(下一章我们将深入探讨wifi_mgmr_sta_connect函数的内部机制)
1.5 发送HTTP请求
现在,我们可以输入特定的命令来通过WiFi发送**HTTP请求**…
httpc
这里是httpc命令的实现代码:main.c
// 使用LWIP发送HTTP GET请求
static void cmd_httpc_test(char *buf, int len, int argc, char **argv) {
// 检查HTTP请求是否已在运行
static httpc_connection_t settings;
static httpc_state_t *req;
if (req) { return; } // 请求已在运行
// 初始化LWIP HTTP设置
memset(&settings, 0, sizeof(settings));
settings.use_proxy = 0;
settings.result_fn = cb_httpc_result;
settings.headers_done_fn = cb_httpc_headers_done_fn;
// 使用LWIP发送HTTP GET请求
httpc_get_file_dns(
"nf.cr.dandanman.com", // Host
80, // Port
"/ddm/ContentResource/music/204.mp3", // URI
&settings, // Settings
cb_altcp_recv_fn, // Callback Function
&req, // Callback Argument
&req); // Request
}
在BL602上,我们采用LWIP(轻量级IP堆栈)来实现IP、UDP、TCP和HTTP网络功能。 httpc_get_file_dns的详细文档可以在此查阅。 想要获取更多关于BL602 WiFi演示固件的信息,请参阅相关文档…
- BL602 WiFi演示固件文档
让我们一起逆向分析BL602 WiFi演示固件… 探索其内部的工作原理!
https://www.nongnu.org/lwip/2_1_x/index.html
https://pine64.github.io/bl602-docs/Examples/demo_wifi/wifi.html
二、连接到WiFi接入点
BL602连接到WiFi接入点时,实际发生了什么过程?
为了深入了解BL602如何连接WiFi接入点,我们将研读BL602 WiFi驱动程序的**源代码**。
让我们观察在连接过程中会发生哪些变化…
- 向WiFi管理任务发起连接请求
- 通过WiFi管理器的状态机来处理该连接请求
- 将连接请求传递给WiFi硬件(LMAC)
- 激发LMAC中断以执行连接操作
2.1 向WiFi管理任务发送请求
我们之前使用wifi_mgmr_sta_connect函数来连接到WiFi接入点。 这里是该函数内部的工作原理:wifi_mgmr_ext.c
// 连接到WiFi接入点
int wifi_mgmr_sta_connect(wifi_interface_t *wifi_interface, char *ssid, char *psk, char *pmk, uint8_t *mac, uint8_t band, uint16_t freq) {
// 设置WiFi SSID和PSK
wifi_mgmr_sta_ssid_set(ssid);
wifi_mgmr_sta_psk_set(psk);
// 连接到WiFi接入点
return wifi_mgmr_api_connect(ssid, psk, pmk, mac, band, freq);
}
我们首先设定了WiFi的SSID和PSK。接着,我们通过调用wifi_mgmr_api_connect函数来连接到WiFi接入点。
wifi_mgmr_api_connect函数的具体实现如下所示:wifi_mgmr_api.c
// 连接到WiFi接入点
int wifi_mgmr_api_connect(char *ssid, char *psk, char *pmk, uint8_t *mac, uint8_t band, uint16_t freq) {
// 省略部分: 复制PSK、PMK、MAC地址、频带和频率
...
// 向WiFi管理器任务发送连接请求
wifi_mgmr_event_notify(msg);
return 0;
}
在此,我们通过调用wifi_mgmr_event_notify函数,将连接**请求发送**给WiFi管理任务。
wifi_mgmr_event_notify函数的定义位于wifi_mgmr.c文件中…
// 向WiFi管理器任务发送请求
int wifi_mgmr_event_notify(wifi_mgmr_msg_t *msg) {
// 省略部分: 等待WiFi管理器启动
...
// 通过消息队列向WiFi管理器发送请求
if (os_mq_send(
&(wifiMgmr.mq), // Message Queue
msg, // Request Message
msg->len)) { // Message Length
// 发送请求失败
return -1;
}
return 0;
}
os_mq_send函数是如何将请求发送给WiFi管理任务的? os_mq_send函数通过调用FreeRTOS,将请求消息发送到WiFi管理器的**消息队列**中:os_hal.h
#define os_mq_send(mq, msg, len) \
(xMessageBufferSend(mq, msg, len, portMAX_DELAY) > 0 ? 0 : 1)
2.2 WiFi管理状态机
WiFi管理器在其后台任务(FreeRTOS)中运行着一个状态机,用来管理每个WiFi连接的状态。 当WiFi管理器接收到我们发出的连接到WiFi接入点的请求时,会发生什么? 让我们深入wifi_mgmr.c文件来一探究竟…
// 当WiFi管理器接收到连接请求时调用
static void stateIdleAction_connect( void *oldStateData, struct event *event, void *newStateData) {
// 为连接请求设置WiFi配置文件
wifi_mgmr_msg_t *msg = event->data;
wifi_mgmr_profile_msg_t *profile_msg = (wifi_mgmr_profile_msg_t*) msg->data;
profile_msg->ssid_tail[0] = '\0';
profile_msg->psk_tail[0] = '\0';
// 记住WiFi管理器中的WiFi配置文件
wifi_mgmr_profile_add(&wifiMgmr, profile_msg, -1);
// 连接到WiFi配置文件。TODO:其他安全支持
bl_main_connect(
(const uint8_t *) profile_msg->ssid, profile_msg->ssid_len,
(const uint8_t *) profile_msg->psk, profile_msg->psk_len,
(const uint8_t *) profile_msg->pmk, profile_msg->pmk_len,
(const uint8_t *) profile_msg->mac, (const uint8_t) profile_msg->band, (const uint16_t) profile_msg->freq);
}
在此,我们配置了WiFi配置文件,并通过调用bl_main_connect函数来连接到该配置文件。 在bl_main_connect函数中,我们为802.11 WiFi协议设定了连接参数:bl_main.c
// 连接到WiFi配置文件
int bl_main_connect(const uint8_t* ssid, int ssid_len, const uint8_t *psk, int psk_len, const uint8_t *pmk, int pmk_len, const uint8_t *mac, const uint8_t band, const uint16_t freq) {
// 802.11 WiFi协议的连接参数
struct cfg80211_connect_params sme;
// 省略部分: 设置802.11连接参数
...
// 使用802.11连接参数连接到WiFi网络
bl_cfg80211_connect(&wifi_hw, &sme);
return 0;
}
连接参数随后被传递给在bl_main.c中定义的bl_cfg80211_connect函数…
// 使用802.11连接参数连接到WiFi网络
int bl_cfg80211_connect(struct bl_hw *bl_hw, struct cfg80211_connect_params *sme) {
// 将填充连接结果
struct sm_connect_cfm sm_connect_cfm;
// 将连接参数转发到LMAC
int error = bl_send_sm_connect_req(bl_hw, sme, &sm_connect_cfm);
// 省略部分: 检查连接结果
该函数通过调用bl_send_sm_connect_req,将连接参数发送至WiFi硬件(LMAC)。
接下来,让我们进一步探究其具体实现方式…
2.3 发送请求到LMAC
LMAC是什么?
LMAC(Lower Medium Access Control 低媒体访问控制)是BL602 WiFi无线电硬件内部运行的固件,负责执行WiFi无线电的相关功能。
为了连接到WiFi接入点,我们会通过调用bl_send_sm_connect_req(定义在bl_msg_tx.c文件中)来将连接参数传递给LMAC…
// 将连接参数转发到LMAC
int bl_send_sm_connect_req(struct bl_hw *bl_hw, struct cfg80211_connect_params *sme, struct sm_connect_cfm *cfm) {
// 生成SM_CONNECT_REQ消息
struct sm_connect_req *req = bl_msg_zalloc(SM_CONNECT_REQ, TASK_SM, DRV_TASK_ID, sizeof(struct sm_connect_req));
// 省略部分: 设置SM_CONNECT_REQ消息的参数
...
// 向LMAC固件发送SM_CONNECT_REQ消息
return bl_send_msg(bl_hw, req, 1, SM_CONNECT_CFM, cfm);
}
在这里,我们构建了一个包含连接参数的SM_CONNECT_REQ消息。
(“SM”指的是RivieraWaves的LMAC状态机)
接着,我们通过调用bl_send_msg函数将该消息发送给LMAC:bl_msg_tx.c
// 向LMAC固件发送消息的函数,静态定义
static int bl_send_msg(struct bl_hw *bl_hw, const void *msg_params, int reqcfm, lmac_msg_id_t reqid, void *cfm) {
// 省略:为消息分配缓冲区
...
// 省略:将消息复制到缓冲区
...
// 将消息添加到LMAC消息队列
int ret = bl_hw->cmd_mgr.queue(&bl_hw->cmd_mgr, cmd);
上述代码使用ipc_host_msg_push函数将消息加入LMAC**消息队列**:ipc_host.c
// 将消息添加到LMAC消息队列。
// IPC = Interprocess Communication 进程间通信
int ipc_host_msg_push(struct ipc_host_env_tag *env, void *msg_buf, uint16_t len) {
// Get the address of the IPC message buffer in Shared RAM
uint32_t *src = (uint32_t*) ((struct bl_cmd *) msg_buf)->a2e_msg;
uint32_t *dst = (uint32_t*) &(env->shared->msg_a2e_buf.msg);
// 将消息复制到仪表板消息缓冲区
for (int i = 0; i < len; i += 4) { *dst++ = *src++; }
env->msga2e_hostid = msg_buf;
// 触发LMAC中断以将消息发送到EMB
// IPC_IRQ_A2E_MSG is 2
ipc_app2emb_trigger_set(IPC_IRQ_A2E_MSG);
在将消息复制到LMAC消息队列(位于共享RAM中)之后,我们调用ipc_app2emb_trigger_set来触发LMAC中断。
随后,LMAC(以及BL602无线电硬件)将发送适当的中继WiFi数据包,用以建立与WiFi**接入点**的网络连接。
这样,BL602就完成了与WiFi接入点的连接过程!
2.4 触发LMAC中断
我们是如何触发LMAC中断的呢?
// 触发LMAC中断以将消息发送到EMB
// IPC_IRQ_A2E_MSG is 2
ipc_app2emb_trigger_set(IPC_IRQ_A2E_MSG);
我们来探究一下ipc_app2emb_trigger_set函数的内部工作原理,了解它是如何激活LMAC中断的:reg_ipc_app.h
// WiFi硬件寄存器基本地址
#define REG_WIFI_REG_BASE 0x44000000
// IPC硬件寄存器基址
#define IPC_REG_BASE_ADDR 0x00800000
// APP2EMB_TRIGGER 寄存器定义
// Bits Field Name Reset Value
// ----- ------------------ -----------
// 31:00 APP2EMB_TRIGGER 0x0
#define IPC_APP2EMB_TRIGGER_ADDR 0x12000000
#define IPC_APP2EMB_TRIGGER_OFFSET 0x00000000
#define IPC_APP2EMB_TRIGGER_INDEX 0x00000000
#define IPC_APP2EMB_TRIGGER_RESET 0x00000000
// Write to IPC 寄存器
#define REG_IPC_APP_WR(env, INDEX, value) \
(*(volatile u32 *) ((u8 *) env + IPC_REG_BASE_ADDR + 4*(INDEX)) \
= value)
// 触发LMAC中断
static inline void ipc_app2emb_trigger_set(u32 value) {
// 写入地址为0x4480 0000的WiFi IPC寄存器
REG_IPC_APP_WR(
REG_WIFI_REG_BASE,
IPC_APP2EMB_TRIGGER_INDEX,
value);
}
这段代码通过向WiFi硬件寄存器(用于进程间通信)写入,触发了LMAC中断。
REG_WIFI_REG_BASE + IPC_REG_BASE_ADDR + 4 * IPC_APP2EMB_TRIGGER_INDEX
这个地址对应的是0x4480 0000。
等等……事实上0x4480 0000这个地址在官方文档中有记录吗?
然而,这个地址并没有在BL602的参考手册中公开文档化的记录。
https://github.com/bouffalolab/bl_docs/blob/main/BL602_RM/en/BL602_BL604_RM_1.2_en.pdf
实际上,整个位于0x4400 0000的WiFi硬件寄存器区域都没有在官方文档中提及。
我们刚刚揭开了一个BL602 WiFi的神秘面纱!
三、反编译WiFi固件演示demo
我们真的在逆向BL602 WiFi驱动程序吗?
还没有。到目前为止,我们一直在阅读BL602 WiFi驱动程序的公开**源代码**。
我们现在可以进行一些真正的逆向工程吗?
当然可以!BL602 WiFi驱动程序的大部分并没有附带**源代码**。 (比如WiFi WPA认证的函数)
但是,BraveHeartFLOSSDev使用Ghidra将BL602 WiFi演示固件成功地反编译成了C语言代码……
- BraveHeartFLOSSDev/bl602nutcracker1
(我们将使用这个分支)
接下来,我们将研究这个反编译的C代码…… 并深入进行BL602 WiFi驱动程序的逆向工程!
- 更多关于Ghidra的信息
- Ghidra针对BL602的配置
https://github.com/BraveHeartFLOSSDev
https://github.com/BraveHeartFLOSSDev/bl602nutcracker1
3.1 链接到反编译代码
遗憾的是,GitHub无法在网页浏览器中展示我们那些庞大的反编译C文件。这意味着直接链接到代码的特定行会比较困难。
不过,你可以通过下载反编译后的C文件仓库来解决这个问题。
- 下载反编译后的C文件仓库
- 当我们看到这样的链接时… 反编译后的代码在这里:bl602_demo_wifi.c
https://github.com/lupyuen/bl602nutcracker1/blob/main/bl602_demo_wifi.c#L38512-L38609
点击链接右侧(或长按),然后选择“复制链接地址”。
- 将复制的地址粘贴到文本编辑器中。
然后可以看到如下
https://github.com/lupyuen/bl602nutcracker1/blob/main/bl602_demo_wifi.c#L38512-L38609
- 记住链接尾部部分
bl602_demo_wifi.c#L38512-L38609
- 这意味着:
我们需要在代码编辑器(比如VSCode)中打开下载的源文件bl602_demo_wifi.c,然后使用Ctrl-G快捷键跳转到第38512行。
- 我们可以看到所提及的反编译C代码。
四、WiFi固件任务
BL602 WiFi驱动程序运行在两个后台任务(FreeRTOS)上…
- WiFi管理任务:负责管理WiFi连接状态
- WiFi固件任务:负责控制WiFi固件
我们已经了解了WiFi管理任务(还记得状态机吗?)。
现在,我们将深入研究WiFi固件任务,看看当我们启动并调度内核事件来处理WiFi数据包,以及处理WiFi数据包传输时会发生什么
- 启动WiFi固件任务
- 调度内核事件以处理WiFi数据包
- 处理WiFi数据包的传输
4.1 启动固件任务
我们之前看到了cmd_stack_wifi调用了hal_wifi_start_firmware_task来启动固件任务。
现在,让我们深入探究hal_wifi_start_firmware_task的内部工作原理:hal_wifi.c
https://github.com/lupyuen/bl_iot_sdk/blob/master/components/hal_drv/bl602_hal/hal_wifi.c#L41-L49
// 启动WiFi固件任务 (FreeRTOS)
int hal_wifi_start_firmware_task(void) {
// WiFi固件任务的堆栈空间
static StackType_t wifi_fw_stack[WIFI_STACK_SIZE];
// WiFi固件任务的任务句柄
static StaticTask_t wifi_fw_task;
// 创建FreeRTOS后台任务
xTaskCreateStatic(
wifi_main, // 即将运行的任务函数
(char *) "fw", // 任务名
WIFI_STACK_SIZE, // 任务堆栈大小
NULL, // 任务参数
TASK_PRIORITY_FW, // 任务优先级
wifi_fw_stack, // 任务堆栈
&wifi_fw_task); // 任务句柄
return 0;
}
这创建了一个FreeRTOS**后台任务,它会一直运行wifi_main**函数。
那么,wifi_main里面是什么呢?
我们没有wifi_main的源代码。但是,得益于BraveHeartFLOSSDev,我们从BL602 WiFi固件中成功反编译得到了C代码。
下面是从反编译的C代码中提取的wifi_main函数:bl602_demo_wifi.c
https://github.com/BraveHeartFLOSSDev
https://github.com/lupyuen/bl602nutcracker1/blob/main/bl602_demo_wifi.c#L32959-L33006
// WiFi固件任务将永远运行
void wifi_main(void *param) {
...
// 初始化LMAC和UMAC
rfc_init(40000000);
mpif_clk_init();
sysctrl_init();
intc_init();
ipc_emb_init();
bl_init();
...
// 循环永远处理WiFi内核事件
do {
...
// 等等
if (ke_env.evt_field == 0) { ipc_emb_wait(); }
...
// 安排WiFi内核事件并进行处理
ke_evt_schedule();
// 休息一下
iVar1 = bl_sleep();
coex_wifi_pta_forece_enable((uint) (iVar1 == 0));
} while( true );
}
wifi_main函数的实际反编译C代码要复杂得多……
因此,我们为逆向工程筛选出了关键部分。(同时我们也添加了一些注释)
wifi_main函数会不断循环,处理WiFi内核事件,以实现WiFi数据包的传输和接收。
(“ke”代表WiFi内核,它是WiFi驱动程序的心脏)
wifi_main通过调用ke_evt_schedule函数来处理WiFi内核事件。
现在,让我们在我们的反编译C代码中查找ke_evt_schedule:bl602_demo_wifi.c
https://github.com/lupyuen/bl602nutcracker1/blob/main/bl602_demo_wifi.c#L28721-L28737
// 安排WiFi内核事件并进行处理
void ke_evt_schedule(void) {
int iVar1;
evt_ptr_t *peVar2;
while (ke_env.evt_field != 0) {
iVar1 = __clzsi2(ke_env.evt_field);
peVar2 = ke_evt_hdlr[iVar1].func;
if ((0x1a < iVar1) || (peVar2 == (evt_ptr_t *)0x0)) {
assert_err("(event < KE_EVT_MAX) && ke_evt_hdlr[event].func","module",0xdd);
}
(*peVar2)(ke_evt_hdlr[iVar1].param);
}
// 这一行可能被错误地反编译了
gp = (code *)((int)SFlash_Cache_Hit_Count_Get + 6);
return;
}
这段反编译代码执行了某些操作,但具体内容并不清晰。
幸运的是,一个特别的技巧能帮助我们解读这段晦涩的反编译代码——那就是GitHub搜索!
https://en.wikipedia.org/wiki/One_weird_trick_advertisements
4.2 调度内核事件
可能ke_evt_schedule并不是为BL602特别设计的?
也许它最初是为其他目的而开发的?
说对了!让我们在GitHub**上搜索**ke_evt_schedule!
- 在GitHub上对ke_evt_schedule进行代码搜索
(搜索结果按最近索引排序)
我们会发现,ke_evt_schedule的代码来自于AliOS Things(一个嵌入式操作系统)和RivieraWaves(下一章会解释),具体在ke_event.c文件中。
https://github.com/search?o=desc&q=ke_evt_schedule&s=indexed&type=Code
// 事件调度程序入口点。必须在后台循环中调用此原语,才能执行所设置事件的事件处理程序。
void ke_evt_schedule(void) {
uint32_t field, event;
field = ke_env.evt_field;
while (field) { // 假定编译器使用循环反转进行优化
// 查找优先级最高的事件集
event = co_clz(field);
// 过滤检查
ASSERT_ERR((event < KE_EVT_MAX) && ke_evt_hdlr[event].func);
// 执行相应的处理程序
(ke_evt_hdlr[event].func)(ke_evt_hdlr[event].param);
// 更新volatile值
field = ke_env.evt_field;
}
}
这个版本与我们反编译的ke_evt_schedule版本进行对比…… 完全一致!
甚至包括断言检查的部分!
(event < KE_EVT_MAX) && ke_evt_hdlr[event].func
既然这两个版本的ke_evt_schedule在功能上是完全相同的,那么我们来阅读AliOS/RivieraWaves版本的ke_evt_schedule代码。
https://github.com/lupyuen/bl602nutcracker1/blob/main/bl602_demo_wifi.c#L28721-L28737
我们注意到,ke_evt_schedule通过调用ke_evt_hdlr中的事件处理程序来处理WiFi内核事件。
以下是来自AliOS / RivieraWaves代码的ke_evt_hdlr事件处理程序:ke_event.c
// ke_evt_schedule调用的事件处理程序
static const struct ke_evt_tag ke_evt_hdlr[32] = {
{&rwnxl_reset_evt, 0}, // [KE_EVT_RESET]
{&ke_timer_schedule, 0}, // [KE_EVT_KE_TIMER]
{&txl_payload_handle, AC_VO}, // [KE_EVT_IPC_EMB_TXDESC_AC3]
// 此事件处理程序看起来很有意思
{&txl_payload_handle, AC_VI}, // [KE_EVT_IPC_EMB_TXDESC_AC2]
{&txl_payload_handle, AC_BE}, // [KE_EVT_IPC_EMB_TXDESC_AC1]
{&txl_payload_handle, AC_BK}, // [KE_EVT_IPC_EMB_TXDESC_AC0]
{&ke_task_schedule, 0}, // [KE_EVT_KE_MESSAGE]
{&mm_hw_idle_evt, 0}, // [KE_EVT_HW_IDLE]
...
txl_payload_handle是一个专门处理WiFi有效负载传输的事件处理程序。
接下来,我们将深入研究它的内部工作原理,了解它是如何发送WiFi数据包的。
4.3 处理传输负载
txl_payload_handle是什么?
得益于AliOS / RivieraWaves的源代码,我们得到了对txl_payload_handle函数的详细描述:位于txl_cntrl.h文件中。
// 对已从主机内存传输的有效负载执行操作。该原语由中断控制器ISR调用。如果需要,它执行LLC翻译和MIC计算。
// LLC = 逻辑链路控制, MIC = Message Integrity Code消息完整性代码
void txl_payload_handle(int access_category);
这表明txl_payload_handle函数被调用来发送WiFi数据包……这发生在从BL602复制数据包有效负载到无线电硬件之后。(通过共享**RAM**缓冲区) 在我们的反编译代码中搜索txl_payload_handle,发现了如下内容:bl602_demo_wifi.c
https://github.com/lupyuen/bl602nutcracker1/blob/main/bl602_demo_wifi.c#L20205-L20216
// 处理传输有效载荷
void txl_payload_handle(void) {
while ((_DAT_44a00024 & 0x1f) != 0) {
int iVar1 = __clzsi2(_DAT_44a00024 & 0x1f);
// 写入0x44A0 0020的WiFi寄存器
_DAT_44a00020 = 1 << (0x1fU - iVar1 & 0x1f);
}
}
看起来它并没有进行太多有效负载的处理。但它向一个未在文档中提及的WiFi寄存器0x44A0 0020写入数据。
这可能触发了LMAC固件传输WiFi数据包吗?
但在此之前,我们发现了一些可能解释txl_payload_handle内部工作原理的信息……
4.4 第二个传输负载payload
在反编译代码中txl_payload_handle函数之后,有一个名为txl_payload_handle_backup的函数。
根据其名称,txl_payload_handle_backup可能是一个处理有效负载传输的辅助函数。
以下是txl_payload_handle_backup函数的反编译要点:bl602_demo_wifi.c
https://github.com/lupyuen/bl602nutcracker1/blob/main/bl602_demo_wifi.c#L20222-L20398
// 另一个传输有效负载处理程序。
//可能与txl_payload_handle的工作方式相同
//在BL602而不是LMAC固件上运行。
void txl_payload_handle_backup(void) {
...
// Iterate through a list of packet buffers (?)
while (ptVar4 = ptVar10->list[0].first, ptVar4 == (txl_buffer_tag *)0x0) {
LAB_230059f6:
uVar3 = uVar3 + 1;
ptVar10 = (txl_buffer_env_tag *)&ptVar10->buf_idx[0].free_size;
ptVar11 = (txl_cntrl_env_tag *)(ptVar11->txlist + 1);
}
txl_payload_handle_backup开始时会遍历一个待传输的数据包缓冲区列表。
然后它调用一些来自RivieraWaves的RXU、TXL和TXU相关的函数。
// 循环 (until when?)
do {
// 调用一些RXU、TXL和TXU函数
rxu_cntrl_monitor_pm((mac_addr *)&ptVar4[1].lenheader);
...
txl_machdr_format((uint32_t)(ptVar4 + 1));
...
txu_cntrl_tkip_mic_append(txdesc,(uint8_t)uVar2);
(关于RivieraWaves的更多内容,请参阅下一章)
接下来,我们将向一些尚未在文档中公开的WiFi寄存器写入数据:0x44B0 8180、0x44B0 8198、0x44B0 81A4和0x44B0 81A8……
// 写入WiFi寄存器
_DAT_44b08180 = 0x800;
_DAT_44b081a4 = ptVar9;
...
_DAT_44b08180 = 0x1000;
_DAT_44b081a8 = ptVar9;
...
_DAT_44b08180 = 0x100;
_DAT_44b08198 = ptVar9;
(这些寄存器也不在上述列表中)
该函数执行了一些断言检查。
这些断言失败的消息可能有助于我们理解和解读反编译后的代码……
// 断言检查
line = 0x23c;
condition = "blmac_tx_ac_2_state_getf() != 2";
...
line = 0x236;
condition = "blmac_tx_ac_3_state_getf() != 2";
...
line = 0x22f;
condition = "blmac_tx_bcn_state_getf() != 2";
...
line = 0x242;
condition = "blmac_tx_ac_1_state_getf() != 2";
...
line = 0x248;
condition = "blmac_tx_ac_0_state_getf() != 2";
...
assert_rec(condition, "module", line);
该函数的执行以设置一个定时器作为结束。
// 设置计时器
blmac_abs_timer_set(uVar6, (uint32_t)(puVar8 + _DAT_44b00120));
// 继续循环
} while( true );
}
txl_payload_handle_backup的原始源代码真的有这么长吗?
很可能不是。C编译器可能会通过内联一些函数来优化固件代码。
当我们对固件进行反编译时,原本内联的代码现在出现在调用它们的函数内部。
(这就是为什么反编译代码中会出现大量重复内容的原因)
接下来,让我们来探讨一下RivieraWaves的相关内容……
五、专题 CEVA RivieraWaves
在GitHub上搜索WiFi事件调度程序ke_evt_schedule时,我们找到了这个源代码:
- mclown/AliOS-Things
(我们将使用这个分支)
这似乎是AliOS Things嵌入式操作系统的源代码,它被移植到了Beken BK7231U WiFi SoC。
但在源文件的顶部,我们注意到……
Copyright (C) RivieraWaves 2011-2016
这意味着WiFi固件的部分源代码实际上来自于CEVA RivieraWaves,而不是AliOS!
https://github.com/mclown/AliOS-Things/tree/master/platform/mcu/bk7231u/beken/ip
https://github.com/lupyuen/AliOS-Things/tree/master/platform/mcu/bk7231u/beken/ip
http://www.bekencorp.com/en/goods/detail/cid/13.html
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/ke/ke_event.c
CEVA RivieraWaves是什么?
RivieraWaves是运行在WiFi SoCs(比如BL602)上实现802.11无线协议的软件/固件。
在BL602上有两层RivieraWaves固件:
- 上层媒体访问控制(Upper Medium Access Control,UMAC):在BL602 RISC-V CPU上运行。我们之前看到的一些代码,比如内核事件调度器,就来自UMAC。
- 下层媒体访问控制(Lower Medium Access Control,LMAC):运行在BL602无线电硬件内部。
我们还可以了解更多关于WiFi媒体访问控制的信息。
以及更多关于RivieraWaves的内容。
RivieraWaves是否在其他地方使用?
是的,RivieraWaves被广泛应用于许多流行的WiFi SoCs。
https://www.ceva-dsp.com/product/rivierawaves-wi-fi-platforms/
https://www.controleng.com/articles/wi-fi-and-the-osi-model/
https://www.ceva-dsp.com/product/rivierawaves-wi-fi-platforms/
5.1 上层媒体访问控制
回想一下,UMAC(上层媒体访问控制)是运行在BL602 RISC-V CPU上的RivieraWaves代码。
当我们比较BL602 WiFi固件的反编译结果与AliOS / RivieraWaves代码时,我们发现了BL602中使用的UMAC模块(和通用模块)的源代码……
- CO模块(通用)
- KE模块(内核)
- ME模块(消息?)
- RC模块(速率控制)
- RXU模块(接收UMAC)
- SCANU模块(扫描SSID UMAC)
- SM模块(状态机)
- TXU模块(传输UMAC)
这些模块在BL602和AliOS / RivieraWaves之间大多是相同的,除了RXU模块看起来有所不同。
(更多关于UMAC模块的匹配将在我们讨论定量分析时介绍)
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/common
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/ke
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/umac/src/me
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/umac/src/rc
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/umac/src/rxu
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/umac/src/scanu
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/umac/src/sm
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/umac/src/txu
5.2 下层媒体访问控制
请记住,LMAC(下层媒体访问控制)是运行在BL602无线电硬件内部的RivieraWaves代码。
通过将BL602 WiFi固件的反编译结果与AliOS / RivieraWaves代码进行匹配,我们识别出了BL602无线电硬件所暴露的LMAC接口……
- APM接口(AliOS中未提供)
- CFG接口(AliOS中未提供)
- CHAN接口(负责MAC频道管理)
- HAL接口(提供硬件抽象层)
- MM接口(负责MAC管理)
- RXL接口(负责接收LMAC数据)
- STA接口(负责站点管理)
- TXL接口(负责传输LMAC数据)
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/lmac/src/chan
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/lmac/src/hal
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/lmac/src/mm
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/lmac/src/rx/rxl
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/lmac/src/sta
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/lmac/src/tx/txl
上述链接的LMAC接口仅供参考,因为BL602实现的LMAC与上面提到的Beken BK7231U实现有很大不同。
这些LMAC模块在BL602和AliOS / RivieraWaves之间似乎是基本相同的……
- PS模块(省电)
- SCAN模块(扫描SSID)
- TD模块(流量检测)
- VIF模块(虚拟接口)
(更多关于LMAC模块匹配的细节将在我们讨论定量分析时提供)
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/lmac/src/ps
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/lmac/src/scan
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/lmac/src/td
https://github.com/lupyuen/AliOS-Things/blob/master/platform/mcu/bk7231u/beken/ip/lmac/src/vif
六、专题 WiFi Supplicant
WiFi Supplicant是负责处理WiFi认证的代码。
(比如WPA和WPA2)
那么,WiFi Supplicant代码是从RivieraWaves来的吗?
不是的。根据反编译的代码,BL602实现了自己的WiFi Supplicant,包含诸如supplicantInit、allocSupplicantData和keyMgmtGetKeySize等函数。(请查看相关部分)
也许WiFi Supplicant的代码来自另一个项目?
当我们搜索GitHub上的这些函数名称时,我们找到了与此相匹配的源代码……
(我们将使用这个分支)
实际上,那是基于Linux的Rockchip RK3399的WiFi Supplicant代码。
它们的代码真的完全一样吗?
我们对比了反编译的BL602 WiFi Supplicant代码和Rockchip RK3399的源代码……它们几乎是一模一样的!(请查看相关部分)
这真是太棒了,因为我们刚刚揭开了BL602固件中(大约)2,500行代码的神秘起源!
(这些数据来自定量分析,我们稍后会详细讨论)
https://en.wikipedia.org/wiki/Wireless_supplicant
https://en.wikipedia.org/wiki/Wpa_supplicant
七、WiFi 物理层
WiFi物理层是什么?
WiFi物理层是控制无线频率的协议,它决定了WiFi数据包应该如何发送和接收。
(它位于媒体访问控制层之下)
更多关于WiFi物理层的信息
难道是BL602的物理层没有使用RivieraWaves吧?
不,我们怀疑BL602的物理层并不是来自RivieraWaves。
BL602物理层的起源有些模糊……
这里是从反编译代码中提取的BL602物理层的一部分:bl602_demo_wifi.c
https://www.controleng.com/articles/wi-fi-and-the-osi-model/
https://github.com/lupyuen/bl602nutcracker1/blob/main/bl602_demo_wifi.c#L33527-L33614
// 来自BL602反编译代码:初始化物理层
void phy_init(phy_cfg_tag *config) {
mdm_reset();
...
mdm_txcbwmax_setf((byte)(_DAT_44c00000 >> 0x18) & 3);
_Var2 = phy_vht_supported();
agc_config();
...
// 初始发射机速率功率控制
trpc_init();
// 初始物理自适应功能
pa_init();
phy_tcal_reset();
phy_tcal_start();
}
在GitHub上搜索phy_init和phy_hw_set_channel(BL602的另一个函数)时,我们找到了一个有用的结果……
- jixinintelligence/bl602-604
(我们将使用这个分支)
它实现了phy_init函数,具体如下:phy_bl602.c……
https://github.com/search?q=phy_init+phy_hw_set_channel&type=code
https://github.com/jixinintelligence/bl602-604
// 从GitHub搜索: Init Physical Layer //初始化物理层:
void phy_init(const struct phy_cfg_tag *config) {
const struct phy_bl602_cfg_tag *cfg = (const struct phy_bl602_cfg_tag *)&config->parameters;
phy_hw_init(cfg);
phy_env->cfg = *cfg;
phy_env->band = PHY_BAND_2G4;
phy_env->chnl_type = PHY_CHNL_BW_OTHER;
phy_env->chnl_prim20_freq = PHY_UNUSED;
phy_env->chnl_center1_freq = PHY_UNUSED;
phy_env->chnl_center2_freq = PHY_UNUSED;
// 初始化传输速率控制
trpc_init();
// 初始物理自适应功能
pa_init();
}
将BL602的反编译代码与GitHub上的搜索结果进行对比……BL602的代码似乎执行了更多的操作?
(tdm_reset、phy_tcal_reset和phy_tcal_start的调用在哪里?)
因此,我们并没有找到BL602物理层的100%完全匹配。(可能只有50%)
尽管如此,这对于我们的逆向工程来说是一个非常有价值的发现!
八、定量分析
我们实际上需要解码多少行反编译代码呢?
BL602 WiFi源代码中已经可以在其他地方找到多少部分?
要回答这些问题,我们需要对BL602固件的反编译代码进行定量分析。
(这是对电子表格进行数据处理的术语)
我们需要执行以下步骤:
- 从BL602 WiFi演示固件的反编译版本中提取所有函数名称。
- 将这些函数名称导入电子表格进行进一步分析。
- 根据模块对这些函数名称进行分类。
- 将反编译的函数代码与通过GitHub搜索找到的源代码**进行匹配**。
- 统计没有找到匹配源代码的反编译代码行数。
8.1 提取反编译函数
BL602 WiFi演示固件bl602_demo_wifi已经被反编译成一个庞大的C文件。
- 反编译的WiFi演示固件bl602_demo_wifi.c
我们运行这个命令来提取所有函数名称及其行号,以便于后续的代码行数统计。
https://github.com/lupyuen/bl_iot_sdk/blob/master/customer_app/bl602_demo_wifi
https://github.com/lupyuen/bl602nutcracker1/blob/main/bl602_demo_wifi.c
# Extract the function names (and line numbers)
# from the decompiled firmware. The line must
# begin with an underscore or a letter,
# without indentation.
grep --line-number \
"^[_a-zA-Z]" \
bl602_demo_wifi.c \
| grep -v LAB_ \
>bl602_demo_wifi.txt
这将生成bl602_demo_wifi.txt,一个包含反编译函数名称及其行号的长列表。(这里是一个示例)
(还包括函数参数和类型定义……我们很快就会处理掉这些内容)
但这个列表包含了所有内容……包括非WiFi函数,对吧?
是的。但是,查看反编译固件中的每一个函数(总共128,000行代码)也很有趣,只是为了看看它是如何运作的。
为什么不直接反编译并分析BL602 WiFi库:libbl602_wifi.a?
BL602 WiFi库libbl602_wifi.a可能包含一些不会链接到WiFi固件的额外WiFi函数。
因此,我们反编译并分析的是实际由WiFi固件调用的WiFi函数。
(顺便说一下:我们计算代码行数时会包括空行和注释行)
https://github.com/lupyuen/bl602nutcracker1/blob/main/bl602_demo_wifi.txt
8.2 整理函数到电子表格
我们将bl602_demo_wifi.txt(包含反编译函数名称和行号的列表)导入到一个电子表格中,用于进一步分析。(见上图)
这是我们进行定量分析**的电子表格**,采用了不同的格式……
我们清理数据,移除了类型定义、函数返回类型和函数参数。
根据行号,我们计算了每个函数的代码行数(包括空行和注释行)。
然后应用条件格式化,以突出显示代码行数最多的反编译函数。(这些函数通常也是最复杂的
在分析过程中,这些函数值得我们更多的关注。
https://github.com/lupyuen/bl602nutcracker1/blob/main/bl602_demo_wifi.txt
8.3 反编译函数分类
接下来,我们将每个反编译函数根据所属模块进行分类。
上面的图片显示我们已经将“rxl_”函数分类为“???RivieraWaves RXL”(RXL代表接收LMAC)。
我们使用“???”来标记那些我们找不到任何源代码的模块。
对所有3,000个反编译函数进行分类听起来很繁琐……吗?
幸运的是,属于同一个模块的反编译函数通常是聚集在一起的。因此,复制并填写一批函数的模块名称是相对简单的。
还记得我们对复杂函数的红色突出显示吗?如果对如何分类不太复杂的函数不确定,跳过分类是可以的。
在我们的电子表格中,我们已经对超过97,000行反编译代码进行了分类,这占所有反编译代码行数的86%。这对于我们的分析来说已经足够好了!
8.4 匹配反编译函数
还记得我们之前通过GitHub**搜索**找到的源代码吗?
(用于RivieraWaves、WiFi Supplicant和物理层)
现在我们深入研究那些源代码,看看它们与反编译函数的匹配程度有多高。
我们如何记录我们的发现?
在我们的电子表格中,有一个列记录了与我们反编译函数匹配的源代码**URL**(来自GitHub搜索)。(见上图)
我们还添加了一条评论,说明它们匹配得有多近。例如:“BL602版本有所不同”
如果发现的原代码与反编译函数不匹配,我们会用“???”标记模块名称。
我们需要匹配每一个反编译函数吗?
为了简化匹配过程,我们从每个模块中选取了一个或两个最复杂的函数进行匹配。
(是的,红色突出显示真的很有帮助!)
因此,我们的匹配并不是100%彻底和准确的……但它相当准确。
8.5 代码行数计算
最后,我们在电子表格中加入了一个数据透视表,用于计算与GitHub搜索结果匹配或不匹配的代码行数。
在我们的电子表格第二页,我们可以看到一个数据透视表,它汇总了我们定量分析**的结果**……
- 需要进行逆向工程的代码行数:10,500行
在GitHub搜索中未找到的代码包括LMAC接口和WiFi Supplicant的一部分。
- 部分需要逆向工程的代码行数:3,500行
我们在GitHub搜索中找到了物理层的部分匹配代码。
(关于BL602 HAL和标准驱动程序的讨论将在下一章进行)
- 在其他地方已经找到的代码行数:11,300行(真是惊人!)
在GitHub搜索中找到了UMAC和WiFi Supplicant的大部分代码。
- 我们还拥有来自BL602 WiFi驱动程序的7,500行代码,这部分代码的源代码可以在BL602**物联网软件开发套件(BL602 IoT SDK**)中找到。(请查看相关部分)
这其中包括我们之前已经了解的WiFi管理器。
总结:我们有大量的**源代码可以用来指导BL602 WiFi的**逆向工程工作!
九、其他模块
WiFi相关函数占据了反编译WiFi固件总代码行数的29%。
那么,剩下的71%的反编译代码中包含了什么呢?
接下来,让我们查看反编译固件中的非WiFi函数部分……
(复杂模块已用红色突出显示)
- AliOS: 用于多任务和设备驱动的嵌入式系统框架
- AWS IoT, AWS MQTT: 演示固件与AWS云进行物联网和MQTT(消息队列)服务的通信
- BL602 Hardware Abstraction Layer (HAL): 硬件抽象层包含引导加载程序、DMA、GPIO、闪存内存、中断、实时时钟、安全(加密)、UART等功能
- BL602 Standard Driver: 标准驱动程序由BL602硬件抽象层调用,用于访问BL602的硬件寄存器
- C Standard Library: C标准库,因为我们的固件是使用GCC编译器编译的
- EasyFlash: 一个嵌入式数据库
- FreeRTOS: 一个运行在AliOS下面的嵌入式操作系统
- Lightweight IP (LWIP): 轻量级IP,提供IP、UDP、TCP和HTTP网络通信功能
- Mbed TLS: 实现传输层安全,AWS IoT和MQTT服务需要这个安全机制
大多数非WiFi函数的源代码都可以找到。
(只需点击上方的链接即可)
十、GitHub搜索:最好的伙伴
我们得到了一个宝贵的启示……GitHub搜索是我们进行逆向工程的最好帮手!
通过GitHub搜索,我们发现了以下信息:
- UMAC和LMAC的源代码
- WiFi Supplicant的源代码
- 物理层的源代码
因此下次在进行任何逆向工程时,别忘了使用GitHub搜索!
十一、 下一步内容
这是一次激动人心的逆向旅程……感谢 Pine64 BL602 Reverse Engineering Project 工程项目的贡献者激发了我撰写这篇文章的灵感!
今天,我们为了教育目的进行了一些逆向工程……只是为了弄明白BL602是如何发送和接收WiFi数据包的。
现在我们知道了如何探测反编译的WiFi固件,以揭开每一个WiFi硬件寄存器的秘密。
我希望有人能继续这项逆向工程工作……也许能为BL602开发一个开源的WiFi驱动程序!
(也许我们应该重启Pine64 BL602 Nutcracker项目)
在下一篇文章中,我们将探讨BL706音频视频板……
所以请继续关注关于BL602、BL604和BL706的更多精彩内容吧!
如果您有任何问题、评论或建议,请在这里创建一个Issue或提交一个Pull Request……
lupyuen.github.io/src/wifi.md
BL706 音视频开发板
十二、 附录与笔记
- 本篇文章属于该推文的完整版本 this Twitter Thread
- 根据推文信息 madushan1000, BL602 WiFi RTL 也许能在这里找到…
- 更多有关 BL602 RF IP and 硬件寄存器信息:
- 有一个有趣的讨论涉及到WiFi Supplicant的许可问题,这个讨论的版本看起来与Rockchip RK3399上的Linux版本完全一样。
5 根据Twitter上的用户SpritesMods,ESP32使用了CEVA的蓝牙IP,但没有使用CEVA的WiFi IP
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
没有评论