news 2026/10/20 9:29:40

STM32时钟树全解析:时钟源、PLL倍频、总线分频与串口乱码排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32时钟树全解析:时钟源、PLL倍频、总线分频与串口乱码排查

我先说个真实经历。有一回我在调一块F103的开发板,刚上电时LED闪烁一切正常,可一旦我想把串口波特率从9600改成115200,接上USB转串口工具后收到的全是乱码。换了个USB口,还是乱码,换波特率,还是乱码。最后发现不是线材问题,也不是串口助手设置问题,而是时钟树配置压根没按我预期走——系统时钟、总线时钟和外设时钟三者没对齐,波特率自然全错。也是从那次起,我意识到做嵌入式绝对不能跳过的第一课,就是STM32时钟树。

这篇内容主要写给刚看完点灯例程、准备接触定时器、串口、ADC这些外设的初学者。串口波特率、定时器定时时间、PWM频率、ADC采样率,全部是从某个时钟频率派生的,时钟树理解不到位,外设就全在"盲跑"。我会从时钟源、PLL倍频、总线分频几个层面,把时钟链路彻底拆开,再给出一套可落地的配置方法和排查手段,尽可能让不同基础的读者都能看懂、能上手、踩坑时知道该怎么查。

1. 为什么每个学STM32的人都要过一遍时钟树

1.1 单片机也是需要"节拍器"的

很多人都玩过LED闪烁,代码里一个HAL_Delay(500)就完事了。这个Delay能用,并不代表你理解了时钟,它只是系统在默认配置下恰好能跑而已。真正的问题是:这个500ms是怎么来的?为什么改成HAL_Delay就准?定时器计时为什么有时候快一倍?串口为什么偏偏乱码?

答案都指向同一个概念:时钟。CPU执行一条指令需要节拍,每个节拍就是一个时钟周期;定时器计数需要节拍,每来一个脉冲计数值加一;串口发送一个bit也需要节拍,波特率就是每秒钟有多少个节拍被采样。可以这么理解,时钟就是整个单片机的"节拍器",而时钟树就是这张把"节拍器"的信号送到CPU、各个总线、各个外设的分配网络。

如果你让CPU跑72MHz,却让某个定时器跑到144MHz,这个定时器对外表现就是"计时快了一倍"。更夸张的情况下,某外设时钟频率超过芯片允许上限,芯片甚至可能直接不工作。所以,时钟树不是一堆可以忽略的寄存器,而是整个系统运行节奏的控制中枢。

1.2 时钟配不好,外设全都在"盲跑"

初学者最容易遭遇的三个场景:

  • 串口乱码:波特率的计算依赖外设时钟PCLK1或PCLK2。如果这个时钟实际频率和代码里假设的频率不一致,算出来的波特率分频值就是错的,结果就是收发双方速率不同,数据乱飞。
  • 定时器不准:定时器的计数频率是PCLK1经分频后得到的。APB1预分频配置成2倍时,定时器时钟会自动加倍,很多人定时1秒结果实测只有0.5秒,根源就在这。
  • ADC数值偏移:ADC采样需要时钟,而ADC时钟一般要求限制在14MHz以内(F1系列)。如果配置时没把ADC预分频设置好,采样得到的数据会直接表现为数值偏差或者不稳定,却又很难联想到是时钟问题。

这些问题的共同特点是:现象发生在应用层,根因却在时钟层。所以学STM32一定得先把时钟树搞明白,不然以后出了问题排查起来会非常痛苦。

1.3 学时钟树的正确心态与学习路径

我不建议一上来就抱着几百页参考手册硬啃。更有效的路径是:先看一张别人画好的时钟树框图,把"时钟从哪里来、经过哪些环节、最终送到谁那里"这条主线梳理出来,然后配合一个最小系统的代码实验去验证,最后再回到手册反查细节。

实践顺序可以这样:先把系统时钟配置函数完整读一遍,对照框图找到对应寄存器位;然后用RCC_GetClocksFreq这类接口读出实际时钟值;最后用外部示波器或延时实验做交叉印证。这样两三轮下来,时钟树的立体感就有了。这一步花的工夫,后面排查外设问题会十倍省回来。

2. 从一颗晶振出发,看懂时钟树上的每一根枝杈

2.1 两大时钟源:HSI内部RC与HSE外部晶振

STM32F1的时钟树有两种最主要的"源头",分别是HSI和HSE。

HSI是芯片内部自带的RC振荡器,频率固定为8MHz。它最大的优点是上电就能用,无需外部器件,缺点是精度一般,温度漂移也比较明显,对时序要求高的场景不能完全信赖。

HSE是外部高速晶振,通常接一个8MHz的晶体,再加两个负载电容就能工作。它精度高、稳定,是大多数系统最终选择的时钟源,但外部晶振需要起振时间,而且PCB上晶振布局不好可能导致起振失败。

除了这两个,还有两个低频源:LSE是32.768kHz的外部晶振,一般给RTC实时时钟用;LSI是约40kHz的内部RC,主要给独立看门狗和部分低功耗模式用。这两类在时钟树主干之外,但也要知道它们的存在。

时钟源来源典型频率精度启动特点常见用途
HSI芯片内部RC8MHz较低上电立即可用默认启动时钟、不需要高精度时的低成本方案
HSE外部晶振4-16MHz(常用8MHz)高需要起振等待系统主时钟、PLL输入
LSE外部低频晶振32.768kHz高需要起振等待RTC实时时钟
LSI芯片内部RC约40kHz低上电立即可用独立看门狗、低功耗唤醒

2.2 PLL锁相环的倍频逻辑:为什么默认是9倍

HSI和HSE只是"原料"。要让CPU跑到72MHz,一般得借助PLL(锁相环)做倍频。F1芯片的PLL结构比较简单:选择一个输入源,乘一个倍频系数,得到最终系统时钟SYSCLK。以最常见的8MHz HSE为例:

SYSCLK = HSE × PLLMUL = 8MHz × 9 = 72MHz

这里9倍就是PLLMUL的配置值。如果用标准库,就是一行宏定义RCC_PLLMul_9;用HAL库,就是PLL.PLLM = 8和PLL.PLLN = 72之类的参数。但本质上,公式就这一条。

为什么一定要是72MHz而不是100MHz或144MHz?这是芯片设计时确定的最高主频上限。F103C8T6这类型号,SYSCLK最高就是72MHz。超过这个值,哪怕只超一点,芯片行为也无法保证,可能出现无法预料的现象。所以配置时务必确认最终SYSCLK不超过上限。

还需要注意:如果外部晶振不是8MHz,比如用了12MHz的晶体,那倍频系数就不能再用9了,得改成6才能在F1上保持72MHz。很多人拿着8MHz的配置直接套12MHz的板子,烧进去芯片不是慢半拍就是完全不动,原因就在这里。

2.3 AHB/APB1/APB2总线分频:外设时钟的分水岭

SYSCLK确定后,还要经过三级分配才能到各个外设:

  1. AHB预分频,决定HCLK。HCLK供给CPU内核、内存、DMA以及大部分AHB总线上的外设。
  2. APB1预分频,决定PCLK1。APB1是低速总线,最高36MHz,挂载的都是一些功耗相对低、速度要求不高的外设。
  3. APB2预分频,决定PCLK2。APB2是高速总线,最高72MHz,挂载的是ADC、GPIO、USART1、定时器1等高要求外设。

F1系列有个经典"隐藏加倍"规则:当APB1预分频系数不为1时,挂在该总线上的定时器时钟会变成PCLK1 × 2。默认配置中APB1分频系数是2,所以PCLK1通常是36MHz,但TIM2这样挂在APB1上的定时器实际时钟是72MHz。初学者在这里最容易懵:明明看到手册说APB1最高36MHz,为什么定时器又是72MHz?其实这就是设计者在保证总线功耗的前提下,让定时器仍能拿到较高频率的手段。

总线最高频率典型外设
AHB72MHzCPU、内存、DMA、Flash接口
APB136MHzUSART2/3/4/5、SPI2/3、I2C1/2、TIM2-7、看门狗
APB272MHzGPIO、USART1、SPI1、ADC1/2/3、TIM1

理解这张表之后,你会发现很多配置都变得有逻辑可循了。比如为什么RCC里使能GPIO时钟用的是RCC_APB2PeriphClockCmd,为什么USART1属于APB2而USART2属于APB1——完全取决于它们挂在哪个总线上。

3. 用代码把时钟链路一步步拉通(以F103为例)

3.1 先看启动文件怎么"悄悄"定了初始时钟

很多初学者烧录程序后直接跑main,并不知道在进入main之前,系统已经执行过一段启动代码。在F1系列中,startup_stm32f10x_hd.s这个汇编启动文件会在复位后调用SystemInit函数。

标准库的SystemInit做的事大致是:关闭PLL、把HCLK和PCLK1/2设为默认分频、打开Flash预取缓冲、如果定义了STM32F10X_CL等宏还会额外配置。最关键的是,如果启用VECT_TAB_SRAM或需要在启动阶段把时钟切到HSE+PLL,SystemInit也会处理。

但要注意:标准库版本的SystemInit默认只把系统配置到内部HSI的8MHz状态(某些库版本会把时钟调度到72MHz,取决于SystemInit里的具体代码),真正切换到HSE+PLL 72MHz的逻辑,通常要在你自己写的RCC_Configuration函数里完成。而HAL库版本则会读取SystemClock_Config里生成的配置并执行。无论是哪种库,核心都是同一套时钟链路。

3.2 标准外设库下的RCC配置套路

F1标准外设库的时钟配置,经典写法是下面这段。

void RCC_Configuration(void) { RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); // 打开外部高速晶振 ErrorStatus status = RCC_WaitForHSEStartUp(); // 等待HSE起振稳定 if (status == SUCCESS) { FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable); // 开启flash预取 FLASH_SetLatency(FLASH_Latency_2); // flash等待周期,72MHz下要2个等待 RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); // HSE 8MHz × 9 = 72MHz RCC_HCLKConfig(RCC_SYSCLK_Div1); // AHB不分频,HCLK=72MHz RCC_PCLK1Config(RCC_HCLK_Div2); // APB1分频2,PCLK1=36MHz RCC_PCLK2Config(RCC_HCLK_Div1); // APB2不分频,PCLK2=72MHz RCC_PLLCmd(ENABLE); // 使能PLL while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET); // 等待PLL锁定 RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); // 切换到PLL作为系统时钟 while (RCC_GetSYSCLKSrc() != 0x08); // 确认切换成功 } else { // HSE起振失败,这里可以退回HSI配置继续运行 } }

这一段代码的先后顺序有讲究:

  • 先开HSE并等待稳定,因为PLL的输入源是HSE,源没稳定就倍频没意义。
  • 先配置Flash等待周期,再提高时钟频率。Flash的读取是需要时间的,72MHz下至少要两个等待周期,否则CPU从Flash取指令会出错。
  • 先进总线分频再使能PLL,避免切换瞬间外设时钟超规格。
  • 最后才把系统时钟切到PLL输出,并且要回头确认切换完成,因为时钟切换是异步的。

很多人图省事,把RCC_DeInit一放就按网上代码抄,抄完也不看顺序。实际上这个顺序是保证系统稳定运行的关键。

3.3 HAL库下的Clock Config:图形化之外的底层逻辑

HAL库和CubeMX把配置过程图形化了,但底层逻辑并没有变。如果你用CubeMX生成工程,会自动生成一个SystemClock_Config函数,里面会依次调用RCC_OscConfig配置振荡器、RCC_ClockConfig配置总线分频,而后配置Flash等待周期。

以STM32F103系列在CubeMX中配置HSE+ PLL到72MHz为例,生成的代码核心逻辑可以简化为这样:

void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; HAL_RCC_OscConfig(&RCC_OscInitStruct); RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2); }

本质上和标准库那套代码对应。CubeMX在STM32CubeMX界面里用下拉框帮你选了"倍频系数9、APB1分频2",但它并不会替你理解为什么要这么选。如果有一天你的板子换了外部晶振,从8MHz改成12MHz,CubeMX里不把PLLMUL改成6,生成代码还是一样跑不动。图形化工具能提高效率,却不能替代对原理的理解。

所以在初学阶段,我更推荐你先用标准库手写一遍上面的RCC_Configuration,再切换到HAL库去看SystemClock_Config。手写一遍,你才能捕捉到那些"自动生成代码"帮你藏起来的细节。

4. 配置时钟树时最容易踩的四个坑

4.1 PLL倍频超上限:72MHz是怎么算出来的

再啰嗦一遍公式:

SYSCLK = 输入源频率 × PLLMUL倍数

F1系列的PLLMUL范围一般是2到16。8MHz外部晶振乘9,刚好72MHz。有人会把倍频系数直接拉到16,得到128MHz。芯片会怎样?轻则不工作,重则Flash读取错乱、程序跑飞,而且这种问题非常隐蔽——你甚至不知道是自己把时钟配超了。

排查方法很简单:先把配置停掉PLL,退回HSI 8MHz,看芯片能不能恢复正常。如果能,基本可以确定问题出在PLL配置上。然后用计算器算一遍当前输入源和倍频系数的乘积,对照数据手册的SYSCLK最高值。对于F103,超过72MHz就是超频。

另外还有封装、温度、电压的影响。即使你刚好卡在72MHz,如果供电电压偏低,也可能出现时钟裕量不足。所以在设计上我会留一点余量,不去挑战上限。

4.2 APB1外设时钟36MHz的硬限制,以及定时器加倍现象

APB1最高只能到36MHz,这个"最高"不是建议,而是数据手册里的绝对限制。如果你把APB1预分频设为1,让PCLK1直接等于72MHz,那挂在APB1上的USART2、I2C等外设就直接处于超规格工作状态。

更经典的问题出在定时器上。F1系列的定时器,比如TIM2、TIM3、TIM4,挂在APB1总线上。当APB1预分频系数不是1时,定时器的时钟会被硬件自动乘以2。也就是说:默认配置下PCLK1=36MHz,但TIM2的实际计数时钟是72MHz。如果你不理解这一点,在计算定时器重装载值时按36MHz算,定时值就会偏小一半,实际中断频率就会翻倍。这是所有初学定时器的人几乎都会遇到的第一个坑。

反过来,如果你哪天把APB1分频改成1,PCLK1变成72MHz,超出36MHz的限制不说,定时器时钟因为不需要加倍而变成72MHz,反而和之前"PCLK1二分频+定时器加倍"的效果一样。这个巧合经常让人更困惑。我的建议是:始终按"定时器实际时钟"来计算,而不是背一个笼统的PCLK1。

4.3 外部晶振没起振:程序"假死"在启动阶段

HSE起振失败是非常常见的。现象通常是:程序能烧录,但一运行就停在某个地方不动;在线调试时,停在了RCC_WaitForHSEStartUp的循环里。造成HSE不起振的原因一般有:

  • 晶振虚焊、焊盘氧化,或PCB上晶体两端走线过长。
  • 负载电容没有焊接,或容值偏差过大,导致振荡条件不满足。
  • 软件误配置了HSE_BYPASS旁路模式,而硬件又没有提供外部时钟输入信号。
  • 晶振频率和初始化代码里假设的完全对不上。

排查思路我一般是这样:先检查两个晶振引脚的波形,用示波器探头接触OSC_IN引脚附近,能看到正弦波说明起振了。如果没有示波器,就先把代码改成内部HSI启动,如果程序能跑,说明问题大概率出在HSE硬件链路而非代码逻辑。

实验室里有个师弟曾经被这个问题卡了两天,最后发现只是晶振的两个负载电容没贴,焊接补上之后一切正常。嵌入式里很多"灵异问题",最后都指向这种极其基础的硬件细节。

4.4 改了时钟后串口乱码的完整排查链路

这个坑我自己踩过,值得写完整。

某块板子默认HSE是8MHz,我把它换成了12MHz晶振,但没有同步修改PLL倍频系数。当时系统时钟变成了108MHz,虽然没有立即崩溃,但USART1挂在APB2上,PCLK2跟着变成108MHz。我在代码里设的波特率9600,实际计算时用的却是108MHz去算分频,于是实际波特率变成了14400左右,跟串口助手的9600对不上,自然乱码。

正确的排查链路应该是:

  1. 确认系统时钟是否是你预期值。直接读寄存器或者在调试器里看RCC->CFGR的SW位和PLLMULL位。
  2. 确认总线时钟HCLK/PCLK1/PCLK2。标准库可以调用RCC_GetClocksFreq,HAL可以调用HAL_RCC_GetClockConfig,把这些值打印出来。
  3. 确认外设时钟。USART1在APB2上,波特率计算就用PCLK2;USART2/3在APB1上,波特率计算就用PCLK1。
  4. 用逻辑分析仪实测TXD引脚的实际波特率,和设置值对比,偏差一目了然。

这里有个细节需要注意:串口波特率寄存器里写入的不是波特率本身,而是一个基于外设时钟的分频系数。外设时钟一变,分频系数没变,实际波特率就跟着变。所以你排查的方向不是去改波特率寄存器,而是先回头看外设时钟本身。

5. 用示波器和一小段代码验证时钟是否真的配对

5.1 把系统时钟从MCO引脚引出来测量

F1系列有一个MCO引脚(PA8),可以把内部时钟输出到外部,用示波器或频率计直接量。这个功能在调试时钟配置时非常好用,却经常被忽略。

配置MCO输出系统时钟的示例代码如下:

void MCO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); RCC_MCOConfig(RCC_MCO_SYSCLK_Div2); // 输出SYSCLK的一半,即36MHz }

注意,F1的MCO输出最高到50MHz左右,72MHz直接输出可能超限,所以一般选Div1可能不稳,稳妥的做法是选Div2输出36MHz。用示波器一量,如果显示36MHz左右,说明系统时钟链路配置正确。如果显示20MHz或任意奇怪频率,立刻就知道PLL配置不对。

这个方法比单纯用延时函数"感觉"准确得多,尤其在排查那些"看起来能跑但不正常"的疑难问题时有奇效。

5.2 用SysTick延时反推主频

如果没有示波器,也可以用内核自带的SysTick定时器做粗验证。SysTick的时钟可以配置成HCLK或HCLK/8。假如HCLK是72MHz,HCLK/8就是9MHz。利用SysTick做1秒延时,如果实际观察到LED翻转周期是2秒或0.5秒,就说明HCLK与预期不符。

思路很简单:先配置SysTick装载一个固定数值,打开中断,让LED在中断里翻转。程序跑起来后在LED引脚上接个频率计或简单用秒表数翻转次数,10秒内应该翻20次左右。偏差超过10%,基本可以怀疑时钟配置有问题。

这个方法精度有限,但不依赖额外设备,适合手头只有一块板子的情况。做出来之后,再回头核对寄存器配置,往往能锁定问题范围。

5.3 实操中我常用的几个验证套路

最后分享几个我实际项目中常用的做法:

第一,调试时利用IDE的寄存器窗口,直接查看RCC_CFGR的SW位和RCC->CFGR里的SWS位,确认切换是否生效。F1在切换时钟源后,SWS位会实时反映当前系统时钟来源。看这个比看代码更直接。

第二,先跑HSI 8MHz的基准配置,确认最小系统、LED、串口链路都能工作,再切换HSE+PLL。分阶段验证的好处是:一旦出问题,能快速缩小范围,而不是在"硬件坏了"和"软件错了"之间反复猜。

第三,代码里不要直接写死波特率,而是尽量用带外设时钟参数的初始化宏。比如USART_InitStructure.USART_BaudRate = 115200没问题,但要确保它的分频计算函数传入的是正确的PCLK值。如果传错了PCLK,即使系统时钟正确,串口一样会乱码。

我在实际使用中发现,只要把时钟树这张图放在手边,在配置任何外设前先花30秒确认"我这个外设挂在哪个总线、总线时钟是多少、外设时钟又是多少",后续的波特率、定时、采样率这些参数就基本不会出大错。这30秒,比出问题后满世界找原因省太多时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 9:28:54

大模型落地企业的四种形态:API嵌入、RAG、Agent与私有化精调实践指南

大模型落地企业早就不是PPT里的事了,年初开始我就在帮好几家公司看这类需求,发现一个很有意思的现象:一边是各种分享会讲得天花乱坠,一边是不少刚接触AI研发的同学连“大模型到底怎么进业务”都还没摸清楚。这里不扯太玄的理论&am…

作者头像 李华
网站建设 2026/10/20 9:28:46

AGI核心突破的技术验证:四把尺子与三组实验

最近圈子里都在转一条消息:某家头部AI实验室的前首席研究官公开表态,AGI的核心突破已经实现。说实话,我第一眼看到这个判断,没有急着当新闻报道划过,而是停下来把它拆了一遍。AGI这个词被说太多次了,但真正…

作者头像 李华
网站建设 2026/10/20 9:28:35

浏览器AI导出插件真相:为什么‘AI导出鸭’根本不存在

1. 问题本质:不是“能不能用”,而是“根本不存在这个插件” 看到标题第一反应是皱眉——“AI导出鸭”这个名称本身就不符合主流浏览器插件生态的基本命名逻辑和功能范式。我从业十年,经手过上千个Chrome系扩展、参与过三个大型企业级插件平台…

作者头像 李华
网站建设 2026/10/20 9:27:51

Linux内核时钟框架实战:从设备树绑定到驱动调试的完整指南

1. 子系统的演进逻辑:为什么内核里要单设一套时钟框架先聊点背景。做过嵌入式Linux驱动的人,大概率都有过这种经历:芯片手册上写着某个外设需要xxMHz的时钟才能工作,你翻遍寄存器手册找到PLL_CTRL、DIV_CFG,再小心翼翼…

作者头像 李华
网站建设 2026/10/20 9:27:49

Linux内核时钟管理:Common Clock Framework核心机制与调试实践

做嵌入式 Linux 驱动开发&#xff0c;迟早都会撞上一个叫 Common Clock Framework&#xff08;CCF&#xff09;的东西。尤其当你开始适配新的主控芯片、给新平台做 bring-up&#xff0c;或者看到设备树里满屏的clocks <&xxx 3>却一脸懵的时候&#xff0c;你就知道你…

作者头像 李华
网站建设 2026/10/20 9:25:59

ESP32-S3离线人脸识别源码拆解:从硬件选型到二次开发实战

简介&#xff1a;这份资源面向嵌入式开发与物联网视觉应用的学习者&#xff0c;提供基于ESP32-S3芯片实现人脸检测与识别的完整工程代码。ESP32-S3集成Wi-Fi、蓝牙及神经网络加速单元&#xff0c;适合在低功耗边缘设备上运行机器学习算法&#xff0c;资源涵盖图像采集、预处理、…

作者头像 李华