STM32 + CubeMX + 串口 + IAP升级
这篇文章分享一个简单的串口IAP Demo,实现使用串口更新我们自己的App程序。
目录
- 一、IAP简介
- 二、Stm32CubeMx配置
- 三、Boot代码及配置
- 1、代码
- 2、配置
- 四、App代码及配置
- 1、代码
- 2、配置
- 五、效果展示
一、IAP简介
IAP介绍可以在网上找找,相关资料很多,如:
https://blog.csdn.net/ba_wang_mao/article/details/110401656
二、Stm32CubeMx配置
1、RCC开启外部高速时钟(略)
2、配置STLink调试口(略)
3、配置串口方便调试输出(略)
4、配置工程名、生成路径,之后生成工程(略)
(1-4步的基础配置可以参考前面的文章《STM32基础工程模板创建》)
Boot和App的工程配置是一样的,配置好时钟、usart1即可
三、Boot代码及配置
1、代码
#include <stdio.h>
#include "string.h"
#include "stdio.h"#define NVIC_VectTab_RAM_Start ((uint32_t)0x20000000) //RAM起始地址
#define NVIC_VectTab_RAM_End ((uint32_t)0x20020000) //RAM结束地址,大小为128K,根据自己的实际芯片大小修改
#define NVIC_VectTab_FLASH ((uint32_t)0x08000000) //Flash起始地址
#define BOOT_SIZE 0x3000 //Boot大小,12KB,0--12页,共128页
#define ApplicationAddress (NVIC_VectTab_FLASH + BOOT_SIZE) //App的起始地址
#define USER_FLASH_PAGES 52 //App所占大小,52KB
//#define FLASH_PAGE_SIZE 1024 //每页所占的字节大小,和系统库定义重复了,注释掉
#define UPDATE_CMD "update" //升级擦除指令
#define FLASH_USER_END_ADDR \((FLASH_PAGE_SIZE*USER_FLASH_PAGES)+ApplicationAddress) //App结束地址typedef void (*iapfun)(void);
void SystemClock_Config(void);int fputc(int ch, FILE *f)
{HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);return ch;
}iapfun jump2app;
unsigned int count2 = 0;
unsigned char datatemp[256] = {0};
unsigned char boot_flag = 0;
unsigned char time_out_flag = 0;int main(void)
{unsigned char i;HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_USART1_UART_Init();printf("boot start\r\n");printf("input \"update\" to erasure user flash, or wait 10s to start user app\r\n");for(i = 0; i<10; i++){//上电后每秒阻塞接收升级指令,连续10秒未收到则跳转App,10秒内收到则接收App数据并写入HAL_UART_Receive(&huart1, datatemp, 256, 1000); if(strstr((const char *)datatemp, UPDATE_CMD) != NULL){// 擦除App区域FLASH_EraseInitTypeDef EraseInitStruct;unsigned int PageError;HAL_FLASH_Unlock();EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;EraseInitStruct.PageAddress = ApplicationAddress;EraseInitStruct.NbPages = USER_FLASH_PAGES;if(HAL_FLASHEx_Erase(&EraseInitStruct, &PageError) != HAL_OK){HAL_FLASH_Lock();printf("Erase fail at:0x%x\n\r",PageError);return 0;}HAL_FLASH_Lock();boot_flag = 1;printf("Erase OK\n\r"); break; }}if(boot_flag == 1){HAL_StatusTypeDef temp;unsigned int Address;unsigned int data_32;unsigned char j = 0;printf("ready to receive bin, please send in 30s\n\r");Address = ApplicationAddress; temp = HAL_UART_Receive(&huart1, datatemp, 256, 30*1000); if(temp == HAL_TIMEOUT){//阻塞30S,未收到App数据则退出printf("time out, end wait to receive bin\n\r");return 0;}else if(temp == HAL_OK){//收到则循环接收,每秒阻塞接收256字节,并写入Flashwhile(1){unsigned char i; HAL_FLASH_Unlock();for(i=0; i<64; i++){data_32 = *(unsigned int *)(&datatemp[i<<2]);if(Address < FLASH_USER_END_ADDR){if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, Address, data_32)==HAL_OK){Address = Address + 4;}else {HAL_FLASH_Lock();printf("write fail at: 0x%x\n\r", Address);return 0;} } }HAL_FLASH_Lock(); printf("write 256 btye OK: 0x%x\t%d\n\r",Address,j++);//最后一包数据不足256字节时会接收超时,防止app数据不完整,不足256字节数据接收完以后处理一次,下一次接收超时认为接收完成,跳转Apptemp = HAL_UART_Receive(&huart1, datatemp, 256, 2*1000); if(temp == HAL_TIMEOUT){time_out_flag++;if(time_out_flag == 2){printf("End write OK\n\r");goto START_APP;}}}}} else if(boot_flag == 0) //没有收到升级命令{
START_APP: printf("start user app\n\r"); HAL_Delay(10);/*判断App的栈顶指针是否合法(即是否有App)ApplicationAddress为App在flash中的地址,APP把中断向量表ApplicationAddress开始的位置,而中断向量表前4字节存储的是栈顶地址这里的目的是判断App的栈顶指针是否在0x20000000到0x2001FFFF之间,在的话就认为有App,不在就没有*/printf("ApplicationAddress:%0x\r\n", (*(unsigned int *)ApplicationAddress));if(((*(unsigned int *)ApplicationAddress)>= NVIC_VectTab_RAM_Start) &&((*(unsigned int *)ApplicationAddress)<= NVIC_VectTab_RAM_End)){ // disable irq, if use this, must enable irq at app//__disable_irq(); //(ApplicationAddress+4)放的是中断向量表的第二项“复位地址”__set_MSP(*(unsigned int *)ApplicationAddress); jump2app=(iapfun)*(unsigned int *)(ApplicationAddress+4);jump2app(); }else{printf("no user app\n\r");return 0;}}while (1){}
}
2、配置
四、App代码及配置
1、代码
#include <stdio.h>#define NVIC_VectTab_FLASH ((uint32_t)0x08000000) //Flash起始地址
#define BOOT_SIZE 0x3000 //Boot大小void SystemClock_Config(void);
int fputc(int ch, FILE *f)
{HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);return ch;
}int main(void)
{uint32_t Count = 0;/* 设置中断向量偏移地址 */SCB->VTOR = NVIC_VectTab_FLASH | BOOT_SIZE;HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_USART1_UART_Init();printf("\n\rapp start\n\r");while (1){HAL_Delay(10);if(Count%100 == 0){printf("this is app!\t%d\n\r",Count/100);}Count++;}
}
2、配置
//添加这条命令生成bin文件
$K\ARM\ARMCC\bin\fromelf.exe --bin --output=Bin\@L.bin !L
五、效果展示
1、先使用Keil将Boot程序烧录进单片机,串口提示请在10s内输入升级命令
2、串口发送“update”命令,提示成功,在30s内发送bin文件
3、配置串口发送延时为100ms.因为stm32是接收一包写一包的,比较耗时。
4、选择并发送文件,开始打印接收升级日志
5、全部接收完成后会跳转到App,并打印App内的日志
相关文章:
STM32 + CubeMX + 串口 + IAP升级
这篇文章分享一个简单的串口IAP Demo,实现使用串口更新我们自己的App程序。 目录 一、IAP简介二、Stm32CubeMx配置三、Boot代码及配置1、代码2、配置 四、App代码及配置1、代码2、配置 五、效果展示 一、IAP简介 IAP介绍可以在网上找找,相关资料很多&am…...
Oracle-—系统包使用
文章目录 系统包dbms_redefinition 系统包 dbms_redefinition 功能介绍:该包体可以实现将Oracle库下的表在线改为分区结构或者重新定义; 说明:在检查表是否可以重定义和开始重定义的过程中,按照表是否存在主键,参数 o…...
使用Hugo和GitHub Pages创建静态网站个人博客
不需要服务器,不需要域名,不需要数据库,可以选择模版,内容为Markdown格式。 Hugo:https://gohugo.io 文档:https://gohugo.io/getting-started/quick-start/ 中文文档:https://www.gohugo.or…...
群晖系统证书延期
群晖系统默认证书过期了 接下来操作续期证书 一直下一步会让下载一个压缩包里面包含私钥和签发证书请求 下载后解压出来 在群晖里用证书续期 对以前的证书签署签发请求 选择刚刚解压出来的证书 执行完成后会下载一个压缩包,解压出来就会得到新证书 给群晖新增证书 选…...
android shader gl_Position是几个分量
在Android的OpenGL ES中,gl_Position是顶点着色器(Vertex Shader)的一个内置输出变量,它用于指定顶点在裁剪空间(Clip Space)中的位置。gl_Position是一个四维向量(4-component vectorÿ…...
JAVA练习-ArrayList数组
需求 建立3个Student类的实例 原始数组: public class Student {private String name;private int score;public Student(String name, int score) {this.name name;this.score score;}Overridepublic String toString() {return name "的分数࿱…...
springboot339javaweb的新能源充电系统pf(论文+源码)_kaic
毕 业 设 计(论 文) 题目:新能源充电系统的设计与实现 摘 要 如今社会上各行各业,都喜欢用自己行业的专属软件工作,互联网发展到这个时候,人们已经发现离不开了互联网。新技术的产生,往往能解…...
数据结构——排序第三幕(深究快排(非递归实现)、快排的优化、内省排序,排序总结)超详细!!!!
文章目录 前言一、非递归实现快排二、快排的优化版本三、内省排序四、排序算法复杂度以及稳定性的分析总结 前言 继上一篇博客基于递归的方式学习了快速排序和归并排序 今天我们来深究快速排序,使用栈的数据结构非递归实现快排,优化快排(三路…...
Jackson:Java对象和JSON字符串的转换处理库使用指南
Jackson介绍 Jackson 是一个非常流行的 Java JSON 处理库,它能够将 Java 对象与 JSON 字符串相互转换。 Jackson 工具主要用于将请求的参数(例如前端发送的 JSON 数据)和响应的数据(例如后端返回给前端的数据)转换成…...
mac maven编译出现问题
背景 进行maven install 命令,报错: [ERROR] COMPILATION ERROR : [INFO] ------------------------------------------------------------- [ERROR] No compiler is provided in this environment. Perhaps you are running on a JRE rather than a J…...
深入讲解Spring Boot和Spring Cloud,外加图书管理系统实战!
很抱歉,我的疏忽,说了这么久还没有给大家详细讲解过Spring Boot和Spring Cloud,那今天给大家详细讲解一下。 大家可以和下面这三篇博客一起看: 1、Spring Boot 和 Spring Cloud 微服务开发实践详解https://blog.csdn.net/speaking_me/artic…...
【AIGC】2023-ICCV-用于高保真语音肖像合成的高效区域感知神经辐射场
2023-ICCV-Efficient Region-Aware Neural Radiance Fields for High-Fidelity Talking Portrait Synthesis 用于高保真语音肖像合成的高效区域感知神经辐射场摘要1. 引言2. 相关工作3. 方法3.1 准备工作和问题设置3.2 三平面哈希表示3.3. 区域注意模块3.4 训练细节 4. 实验4.1…...
如何写一份优质技术文档
作者简介: 本文作者拥有区块链创新专利30,是元宇宙标准化工作组成员、香港web3标准工作组成员,参与编写《数据资产确权与交易安全评价标准》、《链接元宇宙:应用与实践》、《香港Web3.0标准化白皮书》等标准,下面提供…...
ML 系列:第 35 节 - 机器学习中的数据可视化
ML 系列:第 35 天 - 机器学习中的数据可视化 文章目录 一、说明二、数据可视化2.1 直方图2.2 箱线图2.3 散点图2.4 条形图2.5 线图2.6 热图 三、结尾 一、说明 描述性统计和数据可视化是理解和解释机器学习数据的基础。它们有助于总结和直观地呈现数据,…...
存储服务器一般做是做什么阵列?详细列举一下
存储服务器通常使用 RAID(Redundant Array of Independent Disks) 阵列技术来管理磁盘,以提高数据的性能、可靠性和可用性。所选择的 RAID 类型取决于存储服务器的具体用途和需求,比如性能要求、容量需求、容错能力等。 以下是存…...
uniapp使用扩展组件uni-data-select出现的问题汇总
前言 不知道大家有没有学习过我的这门课程那,《uniCloud云开发Vue3版本官方推荐用法》,这么课程已经得到了官方推荐,想要快速上手unicloud的小伙伴们,可以学习一下这么课程哦,不要忘了给一键三连呀。 在录制这门课程…...
pdf.js 预览pdf的时候发票数据缺失显示不全:字体加载出错(缺失)导致部分缺失
首先,排除后端返回的PDF文件流是没有问题的: 但是在vue项目中是这样的: 明显是显示不全,F12查看报错信息,有以下警告: pdf.js:2153 Warning: Error during font loading: The CMap “baseUrl” paramet…...
【设计模式】【结构型模式(Structural Patterns)】之外观模式(Facade Pattern)
1. 设计模式原理说明 外观模式(Facade Pattern) 是一种结构型设计模式,它提供了一个统一的接口,用来访问子系统中的一群接口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。通过隐藏子系统的复杂…...
Redis使用场景-缓存-缓存穿透
前言 之前在针对实习面试的博文中讲到Redis在实际开发中的生产问题,其中缓存穿透、击穿、雪崩在面试中问的最频繁,本文加了图解,希望帮助你更直观的了解缓存穿透😀 (放出之前写的针对实习面试的关于Redis生产问题的博…...
介绍 Apache Spark 的基本概念和在大数据分析中的应用
Apache Spark 是一个开源的大数据处理框架,它提供了快速、通用、可扩展的数据处理能力。Spark可以处理大规模数据集,并且在内存中进行数据操作,从而实现高速的数据处理和分析。 Spark的核心概念是弹性分布式数据集(Resilient Dis…...
OpenCPN-插件之Dashboard Tactics
1:相关链接Dashboard Tactics :: OpenCPN Dashboard Tactics Plugin rgleason/dashboard_tactics_pi: OpenCPN dashboard built-in plugin merger with external tactics_pi plugin NMEAconverter :: OpenCPN 2:显示样式 3:代码 这个插件…...
【LeetCode面试150】——20有效的括号
博客昵称:沈小农学编程 作者简介:一名在读硕士,定期更新相关算法面试题,欢迎关注小弟! PS:哈喽!各位CSDN的uu们,我是你的小弟沈小农,希望我的文章能帮助到你。欢迎大家在…...
JWT介绍和结合springboot项目实践(登录、注销授权认证管理)
目录 一、JWT介绍(一)基本介绍(二)jwt有哪些库1、jjwt(Java JWT)2、nimbus - jwt - jwt - api 和 nimbus - jwt - jwt - impl3、spring - security - jwt(已弃用,但在旧项目中有参考…...
Linux 下安装 Golang环境
Linux 下安装 Golang 获取Golang下载地址 安装 进入终端,登入root来到应用安装目录使用 wget 下载解压文件配置环境变量查看golang版本,测试是否配置成功GO设置代理环境变量 本篇教程 以 centos7 为环境基础 不使用软件包管理器安装,原因&am…...
「Mac畅玩鸿蒙与硬件36」UI互动应用篇13 - 数字滚动抽奖器
本篇将带你实现一个简单的数字滚动抽奖器。用户点击按钮后,屏幕上的数字会以滚动动画的形式随机变动,最终显示一个抽奖数字。这个项目展示了如何结合定时器、状态管理和动画实现一个有趣的互动应用。 关键词 UI互动应用数字滚动动画效果状态管理用户交…...
安装使用Ubuntu18.04超级大全集最初版(anaconda,pycharm,代理,c/c++环境)
本文介绍ubuntu1804中我目前用到的环境的完整配置,包括ubuntu安装软件,更新环境变量等都有涉及。图片非常多,能给的连接和材料都给了。希望能帮助到新同学。 目录 目录 目录 环境及镜像文件 安装Ubuntu 编辑 开机之后 编辑 更新…...
Redis设计与实现第16章 -- Sentinel 总结1(初始化、主从服务器获取信息、发送信息、接收信息)
Sentinel是Redis的高可用解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主…...
ChatGPT科研应用、论文写作、课题申报、数据分析与AI绘图
随着人工智能技术的飞速发展,ChatGPT等先进语言模型正深刻改变着科研工作的面貌。从科研灵感的激发、论文的高效撰写,到课题的成功申报,乃至复杂数据的深度分析与可视化呈现,AI技术均展现出前所未有的潜力。其实众多科研前沿工作者…...
OceanBase数据库系列之:基于docker快速安装OceanBase数据库,基于linux服务器快速部署OceanBase数据库
OceanBase数据库系列之:基于docker快速安装OceanBase数据库,基于linux服务器快速部署OceanBase数据库 一、docker快速安装OceanBase数据库下载OceanBase数据库镜像查看镜像启动OceanBase数据库查看OceanBase数据库是否启动成功 二、基于linux部署OceanBa…...
无星的微前端之旅(四)——qiankun线上服务代理到本地
这个方式其实是我在上家公司的时候体验过,觉得确实很有意思。 所以这里来逆推一下实现方式。 解决了什么痛点 1.开发一个模块,需要启动2-3个项目 在微前端的开发过程中,如果我们要在主应用中看效果,我们至少需要启动一个主应用&am…...
鸿蒙进阶篇-Stage模型、UIAbility
“在科技的浪潮中,鸿蒙操作系统宛如一颗璀璨的新星,引领着创新的方向。作为鸿蒙开天组,今天我们将一同踏上鸿蒙基础的探索之旅,为您揭开这一神奇系统的神秘面纱。” 各位小伙伴们我们又见面了,我就是鸿蒙开天组,下面让我们进入今…...
快速上手:如何开发一个实用的 Edge 插件
在日常浏览网页时,背景图片能够显著提升网页的视觉体验。如果你也想为自己的浏览器页面添加个性化背景图片,并希望背景图片设置能够持久保存,本文将介绍如何通过开发一个自定义Edge插件来实现这一功能。我们将涵盖保存背景设置到插件选项页&a…...
java缓存技术点介绍
1. 缓存(Cache): • 缓存是指用于存储数据的临时存储区域,以便快速访问。在Java中,缓存通常用于存储频繁访问的对象、结果集或其他数据。 2. 缓存命中率(Cache Hit Ratio): • 缓存命…...
【单片机毕业设计12-基于stm32c8t6的智能称重系统设计】
【单片机毕业设计12-基于stm32c8t6的智能称重系统设计】 前言一、功能介绍二、硬件部分三、软件部分总结 前言 🔥这里是小殷学长,单片机毕业设计篇12-基于stm32c8t6的智能称重系统设计 🧿创作不易,拒绝白嫖可私 一、功能介绍 ----…...
音视频流媒体直播/点播系统EasyDSS互联网视频云平台介绍
随着互联网技术的飞速发展,音视频流媒体直播已成为现代社会信息传递与娱乐消费的重要组成部分。在这样的背景下,EasyDSS互联网视频云平台应运而生,它以高效、稳定、便捷的特性,为音视频流媒体直播领域带来了全新的解决方案。 1、产…...
3dtile平移子模型以及修改 3D Tiles 模型的模型矩阵z平移
第一段代码:应用平移变换到子模型 这段代码的目的是获取子模型的变换矩阵,并将其平移 10 个单位。 if (submodel) {// 获取当前子模型的变换矩阵let transform submodel.transform// 创建一个向上的平移矩阵,平移 10 个单位let translation…...
JavaScript中类数组对象及其与数组的关系
JavaScript中类数组对象及其与数组的关系 1. 什么是类数组对象? 类数组对象是指那些具有 length 属性且可以通过非负整数索引访问元素的对象。虽然这些对象看起来像数组,但它们并不具备真正数组的所有特性,例如没有继承 Array.prototype 上…...
【机器学习】机器学习学习笔记 - 监督学习 - 逻辑回归分类朴素贝叶斯分类支持向量机 SVM (可分类、可回归) - 04
逻辑回归分类 import numpy as np from sklearn import linear_modelX np.array([[4, 7], [3.5, 8], [3.1, 6.2], [0.5, 1], [1, 2], [1.2, 1.9], [6, 2], [5.7, 1.5], [5.4, 2.2]]) y np.array([0, 0, 0, 1, 1, 1, 2, 2, 2])# 逻辑回归分类器 # solver:求解器&a…...
Python脚本文件开头两行#!/usr/bin/python和# -*- coding: utf-8 -*-的作用
Linux环境下,Python脚本代码文件,比如test.py,文件的第一行一般要指定解释器,使用Linux的Shebang的形式指定,第二行一般指定编码格式。 首行指定解释器工具,使用方式如下: # 第1种方式&#x…...
使用PyQt5开发一个GUI程序的实例演示
一、安装Python 下载安装到这个目录 G:\Python38-32 安装完成有这些工具,后面备用: G:\Python38-32\Scripts\pyrcc5.exe G:\Python38-32\Scripts\pyuic5.exe 二、PyQt环境配置 pip install PyQt5 pip install pyqt5-tools 建议使用国内源,…...
3d扫描建模产品开发-三维扫描检测蓝光检测
现当下,汽车制造、航空航天,还是消费电子、医疗器械,三维扫描检测与蓝光技术正以前所未有的精度和效率,推动着产品从概念到实物的快速转化。 三维扫描技术,简而言之,就是通过激光、结构光(如蓝…...
多线程——01
1. 进程/任务 process/task 进程是操作系统对一个正在运行的程序的一种抽象,换言之,可以把进程看做程序的一次运行过程 每个进程想要执行,都需要消耗一定的系统资源(硬件资源) 1)进程在系统中的作用 a…...
Makefile中-D选项定义预编译处理宏
1.简单示例假设我们有一个简单的 C 程序main.c,内容如下: #include <stdio.h> #ifdef DEBUGvoid print_debug_info() {printf("This is debug information.\n");} #elsevoid print_debug_info() {} #endif int main() {print_debug_info();printf("Prog…...
NAPI杂记
NAPI和tasklet区别:NAPI是利用的软中断NET_RX_SOFTIRQ实现, tasklet是利用TASKLET_SOFTIRQ实现 另外文章说NAPI主要是采用轮询,可以减少中断触发次数。do_softirq里面是开中断执行的啊? 中断处理函数关闭中断开始处理。如果此时…...
006 MATLAB编程基础
01 M文件 MATLAB输入命令有两种方法: 一是在MATLAB主窗口逐行输入命令,每个命令之间用分号或逗号分隔,每行可包含多个命令。 二是将命令组织成一个命令语句文集,使用扩展名“.m”,称为M文件。它由一系列的命令和语句…...
使用Alpine镜像作为基础镜像的Dockerfile配置
配置阿里 apk源: /etc/apk/repositories: https://mirrors.aliyun.com/alpine/v3.13/main alpine-v3.13-community安装包下载_开源镜像站-阿里云 或者使用命令 sed -i sdl-cdn.alpinelinux.orgmirrors.aliyun.comg /etc/apk/repositories dockerfil…...
RAG (Retrieval Augmented Generation) 检索增强和生成
1 RAG技术简介 1.1 RAG技术概述 RAG(Retrieval Augmented Generation) 是一种结合了检索(Retrieval)和生成(Generation)的技术,旨在通过利用外部知识库来增强大型语言模型(LLMs&am…...
Figma入门-约束与对齐
Figma入门-约束与对齐 前言 在之前的工作中,大家的原型图都是使用 Axure 制作的,印象中 Figma 一直是个专业设计软件。 最近,很多产品朋友告诉我,很多原型图都开始用Figma制作了,并且很多组件都是内置的,…...
039集——渐变色之:CAD中画彩虹()(CAD—C#二次开发入门)
(来左边儿 跟我一起画个龙,在你右边儿 画一道彩虹 ~~~~~~~~~~~ ) 效果如下: namespace AcTools {public class Class1{public Wform.Timer timer;//定时器需建在类下面public static DateTime startTime;[CommandM…...
MTK主板_小型联发科安卓主板_行业智能终端主板基于联发科方案
MTK安卓主板是一款小巧而高效的科技产品,其尺寸仅为43.4mm x 57.6mm。采用了先进的联发科12nm制程工艺,这款主板搭载四核或八核64位A53架构的CPU,主频高达2.0GHz,不但保证了出色的计算能力,还实现了超低功耗的特点。系…...