STM32 零基础可移植教程 17:USART + DMA + IDLE,串口不定长接收怎么做

STM32 零基础可移植教程 17:USART + DMA + IDLE,串口不定长接收怎么做

前面我们已经写过三篇串口:

bash 复制代码
第 07 篇:USART 串口打印,从 CubeMX 配置到 printf 输出

第 08 篇:串口接收一个字节,先把 RX 中断跑通

第 09 篇:串口收一行命令,用 led on 控制 LED

那为什么还要再写一篇 USART + DMA + IDLE?

因为真实项目里,串口数据经常不是固定长度。

比如上位机可能发:

bash 复制代码
led on

也可能发:

bash 复制代码
set pwm 50

也可能发一段二进制协议:

bash 复制代码
AA 55 03 01 02 03 5A

如果每来一个字节就进一次中断,数据量小的时候还好;数据量一大,CPU 就会频繁被打断。

DMA 可以帮我们把字节搬进内存。

但 DMA 又有一个问题:

bash 复制代码
它知道缓冲区什么时候满

但它不知道对方这一帧什么时候发完

这时候就轮到 IDLE 出场。

IDLE 可以理解成:

bash 复制代码
串口线上安静了一小段时间,说明对方可能发完了一帧

所以这篇只做一个明确目标:

bash 复制代码
用 USART + DMA + IDLE 接收不定长数据,并通过 printf 打印长度、文本和 HEX

先不解析复杂协议,不做环形队列,不做 RTOS。

先把"不定长接收"这条链路跑通。

本篇目标

最终现象:

电脑串口助手发送任意内容,比如:

bash 复制代码
hello stm32

STM32 串口打印:

bash 复制代码
RX len=11, lost=0, text=hello stm32, hex=68 65 6C 6C 6F 20 73 74 6D 33 32

发送:

bash 复制代码
led on

STM32 串口打印:

bash 复制代码
RX len=6, lost=0, text=led on, hex=6C 65 64 20 6F 6E

本篇用到的外设:

bash 复制代码
USART

DMA

USART IDLE

本篇跑通标准:

  • 串口能正常 printf 输出;

  • 串口助手发送不同长度文本,STM32 都能收到;

  • 打印出来的 len 和实际发送长度大致一致;

  • 能说清楚 DMA 负责搬数据,IDLE 负责判断一帧可能结束;

  • 知道 MX_DMA_Init() 和 UART 中断配置为什么重要。

准备工作

你需要准备:

|

项目

|

说明

|

| --- | --- |

|

STM32 开发板

|

任意带 USART 和 DMA 的 STM32 都可以

|

|

下载器

|

ST-LINK/V2 或板载 ST-LINK

|

|

USB 转 TTL 模块

|

如果开发板没有板载串口转 USB

|

|

串口助手

|

用来发送不定长数据

|

|

杜邦线

|

外接串口模块时使用

|

接线和第 07 篇一样:

|

USB 转 TTL

|

STM32

|

| --- | --- |

|

TXD

|

STM32 RX

|

|

RXD

|

STM32 TX

|

|

GND

|

GND

|

注意:

bash 复制代码
TX 和 RX 要交叉

GND 必须共地

不要把 5V TTL 直接接到不耐 5V 的 STM32 RX

如果你用的是开发板板载 USB 转串口,那一般不需要额外接线。

先把 DMA 和 IDLE 分清楚

DMA 解决的是"搬运问题"。

不用 DMA 时:

bash 复制代码
来 1 个字节 -> 进 1 次中断 -> CPU 读 1 次 RDR/DR

用了 DMA 后:

bash 复制代码
来 1 个字节 -> DMA 自动搬到数组

来 1 个字节 -> DMA 自动搬到数组下一个位置

CPU 不需要每个字节都进中断处理。

但 DMA 只知道数组长度。

比如你给它一个 128 字节缓冲区:

bash 复制代码
uint8_t rx_buffer[128];

如果对方只发了 7 个字节:

bash 复制代码
led on\n

DMA 不会天然知道"这一帧就是 7 个字节"。

如果你只等 DMA 收满 128 字节,那读者会等到怀疑人生。

IDLE 解决的是"什么时候算一帧结束"的问题。

当串口收到一段数据后,线路空闲超过 1 帧时间,硬件就会产生 IDLE 事件。

可以简单理解为:

bash 复制代码
刚才有数据

现在安静了一小会儿

那这一段数据大概率发完了

所以组合起来就是:

bash 复制代码
DMA 负责把收到的字节搬进数组

IDLE 负责通知程序:现在可以把这一段拿出来处理了

本篇为什么用 HAL_UARTEx_ReceiveToIdle_DMA

STM32 HAL 里提供了一个很适合入门的函数:

bash 复制代码
HAL_UARTEx_ReceiveToIdle_DMA()

它的意思是:

bash 复制代码
用 DMA 接收串口数据

遇到 IDLE 或缓冲区满时,触发回调告诉用户收到了多少字节

回调函数是:

bash 复制代码
void
 
HAL_UARTEx_RxEventCallback
(UART_HandleTypeDef *huart, uint16_t Size)

其中 Size 就是本次收到的数据长度。

本篇代码做的事情很简单:

  1. 启动 HAL_UARTEx_ReceiveToIdle_DMA()

  2. 串口收到一段数据;

  3. 进入 HAL_UARTEx_RxEventCallback()

  4. 把 DMA 缓冲区里的数据复制到一块应用层缓冲区;

  5. 设置 s_frame_ready = 1

  6. 主循环读取这一帧并打印;

  7. 回调里重新启动下一次 DMA + IDLE 接收。

这里有一个小细节:

bash 复制代码
__HAL_DMA_DISABLE_IT(APP_UART_IDLE_HANDLE.hdmarx, DMA_IT_HT);

它是把 DMA 半传输中断关掉。

因为本篇只想在:

bash 复制代码
IDLE

或者缓冲区满

时处理数据。

如果半传输中断也打开,新手可能会看到一帧数据被拆成奇怪的两段,反而更迷糊。

CubeMX 配置步骤

1. 复制前面的串口工程

建议从第 09 篇串口命令工程复制一份,改名为:

bash 复制代码
17_usart_dma_idle

如果你重新建工程,也可以按第一篇流程:

  1. 选择芯片型号;

  2. SYS -> Debug 设置为 Serial Wire

  3. 配置时钟;

  4. 配置 USART;

  5. 配置 DMA;

  6. 生成 Keil 工程。

2. 配置 USART

选择你要用的 USART,比如:

bash 复制代码
USART1

模式选择:

bash 复制代码
Asynchronous

参数先用最常见的:

|

配置项

|

推荐值

|

| --- | --- |

|

Baud Rate

|

115200

|

|

Word Length

|

8 Bits

|

|

Parity

|

None

|

|

Stop Bits

|

1

|

|

Hardware Flow Control

|

None

|

3. 给 USART RX 添加 DMA

进入 USART 的 DMA Settings。

添加:

bash 复制代码
USARTx_RX

DMA 参数推荐:

|

配置项

|

推荐值

|

说明

|

| --- | --- | --- |

|

Direction

|

Peripheral to Memory

|

串口数据寄存器搬到内存

|

|

Mode

|

Normal

|

本篇回调后手动重新开启接收

|

|

Peripheral Increment

|

Disable

|

串口数据寄存器地址不变

|

|

Memory Increment

|

Enable

|

接收数组地址要往后走

|

|

Peripheral Data Width

|

Byte

|

串口 1 字节 1 字节收

|

|

Memory Data Width

|

Byte

|

数组也是 uint8_t

|

|

Priority

|

Low / Medium

|

入门先默认即可

|

为什么这里用 Normal,不用 Circular?

因为本篇目标是先理解:

bash 复制代码
一段数据 -> IDLE -> 回调 -> 主循环处理

Normal 模式更好理解。

后面如果要做高吞吐协议、不断流接收、环形缓冲区,再单独升级 Circular。

4. 打开 USART 全局中断

这一步非常重要。

IDLE 是 USART 外设产生的事件,HAL 要通过 USART 中断去处理。

所以在 NVIC 里打开:

bash 复制代码
USARTx global interrupt

生成代码后,stm32f1xx_it.c 里应该有类似:

bash 复制代码
void USART1_IRQHandler(void)
{

  HAL_UART_IRQHandler(&huart1);

}

如果这个中断没开,DMA 可能在搬数据,但 IDLE 事件进不了 HAL 回调。

5. 确认 DMA 初始化顺序

CubeMX 生成的 main.c 里,通常会有:

bash 复制代码
MX_GPIO_Init();

MX_DMA_Init();

MX_USART1_UART_Init();

建议确认:

bash 复制代码
MX_DMA_Init() 在 MX_USART1_UART_Init() 前面

因为 USART 初始化时会把 UART 句柄和 DMA 句柄关联起来。

如果 DMA 初始化顺序不对,HAL_UARTEx_ReceiveToIdle_DMA() 可能启动失败,或者回调不进。

Keil 工程生成和编译

生成 Keil 工程后,先编译 CubeMX 原始工程:

bash 复制代码
Build / F7

确认没有错误:

bash 复制代码
0 Error(s)

然后新建两个文件:

bash 复制代码
Core/Inc/app_uart_dma_idle.h

Core/Src/app_uart_dma_idle.c

如果你是手动新建 .c 文件,记得在 Keil 工程树里添加:

bash 复制代码
Core/Src/app_uart_dma_idle.c

完整代码

1. 新建 Core/Inc/app_uart_dma_idle.h

bash 复制代码
#ifndef APP_UART_DMA_IDLE_H

#define APP_UART_DMA_IDLE_H


#include "main.h"

#include <stdint.h>


#ifndef APP_UART_IDLE_RX_BUFFER_SIZE

#define APP_UART_IDLE_RX_BUFFER_SIZE 128u

#endif


typedef
struct{

    
uint8_t
 data[APP_UART_IDLE_RX_BUFFER_SIZE];

    
uint16_t
 length;

    
uint32_t
 lost_count;

} App_UARTIdle_Frame;


void App_UARTIdle_Init(void)
;

HAL_StatusTypeDef App_UARTIdle_Start(void)
;

void App_UARTIdle_Stop(void)
;

uint8_t App_UARTIdle_GetFrame(App_UARTIdle_Frame *frame)
;


#endif

这里的 APP_UART_IDLE_RX_BUFFER_SIZE 是单帧最大接收长度。

本篇先设成:

bash 复制代码
128 字节

如果你要收更长的数据,可以适当调大。

但注意,缓冲区越大,占用 RAM 越多。

2. 新建 Core/Src/app_uart_dma_idle.c

bash 复制代码
#include "app_uart_dma_idle.h"


#ifndef APP_UART_IDLE_HANDLE

#define APP_UART_IDLE_HANDLE huart1

#endif


extern
 UART_HandleTypeDef APP_UART_IDLE_HANDLE;


static
uint8_t
 s_dma_rx_buffer[APP_UART_IDLE_RX_BUFFER_SIZE];

static
uint8_t
 s_frame_buffer[APP_UART_IDLE_RX_BUFFER_SIZE];

static
volatile
uint16_t
 s_frame_length = 
0u
;

static
volatile
uint8_t
 s_frame_ready = 
0u
;

static
volatile
uint32_t
 s_lost_count = 
0u
;


static void App_UARTIdle_ClearDmaBuffer(void)
{

    
uint16_t
 i;


    
for
 (i = 
0u
; i < APP_UART_IDLE_RX_BUFFER_SIZE; i++)

    {

        s_dma_rx_buffer[i] = 
0u
;

    }

}


void App_UARTIdle_Init(void)
{

    s_frame_length = 
0u
;

    s_frame_ready = 
0u
;

    s_lost_count = 
0u
;

    App_UARTIdle_ClearDmaBuffer();

}


HAL_StatusTypeDef App_UARTIdle_Start(void)
{

    HAL_StatusTypeDef status;


    status = HAL_UARTEx_ReceiveToIdle_DMA(&APP_UART_IDLE_HANDLE,

                                          s_dma_rx_buffer,

                                          APP_UART_IDLE_RX_BUFFER_SIZE);


    
/*     * We only care about IDLE or full-buffer events in this beginner example.     * Half-transfer interrupts can make readers think one frame arrived twice.     */

    
if
 ((status == HAL_OK) && (APP_UART_IDLE_HANDLE.hdmarx != 
0
))

    {

        __HAL_DMA_DISABLE_IT(APP_UART_IDLE_HANDLE.hdmarx, DMA_IT_HT);

    }


    
return
 status;

}


void App_UARTIdle_Stop(void)
{

    (
void
)HAL_UART_DMAStop(&APP_UART_IDLE_HANDLE);

}


uint8_t App_UARTIdle_GetFrame(App_UARTIdle_Frame *frame)
{

    
uint16_t
 i;


    
if
 (frame == 
0
)

    {

        
return
0u
;

    }


    __disable_irq();

    
if
 (s_frame_ready == 
0u
)

    {

        __enable_irq();

        
return
0u
;

    }


    frame->length = s_frame_length;

    frame->lost_count = s_lost_count;


    
for
 (i = 
0u
; i < frame->length; i++)

    {

        frame->data[i] = s_frame_buffer[i];

    }


    s_frame_ready = 
0u
;

    s_frame_length = 
0u
;

    __enable_irq();


    
return
1u
;

}


void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t size)
{

    
uint16_t
 i;

    
uint16_t
 copy_size;


    
if
 (huart != &APP_UART_IDLE_HANDLE)

    {

        
return
;

    }


    
if
 (size > APP_UART_IDLE_RX_BUFFER_SIZE)

    {

        copy_size = APP_UART_IDLE_RX_BUFFER_SIZE;

    }

    
else

    {

        copy_size = size;

    }


    
if
 (copy_size > 
0u
)

    {

        
if
 (s_frame_ready == 
0u
)

        {

            
for
 (i = 
0u
; i < copy_size; i++)

            {

                s_frame_buffer[i] = s_dma_rx_buffer[i];

            }


            s_frame_length = copy_size;

            s_frame_ready = 
1u
;

        }

        
else

        {

            s_lost_count++;

        }

    }


    (
void
)App_UARTIdle_Start();

}

这段代码有几个点值得单独说。

第一,s_dma_rx_buffer 是 DMA 正在写的缓冲区:

bash 复制代码
static
 
uint8_t
 s_dma_rx_buffer[APP_UART_IDLE_RX_BUFFER_SIZE];

第二,s_frame_buffer 是应用层准备给主循环读取的一帧:

bash 复制代码
static
 
uint8_t
 s_frame_buffer[APP_UART_IDLE_RX_BUFFER_SIZE];

为什么不让主循环直接读 DMA buffer?

因为回调结束后我们会重新启动下一次 DMA 接收,DMA buffer 后面还会继续被写。

所以收到一帧后,先复制出来,主循环读复制后的这一份,更稳。

第三,s_frame_ready 是中断回调和主循环之间的标志:

bash 复制代码
static
 
volatile
 
uint8_t
 s_frame_ready = 
0u
;

回调里置 1:

bash 复制代码
s_frame_ready = 
1u
;

主循环读走后清 0:

bash 复制代码
s_frame_ready = 
0u
;

第四,App_UARTIdle_GetFrame() 里用了临界区:

bash 复制代码
__disable_irq();

...

__enable_irq();

原因和前面讲过的临界区一样:这些标志和长度变量会被中断回调修改,也会被主循环读取。

这段临界区很短,只复制最多 128 字节,入门阶段可以接受。

第五,如果上一帧还没被主循环取走,新的一帧又来了,代码会增加:

bash 复制代码
s_lost_count++;

这表示有帧被丢掉了。

真实项目里可以改成环形队列。本篇先不加,避免一下子把代码写得太重。

main.c 调用方式

1. 添加头文件

main.c 顶部添加:

bash 复制代码
/* USER CODE BEGIN Includes */

#include "app_uart_dma_idle.h"

#include <stdio.h>

/* USER CODE END Includes */

2. 初始化后启动接收

确认 CubeMX 已生成:

bash 复制代码
MX_GPIO_Init();

MX_DMA_Init();

MX_USART1_UART_Init();

然后在 USER CODE BEGIN 2 中添加:

bash 复制代码
/* USER CODE BEGIN 2 */

App_UARTIdle_Init();

if
 (App_UARTIdle_Start() != HAL_OK)

{

    
printf
(
"UART DMA IDLE start failed\r\n"
);

}


printf
(
"\r\nUSART DMA IDLE receive test\r\n"
);

printf
(
"Send any text from serial assistant.\r\n"
);

/* USER CODE END 2 */

3. while 循环里处理收到的一帧

bash 复制代码
/* USER CODE BEGIN WHILE */

while
 (
1
)

{

/* USER CODE END WHILE */


/* USER CODE BEGIN 3 */

  App_UARTIdle_Frame frame;

uint16_t
 i;


if
 (App_UARTIdle_GetFrame(&frame) != 
0u
)

  {

      
printf
(
"RX len=%u, lost=%lu, text="
, frame.length, frame.lost_count);


      
for
 (i = 
0u
; i < frame.length; i++)

      {

          
putchar
(frame.data[i]);

      }


      
printf
(
", hex="
);

      
for
 (i = 
0u
; i < frame.length; i++)

      {

          
printf
(
"%02X "
, frame.data[i]);

      }


      
printf
(
"\r\n"
);

  }


  HAL_Delay(
10
);

/* USER CODE END 3 */

}

这里不需要在 while 里反复启动 DMA 接收。

第一次启动在:

bash 复制代码
App_UARTIdle_Start();

后面每次收到一帧,回调里会重新启动下一次接收。

编译、下载和验证

代码加完后:

  1. Keil 编译;

  2. 下载程序;

  3. 打开串口助手;

  4. 设置 115200 8N1;

  5. 发送不同长度的文本。

比如发送:

bash 复制代码
hello

正常输出:

bash 复制代码
RX len=5, lost=0, text=hello, hex=68 65 6C 6C 6F

发送:

bash 复制代码
stm32 dma idle

正常输出:

bash 复制代码
RX len=14, lost=0, text=stm32 dma idle, hex=73 74 6D 33 32 20 64 6D 61 20 69 64 6C 65

如果串口助手勾选了"发送新行",那实际会多出:

bash 复制代码
\r

\n

HEX 里可能看到:

bash 复制代码
0D 0A

这不是错误,而是串口助手帮你加了回车换行。

移植到其他板子的修改点

|

要改的地方

|

为什么要改

|

在哪里改

|

| --- | --- | --- |

|

USART 实例

|

可能不是 USART1

|

CubeMX USART,代码里的 APP_UART_IDLE_HANDLE

|

|

TX/RX 引脚

|

不同板子串口引脚不同

|

CubeMX Pinout

|

|

波特率

|

要和串口助手一致

|

CubeMX USART 参数

|

|

USART RX DMA 请求

|

不同芯片 DMA 映射不同

|

CubeMX DMA Settings

|

|

DMA 模式

|

本篇用 Normal

|

CubeMX DMA Mode

|

|

USART 全局中断

|

IDLE 需要 UART IRQ 进入 HAL

|

CubeMX NVIC

|

|

DMA 初始化顺序

|

DMA 要先于 USART 初始化

| main.c

MX_DMA_Init() 位置

|

|

接收缓冲区大小

|

单帧长度不同

| APP_UART_IDLE_RX_BUFFER_SIZE |

|

HAL 版本

|

老版本可能没有 HAL_UARTEx_ReceiveToIdle_DMA()

|

升级 Cube HAL 或改手动 IDLE 中断方案

|

如果你用的是 USART2,只需要把代码中的默认句柄改成:

bash 复制代码
#define APP_UART_IDLE_HANDLE huart2

也可以在 app_uart_dma_idle.c 顶部改。

如果你使用的是 USART3,就改成:

bash 复制代码
#define APP_UART_IDLE_HANDLE huart3

常见问题排查

1. 编译报 HAL_UARTEx_ReceiveToIdle_DMA 未定义

这通常说明你的 STM32 HAL 库版本比较旧,或者当前系列 HAL 没有这个扩展函数。

解决思路:

  1. 优先升级对应芯片的 Cube Firmware Package;

  2. 检查 stm32xx_hal_uart_ex.c 是否参与编译;

  3. 如果确实没有这个 API,可以后续单独写一篇"手动 IDLE 中断 + DMA 接收"的版本。

本篇先走 HAL 已经封装好的路线。

2. App_UARTIdle_Start() 返回错误

优先检查:

|

检查项

|

说明

|

| --- | --- |

|

USART 是否初始化

| MX_USARTx_UART_Init()

是否调用

|

|

DMA 是否初始化

| MX_DMA_Init()

是否调用

|

|

初始化顺序

| MX_DMA_Init()

是否在 MX_USARTx_UART_Init()

|

|

RX DMA 是否添加

|

CubeMX USART DMA Settings 是否有 RX

|

|

句柄是否正确

| APP_UART_IDLE_HANDLE

是否和实际 USART 一致

|

3. 串口能 printf,但收不到数据

按这个顺序查:

  1. TX/RX 是否交叉;

  2. GND 是否共地;

  3. 串口助手波特率是否一致;

  4. USART RX DMA 是否配置;

  5. USART global interrupt 是否开启;

  6. USARTx_IRQHandler() 里是否调用 HAL_UART_IRQHandler(&huartx)

  7. APP_UART_IDLE_HANDLE 是否写错。

4. DMA 好像收了,但进不了 RxEventCallback

重点查 IDLE 相关链路:

  • USART 全局中断是否打开;

  • NVIC 里是否启用了 USARTx global interrupt

  • stm32xx_it.c 里是否有 USARTx_IRQHandler()

  • IRQHandler 里是否调用 HAL;

  • 是否调用的是 HAL_UARTEx_ReceiveToIdle_DMA(),而不是普通 HAL_UART_Receive_DMA()

普通 DMA 接收不会自动进 HAL_UARTEx_RxEventCallback()

5. 收到的数据被拆成两段

常见原因:

  • 串口助手发送时中间有停顿;

  • 波特率太低,发送间隔被 IDLE 识别成一帧结束;

  • 半传输中断没有关,导致你误以为一帧被拆开;

  • 上位机不是一次性发送,而是一段一段写串口。

本篇代码里已经关闭了 DMA 半传输中断:

bash 复制代码
__HAL_DMA_DISABLE_IT(APP_UART_IDLE_HANDLE.hdmarx, DMA_IT_HT);

如果仍然被拆分,优先看上位机发送方式。

6. lost 数值增加

说明上一帧还没被主循环取走,下一帧又到了。

解决方向:

  • 主循环不要长时间 HAL_Delay()

  • 不要在主循环里做太慢的操作;

  • 增大处理速度;

  • 后续升级成帧队列或环形缓冲区;

  • 如果数据很密集,考虑 Circular DMA + ring buffer。

本篇为了新手好理解,只保留一个应用层帧缓冲。

7. 编译报回调函数重复定义

如果你的工程里已经有:

bash 复制代码
HAL_UARTEx_RxEventCallback()

再加入本篇代码就会重复定义。

解决方法是合并回调。

一个工程里同名 HAL 回调只能定义一次。

你可以在已有回调里加:

bash 复制代码
if
 (huart == &huart1)

{

    
/* 调用本篇的处理逻辑 */

}

或者把其他串口的处理也合到本篇回调里。

8. 打印文本后面有乱码

本篇接收的是一段字节,不自动在末尾加 \0

所以不要直接这样写:

bash 复制代码
printf
(
"%s"
, frame.data);

因为 printf("%s") 需要 C 字符串以 \0 结束。

本篇用的是逐字节输出:

bash 复制代码
for
 (i = 
0u
; i < frame.length; i++)

{

    
putchar
(frame.data[i]);

}

这样更适合不定长数据,也能处理二进制内容。

本篇小结

这一篇我们完成了 USART + DMA + IDLE 不定长接收。

你现在应该知道:

  • DMA 负责把串口数据自动搬到内存;

  • IDLE 负责判断"这一段数据可能发完了";

  • HAL_UARTEx_ReceiveToIdle_DMA() 很适合入门不定长接收;

  • USART global interrupt 必须打开,否则 IDLE 回调可能进不来;

  • MX_DMA_Init() 一般要在 MX_USARTx_UART_Init() 前面;

  • 回调里不要做复杂业务,复制数据、置标志、重启接收就够了;

  • 主循环拿到一帧后再打印、解析或执行命令;

  • 如果数据很密集,一个帧缓冲不够,后面要升级成队列或环形缓冲。

下一篇我们进入 I2C:

STM32 I2C 入门:先用扫描器找一找总线上有没有设备。

I2C 这部分会先从地址扫描开始,不急着读传感器寄存器。先确认总线上真的有设备,再谈通信。

相关推荐
史蒂芬_丁2 小时前
Cortex-M内核中断保护机制详解:PRIMASK寄存器的正确使用方法
单片机·嵌入式硬件
榴莲llll2 小时前
LED高亮数码管显示驱动芯片数显屏驱动器最大支持13×3的按键VK16K33A
单片机
崇山峻岭之间3 小时前
单片机传感器实验
单片机·嵌入式硬件
芯岭技术5 小时前
PY32F030国产32位MCU,应用场景广泛,宽工作电压、丰富外设
单片机·嵌入式硬件·物联网
FreakStudio9 小时前
大话电容传感器和电容SOC芯片,看这一篇就够了
python·单片机·嵌入式·面向对象·并行计算·电子diy·电子计算机
信看10 小时前
常见通信接口
单片机·嵌入式硬件
m0_3771081411 小时前
USART
stm32
Rsingstarzengjx11 小时前
STM32-F103ZET6开发板
stm32·单片机·嵌入式硬件
我先去打把游戏先12 小时前
VMware NAT 模式 Ubuntu 虚拟机「宿主机能上网、虚拟机 ping 不通外网 + apt 更新卡死」全故障复盘
linux·运维·vscode·单片机·嵌入式硬件·ubuntu·keil5