STM32_SD卡的SDIO通信_基础读写
本篇将使用CubeMX+Keil, 创建一个SD卡读写的工程。
目录
一、SD卡要点速读
二、SDIO要点速读
三、SD卡座接线原理图
四、CubeMX新建工程
五、CubeMX 生成 SD卡的SDIO通信部分
六、Keil 编辑工程代码
七、实验效果
实现效果,如下图:
一、SD卡 速读
SD卡,全称Secure Digital Memory Card(安全数码卡),是嵌入式设备上常用的一种存储介质。
1、尺寸大小 分类
按卡的大小分类,可以为3种:
- 标准SD卡 :体积较大,卡侧带写保护开关;常见于相机和摄像机中,用于存储高分辨率照片和视频;
- mini SD卡 :现在较少看到,已逐渐被microSD卡取代;
- Micro SD卡:旧称 TF卡,2004年更名为 Micro SD Card, 常用于扩展手机和平板电脑的存储空间。
每种卡形状大小不一,但功能一样:遵循相同的 SD卡协议、相同的命令集、相同的块大小(512)。只需确保SDIO引脚配置正确,并且遵循SD卡协议发送正确的命令,程序即可通用。
都是SD卡,但习惯上,标准SD叫SD卡,Micro SD叫TF卡。
目前,STM32开发板、Linux开发板 等,预留的卡座,一般是TF卡座,因为它占用空间最少。
2、卡的容量及标准 分类
在SD卡的表面丝印上,会有HC、XC等字样,表示它所使用的存储标准。
- SD: 早期的版本,基本停用,最高 2GB, 分区格式为 FAT12(FAT)、FAT16。
- SDHC:容量范围 2GB ~ 32GB, 分区格式为 FAT32。
- SDXC:容量范围 32GB ~ 2TB, 分区格式为 exFAT。
- SDUC:容量范围 2TB ~ 128TB, 分区格式为 exFAT。
3、SD卡的传输速度
SD卡的可变时钟频率:0~25MHz。当运行在25M+数据带宽4位时,最大理论传输速度是12.5MB/s。
而操作中,会明显低于理论速度,其受限于不同品牌的芯片优化、制造工芯、采用标准等。
SD卡是Flash存储,读写速度特点是:读快、写慢。
SD卡的最低写入速度,用Class等级来标识。
在表面丝印上,一般会有Class字样,它后面的数表示最低写入速度,单位是:MB/s。
或者,会用一个外面带半圆的数字表示。
- Class 2:2MB/s
- Class 4:4MB/s
- Class 6:6MB/s
- Class 10:10MB/s
附:常用的SD卡读写速率参考,非严谨值。
SD卡容量 | 文件系统 | 写入速度 | 读取速度 |
32G(SDHC) | FAT32 | 2MB/s | 8MB/s |
32G(SDHC) | exFAT | 3.5MB/s | 8.5MB/s |
64G(SDXC) | exFAT | 4MB/s | 8.5MB/s |
4、SD卡的使用寿命
一般是指:擦除的最大次数。
写入数据时需要先擦除扇区内容。读数据是不影响使用寿命的,写数据才会影响使用寿命。
因此,应避免频繁地对同一地址(扇区)进行写数据。如:使用程序每隔一秒保存一次数据到同一地址,这是不妥当的。
- TLC:1000~3000次
- MLC:3000 ~1万次
- SLC:可达10万次
擦写次数对使用寿命影响较小,而更容易直接“致死”的是:带电插拔,很容易坏卡,主要是静电原因!
二、SDIO要点速读
原理比较复杂,有兴趣的请自行csdn搜更详细的技术文档,或STM32的官方文档。
- SD卡的读写通信操作,可以用 SPI、SDIO,本示例使用SDIO。
- SDIO接口是在SD内存卡接口的基础上发展起来的;
- SDIO接口除了能读写SD内存卡,还能连接其它SDIO接口的设备;
- 常用的STM32F103C8,没有SDIO接口,F103系列R型号起,才带SDIO;
- STM32F4系列芯片,带更完善的SDIO主机接口,能与MMC卡、SD卡、SDI/0卡、EC-ATA设备进行通信;
- 三种总线模式:1-bit、4-bit、8-bit(不常用);
三、SD卡座接线原理图
STM32的SDIO外设与SD卡通信,通用接线如下图。
注:当使用弹簧式SD卡座,会有第9个脚(CD), 可不接。它用于判断SD卡是否插入,当插入SD卡时,此脚输出低电平。
四、CubeMX新建工程
建议复制一个已带UART1、printf的工程,这样更省时。
如果没有,可参考以下步骤。
1、新建一个普通的工程
新手可参考如下图解,老司机请直接跳过。
【STM32+CubeMX】 新建工程_STM32F407
2、为工程添加UART1通信、printf输出
用于把SD卡的测试信息,(通过USB转TTl),输出到串口助手观察。
如果,你已知晓如何通过printf输出信息,自行添加,跳过即可。
USART1 DMA发送、DMA空闲中断 接收不定长数据
UART1 快速实现移植、通信 ( bsp_UART.c 、bsp_UART.h)
五、CubeMX 配置 SD卡的SDIO 初始化
通过 CubeMX配置SDIO, 极度简单。
本节为方便测试,只使用普通的读写方式,后续篇章再添加DMA、FATFS等方式。
1、使能SDIO
- Mode:选择SD的四线模式,即 SD 4 bits Wide bus.
- 参数部分:F4系列不用修改配置,默认即可。F103系列,需把时钟分频系数修改为 6,即SDIOCLK Clock divide factor这一项,由默认0改为6, 不然会通信失败。
2、时钟设置
进入时钟树配置页面。
这时可能会弹出一个询问窗:是否自动配置所需时钟?
选择:NO ,手动修改即可。
如果Yes,它将针对已使能的SDIO进行必须值的配置,而系统时钟值,会被修改为其它值。不推荐。
F4系列,如果板上是25M的晶振,用如下参数值;要是8M的晶振,修改晶振、分频两处为8即可。
重点:箭头所指的Q值,它用于控制USB 、SDIO和随机数生成器的时钟。
这个时钟,必须是 48M !
好了,已完成配置。
重新生成工程,即可!
六、Keil 编辑工程代码
1、打开keil 工程,先重新编译一次。
- 正常情况,编译是0 Error的。
- 如果有Error, 应该是新建工程时,路径、名称有中文了,重新开建工程,用英文即可。
2、重要修改:SD卡的初始化,使用 1-bit 模式
CubeMX生成的SDIO初始化代码,有一个bug,需要手动修改,操作如下:
- 编译后,右击 main.c 文件中函数 MX_SDIO_SD_Init(),
- 在弹出菜单中:Go To Definition Of ...; 将跳转到SD卡初始化函数;
跳转到 sdio.c文件的 MX_SDIO_SD_Init()函数内部后,
把下图位置中的 4B,改为 1B ;
它下面还有一个4B,不用修改,只修改刚才那个即可。不要改错位置了!
重点:每次重新生成后,都要手动修改一次。如果不修改,初始化过程会导致程序卡死。
3、编写 读写测试 代码
SD卡的基础读写函数比较简单,常用的函数共4个。
1、获取SD卡信息
HAL_SD_CardInfoTypeDef pCardInfo = {0}; // SD卡信息结构体
HAL_SD_GetCardInfo(&hsd, &pCardInfo); // 获取 SD 卡的信息2、读数据
HAL_SD_ReadBlocks(&hsd, aOldData, 7, 2, 3000); // SD卡的句柄、数据、块地址、块数量、超时ms3、写数据
HAL_SD_WriteBlocks(&hsd, aTestData, 7, 2, 3000) // SD卡的句柄、数据、块地址、块数量、超时ms4、擦除数据
HAL_SD_Erase(&hsd, 7, 8) // SD卡的句柄、块起始地址、块结束地址
在 main函数内的 /* USER CODE BEGIN 2 */ 注释下方,编写以下代码(可复制):
示例代码里已附详细注释,比较容易理解,流程是:
- 获取SD卡信息
- 读取测试块的原数据
- 写入测试
- 擦除测试
- 写回原数据
/***************** SD卡读写通信测试 *****************//* 1、获取卡信息,打印到串口助手 *//* 2、读测试:读出测试位置原数据,保存在 aOldData[] *//* 3、写测试:在测试的块上,写入指定数据 *//* 读出刚才写入的块数据,打印到串口助手观察 *//* 4、擦除测试:擦除指定块上的数据 *//* 读出刚才擦除块的数据,打印到串口助手观察 *//* 5、写回原数据到指定位置 *//* 读出刚才写入的块数据,打印到串口助手观察 */ #define SD_TEST_SIZE 1024 // 测试数据的字节数,刚好是2个块大小:2x512static uint8_t aOldData[SD_TEST_SIZE] = {0}; // 用于存放旧数据,先读出来,测试完了,再把旧数据写回去static uint8_t aTestData[SD_TEST_SIZE] = {0}; // 临时缓存,用来存放测试数据HAL_SD_CardInfoTypeDef pCardInfo = {0}; // SD卡信息结构体uint8_t status = HAL_SD_GetCardState(&hsd); // SD卡状态标志值if (status == HAL_SD_CARD_TRANSFER){/* 1、获取卡信息,打印到串口助手 */HAL_SD_GetCardInfo(&hsd, &pCardInfo); // 获取 SD 卡的信息printf("\r1、获取SD卡信息 ... \r\n");printf("卡类型:%d \r\n", pCardInfo.CardType); // 类型返回:0-SDSC、1-SDHC/SDXC、3-SECUREDprintf("卡版本:%d \r\n", pCardInfo.CardVersion); // 版本返回:0-CARD_V1、1-CARD_V2printf("块数量:%d \r\n", pCardInfo.BlockNbr); // 可用的块数量printf("块大小:%d \r\n", pCardInfo.BlockSize); // 每个块的大小; 单位:字节printf("卡容量:%lluG \r\n", ((unsigned long long)pCardInfo.BlockSize * pCardInfo.BlockNbr) / 1024 / 1024 / 1024); // 计算卡的容量HAL_Delay(1000); // 重要:稍作延时再开始读写测试; 避免有些仿真器烧录期间的多次复位,短暂运行了程序,导致下列读写数据不完整。 /* 2、读测试:读出测试位置原数据,保存在 aOldData[] */printf("\r2、读取测试块的原数据 ... \r\n");memset(aOldData, 0, SD_TEST_SIZE); // 清0数组的数据if (HAL_SD_ReadBlocks(&hsd, aOldData, 7, 2, 3000) == HAL_OK) // 读SD卡数据块; 参数:SD结构体、数据地址、块起始地址、读的块数量、超时时间;{while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); // 等待卡的读写操作结束for (uint32_t i = 0; i < SD_TEST_SIZE; i++) // 打印 原数据printf("%X ", aOldData[i]);printf("\r\n");}else{printf("SD卡 读测试 失败!\n"); }/* 3-1、写测试:在测试的块上写入数据 */printf("\r3、SD卡 写入测试 ...\r\n");memset(aTestData, 0x8, SD_TEST_SIZE); // 为数组准备要写入的测试数据:整个数组填充指定值if (HAL_SD_WriteBlocks(&hsd, aTestData, 7, 2, 3000) == HAL_OK) // 向SD卡写入数据块; 参数:SD结构体、数据地址、块起始地址、写入的块数量、超时时间;{while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); // 等待卡的读写操作结束printf("对指定块写入结束! \r写入的数据是:\n");for (uint32_t i = 0; i < SD_TEST_SIZE; i++) // 打印 写入的数据printf("%X ", aTestData[i]);printf("\r\n");}else{printf("SD卡 写测试 失败!\n");}/* 3-2、读出刚才写测试的块内数据 */printf("\r现在块内的数据是:\r\n");memset(aTestData, 0, SD_TEST_SIZE); // 清0数组的数据if (HAL_SD_ReadBlocks(&hsd, aTestData, 7, 2, 3000) == HAL_OK) // 读SD卡数据块; 参数:SD结构体、数据地址、块起始地址、读的块数量、超时时间;{while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); // 等待卡的读写操作结束for (uint32_t i = 0; i < SD_TEST_SIZE; i++) // 打印 写入后块内现在数据printf("%X ", aTestData[i]);printf("\r\n");}else{printf("SD卡 读测试 失败!\n");}/* 4-1、擦除测试:擦除指定块上的数据 */printf("\r4、擦除块测试 ...\r\n");if (HAL_SD_Erase(&hsd, 7, 8) == HAL_OK) // 擦除SD卡上的数据; 参数:SD结构体、块的起始地址、块的结束地址{while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); // 等待卡的读写操作结束printf("擦除 成功! \r\n");}else{printf("擦除 失败! \r\n");}/* 4-2、读取,擦除后指定块上的数据 */printf("擦除后,现在块内的数据是:\r\n");memset(aTestData, 0, SD_TEST_SIZE); // 清0数组的数据if (HAL_SD_ReadBlocks(&hsd, aTestData, 7, 2, 3000) == HAL_OK) // 读SD卡数据块; 参数:SD结构体、数据地址、块起始地址、读的块数量、超时时间;{while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); // 等待卡的读写操作结束for (uint32_t i = 0; i < SD_TEST_SIZE; i++) // 打印 块内现在的数据printf("%X ", aTestData[i]);printf("\r\n");}else{printf("SD卡 读测试 失败!\n");}/* 5-1、写回测试块上的原数据 */printf("\r5、写回原数据 ...\r\n");//memset(aOldData, 1, SD_TEST_SIZE);if (HAL_SD_WriteBlocks(&hsd, aOldData, 7, 2, 3000) == HAL_OK) // 向SD卡写入数据块; 参数:SD结构体、数据地址、块起始地址、写入的块数量、超时时间;{while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); // 等待卡的读写操作结束printf("写入结束! \n");}else{printf("SD卡 写回原数据 失败!\n");} /* 5-2、读取,写入后的数据 */printf("现在块内的数据是: \r\n");memset(aTestData, 0, SD_TEST_SIZE); // 清0数组的数据if (HAL_SD_ReadBlocks(&hsd, aTestData, 7, 2, 3000) == HAL_OK) // 读SD卡数据块; 参数:SD结构体、数据地址、块起始地址、读的块数量、超时时间;{while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); // 等待卡的读写操作结束for (uint32_t i = 0; i < SD_TEST_SIZE; i++) // 打印 块内现在的数据printf("%X ", aTestData[i]);printf("\r\n\r\n");}else{printf("SD卡 读测试 失败! \r\n");}printf("SD卡 读写测试结束!\r\n");}
完成后,位置如下图:
七、实验效果
程序运行后,串口助手输出如下:
如有错漏 ,望指正~~~!
相关文章:

STM32_SD卡的SDIO通信_基础读写
本篇将使用CubeMXKeil, 创建一个SD卡读写的工程。 目录 一、SD卡要点速读 二、SDIO要点速读 三、SD卡座接线原理图 四、CubeMX新建工程 五、CubeMX 生成 SD卡的SDIO通信部分 六、Keil 编辑工程代码 七、实验效果 实现效果,如下图: 一、SD卡 速读…...
【Docker】私有Docker仓库的搭建
一、准备工作 确保您的系统已安装Docker。如果没有安装,请参考Docker官方文档进行安装。 准备一个用于存储仓库数据的目录,例如/registry_data/。 二、拉取官方registry镜像 首先,我们需要从Docker Hub拉取官方的registry镜像。执行以下命…...

linux 管道符、重定向与环境变量
1. 输入输出重定向 在linux工作必须掌握的命令一文中,我们已经掌握了几乎所有基础常用的Linux命令,那么接下来的任务就是把多个命令适当的组合到一起,使其协同工作,会更高效的处理数据,做到这一点就必须搞清楚命令的输…...

Ansible fetch模块详解:轻松从远程主机抓取文件
在自动化运维的过程中,我们经常需要从远程主机下载文件到本地,以便进行分析或备份。Ansible的fetch模块正是为了满足这一需求而设计的,它可以帮助我们轻松地从远程主机获取文件,并将其保存到本地指定的位置。在这篇文章中…...

wireshark工具简介
目录 1 wireshark介绍 2 wireshark抓包流程 2.1 选择网卡 2.2 停止抓包 2.3 保存数据 3 wireshark过滤器设置 3.1 显示过滤器的设置 3.2 抓包过滤器 4 wireshark的封包列表与封包详情 4.1 封包列表 4.2 封包详情 参考文献 1 wireshark介绍 wireshark是非常流行的网络…...
51单片机——按键控制LED流水灯
引言 在电子制作和嵌入式系统学习中,51 单片机是一个经典且入门级的选择。按键控制 LED 流水灯是 51 单片机的一个基础应用,通过这个实例,我们可以深入了解单片机的输入输出控制原理。 51 单片机简介 51 单片机是对所有兼容 Intel 8051 指…...

【opencv】第9章 直方图与匹配
第9章 直方图与匹配 9.1 图像直方图概述 直方图广泛运用于很多计算机视觉运用当中,通过标记帧与帧之间显著的边 缘和颜色的统计变化,来检测视频中场景的变化。在每个兴趣点设置一个有相近 特征的直方图所构成“标签”,用以确定图像中的兴趣点。边缘、色…...

HTML5 Web Worker 的使用与实践
引言 在现代 Web 开发中,用户体验是至关重要的。如果页面在执行复杂计算或处理大量数据时变得卡顿或无响应,用户很可能会流失。HTML5 引入了 Web Worker,它允许我们在后台运行 JavaScript 代码,从而避免阻塞主线程,保…...

MVCC底层原理实现
MVCC的实现原理 了解实现原理之前,先理解下面几个组件的内容 1、 当前读和快照读 先普及一下什么是当前读和快照读。 当前读:读取数据的最新版本,并对数据进行加锁。 例如:insert、update、delete、select for update、 sele…...

基于ESP32-IDF驱动GPIO输出控制LED
基于ESP32-IDF驱动GPIO输出控制LED 文章目录 基于ESP32-IDF驱动GPIO输出控制LED一、点亮LED3.1 LED电路3.2 配置GPIO函数gpio_config()原型和头文件3.3 设置GPIO引脚电平状态函数gpio_set_level()原型和头文件3.4 代码实现并编译烧录 一、点亮LED 3.1 LED电路 可以看到&#x…...

【优选算法】9----长度最小的子数组
----------------------------------------begin-------------------------------------- 铁子们,前面的双指针算法篇就算告一段落啦~ 接下来是我们的滑动窗口篇,不过有一说一,算法题就跟数学题一样,只要掌握方法,多做…...

LabVIEW太阳能照明监控系统
在公共照明领域,传统的电力照明系统存在高能耗和维护不便等问题。利用LabVIEW开发太阳能照明监控系统,通过智能控制和实时监测,提高能源利用效率,降低维护成本,实现照明系统的可持续发展。 项目背景 随着能源危机…...
MongoDB中单对象大小超16M的存储方案
在 MongoDB 中,单个文档的大小限制为 16MB。如果某个对象(文档)的大小超过 16MB,可以通过以下几种方案解决: 1. 使用 GridFS 适用场景:需要存储大文件(如图像、视频、文档等)。 原…...

三维激光扫描-用智能检测系统提升效率
当下,企业对生产效率和质量控制的要求越来越高。传统的检测方法往往难以满足高精度、快速响应的需求。三维激光扫描技术结合智能检测系统,为工业检测带来了革命性的变革。 传统检测方法的局限性 传统检测方法主要依赖于人工测量和机械检测工具…...
css遇到的一些问题
1.vw单位,在PC端vw单位是包含右侧滚轮的宽度,而在移动端不会包含滚轮的长度,在PC端运用vw单位进行居中对齐,会比实际偏左盒子偏右一点,因为内容区域并不包含滚轮。 2.运用媒体查询进行响应式布局式,媒体查询…...

【langgraph】ubuntu安装:langgraph:未找到命令
langgraph 在ubuntu24.04 参考:langgraph运行:报错: (05_ep_dev) root@k8s-master-pfsrv:/home/zhangbin/perfwork/01_ai/05_ep_dev/expert# langgraph dev langgraph:未找到命令查看langraph的安装情况 pip show langgraph...

mysql 学习2 MYSQL数据模型,mysql内部可以创建多个数据库,一个数据库中有多个表;表是真正放数据的地方,关系型数据库 。
在第一章中安装 ,启动mysql80 服务后,连接上了mysql,那么就要 使用 SQL语句来 操作mysql数据库了。那么在学习 SQL语言操作 mysql 数据库 之前,要对于 mysql数据模型有一个了解。 MYSQL数据模型 在下图中 客户端 将 SQL语言&…...
小识JVM堆内存管理的优化机制TLAB
JVM(Java虚拟机)在堆内存分配空间时,TLAB(Thread Local Allocation Buffer,线程本地分配缓存区)是一种重要的内存管理优化技术。以下是对TLAB的详细解释: 一、TLAB的定义 TLAB是JVM堆内存管理…...

ToDesk云电脑、顺网云、网易云、易腾云、极云普惠云横测对比:探寻电竞最佳拍档
一、云电脑:电竞新宠崛起 在电竞游戏不断发展的今天,硬件性能成为了决定游戏体验的关键因素。为了追求极致的游戏画面与流畅度,玩家们往往需要投入大量资金购置高性能电脑。然而,云电脑技术的出现,为玩家们提供了一种…...

学习ASP.NET Core的身份认证(基于JwtBearer的身份认证10)
基于Cookie传递token的主要思路是通过用户身份验证后,将生成的token保存到Response.Cookies返回客户端,后续客户端访问服务接口时会自动携带Cookie到服务端以便验证身份。之前一直搞不清楚的是服务端程序如何从Cookie读取token进行认证(一般都…...
Linux链表操作全解析
Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表?1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...

【Oracle APEX开发小技巧12】
有如下需求: 有一个问题反馈页面,要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据,方便管理员及时处理反馈。 我的方法:直接将逻辑写在SQL中,这样可以直接在页面展示 完整代码: SELECTSF.FE…...
什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南
文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果
vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...
ip子接口配置及删除
配置永久生效的子接口,2个IP 都可以登录你这一台服务器。重启不失效。 永久的 [应用] vi /etc/sysconfig/network-scripts/ifcfg-eth0修改文件内内容 TYPE"Ethernet" BOOTPROTO"none" NAME"eth0" DEVICE"eth0" ONBOOT&q…...
在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案
这个问题我看其他博主也写了,要么要会员、要么写的乱七八糟。这里我整理一下,把问题说清楚并且给出代码,拿去用就行,照着葫芦画瓢。 问题 在继承QWebEngineView后,重写mousePressEvent或event函数无法捕获鼠标按下事…...

uniapp 小程序 学习(一)
利用Hbuilder 创建项目 运行到内置浏览器看效果 下载微信小程序 安装到Hbuilder 下载地址 :开发者工具默认安装 设置服务端口号 在Hbuilder中设置微信小程序 配置 找到运行设置,将微信开发者工具放入到Hbuilder中, 打开后出现 如下 bug 解…...

什么是VR全景技术
VR全景技术,全称为虚拟现实全景技术,是通过计算机图像模拟生成三维空间中的虚拟世界,使用户能够在该虚拟世界中进行全方位、无死角的观察和交互的技术。VR全景技术模拟人在真实空间中的视觉体验,结合图文、3D、音视频等多媒体元素…...
用递归算法解锁「子集」问题 —— LeetCode 78题解析
文章目录 一、题目介绍二、递归思路详解:从决策树开始理解三、解法一:二叉决策树 DFS四、解法二:组合式回溯写法(推荐)五、解法对比 递归算法是编程中一种非常强大且常见的思想,它能够优雅地解决很多复杂的…...

网页端 js 读取发票里的二维码信息(图片和PDF格式)
起因 为了实现在报销流程中,发票不能重用的限制,发票上传后,希望能读出发票号,并记录发票号已用,下次不再可用于报销。 基于上面的需求,研究了OCR 的方式和读PDF的方式,实际是可行的ÿ…...