简介:基于STM32与ADNS3080的移动距离测量工程,面向嵌入式开发者、电子爱好者和高校电子专业学生,专注解决光学传感器与单片机协同测距问题。资源共148个文件、2.46MB,包含34个C头文件、31个C源文件,以及Keil工程、编译生成的axf/hex/map/lst文件和PDF说明文档,另含部分备份与配置文件,目录结构清晰,便于查阅和二次开发。代码覆盖SPI接口驱动、ADNS3080寄存器初始化、图像帧差异解析、位移比例换算、中断响应等环节,完整演示了从传感器原始数据到实际移动距离的转换流程。已有960人学习下载,该方案既可用于机器人定位、自动化流水线检测等实际项目,也可作为课程设计、毕业设计参考。资源从硬件连线、初始化配置到数据读取均有明确注释,配合PDF文档可显著降低学习门槛,稍作修改即可适配其他微控制器或传感器,适合不同水平的开发者按需使用。 做小车里程计的时候,我一直被轮子打滑的问题折腾得够呛。编码器装在电机轴上,轮子一空转,累计的距离就全废了。后来换了个思路:不用轮子,直接用光学传感器看地面移动了多少。于是就有了这个“基于STM32的ADNS3080测量移动距离”项目。简单说,就是把光电鼠标里的那颗光学传感器拆出来,装在车底当非接触式里程计用。这个方案能解决轮子打滑、悬空、侧滑带来的测距误差,适合做AGV小车、扫地机器人、巡检底盘这类需要低成本和厘米级位移测量的场景。如果你正在做STM32相关的移动平台项目,这篇文章应该能帮你少走不少弯路。
项目本身不复杂:一颗ADNS3080传感器负责“看”地面,STM32通过SPI读取位移增量,再按比例换算成实际毫米数。真正坑人的地方全藏在细节里——三线SPI怎么接、SROM固件怎么加载、轮询频率怎么选、安装角度怎么矫正。下面我把整个实现过程拆开讲,包括我踩过的坑和实测数据。
1. 先弄清楚ADNS3080是怎么把“移动”变成“距离”的
1.1 光流测距的核心原理
ADNS3080本质上是一个内置图像处理引擎的CMOS图像传感器,内部有个30×30的灰度像素阵列。它工作时会以很高的帧率连续拍摄表面图像,然后通过图像匹配算法计算相邻两帧之间的像素位移,再换算成二维的增量数据(Delta_X和Delta_Y)。这个原理和光电鼠标完全一样,区别在于ADNS3080把数据接口开放出来了,主控可以直接读取原始位移量。
每次传感器内部完成一帧图像的处理,就会在寄存器里更新一组位移增量。主控STM32要做的就是不停地轮询“Motion”寄存器,如果检测到移动标志位置位,就读取Delta_X和Delta_Y两个寄存器,把增量累加起来。累加到一定数值后,再按照CPI(每英寸计数)换算成毫米,就得到了实际的移动距离。
有一个指标要特别关注,就是SQUAL(Surface Quality,表面质量数值)。这个寄存器反映的是传感器当前看到的表面纹理是否清晰,数值越高代表表面可识别特征越丰富。我实测下来,SQUAL在100以上时位移数据比较稳定,低于50就容易出现跳变。如果传感器离地面太高、表面太光滑、或者镜头脏了,SQUAL会直接掉到0附近,那时候读出来的位移就是一堆乱数。
1.2 为什么选ADNS3080,而不是编码器或者其他光流传感器
做测距其实有不少方案,各有各的适用场景。我把最常见的几种放在一起对比了一下:
| 方案 | 测量方式 | 优点 | 缺点 | 典型成本 |
|---|---|---|---|---|
| 轮式编码器 | 接触式旋转计数 | 算法简单、响应快 | 打滑/悬空会丢数据 | 低 |
| ADNS3080 | 光学表面图像匹配 | 非接触、不打滑、可测二维位移 | 对表面纹理和安装高度敏感 | 低 |
| PMW3901光流 | 光学表面图像匹配 | 体积小、常用于无人机定点 | 资料少、镜头模组不好配 | 中 |
| 激光测距仪 | 激光反射测距 | 精度高、绝对距离 | 只能测单点、价格高 | 高 |
选ADNS3080主要是三个原因:一是便宜,模块二三十块钱就能买到,拆机件更便宜;二是资料全,数据手册和开源驱动都很好找,遇到问题不愁没人踩过坑;三是它可以直接输出30x30的原始图像帧,调试的时候能直观看到传感器“看”到了什么,这一点对排查安装问题特别有用。
如果你只是在做普通小车测距,编码器确实够用。但如果你的场景里有打滑风险,比如瓷砖地面、斜坡、急加速急刹车,那光学测距就是更稳的选择。ADNS3080只负责测量相对位移,不依赖轮子转没转。
2. 硬件接线:三线SPI是第一个大坑
2.1 引脚定义和STM32接线表
ADNS3080的接口精简到只有一根双向数据线,这是它和普通SPI外设最大的区别。以下是常见的引脚功能以及和STM32的连接方式:
| 传感器引脚 | 方向 | 功能说明 | 连接STM32 |
|---|---|---|---|
| VDD | 电源 | 3.3V供电 | 3.3V |
| GND | 电源 | 地 | GND |
| NCS | 输入 | 片选,低电平有效 | 任意GPIO推挽输出 |
| SCK | 输入 | SPI时钟 | 任意GPIO或SPI_SCK |
| SDIO | 双向 | 数据线,输入/输出复用 | 任意GPIO或SPI_MOSI |
| NRESET | 输入 | 低电平复位,模块上有就接 | 任意GPIO |
| LED | 输出 | 驱动外部LED | 串限流电阻接VDD,不用MCU控制 |
注意ADNS3080的供电必须稳定在3.3V,传感器本身加LED的工作电流大概在30~50mA,直接用STM32板上的3.3V输出一般够用,但如果你PCB上还有其他大电流外设,建议单独加一颗低压差稳压芯片。
2.2 三线半双工SPI:为什么不能直接当成四线标准SPI用
这是整个项目里最容易翻车的地方。ADNS3080的SPI接口不是标准的四线制,它没有独立的MOSI和MISO,只有一根SDIO双向数据线。写寄存器的时候SDIO作为输入,读寄存器的时候SDIO切换成输出。也就是说,这是一条三线半双工总线。
如果你直接用STM32的标准四线SPI去接,会遇到一个麻烦:读数据时主机的MOSI引脚还在持续输出,如果MOSI和SDIO直接相连,主机的输出会和传感器的输出打架,轻则数据错误,重则可能损坏引脚。很多人第一次调这个传感器,寄存器读出来全是0xFF,多半就是这个原因。
稳妥的解决方案有三种:
- 用GPIO软件模拟SPI,这是我最推荐的方式。ADNS3080的SPI时钟并不高,软件模拟完全跑得动,而且天然适配半双工模式,想什么时候切换方向就什么时候切。
- 用STM32硬件SPI的半双工模式(BIDIMODE),也就是把SPI配置成单线双向,通过BIDIOE位控制方向切换。也能用,但每次读数据前都要切一次方向,稍麻烦。
- 把MOSI和MISO短接后接SDIO。这种做法有人用,但读阶段主机MOSI仍会驱动电平,需要在外围加方向控制电路或者把MOSI配置成开漏加外部上拉,不建议新手折腾。
所以我的实际建议是:直接把ADNS3080的SDIO接到STM32的一个普通GPIO上,用软件模拟SPI做通信。
2.3 软件模拟SPI的读写函数框架
我用的是STM32标准库,GPIO模拟SPI,参考下面的结构即可。先定义两个宏控制NCS和SDIO方向,然后按SPI Mode 3的时序收发字节。
#define ADNS_NCS_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_12) #define ADNS_NCS_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_12) void adns_spi_write_byte(uint8_t byte) { uint8_t i; for (i = 0; i < 8; i++) { // 先输出数据位 if (byte & 0x80) GPIO_SetBits(GPIOB, GPIO_Pin_13); else GPIO_ResetBits(GPIOB, GPIO_Pin_13); byte <<= 1; // 产生时钟:SCK先低再高 GPIO_ResetBits(GPIOB, GPIO_Pin_14); GPIO_SetBits(GPIOB, GPIO_Pin_14); } } uint8_t adns_spi_read_byte(void) { uint8_t i, val = 0; // 切换SDIO为输入模式 GPIO_InitTypeDef gpio_init; gpio_init.GPIO_Pin = GPIO_Pin_13; gpio_init.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &gpio_init); for (i = 0; i < 8; i++) { GPIO_ResetBits(GPIOB, GPIO_Pin_14); GPIO_SetBits(GPIOB, GPIO_Pin_14); val <<= 1; if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13)) val |= 0x01; } // 切回推挽输出,供下次写操作使用 gpio_init.GPIO_Pin = GPIO_Pin_13; gpio_init.GPIO_Mode = GPIO_Mode_Out_PP; gpio_init.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &gpio_init); return val; }有了底层字节收发函数,读写寄存器就变得很简单了。ADNS3080的寄存器地址是7位,读操作时把最高位置1,写操作保持最高位为0:
uint8_t adns_read_reg(uint8_t reg) { uint8_t val; ADNS_NCS_LOW(); adns_spi_write_byte(reg | 0x80); // 读地址 val = adns_spi_read_byte(); ADNS_NCS_HIGH(); return val; } void adns_write_reg(uint8_t reg, uint8_t val) { ADNS_NCS_LOW(); adns_spi_write_byte(reg & 0x7F); // 写地址 adns_spi_write_byte(val); ADNS_NCS_HIGH(); }这段代码里有个细节:读寄存器时,地址发完后要立刻把SDIO切成输入,然后主机继续给时钟,从机就在SDIO上把数据送出来。如果切换方向晚了,可能丢掉前面几位数据。我一开始就是在这里吃了亏,读出来的值总是右移了一位。
3. 上电初始化:不加载SROM固件,传感器就是“死”的
3.1 上电时序与Product_ID校验
ADNS3080上电后不会立刻开始工作,它内部有个外置固件需要加载,这就是SROM(串行只读存储器)固件。你可以在数据手册的附录里找到一段十六进制固件数组,长度通常是2048字节,需要在上电初始化阶段通过SPI写进传感器。
初始化时先做两件事:如果模块有NRESET引脚,先拉低再拉高,给传感器一个外部复位;然后等50ms以上,让内部上电复位完成。接着读Product_ID寄存器(地址0x00),ADNS3080的产品ID一般是0x26。如果读出来的值不是预期值,先不要往下走,回去检查接线、供电、SPI模式,别浪费时间。
固件数组在代码里用const uint8_t数组存起来就行,不需要修改,它本质上是传感器图像处理算法的微码。加载函数很简短,但有个关键点:NCS必须在整个加载过程中保持低电平。
3.2 SROM固件加载流程与NCS全程拉低的原因
SROM加载的流程是这样的:先把NCS拉低,然后发送SROM_Load寄存器的地址(0x17),紧接着把2048字节固件连续发出去,全部发完后才能把NCS拉高。
void adns_load_srom(const uint8_t *firmware, uint16_t len) { ADNS_NCS_LOW(); adns_spi_write_byte(0x17); // SROM_Load 寄存器地址 for (uint16_t i = 0; i < len; i++) { adns_spi_write_byte(firmware[i]); } ADNS_NCS_HIGH(); }为什么NCS要全程拉低?因为传感器在SROM加载阶段靠片选信号识别“固件传输会话”的边界,一旦NCS中途拉高,传感器会认为固件数据已被中断,后续字节会被当成普通寄存器写入处理,加载就会失败。固件加载失败后传感器不一定“罢工”,但输出的位移数据一定是零或乱跳。我第一次调试时在循环里每发一个字节就拉高一次NCS,结果Delta_X和Delta_Y永远读不出有效值,查了半天才发现是这个问题。
加载完成后,建议再读一次Product_ID确认传感器仍然正常响应,然后就可以配置工作参数了。
3.3 常用寄存器配置:CPI与帧率
ADNS3080有灵敏度设置功能,也就是CPI,常见的是400、800、1600三档。CPI越高,每英寸对应的计数越多,分辨率越高,但同样位移下增量溢出也越快。这个配置通过Control寄存器(0x0A)调整,不同批次芯片的位定义可能有差异,动手前一定先翻一下你手里数据手册的寄存器表。
我通常用默认值800 CPI,后面换算距离时按800算。帧率在Configuration寄存器(0x0B)里配置,一般不用调,保持默认即可。真正需要关心的反而是表面质量和安装条件,那才是影响实测精度的主要因素。
4. 读取位移并换算成移动距离
4.1 轮询读取Motion、Delta_X、Delta_Y
初始化完成后,主循环里不停轮询Motion寄存器(0x02)。Motion寄存器的bit7表示是否有运动,置1说明这一帧产生了位移增量;bit6表示是否发生了丢帧或观察条件异常,置1时当前帧数据不建议使用。
实用读取方法是:先读Motion,如果bit7为1,再去读Delta_X(0x03)和Delta_Y(0x04)。这两个寄存器的数据是8位有符号数,范围是-128到127,分别表示X和Y方向的增量。注意要用int8_t类型接收,否则负数会变成很大的正数,累加结果直接崩掉。
int32_t total_x = 0, total_y = 0; void adns_tick(void) { uint8_t motion = adns_read_reg(0x02); if (motion & 0x80) { int8_t dx = (int8_t)adns_read_reg(0x03); int8_t dy = (int8_t)adns_read_reg(0x04); if (motion & 0x40) { // bit6置位说明帧质量差,这组增量不可靠,直接丢弃 return; } total_x += dx; total_y += dy; } }这个函数放在定时器中断里调用,或者放在主循环里用轮盘式调度,频率见下节。
4.2 距离换算公式与轮询周期的取舍
读取到累加计数后,换算成物理距离需要知道CPI。公式很简单:
距离(mm) = 累加计数 × 25.4 / CPI
以800 CPI为例,每个计数对应25
本文还有配套的精品资源,点击获取