关于log_node节点的代码,因为一些原因变的很简单了我们先说一下当前的代码,等之后在扩展一下,最开始想要实现的效果。

思路分析

核心功能

该节点现在承担的工作只有CAN的数据收发,只会接收两种数据帧,一种是来自lp_node的电器状态帧,一个是来自host_node的时间校验帧。而自己发送只需要周期性的发送一个时间帧同步一下系统的时间,所以这部分就还是很简单,这是我第二个完成的节点,所以我基本上只是复制过来的host_node节点的代码,然后删删改改。以下是这部分的两个任务

// 处理CAN接收数据
// host -> 时间帧同步	log -> 时间校验帧 电器状态帧  lp -> 控制帧 时间帧
uint8_t led = 5;
uint8_t servo = 5;
void Task_CANReceive(void *arg){

	while(1){
		if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) > 0){
			/*	调试用
			format_string(CAN_rbuffer, 64,"StdID:%lu \nExtId: %lu \nIDE:%u \nRTR:%u \nDLC:%u \nData:%s \nFMI:%u", 
				can_rxstruct.StdId, can_rxstruct.ExtId, can_rxstruct.IDE, can_rxstruct.RTR, can_rxstruct.DLC
				,can_rxstruct.Data, can_rxstruct.FMI);
			*/
			if(can_rxstruct.StdId == 0x333){
				strncpy((char *)timp.ts_8, (char *)can_rxstruct.Data, 4);
				RTC_SetCounter(timp.ts);
				RTC_WaitForLastTask();
			}
			else if(can_rxstruct.StdId == 0x444){
				led = can_rxstruct.Data[3];
				servo = can_rxstruct.Data[7];
			}
			
			OLED_ShowString(4,1,"Receive");
		}
	
	}

}
// CAN发送数据任务
void Task_CANSend(void *arg){
	CanTxMsg sendStruct[] = {
/*   StdId     ExtId         IDE             RTR        DLC         Data[8]          */
	{0x111, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   3, {0xAA, 0xBB, 0xCC}},	  // 控制帧
	{0x222, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {'a', 'b', 'c', 'd'}},    // 时间帧
	{0x333, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x00, 0x00, 0x00, 0x00}},// 时间校验帧
	{0x444, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   8, {0x00, 0x00, 0x00, 0x00}},// 电器状态帧
};	
	
	while(1){
		timp.ts = tim;
		
		CAN_SendMsg(&sendStruct[1], (char*)timp.ts_8);   	// 时间帧
		
		vTaskDelay(pdMS_TO_TICKS(10000));//	每5s校验一次 
		OLED_Clear();
	}
}

业务功能

在这部分代码中,可以看到我定义了两个全局变量,这个就是后面修改的内容,本来打算接收电器状态,生成日志,但是硬件资源有限,只能在OLED屏幕上显示一下电器状态,充当日志了。
除此之外我们还要显示系统时间,这个部分基本上三个MCU都是一致的,所以不过多阐述

td time_and_date;
struct tm *time_info;
void Task_ShowTime(void *arg){
	char buff[15];
	while(1){
		tim = RTC_GetCounter();
		RTC_WaitForSynchro();
		OLED_ShowUnsignedNum(1, 1, tim, 10);
		time_info = localtime(&tim);
		time_and_date.year = time_info->tm_year + 1900; // tm_year 是从 1900 年开始计算的偏移量
		time_and_date.mon = time_info->tm_mon + 1;      // tm_mon 是 0-11
		time_and_date.day = time_info->tm_mday;
						
		// 直接格式化时间,避免生成完整的 asctime 字符串
		snprintf(time_and_date.time, sizeof(time_and_date.time), "%02d:%02d:%02d", 
         time_info->tm_hour, time_info->tm_min, time_info->tm_sec);
		
		OLED_ShowUnsignedNum(2,1,time_and_date.year, 4);
		OLED_ShowUnsignedNum(2,6,time_and_date.mon, 2);
		OLED_ShowUnsignedNum(2,9,time_and_date.day,2);
		OLED_ShowString(3,1,time_and_date.time);
		format_string(buff,15, "led:%u ser:%u\0", led, servo);
		OLED_ShowString(4,1,buff);
		vTaskDelay(pdMS_TO_TICKS(10));
	}
}

以上就是这个部分的全部任务了,就只有三个,可以说都不需要引入FreeRTOS就可以很好的完成。如果你看源码的话,任务中还有个ADC的任务,但是没有创建该任务。因为把该功能移交到host_node了,但是我想以后我可能还会扩展该节点,所以也就没有删除。

扩展

我是因为硬件资源有限,不能很好的查看该节点的数据存储情况,所以实现的很简陋,在项目写完后,想到查看这里面数据的办法,所以下面和大家讲述一下该节点在我设计时的思路。上面演示过的就不再赘述了。

原版核心功能应该还有一个日志系统,使用W25Q64保存数据。关于这方面的实现,我们首先要先清楚,这是flash编程,写入要一页一页的写,那么就是256字节,而flash编程是先擦后写,擦除又是以扇区为单位,每个扇区是4096字字节,也就是每十六页就需要写入下一个扇区了。
这部分就是外设的操作,也就是我们的核心功能逻辑,然后基于此我们要继续盘一盘业务功能逻辑。

业务功能,那么主要就是怎么样存储日志,存储什么样的日志格式,那么就是简单的时间戳+电器状态就可以了。然后传感器数据采集任务也可以放到这里了,还可以加一个传感器数据。
格式就可以敲定了

char state_log[] = "<日期,时间> LED:LED   Servo:Servo状态,"
char sensor_log[] = "<日期,时间> PA0 = 传感器数据  PA1 = 传感器数据"
char sync_log[] = "<日期,时间> sync System Time :时间戳"

基本上这三个就足够了,然后就是写入W25Q64当中,另外还有提供一个读出W25Q64的内容。
关于这部分我的思路是采用一个256字节的缓冲区来完成,刚好1页的大小,当缓冲区剩余容量不能写入下一条日志信息的时候,就把缓冲区内容写入W25Q64,这样做的好处一个是磨损均衡,另外一个是不需要麻烦的定位操作,只需要记录下一个要写入的页号就可以了,这样做擦除扇区的时机也比较好确定。尽管可能会造成一些资源浪费,但是这样操作简便了很多,我们读取数据也是直接读取一页就可以了。

// --------------W25Q64-----------------
#ifdef USE_SPI
	#define SPIX SPI2
	#define SPI_PORT GPIOB
	#define SPI_MOSI GPIO_Pin_15
	#define SPI_MISO GPIO_Pin_14
	#define SPI_SCL	 GPIO_Pin_13
	#define SPI_CSS	 GPIO_Pin_12
	#define W25Q64_PAGESIZE 256					// 一页大小
	#define W25Q64_SECTOR_SIZE 4096				// 一块大小
	#define W25Q64_SECTOR_NUM  2048				// 块的数量
	extern char W25Q64_buffer[];				// 设置一个数据缓冲区,进行一页数据的交互
	extern uint32_t W25Q64_buffer_ptr ;			// 缓冲区下一次写入位置
	extern uint16_t W25Q64_current_page ;		// 记录当前指针所在页	
	extern uint16_t total_log_num;				// 记录log数量
	extern uint16_t current_erase_sector;		// 准备擦除块 
	extern uint32_t W25Q64_data_start;				// 数据起始地址
	extern uint32_t W25Q64_data_end;				// 数据结束地址
	
#endif

下面是写入的一些逻辑
核心实现要有三个,一个是格式化写入,一个是判断log类型,一个是扇区擦除时机,但是因为我加入了一个缓冲区,所以在这里我还要加一个将缓冲区数据搬运到W25Q64当中

// 擦除W25Q64的下一块扇区
void vErase_W25Q64_Sector(void *arg){
	
	while(1){
		if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) > 0){
			EraseSector((current_erase_sector % W25Q64_SECTOR_NUM)   *  W25Q64_SECTOR_SIZE);
			current_erase_sector++;
		}
	}
}
// 格式化日志类型
// 在这里我考虑到以后可能会有多种日志情况所以加了一个互斥锁
// 比如出现什么报警事件,在报警任务内部实现日志,之后再同步写入W25Q64
void vfromatLog(void *arg){
	while(1){
		//等待锁
		xSemaphoreTake(PrintLog_Mutex, portMAX_DELAY);
		if(flag = 1) format_log(STATE_LOG);
		else if(flag = 2)format_log(SENSOR_LOG);
		else format_log(SYNC_LOG);
		xTaskNotifyGive(vPrintLog_handle);
		// 释放锁
		xSemaphoreGive(PrintLog_Mutex);
		// 10s 打印一次日志
		vTaskDelay(pdMS_TO_TICKS(10000));
	}
}

// 输出log信息
void vPrintLog(void *arg){
	// 释放自己的信号量
	xSemaphoreGive(PrintLog_Mutex);
    uint8_t len;
	xTaskNotifyGive(vErase_W25Q64_Sector_handle); // 先擦除一片扇区
	while(1) {
		if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) > 0){		
			// 连接服务器则向服务器输出
			// 计算log长度
			len = strlen(log_buffer);
			// 日志输入到W25Q64缓冲区
			format_string(W25Q64_buffer + W25Q64_buffer_ptr , W25Q64_PAGESIZE, "%s", log_buffer);
			// 更新指针
			W25Q64_buffer_ptr += len;
			// 日志数量
			total_log_num++;  // log总数+1
				// 如果缓冲区不足,则将缓冲去内容输入W25Q64中(数据长度加当前指针 > 页大小)
			if(len + W25Q64_buffer_ptr >  W25Q64_PAGESIZE){
				xTaskNotifyGive(vMovebuffer2W25Q64_handle);
				W25Q64_current_page++;
			}
		}
	}
}
// 将缓冲区数据搬运到W25Q64
void vMovebuffer2W25Q64(void *arg){
	while(1){
		if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) > 0){
			W25Q64WriteStr(W25Q64_data_end, W25Q64_buffer, W25Q64_PAGESIZE);
			W25Q64_data_end += W25Q64_PAGESIZE;		// 更新W25Q64数据尾指针
			W25Q64_buffer_ptr = 0;	
		
			// 清除缓冲区
			clear_buffer(log_buffer, LOG_BUFFERSIZE);
			clear_buffer(W25Q64_buffer, W25Q64_PAGESIZE);
			// 判断是否需要准备新的扇区
			if(((W25Q64_current_page + 1) * W25Q64_PAGESIZE > current_erase_sector * W25Q64_SECTOR_SIZE)) {
				xTaskNotifyGive(vErase_W25Q64_Sector_handle);
			}
		
		}
	}
}

以上基本上就是我的日志系统的逻辑了,这部分实现可能会有问题,毕竟不是结合实际写的,但是思路差不多就是这样了。这样就可以比较完善的记录我们的日志信息。

另外我们需要读取日志的话,可以通过串口相连,当前我们host_node节点只使用了USART1,引脚资源还是很富裕的,所以我们可以将log_node节点的USART1和host_node节点的USART2交叉相连,用来做一个专门的log信息通路,那么这里的实现就比较单一了,不需要很复杂的判断逻辑。
host_node:只发送一个数字就好了,代表查看第几页的日志;接收就是这一页的信息,直接用USART1输出USART2的接收缓冲区

log_node:只发送指定页的日志信息,加一个判断如果越界,那么就发送给host_ndoe一个“ERROR: the {数字}th page have no log!\n”,数据接收就是一个字符串格式的数字,转成无符号就可以了。

以上呢就是该节点的全部逻辑拆解了。在该文章中只贴了任务部分的一些代码,具体代码可以在我的gitee仓库自取,仓库地址:https://gitee.com/xingyexiakong/stm32project_-smart-green-house

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐