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

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

相关推荐
西岸行者3 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意3 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码3 天前
嵌入式学习路线
学习
毛小茛3 天前
计算机系统概论——校验码
学习
babe小鑫3 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms3 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下3 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。3 天前
2026.2.25监控学习
学习
im_AMBER3 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J3 天前
从“Hello World“ 开始 C++
c语言·c++·学习