目录
- 活动图的本质理解
- 活动图 vs 状态图 vs 时序图
- 核心元素详解
- 并发与同步(Fork/Join)
- 判断与循环
- 泳道(Swimlanes)
- 10个完整实战案例
- PlantUML 绘制指南
- 最佳实践与避坑指南
一、活动图的本质理解
一句话概念
活动图 = 带并发语义的流程图
特点对比
普通流程图:
- 描述步骤顺序
- 简单直观
- 缺少严格语义
UML 活动图:
+ 更加严谨
+ 支持并发表达
+ 有标准的图形元素
+ 可以表达复杂业务逻辑
适用场景
✅ 非常适合:
- 初始化流程
- 业务流程
- 算法步骤
- 通信协议
- 数据处理流程
❌ 不太适合:
- 对象间交互(用时序图)
- 状态转换(用状态图)
- 类结构(用类图)
二、活动图 vs 状态图 vs 时序图(必须分清)
三者对比表
| 对比项 | 活动图 | 状态图 | 时序图 |
|---|---|---|---|
| 关注点 | 做事的步骤 | 所处的状态 | 对象间交互 |
| 触发方式 | 顺序执行 | 事件驱动 | 消息传递 |
| 是否强调当前状态 | 否 | 是 | 否 |
| 是否适合算法 | ✅ 非常适合 | ❌ 不合适 | ❌ 不合适 |
| 是否适合长期运行 | ❌ 不适合 | ✅ 适合 | ❌ 不适合 |
| 时间维度 | 顺序(步骤) | 持续(状态) | 时间轴(消息) |
| 典型问题 | 怎么做? | 在干什么? | 谁做什么? |
使用场景判断
场景:设备初始化
→ 活动图 ✅(一系列步骤)
场景:设备运行状态
→ 状态图 ✅(待机、运行、错误)
场景:模块间通信
→ 时序图 ✅(谁先谁后)
场景:单个函数算法
→ 活动图 ✅(算法步骤)
场景:协议交互
→ 时序图 ✅(请求响应)或活动图 ✅(协议流程)
记忆口诀
算法用活动图,逻辑用状态图
自己怎么做 → 活动图
彼此怎么说 → 时序图
现在干什么 → 状态图
三、核心元素详解(工程够用的 9 个)
元素对照表
| UML 元素 | 含义 | 图形表示 | C/C++ 对应 |
|---|---|---|---|
| Initial Node | 开始 | ● | 函数入口 |
| Final Node | 结束 | ◎ | return |
| Action | 动作 | 圆角矩形 | 函数调用/代码块 |
| Control Flow | 控制流 | 箭头 | 程序流 |
| Decision | 判断 | ◇ | if/else |
| Merge | 合并 | ◇ | 分支结束 |
| Fork | 并发开始 | ▬ | 创建任务/线程 |
| Join | 并发结束 | ▬ | 同步点 |
| Swimlane | 泳道 | 分隔区域 | 模块/对象 |
详细说明
1. Initial Node(初始节点)
图形:实心黑色圆圈 ●
含义:活动的开始点
规则:
- 每个活动图只有一个初始节点
- 初始节点只有出边,没有入边
代码对应:
c
void Function(void) // ← 函数入口就是初始节点
{
// ...
}
2. Final Node(终止节点)
图形:圆圈内有实心圆 ◎
含义:活动的结束点
类型:
- Activity Final Node:整个活动结束
- Flow Final Node:某个分支结束
代码对应:
c
void Function(void)
{
if (error) {
return; // ← 终止节点
}
// ...
return; // ← 终止节点
}
3. Action(动作)
图形:圆角矩形
┌──────────────┐
│ Action Name │
└──────────────┘
含义:执行的一个操作单元
命名规范:
c
✅ 好的命名(动宾结构):
- Initialize UART
- Read Sensor Data
- Send Message
- Calculate CRC
❌ 不好的命名:
- UART (只有名词)
- Data (不清楚做什么)
- Process(太抽象)
代码对应:
c
// 每个动作对应一个函数调用或代码块
HAL_UART_Init(&huart1); // ← Action: Initialize UART
ReadSensorData(); // ← Action: Read Sensor Data
SendMessage(data, len); // ← Action: Send Message
4. Decision(判断节点)
图形:菱形 ◇
┌───────┐
│ Check │
└───┬───┘
│
◇─┴─◇ [condition]
Yes │ No
含义:基于条件的分支
标注方式:
[condition] → 条件表达式
[else] → 否则分支
代码对应:
c
if (rx_done) { // ← Decision 节点
ProcessData(); // ← [Yes] 分支
} else {
WaitData(); // ← [No] 分支
}
重要规则:
- 一个 Decision 有一个入边
- 一个 Decision 可以有多个出边
- 每个出边都要标注条件
5. Merge(合并节点)
图形:菱形 ◇(与 Decision 相同图形,但语义不同)
含义:多个分支合并到一起
代码对应:
c
if (mode == 1) {
Process1();
} else {
Process2();
}
// ← 这里是 Merge 节点,两个分支汇合
ContinueWork();
6. Fork(并发开始)
图形:粗黑线 ▬
│
──┼── Fork
│
┌─┼─┐
│ │ │
含义:从一个流分成多个并行流
代码对应:
c
// FreeRTOS 中的并发
xTaskCreate(Task1, ...); // ← Fork 创建并行任务
xTaskCreate(Task2, ...); // ← Fork 创建并行任务
xTaskCreate(Task3, ...); // ← Fork 创建并行任务
// 或者使用中断(硬件并发)
HAL_UART_Receive_IT(&huart1, ...); // 启动异步接收
HAL_TIM_Base_Start_IT(&htim2); // 启动定时器
7. Join(并发结束)
图形:粗黑线 ▬
│ │ │
└─┼─┘
──┼── Join
│
含义:等待所有并行流完成后才继续
代码对应:
c
// 等待多个任务完成
xEventGroupWaitBits(
eventGroup,
TASK1_BIT | TASK2_BIT | TASK3_BIT,
pdTRUE,
pdTRUE, // ← 等待所有位都置位(Join语义)
portMAX_DELAY
);
// 或者信号量计数
for (int i = 0; i < 3; i++) {
xSemaphoreTake(sem, portMAX_DELAY); // 等待3个任务
}
8. Swimlane(泳道)
图形:垂直或水平的分隔区域
┌─────────────┬─────────────┬─────────────┐
│ User │ System │ Database │
├─────────────┼─────────────┼─────────────┤
│ │ │ │
│ [Action1] │ │ │
│ │ │ │ │
│ │ │ [Action2] │ │
│ │ │ │ │ │
│ │ │ │ │ [Action3] │
│ │ │ │
└─────────────┴─────────────┴─────────────┘
含义:将活动按职责/模块分组
适用场景:
- 多个模块协作
- 业务流程涉及多个角色
- 系统分层设计
代码对应:
c
// 每个泳道对应一个模块或层
// User Layer
void User_ClickButton(void);
// System Layer
void System_ProcessRequest(void);
// Database Layer
void DB_Query(void);
四、并发与同步(Fork/Join)
为什么并发是活动图的核心价值?
这是活动图和普通流程图的本质区别!
普通流程图:只能表达串行流程
活动图:可以精确表达并行和同步
并发场景 1:系统启动(多任务并行)
UML 活动图
● (Start)
│
│
┌─────────────┐
│System Init │
└──────┬──────┘
│
──┼── Fork(并发开始)
│
┌──────┼──────┬──────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌────────┐ ┌────┐ ┌────┐ ┌────────┐
│UART RX │ │Key │ │LED │ │Sensor │
│Task │ │Scan│ │Blink│ │Task │
└────────┘ └────┘ └────┘ └────────┘
│ │ │ │
└──────┼──────┴──────────┘
│
──┼── Join(同步等待)
│
◎ (End)
FreeRTOS 代码实现
c
void main(void)
{
// 系统初始化
SystemInit();
// Fork - 创建并行任务
xTaskCreate(UART_RX_Task, "UART", 128, NULL, 1, NULL);
xTaskCreate(Key_Scan_Task, "Key", 128, NULL, 1, NULL);
xTaskCreate(LED_Blink_Task,"LED", 128, NULL, 1, NULL);
xTaskCreate(Sensor_Task, "Sensor",128, NULL, 1, NULL);
// 启动调度器
vTaskStartScheduler(); // 任务开始并行运行
// Join - 这里不会到达(调度器永不返回)
}
void UART_RX_Task(void *param)
{
while (1) {
// UART 接收处理
vTaskDelay(10);
}
}
void Key_Scan_Task(void *param)
{
while (1) {
// 按键扫描
vTaskDelay(20);
}
}
void LED_Blink_Task(void *param)
{
while (1) {
// LED 闪烁
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
vTaskDelay(500);
}
}
void Sensor_Task(void *param)
{
while (1) {
// 传感器读取
ReadSensorData();
vTaskDelay(100);
}
}
并发场景 2:数据采集与处理(生产者-消费者)
UML 活动图
● (Start)
│
──┼── Fork
│
┌─────┴─────┐
│ │
▼ ▼
┌────────┐ ┌────────┐
│Acquire │ │Process │
│ Data │ │ Data │
│ │ │ │
│[loop] │ │[loop] │
└────────┘ └────────┘
│ │
└─────┬─────┘
│
──┼── Join
│
◎ (End)
代码实现(队列同步)
c
QueueHandle_t dataQueue;
void main(void)
{
// 创建队列
dataQueue = xQueueCreate(10, sizeof(SensorData_t));
// Fork - 并行任务
xTaskCreate(DataAcquireTask, "Acquire", 256, NULL, 2, NULL);
xTaskCreate(DataProcessTask, "Process", 256, NULL, 1, NULL);
vTaskStartScheduler();
}
// 生产者任务
void DataAcquireTask(void *param)
{
SensorData_t data;
while (1) {
// 采集数据
data.temp = ReadTemperature();
data.humi = ReadHumidity();
data.timestamp = GetTick();
// 发送到队列
xQueueSend(dataQueue, &data, portMAX_DELAY);
vTaskDelay(100);
}
}
// 消费者任务
void DataProcessTask(void *param)
{
SensorData_t data;
while (1) {
// 从队列接收(等待数据)
if (xQueueReceive(dataQueue, &data, portMAX_DELAY) == pdTRUE) {
// 处理数据
FilterData(&data);
SaveToFlash(&data);
SendToCloud(&data);
}
}
}
并发场景 3:事件同步(Join 的实际应用)
需求描述
- 启动三个初始化任务
- 等待所有任务完成后才继续
UML 活动图
● (Start)
│
──┼── Fork
│
┌─────┼─────┬─────┐
│ │ │ │
▼ ▼ ▼ ▼
┌────┐ ┌────┐ ┌────┐ ┌────┐
│Init│ │Init│ │Init│ │Init│
│GPIO│ │UART│ │SPI │ │I2C │
└────┘ └────┘ └────┘ └────┘
│ │ │ │
└─────┼─────┴─────┘
│
──┼── Join(等待所有完成)
│
┌─────────┐
│Start App│
└─────────┘
│
◎
代码实现(EventGroup 同步)
c
#define GPIO_INIT_BIT (1 << 0)
#define UART_INIT_BIT (1 << 1)
#define SPI_INIT_BIT (1 << 2)
#define I2C_INIT_BIT (1 << 3)
EventGroupHandle_t initEventGroup;
void main(void)
{
// 创建事件组
initEventGroup = xEventGroupCreate();
// Fork - 并行初始化
xTaskCreate(GPIO_InitTask, "GPIO", 128, NULL, 1, NULL);
xTaskCreate(UART_InitTask, "UART", 128, NULL, 1, NULL);
xTaskCreate(SPI_InitTask, "SPI", 128, NULL, 1, NULL);
xTaskCreate(I2C_InitTask, "I2C", 128, NULL, 1, NULL);
// Join - 等待所有初始化完成
EventBits_t bits = xEventGroupWaitBits(
initEventGroup,
GPIO_INIT_BIT | UART_INIT_BIT | SPI_INIT_BIT | I2C_INIT_BIT,
pdFALSE,
pdTRUE, // 等待所有位都置位
portMAX_DELAY
);
if (bits == (GPIO_INIT_BIT | UART_INIT_BIT | SPI_INIT_BIT | I2C_INIT_BIT)) {
printf("All peripherals initialized!\n");
StartApplication();
}
}
void GPIO_InitTask(void *param)
{
HAL_GPIO_Init();
xEventGroupSetBits(initEventGroup, GPIO_INIT_BIT);
vTaskDelete(NULL); // 任务完成,删除自己
}
void UART_InitTask(void *param)
{
HAL_UART_Init(&huart1);
xEventGroupSetBits(initEventGroup, UART_INIT_BIT);
vTaskDelete(NULL);
}
// SPI_InitTask 和 I2C_InitTask 类似
五、判断与循环
Decision(判断)节点详解
基本用法
┌──────────┐
│Read Data │
└────┬─────┘
│
◇──┴──◇ [valid?]
Yes │ No
│
┌─────┴─────┐
│ │
▼ ▼
┌────────┐ ┌────────┐
│Process │ │Discard │
└────────┘ └────────┘
│ │
└─────┬─────┘
│
◇──┴──◇ Merge
│
┌────┴────┐
│Continue │
└─────────┘
代码对应
c
void HandleData(void)
{
uint8_t data = ReadData();
if (IsValid(data)) { // Decision
ProcessData(data);
} else {
DiscardData();
}
// Merge - 两个分支汇合
Continue();
}
多路分支
UML 表示
┌──────────┐
│Get State │
└────┬─────┘
│
◇──┴──◇
│
┌────────┼────────┬────────┐
│ │ │ │
[idle] [running] [error] [shutdown]
│ │ │ │
▼ ▼ ▼ ▼
┌────┐ ┌────┐ ┌────┐ ┌────┐
│Idle│ │Run │ │Fix │ │Stop│
└────┘ └────┘ └────┘ └────┘
│ │ │ │
└────────┼────────┴────────┘
│
◇──┴──◇ Merge
│
代码对应
c
switch (GetState()) { // Decision
case STATE_IDLE:
HandleIdle();
break;
case STATE_RUNNING:
HandleRunning();
break;
case STATE_ERROR:
HandleError();
break;
case STATE_SHUTDOWN:
HandleShutdown();
break;
}
// Merge - 继续后续流程
循环结构
Loop(循环)表示方式
方式 1:使用决策节点回退
┌──────────┐
┌──>│Read Byte │
│ └────┬─────┘
│ │
│ ◇──┴──◇ [complete?]
│ No │ Yes
│ │
└────────┘
│
┌────┴────┐
│Process │
└─────────┘
代码对应:
c
while (!IsComplete()) { // Loop 循环条件
ReadByte();
}
ProcessData();
方式 2:使用循环框标注
┌─ loop [count < 10] ────────────┐
│ │
│ ┌──────────┐ │
│ │Read Byte │ │
│ └────┬─────┘ │
│ │ │
│ ┌────┴────┐ │
│ │Store │ │
│ └─────────┘ │
│ │
└────────────────────────────────┘
│
┌────┴────┐
│Process │
└─────────┘
代码对应:
c
for (int i = 0; i < 10; i++) { // Loop
ReadByte();
Store();
}
ProcessData();
六、泳道(Swimlanes)
什么是泳道?
定义:按职责或组织单位划分的活动区域
类比:
- 游泳池的泳道 → 每个人在自己的道上游
- 活动图的泳道 → 每个角色/模块在自己的区域内工作
泳道的价值
✅ 明确职责边界
✅ 展示跨模块交互
✅ 便于理解复杂流程
✅ 适合业务流程建模
示例 1:订单处理流程(从图片1)
UML 活动图
┌──────────────┬─────────────────┬───────────────┐
│ Customer │ System │ Payment │
├──────────────┼─────────────────┼───────────────┤
│ │ │ │
│ │ ● Start │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │Receive Order│ │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
│ │ ──┼── Fork│ │
│ │ │ │ │
│ │ ┌────┴────┐ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌────┐ ┌────┐ │ │
│ │ │Fill│ │Send│ │ │
│ │ │Order │Inv.│ │ │
│ │ └──┬─┘ └──┬─┘ │ │
│ │ │ ◇ │ │ │
│ │ │[rush]│ │ │
│ │ ┌──┴───┐ │ │ │
│ │ │Overnight│ │ │
│ │ │ or │ │ │
│ │ │Regular │ │ │
│ │ └──┬───┘ │ │ │
│ │ │ ◇ │ │ │
│ │ └──┼───┘ │ │
│ │ ──┼── Join │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ │┌──────────┐ │
│ │ ││Receive │ │
│ │ ││Payment │ │
│ │ │└────┬─────┘ │
│ │ │ │ │ │
│ │ ──┼── Join │ │ │
│ │ │ │ │ │
│ │ ▼ │ │ │
│ │ ◎ End │ │ │
│ │ │ │ │
└──────────────┴─────────────────┴────┴──────────┘
PlantUML 代码
plantuml
@startuml OrderProcessing
|Customer|
start
|System|
:Receive Order;
fork
:Fill Order;
if (rush order?) then (yes)
:Arrange Overnight Delivery;
else (no)
:Arrange Regular Delivery;
endif
fork again
:Send Invoice;
end fork
|Payment|
:Receive Payment;
|System|
end
@enduml
示例 2:在线购物流程(从图片2)
流程描述
泳道:
- Customer(客户)
- System(系统)
- Payment System(支付系统)
- Database(数据库)
流程:
1. 客户搜索商品
2. 系统查询数据库
3. 客户浏览商品
4. 客户添加到购物车
5. 客户结账
6. 支付系统处理支付
7. 系统更新订单
8. 数据库保存记录
PlantUML 代码
plantuml
@startuml OnlineShopping
|Customer|
start
:Search Items;
|System|
:Query Database;
|Database|
:Return Results;
|Customer|
:Browse Items;
repeat
:View Item Details;
if (Add to Cart?) then (yes)
|System|
:Add to Shopping Cart;
else (no)
endif
|Customer|
repeat while (Continue Shopping?) is (yes)
->no;
:Proceed to Checkout;
|System|
:Calculate Total;
|Customer|
:Enter Payment Info;
|Payment System|
:Process Payment;
if (Payment Success?) then (yes)
|System|
:Create Order;
|Database|
:Save Order;
|System|
:Send Confirmation;
|Customer|
:Receive Confirmation;
stop
else (no)
|Customer|
:Payment Failed;
stop
endif
@enduml
七、完整实战案例
案例 1:STM32 系统初始化流程
场景描述
STM32 上电后的完整初始化序列
UML 活动图
● Start
│
┌─────────────┐
│Reset Handler│
└──────┬──────┘
│
┌──────┴──────┐
│SystemInit │
└──────┬──────┘
│
┌──────┴──────┐
│Clock Config │
└──────┬──────┘
│
┌──────┴──────┐
│GPIO Init │
└──────┬──────┘
│
──┼── Fork(并行初始化)
│
┌──────┼──────┬──────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌────┐ ┌────┐ ┌────┐ ┌────┐
│UART│ │SPI │ │I2C │ │ADC │
│Init│ │Init│ │Init│ │Init│
└────┘ └────┘ └────┘ └────┘
│ │ │ │
└──────┼──────┴──────────┘
│
──┼── Join
│
┌──────┴──────┐
│RTOS Init │
└──────┬──────┘
│
┌──────┴──────┐
│Create Tasks │
└──────┬──────┘
│
┌──────┴──────┐
│Start Scheduler
└──────┬──────┘
│
◎ End
代码实现
c
int main(void)
{
// Reset Handler
HAL_Init();
// SystemInit
SystemInit();
// Clock Config
SystemClock_Config();
// GPIO Init
MX_GPIO_Init();
// Fork - 并行初始化(实际上是顺序,但逻辑上独立)
MX_UART_Init();
MX_SPI_Init();
MX_I2C_Init();
MX_ADC_Init();
// Join - 初始化完成
// RTOS Init
osKernelInitialize();
// Create Tasks
CreateAllTasks();
// Start Scheduler
osKernelStart();
// 不会到达这里
while (1);
}
案例 2:UART 数据接收流程
场景描述
接收完整的数据帧,格式:[STX][LEN][DATA...][CRC][ETX]
UML 活动图
● Start
│
┌─────────────┐
│Wait for STX │◄────┐
└──────┬──────┘ │
│ │
◇──┴──◇ │
[rx==STX?] │
Yes│ No │
│ │
├────────────┘
│
┌──────┴──────┐
│Receive LEN │
└──────┬──────┘
│
◇──┴──◇
[len valid?]
Yes│ No
│ └─────────┐
│ │
┌─ loop [count<len] ┴────┐
│ │
│ ┌──────────┐ │
│ │Receive │ │
│ │Data Byte │ │
│ └──────────┘ │
│ │
└────────────────────────┘
│
┌──────┴──────┐
│Receive CRC │
└──────┬──────┘
│
◇──┴──◇
[CRC OK?]
Yes│ No
│ └─────────────┐
│ │
┌──────┴──────┐ │
│Receive ETX │ │
└──────┬──────┘ │
│ │
◇──┴──◇ │
[rx==ETX?] │
Yes│ No │
│ └─────────────┤
┌──────┴──────┐ │
│Process Frame│ │
└──────┬──────┘ │
│ │
◎ End │
│ │
└────────────────┘
[Error - Reset]
代码实现
c
typedef enum {
RX_STATE_WAIT_STX,
RX_STATE_GET_LEN,
RX_STATE_GET_DATA,
RX_STATE_GET_CRC,
RX_STATE_GET_ETX,
RX_STATE_PROCESS,
RX_STATE_ERROR
} RxState_t;
#define STX 0x02
#define ETX 0x03
void UART_ReceiveFrame(void)
{
static RxState_t state = RX_STATE_WAIT_STX;
static uint8_t buffer[256];
static uint8_t length = 0;
static uint8_t index = 0;
static uint8_t crc = 0;
uint8_t rxByte;
while (UART_Available()) {
rxByte = UART_ReadByte();
switch (state) {
case RX_STATE_WAIT_STX:
if (rxByte == STX) { // Decision
state = RX_STATE_GET_LEN;
index = 0;
crc = 0;
}
// else stay in this state
break;
case RX_STATE_GET_LEN:
length = rxByte;
if (length > 0 && length <= 256) { // Decision
crc += length;
state = RX_STATE_GET_DATA;
} else {
state = RX_STATE_ERROR;
}
break;
case RX_STATE_GET_DATA:
buffer[index++] = rxByte;
crc += rxByte;
if (index >= length) { // Loop condition
state = RX_STATE_GET_CRC;
}
break;
case RX_STATE_GET_CRC:
if (rxByte == crc) { // Decision
state = RX_STATE_GET_ETX;
} else {
state = RX_STATE_ERROR;
}
break;
case RX_STATE_GET_ETX:
if (rxByte == ETX) { // Decision
state = RX_STATE_PROCESS;
} else {
state = RX_STATE_ERROR;
}
break;
case RX_STATE_PROCESS:
ProcessFrame(buffer, length);
state = RX_STATE_WAIT_STX; // Reset
break;
case RX_STATE_ERROR:
// Log error
state = RX_STATE_WAIT_STX; // Reset
break;
}
}
}
案例 3:Modbus 主机请求流程
场景描述
Modbus 主机发送请求并等待响应,支持超时重试
UML 活动图
● Start
│
┌─────────────┐
│Build Request│
└──────┬──────┘
│
┌──────┴──────┐
│Add CRC │
└──────┬──────┘
│
┌──────┴──────┐
│Send Request │
└──────┬──────┘
│
┌──────┴──────┐
│Start Timer │
└──────┬──────┘
│
┌─────────────────┐
│Wait for Response│◄───┐
└──────┬──────────┘ │
│ │
◇──┴──◇ │
[response received?] │
Yes│ No │
│ │ │
│ ◇────────◇ │
│ [timeout?] │
│ Yes│ No │
│ │ └─────┘
│ │
│ ◇────────◇
│ [retry < 3?]
│ Yes│ No
│ │ │
│ │ └─────┐
┌──────┴──────┐ │ │
│Verify CRC │ │ │
└──────┬──────┘ │ │
│ │ │
◇──┴──◇ │ │
[CRC OK?] │ │
Yes│ No │ │
│ └───────┤ │
┌──────┴──────┐ │ │
│Parse Response│ │ │
└──────┬──────┘ │ │
│ │ │
◎ Success │ │
│ │
◎ Retry ◎ Timeout
代码实现
c
typedef enum {
MODBUS_SUCCESS,
MODBUS_TIMEOUT,
MODBUS_CRC_ERROR,
MODBUS_RETRY
} ModbusResult_t;
ModbusResult_t Modbus_SendRequest(uint8_t slaveAddr, uint8_t function,
uint16_t addr, uint16_t count)
{
uint8_t request[8];
uint8_t response[256];
uint16_t crc;
int retry = 0;
// Build Request
request[0] = slaveAddr;
request[1] = function;
request[2] = (addr >> 8) & 0xFF;
request[3] = addr & 0xFF;
request[4] = (count >> 8) & 0xFF;
request[5] = count & 0xFF;
// Add CRC
crc = CalculateCRC(request, 6);
request[6] = crc & 0xFF;
request[7] = (crc >> 8) & 0xFF;
while (retry < 3) { // Retry loop
// Send Request
UART_Transmit(request, 8);
// Start Timer
uint32_t startTime = HAL_GetTick();
uint32_t timeout = 1000; // 1 second
// Wait for Response
while (HAL_GetTick() - startTime < timeout) {
if (UART_DataAvailable()) { // Decision: response received?
int len = UART_Receive(response, sizeof(response));
// Verify CRC
uint16_t rxCRC = (response[len-1] << 8) | response[len-2];
uint16_t calcCRC = CalculateCRC(response, len - 2);
if (rxCRC == calcCRC) { // Decision: CRC OK?
// Parse Response
ParseResponse(response, len);
return MODBUS_SUCCESS;
} else {
return MODBUS_CRC_ERROR;
}
}
}
// Timeout - retry
retry++;
}
return MODBUS_TIMEOUT;
}
案例 4:文件读写流程
UML 活动图
● Start
│
┌─────────────┐
│Open File │
└──────┬──────┘
│
◇──┴──◇
[success?]
Yes│ No
│ └────────┐
┌──────┴──────┐ │
│Read Data │ │
└──────┬──────┘ │
│ │
┌──────┴──────┐ │
│Process Data │ │
└──────┬──────┘ │
│ │
┌──────┴──────┐ │
│Write Result │ │
└──────┬──────┘ │
│ │
┌──────┴──────┐ │
│Close File │ │
└──────┬──────┘ │
│ │
◎ Success │
│
◎ Error
代码实现
c
bool ProcessFile(const char *inputPath, const char *outputPath)
{
FILE *fin = NULL;
FILE *fout = NULL;
bool success = false;
// Open File
fin = fopen(inputPath, "rb");
if (fin == NULL) { // Decision
goto error; // Error path
}
fout = fopen(outputPath, "wb");
if (fout == NULL) { // Decision
goto error; // Error path
}
// Read Data
uint8_t buffer[1024];
size_t bytesRead;
while ((bytesRead = fread(buffer, 1, sizeof(buffer), fin)) > 0) {
// Process Data
ProcessBuffer(buffer, bytesRead);
// Write Result
fwrite(buffer, 1, bytesRead, fout);
}
success = true;
error:
// Close File
if (fin != NULL) {
fclose(fin);
}
if (fout != NULL) {
fclose(fout);
}
return success;
}
案例 5:ADC 多通道采样
UML 活动图
● Start
│
┌─────────────┐
│Config ADC │
└──────┬──────┘
│
──┼── Fork(并发采样)
│
┌──────┼──────┬──────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌────┐ ┌────┐ ┌────┐ ┌────┐
│CH0 │ │CH1 │ │CH2 │ │CH3 │
│Read│ │Read│ │Read│ │Read│
└────┘ └────┘ └────┘ └────┘
│ │ │ │
└──────┼──────┴──────────┘
│
──┼── Join
│
┌─────────────┐
│Convert to │
│Physical Unit│
└──────┬──────┘
│
┌──────┴──────┐
│Apply Filter │
└──────┬──────┘
│
┌──────┴──────┐
│Store Result │
└──────┬──────┘
│
◎ End
代码实现
c
typedef struct {
uint16_t ch0_raw;
uint16_t ch1_raw;
uint16_t ch2_raw;
uint16_t ch3_raw;
float ch0_voltage;
float ch1_voltage;
float ch2_voltage;
float ch3_voltage;
} ADC_Result_t;
ADC_Result_t ADC_ReadAllChannels(void)
{
ADC_Result_t result;
// Config ADC
HAL_ADC_Start(&hadc1);
// Fork - 并发采样(DMA模式)
// 实际上 DMA 自动按顺序采样多通道
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&result, 4);
// Wait for completion
while (HAL_ADC_PollForConversion(&hadc1, 100) != HAL_OK);
// Join - 采样完成
// Convert to Physical Unit
result.ch0_voltage = result.ch0_raw * 3.3f / 4096.0f;
result.ch1_voltage = result.ch1_raw * 3.3f / 4096.0f;
result.ch2_voltage = result.ch2_raw * 3.3f / 4096.0f;
result.ch3_voltage = result.ch3_raw * 3.3f / 4096.0f;
// Apply Filter
result.ch0_voltage = LowPassFilter(result.ch0_voltage, 0);
result.ch1_voltage = LowPassFilter(result.ch1_voltage, 1);
result.ch2_voltage = LowPassFilter(result.ch2_voltage, 2);
result.ch3_voltage = LowPassFilter(result.ch3_voltage, 3);
// Store Result
SaveADC_Result(&result);
return result;
}
案例 6:HTTP 请求处理流程(Web 服务器)
UML 活动图
┌─────────────┬────────────────┬──────────────┐
│ Client │ Server │ Database │
├─────────────┼────────────────┼──────────────┤
│ │ │ │
│ ● Start │ │ │
│ │ │ │ │
│ ┌───┴───┐ │ │ │
│ │Send │ │ │ │
│ │Request│───┼───────────────>│ │
│ └───────┘ │ │ │
│ │ ┌────────────┐ │ │
│ │ │Parse │ │ │
│ │ │Request │ │ │
│ │ └─────┬──────┘ │ │
│ │ │ │ │
│ │ ◇──┴──◇ │ │
│ │ [method?] │ │
│ │ GET│ POST│ │ │
│ │ │ │ │ │
│ │ ┌─┴─┐ ┌─┴─┐ │ │
│ │ │GET│ │POST│ │ │
│ │ └─┬─┘ └─┬─┘ │ │
│ │ └───┬─┘ │ │
│ │ │ │ │
│ │ ◇────┴────◇ │ │
│ │ [auth OK?] │ │
│ │ Yes│ No │ │
│ │ │ └───────────────┐ │
│ │ │ │ │
│ │ ┌────┴────┐ │ │
│ │ │Query │ │ │
│ │ │Database │────────────>│ │
│ │ └────┬────┘ │ │
│ │ │ ┌──────────┐ │ │
│ │ │<─│Return │ │ │
│ │ │ │Result │ │ │
│ │ │ └──────────┘ │ │
│ │ ┌────┴────┐ │ │
│ │ │Format │ │ │
│ │ │Response │ │ │
│ │ └────┬────┘ │ │
│ │ │ │ │
│ │ └──┬───────────────┘ │
│ │ │ │
│ │ ◇──────┴──────◇ │
│ │ [200 or 401] │
│ │ │ │
│ ┌───────┐ │ ┌───────┴───────┐ │
│ │Receive│<──┼───│Send Response │ │
│ │Response │ └───────────────┘ │
│ └───────┘ │ │
│ │ │ │
│ ◎ End │ │
│ │ │
└─────────────┴───────────────────────────────┘
案例 7:智能家居场景控制
UML 活动图
● Start
│
┌─────────────┐
│Detect │
│"Coming Home"│
└──────┬──────┘
│
──┼── Fork(并行控制)
│
┌──────┼──────┬──────────┬──────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐
│Turn│ │Turn│ │Turn │ │Set │ │Start│
│On │ │On │ │On │ │AC │ │Music│
│Lights │TV │ │Curtains │22°C│ │ │
└────┘ └────┘ └────┘ └────┘ └────┘
│ │ │ │ │
└──────┼──────┴──────────┴──────────┘
│
──┼── Join
│
┌──────┴──────┐
│Send │
│Notification │
└──────┬──────┘
│
◎ End
代码实现
c
void SmartHome_ComingHomeScene(void)
{
EventGroupHandle_t sceneGroup;
sceneGroup = xEventGroupCreate();
// Fork - 并行控制
xTaskCreate(TurnOnLights, "Lights", 128, sceneGroup, 1, NULL);
xTaskCreate(TurnOnTV, "TV", 128, sceneGroup, 1, NULL);
xTaskCreate(OpenCurtains, "Curtains",128, sceneGroup, 1, NULL);
xTaskCreate(SetAC_22C, "AC", 128, sceneGroup, 1, NULL);
xTaskCreate(StartMusic, "Music", 128, sceneGroup, 1, NULL);
// Join - 等待所有动作完成
xEventGroupWaitBits(
sceneGroup,
LIGHTS_BIT | TV_BIT | CURTAINS_BIT | AC_BIT | MUSIC_BIT,
pdTRUE,
pdTRUE,
pdMS_TO_TICKS(5000)
);
// Send Notification
SendNotification("Welcome home! Scene activated.");
vEventGroupDelete(sceneGroup);
}
void TurnOnLights(void *param)
{
EventGroupHandle_t group = (EventGroupHandle_t)param;
SmartLight_TurnOn(ALL_ROOMS);
xEventGroupSetBits(group, LIGHTS_BIT);
vTaskDelete(NULL);
}
// 其他任务类似实现...
案例 8:固件升级流程(OTA)
UML 活动图
● Start
│
┌─────────────┐
│Check Update │
└──────┬──────┘
│
◇──┴──◇
[available?]
Yes│ No
│ └─────────┐
┌──────┴──────┐ │
│Download │ │
│Firmware │ │
└──────┬──────┘ │
│ │
◇──┴──◇ │
[complete?] │
Yes│ No │
│ └────────┤
┌──────┴──────┐ │
│Verify │ │
│Signature │ │
└──────┬──────┘ │
│ │
◇──┴──◇ │
[valid?] │
Yes│ No │
│ └───────┤
┌──────┴──────┐ │
│Backup │ │
│Current FW │ │
└──────┬──────┘ │
│ │
┌──────┴──────┐ │
│Flash New FW │ │
└──────┬──────┘ │
│ │
◇──┴──◇ │
[success?] │
Yes│ No │
│ └──────┤
┌──────┴──────┐ │
│Reboot │ │
└──────┬──────┘ │
│ │
◎ Success │
│
◎ Failed
代码实现
c
typedef enum {
OTA_SUCCESS,
OTA_NO_UPDATE,
OTA_DOWNLOAD_FAILED,
OTA_VERIFY_FAILED,
OTA_FLASH_FAILED
} OTA_Result_t;
OTA_Result_t OTA_UpdateFirmware(void)
{
FirmwareInfo_t fwInfo;
// Check Update
if (!OTA_CheckUpdate(&fwInfo)) { // Decision
return OTA_NO_UPDATE;
}
// Download Firmware
if (!OTA_Download(fwInfo.url, fwInfo.size)) { // Decision
return OTA_DOWNLOAD_FAILED;
}
// Verify Signature
if (!OTA_VerifySignature()) { // Decision
return OTA_VERIFY_FAILED;
}
// Backup Current FW
OTA_BackupCurrentFirmware();
// Flash New FW
if (!OTA_FlashFirmware()) { // Decision
// Restore backup
OTA_RestoreBackup();
return OTA_FLASH_FAILED;
}
// Reboot
printf("Firmware updated successfully. Rebooting...\n");
HAL_Delay(1000);
NVIC_SystemReset();
return OTA_SUCCESS;
}
案例 9:数据加密流程
UML 活动图
● Start
│
┌─────────────┐
│Read Plain │
│Text │
└──────┬──────┘
│
◇──┴──◇
[data valid?]
Yes│ No
│ └────────┐
┌──────┴──────┐ │
│Generate IV │ │
└──────┬──────┘ │
│ │
┌──────┴──────┐ │
│Load Key │ │
└──────┬──────┘ │
│ │
┌─ loop [each block] ┬───┐
│ │ │
│ ┌──────────┐ │ │
│ │Encrypt │ │ │
│ │Block │ │ │
│ └──────────┘ │ │
│ │ │
└────────────────────┴───┘
│
┌──────┴──────┐
│Add HMAC │
└──────┬──────┘
│
┌──────┴──────┐
│Write │
│Ciphertext │
└──────┬──────┘
│
◎ End │
│
◎ Error
案例 10:数据采集与上报(完整流程)
UML 活动图
● Start
│
┌─────────────┐
│Initialize │
│Sensors │
└──────┬──────┘
│
──┼── Fork
│
┌──────┴──────────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│Acquisition Loop │ │Upload Loop │
│ │ │ │
│ ┌─loop[forever]┐│ │ ┌─loop[forever]┐│
│ │ ││ │ │ ││
│ │ ┌──────────┐ ││ │ │ ┌──────────┐ ││
│ │ │Read │ ││ │ │ │Wait Queue│ ││
│ │ │Sensor │ ││ │ │ └────┬─────┘ ││
│ │ └────┬─────┘ ││ │ │ │ ││
│ │ │ ││ │ │ ┌────┴─────┐ ││
│ │ ┌────┴─────┐ ││ │ │ │Build │ ││
│ │ │Filter │ ││ │ │ │Packet │ ││
│ │ └────┬─────┘ ││ │ │ └────┬─────┘ ││
│ │ │ ││ │ │ │ ││
│ │ ┌────┴─────┐ ││ │ │ ┌────┴─────┐ ││
│ │ │Store │ ││ │ │ │Send to │ ││
│ │ │to Queue │ ││ │ │ │Cloud │ ││
│ │ └────┬─────┘ ││ │ │ └────┬─────┘ ││
│ │ │ ││ │ │ │ ││
│ │ ┌────┴─────┐ ││ │ │ ◇──┴──◇ ││
│ │ │Delay │ ││ │ │ [success?] ││
│ │ │100ms │ ││ │ │ Yes│ No ││
│ │ └──────────┘ ││ │ │ │ └───┐││
│ │ ││ │ │ ┌────┴─────┐││
│ └──────────────┘│ │ │ │Log │││
│ │ │ │ │Success │││
│ │ │ │ └──────────┘││
│ │ │ │ ││
│ │ │ └──────────────┘│
└─────────────────┘ └─────────────────┘
代码实现
c
QueueHandle_t dataQueue;
void main(void)
{
// Initialize Sensors
Sensor_Init();
// Create queue
dataQueue = xQueueCreate(50, sizeof(SensorData_t));
// Fork - 并发任务
xTaskCreate(AcquisitionTask, "Acquire", 256, NULL, 2, NULL);
xTaskCreate(UploadTask, "Upload", 256, NULL, 1, NULL);
vTaskStartScheduler();
}
void AcquisitionTask(void *param)
{
SensorData_t data;
while (1) { // Loop forever
// Read Sensor
data.temperature = ReadTemperature();
data.humidity = ReadHumidity();
data.timestamp = GetTick();
// Filter
data.temperature = LowPassFilter(data.temperature);
// Store to Queue
if (xQueueSend(dataQueue, &data, 0) != pdTRUE) {
// Queue full, data lost
LogWarning("Data queue full");
}
// Delay 100ms
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void UploadTask(void *param)
{
SensorData_t data;
uint8_t packet[128];
while (1) { // Loop forever
// Wait Queue
if (xQueueReceive(dataQueue, &data, portMAX_DELAY) == pdTRUE) {
// Build Packet
int len = BuildPacket(packet, &data);
// Send to Cloud
bool success = SendToCloud(packet, len);
// Decision: success?
if (success) {
LogSuccess("Data uploaded");
} else {
LogError("Upload failed");
// Could add retry logic here
}
}
}
}
八、PlantUML 绘制指南
基本语法
plantuml
@startuml
start
:Action 1;
:Action 2;
stop
@enduml
常用元素
| 元素 | 语法 | 说明 |
|---|---|---|
| 开始 | start |
初始节点 |
| 结束 | stop |
终止节点 |
| 动作 | :Action; |
圆角矩形 |
| 判断 | if (condition) then (yes) |
菱形 |
| 循环 | repeat / while |
循环结构 |
| Fork | fork / fork again |
并发开始 |
| Join | end fork |
并发结束 |
| 泳道 | ` | Swimlane |
| 注释 | note right |
说明文字 |
判断语句
plantuml
@startuml
start
:Read Data;
if (Data Valid?) then (yes)
:Process Data;
else (no)
:Discard Data;
endif
:Continue;
stop
@enduml
循环语句
plantuml
@startuml
start
repeat
:Read Byte;
:Store Byte;
repeat while (More Data?) is (yes)
->no;
:Process Buffer;
stop
@enduml
并发结构
plantuml
@startuml
start
:Initialize;
fork
:Task 1;
fork again
:Task 2;
fork again
:Task 3;
end fork
:Continue;
stop
@enduml
泳道
plantuml
@startuml
|Customer|
start
:Place Order;
|System|
:Process Order;
fork
|Warehouse|
:Prepare Package;
fork again
|Finance|
:Process Payment;
end fork
|Customer|
:Receive Order;
stop
@enduml
完整示例
plantuml
@startuml CompleteExample
title Device Initialization Flow
start
:System Reset;
:Clock Config;
fork
:GPIO Init;
fork again
:UART Init;
fork again
:SPI Init;
fork again
:I2C Init;
end fork
:RTOS Init;
if (All Peripherals OK?) then (yes)
:Start Application;
stop
else (no)
:Enter Error Handler;
stop
endif
@enduml
九、最佳实践与避坑指南
1. 活动图的正确使用场景
✅ 应该使用活动图:
- 复杂函数的算法流程
- 初始化序列
- 数据处理流程
- 业务流程
- 协议处理流程
❌ 不应该使用活动图:
- 简单的 if-else(不值得画)
- 对象间交互(用时序图)
- 长期运行的状态(用状态图)
- 类的结构(用类图)
2. 活动图的粒度控制
❌ 粒度太细:
每一行代码都画一个节点
→ 图太复杂,失去意义
✅ 合适粒度:
一个节点对应一个函数调用或一个有意义的步骤
→ 清晰表达主要流程
❌ 粒度太粗:
一个节点包含大量逻辑
→ 无法看出细节
3. 命名规范
✅ 好的命名:
- Initialize UART(动宾结构)
- Read Sensor Data
- Calculate Checksum
- Send Response
❌ 不好的命名:
- UART(只有名词)
- Data(太抽象)
- Process(不明确做什么)
- Step1(无意义)
4. 并发的正确表达
✅ 正确:
使用 Fork/Join 明确表达并发关系
→ 清楚哪些是并行的,哪些需要同步
❌ 错误:
所有流程都是串行
→ 无法表达真实的并发逻辑
5. 决策节点的使用
✅ 正确:
- 每个分支都标注条件
- 有 Merge 节点合并分支
- 覆盖所有可能的情况
❌ 错误:
- 分支没有标注条件
- 分支后没有合并
- 遗漏某些情况
6. 循环的表达
✅ 清晰的循环:
- 明确循环条件
- 有退出机制
- 循环体清晰
❌ 不清楚的循环:
- 没有退出条件
- 可能的死循环
- 循环逻辑混乱
7. 活动图与代码的对应
✅ 好的实践:
1. 先画活动图
2. 根据图写代码
3. 代码中添加注释引用图中的节点
4. 代码变动时同步更新图
❌ 错误做法:
- 图和代码不一致
- 画完图就不管了
- 图和代码完全对应不上
8. 错误处理
✅ 完整的错误处理:
- 每个可能失败的点都有判断
- 有明确的错误处理路径
- 错误情况下的资源清理
❌ 缺失错误处理:
- 只画正常流程
- 忽略异常情况
- 没有清理资源
十、总结
活动图的核心价值
1. 设计工具:把复杂流程理清楚
2. 沟通工具:团队统一理解
3. 文档工具:维护和交接
4. 调试工具:对照检查逻辑
记忆口诀
算法用活动图,逻辑用状态图
自己怎么做 → 活动图
彼此怎么说 → 时序图
现在干什么 → 状态图
能力自检
你是否能做到:
- 识别适合用活动图的场景
- 正确使用 Fork/Join 表达并发
- 用决策节点和循环表达控制流
- 使用泳道组织多模块流程
- 将活动图翻译成代码
- 保持活动图与代码一致
- 使用 PlantUML 快速绘图
如果能做到 5 条以上,你已经掌握了活动图的核心技能!
附录:参考资源
- UML 2.5 Activity Diagram Specification
- PlantUML 官网:https://plantuml.com
- 《UML Distilled》- Martin Fowler
- 《Real-Time UML》- Bruce Powel Douglass
记住:活动图是设计算法和流程的最佳工具,熟练使用能让你的代码逻辑更清晰!