news 2026/9/9 10:33:00

CameraQRCode工程拆解:Android扫码从相机适配到解码器选型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CameraQRCode工程拆解:Android扫码从相机适配到解码器选型

简介:这是一份基于C#实现的二维码综合应用示例项目,面向需要快速掌握二维码生成、解析及摄像头实时识别的中初级开发者。项目围绕ZXing.Net与AForge.NET展开,演示了如何通过BarcodeWriter生成二维码、BarcodeReader解析图像,并结合AForge视频采集完成实时扫描,覆盖了图像处理与硬件交互的关键流程。压缩包共25个文件,体积约142KB,核心包括6个C#源代码文件、1个解决方案文件与项目配置,同时附带ZXing.dll、AForge.Video.dll等运行所需的第三方库及少量资源文件,结构简单,便于直接打开运行与学习。已有824人浏览学习,适合作为C#图像识别与硬件编程的入门参考。通过阅读并实际运行这套代码,用户可以快速理解二维码编解码的实现原理,掌握调用摄像头输出视频帧并实时解码的完整思路,并可在此基础上扩展为扫码签到、支付等实际应用场景。 说实话,第一次看到“CameraQRCode.rar”这个压缩包名字的时候,我心里是有点嫌弃的——一看就是随手打包的工程,没有版本号,没有说明文档。但这年头做扫码需求,往往就是从这样一个来路不明的压缩包开始的。客户说“参考一下这个Demo”,老板说“看看能不能直接用”,于是你只能老老实实解压、翻代码、跑Demo,再一点点把它变成能落地的东西。

这个工程从名字就能看出核心链路:Camera(相机采集)加 QRCode(二维码识别)。真正拆开之后你会发现,它其实涵盖了摄像头驱动调用、预览帧数据格式转换、旋转角对齐、解码器选型、对焦和曝光调优、甚至多平台相机适配等一系列问题。这篇文章我就以这份工程为线索,把一套扫码模块从底层到上层的实现思路、踩过的坑、以及最终工程化的经验完整捋一遍。适合正在做Android扫码、工业相机扫码,或者想从零搭一个“相机+识别”管线的朋友参考。

1. 解压之后先别急着跑——CameraQRCode的整体模块拆解

拿人手艺的第一步,永远是先搞清楚压缩包里到底有什么。这个工程解压后大体可以分成四个明确的层次,这也是大多数扫码项目的通用骨架。

1.1 从文件结构反推项目要解决的需求

只看“CameraQRCode”这个名字,容易以为它只是一个扫码Demo。但结合里面涉及的Camera驱动、MTK平台接口、工业相机SDK、索尼Camera Remote SDK等热搜词,你会发现它其实覆盖了“嵌入式/移动端相机扫码”的完整场景。

基本模块是这样的:

  • 相机采集层:负责打开摄像头、配置预览尺寸、设置对焦模式、获取每一帧图像数据。Android上通常是Camera2 + ImageReader,老工程可能是Camera1,有的还会用到厂商私有接口。
  • 图像预处理层:把相机吐出来的原始帧(一般是YUV或NV21)转成解码器能吃的格式,比如Bitmap或灰度图,同时处理旋转、裁剪、镜像等问题。
  • 解码层:对这个工程来说就是ZXing或者ZBar/OpenCV系解码器,接收图像输出二维码内容,有的还支持条码。
  • 业务回调层:把识别结果交给上层界面,触发声音、震动、跳转逻辑,或者把结果上传到后台。

我复现这个工程时,一开始就直接跑主界面,结果发现预览画面出来了,但扫什么码都没反应。后来才意识到,问题根本不在解码层,而在前置的图像链路——旋转角没对齐、裁剪区域不对、甚至格式转换时内存拷贝出错。所以解压之后第一步不是点运行,而是把每一层的数据流画清楚,搞清楚“帧从哪里来、经过什么处理、交给谁识别”。

1.2 核心链路的数据流:从sensor到Bitmap再到解码器

这个工程的识别管线可以抽象成一条数据流:Camera Sensor 产出原始帧数据 → 经过HAL和驱动到达应用层(Camera2的ImageReader/工业相机的SDK回调) → 应用层做格式转换(YUV_420_888转NV21,或者转成Bitmap) → 按需旋转和裁剪 → 送入Decoder → 返回结果。

这里最容易被忽视的是帧格式。很多新手写扫码,总想着“拿到的是Bitmap”,但Camera2回调的ImageReader默认是YUV_420_888格式,工业相机SDK甚至可能给你RAW或Bayer格式,索尼Camera Remote SDK的实时取景流又可能是RTSP或JPEG。格式不统一,后面的解码器就跑不起来。

我见过不少人卡在“预览正常但识别失败”这个问题上,最后定位原因都是格式转换写错了。比如YUV_420_888的planes数据是分开存储的,不能当作一个连续数组直接转NV21,必须按照rowStride和pixelStride逐个拷贝。下面这段是我在工程里实际改过的转换逻辑,精简后大概是这样的:

private byte[] yuv420ToNv21(Image image) { Image.Plane[] planes = image.getPlanes(); byte[] nv21 = new byte[image.getWidth() * image.getHeight() * 3 / 2]; ByteBuffer yBuffer = planes[0].getBuffer(); ByteBuffer uBuffer = planes[1].getBuffer(); ByteBuffer vBuffer = planes[2].getBuffer(); int ySize = image.getWidth() * image.getHeight(); int yPixelStride = planes[0].getPixelStride(); int yRowStride = planes[0].getRowStride(); // 拷贝Y分量 int pos = 0; for (int row = 0; row < image.getHeight(); row++) { yBuffer.position(row * yRowStride); for (int col = 0; col < image.getWidth(); col++) { nv21[pos++] = yBuffer.get(); } } // 拷贝UV交错数据(U和V分量) int uvRowStride = planes[1].getRowStride(); int uvPixelStride = planes[1].getPixelStride(); pos = image.getWidth() * image.getHeight(); for (int row = 0; row < image.getHeight() / 2; row++) { uBuffer.position(row * uvRowStride); vBuffer.position(row * uvRowStride); for (int col = 0; col < image.getWidth() / 2; col++) { nv21[pos++] = vBuffer.get(col * uvPixelStride); nv21[pos++] = uBuffer.get(col * uvPixelStride); } } return nv21; }

这段代码的关键在于不能直接拷贝整个buffer,因为底层为了字节对齐往往会在每行末尾填充多余数据,直接按照宽度截取会得到错位的图像。这里还有一个小细节:UV分量在YUV_420_888里可能是U在前或V在前,不同平台不一样,NV21要求V在前,顺序反了虽然不会崩溃,但颜色会明显偏绿偏紫,别问我怎么知道的。

2. 摄像头适配比想象中复杂——从Camera2到MTK到工业相机SDK的取舍

扫码项目有个让人头大的现实:你以为写一套相机调用代码就能通吃,结果Android设备阵营一千个厂商一千种脾气,再加上工业相机和其他嵌入式相机,完全不是一回事。

2.1 Android端:Camera2是主流,但厂商定制接口不能忽略

现在的Android工程基本不会再用Camera1了,Camera2虽然API设计恶心,但已经是唯一官方选择。从这个工程的代码来看,核心逻辑是ImageReader配CameraCaptureSession,预览尺寸选1920x1080,匹配大多数屏幕比例。

真正麻烦的是MTK这类平台。MTK的相机HAL在某些机型上对YUV_420_888的layout处理不太规范,特别是低端机上,planes的顺序可能跟标准定义不一致。还有人遇到过预览帧率只有15fps的问题,其实是HAL层默认开了降噪和HDR,帧率直接被拉低。这种问题应用层很难查,但有两个实用排查手段:

  • 打印实际回调帧的时间戳间隔,确认是不是稳定30fps。
  • 在CameraCharacteristics里读CONTROL_AE_AVAILABLE_MODES和CONTROL_AF_AVAILABLE_MODES,看看设备到底支持哪些模式。

这个工程在MTK设备上跑不通预览,最后是我把ImageReader的maxImages从2改成4,解决了一部分帧消费不及时的问题。HAL层有时候不会及时回收Image,如果你不快速close,帧就会被卡住,预览和识别都会异常。

2.2 工业相机与外部相机SDK:框架完全不一样但抽象思路相似

热搜词里出现了IDS Camera Manager、索尼Camera Remote SDK,这说明这套扫码逻辑可能不止服务于Android手机,还要接工业相机或者索尼微单作为采集端。

IDS相机是工业领域常见的品牌,它有自己的一套SDK和Manager工具。接这种相机时,应用层拿到的往往不是YUV,而是通过SDK配置的像素格式。好消息是,只要是普通相机,最后图像管线里的解码器是可以复用同一套ZXing或OpenCV逻辑的。坏消息是,工业相机的触发模式、曝光时间、增益控制和Android完全是另一套术语,需要重新适配。

索尼Camera Remote SDK又是另一个路子。它走的是RTSP流或SDK实时取景接口,拿到的是JPEG或H.264帧,解码时要把JPEG转成Bitmap再喂给ZXing。这个工程里有兄弟参数是“每隔500毫秒取一帧静态图识别”,效果还不错,适合拍照式扫码,不适合高速连续扫描。

结论是:不管底层相机是什么,上一层的图像预处理和解码器都可以做统一抽象。我建议在工程里定义一个接口,比如FrameProvider,再分别写AndroidCameraProvider、IdsCameraProvider、SonyCameraProvider,这样换硬件不改识别逻辑。CameraQRCode这个压缩包的价值恰恰在于它提供了一个可参考的多源相机抽象雏形,尽管代码写得并不优雅。

2.3 适配时必做的旋转角对齐操作

扫码失败最常见的元凶,就是旋转角没对齐。相机sensor的安装方向、屏幕竖屏/横屏、以及预览TextureView的旋转,这三者叠加起来,会导致解码器拿到的图像是倒着的或横着的。ZXing内部有旋转参数,但你必须在喂图之前就处理掉。

工程里常见的做法是判断当前设备是横屏还是竖屏,然后给解码帧做旋转矩阵操作。Android的Display.getRotation()返回值是0/1/2/3,分别对应0度/90度/180度/270度。竖屏扫码App大部分情况下需要把sensor输出的横图顺时针旋转90度再交给解码器。要注意,旋转会带来额外的内存开销,尤其是1920x1080的Bitmap旋转一次,耗时在10到20毫秒左右,对帧率有影响。

工程在这块做得比较巧,它不是每次都旋转整张图,而是把二维码识别区域(ROI)单独切出来旋转。这样既减少计算量,也变相降低了误识别干扰区域内容的概率。

3. 解码链路踩坑实录:预览清晰但扫不出来的完整排查过程

这是这个工程里最有价值的一段记录。当时现象非常典型:预览画面里二维码明明白白,手动点击对焦也很清晰,但不管怎么对着扫,识别结果就是出不来。我花了一个下午加一个晚上,一步步排查才找到根因。

3.1 第一步:先用静态图隔离问题

调试扫码这种项目,最忌讳在实时流里瞎试。我做的第一件事是把当前预览帧存成一张JPEG图片,然后用ZXing单独识别这张静态图。如果静态图能识别,说明解码器和图像质量没问题,问题出在交互逻辑或者旋转裁切上;如果静态图也识别不了,问题就在图像链路或者在二维码本身质量上。

实测下来,保存出来的静态图居然也识别不了。那就很蹊跷了,因为用手机自带相机对着同样的二维码明明能扫出来。当时我先怀疑是二维码太小,但放大二维码后依然不行。接着我怀疑是解码器的配置问题,把ZXing的DecodeHintType各种参数轮番调了一遍,还是不行。

最后我把保存的图片放到电脑上肉眼观察,才发现图像整体是模糊的,虽然预览画面上看起来清晰,但固化到图片里的一帧恰好是脱焦状态。问题的根因在相机对焦策略,而不是解码逻辑。

3.2 第二步:对焦策略才是元凶

这个工程原本用的是CONTINUOUS_VIDEO对焦模式,默认给录像场景优化的,连续追焦时响应快,但对近处的二维码不够“果断”。视频模式下镜头会一直轻微寻找焦点,跟二维码距离又近,经常处在轻微脱焦状态。

在Camera2里,正确做法是用CONTINUOUS_PICTURE模式,它在静止场景下更稳。另一个更保险的方案是触发一次手动对焦到二维码中心,然后锁焦:

CaptureRequest.Builder requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); requestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); // 发送给相机 session.setRepeatingRequest(requestBuilder.build(), callback, null);

改成CONTINUOUS_PICTURE之后,识别率有了很大提升,但依然不是100%。后来又补了一步:把对焦区域限制到屏幕中央的二维码框范围,告诉相机“我只关心这一块区域的清晰度”,效果立竿见影。

3.3 第三步:帧率与解码节奏的配合

解决了对焦,新的问题又来了——识别不稳定。有时候扫得出来,有时候完全没反应。用Log一打时间戳,发现识别线程和解码帧之间存在明显的竞争:ImageReader回调的帧在变化,而解码器正在处理上一帧,等解码完又错过了最新的清晰帧。

类似工程里常见的解法是做“最近帧优先”策略:用一个单线程的HandlerQueue,把ImageReader的帧放进去,如果队列里积压超过2帧,就丢弃最旧的帧,解码器永远处理最新的一帧。同时限制解码频率,每300毫秒最多解码一次,因为大多数扫码场景并不需要每帧都解。

这两个改动之后,识别稳定性终于让人满意了。字段参数的最终配置是:预览分辨率1920x1080,解码ROI取屏幕中央70%区域,解码间隔300ms,对焦模式CONTINUOUS_PICTURE。这套组合在这个项目的几乎所有测试设备上表现都稳定。

4. 扫码专用相机调优:曝光、补光与画面纯净度

对焦只是第一步,真正想在暗光、高反光、远距离等场景下稳定扫码,还得在曝光和画面处理上做文章。这一节的方法都很实用,不需要太多理论知识,直接试就能看到效果。

4.1 曝光锁定:避免画面忽亮忽暗

自动曝光(AE)在日常拍照时是好事,但扫码时反而是干扰。比如手机在二维码附近轻微移动,背景亮度一变,AE就会重新调整曝光时间,画面跟着忽亮忽暗,二维码局部可能就过曝或者过暗了。

扫码工程里常见做法有二:一是在识别区域内做局部测光,二是完全锁定曝光参数。锁定曝光可以这样设置:

requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF); requestBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, (long) (1000000000L / 30)); // 曝光时间约1/30秒 requestBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, 400); // ISO

但要注意,锁定曝光是一把双刃剑。环境光线变化剧烈时,锁死后画面反而会更黑或全白。所以工程里更稳妥的做法是:在启动扫描时读取当前AE参数,并锁定这个参数;如果用户换了个环境,再重新触发一次AE测量。相当于“每次扫描会话开始时自动测光一次,然后保持”。这个逻辑在CameraQRCode工程里有体现,虽然写得比较靠后,不仔细看留意不到。

4.2 补光灯时序:开了灯反而扫不出来

很多扫码设备带LED补光灯。一开始我天真的以为补光灯可以提高识别率,实测发现如果补光灯的亮度和曝光时间不匹配,会出现工频闪烁的条纹(banding),二维码画面的明暗条纹会直接干扰解码。补光压暗,画面就过暗;补光调亮,又容易曝光过度。

正解是LED补光不要用PWM调光,尽量让灯以恒定电流方式工作。如果硬件不支持恒流,可以在软件上把曝光时间设置在补光频率的整数倍周期附近来规避。比如国内的照明一般50Hz,闪烁周期是10ms,曝光时间最好设置在10ms的整数倍。这个参数细节,很多扫码App的SDK文档里都不会写。

4.3 画面干净度:降噪与锐化的平衡

HDR和夜景模式这类算法处理,在扫码场景里建议直接关掉。它们虽然让画面“看起来好看”,但会引入运动拖影和噪声,二维码的边缘会变模糊。工程里应该优先保证边缘锐利度和画面稳定性,而不是颜色和亮度。

Camera2下可以关闭部分后处理,或者根据你的设备能力调整:

  • 关闭或降低CONTROL_EDGE_MODE
  • 关闭NOISE_REDUCTION的强降噪模式
  • 预览尺寸不要选太高的,1080p已经足够,4K预览除了增加发热和耗时,并不会让识别率更高

原因很简单:ZXing这种解码器认的是黑白模块的对比度,而不是像素数量。只要二维码在画面中占比足够大,1080p的识别率和4K几乎没差别,但处理耗时少得多。

5. 把这个工程当种子,而不是成品:扩展和维护的工程化建议

CameraQRCode.rar这种全称带“.rar”的工程,八成是某个研发中途整理出来分享的,或者是从哪个旧设备项目里拷出来的离线备份。它最大的价值是帮你把相机扫码链路跑通,但距离稳定上线的工程化产品还有一段距离。

5.1 解码器的选型决定了识别率的上限

工程默认用的是ZXing核心库,它轻量、适合移动端,但遇到畸变、低对比度、曲面二维码时就不太够用。我后来在处理一批打印质量很差的标签时,ZXing几乎完全罢工,换OpenCV的WeChat QRCode解码器之后,识别率从不到40%提升到90%以上。

OpenCV WeChat QRCode的模型文件比较大,大概有2MB左右,但识别鲁棒性远强于ZXing。如果你做的是无人售货柜、AGV导航、仓储标签这类复杂场景,建议优先考虑OpenCV WeChat。如果只是常规的卡片二维码扫描,ZXing完全够用,而且调包方便。最好是做成“先ZXing快速解,解不出来再上WeChat重解”的两级策略,兼顾速度和成功率。

5.2 多平台移植时,务必统一帧接口

前面提到过FrameProvider的设计,这里再展开说一下。真正做过Android、工业相机、索尼相机等多端扫码适配后,你会发现底层差异再大,最后关心的无非是几件事:帧数据格式、帧宽高、旋转角度、时间戳。统一抽象成下面这个结构,整个识别层就能做到一次编写,处处复用:

public class FrameData { public byte[] data; // 统一转成NV21或灰度 public int width; public int height; public int rotation; // 需要旋转的角度 public long timestamp; // 帧时间戳 }

有了这一层,不管来的是YUV、JPEG还是RTSP解码后的帧,在进入解码器之前都归一化成统一格式。我在实际项目中把这个思路用得很彻底,底层相机切换几乎不影响上层逻辑,这也是CameraQRCode工程后续改造中最大的收益点。

5.3 二维码的中心区域ROI裁剪

前面提到裁剪ROI,这里补充一点细节。扫码框是不是越居中越好?不一定。不同设备的摄像头位置不同,有的摄像头在左上角,有的在顶部中间,这会影响实际成像区域相对预览框的位置。硬编码“中央70%”只是一个相对安全的默认值,最好的方式是在设置界面暴露一个“扫码区域调节”选项,用户或调试工程师根据实际画面微调。

还有一个新手容易踩的坑:把扫码框的View坐标直接映射到图像坐标。预览画面显示的是裁剪拉伸后的区域,而解码图像是原始sensor帧,这两个坐标系之间有等比映射关系,如果不做换算就直接按View的Rect去裁剪图像,大概率会裁错位置。换算时要注意左右镜像问题,前置摄像头通常需要镜像处理。

5.4 别忽略CPU占用和发热

扫码模块如果不加限制地持续解码,CPU占用轻松蹿到80%以上,手机发烫之后摄像头会自动降帧,识别率反而掉得更厉害。工程里应该在解锁到二维码后主动降低解码频率,或者停止解码,只在UI帧动画层保留预览。

我在实际项目里也养成了一个习惯:识别成功之后,至少暂停解码线程1到2秒,防止同一二维码被重复触发多次。这个场景在连续扫码入库时特别常见,不加锁,用户扫一次会弹好几回结果。

写在最后的实操体会

5.5 调试扫码项目的最好切入点

分享一个我个人屡试不爽的调试思路:拿到这类CameraQRCode工程之后,先不要盯着实时预览跑老半天。第一步把预览的某一帧存成静态图,单独写一个识别静态图片的入口,把解码逻辑先调通。这一步能隔离掉大约60%的变量,因为实时流里旋转、曝光、对焦都是动态变化的,而静态图是死的,问题定位快得多。等静态图识别稳定了,再把它接回实时流,你会惊奇地发现剩下的问题基本都是帧时序和相机参数的调整。

5.6 对这份工程的最终评价

CameraQRCode.rar这份工程如果按商业项目的标准来打分,代码规范和安全处理都不算精致,但它把相机扫码最核心的链路都走了一遍:相机调用、格式转换、帧时序、解码回调。对刚接触扫码开发的人来说,这是一个很好的“活教材”;对已经做过扫码项目的老人来说,它里面关于MTK平台适配和对焦策略的细节,依然有可以抄作业的地方。

如果要用一句话总结我的真实体会:扫码识别从来不是一个解码器就能搞定的事,真正决定识别率上限的,是对相机硬件的理解深度。你把这套相机的脾气摸透了,ZXing也能识别得很好;相机链路处理得粗糙,再强的解码模型也发挥不出威力。这个工程恰好是在“摄像头”和“解码器”之间搭了一座桥,这也是它名为CameraQRCode而不是简简单单叫QRCodeScan的原因。

本文还有配套的精品资源,点击获取

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

magnitude不是CLI命令,而是本地AI推理协议标准

1. “magnitude”不是命令行工具&#xff0c;而是本地AI推理服务的隐性枢纽最近在多个技术社区和开发者群聊里&#xff0c;频繁看到有人发问&#xff1a;“magnitude命令找不到”“unable to locate the magnitude binary”“magnitude cli install失败”&#xff0c;甚至有人把…

作者头像 李华
网站建设 2026/9/9 10:31:41

OpenClaw本地部署实战:Docker接入DeepSeek等国内大模型全攻略

OpenClaw最近的讨论热度一直在线&#xff0c;尤其是“本地部署”和“接国内大模型”这两个方向&#xff0c;大家问得最多。我自己把OpenClaw用Docker跑起来&#xff0c;再把DeepSeek这类国内模型接进去&#xff0c;前后折腾了一整天&#xff0c;踩了不少坑&#xff0c;也把整个…

作者头像 李华
网站建设 2026/9/9 10:31:28

ruflo是幻觉关键词:Claude Code与Codex真实部署指南

1. “ruflo”不是工具名&#xff0c;而是当前AI开发圈一个被误传的“幽灵关键词” 最近在多个技术社区、GitHub Issues、VS Code插件讨论区甚至私聊群组里&#xff0c;频繁看到有人提问&#xff1a;“ruflo怎么安装&#xff1f;”“ruflo和Claude Code冲突吗&#xff1f;”“ru…

作者头像 李华
网站建设 2026/9/9 10:31:15

ARM平台也能改BIOS隐藏项?gsetupmod双架构工具解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 10:30:33

手把手学Linux设备驱动开发:内核机制与实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 10:28:16

STM32/GD32 USB Host读取U盘:寄存器操作绕过HAL库全解析

简介&#xff1a;STM32/GD32 USB Host U盘读取例程是一份面向嵌入式开发者的完整参考工程&#xff0c;主要解决单片机通过USB Host模式识别并操作U盘、结合Fatfs文件系统实现文件读写的问题。资源适用于使用STM32F407/GD32F407等带OTG接口的芯片进行数据记录、文件传输等项目的…

作者头像 李华