简介:OV7725是一款常见的CMOS图像传感器,广泛应用于摄像头与嵌入式设备。该压缩包提供了针对OV7725的Linux驱动源码,面向嵌入式Linux开发者、驱动移植与调试人员,用于在V4L2框架下实现传感器初始化、寄存器配置及图像数据采集。包内含2个文件,分别为ov7725.c驱动实现和ov7725.h头文件,整体仅7KB,代码精简,适合学习驱动框架或快速集成。源码围绕I2C通信控制传感器,包含probe/remove、视频流开启关闭等核心逻辑,并注册为V4L2设备节点,便于用户空间通过标准接口操作。已有358人学习下载,对入门CMOS驱动开发与V4L2编程有直接参考价值。开发者可据此理解驱动与硬件交互流程,调整曝光、增益等参数,或在此基础上适配不同分辨率与帧率,优化特定平台的图像采集性能。
1. 从 ov7725.rar 的命名开始:三个关键词等于一条完整的移植链路
"ov7725.rar" 这类打包名在嵌入式下载站里到处都是,但拆开看它其实是一条完整的 Linux 驱动移植线索:OV7725 是 OmniVision 的 CMOS 传感器,cmos 传感器决定了它走 SCCB 配置 + DVP 并口输出,ov7725 linux 驱动说明我们要给内核喂一个 V4L2 subdev,最后的 v4l2 则直接点名了框架。拿到包之后,很多人先解压找 .c 文件,但我建议先别急着编译。这个标题里没有出现的硬件接线图、上电时序和 CSI 控制器的极性配置,才是决定驱动能不能出图的关键。这篇文章按最常踩的链路讲:寄存器读写能不能通、DVP 时序对不对、V4L2 挂载方式选哪个、出图之后怎么验。适合正在调 ov7725 摄像头驱动、做 Linux 驱动开发的人。
2. 先把硬件链路理清:OV7725 的接口、时序与寄存器依赖
OV7725 是一颗并行 DVP 接口的 CMOS sensor,片内寄存器通过 SCCB 总线访问。和现在主流的 MIPI CSI-2 传感器不同,它没有复杂的 lane 配置,但有一堆 DVP 时序极性要放在驱动里设置。移植驱动时我习惯先把三层关系画出来:SCCB 负责寄存器读写,DVP 负责像素数据搬运,电源和复位负责让 sensor 按手册状态机进入工作模式。这三层只要有一层不对,V4L2 层面表现出来就是 i2c 通信失败、无行场中断、出图花屏这类问题。
2.1 SCCB 是 I2C 的近亲:OV7725 的寄存器读写协议
SCCB 和 I2C 很接近,OV7725 的从机地址是 8 位地址 0x42,换算成 7 位是 0x21。很多驱动直接复用 I2C 控制器,Linux 里也只需要把它当作一个普通 I2C client。差别在读时序:SCCB 规范里的读操作是“先写寄存器地址 + stop,再发起读 + stop”,不推荐使用 I2C 的 repeat-start 一次完成。这句话不是玄学,是 OmniVision 官方时序里明确写了的。有的控制器上 repeat-start 也能通,但也有不少 SoC 的 I2C 控制器对 repeat-start 支持不好,导致 ACK 后读回来的数据全是 0xff。
static int ov7725_sccb_read(struct i2c_client *client, u8 reg) { struct i2c_msg msgs[2]; u8 addr = reg; u8 val = 0; int ret; /* * 第 1 段:写寄存器地址,结束要产生 STOP。 * 第 2 段:重新开始读一个字节,也需要 STOP。 * 这里不拼接 I2C_M_STOP/I2C_M_NOSTART,直接让控制器 * 按两个独立 message 处理,避开 SCCB 对 repeat-start 的兼容问题。 */ msgs[0].addr = client->addr; msgs[0].flags = 0; msgs[0].len = 1; msgs[0].buf = &addr; msgs[1].addr = client->addr; msgs[1].flags = I2C_M_RD; msgs[1].len = 1; msgs[1].buf = &val; ret = i2c_transfer(client->adapter, msgs, 2); if (ret < 0) return ret; if (ret != 2) return -EIO; return val; }这段代码里,msgs[0].flags 为 0 表示写方向,msgs[1].flags 为 I2C_M_RD 表示读方向;client->addr 已经在 i2c 匹配时填入 7 位地址,i2c_transfer 会自己处理 R/W 位。i2c_transfer 的返回值是实际传输的 message 条数,不是字节数,所以必须判 ret != 2,否则可能出现第一段成功、第二段失败但函数仍然当作成功的情况。写寄存器更简单,一个 message 带寄存器地址和数据就能完成:
static int ov7725_sccb_write(struct i2c_client *client, u8 reg, u8 val) { u8 buf[2] = { reg, val }; int ret = i2c_master_send(client, buf, 2); return (ret == 2) ? 0 : -EIO; }写寄存器不用拆两次,因为数据方向没有变化,一次 transaction 即可。OV7725 的寄存器地址是 8 位,驱动里不要套用 16 位寄存器传感器的读写流程,否则地址发错,后续初始化数组全乱。
2.2 DVP 接口信号与像素时钟:PCLK、HREF、VSYNC 的极性要看谁
OV7725 数据输出是 8 位并口,带 PCLK、HREF、VSYNC。SoC 端的 CSI/VPU 控制器要按同样的极性采样。常见配置是 24MHz XCLK 输入,sensor 内部 PLL 倍频后分频得到 PCLK。以 640x480@30fps UYVY 输出为例,每像素 2 字节、总线 8 位,PCLK 至少要 640x480x30x2 = 18.4MHz,实际驱动里还要加行消隐和帧消隐,PCLK 会比这个值高。如果 PCLK 太低,帧率上不去;太高,DMA 带宽和信号完整性都是问题。所以很多初始化数组里第一个关注点就是时钟分频。
| 参数 | 常见配置 | 说明 |
|---|---|---|
| XCLK 输入 | 24 MHz | 多数板子由 SoC 的 camera 时钟输出 |
| PCLK 输出 | 18~48 MHz | 由 PLL 倍频和分频决定 |
| HREF | 高有效 | 行有效信号,结束处即一行像素末尾 |
| VSYNC | 高有效 | 帧同步,部分 SoC 要求反相 |
| 数据形式 | UYVY 8-bit | V4L2 里对应 V4L2_MBUS_FMT_UYVY8_2X8 |
怎么确认实际极性?读 sensor 寄存器只能看到配置值,最可靠是把 VSYNC 和 HREF 接到示波器,看有效电平。很多移植翻车在“寄存器配置是对的,但 endpoint 的 vsync-active 写反”。设备树里 pclk-sample、hsync-active、vsync-active 这三个参数必须和采集端一致,后面第 3 章会再展开。
2.3 上电时序与复位:板级配置里最容易忽略的三条
OV7725 手册对上电顺序有明确要求,常见顺序是:DOVDD 先上,AVDD 和 DVDD 再上,然后输入时钟稳定,最后拉高 RESET 并延时后再开始 SCCB 通信。板子上如果用了电源芯片,还要确认各路电压有没有 on/off 顺序控制;直接用 GPIO 控 LDO 时,要在驱动里按顺序操作。
| 步骤 | 动作 | 延时建议 |
|---|---|---|
| 1 | DOVDD 1.8V 先稳定 | 5ms |
| 2 | AVDD 2.8V、DVDD 1.8V 跟上 | 5ms |
| 3 | XCLK 24MHz 启动并稳定 | 1ms |
| 4 | RESET 拉低再拉高 | 低电平保持 20ms |
| 5 | PWDN 拉低,退出 power down | 20ms |
| 6 | 开始访问 SCCB | 上电完成后 |
驱动里如果直接在 probe 时读 ID,经常会碰到读不到,原因就是上电时序还没完成。我一般会在 probe 里用一个 delayed_work,或者至少 msleep(20) 兜底。这里先不贴代码,第 4 章会把这个坑讲透。
3. 在 V4L2 框架里挂上 OV7725:从 soc_camera 到 v4l2_subdev 的移植
ov7725.rar 里的驱动源码,很大概率是 3.x 内核时代基于 soc_camera 框架写的。老框架把 camera host 和 sensor 绑得比较死,后来内核引入了 media controller 和 v4l2_subdev,V4L2 驱动框架要求 sensor 以 subdev 形式注册到媒体设备里。直接解压拿老代码编译,会看到一票 API 报错。这部分讲清怎么把它迁移到新框架。
3.1 先认框架:soc_camera 驱动的三个识别标志
拿到源码先不要全局搜 OV7725,先看三个特征:文件头 include <media/soc_camera.h>、驱动里有 soc_camera_link 结构、ops 里出现 set_bus_param/query_bus_param。有这几个标志就说明它走的是老路径。常见做法不是把 soc_camera 整个启用,而是把 sensor 的寄存器配置和 s_power/s_stream 逻辑抽出来,装进 v4l2_subdev 的 ops。
| 维度 | soc_camera 老框架 | v4l2_subdev + media controller |
|---|---|---|
| 注册入口 | soc_camera_probe | v4l2_i2c_subdev_init / v4l2_async_register_subdev |
| 核心 ops | video/soc_camera_ops | core/video/pad ops |
| 数据格式协商 | set_bus_param | set_fmt / get_mbus_config |
| 匹配方式 | i2c_device_id + soc_camera_link | compatible + async fwnode 匹配 |
主流 SoC 厂商的 BSP 基本都迁到新框架,与其花时间维护旧接口,不如迁。这不意味着初始化数组要重写,寄存器序列是通用的,真正要改的是驱动骨架。
3.2 驱动入口骨架:把 probe 变成 subdev 注册
这里给出一个最小可用的 v4l2 subdev 骨架,适合把老驱动代码往里填:
static const struct v4l2_subdev_video_ops ov7725_video_ops = { .s_stream = ov7725_s_stream, .g_mbus_config = ov7725_g_mbus_config, }; static const struct v4l2_subdev_core_ops ov7725_core_ops = { .s_power = ov7725_s_power, }; static const struct v4l2_subdev_pad_ops ov7725_pad_ops = { .get_fmt = ov7725_get_fmt, .set_fmt = ov7725_set_fmt, }; static const struct v4l2_subdev_ops ov7725_subdev_ops = { .core = &ov7725_core_ops, .video = &ov7725_video_ops, .pad = &ov7725_pad_ops, }; static int ov7725_probe(struct i2c_client *client) { struct ov7725_dev *dev; struct v4l2_subdev *sd; int ret; dev = devm_kzalloc(&client->dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; sd = &dev->sd; dev->client = client; v4l2_i2c_subdev_init(sd, client, &ov7725_subdev_ops); ret = v4l2_async_register_subdev(sd); if (ret) return ret; return ov7725_check_id(client); }代码里 s_power 负责上电、复位、加载初始化寄存器;s_stream 负责启动和停止输出;pad ops 提供 get_fmt/set_fmt 给上层协商像素格式。v4l2_i2c_subdev_init 会把 i2c_client 和 v4l2_subdev 关联起来,v4l2_async_register_subdev 则让设备树 endpoint 能匹配到它。一个容易忽略的点:probe 阶段尽量只做 ID 校验,不要把整个初始化序列都跑完。老驱动里很多把寄存器配置放在 probe 里,导致 i2c 还没稳定就初始化,后面休眠恢复非常麻烦。
3.3 设备树节点:让驱动被 v4l2 async 框架找到
V4L2 框架下,sensor 想被 CSI 控制器找到,必须通过 async fwnode 匹配。OV7725 在设备树里看起来像这样:
&i2c2 { ov7725: ov7725@21 { compatible = "ovti,ov7725"; reg = <0x21>; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_cam>; clocks = <&clk_cam 0>; reset-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>; pwdn-gpios = <&gpio1 4 GPIO_ACTIVE_LOW>; port { ov7725_ep: endpoint { remote-endpoint = <&csi_ep>; bus-width = <8>; hsync-active = <1>; vsync-active = <1>; pclk-sample = <1>; }; }; }; };compatible 要和驱动里的 of_match_table 对上,reg 是 7 位 I2C 地址 0x21。pinctrl 作用在 I2C 引脚和 CSI 引脚上,很多板子不配置 pinctrl 会导致时钟引脚被复用成 GPIO,SCCB 压根不通。endpoint 里的 bus-width、hsync-active、vsync-active、pclk-sample 会被解析进 v4l2_fwnode_endpoint,最后由 async framework 拿来匹配 sensor subdev。如果设备树缺少 endpoint,很多 CSI 驱动根本不会 probe sensor,这是最常见的“驱动加载了但 /dev/video0 没出现”的原因之一。
3.4 核心 ops 顺序:s_power 和 s_stream 不要混着调
CSI 驱动加载后,典型调用顺序是:先对 subdev 调 s_power(1) 初始化寄存器;开始采集时调 s_stream(1);停止采集时调 s_stream(0),再调 s_power(0)。有些驱动把 s_stream 里重复写所有寄存器,这样应用层每次 start/stop 都会重置 sensor,之前设好的曝光、增益全部被清掉。更合理的分工:s_power(1) 里上电、复位、写完整初始化数组;s_stream(1) 里只改输出使能相关的一两个寄存器。这样既能恢复状态,又不会把曝光配置冲掉。
4. 从裸驱到出图:OV7725 在 Linux 下的最小调试流程
驱动代码能编译不代表能出图。我一般按下述顺序来:先验证 SCCB 链路,再验证像素输出,最后才是 V4L2 应用层。每步用一个命令或小工具,不猜。
4.1 先验证 SCCB 通道:i2cdetect 找从机地址
OV7725 的 7 位地址是 0x21,板子上的 I2C 总线可能是 i2c-0、i2c-1 或 i2c-2,先用 i2cdetect 扫描:
i2cdetect -y 2 i2cdetect -y -r 2第一个是普通 scan,第二个用 read 模式。OV7725 在显示时可能显示为 21,也可能显示为 42,取决于 i2cdetect 版本是否把 R/W 位也算进去。如果 scan 没出现地址,不要急着改驱动,先量 XCLK 有没有、RESET 是否被拉高、PWDN 是高还是低。这三个硬件条件不满足,sensor 不会 ACK。这一步没有通用代码,就是万用表和示波器的事。
如果地址出现了,再读产品 ID:
i2cget -y 2 0x21 0x0b0x0a 和 0x0b 是 OV7725 的 product ID 寄存器,读回来的值和 datasheet 里的 PID 对得上,就说明 SCCB 这条路已经通了。注意 i2cget 走的是 smbus 读,某些 I2C adapter 对 smbus 的 PEC 处理不同,调试时优先用驱动里自定义的 i2c_transfer 方式,避免被 adapter 差异干扰。
4.2 初始化数组与分辨率切换:动态算值,不要整包照抄
OV7725 的寄存器序列决定输出窗口、帧率、时钟分频、AWB 等。老驱动里通常是一个 const u8 regs[][2] 数组,probe 时循环写入。直接照抄的问题很典型:原项目 XCLK 可能是 12MHz,你的板子是 24MHz,PLL 寄存器不跟着改,PCLK 会翻倍,要么花屏要么帧率错。数组拷过来后,先手动核对三类寄存器:时钟分频、窗口尺寸、输出格式。
初始化函数本身很通用,建议写成查表形式,方便在驱动的配置表里改:
static int ov7725_init_regs(struct ov7725_dev *dev) { struct i2c_client *client = dev->client; int i; for (i = 0; i < dev->reg_cnt; i++) { const struct regval *rv = &dev->regs[i]; int ret = ov7725_sccb_write(client, rv->reg, rv->val); if (ret) return ret; if (rv->delay_ms) msleep(rv->delay_ms); } return 0; }regval 结构体里通常有 reg、val、delay_ms 三个字段,delay_ms 用来处理复位和 AGC 收敛等需要延时的寄存器。把数组从 .c 文件移到板级配置表甚至设备树 property,能省掉很多重新编译内核的时间。心法也很简单:初始化数组是“配置”,不是“代码”,配置应该跟着板子走,而不是跟着驱动源码走。
4.3 用 v4l2-ctl 抓帧:从 /dev/video0 拿到第一张原始图
如果 sensor 已经通过 async 注册,CSI 驱动通常会暴露一个 /dev/video0,它本质上是 V4L2 字符设备驱动框架下的 video node。最小验证流程如下:
v4l2-ctl -d /dev/video0 --list-formats-ext v4l2-ctl -d /dev/video0 --set-fmt-video=width=640,height=480,pixelformat=UYVY v4l2-ctl -d /dev/video0 --stream-mmap --stream-count=1 --stream-to=first.raw--list-formats-ext 先确认驱动上报的格式和分辨率;--set-fmt-video 手动指定 640x480 UYVY;--stream-mmap 用 mmap 缓冲,--stream-count=1 抓一帧,--stream-to 落盘。如果命令卡住,多半是 CSI 的 DVP 没收到 VSYNC 中断;如果中断风暴,检查 PCLK 极性。拿到 raw 后可以转成图:
ffmpeg -f rawvideo -pix_fmt uyvy422 -s 640x480 -i first.raw first.png注意 raw 文件不带任何头部,pix_fmt 必须和 set-fmt 一致。如果应用层用 YUYV 打开 UYVY 的 raw,颜色就会互换,看起来像偏色。
4.4 画面异常定位:一张表把所有现象对应到参数
出了图之后,异常画面也能反推问题:
| 现象 | 优先检查项 | 调试方向 |
|---|---|---|
| 完全黑屏/灰屏 | s_stream 是否调用、VSYNC 是否到达 CSI | 看中断计数 |
| 花屏/雪花 | PCLK 采样沿 | 翻转 pclk-sample |
| 图像左移或错位 | HREF 有效电平、窗口起点 | 调整 hsync-active |
| 偏绿或偏紫 | 输出格式 UYVY/YUYV 错,或 R/B gain | 检查 set_fmt 和 AWB |
| 亮度低/过曝 | 曝光、增益寄存器 | 调 V4L2_CID_EXPOSURE |
中断计数可以通过 /proc/interrupts 看,也可以直接在 CSI 驱动的中断 handler 里做一个每秒计数打印。这里不要嫌麻烦,一次看清中断频率,胜过反复抓帧猜问题。
5. 常见问题与避坑排查:OV7725 驱动移植里我踩过的 5 个实例
这章写的是五个最容易被当成“驱动有问题”的案例,实际却都是时序、极性和状态管理问题。每条按现象、原因、解决的顺序写,照着排查比重写驱动有效。
5.1 现象:i2cdetect 能看到地址,但驱动 probe 读 ID 失败
原因:probe 阶段 SCCB 通道已经通了,但 sensor 还没走出上电时序,或者 probe 里读取寄存器的方式用了 repeat-start。有些 i2c adapter 上 i2cdetect 能探测到从机,是因为硬件 scan 方式和驱动的 i2c_transfer 路径不一致,不能作为驱动一定能通的依据。
解决:probe 里进入读 ID 之前先 msleep(20),并确保 RESET/PWDN 的 GPIO 在 i2c client 创建前已经由 pinctrl 设置为正确电平。另一个做法是返回 -EPROBE_DEFER,让框架稍后重试;但更推荐在板级初始化里把时序拉对,而不是靠 defer 碰运气。
5.2 现象:s_stream 后 VSYNC 中断风暴,CPU 占用接近 100%
原因:CSI 驱动在开始 streaming 时使能了 VSYNC 中断,但 DMA buffer 没有排满;OV7725 在 640x480@30 时 VSYNC 只有 30Hz,正常不会造成高 CPU。如果中断风暴,通常是 CSI 中断状态标志没清,中断处理函数反复进入;或者 sensor 输出帧率因为 PCLK 配置错误到了几百 fps。
解决:先用示波器看 VSYNC 实际频率,再在 CSI 中断 handler 里加一个简单计数,打印每秒次数。如果频率过高,回头调 PLL 配置;如果频率正常但 CPU 高,查中断 handler 是否没有及时清中断状态寄存器,必要时在出现场关掉对应中断位。
5.3 现象:图像整体左移、右移,看起来像没对齐
原因:DVP 采集端和 sensor 的 HREF 有效电平不匹配,或者 sensor 输出 window 的水平起始值设置不合理。OV7725 窗口相关的寄存器通常由初始化数组设置,如果数组里的 hstart 和有效图像宽度相加超出 sensor 内部 line 长度,会出现整体偏移。
解决:先用 v4l2-ctl 抓一帧,看偏移量。水平偏移多半是 HREF 和 PCLK 的相位关系,检查 endpoint 的 pclk-sample 是 1 还是 0。对 OV7725,常见做法是让 PCLK 上升沿采样数据,如果当前是下降沿,把 pclk-sample 改为 1 后重新抓帧。窗口寄存器则要和输出分辨率一起算,不能只改宽度不改起点。
5.4 现象:能出图但颜色偏绿/偏紫,白平衡寄存器写了没反应
原因:应用层设置的 V4L2_CID_AUTO_WHITE_BALANCE 没有对应到 OV7725 的 AWB 使能寄存器,或者 color matrix 没有配置。OV7725 默认可能不会自动使能 AWB,写完寄存器不会立刻生效。更常见的是 V4L2 format 协商错误:video device 告知应用输出 YUYV,实际 sensor 输出 UYVY,RGB 分量错位。
解决:先确认 pixelformat 和初始化数组里的输出格式一致。如果格式没问题,手动写 AWB 使能寄存器并读回验证,确认写入成功后再看颜色。不要一把梭把初始化数组里所有寄存器都填成推荐值,某些老驱动里面的 AWB 配置是给当时镜头模组调的,换了镜头模组就不准。
5.5 现象:休眠唤醒后摄像头彻底死掉,重新 insmod 也无效
原因:电源域没恢复,sensor 的寄存器内容在掉电时丢失,但驱动 state 还停留在 running;或者 reset gpio 在 suspend 过程中被拉低,wakeup 后没有拉高。重新 insmod 也无效,说明 i2c client 状态和硬件实际状态已经不一致。
解决:确认 pm_runtime 回调,resume 里重新执行完整初始化序列。老驱动往往没有 pm 支持,需要自己加 dev_pm_ops。测试流程:应用先关闭 streaming,echo mem 到 /sys/power/state,唤醒后重新打开 /dev/video0。唤醒后如果 dmesg 有 i2c timeout,说明 i2c 控制器也睡了,需要先让 i2c controller 恢复再访问 sensor。
6. 驱动跑通之后:把曝光、增益和时间戳做成应用层可用的标准接口
能出图只算完成一半。产品要把自动曝光、增益、白平衡暴露给应用层,还要验证帧率真的稳定。这章讲三个收尾习惯。
6.1 用 VIDIOC_S_CTRL 把曝光和增益接口补上
常见做法是驱动里实现 s_ctrl,处理 V4L2_CID_EXPOSURE 和 V4L2_CID_GAIN,应用层只调 ioctl,不用关心寄存器:
struct v4l2_control ctrl; ctrl.id = V4L2_CID_EXPOSURE; ctrl.value = 320; if (ioctl(fd, VIDIOC_S_CTRL, &ctrl) < 0) perror("VIDIOC_S_CTRL exposure");驱动侧 s_ctrl 里把 value 线性映射到寄存器。OV7725 的曝光值一般不是简单的 1:1,需要参照寄存器说明换算。先让应用层能写一个值,再抓图对比亮度,比直接在驱动里猜公式靠谱。
6.2 用 debugfs 快速读回寄存器,验证配置真的写进去
加 debugfs 比 sysfs 省事,驱动里注册一个 debugfs 文件,读寄存器值:
static int ov7725_debug_show(struct seq_file *s, void *v) { struct ov7725_dev *dev = s->private; int val = ov7725_sccb_read(dev->client, 0x0b); seq_printf(s, "pid=0x%02x\n", val); return 0; }之后用cat /sys/kernel/debug/ov7725/reg查看。如果读值和写值不一致,先怀疑 SCCB 时序,而不是应用层。这个习惯能省掉大量“寄存器到底写没写进去”的争论。
6.3 用 v4l2-ctl 的时间戳命令验证帧率
验证帧率不要看应用层自己算的平均 FPS,要看帧间隔抖不抖。v4l2-ctl 的时间戳命令可以直接用:
v4l2-ctl -d /dev/video0 --stream-mmap --stream-count=30 \ --get-frame-timestamps --stream-to=/tmp/30.raw这条命令会打印每一帧的时间戳,30 帧的间隔应该稳定在 33ms 左右。如果差值抖动很大,优先查 PCLK 配置和 DMA 丢帧;如果 30 帧整体时间偏长,可能是 CSI 缓冲不足导致底层反复丢帧重传。
我的习惯是先把这条时间戳命令写进验证脚本,每次改完驱动配置都跑一遍。OV7725 这类老 sensor 其实已经很成熟,绝大多数翻车都在时序和极性,不在驱动本身。先调好上电、复位和 endpoint 极性,再谈寄存器配置,顺序反了只会越调越乱。希望帮到你。
本文还有配套的精品资源,点击获取