完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
本章节为大家讲解FreeRTOS任务栈大小的确定方法以及栈溢出检测方法。给任务分配多大的栈空间,一直是初学者比较头疼的问题,本章就主要为大家讲解如何解决此问题。
本章教程配套的例子含Cortex-M3内核的STM32F103和Cortex-M4内核的STM32F407以及F429。 11.1 任务栈大小的确定 11.2 什么是栈溢出 11.3 FreeRTOS的栈溢出检测机制 11.4 实验例程说明(任务栈溢出检测方式一) 11.5 实验例程说明(任务栈溢出检测方式二) 11.6总结 11.1 任务栈大小的确定 在基于RTOS的应用设计中,每个任务都需要自己的栈空间,应用不同,每个任务需要的栈大小也是不同的。将如下的几个选项简单的累加就可以得到一个粗略的栈大小: u 函数的嵌套调用,针对每一级函数用到栈空间的有如下四项: l 函数局部变量。 l 函数形参,一般情况下函数的形参是直接使用的CPU寄存器,不需要使用栈空间,但是这个函数中如果还嵌套了一个函数的话,这个存储了函数形参的CPU寄存器内容是要入栈的。所以建议大家也把这部分算在栈大小中。 l 函数返回地址,针对M3和M4内核的MCU,一般函数的返回地址是专门保存到LR(Link Register)寄存器里面的,如果这个函数里面还调用了一个函数的话,这个存储了函数返回地址的LR寄存器内容是要入栈的。所以建议大家也把这部分算在栈大小中。 l 函数内部的状态保存操作也需要额外的栈空间。 |
|
相关推荐
|
|
任务切换,任务切换时所有的寄存器都需要入栈,对于带FPU浮点处理单元的M4内核MCU来说,FPU寄存器也是需要入栈的。
u 针对M3内核和M4内核的MCU来说,在任务执行过程中,如果发生中断: l M3内核的MCU有8个寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余寄存器入栈以及发生中断嵌套都是用的系统栈。 l M4内核的MCU有8个通用寄存器和18个浮点寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余通用寄存器和浮点寄存器入栈以及发生中断嵌套都是用的系统栈。 u 进入中断以后使用的局部变量以及可能发生的中断嵌套都是用的系统栈,这点要注意。 实际应用中将这些都加起来是一件非常麻烦的工作,上面这些栈空间加起来的总和只是栈的最小需求,实际分配的栈大小可以在最小栈需求的基础上乘以一个安全系数,一般取1.5-2。上面的计算是我们用户可以确定的栈大小,项目应用中还存在无法确定的栈大小,比如调用printf函数就很难确定实际的栈消耗。又比如通过函数指针实现函数的间接调用,因为函数指针不是固定的指向一个函数进行调用,而是根据不同的程序设计可以指向不同的函数,使得栈大小的计算变得比较麻烦。 另外还要注意一点,建议不要编写递归代码,因为我们不知道递归的层数,栈的大小也是不好确定的。 一般来说,用户可以事先给任务分配一个大的栈空间,然后通过第8章介绍的调试方法打印任务栈的使用情况,运行一段时间就会有个大概的范围了。这种方法比较简单且实用些。 l 函数栈大小确定 函数的栈大小计算起来是比较麻烦的,那么有没有简单的办法来计算呢?有的,一般IDE开发环境都有这样的功能,比如MDK会生成一个htm文件,通过这个文件用户可以知道每个被调用函数的最大栈需求以及各个函数之间的调用关系。但是MDK无法确定通过函数指针实现函数调用时的栈需求。另外,发生中断或中断嵌套时的现场保护需要的栈空间也不会统计。 |
|
|
|
|
|
11.2 什么是栈溢出
前面为大家讲解了如何确定任务栈的大小,那什么又是栈溢出呢?简单的说就是用户分配的栈空间不够用了,溢出了。下面我们举一个简单的实例,栈生长方向从高地址向低地址生长(M4和M3是这种方式)。 |
|
|
|
|
|
(1) 上图标识1的位置是RTOS的某个任务调用了函数test()前的SP栈指针位置。
void test (void); { int i; int array[10]; : : // Code } |
|
|
|
|
|
(2) 上图标识2的位置是调用了函数test需要保存返回地址到栈空间。这一步不是必须的,对于M3和M4内核是先将其保存到LR寄存器中,如果LR寄存器中有保存上一级函数的返回地址,需要将LR寄存器中的内容先入栈。
(3) 上图标识3的位置是局部变量int i和int array[10]占用的栈空间,但申请了栈空间后已经越界了。这个就是所谓的栈溢出了。如果用户在函数test中通过数组array修改了这部分越界区的数据且这部分越界的栈空间暂时没有用到或者数据不是很重要,情况还不算严重,但是如果存储的是关键数据,会直接导致系统崩溃。 (4) 上图标识4的位置是局部变量申请了栈空间后,栈指针向下偏移(返回地址+变量i+10个数组元素)*4 =48个字节。 (5) 上图标识5的位置可能是其它任务的栈空间,也可能是全局变量或者其它用途的存储区,如果test函数在使用中还有用到栈的地方就会从这里申请,这部分越界的空间暂时没有用到或者数据不是很重要,情况还不算严重,但是如果存储的是关键数据,会直接导致系统崩溃。 |
|
|
|
|
|
11.3 FreeRTOS的栈溢出检测机制
FreeRTOS提供了两种栈溢出检测机制,这两种检测都是在任务切换时才会进行: u 方法一 在任务切换时检测任务栈指针是否过界了,如果过界了,在任务切换的时候会触发栈溢出钩子函数。 void vApplicationStackOverflowHook( TaskHandle_t xTask, signed char *pcTaskName ); 用户可以在钩子函数里面做一些处理。这种方法不能保证所有的栈溢出都能检测到。比如任务在执行的过程中出现过栈溢出。任务切换前栈指针又恢复到了正常水平,这种情况在任务切换的时候是检测不到的。又比如任务栈溢出后,把这部分栈区的数据修改了,这部分栈区的数据不重要或者暂时没有用到还好,但如果是重要数据被修改将直接导致系统进入硬件异常,这种情况下,栈溢出检测功能也是检测不到的。 使用方法一需要用户在FreeRTOSConfig.h文件中配置如下宏定义: #define configCHECK_FOR_STACK_OVERFLOW 1 u 方法二 任务创建的时候将任务栈所有数据初始化为0xa5,任务切换时进行任务栈检测的时候会检测末尾的16个字节是否都是0xa5,通过这种方式来检测任务栈是否溢出了。相比方法一,这种方法的速度稍慢些,但是这样就有效地避免了方法一里面的部分情况。不过依然不能保证所有的栈溢出都能检测到,比如任务栈末尾的16个字节没有用到,即没有被修改,但是任务栈已经溢出了,这种情况是检测不到的。另外任务栈溢出后,任务栈末尾的16个字节没有修改,但是溢出部分的栈区数据被修改了,这部分栈区的数据不重要或者暂时没有用到还好,但如果是重要数据被修改将直接导致系统进入硬件异常,这种情况下,栈溢出检测功能也是检测不到的。 使用方法二需要用户在FreeRTOSConfig.h文件中配置如下宏定义: #define configCHECK_FOR_STACK_OVERFLOW 2 l 栈溢出检测方法 除了FreeRTOS提供的这两种栈溢出检测机制,还有其它的栈溢出检测机制,大家可以在Mircrium官方发布的如下这个博文中学习:https://www.micrium.com/detecting-stack-overflows-part-2-of-2/ l 钩子函数 钩子函数的主要作用就是对原有函数的功能进行扩展,用户可以根据自己的需要往里面添加相关的测试代码,大家可以在FreeRTOS工程中检索这个钩子函数vApplicationStackOverflowHook所在的位置。 |
|
|
|
|
|
11.4 实验例程说明(任务栈溢出检测方式一)
11.4.1 STM32F103开发板实验 配套例子: V4-305_FreeRTOS实验_任务栈溢出检测方法一(模拟栈溢出) 实验目的: 1. 学习FreeRTOS的任务栈溢出检测方法一(模拟栈溢出)。 2. FreeRTOS的任务栈溢出检测方法一说明: a. FreeRTOSConfig.h文件中配置宏定义: #define configCHECK_FOR_STACK_OVERFLOW 1 b. 在任务切换时检测任务栈指针是否过界了,如果过界了,在任务切换的时候会触发栈溢出钩子函数。 voidvApplicationStackOverflowHook( TaskHandle_t xTask, signed char *pcTaskName ); 用户可以在钩子函数里面做一些处理。本实验是在钩子函数中打印出现栈溢出的任务。 c. 这种方法不能保证所有的栈溢出都能检测到。比如任务在执行的过程中发送过栈溢出。任务切换前栈指针又恢复到了正常水平,这种情况在任务切换的时候是检测不到的。又比如任务栈溢出后,把这部分栈区的数据修改了,这部分栈区的数据不重要或者暂时没有用到还不会有什么问题,但如果是重要数据被修改将直接导致系统进入硬件异常,这种情况下,栈溢出检测功能也是检测不到的。 d. 本实验就是简单的在任务vTaskUserIF中申请过大的栈空间,模拟出一种栈溢出的情况,溢出后触发钩子函数,因为我们将溢出部分的数据修改了,进而造成进入硬件异常。 实验内容: 1. K1按键按下,串口打印任务执行情况(波特率115200,数据位8,奇偶校验位无,停止位1)。 2. K2按键按下,模拟栈溢出。 3. 各个任务实现的功能如下: vTaskUserIF任务 :按键消息处理。 vTaskLED任务 :LED闪烁。 vTaskMsgPro任务 :消息处理,这里是用作LED闪烁。 vTaskStart任务 :启动任务,也是最高优先级任务,这里实现按键扫描。 |
|
|
|
|
|
FreeRTOS的配置:
FreeRTOSConfig.h文件中的配置如下: /* Ensure stdint is only used by the compiler, and not the assembler. */ #if defined(__ICCARM__) || defined(__CC_ARM) || defined(__GNUC__) #include extern volatile uint32_t ulHighFrequencyTimerTicks; #endif #define configUSE_PREEMPTION 1 #define configUSE_IDLE_HOOK 0 #define configUSE_TICK_HOOK 0 #define configCPU_CLOCK_HZ ( ( unsigned long ) 72000000 ) #define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) #define configMAX_PRIORITIES ( 5 ) #define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) #define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) #define configMAX_TASK_NAME_LEN ( 16 ) #define configUSE_TRACE_FACILITY 1 #define configUSE_16_BIT_TICKS 0 #define configIDLE_SHOULD_YIELD 1 #define configCHECK_FOR_STACK_OVERFLOW 1 /* Run time and task stats gathering related definitions. */ #define configGENERATE_RUN_TIME_STATS 1 #define configUSE_STATS_FORMATTING_FUNCTIONS 1 #define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() (ulHighFrequencyTimerTicks = 0ul) #define portGET_RUN_TIME_COUNTER_VALUE() ulHighFrequencyTimerTicks //#define portALT_GET_RUN_TIME_COUNTER_VALUE 1 /* Co-routine definitions. */ #define configUSE_CO_ROUTINES 0 #define configMAX_CO_ROUTINE_PRIORITIES ( 2 ) /* Set the following definitions to 1 to include the API function, or zero to exclude the API function. */ #define INCLUDE_vTaskPrioritySet 1 #define INCLUDE_uxTaskPriorityGet 1 #define INCLUDE_vTaskDelete 1 #define INCLUDE_vTaskCleanUpResources 0 #define INCLUDE_vTaskSuspend 1 #define INCLUDE_vTaskDelayUntil 1 #define INCLUDE_vTaskDelay 1 /* Cortex-M specific definitions. */ #ifdef __NVIC_PRIO_BITS /* __BVIC_PRIO_BITS will be specified when CMSIS is being used. */ #define configPRIO_BITS __NVIC_PRIO_BITS #else #define configPRIO_BITS 4 /* 15 priority levels */ #endif /* The lowest interrupt priority that can be used in a call to a "set priority" function. */ #define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 0x0f /* The highest interrupt priority that can be used by any interrupt service routine that makes calls to interrupt safe FreeRTOS API functions. DO NOT CALL INTERRUPT SAFE FREERTOS API FUNCTIONS FROM ANY INTERRUPT THAT HAS A HIGHER PRIORITY THAN THIS! (higher priorities are lower numeric values. */ #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 0x01 |
|
|
|
|
|
几个重要选项说明:
u #define configUSE_PREEMPTION 1 使能抢占式调度器 u #define configCPU_CLOCK_HZ ( ( unsigned long ) 72000000 ) 系统主频72MHz。 u #define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) 系统时钟节拍1KHz,即1ms。 u #define configMAX_PRIORITIES ( 5 ) 定义可供用户使用的最大优先级数,如果这个定义的是5,那么用户可以使用的优先级号是0,1,2,3,4,不包含5,对于这一点,初学者要特别的注意。 u #define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) 定义堆大小,FreeRTOS内核,用户动态内存申请,任务栈等都需要用这个空间。 u #define configCHECK_FOR_STACK_OVERFLOW 1 任务栈检测采用方式一。 u #define INCLUDE_vTaskSuspend 1 使用FreeRTOS的任务挂起函数vTaskSuspend和任务恢复函数vTaskResume必须配置此宏定义为1。 u configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 0x01 定义受FreeRTOS管理的最高优先级中断。简单的说就是允许用户在这个中断服务程序里面调用FreeRTOS的API的最高优先级。为了进一步说明这个宏定义的的作用,解释如下: l 使用CM内核的MCU,官方强烈建议将NVIC的优先级分组配置为全抢占式优先级,全部配置为抢占式优先级的好处就是方便管理。 l 对于STM32来说,设置NVIC的优先级分组为4时,NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4)就是全部配置为抢占式优先级。又因为STM32的优先级设置仅使用CM内核8bit中的高4bit,即只能区分2^4 = 16种优先级。因此当优先级分组设置为4的时候可供用户选择抢占式优先级为0到15,共16个优先级,配置为0表示最高优先级,配置为15表示最低优先级,不存在子优先级。 l 这里配置configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY为0x01表示用户可以在抢占式优先级为1到15的中断里面调用FreeRTOS的API函数,抢占式优先级为0的中断里面是不允许调用的。 |
|
|
|
|
|
FreeRTOS任务调试信息(按K1按键,串口打印):
上面截图中打印出来的任务状态字母B, R, D, S对应如下含义: #definetskBLOCKED_CHAR ( 'B' ) 任务阻塞 #definetskREADY_CHAR ( 'R' ) 任务就绪 #definetskDELETED_CHAR ( 'D' ) 任务删除 #definetskSUSPENDED_CHAR ( 'S' ) 任务挂起 |
|
|
|
|
|
栈溢出串口打印效果(按K2按键,多次打印溢出任务后最终进入硬件异常):
程序设计: u 任务栈大小分配: vTaskUserIF任务 :2048字节 vTaskLED任务 :2048字节 vTaskMsgPro任务 :2048字节 vTaskStart任务 :2048字节 任务栈空间是在任务创建的时候从FreeRTOSConfig.h文件中定义的heap空间中申请的 #defineconfigTOTAL_HEAP_SIZE ( ( size_t )( 17 * 1024 ) ) |
|
|
|
|
|
FreeROTS初始化:
/* ********************************************************************************************************* * 函 数 名: main * 功能说明: 标准c程序入口。 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ int main(void) { /* 在启动调度前,为了防止初始化STM32外设时有中断服务程序执行,这里禁止全局中断(除了NMI和HardFault)。 这样做的好处是: 1. 防止执行的中断服务程序中有FreeRTOS的API函数。 2. 保证系统正常启动,不受别的中断影响。 3. 关于是否关闭全局中断,大家根据自己的实际情况设置即可。 在移植文件port.c中的函数prvStartFirstTask中会重新开启全局中断。通过指令cpsie i开启,__set_PRIMASK(1) 和cpsie i是等效的。 */ __set_PRIMASK(1); /* 硬件初始化 */ bsp_Init(); /* 1. 初始化一个定时器中断,精度高于滴答定时器中断,这样才可以获得准确的系统信息 仅供调试目的,实际项 目中不要使用,因为这个功能比较影响系统实时性。 2. 为了正确获取FreeRTOS的调试信息,可以考虑将上面的关闭中断指令__set_PRIMASK(1); 注释掉。 */ vSetupSysInfoTest(); /* 创建任务 */ AppTaskCreate(); /* 启动调度,开始执行任务 */ vTaskStartScheduler(); /* 如果系统正常启动是不会运行到这里的,运行到这里极有可能是用于定时器任务或者空闲任务的 heap空间不足造成创建失败,此要加大FreeRTOSConfig.h文件中定义的heap大小: #define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) */ while(1); } |
|
|
|
|
|
硬件外设初始化
硬件外设的初始化是在bsp.c文件实现: /* ********************************************************************************************************* * 函 数 名: bsp_Init * 功能说明: 初始化硬件设备。只需要调用一次。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。 * 全局变量。 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_Init(void) { /* 由于ST固件库的启动文件已经执行了CPU系统时钟的初始化,所以不必再次重复配置系统时钟。 启动文件配置了CPU主时钟频率、内部Flash访问速度和可选的外部SRAM FSMC初始化。 系统时钟缺省配置为72MHz,如果需要更改,可以修改 system_stm32f10x.c 文件 */ /* 优先级分组设置为4,可配置0-15级抢占式优先级,0级子优先级,即不存在子优先级。*/ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); bsp_InitUart(); /* 初始化串口 */ bsp_InitLed(); /* 初始LED指示灯端口 */ bsp_InitKey(); /* 初始化按键 */ } |
|
|
|
|
|
FreeRTOS任务创建:
/* ********************************************************************************************************* * 函 数 名: AppTaskCreate * 功能说明: 创建应用任务 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ static void AppTaskCreate (void) { xTaskCreate( vTaskTaskUserIF, /* 任务函数 */ "vTaskUserIF", /* 任务名 */ 512, /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ 1, /* 任务优先级*/ &xHandleTaskUserIF ); /* 任务句柄 */ xTaskCreate( vTaskLED, /* 任务函数 */ "vTaskLED", /* 任务名 */ 512, /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ 2, /* 任务优先级*/ &xHandleTaskLED ); /* 任务句柄 */ xTaskCreate( vTaskMsgPro, /* 任务函数 */ "vTaskMsgPro", /* 任务名 */ 512, /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ 3, /* 任务优先级*/ &xHandleTaskMsgPro ); /* 任务句柄 */ xTaskCreate( vTaskStart, /* 任务函数 */ "vTaskStart", /* 任务名 */ 512, /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ 4, /* 任务优先级*/ &xHandleTaskStart ); /* 任务句柄 */ } |
|
|
|
|
|
栈溢出测试函数:
栈溢出检测函数是通过用户按下按键K2后,通过任务vTaskTaskUserIF调用此函数。 /* ********************************************************************************************************* * 函 数 名: vTaskTaskUserIF /* ********************************************************************************************************* * 函 数 名: StackOverflowTest * 功能说明: 任务栈溢出测试 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ static void StackOverflowTest(void) { int16_t i; uint8_t buf[2048]; (void)buf; /* 防止警告 */ /* 1. 为了能够模拟任务栈溢出,并触发任务栈溢出函数,这里强烈建议使用数组的时候逆着赋值。 因为对于M3和M4内核的MCU,堆栈生长方向是向下生长的满栈。即高地址是buf[2047], 低地址 是buf[0]。如果任务栈溢出了,也是从高地址buf[2047]到buf[0]的某个地址开始溢出。 因此,如果用户直接修改的是buf[0]开始的数据且这些溢出部分的数据比较重要,会直接导致 进入到硬件异常。 2. 栈溢出检测是在任务切换的时候执行的,我们这里加个延迟函数,防止修改了重要的数据导致直接 进入硬件异常。 3. 任务vTaskTaskUserIF的栈空间大小是2048字节,在此任务的入口已经申请了栈空间大小 ------uint8_t ucKeyCode; ------uint8_t pcWriteBuffer[500]; 这里再申请如下这么大的栈空间 -------int16_t i; -------uint8_t buf[2048]; 必定溢出。 */ for(i = 2047; i >= 0; i--) { buf[i] = 0x55; vTaskDelay(1); } } |
|
|
|
|
|
栈溢出钩子函数:
/* ********************************************************************************************************* * 函 数 名: vApplicationStackOverflowHook * 功能说明: 栈溢出的钩子函数 * 形 参: xTask 任务句柄 * pcTaskName 任务名 * 返 回 值: 无 ********************************************************************************************************* */ void vApplicationStackOverflowHook( TaskHandle_t xTask, signed char *pcTaskName ) { printf("任务:%s 发现栈溢出rn", pcTaskName); } |
|
|
|
|
|
栈溢出钩子函数:
/* ********************************************************************************************************* * 函 数 名: vApplicationStackOverflowHook * 功能说明: 栈溢出的钩子函数 * 形 参: xTask 任务句柄 * pcTaskName 任务名 * 返 回 值: 无 ********************************************************************************************************* */ void vApplicationStackOverflowHook( TaskHandle_t xTask, signed char *pcTaskName ) { printf("任务:%s 发现栈溢出rn", pcTaskName); } |
|
|
|
|
|
11.4.2 STM32F407开发板实验
配套例子: V5-305_FreeRTOS实验_任务栈溢出检测方法一(模拟栈溢出) 实验目的: 1. 学习FreeRTOS的任务栈溢出检测方法一(模拟栈溢出)。 2. FreeRTOS的任务栈溢出检测方法一说明: a. FreeRTOSConfig.h文件中配置宏定义: #define configCHECK_FOR_STACK_OVERFLOW 1 b. 在任务切换时检测任务栈指针是否过界了,如果过界了,在任务切换的时候会触发栈溢出钩子函数。 voidvApplicationStackOverflowHook( TaskHandle_t xTask, signed char *pcTaskName ); 用户可以在钩子函数里面做一些处理。本实验是在钩子函数中打印出现栈溢出的任务。 c. 这种方法不能保证所有的栈溢出都能检测到。比如任务在执行的过程中发送过栈溢出。任务切换前栈指针又恢复到了正常水平,这种情况在任务切换的时候是检测不到的。又比如任务栈溢出后,把这部分栈区的数据修改了,这部分栈区的数据不重要或者暂时没有用到还不会有什么问题,但如果是重要数据被修改将直接导致系统进入硬件异常,这种情况下,栈溢出检测功能也是检测不到的。 d. 本实验就是简单的在任务vTaskUserIF中申请过大的栈空间,模拟出一种栈溢出的情况,溢出后触发钩子函数,因为我们将溢出部分的数据修改了,进而造成进入硬件异常。 实验内容: 1. K1按键按下,串口打印任务执行情况(波特率115200,数据位8,奇偶校验位无,停止位1)。 2. K2按键按下,模拟栈溢出。 3. 各个任务实现的功能如下: vTaskUserIF任务 :按键消息处理。 vTaskLED任务 :LED闪烁。 vTaskMsgPro任务 :消息处理,这里是用作LED闪烁。 vTaskStart任务 :启动任务,也是最高优先级任务,这里实现按键扫描。 |
|
|
|
|
|
《DNESP32S3使用指南-IDF版_V1.6》第三十五章 摄像头实验
816 浏览 0 评论
《DNESP32S3使用指南-IDF版_V1.6》第三十章 DHT11数字温湿度传感器
789 浏览 0 评论
880 浏览 0 评论
【敏矽微ME32G070开发板免费体验】之原厂2812测试例程解析
1615 浏览 0 评论
1272 浏览 2 评论
【youyeetoo X1 windows 开发板体验】少儿AI智能STEAM积木平台
12063 浏览 31 评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-12-29 21:50 , Processed in 1.132925 second(s), Total 73, Slave 65 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号