STM32F103ZET6移植FATFS文件系统教程(W25Q32)
一、FATFS核心特性
跨平台支持
支持FAT12/FAT16/FAT32格式,兼容Windows文件系统;
采用标准C语言编写,代码量小且支持RTOS。
配置灵活性
通过宏定义实现功能裁剪,例如:
FF_FS_READONLY:设为1时禁用写操作相关函数(如f_write()、f_unlink());
FF_FS_MINIMIZE:设置不同精简级别(0-3),逐步移除非必要API(如目录操作、文件统计等)。
二、移植关键步骤
存储介质初始化
需初始化底层存储设备(如SD卡、SPI Flash),涉及存储地址映射与读写函数实现;
在user_diskio.c中实现底层接口:disk_initialize(初始化)、disk_read(读)、disk_write(写)。
文件系统挂载
调用f_mount()函数挂载存储设备(如f_mount(fs, "0:", 1)),分配逻辑驱动器号(如"0:")。
三、应用层操作
文件读写流程
创建文件:f_open(&file, "0:/file.txt", FA_CREATE_ALWAYS | FA_WRITE);
数据写入:使用f_write()时需将非字符类型数据(如int/float)转换为字符格式;
资源释放:操作完成后需调用f_close()关闭文件。
辅助功能配置
支持长文件名需启用FF_USE_LFN并选择编码方式(如UTF-8);
多卷管理需配置FF_VOLUMES参数。
四、注意事项
存储介质格式化:首次使用前需通过工具或代码格式化,以创建文件分配表和目录结构;
内存管理:合理分配缓冲区以避免内存溢出,尤其在动态内存模式下;
实时性优化:在RTOS中需确保文件系统操作与任务调度兼容。
开始移植
我们先使用STM32CubeMX创建一个纯净的工程

配置好SPI1

然后在初始化一个片选引脚(PC13)

之后在中间件里设置好FATFS(选择User-defind就好,其他的可以不用改)

最后选择MDK-ARM,生成工程

底层驱动兼容SFUD
一、通用驱动兼容性
广泛硬件支持:兼容市面主流SPI Flash芯片(如W25Q64、SST25VF等),支持SPI/QSPI接口的基础读写擦除操作,无需针对不同芯片重复开发驱动。
接口标准化:通过统一API(如sfud_read、sfud_erase)屏蔽底层硬件差异,降低代码与硬件的耦合度。
二、参数自动检测
动态识别能力:运行时自动读取Flash的厂商ID、容量、擦除粒度等参数,减少因芯片停产或型号变更导致的维护风险。
SFDP协议支持:基于JEDEC标准的SFDP(Serial Flash Discoverable Parameters)规范,自动解析设备特性表,避免手动配置。
三、轻量级设计
低资源占用:标准模式代码量约5.5KB ROM/0.2KB RAM,最小模式可压缩至3.6KB ROM/0.1KB RAM,适用于STM32等资源受限的MCU。
可裁剪性:通过宏定义关闭非必要功能(如QSPI支持),进一步优化存储和运行效率。
四、扩展性与易用性
多设备管理:支持通过注册机制同时驱动多个Flash设备,适用于多存储介质的复杂场景(如主控+备份存储)。
分层架构设计:与FAL(Flash抽象层)组件无缝集成,提供统一的Flash访问接口,简化文件系统(如FATFS)或OTA升级功能的实现。
五、开发效率提升
快速移植:仅需实现底层SPI接口函数(如spi_send、spi_recv),缩短从零开发驱动的时间。
动态适配优势:新增Flash型号时无需修改驱动代码,仅需更新设备参数表或依赖SFDP自动识别。
| 场景 | SFUD作用 |
| 外部存储扩展(如W25Q32) | 提供统一接口,简化文件系统(FATFS)或日志存储的实现流程 |
| 多Flash设备管理 | 通过注册机制区分不同存储介质,支持并行操作 |
| 特性 | 嵌入式开发价值 |
| 兼容性 | 覆盖90%以上主流SPI Flash型号,降低硬件选型限制 |
| 低资源消耗 | 适配低端MCU(如STM32F103),节省ROM/RAM资源 |
| 维护成本低 | 动态参数检测减少代码修改频率,提升长期项目可持续性 |
移植SFUD
SFUD会检测单片机上的Flash芯片,所以我们这里选择SFUD驱动做一个兼容,就算更换芯片后也不需要修改底层代码。
SFUD: https://github.com/armink/SFUD.git - Gitee.com
把代码克隆到本地,然后把sfud文件夹复制出来,放到工程目录下,里面共有3个目录(inc,port,src)

将port和src内的C文件加到Keil工程内

然后添加inc路径

我们只需要实现底层的函数接口
进入到sfud_port.c文件内,实现spi_write_read函数
#include <sfud.h>
#include <stdarg.h>
#include "main.h"static char log_buf[256];extern SPI_HandleTypeDef hspi1;#define SPI_Start() (HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET))
#define SPI_Stop() (HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET))void sfud_log_debug(const char* file, const long line, const char* format, ...);/*** SPI write data then read data*/
static sfud_err spi_write_read(const sfud_spi* spi, const uint8_t* write_buf, size_t write_size, uint8_t* read_buf, size_t read_size) {sfud_err result = SFUD_SUCCESS;uint8_t send_data, read_data;/*** add your spi write and read code*/SPI_Start();if (write_size > 0) {if (HAL_OK != HAL_SPI_Transmit(&hspi1, (uint8_t*)write_buf, write_size, 1000))result = SFUD_ERR_TIMEOUT;}if (read_size > 0) {if (HAL_OK != HAL_SPI_Receive(&hspi1, (uint8_t*)read_buf, read_size, 1000))result = SFUD_ERR_TIMEOUT;}SPI_Stop();return result;
}
实现sfud_spi_port_init函数,注释后面提示Required的都是需要配置的
关于delay的配置,就随便用软件的方式延时一下就好了
void delay(void) {uint16_t count = 10000;while (count--) {__NOP();}
}sfud_err sfud_spi_port_init(sfud_flash* flash) {sfud_err result = SFUD_SUCCESS;/*** add your port spi bus and device object initialize code like this:* 1. rcc initialize* 2. gpio initialize* 3. spi device initialize* 4. flash->spi and flash->retry item initialize* flash->spi.wr = spi_write_read; //Required* flash->spi.qspi_read = qspi_read; //Required when QSPI mode enable* flash->spi.lock = spi_lock;* flash->spi.unlock = spi_unlock;* flash->spi.user_data = &spix;* flash->retry.delay = null;* flash->retry.times = 10000; //Required*/flash->spi.wr = spi_write_read;flash->retry.delay = delay;flash->retry.times = 10000;return result;
}
测试SFUD和芯片是否移植成功
在main.c内引入 sfud.h文件

int fputc(int ch, FILE* f) {HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 1000);return ch;
}// sfud测试
void es(void) {printf("擦除数据\r\n");const sfud_flash* flash = sfud_get_device(0);sfud_erase(flash, 0, 100);
}void ws(void) {//获取到设备Flashconst sfud_flash* flash = sfud_get_device(0);uint8_t buff[300] = {0};for (uint16_t i = 0; i < 300; i++) {buff[i] = i;}//先擦除在写入sfud_erase_write(flash, 0, 300, buff);
}void rs(void) {const sfud_flash* flash = sfud_get_device(0);uint8_t buff[300] = {0};sfud_read(flash, 0, 300, buff);for (uint16_t i = 0; i < 300; i++) {printf("%d ", buff[i]);if (i % 10 == 0) {printf("\r\n");}}
}
// sfud测试int main(void) {/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_SPI1_Init();//MX_FATFS_Init();MX_USART1_UART_Init();/* USER CODE BEGIN 2 */sfud_init();rs();HAL_Delay(1000);ws();HAL_Delay(1000);rs();/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1) {/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}
先读取数据在写入数据,再读出来,看看数据是否一致,读取数据就是把1变0,擦除就是把0变1,所以默认都是1,所以读出来都是255

配置FATFS
打开user_diskio.c 文件,修改USER_initialize和USER_status函数
/*** @brief Initializes a Drive* @param pdrv: Physical drive number (0..)* @retval DSTATUS: Operation status*/
DSTATUS USER_initialize(BYTE pdrv /* Physical drive nmuber to identify the drive */
) {/* USER CODE BEGIN INIT */Stat &= ~STA_NOINIT;return Stat;/* USER CODE END INIT */
}/*** @brief Gets Disk Status* @param pdrv: Physical drive number (0..)* @retval DSTATUS: Operation status*/
DSTATUS USER_status(BYTE pdrv /* Physical drive number to identify the drive */
) {/* USER CODE BEGIN STATUS */Stat &= ~STA_NOINIT;return Stat;/* USER CODE END STATUS */
}
实现USER_read函数
DRESULT USER_read(BYTE pdrv, /* Physical drive nmuber to identify the drive */BYTE* buff, /* Data buffer to store read data */DWORD sector, /* Sector address in LBA */UINT count /* Number of sectors to read */
) {/* USER CODE BEGIN READ */const sfud_flash* flash = sfud_get_device(0);uint32_t read_addr = sector * 4096;sfud_read(flash, read_addr, count * 4096, buff);return RES_OK;/* USER CODE END READ */
}
实现USER_write函数
DRESULT USER_write(BYTE pdrv, /* Physical drive nmuber to identify the drive */const BYTE* buff, /* Data to be written */DWORD sector, /* Sector address in LBA */UINT count /* Number of sectors to write */
) {/* USER CODE BEGIN WRITE */const sfud_flash* flash = sfud_get_device(0);uint32_t write_addr = sector * 4096;sfud_erase_write(flash, write_addr, count * 4096, buff);/* USER CODE HERE */return RES_OK;/* USER CODE END WRITE */
}
实现USER_ioctl函数
DRESULT USER_ioctl(BYTE pdrv, /* Physical drive nmuber (0..) */BYTE cmd, /* Control code */void* buff /* Buffer to send/receive control data */
) {/* USER CODE BEGIN IOCTL */DRESULT res = RES_OK;switch (cmd) {case CTRL_SYNC:break;case GET_SECTOR_COUNT:*((DWORD*)buff) = 1024;break;case GET_SECTOR_SIZE:*((WORD*)buff) = 4096;break;case GET_BLOCK_SIZE:*((DWORD*)buff) = 1;break;default:res = RES_PARERR;}return res;/* USER CODE END IOCTL */
}
打开ffconf.h文件,修改__MAX_SS为4096

进去到fatfs.c文件,USERPath是文件的起始路径,这里使用 \,前面在转义一下

如果我们这里设置起始路径的话,在ff_gen_drv.c文件内需要注释掉path代码,如果不设置的话会给我们一个默认的起始路径,这里就不需要注释掉了

回到main.c文件内,取消注释FATFS初始化函数

测试FATFS文件系统
// fatfs测试
void mkfs(void) {uint8_t res = f_mkfs(USERPath, 1, _MAX_SS);printf("mkfs res: %d\r\n", res);
}FATFS fs;void mnt(void) {uint8_t res = f_mount(&fs, USERPath, 1);printf("mnt res: %d\r\n", res);
}FIL file;void create_file(char* file_name, char* content) {int res = 0;int bwritten = 0;printf("Create file :%s\r\n", file_name);// 创建一个a.txt文件res = f_open(&file, file_name, FA_WRITE | FA_CREATE_ALWAYS);printf("Create file res:%d\r\n", res);// 写入数据到文件中res = f_write(&file, content, strlen(content), (UINT*)&bwritten);printf("write file res:%d\r\n", res);// 关闭文件f_close(&file);
}void list_files(char* path) {printf("file list:\r\n");FILINFO fno;DIR dir;FRESULT res;res = f_opendir(&dir, path);if (res == FR_OK) {while (1) {res = f_readdir(&dir, &fno);if (res != FR_OK || fno.fname[0] == 0)break;printf("%s\r\n", fno.fname);}f_closedir(&dir);}
}void read_file(char* file_name, char* content) {int res = 0;int bread = 0;printf("Show File Content:%s\r\n", file_name);res = f_open(&file, file_name, FA_READ);printf("Open file res : %d\r\n", res);res = f_read(&file, content, 100, (UINT*)&bread);printf("Read file res : %d\r\n", res);f_close(&file);
}
// fatfs测试int main(void) {/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_SPI1_Init();MX_FATFS_Init();MX_USART1_UART_Init();/* USER CODE BEGIN 2 */sfud_init();mnt();mkfs();char content_a[100] = "hello world\r\nhello fatfs\r\nhello violet\r\n";create_file("a.txt", content_a);char content_b[100] = "37193719731831300\r\n";create_file("b.txt", content_b);char content_c[100] = "dadajdlajldjajajflajf\r\n";create_file("c.txt", content_c);list_files(USERPath);char read_content[100] = {0};read_file("a.txt", read_content);printf("%s\r\n", read_content);memset(read_content, 0, sizeof(read_content));read_file("b.txt", read_content);printf("%s\r\n", read_content);memset(read_content, 0, sizeof(read_content));read_file("c.txt", read_content);printf("%s\r\n", read_content);// rs();// HAL_Delay(1000);// ws();// HAL_Delay(1000);// rs();/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1) {/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}
结果

测试成功,完成了对FATFS的移植了,喜欢的话点个关注吧。
相关文章:
STM32F103ZET6移植FATFS文件系统教程(W25Q32)
一、FATFS核心特性 跨平台支持 支持FAT12/FAT16/FAT32格式,兼容Windows文件系统; 采用标准C语言编写,代码量小且支持RTOS。 配置灵活性 通过宏定义实现功能裁剪,例如: FF_FS_READONLY:设为1时禁…...
STM32 模块化开发实战指南:系列介绍
本文是《STM32 模块化开发实战指南》系列的导读篇,旨在介绍整个系列的写作目的、适用读者、技术路径和每一篇的主题规划。适合从事 STM32、裸机或 RTOS 嵌入式开发的个人开发者、初创工程师或企业项目团队。 为什么要写这个系列? 在嵌入式开发中,很多人刚开始都是从点亮一个…...
AF3 create_alignment_db_sharded脚本create_shard函数解读
AlphaFold3 create_alignment_db_sharded 脚本在源代码的scripts/alignment_db_scripts文件夹下。 该脚本中的 create_shard 函数的功能是将一部分链(shard_files)中的所有对齐文件写入一个 .db 文件,并返回这些链的索引信息(字节…...
【Python语言基础】21、Python标准库
文章目录 1. 标准库1.1 标准库构成及特点1.2 常见分类和模块1.3 标准库使用 1. 标准库 Python 标准库就像是 Python 自带的 “百宝箱”,里面装了各种各样已经写好的工具,你在编程的时候可以直接拿来用,不用自己再费劲去编写。 什么是标准库 …...
数据库脱裤
假设你已经getshell 找到mysql账号密码。 网站要连接mysql,就需要把mysql的账号密码保存在一个php文件中,类似config.php、common.inc.php等,在shell中,读取这些文件,找到其中信息即可 下面是一些常见平台的配置文…...
信刻电子档案蓝光光盘刻录安全检测长期归档
信刻一直致力于为档案馆、各行业档案部门,提供跨网数据交换、电子档案数据磁光异质备份归档解决方案。所研制的电子档案光盘智能长期归档系统,满足国产环境下”刻、管、存、检、用”全生命周期管理应用需求,能够提供一份离线归档、一份近线存…...
vue3中,element-plus中el-input的v-model和value的用法示例
el-input的v-model,邦定响应式变量 <el-col :span"6"><el-form-item label"检验类别" prop"verifyType"><el-input v-model"applyAllInfo.applyBasicInfo.verifyTypeName" readonly /></el-form-item…...
文章记单词 | 第33篇(六级)
一,单词释义 poison [ˈpɔɪzn] n. 毒药;毒物;有害的思想(或心情等);vt. 毒死;毒害;下毒;在… 中放毒;污染;adj. 有毒的justification [ˌdʒʌ…...
深度学习算法:从基础到实践
简介 深度学习作为人工智能领域的一个重要分支,近年来在多个领域取得了显著的成就。本文将从基础概念出发,探讨深度学习算法的核心原理,并介绍一些实际应用案例。 深度学习算法的核心概念 深度学习算法基于人工神经网络,通过构…...
L2-052 吉利矩阵分
L2-052 吉利矩阵 - 团体程序设计天梯赛-练习集 所有元素为非负整数,且各行各列的元素和都等于 7 的 33 方阵称为“吉利矩阵”,因为这样的矩阵一共有 666 种。 本题就请你统计一下,把 7 换成任何一个 [2,9] 区间内的正整数 L,把矩…...
计算机网络中各种物理量的单位总结
在计算机网络中,数据速率的单位容易混淆,以下是清晰总结: 一、基本单位区分 比特(bit)与字节(Byte) 小写 b 表示 比特(bit),是数据传输的基本单位。 大写 B…...
Solidity私有函数和私有变量区别,私有变量可以被访问吗
web3面试题 私有函数和私有变量区别,私有变量可以被访问吗 ChatGPT said: 在 Web3 开发,尤其是使用 Solidity 编写智能合约时,关于私有函数和私有变量的区别是常见的面试题。下面是详细解析: ✅ 私有函数(Private Fu…...
解决JSON格式数据大小写问题,以及@JsonProperty 和@JSONField序列化的区别
1、JsonProperty注解方式 JsonProperty注解是annotation包下的一个注解,可以通过value属性定义注解修饰的属性名称,如果你用的是JsonProperty注解,那么你千万不要用JSONObject.toJSONString(实体)去转json,可能很多人在这里就蒙蔽…...
Python正则表达式有哪些常用匹配字符?
处理文本数据时,我们经常需要查找、提取或替换特定模式的字符串。这时候正则表达式就成了程序员最强大的武器之一。今天我们就来详细聊聊Python中那些最常用的正则表达式字符和它们的实际用法。 为什么要学正则表达式? 假设你遇到这些场景:…...
List、Set集合通过Stream流求和
目录 一、泛型为Integer、Long、Double、BigDecimal求和 二、泛型为实体类 对单个属性求和 对多个属性分别分组求和 并返回聚合后的对象 多字段乘积求和(基本数据类型) 多字段乘积求和(BigDecimal) 对对象中的多个字段求和…...
Linux:Makefile
编译器gcc 使用方式:gcc [ 选项 ] 要编译的⽂件 [ 选项 ] [ ⽬标⽂件 ] 编译分为以下几个步骤: 1.预处理(进⾏宏替换) 预处理功能主要包括宏定义,⽂件包含,条件编译,去注释等。 预处理指令是以#号开头的代码⾏。 实例: gcc –E hello.c –o hello…...
基于双闭环PID控制器的永磁同步电机控制系统匝间故障Simulink仿真
欢迎微♥关注“电击小子程高兴的MATLAB小屋”获取巨额优惠 1.模型简介 本仿真模型基于MATLAB/Simulink(版本MATLAB 2013Rb)软件。建议采用matlab2013 Rb及以上版本打开。(若需要其他版本可联系代为转换,高于该版本的matlab均可正…...
硬件电路设计之51单片机(2)
声明:绘制原理图和PCB的软件为嘉立创EDA。根据B站尚硅谷嵌入式之原理图&PCB设计教程学习所作个人用笔记。 目录 一、原理图详解 1、TypeC接口 (1)TypeC接口介绍 (2)TypeC原理图 2、5V转3.3V 3、单片机电源开…...
从零开始学习PX4源码20(遥控器模式切换如何执行)
目录 文章目录 目录摘要1.用到的消息和主题2.遥控器切换模式代码流程摘要 本节主要学习PX4的手动遥控器切换模式,具体是如何实现的,具体改变了哪些变量,和模式管理有什么联系。 1.用到的消息和主题 1.行为请求消息:ActionRequest.msg ///时间信息 uint64 timestamp # t…...
SpringAI+DeepSeek大模型应用开发——1 AI概述
AI领域常用词汇 LLM(LargeLanguage Model,大语言模型) 能理解和生成自然语言的巨型AI模型,通过海量文本训练。例子:GPT-4、Claude、DeepSeek、文心一言、通义干问。 G(Generative)生成式: 根据上…...
经济指标学习(一)
系列文章目录 文章目录 系列文章目录1、市净率**一、定义与计算****二、核心意义****三、应用场景****四、局限性****五、分类与衍生指标****总结** 2、市销率**一、定义与计算****二、核心意义****三、优缺点分析****四、适用场景****五、与其他指标的对比****六、实际应用案例…...
理解 results = model(source, stream=True) 的工作原理和优势
1. 核心概念解析 (1) streamTrue 的作用 生成器模式:当处理视频或图像序列时,streamTrue 会将结果包装成一个 生成器(Generator),逐帧生成 Results 对象,而不是一次性返回所有结果。内存优化:…...
国内互联网大厂推出的分布式数据库 的详细对比,涵盖架构、性能、适用场景、核心技术等维度
以下是 国内互联网大厂推出的分布式数据库 的详细对比,涵盖架构、性能、适用场景、核心技术等维度: 一、主流分布式数据库列表 大厂数据库名称类型适用场景发布时间腾讯云TDSQL分布式HTAP金融、电商、游戏、政企2010年阿里云OceanBase分布式HTAP银行核…...
解释`new`关键字的执行过程,并手动实现一个`myNew`函数。
在 JavaScript 中,new 关键字用于创建一个用户定义的对象实例。它的执行过程分为以下步骤: new 关键字的执行过程 创建空对象: 创建一个新的空对象,其 [[Prototype]](即 __proto__)指向构造函数的 prototy…...
Android 项目配置文件解释
Android 项目配置文件解释 目录 Android 项目配置文件解释1. `plugins` 块2. `android` 块3. `dependencies` 块为什么需要 JDK 和 Kotlin1. plugins 块 plugins {id com.android.applicationid org.jetbrains.kotlin.android }id com.android.application:应用 Android 应用…...
亚马逊热销变维权?5步搭建跨境产品的安全防火墙
“产品热卖,引来维权”——这已经悄然成为越来越多跨境卖家的“热销烦恼”。曾经拼品拼量,如今却要步步谨慎。商标侵权、专利投诉、图片盗用……这些问题一旦发生,轻则下架、账号被限,重则冻结资金甚至封店。 别让“热销”变“受…...
C语言——分支语句
在现实生活中,我们经常会遇到作出选择和判断的时候,在C语言中也同样要面临作出选择和判断的时候,所以今天,就让我们一起来了解一下,C语言是如何作出选择判断的。 目录 1.何为语句? 2.if语句 2.1 if语句的…...
绿盟二面面试题
5000篇网安资料库https://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247486065&idx2&snb30ade8200e842743339d428f414475e&chksmc0e4732df793fa3bf39a6eab17cc0ed0fca5f0e4c979ce64bd112762def9ee7cf0112a7e76af&scene21#wechat_redirect 1. 原理深度&…...
deepseek生成流程图
目录 Mermaid流程图需求询问框架交互显示流程图markdown在线网站 可能会出现的问题语法报错 在职场中,借助AI生成图表是提升效率的重要技能,本篇我们讲解如何使用deepseek生成流程图 Mermaid流程图 需求 学习太差劲了,我想要一个比较好的学…...
界面控件DevExpress WPF v25.1新功能预览 - 文档处理类功能升级
DevExpress WPF拥有120个控件和库,将帮助您交付满足甚至超出企业需求的高性能业务应用程序。通过DevExpress WPF能创建有着强大互动功能的XAML基础应用程序,这些应用程序专注于当代客户的需求和构建未来新一代支持触摸的解决方案。 无论是Office办公软件…...
