转自http://bbs.eeworld.com.cn/thread-1077830-1-1.html
编译了 BLE_p2pServer 这个工程以后,我就可以用GDB进行跟踪调试了。跟踪跟踪,姑且看看这个软件框架是什么样的,不作详细分析了(时间也不允许)。
从 main.c 入手,主函数 main() 就写了一些函数调用而已:
int main(void)
{
HAL_Init();
Reset_Device();
Config_HSE();
SystemClock_Config();
PeriphClock_Config();
Init_Exti();
MX_GPIO_Init();
MX_DMA_Init();
MX_RF_Init();
MX_RTC_Init();
APPE_Init();
while(1)
{
SCH_Run(~0);
}
}
首先是 HAL_Init() 这个库函数, 起初始化 HAL 库的作用,其中又调用了 stm32wbxx_hal_msp.c 中写的用户初始化代码 HAL_MspInit(), 执行工程相关的初始化。本工程执行了一个 __HAL_RCC_HSEM_CLK_ENABLE();
然后是 Reset_Device(), 这个函数就在 main.c 中,里面执行的是 Reset_BackupDomain() 和 Reset_IPCC() ——可能是这部分硬件并不是和CPU一起复位的,所以需要单独处理下。
接下来的 Config_HSE(), SystemClock_Config() 和 PeriphClock_Config() 是配置系统各种时钟的,根据程序运行的需要设置。
Init_Exti(), MX_GPIO_Init(), MX_DMA_Init(), MX_RF_Init(), MX_RTC_Init() 这几个函数配置 EXTI, GPIO, DMA, RTC 这几个基本的硬件。不过 MX_RF_Init() 函数里面是空的,硬件初始化并不在这里(估计是由另外那个CPU2做的)。以上这些函数都容易看懂,就是在使用无线功能以前做好其它片上设备的准备工作。HSEM 和 IPCC 是 STM32WB55 的两个特殊硬件,双核通信要用到。
然后,就到了 APPE_Init() 函数了,它在 app_entry.c 中定义。函数名的意思是初始化应用程序(前面的初始化只是硬件),它里面的函数会跟 WPAN 库打交道。
void APPE_Init( void )
{
SystemPower_Config();
HW_TS_Init(hw_ts_InitMode_Full, &hrtc);
Init_Debug();
LPM_SetOffMode(1 << CFG_LPM_APP, LPM_OffMode_Dis);
Led_Init();
Button_Init();
appe_Tl_Init();
}
这其中 HW_TS_Init() 在 hw_timerserver.c 中定义,顾名思义是初始化软件定时器(服务)。瞅了瞅代码,这是基于RTC实现的功能,至于软件定时器提供给谁用的?姑且发现在 app_ble.c 中调用了 HW_TS_Create.
Init_Debug() 函数用了 dbg_trace.c 中的 DbgTraceInit(), 初始化了 USART1. 运行这个程序时通过串口输出的信息就是软件自带的 Debug 支持。
最后的 appe_Tl_Init() 又是重头戏了, 函数名中 “Tl” 是 Transport layer 的缩写,这个函数在 appe_entry.c 中,不过调用的是 WPAN 库里面的函数。由于我没有找到这个库的手册,这里需要如何初始化不清楚,从代码只能略猜一二。
APPE_Init() 执行完毕之后,回到 main() 当中,开始一个死循环:重复调用 SCH_Run(). 这个函数定义在 scheduler.c 中,也就是执行调度器的意思。但是这里的调度器和我熟悉的 FreeRTOS 的调度器不同,后者是不会返回的,因此不需要放在循环里面。为了确认这一点,我在 SCH_Run() 函数入口设置断点,然后恢复执行,CPU仍然会再次遇到断点。在 SCH_Run() 函数体中,执行每个任务是通过一条语句:
/** Execute the task */
TaskCb31 - bit_nbr;
这里有一个数组 TaskCb 存放任务对应的函数,不妨看看里面有什么:
(gdb) print TaskCb
$1 = {0x8001eb9 , 0x8002371 ,
0x80058ed , 0x8005f5d }
里面有四个“任务”,不过从我对 scheduler.c 的观察看来,这不是真正的 RTOS ——每个任务没有独立的堆栈,不能嵌套执行。
这四个任务分别在什么时候注册的呢?我在 SCH_RegTask() 函数入口设断点,再来跟踪一次。
第一次调用:appe_Il_Init() 里面, task_id=3
SCH_RegTask( CFG_TASK_SYSTEM_HCI_ASYNCH_EVT_ID, shci_user_evt_proc );
第二次调用:APP_BLE_Init() 里(app_ble.c),task_id=2
SCH_RegTask(CFG_TASK_HCI_ASYNCH_EVT_ID, hci_user_evt_proc);
第三次调用:仍然在 APP_BLE_Init() 里,task_id=0
SCH_RegTask(CFG_TASK_ADV_CANCEL_ID, Adv_Cancel);
第四次调用:在 p2p_server_app.c 的 P2PS_APP_Init() 函数里,task_id=1
SCH_RegTask( CFG_TASK_SW1_BUTTON_PUSHED_ID, P2PS_Send_Notification );
这里 CFG_TASK…ID 的定义在 app_conf.h 中可以找到。p2p Server程序一共注册了如上这四个任务。与其说是任务,不如说是事件处理子程序吧。从名称上看,task_id 3,4这两个分别是 hci 和 shci 的用户事件处理程序(HCI是 Host Controller Interface的话,SHCI又是什么呢?),分别在 hci_tl.c 和 shci_tl.c 中定义,结构也相似。task_id为0的这个是停止广播。task_id为1的这个子程序是向BLE client发送通知的,即按键触发的事件处理。
到这里,大概了解到 STM32WB55 的 demo 应用程序是“事件驱动”组织的。在初始化过程中建立起调度器,将处理不同事件的“任务”交由调度器管理。调度器在有任务需要处理的时候调用相应任务的函数。这背后当然还得有硬件中断(IRQ)来驱动。
不妨就跟踪一下按键引发的中断:EXTI的 IRQ 处理过程。按下SW1时,EXTI中断服务程序被硬件执行void PUSH_BUTTON_SW1_EXTI_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(BUTTON_SW1_PIN);
}
就直接调用 HAL 库的中断处理函数
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
/* EXTI line interrupt detected */
if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
HAL_GPIO_EXTI_Callback(GPIO_Pin);
}
}
它调用用户编写的中断处理函数
void HAL_GPIO_EXTI_Callback( uint16_t GPIO_Pin )
{
switch (GPIO_Pin)
{
case BUTTON_SW1_PIN:
APP_BLE_Key_Button1_Action();
break;
case BUTTON_SW2_PIN:
APP_BLE_Key_Button2_Action();
break;
case BUTTON_SW3_PIN:
APP_BLE_Key_Button3_Action();
break;
default:
break;
}
return;
}
结果就是选择调用了
void APP_BLE_Key_Button1_Action(void)
{
P2PS_APP_SW1_Button_Action();
}
最终是到这里
void P2PS_APP_SW1_Button_Action(void)
{
SCH_SetTask( 1<
return;
}
层层嵌套调用很冗长吧,除去简单判断,就是做了个触发事件处理(任务)的操作。对按键的响应转移到了一个任务函数中,而不是在中断服务里面处理,这个特点又和RTOS一致。
再看另外一个,我跟踪了一次 IPCC接收中断(和CPU2通信使用)的响应过程:
void IPCC_C1_RX_IRQHandler(void)
{
HW_IPCC_Rx_Handler();
return;
}
调用的是 hw_ipcc.c 中的 HW_IPCC_Rx_Handler 来处理
void HW_IPCC_Rx_Handler( void )
{
if (HW_IPCC_RX_PENDING( HW_IPCC_THREAD_NOTIFICATION_ACK_CHANNEL ))
{
HW_IPCC_THREAD_NotEvtHandler();
}
else if (HW_IPCC_RX_PENDING( HW_IPCC_BLE_EVENT_CHANNEL ))
{
HW_IPCC_BLE_EvtHandler();
}
else if (HW_IPCC_RX_PENDING( HW_IPCC_SYSTEM_EVENT_CHANNEL ))
{
HW_IPCC_SYS_EvtHandler();
}
else if (HW_IPCC_RX_PENDING( HW_IPCC_TRACES_CHANNEL ))
{
HW_IPCC_TRACES_EvtHandler();
}
else if (HW_IPCC_RX_PENDING( HW_IPCC_THREAD_CLI_NOTIFICATION_ACK_CHANNEL ))
{
HW_IPCC_THREAD_CliNotEvtHandler();
}
return;
}
根据硬件寄存器标志位,选择了调用 HW_IPCC_BLE_EvtHandler 函数
static void HW_IPCC_BLE_EvtHandler( void )
{
HW_IPCC_BLE_RxEvtNot();
LL_C1_IPCC_ClearFlag_CHx( IPCC, HW_IPCC_BLE_EVENT_CHANNEL );
return;
}
在清除 IPCC 寄存器相关标志位之前,调用 tl_mbox.c 中的 HW_IPCC_BLE_RxEvtNot 函数
void HW_IPCC_BLE_RxEvtNot(void)
{
TL_EvtPacket_t *phcievt;
while(LST_is_empty(&EvtQueue) == FALSE)
{
LST_remove_head (&EvtQueue, (tListNode )&phcievt);
BLE_IoBusEvtCallBackFunction(phcievt);
}
return;
}
上面这个函数要处理一个列表,对每个 HCI 事件,调用回调函数 BLE_IoBusEvtCallBackFunction, 但这是一个函数指针
static void ( BLE_IoBusEvtCallBackFunction) (TL_EvtPacket_t *phcievt);
还需要找到它指向的函数。不难找到,在 TL_BLE_Init() 函数里面有这一行:
BLE_IoBusEvtCallBackFunction = pInitHciConf->IoBusEvtCallBack;
也就是 TL_BLE_Init() 传入参数——一个 TL_BLE_InitConf_t 类型的指针所指定的函数。TL_BLE_Init() 又是被间接调用的,在 TlInit() 函数中有如下代码:
Conf.IoBusEvtCallBack = TlEvtReceived;
hciContext.io.Init(&Conf);
所以,BLE_IoBusEvtCallBackFunction 实际上是 TlEvtReceived 的入口地址
static void TlEvtReceived(TL_EvtPacket_t *hcievt)
{
if ( ((hcievt->evtserial.evt.evtcode) == TL_BLEEVT_CS_OPCODE) || ((hcievt->evtserial.evt.evtcode) == TL_BLEEVT_CC_OPCODE ) )
{
LST_insert_tail(&HciCmdEventQueue, (tListNode *)hcievt);
hci_cmd_resp_release(0); /< Notify the application a full Cmd Event has been received */
}
else
{
LST_insert_tail(&HciAsynchEventQueue, (tListNode )hcievt);
hci_notify_asynch_evt((void) &HciAsynchEventQueue); /*< Notify the application a full HCI event has been received /
}
return;
}
它里面的两个函数与调度器有关:
void hci_notify_asynch_evt(void pdata)
{
SCH_SetTask(1 << CFG_TASK_HCI_ASYNCH_EVT_ID, CFG_SCH_PRIO_0);
return;
}
void hci_cmd_resp_release(uint32_t flag)
{
SCH_SetEvt(1 << CFG_IDLEEVT_SYSTEM_HCI_CMD_EVT_RSP_ID);
return;
}
所以,这个中断也触发了事件,后面的处理工作还是由调度器管理的。
分析到这里,对 demo 程序的软件结构大概有了个了解。这是事件驱动的写法,可是嵌套的调用太多,分析起来不直观。不知道作为这上面软件的开发者感觉又如何。