UML 时序图完全指南(嵌入式工程实践版)
目录
- 时序图的本质与作用
- 时序图的核心元素详解
- 三个关键问题:谁先?谁后?调用顺序?
- 时序图 vs 其他 UML 图
- 嵌入式系统中的时序图应用
- 完整实战案例(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 │
│<───────────────┤ │
│ │ │
调用顺序解析:
window调用aChain的makeReservation(序号 1)aChain调用aHotel的makeReservation(序号 1.1)aHotel在循环中调用自己的available(序号 1.1.1)- 如果房间可用,创建
Reservation对象(序号 1.1.2) - 返回确认信息给
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
关键点
- 异步发送 :
HAL_UART_Transmit_DMA立即返回(非阻塞) - 中断回调:DMA 完成后触发中断,调用回调函数
- 时序清晰:可以看出"发送"和"接收"的时间顺序
案例 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
时序关键点:
- 主从模式:上位机(主)发起请求,STM32(从)响应
- 严格时序:从机必须在收到完整帧后才能响应
- 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: 回调通知 │ │ │ │
│<────────────┤ │ │ │
│ │ │ │ │
关键特性
- 仲裁机制:ID 越低优先级越高
- 广播特性:所有节点都能收到,通过过滤器筛选
- 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
关键点
- 异步通信:传感器任务不需要知道谁接收数据
- 阻塞接收:显示任务阻塞等待,不浪费 CPU
- 解耦设计:队列隔离了两个任务,提高了可维护性
案例 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
记住:时序图是理解和设计嵌入式系统交互的最佳工具!