UML之时序图学习

UML 时序图完全指南(嵌入式工程实践版)

目录

  1. 时序图的本质与作用
  2. 时序图的核心元素详解
  3. 三个关键问题:谁先?谁后?调用顺序?
  4. 时序图 vs 其他 UML 图
  5. 嵌入式系统中的时序图应用
  6. 完整实战案例(7个)
  7. PlantUML 绘制指南

一、时序图的本质与作用

什么是时序图?

时序图(Sequence Diagram) = 展示对象之间按时间顺序的交互过程

核心特点

复制代码
时序图回答三个问题:
1. 谁先?谁后?  → 时间顺序(从上到下)
2. 谁调用谁?    → 消息方向(从左到右)
3. 调用什么?    → 消息内容(函数/操作)

时序图的价值

对谁 价值
开发者 理解模块间协作,设计接口
测试者 了解调用流程,设计测试用例
维护者 快速理解系统交互逻辑
团队 统一对协作流程的理解

适用场景

复制代码
✅ 适合画时序图的场景:
- 模块间复杂交互流程
- 消息传递顺序很重要
- 异步通信场景
- 协议交互(Modbus、CAN、TCP/IP)
- 多任务协作(FreeRTOS)

❌ 不适合画时序图的场景:
- 单一模块内部逻辑(用活动图)
- 数据结构设计(用类图)
- 系统功能清单(用用例图)

二、时序图的核心元素详解

图片分析:关键元素标注

根据你上传的图片,让我们逐一解析:

复制代码
┌─────────────────────────────────────────────────────────┐
│  window:UI      aChain:HotelChain     aHotel:Hotel      │  ← Lifeline
├─────┬─────────────────┬──────────────────┬──────────────┤
│     │                 │                  │              │
│     │  1:makeReservation                │              │  ← Message
│     ├────────────────>│                  │              │
│     │                 │                  │              │
│     │                 │  1.1:makeReservation           │  ← Nested Message
│     │                 ├─────────────────>│              │
│     │                 │                  │              │
│     │                 │  loop [each day] │              │  ← Loop Fragment
│     │                 │  ┌───────────────┴────────┐    │
│     │                 │  │ 1.1.1: available()     │    │  ← Self Message
│     │                 │  │        ↓               │    │
│     │                 │  │        ↑               │    │
│     │                 │  └────────────────────────┘    │
│     │                 │                  │              │
│     │                 │  alt [isRoom=true]             │  ← Alternative
│     │                 │  ┌───────────────┴────────┐    │
│     │                 │  │ 1.1.2: <<create>>      │    │  ← Create Message
│     │                 │  │   :Reservation         │    │
│     │                 │  └────────────────────────┘    │
│     │                 │                  │              │
│     │  2: aTicket:Confirmation           │              │  ← Return Message
│     │<────────────────┤                  │              │
│     ×                 │                  │              │  ← Stopped (销毁)
└─────┴─────────────────┴──────────────────┴──────────────┘

元素 1:生命线(Lifeline)

定义

表示一个对象在时序图中的存在周期。

图形表示
复制代码
┌────────────┐
│ object:Class│  ← 矩形框(对象名:类名)
└──────┬─────┘
       │         ← 竖直虚线(生命线)
       │
       │
命名规范
复制代码
格式:对象名:类名

示例:
✅ window:UI              (对象名 + 类名)
✅ :UART                  (匿名对象,只有类名)
✅ uart1:UART_HandleTypeDef  (具体实例)

❌ UI                     (只有类名,不规范)
❌ window                 (只有对象名,不规范)
在嵌入式中的应用
c 复制代码
// C 语言中的"对象"
UART_HandleTypeDef huart1;   → huart1:UART_HandleTypeDef
SPI_HandleTypeDef  hspi1;    → hspi1:SPI_HandleTypeDef
TaskHandle_t       taskMain; → taskMain:TaskHandle

元素 2:激活条(Activation)

定义

表示对象在某个时间段内处于活跃状态(正在执行操作)。

图形表示
复制代码
对象:类
  │
  │    ┌──┐  ← 细长的矩形条(激活条)
  │    │  │    表示对象正在执行
  │    │  │
  │    └──┘
  │
含义
  • 激活条的开始:对象开始执行某个操作
  • 激活条的结束:对象完成操作,返回控制权
  • 嵌套激活条:对象在执行过程中又调用了其他操作
实际例子
复制代码
STM32:MCU
  │
  │    ┌──┐  ← 开始执行 HAL_UART_Transmit()
  │    │  │
  │    │  ├──→ DMA:DMA_Controller (调用 DMA)
  │    │  │      ┌──┐
  │    │  │      │  │ ← DMA 在执行传输
  │    │  │      └──┘
  │    │  │←────────  (DMA 完成)
  │    └──┘  ← HAL_UART_Transmit() 完成
  │

元素 3:消息(Message)

3.1 同步消息(Synchronous Message)

图形 :实线 + 实心箭头 ─────>

含义

  • 发送方调用接收方的方法
  • 发送方等待接收方处理完成
  • 类似于函数调用

示例

复制代码
A ──────> B : function()
   等待返回
A <────── B : return value

代码对应

c 复制代码
// 同步调用示例
uint8_t result = HAL_UART_Transmit(&huart1, data, len, timeout);
//               ↑ 阻塞等待,直到发送完成或超时
3.2 异步消息(Asynchronous Message)

图形 :实线 + 开放箭头 ─────⊳

含义

  • 发送方触发接收方的操作
  • 发送方不等待接收方完成
  • 类似于非阻塞调用或事件触发

示例

复制代码
A ─────⊳ B : function()
   继续执行,不等待

代码对应

c 复制代码
// 异步调用示例
HAL_UART_Transmit_IT(&huart1, data, len);
// ↑ 立即返回,数据在后台发送

// 或者 FreeRTOS 队列
xQueueSend(queue, &data, 0);
// ↑ 不等待队列处理
3.3 返回消息(Return Message)

图形 :虚线 + 开放箭头 - - - ⊳

含义

  • 接收方处理完成,返回结果给发送方
  • 可选绘制(有时为了简化可以省略)

示例

复制代码
A ──────> B : getData()
A <─ ─ ─  B : data
3.4 自调用消息(Self Message)

图形:对象调用自己的方法

复制代码
对象:类
  │    ┌──┐
  │    │  │─┐
  │    │  │ │ method()  ← 自己调用自己
  │    │  │←┘
  │    └──┘

含义

  • 对象调用自己的私有方法
  • 递归调用
  • 内部状态更新

示例

c 复制代码
void ProcessData(uint8_t *data) {
    if (CheckDataValid(data)) {  // ← 自调用
        // ...
    }
}
3.5 创建消息(Create Message)

图形 :虚线 + 箭头 + <<create>> 标签

复制代码
A ─ ─ ─> <<create>> :NewObject
                   ┌────────┐
                   │NewObject│
                   └────┬───┘
                        │

含义

  • 创建新对象
  • 在 C++ 中对应 new 操作
  • 在 C 中对应动态分配或初始化

示例

c 复制代码
// C 语言示例
UART_HandleTypeDef *uart = malloc(sizeof(UART_HandleTypeDef));
HAL_UART_Init(uart);

// 或者静态创建
QueueHandle_t queue;
queue = xQueueCreate(10, sizeof(uint32_t));  // ← 创建队列
3.6 销毁消息(Destroy Message)

图形 :生命线终点的 × 符号

复制代码
对象:类
  │
  │
  ×  ← 对象被销毁

含义

  • 对象生命周期结束
  • 资源被释放

元素 4:组合片段(Combined Fragment)

4.1 Loop(循环)

图形

复制代码
┌─ loop [condition] ─────────┐
│                            │
│  重复执行的消息序列          │
│                            │
└────────────────────────────┘

含义

  • 重复执行一段交互序列
  • 条件为真时持续执行

示例

复制代码
loop [each day]
    检查房间可用性
end

代码对应

c 复制代码
// 循环示例
for (int day = 0; day < 7; day++) {
    bool available = CheckRoomAvailable(day);
    if (available) {
        // 创建预订
    }
}
4.2 Alt(条件分支)

图形

复制代码
┌─ alt [condition1] ─────────┐
│  条件1成立时的消息序列      │
├─ [else] ──────────────────┤
│  否则执行的消息序列         │
└────────────────────────────┘

含义

  • 根据条件选择执行路径
  • 类似于 if-else

示例

复制代码
alt [isRoom = true]
    创建预订
else
    返回错误
end

代码对应

c 复制代码
if (isRoom) {
    CreateReservation();
} else {
    ReturnError();
}
4.3 Opt(可选)

图形

复制代码
┌─ opt [condition] ──────────┐
│  条件成立时执行            │
└────────────────────────────┘

含义

  • 可选执行的序列
  • 条件不满足时跳过

示例

复制代码
opt [errorOccurred]
    发送错误日志
end
4.4 Par(并行)

图形

复制代码
┌─ par ──────────────────────┐
│  并行执行序列1              │
├────────────────────────────┤
│  并行执行序列2              │
└────────────────────────────┘

含义

  • 多个序列同时执行
  • 在多任务系统中很常见

示例

c 复制代码
// FreeRTOS 中的并行任务
xTaskCreate(TaskSensor, ...);  // 并行执行
xTaskCreate(TaskDisplay, ...); // 并行执行
4.5 Ref(引用)

图形

复制代码
┌─ ref ───────────────────┐
│  调用其他时序图          │
│  LoginSequence          │
└─────────────────────────┘

含义

  • 引用另一个时序图
  • 避免重复,提高可读性

三、三个关键问题:谁先?谁后?调用顺序?

问题 1:谁先?谁后?

答案:从上到下看时间轴

复制代码
时间 ↓

对象A        对象B        对象C
  │            │            │
  │  消息1     │            │  ← 第1步
  ├──────────>│            │
  │            │  消息2     │  ← 第2步(在消息1之后)
  │            ├──────────>│
  │            │<───────────┤  ← 第3步(在消息2之后)
  │            │  消息3     │
  │<───────────┤            │  ← 第4步(在消息3之后)
  │            │            │

执行顺序:消息1 → 消息2 → 消息3(返回)→ 消息4(返回)

核心原则

  • 纵轴 = 时间轴:越往下,时间越晚
  • 箭头位置:上面的箭头先发生,下面的箭头后发生

问题 2:谁调用谁?

答案:从箭头方向看

复制代码
主动方        被动方
  │            │
  ├──────────>│  ← 箭头方向:主动方调用被动方
  │            │
  │<───────────┤  ← 箭头方向:被动方返回给主动方
  │            │

规则

  • 箭头起点 :消息的发送方(主动调用者)
  • 箭头终点 :消息的接收方(被调用者)

示例

复制代码
用户 ──────> 系统 : 登录请求
     发起者   接收者

问题 3:调用顺序?

答案:使用序号标注

序号规则
复制代码
格式:[父序号.]子序号 : 消息名称

示例:
1: 第一层调用
  1.1: 第二层调用
    1.1.1: 第三层调用
    1.1.2: 第三层调用
  1.2: 第二层调用
2: 第一层调用
完整示例(从图片中提取)
复制代码
window:UI     aChain:HotelChain    aHotel:Hotel
   │                │                   │
   │ 1: makeReservation                │
   ├───────────────>│                   │
   │                │ 1.1: makeReservation
   │                ├──────────────────>│
   │                │   loop [each day] │
   │                │   ┌───────────────┴────┐
   │                │   │ 1.1.1: available() │
   │                │   └────────────────────┘
   │                │ alt [isRoom = true]    │
   │                │   ┌───────────────┴────┐
   │                │   │ 1.1.2: <<create>>  │
   │                │   │   :Reservation     │
   │                │   └────────────────────┘
   │ 2: aTicket:Confirmation            │
   │<───────────────┤                   │
   │                │                   │

调用顺序解析

  1. window 调用 aChainmakeReservation(序号 1)
  2. aChain 调用 aHotelmakeReservation(序号 1.1)
  3. aHotel 在循环中调用自己的 available(序号 1.1.1)
  4. 如果房间可用,创建 Reservation 对象(序号 1.1.2)
  5. 返回确认信息给 window(序号 2)

四、时序图 vs 其他 UML 图

对比表

图类型 关注点 时间维度 典型问题
用例图 功能清单 系统能做什么?
类图 结构关系 如何组织代码?
时序图 交互顺序 谁先谁后?如何协作?
活动图 流程逻辑 步骤是什么?条件判断?
状态图 状态变化 状态如何转换?

时序图的独特价值

复制代码
时序图是唯一能够清晰展示"时间顺序"和"对象交互"的图

适合回答:
✅ A 和 B 谁先调用?
✅ 消息传递顺序是什么?
✅ 异步操作如何协作?
✅ 多任务如何通信?

五、嵌入式系统中的时序图应用

应用场景总结

场景 为什么需要时序图 关注点
串口通信 收发顺序、握手协议 谁先发?谁先收?
Modbus 协议 主从问答、帧结构 请求-响应顺序
CAN 总线 消息优先级、仲裁 多节点通信时序
I2C 通信 起始-应答-停止 主从时序关系
FreeRTOS 任务 任务间消息传递 队列、信号量时序
中断处理 中断嵌套、优先级 中断响应顺序

六、完整实战案例

案例 1:串口数据收发(UART)

场景描述

STM32 通过 UART 发送数据给 PC 上位机,并等待回复。

时序图
复制代码
用户      主程序        UART硬件      DMA控制器      PC上位机
 │          │             │              │             │
 │ 点击发送  │             │              │             │
 ├─────────>│             │              │             │
 │          │             │              │             │
 │          │ 1: HAL_UART_Transmit_DMA() │             │
 │          ├────────────>│              │             │
 │          │             │              │             │
 │          │             │ 1.1: 配置DMA  │             │
 │          │             ├─────────────>│             │
 │          │             │              │             │
 │          │             │              │ 1.2: 发送数据│
 │          │             │              ├────────────>│
 │          │             │              │             │
 │          │ 返回 (非阻塞)│              │             │
 │          │<────────────┤              │             │
 │          │             │              │             │
 │          │   ... 继续执行其他任务 ...   │             │
 │          │             │              │             │
 │          │             │    DMA中断    │             │
 │          │             │<──────────────┤             │
 │          │             │              │             │
 │          │ 2: HAL_UART_TxCpltCallback() │           │
 │          │<────────────┤              │             │
 │          │             │              │             │
 │ 显示完成  │             │              │             │
 │<─────────┤             │              │             │
 │          │             │              │             │
 │          │             │              │ 3: PC回复数据│
 │          │             │              │<────────────┤
 │          │             │              │             │
 │          │             │    接收中断   │             │
 │          │             │<──────────────┤             │
 │          │             │              │             │
 │          │ 4: HAL_UART_RxCpltCallback() │           │
 │          │<────────────┤              │             │
 │          │             │              │             │
 │ 显示数据  │             │              │             │
 │<─────────┤             │              │             │
 │          │             │              │             │
PlantUML 代码
plantuml 复制代码
@startuml UARTCommunication

actor "用户" as User
participant "主程序" as Main
participant "UART硬件" as UART
participant "DMA控制器" as DMA
participant "PC上位机" as PC

User -> Main: 点击发送
activate Main

Main -> UART: 1: HAL_UART_Transmit_DMA(data)
activate UART

UART -> DMA: 1.1: 配置DMA传输
activate DMA

DMA -> PC: 1.2: 发送数据
PC --> DMA: 

deactivate DMA

UART --> Main: 返回 (非阻塞)
deactivate UART

note right of Main
  主程序继续执行
  其他任务
end note

...DMA传输完成...

DMA -> UART: DMA传输完成中断
activate UART

UART -> Main: 2: HAL_UART_TxCpltCallback()
activate Main

Main -> User: 显示发送完成

deactivate Main
deactivate UART

...PC处理数据并回复...

PC -> DMA: 3: PC回复数据
activate DMA

DMA -> UART: 接收完成中断
activate UART

UART -> Main: 4: HAL_UART_RxCpltCallback()
activate Main

Main -> User: 显示接收到的数据

deactivate Main
deactivate UART
deactivate DMA

@enduml
关键点
  1. 异步发送HAL_UART_Transmit_DMA 立即返回(非阻塞)
  2. 中断回调:DMA 完成后触发中断,调用回调函数
  3. 时序清晰:可以看出"发送"和"接收"的时间顺序

案例 2:Modbus RTU 通信(典型主从模式)

场景描述

上位机通过 Modbus RTU 协议读取 STM32 设备的寄存器数据。

时序图
复制代码
上位机       STM32主程序    Modbus解析器   设备寄存器    UART
  │             │              │              │           │
  │ 1: 发送读请求(0x03)         │              │           │
  ├────────────────────────────────────────────────────────>│
  │             │              │              │           │
  │             │              │           接收中断         │
  │             │              │<─────────────────────────┤
  │             │              │              │           │
  │             │ 2: 通知有数据  │              │           │
  │             │<─────────────┤              │           │
  │             │              │              │           │
  │             │ 3: ParseModbusFrame()        │           │
  │             ├─────────────>│              │           │
  │             │              │              │           │
  │             │              │ 3.1: 校验CRC │           │
  │             │              ├─┐            │           │
  │             │              │ │            │           │
  │             │              │<┘            │           │
  │             │              │              │           │
  │             │              │ alt [CRC正确] │          │
  │             │              │ ┌────────────┴────┐      │
  │             │              │ │ 3.2: 读取寄存器 │      │
  │             │              │ ├────────────────>│      │
  │             │              │ │                 │      │
  │             │              │ │ 3.3: 返回数据   │      │
  │             │              │ │<────────────────┤      │
  │             │              │ │                 │      │
  │             │              │ │ 3.4: 构建响应帧 │      │
  │             │              │ │─┐               │      │
  │             │              │ │ │               │      │
  │             │              │ │<┘               │      │
  │             │              │ └─────────────────┘      │
  │             │              │              │           │
  │             │ 4: 发送响应   │              │           │
  │             │<─────────────┤              │           │
  │             │              │              │           │
  │             │ 5: HAL_UART_Transmit()      │           │
  │             ├────────────────────────────────────────>│
  │             │              │              │           │
  │ 6: 接收响应数据              │              │           │
  │<────────────────────────────────────────────────────────┤
  │             │              │              │           │
  │ 7: 解析数据  │              │              │           │
  │─┐           │              │              │           │
  │ │           │              │              │           │
  │<┘           │              │              │           │
  │             │              │              │           │
PlantUML 代码
plantuml 复制代码
@startuml ModbusRTU

participant "上位机" as PC
participant "STM32主程序" as Main
participant "Modbus解析器" as Modbus
participant "设备寄存器" as Register
participant "UART" as UART

PC -> UART: 1: 发送读请求 (功能码 0x03)
activate UART

UART -> Modbus: 接收中断
activate Modbus

Modbus -> Main: 2: 通知有数据
activate Main

Main -> Modbus: 3: ParseModbusFrame()

Modbus -> Modbus: 3.1: 校验CRC

alt CRC正确
    Modbus -> Register: 3.2: 读取寄存器
    activate Register
    Register --> Modbus: 3.3: 返回数据
    deactivate Register
    
    Modbus -> Modbus: 3.4: 构建响应帧
    
    Modbus -> Main: 4: 发送响应
    deactivate Modbus
    
    Main -> UART: 5: HAL_UART_Transmit(response)
    
else CRC错误
    Modbus -> Main: 返回错误码
    note right: 不发送响应
end

UART -> PC: 6: 响应数据
deactivate UART

PC -> PC: 7: 解析数据

deactivate Main

@enduml
Modbus 协议要点

请求帧格式

复制代码
[从机地址][功能码][起始地址][寄存器数量][CRC校验]
   0x01      0x03    0x0000      0x0002      0xXXXX

响应帧格式

复制代码
[从机地址][功能码][字节数][数据1][数据2][CRC校验]
   0x01      0x03    0x04   0xXX  0xXX    0xXXXX

时序关键点

  1. 主从模式:上位机(主)发起请求,STM32(从)响应
  2. 严格时序:从机必须在收到完整帧后才能响应
  3. CRC 校验:必须先校验,校验失败则不响应

案例 3:CAN 总线通信

场景描述

两个 ECU(电子控制单元)通过 CAN 总线交换数据。

时序图
复制代码
ECU1       CAN控制器1    CAN总线    CAN控制器2      ECU2
 │             │           │            │            │
 │ 1: 发送CAN消息│           │            │            │
 ├────────────>│           │            │            │
 │             │           │            │            │
 │             │ 1.1: 仲裁  │            │            │
 │             ├──────────>│            │            │
 │             │           │            │            │
 │             │  (低ID优先) │            │            │
 │             │           │            │            │
 │             │ 1.2: 发送帧│            │            │
 │             ├──────────>│            │            │
 │             │           │            │            │
 │             │           │ 1.3: 广播   │            │
 │             │           ├───────────>│            │
 │             │           │            │            │
 │             │           │            │ 1.4: 过滤   │
 │             │           │            ├───────────>│
 │             │           │            │            │
 │             │           │            │ 1.5: 接收中断│
 │             │           │            │<───────────┤
 │             │           │            │            │
 │             │           │  2: ACK    │            │
 │             │           │<───────────┤            │
 │             │           │            │            │
 │             │ 3: 确认发送成功         │            │
 │             │<──────────┤            │            │
 │             │           │            │            │
 │ 4: 回调通知  │           │            │            │
 │<────────────┤           │            │            │
 │             │           │            │            │
关键特性
  1. 仲裁机制:ID 越低优先级越高
  2. 广播特性:所有节点都能收到,通过过滤器筛选
  3. ACK 确认:接收方必须发送应答位

案例 4:I2C 读取传感器数据

场景描述

STM32 作为主机,通过 I2C 读取温湿度传感器(如 SHT30)的数据。

时序图
复制代码
STM32(主)   I2C硬件    I2C总线    传感器(从)   
  │           │          │           │
  │ 1: 发起读操作         │           │
  ├──────────>│          │           │
  │           │          │           │
  │           │ 1.1: START │          │
  │           ├─────────>│           │
  │           │          │           │
  │           │ 1.2: 发送地址+W       │
  │           ├─────────>│──────────>│
  │           │          │           │
  │           │          │ 1.3: ACK  │
  │           │          │<──────────┤
  │           │          │           │
  │           │ 1.4: 发送命令字       │
  │           ├─────────>│──────────>│
  │           │          │           │
  │           │          │ 1.5: ACK  │
  │           │          │<──────────┤
  │           │          │           │
  │           │ 1.6: RESTART          │
  │           ├─────────>│           │
  │           │          │           │
  │           │ 1.7: 发送地址+R       │
  │           ├─────────>│──────────>│
  │           │          │           │
  │           │          │ 1.8: ACK  │
  │           │          │<──────────┤
  │           │          │           │
  │           │          │ 1.9: 读取数据
  │           │          │<──────────┤
  │           │          │           │
  │           │ 1.10: ACK │           │
  │           ├─────────>│──────────>│
  │           │          │           │
  │           │          │ 1.11: 读取数据
  │           │          │<──────────┤
  │           │          │           │
  │           │ 1.12: NACK (最后一字节)│
  │           ├─────────>│──────────>│
  │           │          │           │
  │           │ 1.13: STOP │          │
  │           ├─────────>│           │
  │           │          │           │
  │ 2: 返回数据 │          │           │
  │<──────────┤          │           │
  │           │          │           │
I2C 协议要点

主机读操作流程

复制代码
START → 地址+W → ACK → 命令 → ACK → RESTART → 
地址+R → ACK → 数据1 → ACK → 数据2 → NACK → STOP

时序约束

  • START:主机拉低 SDA(SCL 为高)
  • ACK:从机必须应答
  • NACK:最后一个字节主机不应答,告知传输结束
  • STOP:主机拉高 SDA(SCL 为高)

案例 5:FreeRTOS 任务间通信(队列)

场景描述

传感器任务采集数据,通过队列发送给显示任务。

时序图
复制代码
传感器任务   队列(Queue)   显示任务    OLED屏幕
   │           │            │           │
   │ loop      │            │           │
   │───┐       │            │           │
   │   │ 1: 读取传感器       │           │
   │<──┘       │            │           │
   │           │            │           │
   │ 2: xQueueSend(data)    │           │
   ├──────────>│            │           │
   │           │            │           │
   │  等待下次采集...        │           │
   │           │            │           │
   │           │ 3: 等待队列消息         │
   │           │<───────────┤           │
   │           │            │           │
   │           │ (阻塞状态)  │           │
   │           │            │           │
   │           │ 4: 有新数据 │           │
   │           ├───────────>│           │
   │           │            │           │
   │           │            │ 5: 更新显示│
   │           │            ├──────────>│
   │           │            │           │
   │           │ 6: 再次等待 │           │
   │           │<───────────┤           │
   │           │            │           │
PlantUML 代码
plantuml 复制代码
@startuml FreeRTOS_Queue

participant "传感器任务" as SensorTask
participant "队列(Queue)" as Queue
participant "显示任务" as DisplayTask
participant "OLED屏幕" as OLED

loop 每秒一次
    SensorTask -> SensorTask: 1: 读取传感器数据
    
    SensorTask -> Queue: 2: xQueueSend(data, 0)
    note right: 不阻塞,立即返回
end

DisplayTask -> Queue: 3: xQueueReceive(data, portMAX_DELAY)
activate DisplayTask
note right: 阻塞等待,直到有数据

Queue --> DisplayTask: 4: 返回数据
note right: 队列有数据后立即返回

DisplayTask -> OLED: 5: 更新显示
activate OLED
OLED --> DisplayTask: 显示完成
deactivate OLED

DisplayTask -> Queue: 6: 再次等待新数据
note right: 循环等待

deactivate DisplayTask

@enduml
关键点
  1. 异步通信:传感器任务不需要知道谁接收数据
  2. 阻塞接收:显示任务阻塞等待,不浪费 CPU
  3. 解耦设计:队列隔离了两个任务,提高了可维护性

案例 6:中断嵌套与优先级

场景描述

定时器中断和 UART 接收中断同时触发,展示中断优先级和嵌套。

时序图
复制代码
主程序      TIM中断     UART中断    NVIC
  │           │           │          │
  │  执行中... │           │          │
  │           │           │          │
  │           │  定时器溢出 │          │
  │           │<──────────────────────┤
  │           │           │          │
  │ 1: 进入TIM_IRQHandler │          │
  │<──────────┤           │          │
  │           │           │          │
  │  处理中... │           │          │
  │           │           │          │
  │           │           │ 串口接收  │
  │           │           │<─────────┤
  │           │           │          │
  │           │  (优先级高,抢占)     │
  │           │           │          │
  │ 2: 进入UART_IRQHandler│          │
  │<──────────────────────┤          │
  │           │           │          │
  │  处理接收数据          │          │
  │           │           │          │
  │ 3: 退出UART中断        │          │
  ├──────────────────────>│          │
  │           │           │          │
  │ 4: 恢复TIM中断处理     │          │
  ├──────────>│           │          │
  │           │           │          │
  │  继续处理  │           │          │
  │           │           │          │
  │ 5: 退出TIM中断         │          │
  ├──────────>│           │          │
  │           │           │          │
  │  恢复主程序│           │          │
  │           │           │          │
关键概念

中断优先级

c 复制代码
// STM32 中断优先级配置
HAL_NVIC_SetPriority(TIM2_IRQn, 2, 0);    // 优先级 2(低)
HAL_NVIC_SetPriority(USART1_IRQn, 1, 0);  // 优先级 1(高)

中断嵌套

  • 高优先级中断可以抢占低优先级中断
  • 低优先级中断不能抢占高优先级中断

案例 7:SPI 读写 Flash(W25Q128)

场景描述

STM32 通过 SPI 接口读取外部 Flash 芯片的数据。

时序图
复制代码
主程序     SPI硬件    CS片选    W25Q128 Flash
  │          │         │            │
  │ 1: 读取Flash        │            │
  ├─────────>│         │            │
  │          │         │            │
  │          │ 1.1: 拉低CS           │
  │          ├────────>│            │
  │          │         │            │
  │          │         │ CS = 0     │
  │          │         ├───────────>│
  │          │         │            │
  │          │ 1.2: 发送读命令(0x03) │
  │          ├────────────────────>│
  │          │         │            │
  │          │ 1.3: 发送24位地址     │
  │          ├────────────────────>│
  │          │         │            │
  │          │         │ 1.4: 读取数据
  │          │         │<───────────┤
  │          │<────────┤            │
  │          │         │            │
  │          │ 1.5: 拉高CS           │
  │          ├────────>│            │
  │          │         │            │
  │          │         │ CS = 1     │
  │          │         ├───────────>│
  │          │         │            │
  │ 2: 返回数据         │            │
  │<─────────┤         │            │
  │          │         │            │
SPI 读操作流程
c 复制代码
// 代码示例
void W25Q_ReadData(uint32_t addr, uint8_t *buf, uint16_t len) {
    CS_LOW();                          // 1.1: 拉低片选
    SPI_Transmit(0x03);                // 1.2: 发送读命令
    SPI_Transmit((addr >> 16) & 0xFF); // 1.3: 发送地址(高字节)
    SPI_Transmit((addr >> 8) & 0xFF);  //      中字节
    SPI_Transmit(addr & 0xFF);         //      低字节
    SPI_Receive(buf, len);             // 1.4: 读取数据
    CS_HIGH();                         // 1.5: 拉高片选
}

时序要求

  • CS 必须在整个操作期间保持低电平
  • 先发送命令和地址,再读取数据
  • 操作完成后拉高 CS,Flash 芯片进入待机

七、PlantUML 绘制指南

基本语法

plantuml 复制代码
@startuml

' 定义参与者
participant "对象A" as A
participant "对象B" as B

' 同步消息(实线实心箭头)
A -> B: 同步调用

' 异步消息(实线开放箭头)
A ->> B: 异步调用

' 返回消息(虚线)
A <-- B: 返回

' 自调用
A -> A: 自己调用自己

' 激活/失活
activate A
A -> B: 消息
deactivate A

' 创建对象
create participant "新对象" as C
A -> C: <<create>>

' 销毁对象
destroy C

' 循环
loop 条件
    A -> B: 重复的操作
end

' 条件分支
alt 条件1
    A -> B: 分支1
else 条件2
    A -> B: 分支2
end

' 可选
opt 条件
    A -> B: 可选操作
end

' 并行
par
    A -> B: 并行操作1
    A -> C: 并行操作2
end

' 注释
note right of A: 这是注释
note left of A
  多行注释
  第二行
end note

@enduml

常用箭头类型

箭头 PlantUML 语法 含义
──────> A -> B 同步消息(实心箭头)
─ ─ ─> A ->> B 异步消息(开放箭头)
<────── A <- B 同步返回
<─ ─ ─ A <<-- B 异步返回
─────⊳ A -\ B Lost message
⊲───── A /- B Found message

完整模板(嵌入式系统)

plantuml 复制代码
@startuml EmbeddedTemplate

title 嵌入式系统时序图模板

actor "用户" as User
participant "主程序" as Main
participant "硬件驱动" as Driver
participant "外设" as Peripheral

User -> Main: 1: 发起操作
activate Main

Main -> Driver: 2: 调用驱动函数
activate Driver

Driver -> Peripheral: 3: 配置硬件
activate Peripheral

Peripheral --> Driver: 4: 配置完成
deactivate Peripheral

Driver --> Main: 5: 返回状态
deactivate Driver

alt 操作成功
    Main -> User: 6: 显示成功
else 操作失败
    Main -> User: 6: 显示错误
end

deactivate Main

@enduml

八、时序图设计的最佳实践

1. 保持简洁

复制代码
❌ 不好的做法:
- 包含过多细节(每个变量赋值都画)
- 对象太多(超过 7 个)
- 消息太密集

✅ 好的做法:
- 聚焦核心交互
- 合并不重要的细节
- 关键对象 3-5 个

2. 清晰的命名

复制代码
❌ 不好的命名:
- data1, data2
- func1, func2
- obj

✅ 好的命名:
- sensorData, displayData
- ReadTemperature, UpdateDisplay
- huart1:UART_HandleTypeDef

3. 使用序号

复制代码
建议使用序号标注消息,便于追踪:

1: 第一层调用
  1.1: 第二层调用
    1.1.1: 第三层调用
  1.2: 第二层调用
2: 第一层调用

4. 注释关键点

plantuml 复制代码
note right of Object
  关键点说明:
  - 这里会阻塞等待
  - 超时时间 1000ms
end note

5. 分层设计

复制代码
复杂系统分多个时序图:
- 高层时序图:模块间交互
- 详细时序图:单个模块内部
- 协议时序图:通信协议细节

九、常见错误与避免

错误 1:时间方向错误

复制代码
❌ 错误:箭头从下往上
正确:时间从上往下,箭头也应该向下

错误 2:箭头方向混乱

复制代码
❌ 错误:
A <───> B  (双向箭头不明确)

✅ 正确:
A ────> B  (A 调用 B)
A <──── B  (B 返回给 A)

错误 3:激活条不匹配

复制代码
❌ 错误:
activate A
... 很多操作 ...
(忘记 deactivate)

✅ 正确:
activate A
... 操作 ...
deactivate A

错误 4:对象名称不一致

复制代码
❌ 错误:
participant "UART" as uart
uart -> ...  (小写)
UART -> ...  (大写)

✅ 正确:
participant "UART" as UART
UART -> ...  (始终使用别名)

十、总结:时序图的核心价值

对嵌入式开发的价值

复制代码
1. 设计阶段:
   - 明确模块间接口
   - 发现设计缺陷
   - 规划中断优先级

2. 开发阶段:
   - 指导代码编写
   - 明确调用顺序
   - 避免竞态条件

3. 调试阶段:
   - 理解执行流程
   - 定位时序问题
   - 分析死锁原因

4. 维护阶段:
   - 快速理解代码
   - 文档化设计
   - 便于交接

关键要点回顾

复制代码
1. 谁先谁后?  → 从上到下看时间轴
2. 谁调用谁?  → 从箭头方向看
3. 调用顺序?  → 使用序号标注

适用场景:
✅ 串口通信
✅ Modbus / CAN / I2C
✅ FreeRTOS 任务交互
✅ 中断处理
✅ 状态机设计

学习建议

复制代码
1. 从简单开始:先画双对象交互
2. 实践为主:结合实际项目绘制
3. 工具辅助:使用 PlantUML 等工具
4. 迭代优化:先粗后细,逐步完善
5. 团队评审:与同事讨论,统一理解

附录:参考资源

  • UML 规范:OMG UML 2.5 Specification
  • PlantUML 官网https://plantuml.com
  • 嵌入式协议
    • Modbus Protocol Specification
    • CAN Bus Protocol
    • I2C Specification
  • RTOS 文档:FreeRTOS Reference Manual

记住:时序图是理解和设计嵌入式系统交互的最佳工具!

相关推荐
行业探路者2 小时前
如何利用活码生成产品画册二维码?
学习·音视频·语音识别·二维码·设备巡检
好奇龙猫2 小时前
人工智能学习-AI-MIT公开课-第三节:推理:目标树与基于规则的专家系统-笔记
人工智能·笔记·学习
好奇龙猫2 小时前
【AI学习-comfyUI学习-第二十节-controlnet线稿+softedge线稿处理器工作流艺术线处理器工作流-各个部分学习】
人工智能·学习
小林有点嵌2 小时前
UML之状态图学习
网络·学习·uml
小林有点嵌3 小时前
UML之类图学习
学习·uml
小林有点嵌3 小时前
UML之用例图学习
学习·microsoft·uml
wdfk_prog3 小时前
[Linux]学习笔记系列 -- [fs][fs_parser]
linux·笔记·学习
白帽子凯哥哥3 小时前
在学习SQL注入或XSS这类具体漏洞时,如何设计一个高效的“理论+实践”学习循环?
sql·学习·漏洞·xss
全栈陈序员4 小时前
v-if 和 v-for 的优先级是什么?
前端·javascript·vue.js·学习·前端框架·ecmascript