news 2026/10/12 2:49:21

STM32C5开发实战:从环境搭建到TrustZone安全量产

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32C5开发实战:从环境搭建到TrustZone安全量产

拿到一块新的STM32C5开发板,第一反应是什么呢?很多人会直接打开配置工具,照着自己旧工程的习惯把外设点一遍,然后点灯试试。这个路径放在M4时代的芯片上完全没问题,但放在stm32c5软件开发这件事上,我建议你慢半拍。C5这个系列和更早的M4平台比起来,内核换了、时钟树逻辑变了、安全策略也成了正经话题,如果不先把底层的差异理清楚,后面大概率会撞上一堆看似莫名其妙的问题。这篇文章我会从环境搭建一直讲到TrustZone和量产烧录,把我在实际项目里踩过的坑和验证过的方法都摊开说,适合正在评估迁移的工程师,也适合刚拿到评估板想快速跑通外设的人。

1. 拿到C5之后,先想清楚它和F4、L4到底差在哪

1.1 Cortex-M33内核到底新在哪:不止是TrustZone

C5用的Cortex-M33属于新一代M系列内核架构。很多从M4时代过来的工程师,一看到M33就先想到TrustZone,觉得和自己没什么关系。其实M33在微架构层面的改进是实打实的,不用安全功能也能受益。比如它的中断延迟比M4优化了不少,对实时控制任务很友好;再比如它支持可选的DSP扩展和浮点单元,跑传感器融合、闭环控制这类带小数运算的任务时不会吃亏;调试跟踪能力也更强,遇到疑难杂症时多出来的调试手段往往能救命。

TrustZone本身是M33上最吸引人的特性,它把系统分为安全世界和非安全世界,相当于在同一颗芯片里做了运行时隔离。对工业控制、医疗电子、物联网设备来说,这比单纯堆算力值钱得多。你先不用立刻搞懂它的全部细节,但得知道C5在架构上已经准备好了这条升级路径,后面要上安全启动、防抄板,不用换芯片,直接在软件层面和工程结构层面去规划就行。

1.2 C5的硬件资源画像:Flash、RAM、模拟外设与功耗

C5系列在具体型号上资源是有梯度的。Flash大体从256KB起步,往上能到1MB级别,RAM从几十KB跨度到300多KB,具体组合要看封装和型号选型。相比老平台,C5在模拟外设上的增强是很多人上手后才会注意到的:ADC、DAC、比较器这些模块都做了升级,单次采样的精度和稳定性表现不错,工业采集类应用会很喜欢这一点。主频方面,C5在同类中属于能打的,配合M33的核心效率,日常控制类负载完全跑得动。

功耗不是C5最极致的卖点,但也不用失望。它提供了多档低功耗模式,对"大部分时间休眠、偶尔唤醒干活"这种典型场景够用。当然,如果你要的是电池供电好几年的超低功耗产品,那还是得去看专门的低功耗系列。选型这事没有最好,只有合不合适,先把需求里的功耗、算力、外设三个维度画成坐标,再拿C5往坐标里放,答案会清楚很多。

1.3 谁最适合迁移到C5:老项目的升级判断标准

我自己判断一个项目值不值得迁到C5,就看三条。第一,你现在的平台算力基本够用,但外设细节或者安全能力有点跟不上;比如老平台没有硬件加速加密,软件算对称加密算法又慢又占CPU,这在通信密集型产品上会非常难受。第二,产品有防抄板或认证启动的刚需,想省掉一颗独立安全芯片;C5内置的安全特性可以把这部分功能收编进来,物料成本也能降下去。第三,供应链愿意接受新物料,因为C5是新一代主流系列,生命周期能拉得很长,不用像某些老型号一样担心停产风险。

反过来,如果项目已经在某个低功耗或高端系列上深度沉淀了好几年,代码量很大,团队也没有M33经验,那就不建议为了追新而硬迁。迁移成本主要不在点亮LED,而在那些"名字差不多、行为不一样"的底层细节,比如时钟树的组织方式、Flash接口的处理、复位策略的差异,这些才是消耗时间的地方。我的建议是先拿手头尚未量产的新项目去试水C5,而不是拿已经稳定的老产品去冒险。

下面我用一张表快速概括M33和老M4平台的关键差异,方便你做选型判断:

对比项经典M4平台C5的M33平台
内核架构基于经典Cortex-M4新一代Cortex-M33,支持TrustZone
中断延迟表现常规设计有优化,响应更快
安全扩展基本没有TrustZone+硬件密码加速
模拟外设常规水平高精度ADC/DAC,更适合采集
产品定位存量市场大新一代主流,生命周期长

2. 搭建C5开发环境时,最容易翻车的三个环节

2.1 工具链与固件包的版本对齐

我刚上手C5那阵子,第一个项目就卡在工具链上整整半天。现象很典型:配置工具生成工程没问题,一编译就报错,或者提示找不到芯片头文件。后来才明白,C5是新系列,老版本的工具和固件包根本没收录它的支持信息。所以搭建环境的第一原则是:不要把旧电脑里的老版本IDE和厂商中心装好的旧固件包直接拿来用,先把配置工具升级到比较新的版本。

固件包这边,要在软件包的组件管理里确认能找到C5系列的固件支持包。如果看不到,优先去升级配置工具而不是手动乱下载,因为工具版本太老的情况下,即使固件包下载回来也可能不被识别。第三方IDE用户更要留意:你的IDE编译器版本和器件支持包必须更新到适配C5的版本。一个快速自检方法是生成一个空工程,不写任何业务代码,直接编译,零错误通过,工具链基本就算对齐了。

2.2 在配置工具里创建第一个C5工程

创建工程的流程和旧系列很接近。选MCU型号时,在筛选器里输入C5,会列出一堆具体型号,注意封装、Flash容量、RAM容量要和手头板子一致,选错了后续链接脚本和调试配置很容易出问题。工程设置里,输出工程类型选你要用的IDE,官方一体化环境还是第三方IDE,看团队习惯。有一点我特别推荐:生成代码时,把外设初始化设置为每个外设单独生成一对.c/.h文件,而不是把所有初始化都堆在一个文件里。C5的外设多起来之后,如果全堆在单个文件里,排错会非常痛苦。

配置界面里还有一项对C5特别重要:是否启用TrustZone。如果你暂时用不到安全功能,可以保持关闭,不影响后面体验M33的日常优势。但如果你有安全启动的计划,建议从第一个工程就开启,因为工程结构会分成安全侧和非安全侧两部分,中后期再改比较折腾。这个决定的成本和收益,和租房时选户型很像:越早决定,拆改越少。

2.3 工程生成之后的三个必做检查

每次生成工程后,我习惯做三个检查,全都是在实际项目里换来的教训。

第一个,确认主函数里时钟初始化是否在外设初始化之前调用。这个顺序错了,寄存器的默认配置会让你一些外设找不到根因,表现为"代码看着都对,运行起来就是不对"。第二个,检查链接脚本和存储器配置。C5某些型号的Flash有多个区块,如果你打算做bootloader加应用程序的结构,链接脚本就必须按区块规划好,否则应用程序跑飞或者固件下载失败,排查起来很费劲。第三个,检查调试引脚有没有被误配成普通IO。SWD引脚是调试生命线,但有时电路设计不留意,就把它们复用成了点灯引脚。配置工具一般会警告,你如果强行忽略,代码一跑调试就断连。正确的做法是提前规划引脚,把调试端口保留下来。

3. 点灯其实是在点时钟:GPIO背后的时钟树逻辑

3.1 从复位到亮灯的完整调用链条

很多教程教点灯只教到"配置引脚、写高电平"这一层,导致后来出了问题不知道怎么排查。实际上从芯片复位到LED点亮,中间涉及好几层操作。首先是时钟:GPIO外设挂在某个总线上,总线时钟没有使能之前,你对这个外设的所有寄存器写入都是无效的。然后是引脚本身的模式配置:是输入还是输出,是推挽还是开漏,要不要上拉,速度选多少。最后才是把数据写到输出寄存器。

这里我想强调时钟源的重要性。C5复位之后默认跑的是内部高速时钟,频率不高也不够准。如果你不主动配置时钟树,HAL库的延时函数、定时器、串口波特率全都基于这个内部的默认时钟,前期点灯可能没感觉,一旦接上需要精确时间的传感器或通信外设,偏差就来了。所以把时钟树想清楚,不是有没有用的问题,是早晚要用到的问题。

3.2 GPIO初始化代码逐行解读

下面这段是典型的C5程序GPIO输出配置代码,看着和M4时代很像,但你得知道每一行的意义,因为后面排查问题都需要读它:

GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); // 使能PA端口时钟,寄存器写入才有效 GPIO_InitStruct.Pin = GPIO_PIN_5; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出,适合驱动LED GPIO_InitStruct.Pull = GPIO_NOPULL; // 外部已有电阻则不使用内部上下拉 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 低速输出,降低EMI和功耗 HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);

Speed选LOW不是随便选的。点LED这种低频信号,输出翻转速度完全够用;如果你选高速输出,反而会带来更大的电磁干扰和功耗。Pull选NOPULL也合理,如果板子上已经有外部上下拉电阻,MCU内部的上下拉可能和外部配置冲突,测出来的电平不是你预想的。这些细节在文档里都写了,但只有当你在项目里因为EMI问题返工时,才会真正记住。

3.3 灯不亮的排查清单与我的实测经验

如果按上面的代码配置完,灯还是不亮,我的排查顺序是:先拿万用表量LED两端电压,再查芯片供电和复位引脚,然后看代码有没有真正跑起来。有一个很隐蔽的坑:如果BOOT引脚被拉高,芯片可能从系统存储器启动,根本没有执行你的用户程序,灯当然不亮。这种情况复位状态是好的、供电也是好的,但程序就是没跑。

还有一个我实际遇到过的问题:引脚和调试端口冲突。比如你想用某个SWD相关的引脚当普通IO,配置时工具会警告,你要是硬着头皮忽略,结果就是程序烧进去之后调试器马上掉线,再也连不上。遇到这种局面,先通过boot引脚切换到下载模式,擦除用户区,救回来之后再重新规划引脚。教训就一句话:调试相关引脚,默认别乱动。

4. 把printf弄到串口上:调试效率的转折点

4.1 UART引脚复用与波特率配置细节

点灯只是热身,程序一复杂,能靠调试输出看清程序走到哪,效率会高一个档次。C5的UART引脚一般工作在复用功能模式,这就是AF编号的由来。很多人串口没输出,八成是AF编号填错了。不同引脚支持的外设映射是不一样的,你在配置工具里如果看到引脚上有个灰色的锁,多半就是和外设映射冲突了。选择UART引脚的时候别只看是第几个引脚,要看它对应的AF编号和你要用的UART外设是否匹配,这个映射表在数据手册里都有,动手配之前先花两分钟查一下,能省半天排查时间。

波特率这块,它表面上是串口模块的事,实际上是时钟树的事。UART的波特率分频基准来自总线时钟,而总线时钟又来源于系统时钟和PLL的组合。如果你在代码里手动改了系统时钟,却没有重新生成时钟初始化代码,串口的实际波特率会偏离你预期的值。这就是很多同事说"配置成115200还是乱码"的常见原因,不是串口坏了,是时钟源头变了。

4.2 printf重定向的几种姿势与避坑

嵌入式环境里,printf默认不会把字符送到串口,需要自己把输出重定向到UART发送函数。最通用的做法是重写底层字符输出函数,下面这段是HAL工程里常用的写法:

#ifdef __GNUC__ int __io_putchar(int ch) #else int fputc(int ch, FILE *f) #endif { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY); return ch; }

这里最常翻车的点是:不同编译器识别的重定向函数名不一样。GNU系编译器要写__io_putchar,另一些编译器环境要写fputc。你从网上抄了一段代码,先确认自己用的工具链是哪类,不然看着逻辑完全正确,编译也不报错,printf就是不输出。还有一些裸机工程需要额外开启MicroLib之类的运行时选项,关着的话printf底层实现根本没进到你的重定向函数里来。除了UART,也可以借助调试器端的打印通道,比如SWO或RTT这类方案,不占硬件串口、速度快,但依赖调试器品牌和具体配置,项目后期统一环境还行,初期调试还是建议先用UART。

4.3 串口乱码、卡死的常见诱因

串口乱码,九成以上是波特率不匹配,但有一种特殊情况:两边界面都写着115200,实际上串口模块算出来的真实波特率偏移了。这个要从时钟源头查,先固化系统时钟配置,再用示波器或逻辑分析仪抓TX引脚的位宽,拿实测位宽反推真实波特率。

另一个常见问题是程序卡死。HAL_UART_Transmit的阻塞模式会一直等到数据发完或者超时,如果对端设备没有及时取数据,发送函数一直阻塞,主循环就被卡住。改法有两个:要么加一个合理超时,要么改用DMA发送。还有中断优先级的问题:在串口中断回调函数里做耗时操作,会把其他中断饿死,看起来就是整个程序突然不走了。

这里我给一个排查速查表,实际工作中很管用:

现象最常见原因处理思路
输出全是乱码系统时钟变更导致波特率偏移固定时钟树后再查波特率
首字符丢失复位后发送早于对方就绪加延时等待或检查对端握手
输出一会就卡死阻塞发送被高优先级中断打断降低回调耗时、改DMA发送
完全不输出AF编号或引脚复用冲突回配置工具核对引脚映射

5. 定时器、ADC和DMA:让C5开始处理真实信号

5.1 定时器中断:预分频与重装载值的数学

定时器中断是嵌入式的基本功。C5内部有多个定时器,分布在不同总线上,而定时器时钟可能是总线时钟的倍数。准备配1ms中断时,先打开参考手册确认你的定时器挂在哪个总线、时钟来源是什么,再套公式:

定时频率 = 定时器时钟 / (预分频 + 1) / (自动重载值 + 1)

举个例子,如果定时器时钟是80MHz,想要1ms进一次中断,可以先分频到1MHz,也就是预分频寄存器写80-1,自动重载值写1000-1。这里的"减1"是新手重灾区:因为计数器是从0开始计数的,你写80实际是81个时钟周期,写1000实际是1001个周期,最终中断频率就和预期偏差了。在配置工具里图形界面会自动把这个数学算好,所以你只需要知道原理,但当你手工改代码时,不知道这个逻辑就会出错。

5.2 ADC多通道采样:校准与DMA搬运

C5的ADC精度确实不错,但不做校准就是浪费。初始化的时候一定要调用校准函数,它会把内部偏移误差修正掉,不校准的话采样值的准确性会差不少。多通道连续采样最省心的方式是配合DMA:配置好ADC通道序列和采样时间,开DMA循环搬运,转换结果会自动落到内存数组里,CPU主循环只需要在DMA传输完成中断里取数据。

这里有个容易踩的坑:DMA数据宽度要和ADC分辨率匹配。比如12位分辨率,一次转换结果是半个字宽度,DMA搬运时就要按半字配置。如果配置成字节,数据会错位排列,你看到采样值乱跳,以为ADC坏了,其实只是搬运宽度不匹配。我习惯的是开一个DMA传输完成标志,置位后一次性批量处理一组采样数据,而不是每个单通道转换完成都去打扰主循环,这样CPU占用很低。

5.3 低功耗模式下的外设注意点

C5支持多档低功耗模式,但我不建议你在功能没调通之前就开。低功耗一旦生效,SWD调试连接可能断开,调试器会直接卡住;更麻烦的是,调功耗和调业务逻辑的bug混在一起,排查难度翻倍。正确节奏是先把所有功能在正常模式下调通,最后再优化功耗。

如果你确实要加低功耗,注意两类问题。一是定时器在睡眠状态下可能仍然运行,你以为睡了,实际上定时器周期性唤醒,功耗根本没降下来;二是ADC、DAC这类模拟外设在低功耗下通常会掉电,唤醒后需要重新校准,这个恢复流程必须写进代码里,否则唤醒后第一次采样的数据会明显异常。这些坑不肯花时间踩一遍,量产阶段的功耗测试就会让你花更多时间补课。

6. 面向量产的安全启动:C5的TrustZone到底怎么用

6.1 从Flash读保护到安全启动的概念演进

老工程师对Flash读保护很熟,它能挡掉一大波抄板行为,但读保护本质上只是"防止别人读",并没有回答"怎么验证固件是可信的"这个问题。C5平台上可以做得更彻底。借助安全启动,芯片复位后先执行一段安全代码,用内置的密钥算法核验用户固件的签名和完整性,校验通过才跳转执行。这对量产产品来说意义很大:固件就算被破解提取出来,复制到别的板子也跑不起来。

做产品时间长了你会发现,抄板不是最怕的,最怕的是有人改了你的固件塞回设备里,出现安全事故。签名校验能挡住这种"篡改固件"的路径。C5的安全能力相当于把以前需要外挂安全芯片才能做好的事,集成到主控芯片里了,节省物料成本的同时,也让安全方案在产品里更可控。

6.2 TrustZone分区配置的一个最小可运行流程

TrustZone的概念是隔离安全世界和非安全世界,刚到工程实践时可以理解为:把敏感的代码和密钥放在安全侧,把普通应用放在非安全侧。在配置工具里勾选启用TrustZone之后,生成的工程会被拆成两个子工程。第一次看到两个工程不要慌,这是C5安全开发的正常结构,意味着安全代码和应用代码的边界被工程结构强制分开了。

一个最小可运行流程可以这样走:在安全侧初始化硬件随机数发生器,把根密钥存储在安全侧的受保护存储区;普通应用的启动流程只能通过安全侧导出的调用来获取相关服务,两者用固定的接口通信。我第一次跑通这个闭环时,最大的感触是:安全和业务代码的边界,靠工程结构就强制分开了,而不是靠程序员自觉。这种约束在团队协作时特别重要,因为不是每个人都清楚哪些代码必须留在安全侧。

这里还要特别说明,启用TrustZone后,芯片世界被切分,你对Flash的规划必须跟着变。安全侧的代码、非安全侧的代码、共享的存储区,都要在链接脚本里安排明白。不要指望在关闭TrustZone的工程基础上直接多写几行代码就完事,架构层面的事,还是得按架构的规矩来。

6.3 调试与安全策略冲突时的自救方案

开了TrustZone之后,调试这件事会变得复杂。安全世界和非安全世界的调试权限不同,如果你把自己的调试器配置成对两个世界都有完全权限,早期没开保护时很爽,但一旦烧录保护等级上去,调试器可能直接连不上。我见过好几个团队把板子"锁死"在调试器外,最后靠特殊启动模式全片擦除才救回来。

所以我的建议是:开发阶段不要把安全等级调到最高档,保留一个能降级恢复的通道;所有安全配置脚本和链接脚本必须纳入版本管理,改乱了随时能回到上一个可用状态。还有千万别在产品原型阶段就急着把密钥烧进去,先用测试密钥开发,量产阶段再切换到正式密钥,流程分开,风险就小了。

7. 调试点与量产点的真实经验补充

7.1 SWD接口的锁定与解除

SWD是工程师的生命线,但被锁的桥段几乎每个人都会经历。最常见的是程序里把SWD引脚配成了普通IO,代码一烧进去调试器就掉线。预防方法很简单:在引脚配置和低功耗进入逻辑里,预留一个启动延时,让系统复位后有足够时间保持调试口可用。就算程序要关掉调试口,也先延时个几秒再关,这样你有时间连上调试器、烧一版修复代码进去。

如果已经锁死了,别慌。通过启动引脚把芯片切入系统存储器模式,内置启动程序会接管Flash操作,先全片擦除,然后重新连接调试器,烧新的固件。整个过程要严格按照芯片参考手册的步骤走,不同系列恢复方式大同小异,千万别拿普通IO配置的方案硬套。我自己被锁过两次之后,现在所有工程的启动代码里都会故意放一个几百毫秒的延时,就是不想再重复那种心跳漏一拍的体验。

7.2 烧录失败的排查流程

烧录失败时,先分清是"电脑没有识别到调试器"还是"识别到了但下载失败"。如果是前者,九成是驱动、线序、供电问题,换根短一点的线,检查调试器供电跳线。后者的问题就多了:目标电压有没有在芯片工作范围、SWD连接是不是完整、复位引脚有没有被外部元件强行拉低。排查的时候用示波器测一下复位脚电平,比猜快得多。

还有一类场景:Flash里已经存在固件且开了读保护,下载器在擦除阶段权限不够,下载失败。这个先执行一次全片擦除解除保护,再重新下载,顺序别搞反。最后检查一下C5的调试接口配置,有的加密或安全选项会影响调试器的访问权限,如果你在产品代码里动了这些配置,调试器访问受限也是正常现象,不要以为是硬件坏了。

7.3 给量产的三个小建议

第一个,出厂前把读保护等级配置好,把这个操作写进自动化产测脚本,不要依赖产线工人手工点。第二个,每块板子出厂时读出芯片唯一ID,和整机序列号绑定存储。以后客户退货、批量追溯、防串货,全靠这个关联关系。第三个,如果你计划做OTA升级,Flash分区从设计第一天就按三区规划:bootloader区、应用区、下载暂存区。等产品开发完再改这部分,牵一发而动全身。

我还有一个很个人的习惯,也一并分享:在配置工具生成的"用户代码保留区"注释块里写变更日记,每次手动改寄存器后,用一句话记录为什么改。这个注释块在重新生成代码时会原样保留。项目结束半年后回看,你面对一堆自己改过的代码,能快速回忆起每个改动的动机,比任何设计文档都靠谱。嵌入式开发里,能让自己少挠头的事,都值得认真做。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 2:48:43

删了大文件磁盘空间不释放?Linux文件系统底层原理与排障指南

如果你在Linux服务器上遇到过“删了一个大文件,但df显示磁盘空间还是没释放”的诡异现象,或者“磁盘明明还有几十G,系统却提示No space left on device”,那说明你已经站在了文件系统的门边上,就差推开那扇门了。这篇文…

作者头像 李华
网站建设 2026/10/12 2:48:31

eBPF CO-RE实战:从BTF到libbpf解决内核版本兼容问题

写 eBPF 观测程序,最让人头疼的从来不是 BPF 指令怎么写,而是写完之后怎么让它在不同内核版本上都能跑。eBPF CO-RE 模式(Compile Once, Run Everywhere)就是为解决这个可移植性问题而生的。第一次接触 CO-RE 的时候,我…

作者头像 李华
网站建设 2026/10/12 2:48:14

SpringBoot+Vue全栈实现宽带业务管理系统:权限、订单与工单实战

宽带业务管理系统这类题目,在Java方向的毕业设计和课程设计里出现频率一直很高。单看标题,SpringBoot、Vue、MySQL、MyBatis这几个词几乎把所有主流技术栈都串起来了,后端、前端、数据库三层全部覆盖,是一套标准的前后端分离全栈项…

作者头像 李华
网站建设 2026/10/12 2:48:13

9款降AI率工具实测:继续教育AI写作检测与改写全攻略

最近继续教育圈子里聊得最多的一个话题,就是“降AI率”。不少同学平时工作忙,写课程论文、研修报告、学习心得都习惯先让AI出一版初稿,结果提交时发现平台标注“AI生成内容比例偏高”,轻则打回重写,重则影响成绩甚至涉…

作者头像 李华
网站建设 2026/10/12 2:47:43

Windows下用Docker构建Linux版Electron安装包的完整方案

做桌面端发版,最难受的往往不是写业务代码,而是跨平台打包这一脚。我的开发机一直是Windows,但交付安装包必须包含Linux的deb和AppImage。Electron应用本身是可移植的,可一旦涉及安装包生成、原生模块编译,Windows下的…

作者头像 李华
网站建设 2026/10/12 2:46:29

QuickReport 4.05 在 BCB6/D7 下的安装、导出与避坑全指南

简介:Quick Report 4.05 是面向 BCB6 与 Delphi 7 开发环境的专业报表生成工具,专注于可视化报表设计、数据分组与打印输出,尤其针对自定义纸张尺寸下的布局与打印精度做了优化,适合需要在 C Builder 或 Delphi 中集成复杂报表功能…

作者头像 李华