当前位置: 首页 > news >正文

STM32F103串口非阻塞收发

预期效果:波特率9600bps,PC发送200字节/100ms,发送一万字节,MCU原封不动发回来。

接收

肯定要用DMA啦。DMA用循环模式,就是说把接收缓冲当成一个队列,满了或者到了一半,就更新一下队列的头尾指针。还有串口的空闲事件,也要用上,外面来的数据要是停了能及时响应处理。
image

缓冲区长一点比较好,因为太短的话外来数据老是覆盖掉不久前的内容,长一点的话能给足你复制数据的时间。

读数据的话,就用类似uint16_t read_uart(uint8_t *buf, uint16_t len)的原型就行。本来想着加一个参数uint16_t timeout_ticks的:要是len参数太大,缓冲区内容不够就让它先等等。后来一想需要原地硬等,就又变成了阻塞式,不合适。所以还是立即返回比较好,实际拿到多少数据就返回多少。

具体讲呢,主要是两个地方。一个是上面的那个函数,另一个就是在void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)里面,判断一下这个事件的类型。

这个事件类型是个枚举值。这里只需要更新这个队列的尾指针rxbuf_wp就行。除此之外还有个标志,给外部用的。我现在的做法是主循环检查它,决定要不要读。也可以把它塞进read_uart函数里面,当时候没数据就等一会儿,给read_uart一个参数来决定要不要它等待。

现在先这样吧,以后上RTOS了直接用同步工具了,信号量什么的。当然也可以延时等待,毕竟任务调度还是能保证把时间分给别的任务的。这里就是裸机,学习一下这种机制就行,没必要那么深入。整一个全局标志,你写我读,没啥毛病。
image

这里就直接更新队列指针,不复制了,等到啥时候发现有数据了,has_data不为零了,就调用read_uart,那时候咱再复制。DMA队列是循环的,指针到末尾了别忘了回绕回来。
image
函数里面就是先看队列里面有多少数据了,看看够不够取。不够取的话能取多少取多少,就是变量【J】。然后就是复制数据出来到参数buf指向的内存区里。

这儿分两种情况,就是队列数据区是一整块儿【rp..wp】,还是【0..wp】一块【rp..END】一块。如果是前者,那就一次复制就完事儿了。如果后者,又分情况,右边那一块儿地方数据多不多,够不够取。要是不够取,复制完【rp..END】还得接着复制【0..N】才行。反正变量【J】检查过,队列里总数据量肯定是够的。要是【rp..END】够取,那和【rp..wp】这种情况是一模一样的操作。

复制完数据之后,更新一下读指针和标志,返回【J】的值。

有一个问题:这函数执行一半,那边DMA往里头扔数据咋办?我读的rxbuf_wp就失效了?

这确实是个问题。本来想用__disable_irq,觉得没啥必要,只要原子操作就行。后来又觉得写__DSB,DMB内存屏障宏会好一点。再后来终于明白,因为我测试用的stm32f103c8t6属于黑铁段位,这个芯片串行执行指令,读写指令相当于原子操作,根本不配用内存屏障。最后怕编译器打乱顺序,直接无脑加 volatile 就完事儿了,也不想搞那么复杂。

重点就是缓冲区得稍微大一点。不能说这边正复制数据呢,那边儿DMA把我数据给覆盖了。

这方法就这样儿,没办法。DMA自己要用一个缓冲区,你为了非阻塞,还是不定长数据,而且更离谱的是上层还想着要能流式处理。还能咋样呢?裸机又没有硬件FIFO,就只能牺牲内存了呗。

简直拉完了

你算算,dma底层发送得要缓存吧,接收得要缓存吧。你读取可以直接从DMA缓存区复制,只要人家不给你覆盖。但是发送的时候为了避免两次挨得太近HAL库函数给你返回BUSY忙状态,得再要一个队列吧。然后read_uart的参数,是不是又是一个缓冲区?一共三个缓存区,你的队列还得保证一次至少能放得下两帧。

有这功夫,我为啥不直接一个1KB缓存,用HAL_UARTEx_ReceiveToIdle()呢?我就赌你一次发不了那么多,赌你连续发送连一个IDLE frame都没有。最多用户缓冲再占1KB。BUG又少效率又高。既然让内存硬扛,那不如一开始就贯彻到底。

发送

ST给的HAL库函数能DMA发送。但是两次发送挨得太近的话第二次会返回HAL_BUSY,因为第一次还没完成。也就是说,你得自己维护一个队列,每次DMA发送完了,让它自己到里面去取一块数据发出去。前面讲过了,得要一个队列。无语的是这队列不能小了。太小你上层write_uart老是堵住,执行不下去,因为队列给写满了,DMA还在忙。

不管怎么样吧,就这个DMA加队列的思路,简单实现一下。

image

那里有一个等待超时的for循环,就是现在txbuf满了,发不了,停一下,再重新检查一下。其实可以让【rp】和【txbuf_rp】比较,就看它俩相不相同。变量【txbuf_rp】一旦动了,说明dma从这儿把数据取走,向外发送去了。这样就有空闲空间了,接下来就可以自由发挥了。比如设置一个阈值,规定dma给我腾出来至少64字节我write_uart才愿意继续跑。不然老在这里等你dma,成何体统?

里面那个函数uart_start_send(),它就是把队列里的数据复制出来,排好序,送DMA挨个儿发出去,算是一线干活的。
image

为啥要复制呢,就是把数据从队列里领出来,拼成一个连续的缓存里。这样DMA发送的时候既不会打乱顺序又不会占用原来write_uart用的txbuf。后续write_uart调用能直接往txbuf里面扔新数据,因为上一次的数据被复制到dma的txbuf2里面了。道理就是这么个道理,反正复制的时候注意是复制一块连续内存还是头尾两块儿就行了。

最后还有一个void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)。就看看txbuf里面还有没有东西能发的。要是有就继续干,没有就把忙状态清零告诉别人我dma干完活了。

下次执行write_uart时候,write_uart里面会重新调一次uart_start_send,dma又开始干活。别的地方不需要用这个标志。
image

其实这个判断【rp】和【wp】为真之后可以直接把他俩清零,回到初始状态。反正这个时候队列里要发的东西dma都发完了。不过既然是循环队列,那就无所谓啦。

效果

100ms定时发送,一次发190字节,也不算很快吧。主要是测试一下正确不正确,丢数据不丢。

image

一边发一边收,效果还行吧。就是太占内存,拉完了

http://www.zskr.cn/news/1409192.html

相关文章:

  • 2026年最新:论文AI率从60%降至5%实测,10款降AI工具与手改技巧指南 - 降AI实验室
  • 《B4450 [GESP202512 三级] 小杨的智慧购物》
  • 消费类平台“四边商业模型”:激活县域经济增长的新范式
  • PL2303老芯片驱动终极解决方案:3步让Windows 10/11完美识别串口设备
  • 用ESP32C3和PCM5102A做个高音质小DAC:手把手教你焊接、配置I2S,告别底噪
  • 2026年5月更新:宜兴有名的硝化菌公司深度剖析,聚焦宜兴橡树 - 2026年企业资讯
  • 护眼台灯哪个牌子的性价比高?家长公认性价比护眼灯品牌,不踩雷
  • 古典舞在线交流平台的设计与实现(源码+论文)
  • 不用第三方软件!修改注册表开启电脑任务栏秒数显示,附详细步骤
  • 锻炼学龄前孩子自理能力,养成独立生活习惯
  • 2026年 宝钢HC550/980DP双相钢/吉帕钢推荐榜单:超高强度与冷弯性能俱佳,冲压成形解决方案优选! - 品牌企业推荐师(官方)
  • 如何与Android共享 iPhone 相册?
  • LLM推理服务中的Block调度器设计与优化实践
  • 儿童护眼灯哪个最好?盘点儿童护眼灯年度人气爆款,回头客超多
  • 评分生成模型在ISAC性能评估中的创新应用
  • 光伏行业从业者:如何快速高效出一份专业的光伏可研报告?
  • 中国财务领域的 OpenClaw已经落地3年!——管理层最关心的3大资金难题,KBOT 给出终极解决方案
  • OPD 一人部门适合哪些岗位?全行业大盘点
  • 律师正在悄悄使用的ChatGPT法律起草模板库(含保密协议/股权转让条款/管辖权异议申请书)
  • LP3798ESM 原理图+变压器参数全公开:24W PSR 方案可直接拿去打样
  • Carla地图导入后别忘了这一步:手动生成与修正行人导航.bin文件详解
  • AI赋能PPT制作:告别低效设计,开启智能办公新时代
  • 工业视觉实战 | WPF + Halcon/OpenCvSharp 的锂电池极耳视觉检测系统
  • Agent Framework 自我改进模式 构建 Agent 自优化闭环
  • 免费下载B站大会员4K视频:bilibili-downloader完全指南
  • CANN ops-transformer:AllReduce 与 AllGather 在分布式推理中的选型
  • 小米MiMo降价是要干嘛?
  • 极致沉浸感官体验,超元力重新定义VR枪战竞技新玩法
  • AI赋能医疗影像:重塑精准诊疗新范式
  • 酒店门锁V10SDK接口VB-幽冥大陆(一百26)—东方仙盟