新闻详情

新闻详情

首页 / 资讯中心 / 详情

STM32 FreeModbus串口接收优化:DMA+空闲中断解决高频轮询丢帧

发布时间:2026/9/28 13:53:56来源:尧图网络
STM32 FreeModbus串口接收优化:DMA+空闲中断解决高频轮询丢帧
1. 为什么在FreeModbus里必须优化串口接收1.1 一个真实的丢帧现场上一篇文章里我按常规流程把FreeModbus从机移植到了STM32F103上串口接收用最简单的方式——每个字节进一次RXNE中断。波特率9600主机用Modbus Poll周期100ms轮询一切正常。后来客户要求把波特率提到115200轮询周期也压到10ms马上露馅了从机偶尔丢一帧CRC错误开始出现严重的时候整个从机卡死必须复位才能恢复。一开始我怀疑是FreeModbus协议栈的问题后来单独用串口助手测试发现从机CPU里面那个接收状态机根本没有收到完整帧。问题就出在“每个字节都进中断”这条路上。波特率115200时一个字节大约87微秒如果主站连续发送8个字节从机CPU要在不到700微秒内连续进出8次中断每次中断里还要跑FreeModbus的接收状态机、刷新3.5T定时器、处理Modbus地址判断。这个工作量放在72MHz的F103上再叠加主循环里的eMBPoll处理很容易出现中断响应不及时导致串口硬件RXNE标志还没处理新字节又到了最终丢字节或者覆盖缓冲区。这个场景非常典型。Modbus从机在RS485总线上理论上是一问一答负载不算高。但实际项目里上位机可能用很短的轮询周期甚至还有广播报文、事件上报数据接收侧一旦成为瓶颈整个系统稳定性就会崩。1.2 逐字节中断的CPU成本和隐性风险很多人觉得串口波特率115200不算高但“不高”指的是传输速率不是CPU中断负载。我们来算一笔账。115200波特率标准Modbus RTU帧格式10位1起始位8数据位1停止位单字节耗时约86.8微秒。也就是说串口每86.8微秒就要拉一次中断。中断服务程序哪怕只做50个周期的工作也要花掉CPU接近30%的处理能力。如果此时还有定时器中断、ADC中断、按键扫描等任务CPU的负担会明显变得紧张。更隐蔽的风险是中断嵌套和临界区。FreeModbus在接收状态机里会用临界区保护共享变量比如进入接收状态时关闭中断、处理完再打开。如果串口中断频繁触发临界区被反复进出其他中断的实时性就会受到挤压。当主站连续发帧、从机又要回复时发送中断和接收中断互相抢占很容易出现某一个字节处理超时导致CRC校验失败。还有一个常见隐患逐字节接收时FreeModbus的3.5T帧间隔定时器必须每个字节都重置。如果某个字节进入中断的时间稍微晚了点定时器超时了协议栈会认为上一帧结束这个字节就成了下一帧的起始字节帧错位随之而来。这类问题在低波特率下不明显但115200甚至更高时时序裕量非常小。1.3 DMA空闲中断如何解决这个问题DMA空闲中断的思路简单说就是让DMA硬件把串口收到的数据自动搬到内存缓冲区CPU完全不用管每个字节当串口总线空闲下来时串口外设会产生一个IDLE中断CPU只需要在这个中断里处理一整批数据即可。这样有两个明显优势。第一CPU不再被每个字节打断而是按“帧”为单位处理中断次数从N次降到1次第二DMA搬运数据不占用CPU周期即使波特率更高数据也不容易丢。对于FreeModbus这种帧长度明确、帧间隔有标准的协议来说DMA空闲中断是天然的绝配。需要说明的是这里的“空闲中断”不是用来替代FreeModbus的3.5T帧间隔定时器而是用来告诉应用层“有一批新数据到了可以取走了”。FreeModbus本身仍然通过定时器判断帧是否结束因为Modbus标准规定帧间间隔必须大于3.5个字符时间这个严格性靠IDLE中断无法完全保证IDLE在1个字符时间左右就会触发。所以我们优化的核心是“字节搬运方式”而不是改变协议判断逻辑。这一点先想清楚后面接入协议栈时思路就不会乱。2. CubeMX配置把硬件基础搭对2.1 USART与DMA的参数配置我用的是STM32F103C8T672MHz主频USART1做Modbus从机串口。CubeMX版本是6.xHAL库版本F1系列1.8.x。先在Pinout标签页把USART1的TX、RX引脚分配好然后进入USART1配置界面Mode选择Asynchronous异步模式波特率按实际需求填我测试时用115200数据位8无校验停止位1Modbus RTU标准使能USART1全局中断关键是DMA设置。在USART1的DMA Settings标签页里Add一个DMA请求Direction选择Peripheral To Memory这就是接收通道。F103上USART1_RX默认映射到DMA1的Channel5不用手动去查映射表CubeMX会自动分配。DMA参数这样设Mode这里有两种选择Normal或Circular后面第3章详细对比先按Normal模式配置Increment AddressPeripheral不变Memory地址递增这个必须开否则每次写入同一个位置Data WidthPeripheral和Memory都选Byte因为Modbus帧是字节流Priority建议High。接收通道是核心数据链路优先级不能比其它杂七杂八的DMA低TX方向的DMA通道看个人需求。如果发送也想用DMA可以再加一个Memory To Peripheral的通道。但我的建议是先只做接收优化发送保持原生的中断发送方式。原因很简单Modbus从机的发送量远小于接收量一次请求才回复几十个字节发送中断的开销完全扛得住。如果发送也上DMA还要额外处理发送完成事件与Modbus状态机的联动风险大于收益。配置完成后生成工程HAL库会帮你把USART和DMA的初始化代码写好。你会看到类似这样的代码static void MX_USART1_UART_Init(void) { huart1.Instance USART1; huart1.Init.BaudRate 115200; huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; huart1.Init.OverSampling UART_OVERSAMPLING_16; }2.2 空闲中断在HAL库中的正确开启方式这是很多新手最容易卡住的地方。CubeMX界面里没有“空闲中断”这个开关串口全局中断即使使能了HAL库默认的HAL_UART_IRQHandler也不会去处理IDLE这个外设事件。你必须自己在代码里手动开启并在中断服务函数里手动判断。开启IDLE中断用这个宏__HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);调用位置可以放在MX_USART1_UART_Init()之后或者FreeModbus的串口初始化函数里只要确保在开始DMA接收之前生效。IDLE中断的处理位置也很讲究。F1系列的USART1_IRQHandler在stm32f1xx_it.c里默认长这样void USART1_IRQHandler(void) { HAL_UART_IRQHandler(huart1); }如果你直接在里面加IDLE判断我的写法是把它放在HAL_UART_IRQHandler调用之前void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE) ! RESET) { __HAL_UART_CLEAR_IDLEFLAG(huart1); /* 在这里处理接收到的数据 */ } HAL_UART_IRQHandler(huart1); }这里要注意一个细节__HAL_UART_CLEAR_IDLEFLAG这个宏在HAL库里实际执行的是读SR寄存器、再读DR寄存器的操作。如果你的IDLE中断处理函数里后续还要读取DMA计数器必须先清标志再读否则DR寄存器被读走后串口外设的状态寄存器会有变化。我测试时先清标志、后取DMA计数值一切正常反过来写顺序偶尔会出现DMA计数错位。2.3 时钟、NVIC和工程模板检查F103的USART1挂载在APB2总线上时钟频率就是72MHz。CubeMX默认时钟配置没问题但如果你改了时钟树一定要确认USART1的时钟源是72MHz而不是36MHz否则波特率计算会偏。HAL库的波特率寄存器计算是基于APB时钟的时钟错了串口数据全是乱码。NVIC优先级方面我的建议是USART1全局中断优先级设为1抢占优先级1子优先级0如果DMA的传输完成中断也用DMA中断优先级可以设为2。这样串口IDLE中断能及时抢占处理保证帧处理及时。FreeModbus的3.5T定时器中断优先级也要考虑如果定时器优先级太低帧间隔超时判断会被延迟可能导致协议栈没有及时结束一帧。我习惯把定时器中断优先级设为和串口中断同级或者略低因为定时器超时属于慢事件晚几个微秒没有实质影响。工程模板上如果你已经用CubeMX生成了freeModbus的移植工程那么串口接收相关的中断处理函数都已经有了雏形。改造之前建议先把原始的prvUARTRxISR中断逻辑完全禁止不能让它和DMA同时从串口读数据否则两边抢数据寄存器必乱。这一点后面第3章会详细说明。3. 裸机下的核心实现DMA接收与协议栈对接3.1 用Normal模式还是Circular模式我给你的选型建议这是设计DMA接收时第一个要做的选择题。Normal模式和Circular模式在CubeMX里只有一个下拉菜单的区别但实际影响很大。Normal模式下DMA从外设搬运指定长度的数据搬够数量后DMA停止需要软件重新启动下一次传输。Circular模式下DMA像一个环形缓冲一样不断搬运数据写满后自动从缓冲区头部重新开始软件只需要管理读写位置。对于Modbus从机这种“一帧一帧”的通信场景我的建议是先上Normal模式。理由有三个。第一Modbus RTU一帧最大256字节地址1字节功能码1字节数据最多252字节CRC 2字节固定长度不会变缓冲区按256字节配就好第二Normal模式每次最多收满256字节就停止不会出现缓冲绕环的问题逻辑清晰第三空闲中断触发后软件把数据取走再重新启动DMA整个过程在中断里几微秒就完成性能完全够。Circular模式更适合数据流不确定、DMA不能停的场景比如持续接收长串数据。它也可以用于Modbus但要自己处理“上一次读到哪里、这一次新数据到哪里”的指针关系代码复杂度明显上升。所以初版实现我选Normal模式。3.2 空闲中断处理函数从硬件标志到有效数据先定义DMA接收的缓冲区和相关状态量#define RX_BUF_SIZE 256 uint8_t ucRxDmaBuf[RX_BUF_SIZE]; volatile uint16_t usRxDmaLen 0;在串口初始化完成后启动第一轮DMA接收HAL_UART_Receive_DMA(huart1, ucRxDmaBuf, RX_BUF_SIZE);这条语句让串口外设把收到的数据自动搬运到ucRxDmaBuf里只有当RX_BUF_SIZE个字节全部收满时DMA才会停止。但我们的Modbus帧通常只有8到几十个字节远远不到256字节所以DMA不会自己停而是等串口总线空闲时触发IDLE中断。IDLE中断处理函数里先清标志然后读取DMA当前还剩多少个字节没搬运void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE) ! RESET) { __HAL_UART_CLEAR_IDLEFLAG(huart1); /* 计算本次接收到的字节数 */ uint16_t usCnt RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); /* 处理这批数据 */ vMBPortSerialProcessRx(ucRxDmaBuf, usCnt); /* 重新启动DMA接收 */ HAL_UART_Receive_DMA(huart1, ucRxDmaBuf, RX_BUF_SIZE); } HAL_UART_IRQHandler(huart1); }这里的关键点在于__HAL_DMA_GET_COUNTER。DMA计数器CNDTR寄存器存的是“还剩多少数据没搬”用缓冲区总长减去剩余值就是本次实际收到的字节数。有一个坑要提醒如果DMA在Normal模式下已经完成了256字节的搬运但空闲中断还没触发比如收到恰好256字节整一帧那么再调用HAL_UART_Receive_DMA会返回HAL_BUSY导致下一次接收无法启动。此时需要在DMA传输完成中断里也处理一次。为了避免这个边界问题可以把缓冲区设成257或者512大于最大帧长这样DMA永远不会自己完成。我测试时用的是512字节缓冲Modbus最大帧256字节安全角余更大。如果你坚持用256字节精确缓冲那还要给DMA的传输完成中断加一个回调函数在HAL_UART_TxCpltCallback里处理数据并重新启动DMA。这个边界情况不难但对新手来说直接用512字节缓冲更省心。3.3 关键一步把这些字节喂给FreeModbus现在DMA已经帮我们把一帧数据完整地收进了ucRxDmaBuf接下来要想办法把这批数据交给FreeModbus协议栈。这一步是优化方案的核心。很多人的做法是绕过FreeModbus接收状态机直接往协议栈的内部缓冲区塞数据。但FreeModbus的接收状态机内部变量ucRTUBuf、usRxBufferCount等是静态的外部无法访问。真正干净的接入方式是继续调用FreeModbus提供的字节接收回调只是把“每个串口中断字节”改成“DMA空闲中断批量喂字节”。具体来说FreeModbus在portserial.c中原本是这样接收一个字节的static void prvUARTRxISR(void) { UCHAR ucByte; if (xMBPortSerialGetByte((CHAR *)ucByte)) { pxMBFrameCBByteReceived(); } }pxMBFrameCBByteReceived是一个函数指针RTU模式下指向eMBRTUReceiveFSM也就是FreeModbus的接收状态机。这个状态机内部会调用xMBPortSerialGetByte来取字节。注意这个xMBPortSerialGetByte是我们自己在portserial.c里实现的它不一定非要从串口硬件寄存器取数据也可以从一个软件变量里取。所以我们的小技巧就是事先把DMA缓冲里的某一个字节放到一个全局变量里然后调用pxMBFrameCBByteReceived()状态机内部通过xMBPortSerialGetByte读到这个全局变量这样就完成了一个字节的“投喂”。实现起来很简单。在portserial.c里加一个全局变量static uint8_t ucRxFeedByte; BOOL xMBPortSerialGetByte(CHAR *pucByte) { *pucByte (CHAR)ucRxFeedByte; return TRUE; }然后写一个批量处理函数void vMBPortSerialProcessRx(uint8_t *pucBuf, uint16_t usLen) { uint16_t i; for (i 0; i usLen; i) { ucRxFeedByte pucBuf[i]; pxMBFrameCBByteReceived(); } }在IDLE中断里调用vMBPortSerialProcessRx(ucRxDmaBuf, usCnt)DMA收进来的一整帧数据就逐个字节喂给FreeModbus了。协议栈内部会正常做地址匹配、CRC校验、帧结束判断逻辑和标准实现完全一致只是数据来源从串口寄存器变成了DMA缓冲区。这个方案的好处是完全不需要修改FreeModbus的状态机源码只在portserial.c端口层做适配后续升级协议栈版本也不会有兼容性问题。还要注意一点vMBPortSerialEnable这个函数是FreeModbus控制串口收发使能的。原来的实现里接收使能时开启接收中断接收禁用时关闭接收中断。改成DMA方式后不需要再开关RXNE中断了但函数还是要保留只是内部要把“开中断”改成“启动DMA”把“关中断”改成“停止DMA”。我的实现是这样void vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable) { if (xRxEnable) { HAL_UART_Receive_DMA(huart1, ucRxDmaBuf, RX_BUF_SIZE); } else { HAL_UART_DMAStop(huart1); } }这里有一个细节vMBPortSerialEnable可能会被协议栈在发送前调用这时把接收DMA停掉是合理的防止发送数据通过RS485回环到接收引脚被误判成接收数据。发送完成后再调用vMBPortSerialEnable(TRUE, FALSE)重新启动DMA接收。3.4 485方向控制与发送侧的联动用RS485时还要处理方向控制引脚DE/RE。一般把某个GPIO接到485芯片的DE和RE上发送时置高接收时置低。在FreeModbus里这个切换通常放在vMBPortSerialEnable里。我的做法是#define RS485_DIR_GPIO_PORT GPIOB #define RS485_DIR_PIN GPIO_PIN_12 void vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable) { if (xTxEnable) { HAL_GPIO_WritePin(RS485_DIR_GPIO_PORT, RS485_DIR_PIN, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(RS485_DIR_GPIO_PORT, RS485_DIR_PIN, GPIO_PIN_RESET); } if (xRxEnable) { HAL_UART_Receive_DMA(huart1, ucRxDmaBuf, RX_BUF_SIZE); } }需要特别注意的是发送完成后再切回接收这个时机不能太早。HAL库的HAL_UART_Transmit是一个阻塞函数等它返回时最后一个字节的停止位可能还没完全发完。如果此时立刻把485方向切回接收最后一个字节会被截断。解决办法是在发送函数后面加一个短延时或者使用发送完成中断来切换方向。实际项目中我用的是HAL_UART_Transmit加几十微秒延时考虑到115200波特率下1个字节约87微秒延时10微秒到20微秒就够了。更稳妥的做法是在HAL_UART_TxCpltCallback里切换方向。对于DMA接收方案发送侧保持阻塞式发送还有一个好处裸机环境下FreeModbus的eMBPoll在发送时本来就要求数据快速发完阻塞发送几十微秒对系统整体影响很小。用了DMA发送反而要处理发送完成事件和eMBState状态机的联动复杂度上去了收益却不大除非你的从机要频繁上报大量数据。4. 调试实录与常见问题速查4.1 高频轮询下的稳定性验证我在把代码改完后用Modbus Poll做了一轮压力测试。测试条件是这样F103从机115200波特率Modbus Poll轮询周期从100ms一路压到10ms功能码03读保持寄存器每帧请求8字节从机响应7字节。连续跑了一个小时统计结果总请求次数大约36万次从机响应正常没有出现超时、CRC错误或从机卡死的现象。对比优化前同样条件下跑10分钟就开始零星丢帧说明DMA空闲中断的改动确实把接收链路的瓶颈解掉了。为了再压一压我用两个USB转串口模块对发数据一个发一个收连续跑大半天数据对比没有发现丢字节。当然这属于纯粹的串口压力测试不涉及Modbus协议栈。测试过程中有一个现象值得说如果把IDLE中断里的处理代码写得过长比如在里面做CRC校验、打印日志、甚至调用HAL_Delay那整个系统会卡死。原因很简单中断处理函数里执行耗时操作等于把后面的字节接收阻塞了DMA虽然在搬运但这个IDLE中断还没退出下一帧到了也只能等。所以我的经验是IDLE中断里只做计数读取、数据搬运和FreeModbus状态机喂字节其余工作全部放到主循环的eMBPoll里。4.2 现象排查收不到、乱码、粘帧、偶发超时实际调试中我踩过不少坑整理成一张速查表按现象排查效率很高。现象可能原因排查与解决DMA启动后串口收不到任何数据没有调用HAL_UART_Receive_DMA初始化后检查是否启动DMA接收DMA启动后一直触发IDLE中断IDLE标志没有清除或UART没有数据但总处于空闲状态确认清标志语句在中断入口执行收到数据全是乱码波特率不匹配或时钟配置错误核对APB时钟和波特率寄存器偶尔丢失第一个字节重新启动DMA前串口数据已经到达启动DMA前先清Ovr标志或改用Circular模式粘帧两帧数据合成一帧IDLE中断处理不及时或主站帧间隔小于3.5T优化中断处理耗时检查主站时序FreeModbus一直不回复喂字节函数没有调用pxMBFrameCBByteReceived在批量处理函数中打断点确认调用链某个地址的从机全部无响应地址匹配错误或CRC校验没生效检查Modbus从机地址配置和CRC使能还有一个常见问题DMA计数器读出来的字节数总是0或者是一个很大的值。这个多半是DMA中断优先级和串口优先级配置不合理导致在IDLE中断触发时DMA搬运还没完成。实际上IDLE中断和DMA搬运之间是有时间差的IDLE标志置位时最后一个字节可能还在DMA搬移的路上。解决方法是读计数器前加一个短暂的等待循环或者把DMA优先级调高。我在F103上实测DMA优先级High串口优先级1不需要额外等待就能正确读取。4.3 性能数据与优化边界我做了一个简单的对比测试在同一块F103上分别跑原生逐字节中断接收和DMA空闲中断接收测量每帧数据8字节的CPU中断处理耗时。用的方法是直接在GPIO翻转做示波器测量。结果如下方案每帧中断次数中断处理总耗时备注原生RXNE中断8次约220微秒每次中断约25-30微秒DMA空闲中断Normal1次IDLE约25微秒含数据搬运和状态机喂字节DMA空闲中断Circular1次IDLE约25微秒连续流场景更稳这个数据说明8字节的Modbus帧DMA方案把中断处理耗时压缩到原来的十分之一左右。如果你是看门狗喂狗、跑一些实时控制算法多出来的CPU时间相当可观。优化边界也要讲清楚。DMA空闲中断不适合所有场景。如果串口数据流是连续不断的比如每秒上千帧Modbus请求那么IDLE中断还是会频繁触发中断次数并没有减少太多。这种情况更合理的做法是上RTOS信号量高优先级任务处理裸机方案能优化的范围有限。另外DMA缓冲区如果太小或者主站发送超长数据帧DMA可能在IDLE触发前就满了这时候需要考虑缓冲区加长或者改用Circular模式。4.4 关于FreeModbus定时器配置的一个提醒第三个坑是FreeModbus的3.5T定时器。前面说过DMA空闲中断只是提高了字节搬运效率帧结束判断仍然靠FreeModbus内部的3.5T定时器。这个定时器在porttimer.c里配置周期要严格按3.5字符时间计算。公式是3.5个字符时间 3.5 * 11位 / 波特率。115200波特率下约334微秒9600波特率下约4.01毫秒。如果你用HAL库的定时器中断做时基要注意定时器的预分频值和自动重载寄存器别配错。我一开始图省事直接把F103的定时器配置成1ms周期中断然后调用vMBPortTimersT35Expired结果在115200波特率下1ms的定时周期和334微秒的理论值差了3倍导致协议栈把正常的一帧拆成多次经常出现CRC错误。正确做法是让定时器周期精确匹配3.5T时间。比如用TIM372MHz时钟预分频72则计数频率1MHz一个计数单位1微秒。115200波特率下自动重载值设为3349600波特率下设为4010。如果你在CubeMX里配置定时器这些参数可以直接填到TIM的Period字段里。还有一点vMBPortTimersEnable和vMBPortTimersDisable这两个函数也要正确实现。FreeModbus会在接收状态机重置定时器时调用它们。裸机环境下最简单的实现是把定时器中断打开或关闭或者用一个自增计数器标志在主循环里轮询判断是否超时。我自己用的是中断方式定时器溢出中断里调用vMBPortTimersT35Expired然后清标志重新计时。最后再分享一个细节在IDLE中断里调用pxMBFrameCBByteReceived喂字节时FreeModbus的状态机会在每一批字节的最后一个字节处启动3.5T定时器。如果主站发送的下一帧请求间隔小于3.5T比如某些上位机优化得太激进协议栈会认为这是同一帧的数据从而解析失败。这种问题不是DMA方案引入的原生逐字节接收也一样会出问题。解决办法是确保主站轮询周期大于从机的处理时间加上3.5T间隔一般建议至少留出10ms以上的余量。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

多卡微调 ChatGLM 实战:Deepspeed ZeRO 优化与四卡并行训练指南 2026/9/28 14:50:11

多卡微调 ChatGLM 实战:Deepspeed ZeRO 优化与四卡并行训练指南

简介:本资源面向希望上手大模型微调的开发者与研究者,聚焦用Deepspeed实现ChatGLM的多卡并行训练,解决单卡显存不足、训练效率低、微调流程不清晰等实际问题,适合具备Python与PyTorch基础、想进阶多卡训练的中级学习者。压缩包共1…

阅读更多 →
ChatGLM多卡微调实战:Deepspeed ZeRO显存优化与避坑指南 2026/9/28 14:50:11

ChatGLM多卡微调实战:Deepspeed ZeRO显存优化与避坑指南

简介:本资源面向希望上手大模型微调的开发者与研究者,聚焦用Deepspeed实现ChatGLM多卡并行训练这一实战场景,帮助跨过环境配置与分布式训练的技术门槛。压缩包共17个文件,以11个Python脚本为核心,覆盖模型加载、数据加…

阅读更多 →
Altium Designer ROOM模块复用实战指南 2026/9/28 14:50:11

Altium Designer ROOM模块复用实战指南

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

阅读更多 →
基于YOLOv5与OpenPose的摔倒检测系统:双阶段姿态估计实战解析 2026/9/28 14:50:04

基于YOLOv5与OpenPose的摔倒检测系统:双阶段姿态估计实战解析

简介:这是一份基于YOLOv5与OpenPose的摔倒检测完整项目,面向具备一定深度学习基础、希望快速入手人体姿态识别与异常行为检测的开发者。项目中,YOLO负责行人定位,OpenPose负责关键点提取,两者结合后可对跌倒动作进行判…

阅读更多 →
STM32 DMA+空闲中断完美适配FreeModbus从机串口接收 2026/9/28 14:50:04

STM32 DMA+空闲中断完美适配FreeModbus从机串口接收

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

阅读更多 →
小米Mix2S刷入EDK2 UEFI固件:骁龙845设备引导Linux与Windows on ARM实战 2026/9/28 14:50:04

小米Mix2S刷入EDK2 UEFI固件:骁龙845设备引导Linux与Windows on ARM实战

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

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉