
简介AN41908 驱动源码压缩包面向嵌入式驱动开发与自动对焦系统开发者解决通过 SPI 总线控制 AN41908 芯片、实现镜头精确聚焦的核心问题。包体共 9 个文件包含 6 个 C 源码覆盖聚焦、变焦、读写等核心逻辑、2 个头文件与 1 个 Makefile整体约 16KB结构紧凑方便直接阅读、编译与移植。源码完整涵盖驱动初始化、SPI 通信协议、传感器位置数据处理、电机运动控制、错误检测恢复及性能优化等关键环节借助配套 Makefile 可快速搭建工程围绕自动聚焦命令集与寄存器配置进行二次开发以适应不同硬件平台和光照条件。目前已有 1203 人学习下载适合需要从零理解 AN41908 交互逻辑、或在现有驱动框架上做定制优化的嵌入式工程师。1. AN41908 的 SPI 驱动源码包先别急着编译第一次拿到 an41908a.zip 时我干过最傻的一件事是先把工程打开想找到一个能直接调用的 SPI 收发函数。结果发现真正该先读的是跟压缩包同目录的那份 AN41908 数据手册里的时序图这芯片表面上是标准 SPI 接口实际上它把 SCK 当作移位时钟数据从 SI 一位一位灌进去之后还要在 LATCH 引脚上给一个上升沿内部寄存器才会真正更新。因为多了这么一步直接把 STM32 的硬件 SPI 接上去能看见波形电机却纹丝不动。这篇就是顺着这个痛点讲清楚AN41908 的 SPI 驱动该怎么看帧结构、怎么写寄存器、怎么用 stm32f103 的 HAL 库在 stm32cube MX 环境里落地以及最后怎么用逻辑分析仪和镜头实测反推参数。适合手里有模组、正要移植 41908 驱动源码的嵌入式工程师也适合刚接手别人代码、想搞明白那几十个宏是干什么的 BSP 新人。2. AN41908 的 SPI 接口与帧结构时序图比注释更值得看2.1 引脚不是标准四线LATCH 和 EN 才是驱动命门AN41908 这类镜头聚焦驱动 IC表面上是 SPI 从机实际上更接近一个“带锁存的移位寄存器”。常见的引脚功能可以按下面这张表去核对而不是直接套用标准 SPI 的 MOSI/MISO 概念引脚名方向功能对 STM32 的接法SCK输入串行移位时钟SPI_SCK速率先按 1MHz 起步SI输入串行数据输入SPI_MOSI注意芯片侧一般没有数据输出LATCH输入帧锁存上升沿生效普通 GPIO软件控制EN输入输出级使能普通 GPIO上电后由程序拉高RESET输入复位内部逻辑GPIO系统复位后给一次低脉冲注意这里我没有写 MISO。很多电机驱动芯片是纯写器件SI 进去的数据只进不出不像 nrf24l01 那种射频芯片有完整的双向 SPI。如果你在移植代码时发现源码里没有读函数这是正常的不要浪费时间去找回读寄存器。提示如果芯片手册里确实有 SO/SDO 引脚那说明支持回读此时要把回读当成“校验手段”而不是“控制手段”控制流程仍然以写帧为主。EN 引脚是最容易漏配置的一根线。驱动源码里可能已经把 EN 写死在初始化函数里了但硬件上如果没接对 GPIO或者初始化顺序在 SPI 之前电机就会处于高阻态。我一般会把 EN 单独拎出来放在AN41908_PowerOn函数里而不是混在寄存器配置里这样出问题时隔离更快。2.2 16 位帧的位序与字段划分先搞清楚地址位在哪一端AN41908 的 SPI 写入一般是一帧 16 位先发最高位。16 位里通常包含寄存器地址和数据两个部分。最常见的划分是高 8 位地址、低 8 位数据但也有人遇到过高 4 位地址、低 12 位数据或者地址里还带读写标志的。所以拿到 an41908a.zip 里的源码第一件事不是看函数名而是看#define的寄存器地址值如果地址从 0x00 到 0x0F很可能是 4 位地址如果地址间隔很大、出现 0x30 这种多半是 8 位地址。不管哪种划分方式写帧的骨架是一样的。下面这段代码就是通用写寄存器流程uint16_t frame; frame (uint16_t)((reg_addr 8) | (reg_data 0xFF)); HAL_GPIO_WritePin(LATCH_GPIO_Port, LATCH_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, (uint8_t *)frame, 2, 100); HAL_GPIO_WritePin(LATCH_GPIO_Port, LATCH_Pin, GPIO_PIN_SET);这段代码有三个关键点。第一reg_addr 8是假设高 8 位地址如果你的芯片手册写的是其他划分方式这里要改成对应的位移量第二HAL_SPI_Transmit发送的字节序取决于 STM32 的配置如果 SPI 配置成 MSB First 且数据长度为 8 位那么frame的字节序是反的需要把frame拆成两个 uint8_t 按高位在前发送或者直接把 SPI 数据宽度配成 16 位第三LATCH 的低电平时长不需要太长只要保证在 SPI 传输完成之后、且下一个片选周期之前给上升沿即可。2.3 上电与复位顺序先复位再配置最后使能驱动源码里最容易被人忽略的就是上电顺序。AN41908 上电后内部逻辑需要一个稳定时间如果在电源还没稳定时就拉高 EN输出桥臂可能进入不定态。常见做法是按下述顺序操作void an41908_power_on(void) { // 1. 复位引脚拉低让内部逻辑回到已知状态 HAL_GPIO_WritePin(RESET_GPIO_Port, RESET_Pin, GPIO_PIN_RESET); HAL_Delay(10); // 2. 释放复位等待内部振荡器起振 HAL_GPIO_WritePin(RESET_GPIO_Port, RESET_Pin, GPIO_PIN_SET); HAL_Delay(2); // 3. 先配好 SPI 相关寄存器再打开输出使能 an41908_write_reg(REG_MODE, 0x01); an41908_write_reg(REG_DRIVER, 0x03); // 4. 最后拉高 EN HAL_GPIO_WritePin(EN_GPIO_Port, EN_Pin, GPIO_PIN_SET); }参数说明里值得注意的就是REG_MODE和REG_DRIVER这两个地址它们在不同的 41908 版本里定义不同但功能上一定是“工作模式选择”和“驱动电流/步进方式配置”。翻译时千万不要照搬网上的通用驱动。我踩过一个坑把别人芯片的 REG_MODE 值直接写进去结果电机在一个方向一直转因为模式寄存器里包含了旋转方向位。上电时序里HAL_Delay(10)不是随便定的很多电源芯片的稳压输出要 5 到 10 毫秒才能稳定太短会出现“第一次上电能转、断电重启后不转”的诡异现象。3. STM32CubeMX 起一个 AN41908 最小驱动HAL 库 SPI 读写的正确姿势3.1 CubeMX 里把 SPI 和 LATCH/EN 引脚一次性配齐用 stm32cube MX 打开工程后SPI 部分按下面这张表的参数配置基本可以覆盖大部分 AN41908 应用配置项推荐值说明ModeTransmit Only Master不需要接收时选这个省一个 MISO 引脚Hardware NSS SignalDisable片选和锁存都用 GPIO 控制更灵活Data Size8 Bits配合软件拼帧兼容性最好First BitMSB First符合移位寄存器的高位在前习惯Prescaler32 或 64先慢后快1MHz 起步CPOL / CPHAMode 0 或 Mode 3具体看手册时序图见 3.2 说明把 SPI1_SCK 和 SPI1_MOSI 分配给 GPIO然后 LATCH 和 EN 用两个普通输出引脚。这里我强烈建议把 LATCH 独立出来而不是用硬件的 NSS因为硬件 NSS 在 HAL 库下受SPI_InitStruct控制很难做到“传输完后手动拉高且不影响下一帧”而 GPIO 控制可以在任意时刻改变电平。3.2 8 位数据宽度下拼帧避免字节序踩坑如果 CubeMX 里 Data Size 配的是 8 Bits那么HAL_SPI_Transmit发送 2 字节时byte 顺序由你的代码决定。AN41908 一般在 SI 引脚上先收到高位地址再收到低位数据。所以写帧时不能直接把 uint16_t 的地址丢进去而是要把高字节先发void an41908_write_reg(uint8_t addr, uint8_t data) { uint8_t tx_buf[2]; tx_buf[0] addr; // 高 8 位寄存器地址 tx_buf[1] data; // 低 8 位寄存器数据 HAL_GPIO_WritePin(LATCH_GPIO_Port, LATCH_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, tx_buf, 2, 10); HAL_GPIO_WritePin(LATCH_GPIO_Port, LATCH_Pin, GPIO_PIN_SET); }这段代码比直接传uint16_t安全因为HAL_SPI_Transmit的形参是uint8_t *如果你传(uint8_t *)frame在大小端不同的 MCU 上会有完全不一样的结果。STM32F103 是小端uint16_t frame (addr 8) | data在内存里低地址存的是data高地址存的是addr而 SPI 移位寄存器通常要求先发高地址里的内容所以直接传frame会把数据当成地址发出去。提示如果确认 MCU 和芯片都是 MSB 在前也可以把 SPI 配置成 16 Bits 数据宽度直接发addr * 256 data。但 8 位宽度加数组的方式在以后要调整帧长度时改动最小。3.3 驱动源码的文件分层一个寄存器宏头文件值一套驱动在 an41908a.zip 这种老源码包里经常能看到一个大.c文件把所有功能写完函数之间共享全局变量。我自己的习惯是把它拆成三层an41908_reg.h放寄存器地址和位段宏an41908.c放写寄存器、复位、使能等基础操作focus_ctrl.c放聚焦流程。拆分的理由很简单寄存器定义在芯片型号升级后要换而聚焦流程和硬件机械结构绑定两者改动的时机完全不同。3.3.1 寄存器头文件的组织方式#ifndef __AN41908_REG_H #define __AN41908_REG_H #define AN41908_REG_MODE 0x01 #define AN41908_MODE_BIT_SLEEP (1 7) #define AN41908_MODE_BIT_DIR (1 4) #define AN41908_REG_CURRENT 0x02 #define AN41908_CURRENT_MASK 0x0F #define AN41908_REG_STEP 0x03 #define AN41908_STEP_ENABLE (1 0) #endif这里宏命名带了AN41908_前缀是因为工程里可能还有别的 SPI 器件避免MODE这种名字在头文件里冲突。位段的注释写清楚是哪个 bit后续调试时直接查宏就能定位到寄存器里某个功能。3.3.2 基础驱动的接口约定typedef struct { SPI_HandleTypeDef *hspi; GPIO_TypeDef *latch_port; uint16_t latch_pin; GPIO_TypeDef *en_port; uint16_t en_pin; } an41908_dev_t;把 SPI 句柄和 GPIO 引脚打包成一个结构体是为了移植到不同板卡时只改结构体实例。源码包里如果用了静态全局变量比如static SPI_HandleTypeDef *hspi_an41908我会把它替换成上面这种结构体否则同一份代码很难在第二块板卡上复用。4. 驱动源码移植与调试用波形验证 AN41908 命令有没有被吃进去4.1 移植路径选择沿用源码底层还是按 HAL 重写拿到 an41908a.zip 之后有两种做法。第一种是把源码里原本的寄存器定义和驱动逻辑直接抄过来只把 SPI 发送底层替换成 HAL 库第二种是以 HAL 库为准重新写一份极简驱动只参考源码里的寄存器值。这两种我分别用过它们的取舍如下对比项保留源码逻辑HAL 重写寄存器定义完整度高旧寄存器值不会漏中容易抄漏保留位代码可读性低多为裸机寄存器操作高结构清晰调试点集中在底层集中在业务层风险点旧平台时序假设可能失效寄存器初始化顺序可能不对我的建议是第二种重写底层但寄存器定义全部从旧源码里拷过来尤其是那些带_MASK和_SHIFT的宏。因为 AN41908 内部寄存器里通常有保留位这些保留位必须按手册写固定值重写时最容易漏的就是这个。4.2 用逻辑分析仪抓 LATCH 与 MOSI 波形驱动写完后先别接电机用逻辑分析仪同时抓 SCK、MOSI、LATCH 三根线。这时候你会看到 LATCH 在每次传输后抬高一次。理想的波形顺序是LATCH 拉低 → 16 个 SCK 脉冲 → MOSI 依次输出地址和数据 → LATCH 拉高 → 空闲。如果 LATCH 电平变化和 SCK 不齐或者 MOSI 在 LATCH 拉低之前就变了说明 SPI 复用配置有问题。如果芯片支持回读可以在写完寄存器后读回来对比。下面是一个回读示例void an41908_read_reg(uint8_t addr, uint8_t *rx_data) { uint8_t tx_buf[2]; uint8_t rx_buf[2]; // 地址的高位带读标志比如 bit7 1 表示读 tx_buf[0] addr | AN41908_READ_FLAG; tx_buf[1] 0x00; HAL_GPIO_WritePin(LATCH_GPIO_Port, LATCH_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(hspi1, tx_buf, rx_buf, 2, 10); HAL_GPIO_WritePin(LATCH_GPIO_Port, LATCH_Pin, GPIO_PIN_SET); *rx_data rx_buf[1]; }注意这里用了HAL_SPI_TransmitReceive因为回读时 MOSI 上还要继续给时钟MISO 上才会把数据送出来。很多芯片的回读是“先发送地址然后在接下来的时钟周期返回数据”所以收到的第一字节可能是无效数据第二字节才是寄存器内容。提示回读时片选和 LATCH 必须保持低电平跨过整个传输周期不能像普通 SPI 那样每字节拉高一次 CS。4.3 最常见的问题能写不能动、失步、电流声异常现象可能原因排查手段波形正常但电机不动EN 没拉高或电源没到驱动桥测量 EN 引脚电平是否接近 VCC电机只往一个方向转DIR 位写反或步进序列顺序错抓 MOSI对照寄存器掩码改方向位转到一半卡住或抖动步进时序太快内部逻辑跟不上把 step 间隔从 1ms 慢慢调到 5ms电机发热明显电流寄存器配得过大把 CURRENT_MASK 对应值减小一档这些问题的共性根源在于AN41908 的寄存器写入只是配置了驱动参数真正的电机转动还需要内部逻辑产出步进脉冲。所以就算 SPI 波形完全正确也要检查 EN 和电源。反过来如果电机转但不顺问题大概率在寄存器值而不是 SPI 时序上。调试时我习惯在每次写寄存器前加一个 GPIO 翻转点用示波器看“哪一条命令写进去之后电机开始异常”比盯着数据手册猜快得多。5. 聚焦驱动落地的三个参数步序、电流与加减速判据5.1 聚焦流程的最小闭环归位、移位、停止镜头聚焦驱动最常见的动作是开机 → 归位到初始端 → 按目标步数移动 → 停止 → 保持位置。归位时需要读取限位开关或检测堵转这一步依赖硬件结构AN41908 本身不负责。移动部分通常这样组织void focus_move_steps(int32_t target_steps) { int32_t delta target_steps - current_position; uint8_t dir (delta 0) ? 0x01 : 0x00; // DIR 位值 an41908_write_reg(AN41908_REG_MODE, (an41908_read_reg(AN41908_REG_MODE) ~AN41908_MODE_BIT_DIR) | (dir 4)); // 按固定步进间隔发送步进脉冲 for (uint32_t i 0; i abs(delta); i) { an41908_write_reg(AN41908_REG_STEP, 0x01); // 触发一个步进脉冲 delay_us(step_interval_us); } current_position target_steps; }这段代码的两个关键参数是dir的位位置和step_interval_us。前者由寄存器定义决定后者由镜头机械结构决定。对焦镜头通常要求运动平稳步进间隔太短会导致丢步太长则对焦速度慢。经验上从 2ms 开始往下降每次降 200us直到听见电机出现明显噪音再回调一档。5.2 加减速只调一个加速度参数就够了固定频率驱动的问题是启动瞬间冲击大尤其镜头模组有齿轮箱时容易打滑。梯形加减速在 AN41908 上实现很简单因为它只关心步进脉冲间隔不需要芯片支持细分插补。下面这张参数表可以直接作为起点参数初始值调节方向起始间隔5ms越大越稳但启动慢运行间隔2ms越小越快但可能丢步加速度步长0.2ms/步每 10 步减少 0.2ms 间隔保持电流最小一档静止时发热明显减小实现时不需要真的做浮点运算用一个累加变量在循环里递减延时即可。调试到极限时要以“镜头能从最远端匀速滑到最近端且不卡顿”作为可接受标准。5.3 用成像结果验证而不是用耳朵验证驱动调到最后判断对错的标准是成像而不是声音。把镜头对准一个对比度明显的标靶从远到近移动步数每走一步拍一张图计算清晰度指标。如果清晰度随步数单调上升到峰值再下降说明步进方向和位置映射正确如果出现多个峰值或者平台说明丢步了或者回程间隙过大。把两次逼近的清晰度曲线叠在一起如果峰值位置偏移超过 3 步就回程补偿补偿步数通常等于归位后空回程的脉冲数。这一步做完AN41908 的 SPI 驱动才算真正跟镜头成了闭环。本文还有配套的精品资源点击获取