LK 博客
第二讲:USART2 + PB8(DE/RE) + TIM6是怎样组成 RS485 驱动层的
项目
约 1 分钟阅读 0 赞 0 条评论 鸿蒙黑体

第二讲:USART2 + PB8(DE/RE) + TIM6是怎样组成 RS485 驱动层的

Yukikaze
Yukikaze @Yukikaze
累计点赞 0 登录后每个账号只能点一次
内容长度 0 正文词元数
正文
目录会跟随阅读位置移动。
阅读进度

第二讲:USART2 + PB8(DE/RE) + TIM6是怎样组成 RS485 驱动层的

项目地址(对应分支)

仓库主页:
https://github.com/AIITVisionLab/shihu

本讲对应分支:
https://github.com/AIITVisionLab/shihu/tree/feat/stm32f429-modbus-gateway

本讲聚焦的内容

mcu/bsp/rs485
mcu/middleware/Modbus-RTU
docs/architecture.md
docs/modbus-register-map.md

一、为什么从“底层入口”讲起

如果说之前解决的是“Modbus-RTU 是什么”“为什么实现必须分层”, 那么从这一讲开始,就要真正进入 F429 这条链路最底下的那一层:RS485 驱动入口。

原因非常简单。

对于一个 Modbus-RTU 主站来说,真正的起点不是功能码,不是寄存器地址,甚至也不是 CRC, 而是下面这几个最现实、最硬的底层问题:

  • 字节到底通过哪个串口发出去、收进来;
  • 总线当前是发送态还是接收态;
  • 一帧什么时候才算真正结束;
  • 底层中断到底该负责什么,不该负责什么;
  • 上层中间件应该从底层拿到哪些“事件”,而不是去猜底层状态。

如果这些问题没有先站稳,那么后面的主站事务、中间件状态机、寄存器轮询,都会建立在一层很脆弱的地基上。

而在当前这个分支里,这层地基已经很明确了:

  • 串口:USART2
  • 方向控制:PB8(DE/RE)
  • 帧间计时:TIM6
  • 运行模式:9600 8E1
  • 总线形式:RS485半双工
  • 驱动原则:ISR 只做收发字节与 T3.5到期通知,不在中断里跑协议状态机

这几项拼在一起,才构成了 F429 采集网关真正的 Modbus-RTU 底层入口。

二、现实约束

很多人看代码时会下意识觉得:

“无非就是一个串口,再加一个方向控制脚,再加一个定时器。”

但如果真这么理解,就会低估这层的重要性。 因为它并不是三个孤立的外设,而是围绕 半双工 RTU 总线 这个现实约束,组成的一整套协同行为。

三、USART2:字节流入口,不是协议入口

1. 串口负责的是“字节”,不是“Modbus”

基本原则:

UART 只负责字节流,不负责协议语义。

也就是说,在 USART2 看来:

  • 它不知道你发的是读光照还是写 PLC;
  • 它不知道这是不是一个异常响应;
  • 它也不知道当前轮询到了第几个从站。

对它来说,事情只有两类:

  • 一个字节被发出去了;
  • 一个字节被收进来了。

这听起来很基础,但正因为如此,驱动层必须克制。 否则一旦 UART 层开始承担“协议含义”,分层马上就会塌。

2. 为什么当前选 USART2

在当前这套板级方案里,USART2 不是一个抽象概念,而是已经和具体口线绑定:

  • PD5 / PD6
  • PB8(DE/RE)

这说明当前实现是贴着板级资源来的。 这没有问题,实际工程本来就必须先落到具体硬件上。

但这也意味着后人接手时要明白一件事:

当前 RS485 驱动层的第一层“固定条件”,就是它默认跑在 USART2 上。

后面如果迁移板子、换外设资源、或者总线数量增加,这一层会是最先需要抽象的地方之一。

3. 为什么串口格式是 9600 8E1

当前主站参数里已经明确写了:

  • 9600
  • 8E1

也就是:

  • 波特率 9600
  • 8 位数据位
  • Even parity 偶校验
  • 1 位停止位

这不是随手填的,而是 Modbus-RTU 在当前现场设备之间的共同约定。 只要总线上任意一方串口格式不一致,后果通常不是“值有点不对”,而是整帧都无法稳定解码。

所以在驱动层看来,USART2 初始化时最重要的不是“把串口打开”,而是:

确保总线上的所有设备都在同一个字节格式假设里说话。

换句话说,9600 8E1 在这里不只是配置项,它是整条 RTU 总线的语言环境。

四、PB8(DE/RE):为什么方向控制是 RS485 的核心,而不是附属细节

1. RS485 的最大现实约束:半双工

在二线 RS485 总线上,一个最基本的事实就是:

同一时刻,总线上只能有一个方向在说话。

这就是半双工。

所以,和普通 UART 最大的不同在于:

  • 不是“想发就发”;
  • 也不是“发完就自然继续收”;
  • 而是必须明确控制总线收发方向。

这就是 DE/RE 存在的意义。

2. DE/RE 解决的不是“能不能发”,而是“谁在占总线”

很多初学者看到 DE/RE,会把它理解成“串口使能脚”。 这个理解太浅了。

更准确地说:

  • DE 决定驱动器是否把 MCU 发送的数据真正推到总线上;
  • RE 决定接收器是否处于接收状态;
  • 它们合起来,决定当前这块板子是在“说”还是在“听”。

所以,PB8(DE/RE) 在当前系统里的本质作用不是 GPIO 控制,而是:

主站对总线占用权的控制开关。

3. 一个完整请求为什么必须围绕 DE/RE 切换组织

在当前这套主站里,一次最完整的请求流程,从最底层看,通常都是这样的:

  • 确认总线空闲;
  • 切换到发送态;
  • 逐字节发出请求帧;
  • 等最后一个字节真正移位完成;
  • 切回接收态;
  • 开始等待从站响应;
  • 进入帧接收与 T3.5 判定流程。

注意这里最容易被写错的一点:

“发送完成”不是指软件把最后一个字节写进发送寄存器,而是指最后一个字节真的已经离开 UART,离开收发器,送上总线。

如果方向切换太早,最后几个字节可能会被截断; 如果方向切换太晚,从站的响应起始部分可能会被主站自己挡掉。

所以,DE/RE 控制不是一个附带动作,而是 RS485 驱动层最关键的状态切换点之一。

4. 为什么这一层不能交给协议层“顺手处理”

如果让协议层自己到处拉高/拉低方向控制脚,看起来好像省掉了一层封装, 实际上会带来几个严重问题:

  • 协议层被迫知道硬件方向时序;
  • 后面更换收发器或更换控制策略时,协议层会被牵连;
  • 串口发送完成与移位完成的边界容易被写错;
  • 协议状态机和物理层时序会被强耦合。

所以更合理的方式应该是:

方向控制属于 RS485 驱动层的职责,不应该散落在上层事务代码里。

五、TIM6:为什么 Modbus-RTU 一定要有“帧间静默时间”建模

1. RTU 不是靠固定帧头判帧

很多私有串口协议会用固定帧头,比如 0x55AA、0xA5 0x5A 来判定一帧开始。 Modbus-RTU 不是这样。

它的一帧边界,依赖的是:

  • 字节流;
  • CRC;
  • 以及最关键的——静默时间。

这意味着你不能只靠“收到了多少字节”来判断一帧是否结束。 你必须再问一个问题:

这些字节之间的时间关系,是否已经满足 RTU 规定的帧结束条件?

这就是 T3.5 存在的意义。

2. 为什么一定要把 T3.5 单独建模出来

在当前工程里,TIM6 被专门拿来做这件事。 这说明设计不是“收完了就算一帧”,而是明确承认:

RTU 的帧边界,本身就是协议的一部分。

如果没有独立的 T3.5 建模,常见问题会很多:

  • 收到半帧就提前解析;
  • 前一帧和后一帧黏在一起;
  • 噪声字节导致帧缓存污染;
  • 上层只能靠长度猜测帧是否完整;
  • 总线上多个节点响应时,边界混乱。

所以,TIM6 在这里不是“正好有个空闲定时器”,而是:

RTU 帧边界的时序裁判。

3. TIM6 在这套系统里的职责

它最核心的职责其实只有一个:

当接收过程中出现足够长的静默时间时,通知上层:这一帧可以尝试收口了。

注意,是“通知可以尝试收口”,而不是“在定时器中断里直接完成协议解析”。

这点非常重要。

因为 TIM6 中断本质上只是一个底层事件:

  • 静默时间到了;
  • 接收窗口大概率已经结束;
  • 可以把当前缓存交给链路层进一步判断。

它不应该承担:

  • CRC 计算;
  • 功能码解析;
  • 数据区拆解;
  • 主站事务流转;
  • 业务快照更新。

这些都已经越层了。

六、为什么我坚持:ISR 只做最短动作,不在中断里跑状态机

当前工程文档里,已经把这条原则写得很明确:

  • ISR 只做收字节、发字节、T3.5 到期通知;
  • 协议状态机不放到中断里执行。

这是这套系统的核心边界之一。

1. 中断里应该留下哪些动作

对当前 RS485 驱动层来说,中断里真正合理的动作只有这些:

(1)串口接收中断

  • 收到一个字节;
  • 把这个字节放进接收缓存;
  • 必要时刷新帧间定时逻辑;
  • 然后立刻返回。

(2)串口发送相关中断

  • 当前字节已经进入发送流程;
  • 如果发送缓冲区还有下一个字节,就继续装载;
  • 如果最后一个字节真正完成,则上报“发送完成”事件;
  • 然后立刻返回。

(3)TIM6 中断

  • 说明当前已经满足 T3.5 条件;
  • 上报“接收帧结束候选”事件;
  • 然后立刻返回。

(4)硬件错误中断

  • 记录底层错误事实;
  • 上报给上层;
  • 然后立刻返回。

这些动作有一个共同特点:

它们只报告事实,不解释语义。

2. 为什么不在中断里直接跑协议状态机

因为一旦这么做,问题会立刻变复杂。

(1)执行时间不可控

中断本来应该尽快退出。 如果里面开始 CRC、开始解功能码、开始判断异常响应,响应时间会迅速拉长。

(2)分层会塌

ISR 本来只该知道“我收到了字节”“定时器到了”。 如果它开始知道“这是温度寄存器响应”,那说明链路层、协议层、应用层已经缠在一起了。

(3)调试会非常困难

当 ISR、协议解析、主站事务、业务轮询混在一层时, 出了问题以后你很难判断到底是:

  • 硬件收发错了;
  • 帧边界判错了;
  • CRC 算错了;
  • 协议解析错了;
  • 还是业务调度本身有问题。

所以,这里真正应该坚持的不是“中断越少越好”,而是:

中断只负责产出底层事件,不负责解释这些事件的高层意义。

七、底层驱动到底应该向上层提供什么“事件”

这是后人接手时非常关键的一个问题。 因为驱动层不是为了“把外设初始化好”而存在的,而是为了向上提供一个稳定、可被中间件消费的事件接口。

对于当前这套系统,我认为最合理的底层事件至少应该包括:

1. TX_BEGIN

表示驱动已经正式切到发送态,准备占用总线。

它的意义在于让上层知道: 当前总线方向已经切换完成,请求发送流程已经开始。

2. TX_DONE

表示最后一个字节已经真正发送完成,可以安全切回接收态。

这个事件特别关键。 因为它不是“软件已经写完发送缓冲区”,而是“硬件层面真正发完了”。

3. RX_BYTE

表示驱动层收到了一个新字节,并已经进入接收缓存。

这个事件本身通常不一定需要完整上抛给任务层, 但在驱动内部,它一定是帧接收逻辑的基本输入。

4. RX_FRAME_TIMEOUT 或等效事件

表示 T3.5 已到,可以认为当前接收帧已经结束,交给链路层收口。

这个事件是 RTU 帧边界的关键。

5. PORT_ERROR

表示发生底层硬件错误。

例如:

  • 串口硬件异常;
  • 接收异常;
  • 缓冲区越界;
  • 明显不合法的底层状态切换。

6. BUS_IDLE

表示当前总线已经处于可启动新事务的空闲态。

这个事件不是每套实现都必须显式化, 但在主站事务层设计清晰的时候,它会非常有帮助。

八、从底层事件到中间件通知,边界应该怎么划分

驱动层最大的职责,不是让上层“知道得更多”,而是让上层“猜得更少”。

也就是说,底层驱动不应该要求中间件去猜:

  • 现在是不是已经发完了;
  • 这一帧是不是可能已经结束了;
  • 当前总线是不是已经切回接收态了。

更好的做法应该是:

驱动层把有限、清晰、可验证的底层事件明确抛出来;

中间件只基于这些事件推进链路状态机。

于是边界就会非常清楚:

RS485 驱动层负责:

  • 管 UART;
  • 管方向控制;
  • 管 T3.5 计时;
  • 管底层硬件错误;
  • 产出底层事件。

RTU 链路层负责:

  • 管帧缓存;
  • 管帧结束收口;
  • 管 CRC 检查;
  • 决定是否把当前帧交给协议层。

协议层负责:

  • 判断地址、功能码、异常响应、数据区合法性。

主站事务层负责:

  • 发请求;
  • 等结果;
  • 超时与重试;
  • 失败分类。

应用层负责:

  • 决定轮询谁、何时写命令、如何生成快照并上报。

这样一来,每层都不用替别层做事。

九、总结

很多人会把 RS485 驱动层看成一个相对边缘的“底层细节”, 但在我看来,它其实决定的是整条总线最初的秩序。

因为从这里开始,系统已经明确了:

  • 字节怎样进出总线;
  • 方向怎样切换;
  • 一帧怎样结束;
  • 中断应该做到哪一步为止;
  • 上层应该接收哪些清晰事件,而不是去猜底层状态。

这意味着,这一层并不只是“把串口点亮”,而是在为上面的 RTU 链路层、协议层、事务层、轮询层打地基。

所以这一讲真正想留下来的结论只有一句:

不要把 RS485 驱动层当成附属实现; 它是整条 Modbus-RTU 主站链路的入口秩序。

只要这层入口秩序清楚,后面的中间件和主站事务就能稳定长出来。 如果这层入口秩序混乱,后面无论协议写得多漂亮,都会建立在不稳的基础上。

作者名片

Yukikaze
Yukikaze
@Yukikaze

私にできることなら何でもするから

评论区
文章作者和管理员都可以管理这里的评论。
0 条评论
登录后即可参与评论。 去登录
还没有评论,欢迎留下第一条交流内容。