基于HAL库STM32串口驱动不定长数据接收_hal库串口接收不定长数据(1)

72 阅读11分钟

* * DESCRIPTION:-- */ #ifdef __cplusplus //use C compiler extern "C" { #endif #include "UART_Port.h"/*外部接口*/ #define USE_LOOPBACK 1 #define DEBUG_UART &huart1 /* External variables --------------------------------------------------------*/ extern UART_HandleTypeDef huart1; extern UART_HandleTypeDef huart2; extern UART_HandleTypeDef huart3; extern UART_HandleTypeDef huart4; extern UART_HandleTypeDef huart5; extern UART_HandleTypeDef huart6; extern DMA_HandleTypeDef hdma_usart1_rx; extern DMA_HandleTypeDef hdma_usart1_tx; extern DMA_HandleTypeDef hdma_usart2_rx; extern DMA_HandleTypeDef hdma_usart2_tx; extern DMA_HandleTypeDef hdma_usart3_rx; extern DMA_HandleTypeDef hdma_usart3_tx; extern DMA_HandleTypeDef hdma_usart4_rx; extern DMA_HandleTypeDef hdma_usart4_tx; extern DMA_HandleTypeDef hdma_usart5_rx; extern DMA_HandleTypeDef hdma_usart5_tx; extern DMA_HandleTypeDef hdma_usart6_rx; extern DMA_HandleTypeDef hdma_usart6_tx;

static uint8_t get_uart_index(USART_TypeDef *Instance); static Uart_Dev_info_t *Create_Uart_Dev(Uart_num_t uart_num ,UART_HandleTypeDef *huart ,DMA_HandleTypeDef *hdma_rx ,uint16_t rx_temp_size ,uint32_t rxsize ,int work_mode ,osSemaphoreId *pRX_Sem);

/*预定义串口设备信息*/ Uart_Dev_info_t *Uart_pDevice[UART_MAX_NUM+1]; /** ****************************************************************** * @brief 初始化串口设备信息 * @author aron66 * @version v1.0 * @date 2020/3/15 ****************************************************************** */ void Uart_Port_Init(void) { Uart_pDevice[UART_NUM_1] = Create_Uart_Dev(UART_NUM_1 ,&huart1 ,&hdma_usart1_rx ,128 ,128 ,0 ,NULL); Uart_pDevice[UART_NUM_2] = Create_Uart_Dev(UART_NUM_2 ,&huart2 ,&hdma_usart2_rx ,128 ,128 ,0 ,NULL);

}

/** ****************************************************************** * @brief 建立串口设备,为其建立双缓冲区-->使能串口空闲中断 * @param 串口号 串口设备指针 dma操作地址 ,临时缓冲大小 接收队列大小 工作模式 二值信号量(适用于带FreeRTOS操作系统的工程) * @author aron66 * @version v1.0 * @date 2020/3/15 ****************************************************************** */ static Uart_Dev_info_t *Create_Uart_Dev(Uart_num_t uart_num ,UART_HandleTypeDef *huart ,DMA_HandleTypeDef *hdma_rx ,uint16_t rx_temp_size ,uint32_t rxsize ,int work_mode ,osSemaphoreId *pRX_Sem) { Uart_Dev_info_t *pUart_Dev = (Uart_Dev_info_t *)malloc(sizeof(Uart_Dev_info_t)); pUart_Dev->phuart = huart; pUart_Dev->phdma_rx = hdma_rx; pUart_Dev->cb = cb_create(rxsize); pUart_Dev->MAX_RX_Temp = rx_temp_size; pUart_Dev->RX_Buff_Temp = (uint8_t *)malloc(sizeof(uint8_t)*rx_temp_size); if(NULL == pUart_Dev->RX_Buff_Temp) { return NULL; } pUart_Dev->Is_Half_Duplex = work_mode; pUart_Dev->pRX_Sem = pRX_Sem;

//打开空闲中断
\_\_HAL\_UART\_ENABLE\_IT(huart,UART_IT_IDLE);
//使能DMA接收
HAL\_UART\_Receive\_DMA(huart, pUart_Dev->RX_Buff_Temp, pUart_Dev->MAX_RX_Temp);
return pUart_Dev;

}

/************************************************************ * @brief Rx Transfer IRQ * @param huart UART handle. * @return None * @author aron66 * @date 2020/3/15 * @version v1.0 * @note @@ ***********************************************************/ void USER_UART_IRQHandler(UART_HandleTypeDef *huart) { uint8_t index = get_uart_index(huart->Instance); if(index != 0) { if((__HAL_UART_GET_FLAG(Uart_pDevice[index]->phuart ,UART_FLAG_IDLE) != RESET)) { /* 首先停止DMA传输, 1.防止后面又有数据接收到,产生干扰,因为此时的数据还未处理。 2.DMA需要重新配置。 */ HAL_UART_DMAStop(Uart_pDevice[index]->phuart); /*清楚空闲中断标志,否则会一直不断进入中断*/ __HAL_UART_CLEAR_IDLEFLAG(Uart_pDevice[index]->phuart); /*计算本次接收数据长度*/ uint32_t data_length = Uart_pDevice[index]->MAX_RX_Temp - __HAL_DMA_GET_COUNTER(Uart_pDevice[index]->phdma_rx); /*将数据记录至环形区*/ CQ_putData(Uart_pDevice[index]->cb ,Uart_pDevice[index]->RX_Buff_Temp ,(uint32_t)data_length); #if USE_LOOPBACK HAL_UART_Transmit(DEBUG_UART, (uint8_t *)Uart_pDevice[index]->RX_Buff_Temp,(uint16_t)data_length,0xFFFF); #endif /*清空临时缓冲区*/ memset(Uart_pDevice[index]->RX_Buff_Temp ,0 ,data_length); data_length = 0; /*打开空闲中断*/ __HAL_UART_ENABLE_IT(Uart_pDevice[index]->phuart ,UART_IT_IDLE); /*重启开始DMA传输*/ HAL_UART_Receive_DMA(Uart_pDevice[index]->phuart ,Uart_pDevice[index]->RX_Buff_Temp, Uart_pDevice[index]->MAX_RX_Temp); } } }

/*获得当前串口信息索引*/ static uint8_t get_uart_index(USART_TypeDef *Instance) { uint8_t index = 0; for(;index < UART_MAX_NUM+1;index++) { if(Uart_pDevice[index]->phuart->Instance == Instance) { return index; }
} return 0; }

#if (USE_NEW_REDIRECT == 0) #include "stdio.h" /************************************************* * 函数功能: 重定向c库函数printf到HAL_UART_Transmit * 输入参数: 无 * 返 回 值: 无 * 说 明:无 */ int fputc(int ch, FILE *f) { HAL_UART_Transmit(DEBUG_UART, (uint8_t *)&ch, 1, 10);//原来使用阻塞式传输 return ch; } /** * 函数功能: 重定向c库函数getchar,scanf * 输入参数: 无 * 返 回 值: 无 * 说 明:无 */ int fgetc(FILE * f) { uint8_t ch = 0; while(HAL_UART_Receive(DEBUG_UART,&ch, 1, 0xffff)!=HAL_OK); return ch; }

#else /*新式重定向*/ #include "stdio.h" int __io_putchar(int ch) { HAL_UART_Transmit(DEBUG_UART ,()uint8_t)&ch ,1 ,0xFFFF); return ch; } int __write(int file, char *ptr, int len) { int DataIdx; for(DataIdx = 0; DataIdx < len; DataIdx++) { __io_putchar(*ptr++); } return len; } #endif

#ifdef __cplusplus //end extern c } #endif


`UART_Port.h`文件



/* * FILE: UART_Port.h * * Created on: 2020/2/22 * * Author: aron566 * * DESCRIPTION:-- */ #ifndef UART_PORT_H #define UART_PORT_H #ifdef __cplusplus //use C compiler extern "C" { #endif /*库接口*/ #include <stdlib.h> #include <stdio.h> #include <stdint.h> #include <string.h> /*外部接口*/ #include "stm32f1xx_hal.h" #include "usart.h" #include "cmsis_os.h" /*内部接口*/ #include "CircularQueue.h"

#define UART_MAX_NUM 6

typedef enum { UART_NUM_0 = 0, UART_NUM_1, UART_NUM_2, UART_NUM_3, UART_NUM_4, UART_NUM_5, UART_NUM_6, }Uart_num_t;

typedef struct { UART_HandleTypeDef *phuart; //uart端口 DMA_HandleTypeDef *phdma_rx; CQ_handleTypeDef *cb; //环形队列 uint8_t *RX_Buff_Temp; //接收缓冲 uint16_t MAX_RX_Temp; //最大接收数量 int Is_Half_Duplex; //半双工模式 osSemaphoreId *pRX_Sem; //接收二值信号量,如果没有使用FreeRTOS则屏蔽即可 }Uart_Dev_info_t;

void Uart_Port_Init(void); void USER_UART_IRQHandler(UART_HandleTypeDef *huart); #ifdef __cplusplus //end extern c } #endif #endif


### 环形缓冲区接口文件


`CircularQueue.c`文件



/** * @file CircularQueue.c * * @date 2020/6/25 * * @author aron566 * * @copyright None * * @brief None * * @details None * * @version v1.1 */ #ifdef __cplusplus ///<use C compiler extern "C" { #endif /** Includes -----------------------------------------------------------------*/ /* Private includes ----------------------------------------------------------*/ #include "CircularQueue.h" #if USE_LINUX_SYSTEM #include <sys/types.h> #endif /** Private typedef ----------------------------------------------------------*/ /** Private macros -----------------------------------------------------------*/ /** * @name 返回值定义 * @{ */ #define TRUE true #define FALSE false /** @}*/ /** Private constants --------------------------------------------------------*/ /** Public variables ---------------------------------------------------------*/ /** Private variables --------------------------------------------------------*/ /** Private function prototypes ----------------------------------------------*/ /** Private user code --------------------------------------------------------*/

/** Private application code -------------------------------------------------*/ /******************************************************************************* * * Static code * ******************************************************************************** */ /** Public application code --------------------------------------------------*/ /******************************************************************************* * * Public code * ******************************************************************************** */

/*初始化参数定义: *CircularQueue作为环形冲区的记录器是个结构体 *memAdd 作为实际数据存储区, *len 记录实际存储区的最大长度,需为2的整数倍 */ bool CQ_init(CQ_handleTypeDef *CircularQueue ,uint8_t *memAdd, uint16_t len) { CircularQueue->size = len;

if (!IS\_POWER\_OF\_2(CircularQueue->size))
    return FALSE;

if(memAdd == NULL)
{
	return FALSE;
}

CircularQueue->dataBufer = memAdd;

memset(CircularQueue->dataBufer, 0, len);
CircularQueue->entrance = CircularQueue->exit = 0;

return TRUE;

}

/*环形缓冲区判断是否为空: *CircularQueue作为环形冲区的记录器,是个结构体 *若写入数据与,读取数据长度一致,那么缓冲区为空return 1 */ bool CQ_isEmpty(CQ_handleTypeDef *CircularQueue) { if (CircularQueue->entrance == CircularQueue->exit) return TRUE; else return FALSE; }

/*环形缓冲区判断是否为满 *CircularQueue作为环形冲区的记录器,是个结构体 *若 【已】写入数据与,减去 【已】读取数据长度 = 剩余空间 剩余空间==总长度 判断满 */ bool CQ_isFull(CQ_handleTypeDef *CircularQueue) { if ((CircularQueue->entrance - CircularQueue->exit) == CircularQueue->size)//MAXSIZE=5,Q.rear=2,Q.front=3? return TRUE;//空 else return FALSE; }

/*环形缓冲区获取剩余空间长度: *CircularQueue作为环形冲区的记录器,是个结构体 *若 【已】写入数据与,减去 【已】读取数据长度 = 剩余空间 */ uint32_t CQ_getLength(CQ_handleTypeDef*CircularQueue) { return (CircularQueue->entrance - CircularQueue->exit); }

/*环形缓冲区清空操作: *CircularQueue作为环形冲区的记录器,是个结构体 * 已读和可读数据长度清零 实际存储区清空 */ void CQ_emptyData(CQ_handleTypeDef*CircularQueue) { CircularQueue->entrance = CircularQueue->exit = 0; memset(CircularQueue->dataBufer, 0, CircularQueue->size); }

/* *环形缓冲区读走数据: *CircularQueue作为环形冲区的记录器,是个结构体 *targetBuf 为临时数据处理处 *len 为本次数据读取长度 *使用写入长度-读取的长度 == 剩余可读 ,要读 取小值 */ uint32_t CQ_getData(CQ_handleTypeDef *CircularQueue, uint8_t *targetBuf, uint32_t len) { uint32_t size = 0;

/\*此次读取的实际大小,取 缓存事件数据大小 和 目标读取数量 两个值小的那个\*/
len = GET\_MIN(len, CircularQueue->entrance - CircularQueue->exit);// 假设总大小10 写入了5 - 已读4 == 1 未读 要读5个 返回1
/\*原理雷同存入\*/
size = GET\_MIN(len, CircularQueue->size - (CircularQueue->exit & (CircularQueue->size - 1)));//10 - 0 > 1 返回1
memcpy(targetBuf, CircularQueue->dataBufer + (CircularQueue->exit & (CircularQueue->size - 1)), size);//偏移0个 复制一个字节
memcpy(targetBuf + size, CircularQueue->dataBufer, len - size);// 存储区偏移0个字节
/\*利用无符号数据的溢出特性\*/
CircularQueue->exit += len;//取出数据加 len 记录

return len;

}

/*环形缓冲区加入新数据:存入数据功能已做修改:每次数据帧开头先存入本帧的数据长度,所以每次先取一个字节得到包长度,再按长度取包 *CircularQueue作为环形冲区的记录器,是个结构体 *sourceBuf 为实际存储区地址 *len 为本次数据存入长度 *使用总长度-已写入+读取完的 == 可用空间大小 *对kfifo->size取模运算可以转化为与运算,如:kfifo->in % kfifo->size 可以转化为 kfifo->in & (kfifo->size – 1) */ uint32_t CQ_putData(CQ_handleTypeDef *CircularQueue, uint8_t * sourceBuf, uint32_t len) { uint32_t size = 0; /*此次存入的实际大小,取 剩余空间 和 目标存入数量 两个值小的那个*/ len = GET_MIN(len, CircularQueue->size - CircularQueue->entrance + CircularQueue->exit);

/\*&(size-1)代替取模运算,同上原理,得到此次存入队列入口到末尾的大小\*/
size = GET\_MIN(len, CircularQueue->size - (CircularQueue->entrance & (CircularQueue->size - 1)));
memcpy(CircularQueue->dataBufer + (CircularQueue->entrance & (CircularQueue->size - 1)), sourceBuf, size);
memcpy(CircularQueue->dataBufer, sourceBuf + size, len - size);//下次需要写入的数据长度

/\*利用无符号数据的溢出特性\*/
CircularQueue->entrance += len; //写入数据记录

return len;

}

/*修改后的-->环形缓冲区加入新数据:存入数据功能已做修改:每次数据帧开头先存入本帧的数据长度,所以每次先取一个字节得到包长度,再按长度取包 *CircularQueue作为环形冲区的记录器 *sourceBuf 为实际存储区地址 *len 为本次数据存入长度 *使用总长度-已写入+读取完的 == 可用空间大小 *对kfifo->size取模运算可以转化为与运算,如:kfifo->in % kfifo->size 可以转化为 kfifo->in & (kfifo->size – 1) */ uint32_t DQ_putData(CQ_handleTypeDef *CircularQueue, uint8_t * sourceBuf, uint32_t len) { uint32_t size = 0; uint32_t lenth = 1; uint32_t pack_len = len; /*此次存入的实际大小,取 剩余空间 和 目标存入数量 两个值小的那个*/ len = GET_MIN(len+lenth, CircularQueue->size - CircularQueue->entrance + CircularQueue->exit);//长度上头部加上数据长度记录

/\*&(size-1)代替取模运算,同上原理,得到此次存入队列入口到末尾的大小\*/
size = GET\_MIN(len, CircularQueue->size - (CircularQueue->entrance & (CircularQueue->size - 1)));
memcpy(CircularQueue->dataBufer + (CircularQueue->entrance & (CircularQueue->size - 1)), &pack_len, lenth);
memcpy(CircularQueue->dataBufer + (CircularQueue->entrance & (CircularQueue->size - 1))+lenth, sourceBuf, size-lenth);
memcpy(CircularQueue->dataBufer, sourceBuf + size - lenth, len - size);

/\*利用无符号数据的溢出特性\*/
CircularQueue->entrance += len;

return len;

}

/* *修改后的-->环形缓冲区读走数据:DQ会调用CQ取走一字节数据用来判断本次数据包长度 *CircularQueue作为环形冲区的记录器,是个结构体 *targetBuf 为临时数据处理处 *len 为本次数据读取长度 *使用写入长度-读取的长度 == 剩余可读 ,要读 取小值 */ uint32_t DQ_getData(CQ_handleTypeDef *CircularQueue, uint8_t *targetBuf) { uint32_t size = 0; uint32_t len = 0; //存储帧头 长度信息 uint8_t package_len[1]; //获取长度信息 CQ_getData(CircularQueue, (uint8_t *)package_len, 1); len = package_len[0]; /*此次读取的实际大小,取 缓存事件数据大小 和 目标读取数量 两个值小的那个*/ len = GET_MIN(len, CircularQueue->entrance - CircularQueue->exit); /*原理雷同存入*/ size = GET_MIN(len, CircularQueue->size - (CircularQueue->exit & (CircularQueue->size - 1))); memcpy(targetBuf, CircularQueue->dataBufer + (CircularQueue->exit & (CircularQueue->size - 1)), size); memcpy(targetBuf + size, CircularQueue->dataBufer, len - size); /*利用无符号数据的溢出特性*/ CircularQueue->exit += len;

return len;

}

/* *环形缓冲区读走数据:(手动缓冲区长度记录---适用于modbus解析) *CircularQueue作为环形冲区的记录器,是个结构体 *targetBuf 为临时数据处理处 *len 为本次数据读取长度 *使用写入长度-读取的长度 == 剩余可读 ,要读 取小值 */ uint32_t CQ_ManualGetData(CQ_handleTypeDef *CircularQueue, uint8_t *targetBuf, uint32_t len) { uint32_t size = 0;

/\*此次读取的实际大小,取 缓存事件数据大小 和 目标读取数量 两个值小的那个\*/
len = GET\_MIN(len, CircularQueue->entrance - CircularQueue->exit);
/\*原理雷同存入\*/
size = GET\_MIN(len, CircularQueue->size - (CircularQueue->exit & (CircularQueue->size - 1)));
memcpy(targetBuf, CircularQueue->dataBufer + (CircularQueue->exit & (CircularQueue->size - 1)), size);
memcpy(targetBuf + size, CircularQueue->dataBufer, len - size);

return len;

}

/** * [CQ_ManualGet_Offset_Data 读取指定索引号的数据] * @param CircularQueue [环形缓冲区句柄] * @param index [索引号] */ uint8_t CQ_ManualGet_Offset_Data(uint32_t index ,CQ_handleTypeDef *CircularQueue) { /*计算偏移*/ uint32_t read_offset = ((CircularQueue->exit + index) & (CircularQueue->size - 1)); /*取出数据*/ uint8_t data = *((uint8_t*)CircularQueue->dataBufer + read_offset);

return data;

}

/** * [CQ_ManualOffsetInc 手动增加已取出长度] * @param CircularQueue [环形缓冲区句柄] * @param len [偏移长度] */ void CQ_ManualOffsetInc(CQ_handleTypeDef *CircularQueue, uint32_t len) { CircularQueue->exit += len ; }

/** * [cb_create 申请并初始化环形缓冲区] * @param buffsize [申请环形缓冲区大小] * @return [环形队列管理句柄] */ CQ_handleTypeDef *cb_create(uint32_t buffsize) { if (!IS_POWER_OF_2(buffsize)) return NULL; CQ_handleTypeDef *cb = (CQ_handleTypeDef *)malloc(sizeof(CQ_handleTypeDef)); if(NULL == cb) { return NULL; } buffsize = (buffsize <= 2048 ? buffsize : 2048); cb->size = buffsize; cb->exit = 0; cb->entrance = 0; //the buff never release! cb->dataBufer = (uint8_t *)malloc(sizeof(uint8_t)*cb->size); if(NULL == cb->dataBufer) { return NULL; } return cb; }

/** * [CQ_16_init 静态初始化16bit环形缓冲区] * @param CircularQueue [缓冲区指针] * @param memAdd [uint16_t 缓冲区地址] * @param len [缓冲区长度>1] * @return [初始化状态] */ bool CQ_16_init(CQ_handleTypeDef *CircularQueue ,uint16_t *memAdd,uint16_t len) { CircularQueue->size = len;

if (!IS\_POWER\_OF\_2(CircularQueue->size))
    return FALSE;

if(memAdd == NULL)
{
	return FALSE;
}

CircularQueue->data16Bufer = memAdd;

memset(CircularQueue->data16Bufer, 0, len\*2);
CircularQueue->entrance = CircularQueue->exit = 0;

return TRUE;

}

/** * [cb_16create 动态申请并初始化环形缓冲区] * @param buffsize [申请环形缓冲区大小] * @return [环形队列管理句柄] */ CQ_handleTypeDef *cb_16create(uint32_t buffsize) { if (!IS_POWER_OF_2(buffsize)) return NULL; CQ_handleTypeDef *cb = (CQ_handleTypeDef *)malloc(sizeof(CQ_handleTypeDef)); if(NULL == cb) { return NULL; } buffsize = (buffsize <= 2048 ? buffsize : 2048); cb->size = buffsize; cb->exit = 0; cb->entrance = 0; //the buff never release! cb->data16Bufer = (uint16_t *)malloc(sizeof(uint16_t)*cb->size); if(NULL == cb->data16Bufer) { return NULL; } return cb; }

/** * [CQ_16getData 取出数据] * @param CircularQueue [环形缓冲区句柄] * @param targetBuf [目标地址] * @param len [取出长度] * @return [取出长度] */ uint32_t CQ_16getData(CQ_handleTypeDef *CircularQueue, uint16_t *targetBuf, uint32_t len) { uint32_t size = 0; uint32_t len_temp = 0; uint32_t size_temp = 0; /*此次读取的实际大小,取 缓存事件数据大小 和 目标读取数量 两个值小的那个*/ len = GET_MIN(len, CircularQueue->entrance - CircularQueue->exit);// 假设总大小10 写入了5 - 已读4 == 1 未读 要读5个 返回1 /*原理雷同存入*/ size = GET_MIN(len, CircularQueue->size - (CircularQueue->exit & (CircularQueue->size - 1)));//10 - 0 > 1 返回1

len_temp = 2\*len;
size_temp = 2\*size;

memcpy(targetBuf, CircularQueue->data16Bufer + (CircularQueue->exit & (CircularQueue->size - 1)), size_temp);//偏移0个 复制一个字节
memcpy(targetBuf + size, CircularQueue->data16Bufer, len_temp - size_temp);// 存储区偏移0个字节
/\*利用无符号数据的溢出特性\*/
CircularQueue->exit += len;//取出数据加 len 记录

return len;

}

/** * [CQ_16putData 加入数据] * @param CircularQueue [环形缓冲区句柄] * @param sourceBuf [源地址] * @param len [长度] * @return [加入数据长度] */ uint32_t CQ_16putData(CQ_handleTypeDef *CircularQueue, uint16_t * sourceBuf, uint32_t len) { uint32_t size = 0; uint32_t len_temp = 0; uint32_t size_temp = 0; /*此次存入的实际大小,取 剩余空间 和 目标存入数量 两个值小的那个*/ len = GET_MIN(len, CircularQueue->size - CircularQueue->entrance + CircularQueue->exit);

/\*&(size-1)代替取模运算,同上原理,得到此次存入队列入口到末尾的大小\*/
size = GET\_MIN(len, CircularQueue->size - (CircularQueue->entrance & (CircularQueue->size - 1)));

len_temp = 2\*len;
size_temp = 2\*size;

memcpy(CircularQueue->data16Bufer + (CircularQueue->entrance & (CircularQueue->size - 1)), sourceBuf, size_temp);
memcpy(CircularQueue->data16Bufer, sourceBuf + size, len_temp - size_temp);

/\*利用无符号数据的溢出特性\*/
CircularQueue->entrance += len; //写入数据记录

return len;

}

/** * [CQ_32_init 静态初始化16bit环形缓冲区] * @param CircularQueue [缓冲区指针] * @param memAdd [uint16_t 缓冲区地址] * @param len [缓冲区长度>1] * @return [初始化状态] */ bool CQ_32_init(CQ_handleTypeDef *CircularQueue ,uint32_t *memAdd ,uint16_t len) { CircularQueue->size = len;

if (!IS\_POWER\_OF\_2(CircularQueue->size))

img img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取