作用
產品到客戶現場出現異常情況,這個時候就需要一個日志記錄儀、黑匣子,可以記錄產品的工作情況,當出現異常時,可以搜集到上下文的數據,從而判斷問題原因。
之前從網上買過,但是出現過丟數據的情況耽誤了問題分析,自此以后就一直心有懷疑不敢全信它了,后面都是掛兩個來交叉對比,生怕又被坑。于是索性乘自己有空來做一個。
軟硬件開源地址:https://gitee.com/qlexcel/serial-port-data-logger
硬件
尺寸長38mm,寬21mm
使用
找一張TF卡,格式化為FAT32格式。沒有的話可以網上買,8G的8塊錢包郵。比如: TF卡
記錄儀開機的時候會讀取SD中的配置文件config.txt,獲取用戶配置。如果沒有讀取成功或者參數異常,就會恢復默認配置。
恢復默認配置后,config.txt文件內容就會被覆蓋為以下內容:
baud:115200,stopbit:0,parity:0,filemb:30,time:0,timeout:4,filename:log,
來解釋下配置的作用:
baud:115200, //波特率 9600 115200 921600
stopbit:0, //停止位 =0,1停止位 =1,0.5停止位 =2,2停止位 =3,1.5停止位
parity:0, //奇偶校驗位 =0,無校驗 =2,偶校驗 =3,奇校驗
filemb:30, //日志文件最大大小,單位MB
time:0, //是否添加時間戳 =1,添加 =0,不添加
timeout:4, //超時時間 超過此時間沒有收到新數據,就會關閉日志文件,此時可以安全拔出SD卡
filename:log, //日志文件名
LED燈的作用:
綠燈閃爍表示接收到數據。
藍燈亮起表示正在保存數據到SD卡。
藍燈快閃表示出現故障。
軟件
如下是main函數代碼,功能實現都在這里
#include "bsp.h"/*
最好每次抓數據時,把SD中的log文件清空
*//******************************************* 宏定義區域 ****************************************/
#define QL_SUCCESS 0 //函數執行成功
#define QL_FAIL 1 //函數執行失敗
#define QL_ERROR_SD_CARD 2 //SD卡故障
#define QL_ERROR_FILE_SYS 3 //文件系統故障#define CFG_FILE_NAME "config.txt" //配置文件名
#define FILE_NAME "log" //默認的日志文件名
#define DEBUG_STA 1 //=1,打開調試模式 =0,關閉調試模式
#define WR_BUF_LEN 14000 //寫數據buf大小/******************************************* 變量定義區域 ****************************************/
FATFS fs;
FIL fsrc;
uint8_t wrbuffer[WR_BUF_LEN]; //寫數據buf
// 1 2 4 5 7 8 10 11
uint8_t Date[14]={'[','0','0','_','0','1',':','0','1',':','0','1',']',0};
uint8_t TimeStamp; //是否添加時間戳 =1,添加 =0,不添加
uint8_t Day=0; //天數
uint8_t PastSecond; //多長時間沒有接收到數據,單位秒
uint8_t TimeOut=4; //超時時間
uint8_t FileOpenSta=0; //日志文件打開狀態
uint32_t BaudRate=115200; //波特率
uint8_t StopBits=0; //停止位 =0,1停止位 =1,0.5停止位 =2,2停止位 =3,1.5停止位
uint8_t Parity=0; //奇偶校驗位 =0,無校驗 =2,偶校驗 =3,奇校驗
uint16_t Head,Tail,len;
uint32_t TotalLen=0,FileMaxLen;
uint32_t len_bk,DateWr,DateWrOld;
char FileName[11]; //日志文件名字
uint8_t FileNameIndex=0; //日志文件名字編號
uint32_t LedG_Blink_Cnt; //接收到數據綠燈閃爍計數
uint8_t LedG_Blink_Flg; //接收到數據綠燈閃爍標志/******************************************* 函數聲明區域 ****************************************/
uint8_t Filesystem_Handle(void);
uint8_t GetstrstrValue(uint8_t* str, char* substr, uint8_t* out, uint16_t MaxLen);
uint32_t MyStr2Int(uint8_t* str, uint8_t dot);int main(void) //監視下文件系統每次寫扇區個數
{ uint8_t res,tmp;mGPIO_Init(); RTC_Init();res=Filesystem_Handle();UART_Init();printf("/** Uart Data Saver V1.0**/\r\n");if(res==QL_ERROR_SD_CARD)printf("SD Card Error!\r\n");else if(res==QL_ERROR_FILE_SYS)printf("File System Error!\r\n");else if(res)printf("Other Error!\r\n");elseprintf("Init ok!\r\n");while(res) //初始化不成功,就死循環閃燈提示{ gd_eval_led_toggle(LED_BLUE);delay_1ms(200);} LED_G_OFF();LED_B_OFF();while(1){ Head=RX_BUF_LEN-DMA_CHCNT(X1_UART_DMA, X1_UART_DMA_CH_RX); //DMA已經接收到的數據個數 if(Head!=Tail) //如果接收到數據{wrbuffer[len++]=rxbuffer[Tail++];if(Tail==RX_BUF_LEN) Tail=0;if(TimeStamp && DateWrOld!=DateWr) //時間戳打開 且 寫入的時間發生了改變{if(wrbuffer[len-1]=='\n' || wrbuffer[len-1]=='\r') //遇到換行,寫入一次時間{tmp=0;while(tmp<13){wrbuffer[len++]=Date[tmp++];DateWrOld=DateWr;}
#if DEBUG_STA printf("%s\r\n",Date);
#endif }}PastSecond=0;LedG_Blink_Flg=1; //接收到數據,LED閃爍提示}if(len>(WR_BUF_LEN-5)) //當接收數據比較多時,就寫入一次SD卡。數據寫完后,日志文件不關閉{LED_B_ON();if(FileOpenSta==0) //如果日志文件沒有打開{res = f_open(&fsrc,FileName,FA_WRITE);f_lseek(&fsrc, f_size(&fsrc));FileOpenSta=1;}res = f_write(&fsrc,wrbuffer,len,&len_bk);if(res) break;printf("wr%d\r\n",len); TotalLen+=len;if(TotalLen>FileMaxLen) //如果數據大小超過了文件最大{tmp=strlen(FileName); //文件編號+1FileNameIndex++;FileName[tmp-7]=FileNameIndex/100+'0';FileName[tmp-6]=FileNameIndex%100/10+'0';FileName[tmp-5]=FileNameIndex%10+'0';f_close(&fsrc);res = f_open(&fsrc,FileName,FA_WRITE|FA_OPEN_ALWAYS); //打開新文件f_lseek(&fsrc, f_size(&fsrc));TotalLen=0;}len=0; LED_B_OFF(); }else if(PastSecond>TimeOut && (len>0 || FileOpenSta==1)) //沒有新數據一段時間后,如果還有數據沒寫入,就立即寫入{ //或者日志文件處于打開狀態,就關閉LED_B_ON();if(len>0) //有數據沒寫入,就立即寫入{if(FileOpenSta==0) //沒有打開文件{ res = f_open(&fsrc,FileName,FA_WRITE);f_lseek(&fsrc, f_size(&fsrc));FileOpenSta=1; } res = f_write(&fsrc,wrbuffer,len,&len_bk);if(res) break;}if(FileOpenSta==1){ f_close(&fsrc);FileOpenSta=0;} printf("wr%d\r\n",len); TotalLen+=len;len=0; LED_B_OFF(); }if(LedG_Blink_Flg) //接收到數據,LED閃爍提示{LedG_Blink_Cnt++;if(LedG_Blink_Cnt>30000){LedG_Blink_Cnt=0;if(gpio_input_bit_get(LED_G_PORT, LED_G_PIN)) //如果IO是高,LED滅的狀態{LED_G_ON();}else //如果IO是低,LED亮的狀態{LED_G_OFF();LedG_Blink_Flg=0;}}}if(RTC_CTL & RTC_FLAG_SECOND) //秒中斷標志{RTC_CTL &= ~RTC_FLAG_SECOND; //清除中斷標志DateWr=(RTC_CNTH << 16)|RTC_CNTL; //獲取當前時間if (DateWr == 86399) //當時間到達23:59:59時清零,天數加1{RTC_CTL |= RTC_CTL_CMF;RTC_CNTH=0;RTC_CNTL=0;RTC_CTL &= ~RTC_CTL_CMF;Day++;rtc_lwoff_wait();} PastSecond++;Date[1]=Day%100/10+'0';Date[2]=Day%10+'0'; tmp=DateWr/3600; //hoursDate[4]=tmp/10+'0';Date[5]=tmp%10+'0';tmp=DateWr%3600/60; //minutesDate[7]=tmp/10+'0';Date[8]=tmp%10+'0'; tmp=DateWr%60; //seconds Date[10]=tmp/10+'0';Date[11]=tmp%10+'0';
// printf("%s\r\n",Date);} }while(1) //當文件系統有問題,就會跳出上面循環到達這里,閃燈提示{ gd_eval_led_toggle(LED_BLUE);delay_1ms(50);}
}#define CONFIG_DEFAULT_LEN 71
static const char CONFIG_DEFAULT[CONFIG_DEFAULT_LEN]="baud:115200,stopbit:0,parity:0,filemb:30,time:0,timeout:4,filename:log,";
uint8_t Filesystem_Handle(void)
{uint8_t res,i;res=SD_Init();if(res) return QL_ERROR_SD_CARD; //SD卡有問題res=f_mount(&fs,"0:",1);if(res!=FR_OK) return QL_ERROR_FILE_SYS; //文件系統有問題res = f_open(&fsrc,CFG_FILE_NAME,FA_READ); //以只讀方式打開配置文件if(res==FR_OK) //打開成功,說明文件存在{res = f_read(&fsrc,rxbuffer,200,&len_bk);f_close(&fsrc);if(res==FR_OK && len_bk>50) //數據讀取成功且大小正常{res |= GetstrstrValue(rxbuffer,"baud:",wrbuffer,10); //波特率BaudRate=MyStr2Int(wrbuffer,0);res |= GetstrstrValue(rxbuffer,"stopbit:",wrbuffer,10); //停止位StopBits=MyStr2Int(wrbuffer,0);res |= GetstrstrValue(rxbuffer,"parity:",wrbuffer,10); //校驗位Parity=MyStr2Int(wrbuffer,0);res |= GetstrstrValue(rxbuffer,"filemb:",wrbuffer,10); //文件大小 FileMaxLen=MyStr2Int(wrbuffer,0);res |= GetstrstrValue(rxbuffer,"time:",wrbuffer,10); //時間戳 TimeStamp=MyStr2Int(wrbuffer,0);res |= GetstrstrValue(rxbuffer,"timeout:",wrbuffer,10); //超時事件TimeOut=MyStr2Int(wrbuffer,0);res |= GetstrstrValue(rxbuffer,"filename:",wrbuffer,10); //日志文件名strcpy(FileName,(char*)wrbuffer);}elseres=1;}if(res!=FR_OK) //如果配置文件讀取失敗,使用默認配置參數{BaudRate=115200;StopBits=0;Parity=0;FileMaxLen=30;TimeStamp=0;TimeOut=4;strcpy(FileName,FILE_NAME);res = f_open(&fsrc,CFG_FILE_NAME,FA_WRITE|FA_CREATE_ALWAYS); //把默認參數寫入SD卡res |= f_write(&fsrc,CONFIG_DEFAULT,CONFIG_DEFAULT_LEN,&len_bk); f_close(&fsrc);if(res!=FR_OK) return QL_FAIL; for(i=0;i<100;i++) //配置參數被恢復默認了,要閃燈提示,避免用戶不知道導致數據丟{ gd_eval_led_toggle(LED_BLUE);delay_1ms(50);gd_eval_led_toggle(LED_GREEN);} }FileMaxLen<<=20; //總大小單位由MB變成字節i=strlen(FileName);FileName[i] =FileNameIndex/100+'0'; //日志文件名編號,第1個文件名是log000.txt,當超過文件大小,保存到第2個文件log001.txtFileName[i+1]=FileNameIndex%100/10+'0';FileName[i+2]=FileNameIndex%10+'0';FileName[i+3]='.';FileName[i+4]='t';FileName[i+5]='x';FileName[i+6]='t';FileName[i+7]=0;res = f_open(&fsrc,FileName,FA_OPEN_ALWAYS); //如果日志文件存在,則打開該文件。 如果沒有,將創建一個新文件。f_close(&fsrc); if(res!=FR_OK) return QL_FAIL;elsereturn QL_SUCCESS;
}/*********************************************************************************************************
* 功能說明: 在字符串str中匹配子字符串,并獲取子字符串后面的字符串
*********************************************************************************************************/
uint8_t GetstrstrValue(uint8_t* str, char* substr, uint8_t* out, uint16_t MaxLen)
{uint16_t len = 0;char *p1,*p2;while(*str != 0){p1 = (char*)str;p2 = substr;while(*p1 && *p2 && *p1 == *p2){p1++;p2++;}if (*p2 == 0){p2 = (char*)out;while (*p1 != ','){*p2 = *p1;p1++;p2++;len++;if (len >= MaxLen)return QL_FAIL;}*p2 = 0;return QL_SUCCESS;}str++;}return QL_FAIL;
}/*********************************************************************************************************
* 功能說明: 字符串轉整型數
* 形 參: str:字符串
* dot:小數點 =0表示不放大 =2表示放大100倍
* 返 回 值: 整型數
*********************************************************************************************************/
uint32_t MyStr2Int(uint8_t* str, uint8_t dot)
{int out = 0;while (*str != 0){out *= 10;out += *str - '0';str++;if (*str == '.'){if (dot == 2){str++;out *= 10;if (*str >= '0' && *str <= '9')out += *str - '0';str++;out *= 10;if (*str >= '0' && *str <= '9')out += *str - '0';return out;}elsereturn out;}}if (dot == 2)out *= 100;return out;
}
可以看到main函數中,先初始化IO、RTC后,就執行Filesystem_Handle函數。
mGPIO_Init(); RTC_Init();res=Filesystem_Handle();UART_Init();printf("/** Uart Data Saver V1.0**/\r\n");if(res==QL_ERROR_SD_CARD)printf("SD Card Error!\r\n");else if(res==QL_ERROR_FILE_SYS)printf("File System Error!\r\n");else if(res)printf("Other Error!\r\n");elseprintf("Init ok!\r\n");while(res) //初始化不成功,就死循環閃燈提示{ gd_eval_led_toggle(LED_BLUE);delay_1ms(200);}
在Filesystem_Handle函數中初始化SD卡,然后讀取配置文件config.txt中的內容來獲取用戶配置,如果沒有讀取成功就使用默認配置。
一切正常就進入while循環。在while循環中不停讀取串口buf中的數據到wrbuffer中,當wrbuffer大小夠大時就寫入SD卡。
測試
方法1:不同波特率發送1M大小txt文件給串口數據記錄儀,對比原始文件和日志文件內容。
方法2:單片機發送隨機數據給兩個串口數據記錄儀,長時間測試后對比兩個日志文件內容。