作用
产品到客户现场出现异常情况,这个时候就需要一个日志记录仪、黑匣子,可以记录产品的工作情况,当出现异常时,可以搜集到上下文的数据,从而判断问题原因。
之前从网上买过,但是出现过丢数据的情况耽误了问题分析,自此以后就一直心有怀疑不敢全信它了,后面都是挂两个来交叉对比,生怕又被坑。于是索性乘自己有空来做一个。
软硬件开源地址:https://gitee.com/qlexcel/serial-port-data-logger
硬件


尺寸长38mm,宽21mm
使用
找一张TF卡,格式化为FAT32格式。没有的话可以网上买,8G的8块钱包邮。比如: TF卡
记录仪开机的时候会读取SD中的配置文件config.txt,获取用户配置。如果没有读取成功或者参数异常,就会恢复默认配置。
恢复默认配置后,config.txt文件内容就会被覆盖为以下内容:
c
baud:115200,stopbit:0,parity:0,filemb:30,time:0,timeout:4,filename:log,
来解释下配置的作用:
c
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函数代码,功能实现都在这里
c
#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");
else
printf("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); //文件编号+1
FileNameIndex++;
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; //hours
Date[4]=tmp/10+'0';
Date[5]=tmp%10+'0';
tmp=DateWr%3600/60; //minutes
Date[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);
}
else
res=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.txt
FileName[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;
else
return 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;
}
else
return out;
}
}
if (dot == 2)
out *= 100;
return out;
}
可以看到main函数中,先初始化IO、RTC后,就执行Filesystem_Handle函数。
c
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");
else
printf("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:单片机发送随机数据给两个串口数据记录仪,长时间测试后对比两个日志文件内容。