当前位置: 首页 > article >正文

串口数据记录仪DIY,体积小,全开源

作用

产品到客户现场出现异常情况,这个时候就需要一个日志记录仪、黑匣子,可以记录产品的工作情况,当出现异常时,可以搜集到上下文的数据,从而判断问题原因。

之前从网上买过,但是出现过丢数据的情况耽误了问题分析,自此以后就一直心有怀疑不敢全信它了,后面都是挂两个来交叉对比,生怕又被坑。于是索性乘自己有空来做一个。
在这里插入图片描述

软硬件开源地址: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");
	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函数。

	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:单片机发送随机数据给两个串口数据记录仪,长时间测试后对比两个日志文件内容。


http://www.kler.cn/a/580283.html

相关文章:

  • 代码随想录刷题day40|(二叉树篇)101.对称二叉树
  • langchain4j+PDFBox小试牛刀
  • flink cdc同步mysql数据
  • deepseek在pycharm中的配置和简单应用
  • 科技的成就(六十七)
  • 深度学习五大模型全解析:CNN、Transformer、BERT、RNN、GAN 的区别与联系,一文读懂!
  • osgEarth 加载MapBox的pbf瓦片数据
  • 控制--机器人模型--四旋翼无人机
  • 深入探索 Dubbo:高效的 Java RPC 框架
  • 化工厂防爆气象站:为石油化工、天然气等领域提供安全保障
  • (每日一题) 力扣 14 最长公共前缀
  • GreatSQL 8.0.32-27 GA (2025-3-10)
  • 几种linux获取系统运行时间的方法
  • java中泛型
  • Mysql主从复制和Mysql高可用以及负载均衡配置
  • 解锁下一代开发范式:IntelliJ Idea AI插件全景实战与未来展望
  • Python数据可视化自动化工具:让数据跃然纸上
  • King3399(ubuntu文件系统)Qt(C++)串口工具开发
  • 掌握Excel快捷键与函数公式,开启高效办公之旅
  • android如何实现OOM治理