思路分析

核心功能

host_node节点的核心逻辑就是承担着与用户交互和其它节点进行数据交互。与用户交互,我们无疑是使用USART通信了,与其他主机进行通信我们使用CAN。

业务功能

在这里我把传感器数据采集放到了这里,所以需要用到ADC。另外还要实时的显示当前时钟,以确保三个MCU的时间是同步的,所以还需要初始化RTC。

辅助功能

DMA+IDLE和DMA+ADC的自动化采集都是可以极大的节省CPU的开销。所以我们引入DMA。采集数据后,我们是通过OLED屏幕显示,我的屏幕是I2C屏幕,所以需要用到I2C(不过这里的代码是从店铺客服哪里获取的现成的不需要我们手写)。

USART初始化实现

在这里我想要专门展示一下USART的初始化实现,这个代码是我上个项目中写的,我觉得设计的很好。主要是通过宏定义,能够比较简单的开启和关闭对应的串口和接收数据方式。在我的上一个项目,项目整体都是使用这种宏的方式来控制。最后通过一个配置文件来控制对应宏的开启,以下是代码部分仅供参考。


#include "USART.h"



#ifndef USART_DMA_RECEIVE 			// 定义接收数据方式
#define USART_DMA_RECEIVE 	0		// 0 为中断接收数据(默认), 1为DMA接收数据
#endif 

#ifndef USARTID
#define USARTID 1
#endif


#if(USARTID & 0x01)
	extern char usart_buffer1[];
	extern volatile uint8_t usart_buffer1_idx;
	#define USART1_PORT 		GPIOA
	#define USART1_PIN_TX 		GPIO_Pin_9
	#define USART1_PIN_RX 		GPIO_Pin_10
	#define USART1_PORT_CLOCK 	RCC_APB2Periph_GPIOA
	#define USART1_CLOCK 		RCC_APB2Periph_USART1
#endif

#if(USARTID & 0x02)
	extern char usart_buffer2[];
	extern volatile uint8_t usart_buffer2_idx;
	#define USART2_PORT 		GPIOA
	#define USART2_PIN_TX 		GPIO_Pin_2
	#define USART2_PIN_RX 		GPIO_Pin_3
	#define USART2_PORT_CLOCK 	RCC_APB2Periph_GPIOA
	#define USART2_CLOCK 		RCC_APB1Periph_USART2
#endif


#if(USARTID & 0x04)
	
	extern char wifi_name[20];
	extern char paswd[20];
	extern volatile uint8_t Flag_count;
	volatile static char *ptr = wifi_name;
	#define USART3_PORT 		GPIOB
	#define USART3_PIN_TX 		GPIO_Pin_10
	#define USART3_PIN_RX 		GPIO_Pin_11
	#define USART3_PORT_CLOCK 	RCC_APB2Periph_GPIOB
	#define USART3_CLOCK 		RCC_APB1Periph_USART3
#endif

static void init_GPIO(uint32_t USART_PORT_CLOCK, uint16_t USART_PIN_TX, uint16_t USART_PIN_RX, GPIO_TypeDef* USART_PORT){
	RCC_APB2PeriphClockCmd(USART_PORT_CLOCK, ENABLE);
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStruct.GPIO_Pin = USART_PIN_RX;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
	GPIO_Init(USART_PORT, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStruct.GPIO_Pin = USART_PIN_TX;
	GPIO_Init(USART_PORT, &GPIO_InitStruct);
}


void init_USART(void){
	#if(USARTID & 0x01)
	init_GPIO(USART1_PORT_CLOCK, USART1_PIN_TX, USART1_PIN_RX, USART1_PORT);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
	#endif
	#if(USARTID & 0x02)
	init_GPIO(USART2_PORT_CLOCK, USART2_PIN_TX, USART2_PIN_RX, USART2_PORT);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
	#endif
	#if(USARTID & 0x04)
	init_GPIO(USART3_PORT_CLOCK, USART3_PIN_TX, USART3_PIN_RX, USART3_PORT);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
	#endif
	USART_InitTypeDef USART_InitStruct;
	
	USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
	USART_InitStruct.USART_Parity = USART_Parity_No;
	USART_InitStruct.USART_StopBits = USART_StopBits_1;
	USART_InitStruct.USART_WordLength = USART_WordLength_8b;
	#if(USARTID & 0x01)
		USART_InitStruct.USART_BaudRate = 115200;		// PC波特率
		USART_Init(USART1, &USART_InitStruct);
		USART_Cmd(USART1, ENABLE);
	
		#if (USART_DMA_RECEIVE == 1)
			USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE);
		#endif
	#endif
	#if(USARTID & 0x02)		
		USART_InitStruct.USART_BaudRate = 115200;			//wifi波特率
		USART_Init(USART2, &USART_InitStruct);
		USART_Cmd(USART2, ENABLE);
		#if (USART_DMA_RECEIVE == 1)
			USART_DMACmd(USART2,USART_DMAReq_Rx,ENABLE);
		#endif
	#endif
	#if(USARTID & 0x04)
		USART_InitStruct.USART_BaudRate = 9600;			// 蓝牙波特率
		USART_Init(USART3, &USART_InitStruct);
		USART_Cmd(USART3, ENABLE);
		#if (USART_DMA_RECEIVE == 1)
			USART_DMACmd(USART3,USART_DMAReq_Rx,ENABLE);
		#endif
	#endif
	
}

void USART_SendStr(USART_TypeDef* USARTX, const char *str){
	while(*str){
		while(USART_GetFlagStatus(USARTX, USART_FLAG_TXE) == RESET);
		USART_SendData(USARTX, *str++);
	}
}


void init_USART_NVIC(void){
	NVIC_InitTypeDef NVIC_InitStruct;
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 11;
	#if(USARTID & 0x01)
	NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
	NVIC_Init(&NVIC_InitStruct);
	
	#if USART_DMA_RECEIVE
	USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
	#else
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
	#endif
	
	#endif
	#if(USARTID & 0x02)
	NVIC_InitStruct.NVIC_IRQChannel = USART2_IRQn;
	NVIC_Init(&NVIC_InitStruct);
	
	#if USART_DMA_RECEIVE
	USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);
	#else
	USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
	#endif
	
	#endif
	#if(USARTID & 0x04)
	NVIC_InitStruct.NVIC_IRQChannel = USART3_IRQn;
	NVIC_Init(&NVIC_InitStruct);
	
	#if USART_DMA_RECEIVE
	USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);
	#else
	USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
	#endif
	
	#endif
	//__enable_irq(); // 打开CPU总中断

}

// 接收PC交互数据
#if(USARTID & 0x01)
#if(USART_DMA_RECEIVE == 1)
void USART1_IRQHandler(void){
	if(USART_GetITStatus(USART1, USART_IT_IDLE) == SET){
		
		// 清除IDLE标志位,必须遵循特定的软件读取序列
		volatile uint32_t temp;
		temp = USART1->SR; 
        temp = USART1->DR; 
        (void)temp;
		
		// 计算实际接收长度
        // uint16_t len = BUFFERSIZE - DMA_GetCurrDataCounter(USART1_RX_CHANNEL);
		
		DMA_Cmd(USART1_RX_CHANNEL, DISABLE);
		DMA_SetCurrDataCounter(USART1_RX_CHANNEL, BUFFERSIZE);
		DMA_Cmd(USART1_RX_CHANNEL, ENABLE);
		
		BaseType_t Flag = pdFALSE;
		USART_ClearITPendingBit(USART1, USART_IT_RXNE); 
		vTaskNotifyGiveFromISR(Task_USART1Receive_handle, &Flag);
		portYIELD_FROM_ISR(Flag);
	}

}
#else
void USART1_IRQHandler(void){
	if( USART_GetITStatus(USART1, USART_IT_RXNE) == SET){
		char ch = USART_ReceiveData(USART1);
		if(ch == '\r' || ch == '\n') {
			usart_buffer1[usart_buffer1_idx] = '\0';
			usart_buffer1_idx= 0;
			BaseType_t Flag = pdFALSE;
			USART_ClearITPendingBit(USART1, USART_IT_RXNE); 
			vTaskNotifyGiveFromISR( vAnalyseDataFromUSART1_handle, &Flag);
			portYIELD_FROM_ISR(Flag);
		}
		else {
		  if(usart_buffer1_idx < USART_BUFFERSIZE  - 1) { 
                usart_buffer1[usart_buffer1_idx++] = ch;
			}
		  else {
                usart_buffer1[usart_buffer1_idx] = '\0'; 
                usart_buffer1_idx= 0;
			}
		}
	}
}
#endif

#endif
// 接收USART2数据并处理
#if(USARTID & 0x02)
#if(USART_DMA_RECEIVE == 1)
void USART2_IRQHandler(void){
	USART1_SNED_FROM flag = USART1_SEND_FROM_USART2_RECEIVE;
	if(USART_GetITStatus(USART2, USART_IT_IDLE) == SET){
		
		// 清除IDLE标志位,必须遵循特定的软件读取序列
		volatile uint32_t temp;
		temp = USART2->SR; 
        temp = USART2->DR; 
        (void)temp;
		
		// 计算实际接收长度
        // uint16_t len = BUFFERSIZE - DMA_GetCurrDataCounter(USART1_RX_CHANNEL);
		
		DMA_Cmd(USART2_RX_CHANNEL, DISABLE);
		DMA_SetCurrDataCounter(USART2_RX_CHANNEL, BUFFERSIZE);
		DMA_Cmd(USART2_RX_CHANNEL, ENABLE);
		BaseType_t Flag = pdFALSE;
		xQueueSendFromISR(Task_USART1Send_QueueHandle, &flag, &Flag);
		portYIELD_FROM_ISR(Flag);
	}
}
#else
void USART2_IRQHandler(void){
	if( USART_GetITStatus(USART2, USART_IT_RXNE) == SET){
		char ch = USART_ReceiveData(USART2);
		
		if(ch == '\r' || ch == '\n') {
			//usart_buffer2[usart_buffer2_idx] = '\0';    
			usart_buffer2_idx= 0;
			BaseType_t Flag = pdFALSE;
			USART_ClearITPendingBit(USART2, USART_IT_RXNE); 
			vTaskNotifyGiveFromISR( vWifiSendData2PC_handle, &Flag);
			portYIELD_FROM_ISR(Flag);
		}
		else {
		  if(usart_buffer2_idx < USART_BUFFERSIZE  - 1) { 
                usart_buffer2[usart_buffer2_idx++] = ch;
			}
		  else {
                usart_buffer2[usart_buffer2_idx] = '\0'; 
                usart_buffer2_idx= 0;
			}
		}
	
	}
}
#endif
#endif
// 接收蓝牙数据并处理
#if(USARTID & 0x04)
void USART3_IRQHandler(void){
	if( USART_GetITStatus(USART3, USART_IT_RXNE) == SET){
		char ch = USART_ReceiveData(USART3);
		switch(ch){
			case '!': Flag_count++;break;
			case '=': ptr = wifi_paswd;break;
			default : *ptr++ = ch;
		}
		if(Flag_count == 2){
			*ptr = '\0';
			ptr = wifi_name;
			BaseType_t Flag = pdFALSE;
			USART_ClearITPendingBit(USART1, USART_IT_RXNE); 
			vTaskNotifyGiveFromISR(vBlueTooth_handle, &Flag);
			portYIELD_FROM_ISR(Flag);
		}
	}
}
#endif

实现逻辑

拆解完功能,我们也就清楚了需要哪些外设,下面就可以进一步细化如何实现。首先我们引入了FreeRTOS系统,所以需要明确至少要有哪些任务,然后根据我们实际的业务需求再继续增加。

首先是我们的核心功能,USART的收发和CAN的收发。

USART相关实现

先以USART为例,在该项目中我们只启用了USART1与PC端相连同用户进行交互。USART接收完我们的数据,肯定是需要解析我们的指令,然后再执行或发送相应的命令,所以我们需要有一个USART数据接收后的处理任务。另外就是我们使用的是DMA+IDLE的方式,所以需要在设置好IDLE中断并在IDLE中断中唤醒该任务,为了节省开销采用任务通知的方式。所以就还需要该任务的句柄,另外就是可以对接收的数据解析封装成一个函数,但是在我这比较简单所以没有封装。这里的功能不固定,我也是随用随加的。

// USART1处理接收数据任务
void Task_USART1Receive(void *arg){
	USART1_SNED_FROM flag = USART1_SNED_FROM_USART1_RECEIVE; 
	// 调试用
	UBaseType_t uxHighWaterMark;
	uint32_t ulUsedBytes;
	while(1){
		if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) > 0){
			// 测试,USART2打印输入的字符串
			if(!memcmp(buffer1, "text:", 5)){
				xQueueSend(Task_USART1Send_QueueHandle, &flag, portMAX_DELAY);
			}
			// 发送时间校验帧
			else if(!memcmp(buffer1, "jy:",3)){
				sscanf(buffer1,"jy:%ul", &timp.ts);
				timp.ts += 8 * 60 * 60;
				xTaskNotifyGive(Task_CANSend_handle);
			}
			// 改变LED状态
			else if(!memcmp(buffer1, "cmdl", 4)){
				command = CON_LED;
				xTaskNotifyGive(Task_CANSend_handle);
			}
			// 开启通风
			else if(!memcmp(buffer1, "open", 4)){
				command = CON_Servo_OPEN;
				xTaskNotifyGive(Task_CANSend_handle);
			}
			// 关闭通风
			else if(!memcmp(buffer1, "clos", 4)){
				command = CON_Servo_CLOSE;
				xTaskNotifyGive(Task_CANSend_handle);
			}
			
			// 调试功能
			// 创建任务时的栈大小参数(usStackDepth)单位是字(Word),而不是字节, 128 * 4 = 512
			else {
				uxHighWaterMark = uxTaskGetStackHighWaterMark(Task_USART1Receive_handle);
				ulUsedBytes = (configMINIMAL_STACK_SIZE - uxHighWaterMark) * sizeof(StackType_t);
				format_string(buffer1, BUFFERSIZE, "Stack MAX: %lu Bytes", ulUsedBytes);
				xQueueSend(Task_USART1Send_QueueHandle, &flag, portMAX_DELAY);
			}
		
		}
	}
}

关于USART数据发送,从上面的代码我们看到了已经封装好了一个发送字符串的函数,又需要直接可以在任务中发送就可以了。这在小项目中不会有问题,但是如果是一个相对复杂的项目就会出现问题,不妨试想一下,一个任务正在发送100个字节的数据,发送到第80个字节的时候,突然被高优先级任务抢占,而高优先级任务也要发送数据…所以,我们需要为发送数据也设置一个任务用来管理数据发送的顺序,这里就适合用消息队列进行管理了,但是对于消息队列的设计,分享一个自己的小巧思。消息队列接收枚举类型,然后定义一个函数,对枚举类型进行匹配,将对应的缓冲区数据搬运到发送缓冲区中,这样做可能会多浪费一些空间给很多任务做缓冲区,但是这样做的同步效果也是很明显的,下面是该部分代码的核心逻辑。

// USART1发送任务
void Task_USART1Send(void *arg){
	BaseType_t Ret = pdFAIL;
	// 调试用 112
	UBaseType_t uxHighWaterMark;
	uint32_t ulUsedBytes;
	while(1){
		Ret = xQueueReceive(Task_USART1Send_QueueHandle, &Task_USART1Send_From, portMAX_DELAY);
		if(Ret == pdPASS){
			match_usart1_send_from();
		}
		else {
			// 调试用(查看水位线)
			uxHighWaterMark = uxTaskGetStackHighWaterMark(Task_USART1Receive_handle);
			ulUsedBytes = (configMINIMAL_STACK_SIZE - uxHighWaterMark) * sizeof(StackType_t);
			format_string(buffer_send1, BUFFERSIZE, "Stack MAX: %lu Bytes", ulUsedBytes);
		}

		// 发送数据
		USART_SendStr(USART1, buffer_send1);
		// 重置标志位
		Task_USART1Send_From = USART1_SNED_FROM_NONE;
		// 清空缓冲区
		clear_buffer(buffer_send1, BUFFERSIZE);
	}
} 

// USART1Send枚举类型
typedef enum{
	USART1_SNED_FROM_NONE,
	USART1_SNED_FROM_USART1_RECEIVE,
	USART1_SEND_FROM_CANReceive,
	USART1_SEND_FROM_CHECK
}USART1_SNED_FROM;
// 标志位
extern USART1_SNED_FROM Task_USART1Send_From;
// 匹配函数
void match_usart1_send_from(void){
	switch(Task_USART1Send_From){
		case USART1_SNED_FROM_NONE: break;
		
		case USART1_SNED_FROM_USART1_RECEIVE:{
			format_string(buffer_send1, BUFFERSIZE, "%s", buffer1 + 5);
			clear_buffer(buffer1, BUFFERSIZE);
		};break;
		
		case USART1_SEND_FROM_CANReceive:{
			format_string(buffer_send1, BUFFERSIZE, "CAN Receive:%s", CAN_rbuffer);
			clear_buffer(CAN_rbuffer, 64);
		}break;
		
		case USART1_SEND_FROM_CHECK:{
			format_string(buffer_send1, BUFFERSIZE, "Sensor Check:%s", check_buffer);
			clear_buffer(check_buffer, 32);
		}break;
		
		default:
			clear_buffer(buffer1, BUFFERSIZE);
	}

}

CAN相关实现

其实和USART的思路大差不差,同样需要两个任务,不同的是,CAN的数据收发比较固定,不需要太多的逻辑,因为他就是负责与节点的通信。
关于接收数据,在上一篇中我们就得值,host_node节点只会接收到时间帧同步时间,所以这个任务只是将接收的时间戳给RTC同步就可以了。

关于CAN的数据发送,在这里我也建议刚接触CAN的同学用这样的方式去写。

// 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){
		if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) > 0){
			if(command == CON_NONE){				// 时间校验帧
				CAN_SendMsg(&sendStruct[2], (char*)timp.ts_8);
			}
			else{								// 控制帧
				if(command == CON_LED) CAN_SendMsg(&sendStruct[0], "cled");
				else if(command == CON_Servo_OPEN) CAN_SendMsg(&sendStruct[0], "open");
				else if(command == CON_Servo_CLOSE) CAN_SendMsg(&sendStruct[0], "clos");
			}
			command = CON_NONE;
		}
	}
}

因为我的项目比较简单,帧的类型不多,我就在我所有的节点实现的代码中的这部分初始化了四种帧格式,这样就发送起来简单,另外就是不需要来回的看和翻找确认了,尽管这对于程序来讲是一个无意义且浪费资源的行为,但是可以极大的便利我们,当然注释放在这里一样可以提醒,但是这样还有另外一个好处就是,我们做环回测试的时候,可以更方便的在一个MCU配置过滤器。
以上就是核心功能的实现了!

接下来是业务功能的拆解
这里就是展示ADC的数据采集和当前的时间,因为他们都是输出到OLED屏幕上可以放到一个任务中,因为时间戳是RTC自动累加的,ADC我们也是配合DMA自动化输出的,所以这部分任务很简单,我们只需要调用相关函数输出到OLED就好了。在这里可能需要看一下C语言的time.h,我也是现看现用的,并不复杂

void Task_ShowTime(void *arg){
	td time_and_date;
	char sensor_data[15];
	struct tm *time_info;
	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(sensor_data, 15,"[%4u] [%4u]", ADC_buffer[0], ADC_buffer[1]);
		OLED_ShowString(4,1,sensor_data);
		vTaskDelay(pdMS_TO_TICKS(20));
		
	}
}

除此之外我们还应该实现一个功能,那就是根据采集到的传感器数据进行相应的控制,比如温度高了就通风,温度低了就关闭通风,这里我使用的是电位器,所以就简单的用模拟量代替这个功能。另外就是需要一个注意的点。就是我们检查完一次之后应该给一些时间让控制节点去处理,不能一直去触发,而且正常应该是每隔时间检查一次,到这里可能会有些处理不好这个时间关系,那么我们可以发现异常之后不去检查,并设置一个闹钟,闹钟计时好了再开启检查。

// 检查环境
void Task_CheckEnviroment(void *arg){
	uint8_t check = 1;
	USART1_SNED_FROM flag = USART1_SEND_FROM_CHECK;
	uint8_t trigger = 0;
	while(1){
		if(check){
			// 过热
			if(ADC_buffer[0] > 3000 || ADC_buffer[1] > 3000 ){
				command = CON_Servo_OPEN;
				format_string(check_buffer, 32, "Exceeds the threshold!\n");
				trigger  =  1;
			}
			// 过冷
			else if(ADC_buffer[0] < 1000 || ADC_buffer[1] < 1000){
				command = CON_Servo_CLOSE;
				format_string(check_buffer, 32, "Below the threshold!\n");
				trigger  =  1;
			}
			else{
				format_string(check_buffer, 32, "[checking]Everythinfg is normal!\n");
				trigger  =  0;
			}
			// 出现异常则控制节点转动舵机
			if(trigger){
				xTaskNotifyGive(Task_CANSend_handle);
				// 一段时间内停止检查
				check = 0;
				// 设置闹钟5分钟后恢复
				RTC_SetAlarm(tim + 5 * 60);
			}
			xQueueSend(Task_USART1Send_QueueHandle, &flag, portMAX_DELAY);
		}
		else{
			if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) > 0){
				// 等待闹钟唤醒
				check = 1;
				trigger = 0;
			}
		}
		// 每秒检查一次
		vTaskDelay(pdMS_TO_TICKS(1000));
	}
}

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

Logo

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

更多推荐