STM32F103标准库USART3 DMA收发实战:彻底解放CPU

简介:这份资源面向嵌入式开发初学者与中级工程师,聚焦STM32F103标准库下USART3的DMA收发实现,帮助解决串口通信中CPU占用高、实时性不足的问题。包内共171个文件,以c源码、h头文件、s汇编启动文件为主,辅以o目标文件、crf编译中间文件、uvproj与uvopt工程配置、hex烧录文件及map映射文件等,压缩包约3.41MB,构成一套可直接编译的Keil工程模板。内容围绕DMA通道选择、传输方向与源目标地址配置、USART3时钟与波特率设置、DMA请求使能、传输完成与错误中断处理等关键环节展开,并给出初始化、启动传输与中断回调的软件流程。已有3312人学习下载,适合希望掌握标准库DMA串口收发、对照工程模板快速搭建实验环境并排查传输异常的开发者参考。

1. 串口收包还在死等中断?这套 DMA 方案让 CPU 彻底解放

如果你用 STM32F103 做过串口通信,大概率经历过这样的场景:主循环里塞了一个while(USART_GetFlagStatus(USART3, USART_FLAG_RXNE) == RESET),波特率一高、数据一多,CPU 就被拖死在轮询里,稍微加点别的任务就开始丢字节。更别提用中断逐字节收,每来一个字节进一次中断,115200 波特率下每秒上万次中断,主循环基本别想干正事了。

这个项目就是冲着这个痛点来的:在 STM32F103 上用标准库(Standard Peripheral Library)配置 USART3,收发全部走 DMA 通道,CPU 只在数据搬完之后处理一次,中间完全不参与字节搬运。它适合正在做多串口数据采集、Modbus 通信、上位机指令解析的嵌入式开发者,尤其是那些主控资源紧张、又不想上 RTOS 的场景。整套代码基于标准库而非 HAL,对习惯寄存器级控制的开发者更友好,移植到同系列芯片也直接可用。

2. 架构拆解:USART3 与 DMA1 的通道映射关系

2.1 为什么是 USART3 配 DMA1 的这两个通道

STM32F103 的 DMA 控制器分 DMA1 和 DMA2,其中 DMA1 有 7 个通道,每个通道固定映射到特定的外设请求。USART3 的收发请求在 DMA1 上的映射是硬连线的,不能随便换通道:

外设请求DMA 控制器通道传输方向
USART3_TXDMA1Channel 2存储器 → 外设
USART3_RXDMA1Channel 3外设 → 存储器

这个映射关系在参考手册的 DMA 请求映射表里写死了,配错通道 DMA 请求根本不会触发,现象就是「代码看着没问题但一个字节都收不到」。我见过有人把 RX 配到 Channel 2,调了一下午以为是波特率问题,血泪经验。

发送方向是「存储器到外设」,DMA 从你给的缓冲区里读数据,逐个写到USART3->DR;接收方向是「外设到存储器」,每收到一个字节,USART3 向 DMA 发一次请求,DMA 把USART3->DR的值搬到你的缓冲区。两个方向互不干扰,可以同时开启。

2.2 整体数据流与中断分工

整个方案的数据流是这样的:

  • 发送链路:应用层把待发数据放进USART3_TX_BUF,调用发送函数 → 配置 DMA 传输长度 → 使能 DMA 通道 → DMA 自动把数据搬到 USART3 数据寄存器 → 搬完后触发 DMA 传输完成中断 → 在中断里关闭通道、置发送完成标志。
  • 接收链路:USART3 每收到一个字节触发 DMA 请求 → DMA 把字节搬到USART3_RX_BUF→ 收满预设长度后触发 DMA 传输完成中断 → 在中断里置接收完成标志,应用层轮询这个标志来处理数据。

关键点在于:CPU 只在「一整包数据搬完」时进一次中断,而不是每个字节进一次。这就是 DMA 方案相比中断收发的核心优势。

提示:接收用 DMA 传输完成中断,前提是你知道每包数据的固定长度。如果是变长协议(比如 Modbus RTU 靠 3.5 字符间隔断帧),需要额外配合 USART 的空闲中断(IDLE)来判断一帧结束,这个后面在改造章节会讲。

2.3 缓冲区设计:双缓冲与长度约束

缓冲区大小直接决定了单次能收发的最大数据量。DMA 的传输长度寄存器DMA_CNDTRx是 16 位的,所以单次传输最大 65535 字节。实际项目里一般不会开这么大,按协议最大帧长来定就行。

#define USART3_TX_BUF_SIZE 256 #define USART3_RX_BUF_SIZE 256 u8 USART3_TX_BUF[USART3_TX_BUF_SIZE]; u8 USART3_RX_BUF[USART3_RX_BUF_SIZE]; volatile u8 usart3_tx_done = 1; // 发送完成标志,1 表示空闲 volatile u8 usart3_rx_done = 0; // 接收完成标志

这里usart3_tx_done初值设为 1,表示上电后发送通道空闲,可以直接发第一包。usart3_rx_done初值 0,等 DMA 收满一包后在中断里置 1。两个标志都用volatile修饰,防止编译器优化掉中断里的修改。

缓冲区建议用全局数组而不是局部变量,因为 DMA 传输是异步的,函数返回后局部数组就失效了,DMA 还在往里搬数据,这就是典型的踩坑点。

3. 初始化实战:从 GPIO 到 DMA 的完整配置链路

3.1 GPIO 与 USART3 复用配置

USART3 的默认引脚是 PB10(TX)和 PB11(RX)。F103 上 USART3 还可以重映射到 PC10/PC11 或 PD8/PD9,这里用默认引脚。

void USART3_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; // 使能 GPIOB 和 USART3 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); // PB10 - TX,复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // PB11 - RX,浮空输入或上拉输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &GPIO_InitStructure); }

TX 引脚必须配成GPIO_Mode_AF_PP(复用推挽),配成普通推挽输出的话 USART 外设根本控制不了这个引脚。RX 配浮空输入即可,如果线路干扰大可以改成上拉输入。时钟这里有个容易翻车的点:USART3 挂在 APB1 上,要用RCC_APB1PeriphClockCmd,而 GPIOB 挂在 APB2 上,用RCC_APB2PeriphClockCmd。两个总线搞混了,外设就是不动。

3.2 USART3 参数初始化

void USART3_Config(void) { USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART3, &USART_InitStructure); // 使能 USART3 的 DMA 发送和接收请求 USART_DMACmd(USART3, USART_DMAReq_Tx | USART_DMAReq_Rx, ENABLE); // 使能 USART3 USART_Cmd(USART3, ENABLE); }

USART_DMACmd这一句是很多人漏掉的关键步骤。它负责打开 USART3 内部的 DMA 请求开关,不调用的话,USART3 收到数据也不会向 DMA 发请求,DMA 自然一动不动。参数USART_DMAReq_Tx | USART_DMAReq_Rx表示收发两个方向的 DMA 请求都使能。

波特率 115200 是常见默认值,改波特率直接改USART_BaudRate字段即可,标准库会根据 APB1 时钟自动算分频。注意 APB1 时钟默认是 36MHz,如果你改了系统时钟树,要确认RCC_GetClocksFreq算出来的 APB1 频率是对的,否则波特率会有偏差。

3.3 DMA1 通道配置(收发分开)

DMA 的配置分发送通道(Channel 2)和接收通道(Channel 3),两者结构类似但方向和外设地址不同。

void USART3_DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; // 使能 DMA1 时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // ===== 发送通道 DMA1_Channel2 ===== DMA_DeInit(DMA1_Channel2); DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&USART3->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (u32)USART3_TX_BUF; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 存储器 -> 外设 DMA_InitStructure.DMA_BufferSize = USART3_TX_BUF_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel2, &DMA_InitStructure); // ===== 接收通道 DMA1_Channel3 ===== DMA_DeInit(DMA1_Channel3); DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&USART3->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (u32)USART3_RX_BUF; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设 -> 存储器 DMA_InitStructure.DMA_BufferSize = USART3_RX_BUF_SIZE; DMA_Init(DMA1_Channel3, &DMA_InitStructure); // 配置 DMA 传输完成中断(接收通道) DMA_ITConfig(DMA1_Channel3, DMA_IT_TC, ENABLE); NVIC_Configuration(); }

逐项说明关键参数:

  • DMA_PeripheralBaseAddr固定为&USART3->DR,因为收发都是通过这一个数据寄存器。
  • DMA_DIR是方向控制,发送用DMA_DIR_PeripheralDST(外设做目的地),接收用DMA_DIR_PeripheralSRC(外设做源)。
  • DMA_PeripheralInc必须Disable,外设地址固定不变;DMA_MemoryInc必须Enable,存储器地址要递增,否则所有字节都堆在缓冲区第一个位置。
  • DMA_Mode_Normal表示搬完设定长度就停,适合定长收发。如果要循环接收(比如环形缓冲),改成DMA_Mode_Circular。
  • DMA_M2M_Disable关闭存储器到存储器模式,串口通信必须关掉。

接收通道的中断配置里,DMA_ITConfig(DMA1_Channel3, DMA_IT_TC, ENABLE)打开传输完成中断,这样收满一包后能通知应用层。

3.4 NVIC 中断优先级配置

void NVIC_Configuration(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); }

DMA1_Channel3 的中断向量是DMA1_Channel3_IRQn,别写成 Channel2 的。优先级分组用NVIC_PriorityGroup_2,抢占优先级 1、子优先级 0,这个优先级在大多数项目里够用。如果你的系统里还有更紧急的中断(比如电机控制 PWM),把 DMA 接收中断的抢占优先级调低一些。

4. 收发核心逻辑:中断服务函数与状态机

4.1 发送函数:启动 DMA 并等待完成

u8 USART3_SendData(u8 *buf, u16 len) { if (len > USART3_TX_BUF_SIZE) return 1; // 超长直接拒绝 if (usart3_tx_done == 0) return 2; // 上一包还没发完 usart3_tx_done = 0; // 拷贝数据到发送缓冲区 memcpy(USART3_TX_BUF, buf, len); // 关闭通道后才能改传输长度 DMA_Cmd(DMA1_Channel2, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel2, len); DMA_Cmd(DMA1_Channel2, ENABLE); return 0; }

这个函数做了三件事:长度校验、状态检查、启动 DMA。返回值 0 表示成功,1 表示数据超长,2 表示发送通道忙。调用方根据返回值决定是重试还是丢弃。

关键细节:修改DMA_CNDTRx之前必须先关闭通道。DMA 通道在使能状态下,传输长度寄存器是只读的,直接写不进去。所以顺序一定是「关通道 → 设长度 → 开通道」。这个顺序搞反了,现象是发送长度永远是上次的值,或者干脆发不出去。

4.2 发送完成中断处理

void DMA1_Channel2_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC2) != RESET) { DMA_ClearITPendingBit(DMA1_IT_TC2); DMA_Cmd(DMA1_Channel2, DISABLE); // 搬完关通道 usart3_tx_done = 1; // 置空闲标志 } }

发送完成中断里做两件事:关通道、置标志。关通道是为了下次发送时能重新设置长度。标志置 1 后,应用层就知道可以发下一包了。

注意这里用的是DMA1_IT_TC2,对应 Channel 2 的传输完成标志。中断服务函数名DMA1_Channel2_IRQHandler必须和启动文件里的向量表名字完全一致,拼错了就进不了中断。

4.3 接收完成中断与数据处理

void DMA1_Channel3_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC3) != RESET) { DMA_ClearITPendingBit(DMA1_IT_TC3); DMA_Cmd(DMA1_Channel3, DISABLE); usart3_rx_done = 1; // 通知应用层有完整一包数据 } }

接收中断和发送中断结构一样,区别在于接收完成后置的是usart3_rx_done。应用层在主循环里轮询这个标志:

int main(void) { USART3_GPIO_Config(); USART3_DMA_Config(); USART3_Config(); // 启动接收 DMA,等待第一包数据 DMA_Cmd(DMA1_Channel3, ENABLE); while (1) { if (usart3_rx_done) { usart3_rx_done = 0; // 处理 USART3_RX_BUF 里的数据 ProcessPacket(USART3_RX_BUF, USART3_RX_BUF_SIZE); // 重新启动接收 DMA_SetCurrDataCounter(DMA1_Channel3, USART3_RX_BUF_SIZE); DMA_Cmd(DMA1_Channel3, ENABLE); } // 其他任务照常跑,CPU 不被串口占用 } }

主循环里处理完一包数据后,要重新设置接收长度并重新使能通道,准备接下一包。这个「处理完再重启」的模式适合定长协议。如果是变长协议,处理逻辑要配合空闲中断,下一章会讲。

注意:ProcessPacket里如果处理时间较长,期间来的数据会丢失,因为 DMA 通道还没重新使能。对实时性要求高的场景,建议用双缓冲或者环形缓冲 + 空闲中断。

5. 避坑与排查:那些让 DMA 串口「装死」的细节

5.1 一个字节都收不到

现象:代码编译通过,串口助手发数据,接收缓冲区始终是初始值,usart3_rx_done永远为 0。

原因:最常见的是 DMA 通道映射搞错。USART3_RX 必须用 DMA1_Channel3,配成 Channel2 或别的通道,DMA 请求根本不会触发。其次是漏了USART_DMACmd(USART3, USART_DMAReq_Rx, ENABLE),USART 内部的 DMA 请求开关没打开。

解决:对照参考手册的 DMA 请求映射表确认通道号,检查USART_DMACmd是否调用,检查DMA_Cmd(DMA1_Channel3, ENABLE)是否执行。

5.2 只能收到第一个字节

现象:发一串数据,只有第一个字节正确,后面的全是 0 或者乱码。

原因:DMA_MemoryInc配成了Disable,DMA 每次搬运都写到缓冲区同一个地址,后面的字节把第一个覆盖了。或者DMA_PeripheralDataSize和DMA_MemoryDataSize配成了 HalfWord/Word,而串口是 8 位数据。

解决:确认DMA_MemoryInc = DMA_MemoryInc_Enable,两个 DataSize 都配成DMA_PeripheralDataSize_Byte和DMA_MemoryDataSize_Byte。

5.3 发送一次后再也发不出去

现象:第一包数据发送正常,第二包调用发送函数返回 2(忙),或者干脆没反应。

原因:发送完成中断没进,usart3_tx_done一直是 0。可能是中断向量名写错、NVIC 没使能、或者忘了在中断里清标志位导致反复进中断但逻辑不对。

解决:检查DMA1_Channel2_IRQHandler拼写,确认NVIC_Init里DMA1_Channel2_IRQn使能,确认中断里调用了DMA_ClearITPendingBit。

5.4 数据错位或偶尔丢包

现象:大部分数据正确,偶尔出现字节错位或整包丢失。

原因:接收处理期间 DMA 通道关闭,新来的数据无人接收。或者发送时没等上一包完成就修改了缓冲区。

解决:接收侧改用双缓冲或环形缓冲,配合 USART 空闲中断;发送侧严格检查usart3_tx_done标志,不要在忙的时候 memcpy 覆盖发送缓冲区。

5.5 波特率偏差导致乱码

现象:数据能收到但全是乱码,或者高波特率下误码率明显。

原因:系统时钟配置和标准库计算的 APB1 频率不一致,导致 USART 分频值算错。

解决:用示波器量一下 TX 引脚的实际波特率,或者打印RCC_GetClocksFreq的结果确认 APB1 频率。F103 在 72MHz 系统时钟下 APB1 是 36MHz,如果外部晶振不是 8MHz,要在system_stm32f10x.c里改HSE_VALUE。

6. 二次开发改造:变长协议与环形缓冲的落地思路

定长收发在实际项目里其实不多见,大部分协议是变长的。比如 Modbus RTU 靠帧间隔断帧,自定义协议靠帧头帧尾。这时候纯 DMA 传输完成中断就不够用了,需要引入USART 空闲中断(IDLE)。

思路是这样的:DMA 配成DMA_Mode_Circular或者保持 Normal 但缓冲区开大,同时打开 USART3 的 IDLE 中断。每收到一帧数据,总线空闲时触发 IDLE 中断,在中断里读取 DMA 剩余传输数量DMA_GetCurrDataCounter(DMA1_Channel3),用「缓冲区总长 - 剩余数量」算出这一帧的实际长度。

void USART3_IRQHandler(void) { if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) { // 清除 IDLE 标志:先读 SR 再读 DR volatile u16 tmp; tmp = USART3->SR; tmp = USART3->DR; (void)tmp; // 计算本帧长度 u16 remain = DMA_GetCurrDataCounter(DMA1_Channel3); u16 recv_len = USART3_RX_BUF_SIZE - remain; if (recv_len > 0) { // 标记本帧有效,交给应用层处理 usart3_rx_len = recv_len; usart3_rx_done = 1; } // 重启 DMA 接收 DMA_Cmd(DMA1_Channel3, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel3, USART3_RX_BUF_SIZE); DMA_Cmd(DMA1_Channel3, ENABLE); } }

这里有个细节:清 IDLE 标志必须「先读 SR 再读 DR」,这是 F103 的硬件要求,只读 SR 清不掉。我一般会用一个volatile临时变量接一下,防止编译器把读操作优化掉。

再进一步,如果数据量大、处理耗时,可以把接收缓冲区做成环形缓冲,DMA 配成循环模式,应用层按读指针取数据。这样 DMA 永远在收,不会因为处理慢而丢包。环形缓冲的读写指针要注意原子性,单生产者(DMA 中断)单消费者(主循环)的场景下,读写指针各自只被一方修改,不需要加锁,但指针类型要用volatile并且保证读写顺序。

改造完之后,验证方法很简单:用串口助手发不同长度的数据帧,看usart3_rx_len是否和实际发送长度一致;再连续快速发多帧,看有没有丢帧。我习惯在ProcessPacket里加一个帧计数器,跑一晚上看计数和发送端是否对得上。

从那以后我每次配 DMA 串口,都强制走一遍「时钟 → GPIO → USART → DMA 通道 → NVIC → 中断向量名」的检查清单,尤其是通道映射和USART_DMACmd这两步,翻车太多次了。希望帮到你。

本文还有配套的精品资源,点击获取

相关新闻

STM32驱动VL53L1X激光测距:从I2C通信到稳定读数

STM32驱动VL53L1X激光测距:从I2C通信到稳定读数

简介:一份基于STM32F103VET6微控制器与VL53L1X激光测距传感器的完整工程包,面向需要实现非接触式距离测量的嵌入式开发者。主控芯片采用ARM Cortex-M3内核,通过I2C总线与VL53L1X通信,PA2/PA3分别配置为SDA/SCL,PA4接XS…

2026/10/20 11:46:43 阅读更多 →
无线传感器网络覆盖优化:MATLAB虚拟力算法与PSO/GA代码实战

无线传感器网络覆盖优化:MATLAB虚拟力算法与PSO/GA代码实战

简介:这份MATLAB代码合集面向无线传感器网络(WSN)覆盖优化方向的学习者与研究者,适合具备一定MATLAB基础、希望快速复现并对比多种智能优化算法效果的高年级本科生、研究生及科研人员。资源围绕WSN节点部署与覆盖增强问题&#xf…

2026/10/20 11:46:43 阅读更多 →
双指针精讲:从移动零到数组原地压缩的通用套路

双指针精讲:从移动零到数组原地压缩的通用套路

LeetCode hot100 里的移动零,我隔一阵子就会刷到一次,每次看都觉得这题简直是把“双指针”三个字浓缩成了一碗汤。题面很朴素:给定一个数组,把所有0移动到末尾,同时保持非零元素的相对顺序,并且不能拷贝额外…

2026/10/20 11:46:43 阅读更多 →

最新新闻

水风光互补调度与净现值计算:MATLAB资源包实战指南

水风光互补调度与净现值计算:MATLAB资源包实战指南

简介:本资源面向计算机、电子信息工程、数学等专业的大学生及新能源调度方向研究者,提供水风光互补调度系统的净现值计算与不同网格装机容量、出力系数影响分析的完整MATLAB实现方案,可用于课程设计、期末大作业与毕业设计。压缩包共18个文件…

2026/10/20 12:26:14 阅读更多 →
情感化无感防刷与手势阻尼反馈:基于用户注意力疲劳度动态微调按钮交互阈值

情感化无感防刷与手势阻尼反馈:基于用户注意力疲劳度动态微调按钮交互阈值

在电商狂欢秒杀、限量潮流球鞋发售或火热赛事抢票的关键瞬间,前端页面承受着来自全球数百万用户的极速疯狂连击。 面对这种狂热的并发点击,传统的防刷防重复提交方案往往充斥着一种“居高临下的冰冷傲慢”: 方案一:粗暴弹窗报错—…

2026/10/20 12:26:14 阅读更多 →
推理引擎底层逻辑与优化实践:从计算图到量化部署

推理引擎底层逻辑与优化实践:从计算图到量化部署

做推理引擎方向有段时间了,经常有人拿着模型部署的报错来问我,聊到最后发现卡点往往不在算子实现上,而是对“推理”这件事本身的底层逻辑没吃透。这篇文章就是想把这些基础的东西掰开揉碎讲清楚,从模型是什么、推理时计算机到底在…

2026/10/20 12:26:14 阅读更多 →
AI智能体架构图绘制指南:四层框架与实战排查

AI智能体架构图绘制指南:四层框架与实战排查

画AI智能体架构图这件事,看着不难,翻车率却很高。前阵子帮一个小团队评审方案,对方贴出一张图:中间一个“LLM”的大框,左边拖进来几个接口,右边连着一个数据库,就敢叫“AI智能体架构图”。我说这…

2026/10/20 12:26:14 阅读更多 →
动效运行时弹簧物理引擎:纯 TypeScript 实现 RK4 龙格-库塔高精度数值积分

动效运行时弹簧物理引擎:纯 TypeScript 实现 RK4 龙格-库塔高精度数值积分

在现代自然交互动效中,传统的基于固定时间持续与三次贝塞尔曲线(Cubic-bezier)的过渡动画,正日益暴露出其致命的机械感与割裂感。 假设一个用户正在用手指拖拽一个浮动卡片:当手指松开时,卡片已经具有了初速…

2026/10/20 12:26:14 阅读更多 →
Flink SQL与DataStream本质差异:从API选择到生产就绪

Flink SQL与DataStream本质差异:从API选择到生产就绪

1. 为什么这节课是Flink学习者真正的分水岭我带过不少刚接触流处理的开发者,他们往往在学完DataStream API后会陷入一种“我已经会了”的错觉——能写个WordCount、能接Kafka、能做简单窗口聚合,就以为掌握了Flink。直到某天被业务方甩来一份需求&#x…

2026/10/20 12:25:14 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →