6.3 软件升级与存储
📚 本文内容摘自本人的开源书《从沙子到车辙 - 一个工程师的理解》
🔗 在线阅读/下载:from-sand-to-ruts
bash
git clone https://github.com/Lularible/from-sand-to-ruts
⭐ 如果对您有帮助,欢迎 Star 支持,也欢迎通过 GitHub Issues 交流讨论。
你给一百万个ECU发了一封邮件
你坐在办公室。屏幕上是OTA后台界面。你选择了一个固件包------v2.3.1,修复了制动能量回收模块的一个偶发性迟滞bug。目标范围:全球97万辆搭载该ECU的车型。
你点了"发布"。
现在有97万个接收方,分布在全球各地的道路上。有的在德国无限速高速上以200km/h行驶。有的在挪威零下25度的车库里熄火休眠。有的在印度乡村道路上颠簸,蜂窝信号只有一格。有的车已经报废了------但ECU可能还在上电。
你必须确保------不是"尽量确保",是"必须确保"------这97万个ECU每一个都不会变砖。
这就是OTA(Over-The-Air)。这不是"发个文件过去"。OTA是分布式系统工程的极端形态:在不可靠的网络通道上、对可能永不返厂的目标设备、执行不可逆的固件修改。
A/B分区的智慧:永远留一条退路
OTA的第一个核心问题不是传输。是安全网。
如果把新固件直接覆盖写入正在运行的旧固件------写到一半断电,车变砖。新固件有未知bug------无法回退。签名验证失败------但旧固件已经被覆盖了。
所以不能原地升级。
A/B分区是最优雅的解法。Flash被分成两个大小相等的独立分区------A区和B区。当前固件运行在A区。OTA下载的新固件全部写入B区。A区在整个下载和验证过程中从未被触碰------它完好地保存着你开始升级之前那一刻的完整系统状态。
下载完成后,系统验证B区固件的签名和完整性。验证通过后,在一个非易失标志中写"下次启动请选择B区"。然后软复位。
上电。HSM先启动。安全启动检查启动标志------"目标:B区"。HSM验证B区固件签名。通过------B区启动。B区固件的初始化代码在运行后前30秒自检------检查关键外设响应、所有SWC初始化状态、RAM和Flash的ECC。一切正常------确认新固件启动成功。B区成为新的"运行区"。
如果B区启动失败------自检超时,WDOG咬死复位。HSM再次检查启动标志,看到尝试次数尚未耗尽。再试。重试三次后仍失败------HSM强制把启动标志切回A区,复位。A区从未被碰过------车辆照常运行。驾驶员甚至不知道发生过一次安静的失败回退。
A/B分区的精髓:任何时刻,至少有一个完整可启动的固件分区存在,且这个分区不被正在进行的升级操作触碰。
启动引导器:读到标志,跳到正确的分区
A/B 分区的控制核心是一个微小的启动引导器(Bootloader)------它在这个固件升级的宇宙里是唯一"不参与轮换"的代码。它永远放在 Flash 的最开头,不随 A/B 切换而改变。
c
/* bootloader.c --- A/B分区启动引导器(简化) */
#define FLASH_PARTITION_A 0x08010000 /* A区起始地址 */
#define FLASH_PARTITION_B 0x08080000 /* B区起始地址 */
#define BOOT_FLAG_ADDR 0x0800F800 /* 启动标志所在扇区 */
typedef enum {
BOOT_TARGET_A = 0xAA,
BOOT_TARGET_B = 0xBB,
} BootTarget_t;
typedef struct {
BootTarget_t target; /* 目标分区 */
uint8_t attempt; /* 当前分区的启动尝试次数 */
uint8_t max_attempt; /* 最大尝试次数(通常3次) */
uint32_t magic; /* 魔数:0xB007B007 表示标志有效 */
} BootFlag_t;
/* 从Flash读取启动标志 */
static BootFlag_t boot_read_flags(void)
{
BootFlag_t flags;
memcpy(&flags, (void *)BOOT_FLAG_ADDR, sizeof(BootFlag_t));
/* 检查魔数------如果无效,要么是首次上电,要么是Flash损坏 */
if (flags.magic != 0xB007B007) {
flags.target = BOOT_TARGET_A;
flags.attempt = 0;
flags.max_attempt = 3;
flags.magic = 0xB007B007;
boot_write_flags(&flags); /* 初始化标志 */
}
return flags;
}
/* 向Flash写入启动标志------需要先擦除扇区 */
static int boot_write_flags(const BootFlag_t *flags)
{
/* Flash擦除------必须整个扇区 */
flash_erase_sector(BOOT_FLAG_ADDR);
/* Flash编程------按字写入 */
flash_write_words(BOOT_FLAG_ADDR,
(const uint32_t *)flags,
sizeof(BootFlag_t) / 4);
/* 验证写入 */
BootFlag_t verify;
memcpy(&verify, (void *)BOOT_FLAG_ADDR, sizeof(BootFlag_t));
return (memcmp(flags, &verify, sizeof(BootFlag_t)) == 0) ? 0 : -1;
}
/* 主入口------芯片上电后第一个执行的C函数 */
void bootloader_main(void)
{
BootFlag_t flags;
void (*app_entry)(void);
uint32_t app_start;
/* 基本硬件初始化:时钟、WDOG、HSM接口 */
system_clock_init();
watchdog_init();
/* 读取启动标志------不调用任何复杂函数 */
flags = boot_read_flags();
/* 确定目标分区地址 */
if (flags.target == BOOT_TARGET_B) {
app_start = FLASH_PARTITION_B;
} else {
app_start = FLASH_PARTITION_A;
}
/* 递增启动尝试计数器 */
flags.attempt++;
if (flags.attempt > flags.max_attempt) {
/* 已达最大尝试次数------切换到备选分区 */
if (flags.target == BOOT_TARGET_B) {
flags.target = BOOT_TARGET_A;
app_start = FLASH_PARTITION_A;
} else {
/* A区也失败了------没有地方回退了 */
/* 进入安全模式:只跑WDOG循环,等待诊断仪 */
flags.target = BOOT_TARGET_A;
app_start = FLASH_PARTITION_A;
}
flags.attempt = 1; /* 给新分区一次机会 */
}
boot_write_flags(&flags);
/* 安全启动验证------交给HSM */
if (hsm_secure_boot_verify(app_start) != HSM_BOOT_OK) {
/* 签名验证失败------固件被篡改或损坏 */
if (flags.target == BOOT_TARGET_B) {
/* 回退到A区 */
flags.target = BOOT_TARGET_A;
flags.attempt = 1;
boot_write_flags(&flags);
app_start = FLASH_PARTITION_A;
if (hsm_secure_boot_verify(app_start) != HSM_BOOT_OK) {
/* A区签名也无效------无法启动 */
while (1) { watchdog_kick(); }
}
}
}
/* 获取应用入口地址------通常在向量表的第二项 */
uint32_t *vector_table = (uint32_t *)app_start;
uint32_t stack_ptr = vector_table[0]; /* 初始SP */
app_entry = (void (*)(void))vector_table[1]; /* Reset_Handler */
/* 设置主栈指针,关闭所有中断,跳转到应用 */
__set_MSP(stack_ptr);
__disable_irq();
/* 启动成功------清除尝试计数器 */
flags.attempt = 0;
boot_write_flags(&flags);
/* 跳转到应用------这一跳之后,bootloader永远不会再运行
* (直到下次复位) */
app_entry();
/* 应用绝不应该返回 */
while (1) { watchdog_kick(); }
}
Bootloader 的代码量非常小------通常小于 4KB。它不参与 A/B 轮换------始终保持在 Flash 开头的一个独立扇区里。它的逻辑极其简单:读标志、选分区、验签名、跳转。简单的逻辑意味着更少的 bug------而 bootloader 里的任何一个 bug 都可能让 ECU 永远无法启动。
OTA下载:把200MB切成小块
OTA 固件包通常 200+ MB。你不能一次性下载------蜂窝网络可能在任何时刻中断。必须分块传输。
c
/* OTA_StateMachine.c --- OTA升级状态机(简化) */
#define CHUNK_SIZE 4096 /* 每块4KB------匹配Flash页编程粒度 */
#define HASH_SIZE 32 /* SHA-256 哈希值长度 */
typedef enum {
OTA_IDLE, /* 空闲------等待开始 */
OTA_DOWNLOADING, /* 下载中------接收chunk */
OTA_VERIFYING, /* 验证中------校验完整性和签名 */
OTA_ACTIVATING, /* 激活中------写启动标志 */
OTA_ACTIVE /* 升级完成------新固件已生效 */
} OTA_State_t;
typedef struct {
OTA_State_t state;
uint32_t total_size; /* 总固件大小 */
uint32_t received_size; /* 已接收大小 */
uint32_t current_chunk; /* 当前chunk编号 */
uint8_t expected_hash[HASH_SIZE]; /* 整体SHA-256期望值 */
uint8_t chunk_hash[HASH_SIZE]; /* 当前chunk的SHA-256 */
uint32_t target_partition; /* 目标分区起始地址 */
uint32_t retry_count; /* chunk重试次数 */
} OTA_Context_t;
static OTA_Context_t g_ota_ctx;
/* OTA状态机主循环------由OTA任务周期性调用 */
void OTA_StateMachine_Run(void)
{
switch (g_ota_ctx.state) {
case OTA_IDLE:
/* 等待OTA请求------诊断仪或OTA客户端触发 */
break;
case OTA_DOWNLOADING:
/* 接收下一个chunk */
if (g_ota_ctx.received_size >= g_ota_ctx.total_size) {
/* 所有chunk已下载------进入验证阶段 */
g_ota_ctx.state = OTA_VERIFYING;
}
break;
case OTA_VERIFYING: {
/* 对整个固件映像做SHA-256 */
uint8_t full_hash[HASH_SIZE];
sha256_calculate(
(const uint8_t *)g_ota_ctx.target_partition,
g_ota_ctx.total_size,
full_hash);
if (memcmp(full_hash, g_ota_ctx.expected_hash, HASH_SIZE) == 0) {
/* SHA-256通过------交给HSM验证签名 */
if (hsm_verify_signature(g_ota_ctx.target_partition) == 0) {
g_ota_ctx.state = OTA_ACTIVATING;
} else {
/* 签名无效------OTA失败,报告DTC */
ota_report_error(OTA_ERR_SIGNATURE);
g_ota_ctx.state = OTA_IDLE;
}
} else {
/* SHA-256不匹配------固件损坏 */
ota_report_error(OTA_ERR_CHECKSUM);
g_ota_ctx.state = OTA_IDLE;
}
break;
}
case OTA_ACTIVATING: {
/* 更新启动标志------下一次复位后bootloader将启动新分区 */
BootFlag_t flags = boot_read_flags();
flags.target = (g_ota_ctx.target_partition == FLASH_PARTITION_B)
? BOOT_TARGET_B : BOOT_TARGET_A;
flags.attempt = 0;
boot_write_flags(&flags);
g_ota_ctx.state = OTA_ACTIVE;
/* 通知OTA客户端:升级成功,请求复位 */
ota_send_response(OTA_RESP_READY_TO_RESET);
break;
}
case OTA_ACTIVE:
/* ECU在下次复位后才会实际运行新固件 */
/* 诊断仪或OTA客户端触发ECU Reset (0x11 0x01) */
break;
}
}
/* 当收到一个chunk时被调用 */
int OTA_ReceiveChunk(uint32_t chunk_num, const uint8_t *data, uint32_t len)
{
if (g_ota_ctx.state != OTA_DOWNLOADING) {
return OTA_ERR_WRONG_STATE;
}
/* 验证chunk的SHA-256 */
uint8_t computed_hash[HASH_SIZE];
sha256_calculate(data, len, computed_hash);
if (memcmp(computed_hash, g_ota_ctx.chunk_hash, HASH_SIZE) != 0) {
/* chunk校验失败------请求重传 */
g_ota_ctx.retry_count++;
if (g_ota_ctx.retry_count > 3) {
ota_report_error(OTA_ERR_CHUNK_HASH);
g_ota_ctx.state = OTA_IDLE;
return OTA_ERR_CHUNK_HASH;
}
return OTA_ERR_RETRY;
}
g_ota_ctx.retry_count = 0;
/* 写入Flash------目标分区 + 偏移 */
uint32_t flash_addr = g_ota_ctx.target_partition +
(chunk_num * CHUNK_SIZE);
/* Flash写入:对于非页对齐的写入,需要读-改-写 */
flash_program_page(flash_addr, data, len);
/* 验证写入 */
if (memcmp((void *)flash_addr, data, len) != 0) {
return OTA_ERR_FLASH_WRITE_VERIFY;
}
g_ota_ctx.received_size += len;
g_ota_ctx.current_chunk = chunk_num;
/* 存储进度------如果掉电,从这里继续 */
nvm_save_ota_progress(chunk_num, g_ota_ctx.received_size);
return OTA_OK;
}
/* 掉电后恢复------从上次保存的chunk继续 */
void OTA_ResumeAfterPowerLoss(void)
{
uint32_t last_chunk;
uint32_t received;
if (nvm_load_ota_progress(&last_chunk, &received) == 0) {
g_ota_ctx.state = OTA_DOWNLOADING;
g_ota_ctx.current_chunk = last_chunk;
g_ota_ctx.received_size = received;
/* 请求服务器从last_chunk+1开始继续发送 */
ota_request_resume(last_chunk + 1);
}
}
OTA 状态机的代码量比 bootloader 大得多------但它仍然遵循一个清晰的原则:在不可靠的网络上可靠地传输数据。 每个 chunk 独立做 SHA-256 校验------一个 chunk 校验失败不会影响之前已确认的 chunk。掉电后可以从上次保存的进度恢复,不需要从头开始。SHA-256 和签名验证都交给 HSM------主 CPU 不做密码运算。
Flash的物理铁律:为什么需要A/B
说到这里一个自然的问题浮现:为什么不能像手机升级 App 一样,把新固件下载下来、校验一下、然后覆盖安装?为什么需要两倍空间?
因为 Flash 有一个物理铁律。
Flash不能原地改写。 你必须先擦除整个扇区------通常 4KB 到 64KB,耗时几十到几百毫秒。擦除操作把扇区里所有位统一恢复为 1(0xFF)。然后你再编程写入新数据------每次几十微秒,把需要变成 0 的位翻成 0。
Flash 的物理决定了它只能把位从 1 翻到 0,不能从 0 翻回 1。只有扇区擦除能把整个扇区统一复位为全 1。而擦除的粒度是扇区------你改一个字,整个扇区都得擦。
如果你试图原地升级:读出当前扇区内容→备份到 RAM→擦除整个扇区→把新数据写进去。如果擦除之后、写入之前断电了------扇区是擦完的全 0xFF 状态。所有代码没了。变砖。
A/B分区解决的就是这个:升级操作永远在"不在运行的"分区上执行。即使 B 区在升级中途断电、留下一片半擦除的垃圾------没关系。A 区完好。下次启动跑 A 区。
这里还有一个工程微妙点:擦除比编程慢一千倍。 擦除一个 64KB 扇区可能需要 200ms。在这 200ms 窗口内,电源必须稳定。而汽车 ECU 的电源环境并不总是稳定------启动电机的电压塌陷、发电机的电压脉冲、保险丝熔断前的毛刺------这些都可能在 200ms 窗口内出现。
所以一个完整的 Flash 写入方案,必须同时处理两层防护:软件层面 的掉电保护------写前备份扇区、脏标记机制;硬件层面的电压监控------LVD(低电压检测)电路在电源掉到安全阈值以下时提前触发 NMI 中断,在电压彻底崩溃前抢出几十微秒完成收尾。
穿透:Flash擦除的本质------Fowler-Nordheim隧穿
Flash擦除的本质是 Fowler-Nordheim 隧穿------电子在强电场下穿过氧化层。
NOR Flash 的每一个存储单元由一个浮栅晶体管组成。浮栅------一层被二氧化硅绝缘层包围的多晶硅------电隔离于外部世界。电子一旦进入浮栅,就无处可去------除非你施加一个足够强的电场让它们量子隧穿出去。
擦除操作:Flash 控制器启动内部电荷泵,把 VDD(3.3V 或 5V)升压到约 -12V 或 +12V 施加在控制栅和源极之间。这个强电场(约 10 MV/cm)使得浮栅中的电子通过 Fowler-Nordheim 隧穿穿过二氧化硅势垒,回到沟道。浮栅中的电子被抽空------晶体管的阈值电压恢复为擦除态------读取为逻辑 1。
编程操作:Flash 控制器在漏极和源极之间施加约 5V 电压,控制栅施加约 10V 编程电压。沟道中的电子在横向电场下加速,获得足够能量的"热电子"在纵向电场作用下越过二氧化硅势垒,被注入浮栅。浮栅中的负电荷改变了晶体管的阈值电压------读取为逻辑 0。
从你指尖的 HTTP 请求,到 NOR Flash 浮栅中被困住的电子。 OTA 依赖的是整个物理栈------从 CDN 边缘节点到基站射频,到蜂窝调制解调器,到 SPI 总线,到 Flash 控制器的电荷泵,到二氧化硅层中的量子隧穿。
这就是"穿透"的意义。你不是只看见"文件传过去了"。你看见了从云端到浮栅的完整因果链。
磨损均衡:Flash不是永生的
Flash 有一个隐藏的寿命限制:每个扇区只能承受有限次的擦除/编程循环。
SLC NAND Flash 约 100,000 次。MLC NAND 约 3,000-10,000 次。汽车级 NOR Flash 约 100,000 次。超过这个次数后,氧化层中积累的陷阱电荷使隧穿效率下降------擦除越来越慢,编程越来越不稳定,数据保持时间越来越短。
如果一个 ECU 每秒记录一次 DTC 到 Flash------每次都要擦除同一个扇区------10 万秒 = 约 28 小时。一天多一点,那个扇区就报废了。
磨损均衡(Wear Leveling) 把写入分布到多个扇区上。不总是在同一个物理位置写------每次写到一个"新"的擦除过的扇区上,旧扇区标记为"脏"等待擦除回收。一个 FTL(Flash Translation Layer)维护逻辑地址到物理地址的映射------逻辑块 0 这次写在物理扇区 7,下次写在物理扇区 23,再下次写在物理扇区 41。
c
/* 简化的磨损均衡算法------每次都找擦除次数最少的空闲扇区 */
uint32_t wear_leveling_allocate(void)
{
uint32_t best_sector = INVALID_SECTOR;
uint32_t min_erase_count = 0xFFFFFFFF;
for (uint32_t i = 0; i < TOTAL_SECTORS; i++) {
if (sectors[i].state == SECTOR_FREE &&
sectors[i].erase_count < min_erase_count) {
min_erase_count = sectors[i].erase_count;
best_sector = i;
}
}
if (best_sector == INVALID_SECTOR) {
/* 没有空闲扇区------触发垃圾回收 */
garbage_collection();
return wear_leveling_allocate(); /* 重试 */
}
sectors[best_sector].state = SECTOR_ALLOCATED;
return best_sector;
}
磨损均衡不是 OTA 的事------OTA 是整块分区擦除重写,寿命不是主要担心。磨损均衡是 NvM(非易失存储管理)的事------DTC 记录、标定数据、学习值------这些频繁写入的小数据块需要磨损均衡保护 Flash 寿命。
没有人能回到过去:回滚保护
A/B 分区保你不断电。但它不防人性之恶。
假设一个场景:v2.3.1 通过 OTA 推送到全国。但 v2.3.0 有一个已知的高危安全漏洞------SecOC 的 CMAC 验证在特定条件下会跳过。v2.3.1 修复了这个漏洞。
现在一个攻击者手里有 v2.3.0 的固件镜像------它被 OEM 合法签名过,签名完全有效。攻击者以某种方式把这个旧镜像推送给一个 ECU。ECU 的 HSM 验证签名------匹配。合法。ECU 启动 v2.3.0------带着已修复的安全漏洞重新上线。
这就是降级攻击(Rollback / Downgrade)。签名机制本身不能阻止安装旧版本------旧版本的签名在当时也是合法的。
回滚计数器(Rollback Counter)就是针对这个攻击面的。
HSM 内部维护一个单调递增计数器,存储在 OTP 或受物理保护的存储块中。它的物理特性是:只能加 1,不能减回去。 这不是软件逻辑保证的------是 HSM 的硬件设计保证的。
每次 OTA 升级包的 manifest 中携带一个版本号。HSM 验证签名后,额外做一次检查:这个版本号是否 ≥ 当前回滚计数器的值?如果是------接受升级,计数器更新为新版本号。如果不是------拒绝,即使签名完全合法。
旧版本永远不能被重放。 攻击者手里有一个完整合法签名的旧固件------HSM 说:"你这个版本比我见过的版本低。你过时了。我不接受。"
降级攻击被堵死。旧固件带着旧漏洞------永远不能重返系统。
不可逆的承诺
OTA 是你对一个永远不会再被你亲手触碰的设备的终极承诺。
一辆车出厂后,在接下来的 15 年里,它不会再回到你的工厂。但它会跑 50 万公里。经过赤道和北极圈。被颠簸得螺丝松动。被电磁干扰轰击。被雷击中的物体的电流脉冲感应。被泡过水。被换了三手车主。被在路边摊改过电路。被停在地下车库三年没动。
在这 15 年里,你只有一次机会给它升级------通过 OTA。没有物理接触。没有 JTAG 调试器。没有产线编程器。
你必须信任你的 A/B 分区------B 区坏了回 A 区。你必须信任你的 HSM 签名验证------被篡改的固件不会启动。你必须信任你的 Flash 掉电保护------断电也不会丢数据。你必须信任你的回滚计数器------旧漏洞不会重生。
一个 ECU 是 4MB Flash、256KB RAM、一个 ARM Cortex-M 核。它在不可靠的电源、不可靠的网络、不可预知的物理环境中运行。但你必须保证------在所有这些不确定性中------变砖的概率无限接近零。
这就是 OTA 工程师的责任。不是"代码发出去就算完了"。你赌的是:遥远未来的任何一天、世界任何角落------"你当年写的那段升级逻辑不会害死这辆车。"
本篇小结
今天我们做了一件事:理解OTA不是"下载固件"------是对一个永远不会再被物理触碰的设备执行的不可逆修改,而你必须保证变砖概率无限接近零。
关键结论:
- A/B分区的本质不是软件设计模式------是Flash物理铁律的必然推论:擦除必须整个扇区进行,写入只能把1翻成0,擦除比编程慢一千倍。A/B分区让升级始终有一条安全退路------B区坏了回A区,A区始终完好。
- OTA调用了你学过的所有知识:裸机的状态机逻辑、RTOS的任务调度、AUTOSAR的NvM块、CAN/以太网的传输、UDS的RoutineControl、HSM的签名验证------七块知识在同一个不可逆转的操作中交汇。
- 这是不可逆的承诺:一辆车在出厂后15年里不会再回工厂。你只有一次机会通过OTA给它升级------你赌的是,当年写的那段升级逻辑不会在任何一天害死这辆车。
回顾 Part 6 这一大章。
诊断让系统能说话。DTC 不只是故障标记------诊断是工程师留给未来的信。
安全让系统不可被攻破。密钥不只是加密工具------HSM 是驾驶员对车厂信任的物理基础。
升级与存储让系统持续进化。OTA 不只是文件传输------是你在不可靠的物理世界上、对永不会再触及的设备执行的不可逆修改。
这三个世界在每一辆车的生命里是同一套系统的三条线程。诊断发现故障→分析定位 bug→OTA 推送修复→安全启动验证固件签名→安全回滚计数→车安全升级。安全报警被触发→诊断读取安全事件日志→分析入侵痕迹→OTA 推送安全补丁→回滚保护阻止降级。
你学过的每一块知识------裸机调度、RTOS 抢占、AUTOSAR 分层、CAN 通信、UDS 诊断、HSM 验签、Flash 写入------不是七个孤岛。它们在同一个 ECU 上同时运行,在同一段生命周期中交织。
现在所有技术都已就位。
然后呢?
下一节,Part 7全书结语------思想的回归。从低精度传感器测出精确值的工程智慧,到资源匮乏是永恒的世界观。这不是结束,是开始。
【下集预告】
全书最后一部分------Part 7:精神的回归。
你手握全部技术武器。裸机、RTOS、AUTOSAR、通信协议、诊断、安全、OTA------每一件你都知其然,也知其所以然。但你学这些,不是为了学技术本身。你学这些,是为了造出一些推动世界向前走的东西。
下一章,一个思想实验:一颗精度只有 1% 的传感器,能不能测出千分之一精度的物理量?答案凝聚了全人类的工程智慧------过采样、统计校准、卡尔曼滤波。1964 年戈壁滩上那群工程师用示波器拍照、人工读数、手算平均,在资源极度匮乏的条件下做出了精确的结果。
从"两弹一星"的精神内核,到"资源匮乏是永恒的"工程观,再到兰亭集序的哲学启示------全书的灵魂篇。不是技术,但让所有技术有了方向。
这不是结束。这是开始。