1、項目背景
? ? ? 一款2年前開發的無線網絡通信軟件在最近的使用過程中出現網絡中傳感器離線的問題,此軟件之前已經使用的幾年了,基本功能還算穩定。這次為什么出了問題。
? ? ? ?先派工程師去現場調試一下,初步的結果是網絡信號弱,并且有個別主干網絡設備離線不工作。這次網絡的中的傳感器數量與比前比相差不多,但是傳感器數據的上報間隔較短。
2、臨時辦法
? ? ?網絡信號弱導致的通信時有中斷,需要通過加裝主干網絡設備增加信號覆蓋解決。同時調整傳感器數據的上報間隔變長,網絡中設備離線的情況不再出現,看來是數據量上報大有關了。
3、產品軟件問題
? ? ?雖然說現場問題臨時解決了,傳感器都上線了。但是這里面還是說明網絡設備的軟件有問題,這個問題是只有在大數量傳輸時會出現。那么就在公司搭建網絡環境,發送大數量測試,經過長達3周的測試,復現出現場的問題。網絡設備會出現內部MCU硬件定時器不工作和內存分配異常導致的無線網絡發送空數據包兩種問題。
3.1 定時器不工作
? ? ? 在某種特定的網絡情況下,網絡中的設備內部的MCU硬件定時器會被連續調用兩次啟動,正常的情況下是調用1次啟動,1次停止。可能由于數據量大,調用停止的操作未成功,就出現了連續兩次調用啟動的情況。經過分析定時器的驅動代碼和實際跟蹤發生問題時的情況,當第二次調用定時器啟動時,HAL_TIM_Base_Start(&htim2);函數是返回失敗,跟蹤代碼后發現定時器內部的狀態是工作狀態是BUSY狀態,返回錯誤,因為定時還在運行狀態。
? ? ? ? ? 分析下面的代碼,發現當調用StartWork函數-》InitTimer函數,此函數會通過寄存器操作把定時器關閉,之后再調用HAL_TIM_Base_Start函數啟動,由于寄存器操作把定時器并不修改定時器的狀態變量,導致再次調用啟動會失敗,正確的操作邏輯基于對外設備定時器的操作應統一方式,不能API函數調用和定時器操作混合使用,會導致異常情況驅動內部的狀態不正常。
/*==========================================================================
brief : 硬件定時器初始化
param : None
retval : None
//==========================================================================*/
void InitTimer(TimeIsUpCb_t callback)
{HTimer2Timing.Status = eSltTiming_Idle;HTimer2Timing.CurSltIdx = 0;HTimer2Timing.StartCnt = 0;HTimer2Timing.EndtCnt = myAttributes.TotalSltSize_us - myAttributes.SltSize_us - 1;HTimer2Timing.NextSltTCnt = 0;HTimer2Timing.CallBackWhenTimeIsUp = callback;寄存器操作停止了定時器,不能修改定時器內部的狀態變量__HAL_TIM_DISABLE(&htim2); //停止timier2計數htim2.Instance->CNT = 0;htim2.Instance->ARR = myAttributes.TotalSltSize_us - 1;__HAL_TIM_CLEAR_IT(&htim2, (TIM_IT_UPDATE | TIM_IT_CC1));__HAL_TIM_ENABLE_IT(&htim2, (TIM_IT_UPDATE | TIM_IT_CC1));
}/*==========================================================================
brief : 開始工作命令處理函數,模塊開始廣播BCH,等待設備接入
param : puartpkt: 串口幀數據指針,包含串口handle(用于說明串口數據是從哪個串口來的)
retval : None
//==========================================================================*/
void InitEachBuf(void);
void StartWork(UartFrame_t* puartpkt)
{//計算一次發送時不同時隙與最大傳輸字節數據對應表CalMaxBytesInOnePktBySlt(myAttributes.LoRaCfg.ModulationIndex, (myAttributes.SltSize_us),(myAttributes.BCHInfo.GP_Dphy * 100));InitAllTheadPt();InitEachBuf();BCHFrame_Init();InitTimer(Send_DL_Frame);調用定時器通過API函數調用 HAL_TIM_Base_Start(&htim2);myAttributes.WorkStatus = eModuleWorkSt_InNet;
}/*==========================================================================
brief : 停止工作命令處理函數
param : puartpkt: 串口幀數據指針,包含串口handle(用于說明串口數據是從哪個串口來的)
retval : None
//==========================================================================*/
void StopWork(UartFrame_t* puartpkt)
{myAttributes.WorkStatus = eModuleWorkSt_Idle;InitRadio();HAL_TIM_Base_Stop(&htim2);InitEachBuf();DevSltList_Init();DevRegList_Init();DevDRxList_Init();
}
? ? ?找到問題,解決辦法很簡單了,重新整理定時器的調用邏輯,全部通過API接口來調用定時器啟動和停止 ,這樣連續兩次調用啟動定時器也沒有問題。
3.2 內存分配異常
? ? ? ? 內存分配異常很難解決,前前后后增加了很多調試代碼,一步一步跟蹤分析,確定是內存分配導致的異常。
? ??
typedef struct
{uint8_t PktNum : 7;uint8_t FlagRewind : 1;uint16_t Size;uint16_t Head;uint16_t Tail;uint16_t DataTail;//有效的數據結尾uint8_t* DataArr;
}StreamBuffer;內存申請代碼:
//申請一個保留空間給外部使用,不支持多線程、多次調用
uint8_t* ApplyMemByStreamBuffer(StreamBuffer *ptStreamBuffer, uint16_t size)
{uint8_t* Result = NULL;do{//如果申請空間大于空間大小則放棄if(size > ptStreamBuffer->Size)break;if(ptStreamBuffer->PktNum == 0){ptStreamBuffer->Head = 0;ptStreamBuffer->Tail = 0;ptStreamBuffer->DataTail = 0;ptStreamBuffer->FlagRewind = 0;}uint16_t newtail = ptStreamBuffer->Tail + size;if(newtail <= ptStreamBuffer->Size){if(ptStreamBuffer->FlagRewind){ //第2種情況,Tail超過Head產生數據覆蓋,不能分配出內存if(newtail > ptStreamBuffer->Head)break;}Result = (ptStreamBuffer->DataArr + ptStreamBuffer->Tail);}else //尾部空間浪費一些,不用了,這樣處理簡單{ptStreamBuffer->Tail = 0;ptStreamBuffer->FlagRewind = 1;if(ptStreamBuffer->PktNum){ //第2種情況,Tail超過Head產生數據覆蓋,不能分配出內存if((ptStreamBuffer->Tail + size) > ptStreamBuffer->Head)break;}//從緩存區頭部開始使用Result = ptStreamBuffer->DataArr;}}while(0);return(Result);
}
存在的問題:當可用內存為10240長度時,每次申請10個字節長度時,可申請出1024塊內存,使用PktNum 7位表不了這么多的內存,當申請的1024塊時,PktNum只能到127,無法表達內部存儲了多少塊?釋放內存時,只能釋放127塊。申請0長度時,能返回可以申請,需要處理一下。
? ? ?根據以上內存代碼,做了初步的代碼邏輯分析,發現一些問題,但實際測試是這些問題點全部未發生,做了分配的原理分析圖:內存分配基本邏輯正常。
? ? ? 后面經過多次測試,發現一個共同的現場,當內部出問題時,內存塊分配出去的計數PktNum與內存中已存儲的有效數據不一樣多。如下圖,計數中有8塊分配,根據內存指針來看已經沒有有效數據了。
? ? ? ?此種問題多次復現,并且出現時PktNum都是8,很有規律,當時內存中申請的數據塊長度為1225字節,整個總內存的長度為10240長度,計算下來正好能存儲8*1225,即8塊。說明內存在用完了后發生第二申請覆蓋,所以只有計數增加了。
? ? ? ?跟蹤內存分析的數據長度,發現如果內存中先分配8塊1225長度塊,再分配1個小塊147長度,最后釋放掉8塊1225長度的塊,再申請8塊1225長度的塊后,內存就滿了,再申請1塊1225長度的內存,內存出現的反轉,開始占用已經申請出去的內存。解決辦法,不允許此種情況下再分配出新的內存了。
uint8_t* ApplyMemByStreamBuffer(StreamBuffer *ptStreamBuffer, uint16_t size)
{uint8_t* Result = NULL;do{//如果申請空間大于空間大小則放棄/* 如果申請的空間是0或是內部存儲的PktNum已經最大,申請的數量已超總內存大小,返回失敗 zhaoshimin 20250429*/if((size > ptStreamBuffer->Size) || (size == 0) || (STREAM_BUFFER_PKTNUM_MAX == ptStreamBuffer->PktNum)){break;} if(ptStreamBuffer->PktNum == 0){ptStreamBuffer->Head = 0;ptStreamBuffer->Tail = 0;ptStreamBuffer->DataTail = 0;ptStreamBuffer->FlagRewind = 0;}uint16_t newtail = ptStreamBuffer->Tail + size;if(newtail <= ptStreamBuffer->Size){if(ptStreamBuffer->FlagRewind){if(newtail > ptStreamBuffer->Head)break;}Result = (ptStreamBuffer->DataArr + ptStreamBuffer->Tail);}else //尾部空間浪費一些,不用了,這樣處理簡單{/*當內存中先放入1225的8包數據再放入147長度的包后,
釋放掉8包數據后,再申請放入1225長度的8包數據后,內存滿了,
發生第二次產生了反轉 20250429*/if(ptStreamBuffer->FlagRewind){break;}else{ptStreamBuffer->Tail = 0;ptStreamBuffer->FlagRewind = 1;if(ptStreamBuffer->PktNum){if((ptStreamBuffer->Tail + size) > ptStreamBuffer->Head)break;}} Result = ptStreamBuffer->DataArr;}}while(0);return(Result);
}