STM32F7驱动PAJ7620手势传感器:I2C时序、Bank切换与实战排错
简介面向STM32F7系列嵌入式开发者的PAJ7620手势传感器驱动工程以STM32F740为主控演示通过I2C/SPI接口完成传感器初始化、手势数据读取与动作识别。压缩包内含234个文件其中122个.h头文件与108个.c源文件覆盖HAL库配置、外设驱动和业务逻辑另附Keil工程文件、hex固件及启动汇编整体1.66MB可直接编译烧录。已有144人浏览学习。工程完整包含GPIO引脚配置、时钟与总线初始化、PAJ7620寄存器写入、手势采集解析及响应处理代码并给出中断或主循环两种应用思路适合智能家居、人机交互等项目参考也可作为学习ARM Cortex-M7外设编程和I2C/SPI通信的实战案例。1. 为什么说 STM32 驱动 PAJ7620 传感器做手势检测难点不在算法STM32 驱动 PAJ7620 传感器做手势检测最常见的误区是把它当成摄像头方案一头扎进图像算法里。PAJ7620 是 PixArt 的光学手势芯片内部把红外收发和手势识别算法全封装好通过 I2C 直接吐出手势编号上下左右、前推后拉、顺逆时针加摆动共九种。F7 要做的只是把寄存器读回来映射成语义真正的坎在初始化序列、Bank 切换和 I2C 时序上这正是这份工程包解决的事。工程适合想在非接触交互上快速出原型的工程师也适合想在 STM32F7 上跑通 HAL 驱动参考的初学者。标题里的 F740 在 ST 官方型号表里并不存在工程按 F7 系列通用方式组织F746、F767 都能直接套。下面按拆包顺序把接线、初始化和排错一次讲清。2. 硬件连接与 I2C 初始化F7 引脚怎么接、参数怎么配2.1 PAJ7620 的引脚定义3.3V、开漏 INT 和上拉电阻PAJ7620 模块的引脚一般是 VCC、GND、SCL、SDA、INT部分板子还会引出 AD_SEL。VCC 接 3.3V优先按模块丝印来如果模块板载了 LDO 也可以接 5V没有 LDO 的模块直接上 5V 就等着冒烟。SCL/SDA 接 F7 的 I2C 引脚我习惯用 I2C1 的 PB8/PB9CubeMX 里选 I2C1 后会自动列出所有可用引脚挑一组顺手的就行。INT 是开漏输出检测到手势后会拉低或者拉高具体电平取决于模块设计。开漏意味着它本身没有主动驱动高电平的能力必须接上拉电阻常规模块上已经贴了 4.7k 或 10k。自己画板的话这个电阻一定要补否则中断模式根本读不到信号。I2C 走的是 3.3V 电平直接连 F7 的 GPIO 即可不需要额外电平转换。表格F7 与 PAJ7620 接线表PAJ7620 引脚STM32F7 引脚说明VCC3.3V无板载 LDO 时严禁接 5VGNDGND与 F7 共地SCLPB8I2C1_SCL外部上拉 4.7k 到 3.3VSDAPB9I2C1_SDA外部上拉 4.7k 到 3.3VINTPC13 或任意 EXTI 引脚开漏输出需上拉AD_SELGND接地保持默认从机地址SCL 和 SDA 的上拉电阻是最容易忽略的一环。F7 内部虽然也有上拉但强度有限而且芯片手册不建议拿内部上拉当 I2C 上拉用。只要模块没有自带电阻就外挂两个 4.7k 到 3.3V总线空闲时才能稳定拉高。上了拉之后用万用表量一下 SCL/SDA 的空闲电平应该都在 3.3V 附近低于 2.5V 就要怀疑上拉没贴好或者引脚配置成了推挽。2.2 CubeMX 里的 I2C1 参数速率、地址位宽与超时打开工程里的 .ioc 文件或者自己新建一个 STM32F746/F767 工程把 PAJ7620 挂在 I2C1 上。CubeMX 里的配置并不复杂但有几个参数会直接影响能不能通信起来。我第一次直接选了 400kHz结果初始化就丢 ACK后来降到 100kHz 一切正常。这个传感器的 I2C 时序窗口没有想象中宽初次调通建议先按标准模式跑。表格CubeMX I2C1 关键参数参数项建议值说明I2C Speed ModeStandard Mode先跑 100kHz稳定后再试 Fast ModeClock Speed (Hz)100000总线负载重时保持 100k 最稳Addressing Mode7-bitPAJ7620 是 7 位从机地址Own Address0x00本机作为主机用不到保持默认CubeMX 生成代码后I2C1 的 GPIO 会自动配成复用开漏模式GPIO 速度可以选 High。这里有个 F1 和 F7 的差别F1 时代很多人习惯把 SCL/SDA 配成推挽输出再靠外部上拉F7 上直接选开漏复用更符合 HAL 的习惯。走查一遍 HAL_I2C_MspInit 函数确认 GPIO 配置里确实是 GPIO_OPEN_DRAIN如果拷贝了 F1 的工程过来经常这里还是 GPIO_PUSH_PULL虽然偶尔也能跑但时序余量会差不少。2.3 HAL 读写封装DevAddress 到底该填 0x39 还是 0x73HAL 库 I2C 传输函数的 DevAddress 参数填的是 7 位从机地址而 PAJ7620 数据手册里给的 0x73/0x74 是 8 位写读地址。很多从 Arduino 移植过来的例程直接拿 0x73 传给 HAL结果就是第一个字节发出去从机不 ACK。7 位地址就是 0x39HAL 内部会自己左移一位千万不要再手动左移否则发出去的地址变成 0xE6整个 I2C 总线上根本没人认。#define PAJ7620_I2C_ADDR (0x39) /* 7-bit address别传 0x73 */ #define PAJ7620_REG_BANK_SEL (0xEF) HAL_StatusTypeDef paj7620_i2c_write_reg(uint8_t reg, uint8_t val) { uint8_t buf[2] { reg, val }; return HAL_I2C_Master_Transmit(hi2c1, PAJ7620_I2C_ADDR, buf, 2, 100); } HAL_StatusTypeDef paj7620_i2c_read_reg(uint8_t reg, uint8_t *val) { HAL_StatusTypeDef st; st HAL_I2C_Master_Transmit(hi2c1, PAJ7620_I2C_ADDR, reg, 1, 100); if (st ! HAL_OK) { return st; } return HAL_I2C_Master_Receive(hi2c1, PAJ7620_I2C_ADDR, val, 1, 100); }这段封装是后面所有驱动代码的基础。写寄存器时把寄存器地址和值拼成一个两字节数组一次发出去读寄存器时先发寄存器地址再发起一次读。HAL 的 Master_Transmit 和 Master_Receive 每次传输都会带 STOP 条件对 PAJ7620 这种时序敏感的设备来说比用 HAL_I2C_Mem_Read 的连续传输方式更稳。超时时间 100ms 对单字节操作足够I2C 总线异常挂死时能及时返回错误。提示不要在这里用 HAL_I2C_Mem_Read虽然它能少写一行代码但部分 F7 型号的 I2C 外设对 PAJ7620 的 Restart 时序兼容性不好偶发读空。分段 Master_Transmit/Receive 是我验证过最稳的写法。3. 驱动源码核心Bank 切换和初始化序列逐条拆给你看3.1 Bank0 和 Bank1为什么 PAJ7620 要分两层寄存器PAJ7620 内部寄存器分两个 Bank通过地址 0xEF 写入的值来切换。Bank0 是系统、感光和输出配置Bank1 是手势检测配置与结果。这个设计和大多数传感器不一样也是第一次看它驱动代码时最容易懵的地方寄存器地址看着连续从 0x00 到 0xFF但同一个地址在不同 Bank 下含义完全不一样。0x43 在 Bank1 是手势结果在 Bank0 里可能是完全不相干的其他配置。读手势结果前要先写 0xEF0x01 切到 Bank1改配置时又要写 0xEF0x00 切回 Bank0。如果初始化完 Bank1 后直接去读 0x43 没问题但中途一旦写过 Bank0 的寄存器忘了切回去读到的就是另一层的数据。常见表现是手势 ID 恒为 0xFF 或者数值乱跳跟传感器坏了似的。理解了 Bank 机制这类问题大多能定位到 当前在哪个 Bank 这一个问题上。3.2 初始化序列按照寄存器表走一遍PAJ7620 上电后不能马上初始化典型要求是等 250ms 左右再动 I2C。工程里 paj7620_init() 的第一件事就是延时而不是写寄存器这个顺序很多人忽略结果第一片 NACK 就来了。初始化序列本身接近一百项官方把它作为一张表给出来驱动代码里最常见的组织方式就是 const 数组逐条写。typedef struct { uint8_t reg; uint8_t val; } paj7620_reg_t; static const paj7620_reg_t s_init_seq[] { /* Bank0基础配置 */ { 0xEF, 0x00 }, /* 切到 Bank0 */ { 0x00, 0x00 }, /* 唤醒传感器 */ { 0x04, 0x00 }, { 0x40, 0x21 }, /* 手势模式相关vendor 默认值 */ /* ... 中间约 100 项完整数组见 paj7620.c ... */ /* Bank1手势检测块 */ { 0xEF, 0x01 }, /* 切到 Bank1 */ { 0x02, 0x0F }, /* 手势使能vendor 默认 */ { 0x03, 0x00 }, /* ... 中间项见完整数组 ... */ { 0xEF, 0x00 }, /* 切回 Bank0准备读取 */ }; int paj7620_init(void) { uint16_t i; HAL_Delay(300); /* 等 PAJ7620 内部起振和算法加载完成 */ for (i 0; i sizeof(s_init_seq) / sizeof(s_init_seq[0]); i) { if (paj7620_i2c_write_reg(s_init_seq[i].reg, s_init_seq[i].val) ! HAL_OK) { return -1; } HAL_Delay(1); /* 给每个寄存器写入留一点间隔 */ } return 0; }这段代码有三个值得留意的点。第一最前面的 HAL_Delay(300) 是为了避开上电不稳定窗口不是随便加的PAJ7620 内部有成像和算法链路上电到 I2C 就绪需要时间。第二每写一个寄存器后延时 1ms这个延时在单独挂 PAJ7620 时不是必须的但当 I2C 线上还挂了 OLED、总线负载较重时连续快速写会偶发丢 ACK留 1ms 能明显降低概率。第三0xEF 的两次切换不是配置而是上下文切换它决定后面一串寄存器落在哪个 Bank。切错 Bank 的话后面所有写入都写到了错误的地方初始化看起来成功但实际没生效。3.3 初始化失败怎么定位ACK 错误和回读校验初始化失败先要分两类一是在某个寄存器上返回 HAL_ERROR二是全流程返回成功但手势读不出来。前者大概率是地址或 Bank 切换问题后者要看是不是真的写进去了。回读校验是最直接的验证手段写完关键寄存器立刻读回来比对能快速缩小排查范围。int paj7620_reg_verify(uint8_t reg, uint8_t expect) { uint8_t val 0; if (paj7620_i2c_read_reg(reg, val) ! HAL_OK) { return -1; /* 读失败多半是 I2C 时序或地址问题 */ } return (val expect) ? 0 : -2; /* 值不对可能 Bank 没切对 */ }验证时一定要确认当前在哪个 Bank。比如切到 Bank0 读 0x00期望值是 0x00切到 Bank1 读手势结果寄存器期望值应该在 0x00 到 0x09 之间。如果读回来恒为 0xFF先检查 Bank如果读回来恒为 0x00先怀疑 I2C 地址是不是写成了 0x73。这套回读逻辑可以直接加在 paj7620_init() 尾部把每一段的第一个和最后一个寄存器都验一遍初始化失败时能立刻知道是第几段挂了。4. 手势读取与中断模式轮询、EXTI 和九种手势编号映射4.1 从 Bank1 的 0x43 读出九种手势手势结果在 Bank1 的 0x43 寄存器低 4 位是手势编号。读取前先写 0xEF0x01 切到 Bank1读完再切回 Bank0这样后续任何写配置的代码都不会踩错 Bank。这个读完切回的习惯是我吃了亏之后养成的因为主循环里往往还有 LED、蜂鸣器之类的控制代码它们都依赖 Bank0。#define PAJ7620_REG_BANK_SEL (0xEF) #define PAJ7620_GESTURE_RESULT (0x43) #define GES_UP 0x01 #define GES_DOWN 0x02 #define GES_LEFT 0x03 #define GES_RIGHT 0x04 #define GES_FORWARD 0x05 #define GES_BACKWARD 0x06 #define GES_CLOCKWISE 0x07 #define GES_ANTI_CLOCKWISE 0x08 #define GES_WAVE 0x09 int paj7620_get_gesture(uint8_t *ges) { uint8_t val 0; if (paj7620_i2c_write_reg(PAJ7620_REG_BANK_SEL, 0x01) ! HAL_OK) { return -1; } if (paj7620_i2c_read_reg(PAJ7620_GESTURE_RESULT, val) ! HAL_OK) { return -2; } paj7620_i2c_write_reg(PAJ7620_REG_BANK_SEL, 0x00); /* 切回 Bank0 */ *ges val 0x0F; /* 高四位在不同版本含义不一致只取低四位 */ return 0; }注意最后的掩码 0x0F。PAJ7620 的手势结果寄存器低 4 位是手势编号高 4 位在不同批次和版本里含义不完全一致直接整个字节拿去比对很容易误判。只取低四位是目前兼容性最好的做法。切回 Bank0 那一步即使失败也不要忽略返回值但主逻辑里一般不用卡死继续跑下一帧就行毕竟手势是连续事件丢一帧不影响。表格手势编号与触发场景编号宏触发场景0x01GES_UP手掌在传感器上方 5-15cm 上移0x02GES_DOWN手掌下移0x03GES_LEFT手掌左移0x04GES_RIGHT手掌右移0x05GES_FORWARD手掌向前推近传感器0x06GES_BACKWARD手掌向后远离0x07GES_CLOCKWISE在传感器上方顺时针画圈0x08GES_ANTI_CLOCKWISE逆时针画圈0x09GES_WAVE手掌在传感器前晃动4.2 轮询方案用 HAL_Delay 或定时器周期读轮询是最简单的方案适合原型验证。PAJ7620 内部检测频率约 150Hz手势本身持续几百毫秒主循环 50ms 轮询一次完全够用。轮询间隔太短反而容易读到中间态间隔太长会漏掉快手势。uint8_t gesture 0; while (1) { if (paj7620_get_gesture(gesture) 0 gesture ! 0) { gesture_dispatch(gesture); /* 分发到具体业务 */ } HAL_Delay(50); /* 约 20Hz 轮询 */ }这里有个容易被忽视的点手势结果寄存器读到之后并不会自动清零PAJ7620 是不断更新这个值。如果一直拿到同一个非零手势gesture_dispatch 会被反复触发。解决方式要么在 dispatch 里做边沿检测只处理手势编号变化的时刻要么每次读出后用一个变量记住上一帧连续相同不重复触发。轮询方案的劣势就是 CPU 空转但在裸机项目里 20Hz 的 I2C 轮询开销极低大部分场景完全够用。4.3 中断方案用 INT 引脚配合 EXTI事件驱动省 CPU想省 CPU 就用 INT 引脚PAJ7620 检测到手势后 INT 输出有效电平。CubeMX 里把 INT 引脚配成 GPIO_EXTI触发沿根据模块确定常见模块是低电平有效配下降沿触发。HAL 统一走 HAL_GPIO_EXTI_Callback 回调在里面读取手势结果。void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { uint8_t ges 0; if (GPIO_Pin PAJ7620_INT_PIN) { if (paj7620_get_gesture(ges) 0 ges ! 0) { gesture_dispatch(ges); /* 只置标志位别做耗时操作 */ } } }回调里跑 I2C 读写其实不太推荐因为 I2C 在中断上下文里可能被更高优先级中断打断偶发超时。常见做法是回调里只置一个g_gesture_ready 1的标志主循环检测到标志后再去读手势结果。PAJ7620 的 INT 是可重复触发的这次丢了下一帧还会再来所以不用担心中断丢失导致功能永久失效。中断方案适合低功耗或任务较多的场景轮询则适合代码尽量简单的场景选哪种取决于你的主循环还有多少事要做。5. 避坑STM32F7 上驱动 PAJ7620 最常翻车的五个点5.1 I2C 一直 NACKDevAddress 填了 0x73 而不是 0x39现象HAL_I2C_Master_Transmit 返回 HAL_ERROR逻辑分析仪上看到主机发出地址字节后从机没有拉低 SDA 应答。反复检查接线和供电都正常。原因PAJ7620 数据手册和大多数 Arduino 例程用的是 8 位地址 0x73/0x74HAL 库要求填 7 位地址。直接填 0x73HAL 内部再左移一位总线上实际发出的地址是 0xE6从机当然不应答。更迷糊的是网上有些文章建议把 0x73 左移一位传给 HAL等于把 8 位地址左移成 0xE6错得更远。解决DevAddress 统一填 0x39。从别处移植代码时把宏定义改成 0x39再全局搜一遍所有传地址的地方确认没有二次左移。5.2 初始化成功但手势结果全是 0xFFBank 没切对现象paj7620_init() 全流程返回 0但读 0x43 永远拿到 0xFF手在传感器前怎么挥都没反应。代码看起来没问题I2C 通信也正常。原因当前实际停在 Bank00x43 在 Bank0 是另一个寄存器。初始化序列里有几处 Bank 切换只要中间任何一次 0xEF 写入被跳过或者写失败后续的配置就错位了。还有一种情况是其他模块的代码在初始化之后又写了 0xEF把 Bank 改乱了。解决每次读手势结果前强制先写 0xEF0x01读完立刻写回 0x00。不要依赖之前切过所以现在是 Bank1这种假设。拿到工程后第一件事就是全局搜索 0xEF 的操作确认只有初始化里和读取函数里动了 Bank 选择。5.3 上电立刻初始化失败PAJ7620 的启动窗口现象按下复位键后马上执行 paj7620_init第一个寄存器就 NACK等两三秒再初始化又完全正常。断电重新上电必现复位后则偶发。原因PAJ7620 内部上电后需要时间完成晶振起振和算法加载这个期间 I2C 从机逻辑还没准备好。F7 主频高从复位到 main() 可能只用了十几毫秒传感器还没醒过来。解决init 里的 HAL_Delay(300) 不能省而且延时期间不要碰 I2C。如果项目里有其他外设需要在开机时尽快配置可以把 PAJ7620 的初始化放到最后给传感器留足上电时间。5.4 F7 的 I2C 状态卡死一次 NACK 之后所有传输全部超时现象某次 I2C 通信出错后后续所有 Master_Transmit 都返回 HAL_BUSY 或 HAL_TIMEOUT不复位不恢复只能重新上电。原因HAL 库的 I2C 驱动在收到 NACK 或总线错误后外设状态寄存器还停留在 BUSY。这是 F7/F4 上 HAL I2C 的经典问题不是 PAJ7620 独有的但在传感器热插拔或者供电抖动时很容易被触发。解决每次 I2C 返回错误后对外设做一次软复位。static void paj7620_i2c_reset(void) { HAL_I2C_DeInit(hi2c1); HAL_Delay(1); HAL_I2C_Init(hi2c1); }在 paj7620_i2c_write_reg 和 paj7620_i2c_read_reg 的错误分支里调用这个函数比单纯重新发送数据可靠得多。逻辑上就是出错先复位再重试代价是几十微秒的延时对几十毫秒一次的手势读取毫无影响。5.5 I2C 总线上挂了 OLED 等设备速率和时序互相干扰现象PAJ7620 单独跑一切正常跟 OLED 挂同一条 I2C 总线后偶尔丢手势OLED 花屏甚至初始化失败。单独测试两个设备都正常。原因总线电容变大400kHz 下沿不够陡。另一个原因是某些 I2C 设备在连续传输时对 Restart 条件敏感PAJ7620 和 OLED 交替访问时F7 的 I2C 外设产生的时序和单个设备独占时不一样。网上常见的 HAL 库驱动 OLED 代码默认按自己的时序来两个设备叠加后就出问题。解决总线频率先降到 100kHz这是最简单的一招。每个设备访问完成后确保 STOP 条件完整HAL 的 Master_Transmit 天然带 STOP用 Mem_Read 时要注意部分写法会把 STOP 吞掉。如果还不行把 OLED 换到软件模拟 I2C硬件 I2C 留给 PAJ7620软件 I2C 跑 OLED 这种低速设备绰绰有余。6. 把九种手势变成可用交互状态机消抖、串口调试与结果验证6.1 连续三帧一致才上报原始手势数据直接用于产品会误触手掌抖一下就可能丢出一个左移。工程里常见做法是做一个连续帧消抖同一手势连续出现三帧才判定有效。帧间隔取 50ms三帧约 150ms既不会迟钝也不会漏掉快速滑动。void gesture_sm(uint8_t ges) { static uint8_t last_ges 0; static uint8_t cnt 0; if (ges last_ges) { if (ges ! 0 cnt 3) { cnt; } if (cnt 3) { cnt 0; gesture_action(last_ges); /* 稳定三帧执行动作 */ } } else { last_ges ges; /* 手势变化重新计数 */ cnt 0; } }这个状态机放在轮询循环里每次调用即可。gesture_action 里做具体业务简单点就是点 LED 或者蜂鸣器复杂一点可以驱动 WS2812B 灯环看项目需求。消抖的本质是牺牲 150ms 延迟换取稳定性对于菜单切换、翻页这类交互完全可接受。6.2 串口输出与手势验证手势 ID 打出来是最快的验证方式。F7 工程里配一个调试串口重定向 printf 后用串口助手观察。电脑侧如果是 CH340/CP2102 这类 USB 转串口驱动装好后就能识别用 ST-Link/J-Link 的话调试器和虚拟串口的驱动也一并装好省一根外接 USB-TTL。printf(gesture0x%02X\r\n, gesture);波特率统一用 115200 8N1串口没输出时先查波特率和 TX/RX 是否接反。验证手势时手掌在传感器前方 5-15cm 距离做标准动作不要贴太近PAJ7620 的红外窗口对距离有要求贴太近反而进入盲区。如果上下左右规律性混淆先检查传感器的安装朝向再回头看代码方向问题九成是硬件装反了。我从第一次在这个项目上被 0x73 卡了一整天之后现在不管换到哪颗 F7、哪个手势传感器第一件事都强制追问一句这个地址是 7 位还是 8 位同一份初始化序列也会先过逻辑分析仪确认 Bank 切换再谈其他排查。这套顺序看着啰嗦但每次都能把手势驱动的调试时间压到半天以内。工程包拿到后先把 I2C 地址宏和 Bank 切换两个点核对一遍再编译基本就能直接跑起来。希望帮到你。本文还有配套的精品资源点击获取