news 2026/9/8 22:14:23

基于STM32F103的摄像头循迹小车:从图像处理到PID控制全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于STM32F103的摄像头循迹小车:从图像处理到PID控制全解析

简介:基于STM32F103的摄像头循迹智能小车系统,面向嵌入式开发学习者和智能车竞赛爱好者,融合OV7670图像采集、二值化道路识别和超声波避障等关键技术,可广泛应用于毕业设计、课程设计与机器人入门实践。压缩包共166个文件,以C源码与头文件为主体(77个h、72个c),另有Keil工程配置(uv2)、启动文件(s)、编译脚本(bat)、固件(hex)及测试位图(bmp)等,整体仅751KB,结构紧凑,便于按模块查阅和移植。已有2338人学习,是经过实践验证的完整项目。资源内提供全套工程源码,涵盖摄像头驱动、图像二值化与边缘检测、道路边界提取、循迹方向控制、超声波测距避障等模块,代码注释清晰,配合Keil工程可直接编译烧录,适合对照原理图逐模块学习,也可在此基础上进行二次开发。 手头这个“基于stm32f103摄像头循迹.zip”,我相信很多人是跟我一样,从学长手里拷过来或者从某个开源社区下载的。解压以后一看,里面的工程文件、原理图、文档乱七八糟,折腾了半天也不知道从哪下手。作为一个把摄像头循迹小车从入门到放弃再到入门反复折腾过好几遍的人,今天我就拿这个典型工程当例子,把基于STM32F103的摄像头循迹项目拆开揉碎,从硬件选型到图像处理,再到PID控制,一次讲清楚。

这篇文章不光是解释代码,重点是把“为什么这么设计”讲明白。比如为什么很多人选F103而不是F407,为什么摄像头要用带FIFO的OV7670而不是直接怼DVP接口,为什么边线提取要按行扫描而不是全局遍历。这些都是课本上不讲、但实际做项目一定绕不开的问题。无论你是准备智能车竞赛,还是做毕业设计,或者是单纯想给自己搞一辆能自动跑的小车,这篇文章都值得你花十分钟看完。

1. 项目整体设计与方案选型

1.1 为什么主控选STM32F103

摄像头循迹这个场景,对主控的要求其实就三条:能采集图像、能跑图像处理算法、能输出PWM控制电机。STM32F103系列虽然已经是“上古时代”的芯片了,但在这个场景下简直是为它量身定做的。

以最常见的STM32F103C8T6为例,72MHz主频、64KB Flash、20KB SRAM。很多人担心20KB内存不够用,实际上摄像头循迹根本不需要存整幅大图。如果用的是OV7670带FIFO的模块,输出分辨率可以设到QQVGA(160x120),二值化之后每个像素只需要1个bit,整幅图也才160x120/8 = 2400字节,内存完全绰绰有余。就算是灰度图,160x120个unsigned char也才19.2KB,勉强能塞下。

(这里顺便说一句,20KB SRAM这个数字,决定了你不能用太大分辨率的图像直接做全图处理,后面讲算法的时候会提到怎么在内存限制下做优化。)

F103的另一个优势是生态成熟。标准外设库V3.5随便一搜就是下载链接,中文参考手册也到处都是。哪怕你用的不是C8T6,换成了RCT6或者ZET6,代码基本不用改,改一下启动文件和芯片型号就能跑起来。对于学生项目来说,这是巨大的时间成本优势。

1.2 摄像头模块怎么选:OV7670与总钻风对比

摄像头模块是整个项目里最关键的传感器,选错了一切白搭。先明确一点:循迹小车用的摄像头,核心任务不是拍出好看的照片,而是“在尽量低的主控负载下,清晰地区分出赛道和背景”。

目前市面上主流的选择就两类:

一是OV7670带FIFO模块。这是入门最常见的方案。模块自带AL422B FIFO芯片,摄像头把图像数据先写入FIFO,主控需要的时候再从FIFO读出来。这解决了OV7670本身没有读时钟、必须按像素时序读出的尴尬问题。主控只需要控制FIFO的读使能和读时钟,就能把数据慢慢读出来,不要求实时处理,对主控的时序压力小很多。

二是总钻风(MT9V032),这是智能车竞赛圈子里用得很多的方案。它的优势是灰度输出、动态范围大、对光线适应性好,而且逐飞科技有配套的库和资料,上手非常方便。当然价格也比OV7670贵不少。

如果你是想快速把项目跑起来,我建议先用OV7670 FIFO模块,成本控制在30块以内,坏了不心疼。等把整个流程调通了,再考虑要不要换总钻风。

1.3 工程文件结构:这个zip包里应该有什么

一个规范的摄像头循迹工程,解压以后至少应该有这几块内容:

  • 基础驱动层:包括GPIO初始化、定时器PWM初始化、串口初始化、I2C初始化(用于配置OV7670寄存器)。这部分属于“伺候硬件”的活,代码量最大但技术含量相对低。
  • 摄像头驱动层:核心是摄像头的寄存器初始化序列和图像采集逻辑。OV7670的寄存器配置非常繁琐,网上的初始化函数动辄几百行,但基本都是对着数据手册抄的,注意SCCB接口的时序要严格按手册来。
  • 图像处理层:这是整个项目的灵魂。包括灰度图转二值图、边线提取、中线计算、偏差输出。后面我会重点拆这部分。
  • 控制层:根据图像处理得到的偏差,计算舵机打角和电机转速。通常用PID实现。
  • 调试辅助层:串口打印图像、参数在线调整、蓝牙遥控等。

拿到一个陌生工程,我建议先按照这个分层去读代码,不要上来就盯着一两个函数死磕。

2. 硬件搭建与电路设计要点

2.1 最小系统的供电与接线方案

摄像头循迹小车说白了就是一个典型的嵌入式系统,硬件搭建的核心是供电和信号连接。先说供电,大部分人的第一辆车都是用两节18650锂电池串联,标称7.4V。这个电压给电机驱动模块(如TB6612或L298N)非常合适,但给STM32、摄像头、舵机供电就必须降压。

实际测试下来,我推荐的分配方案是这样的:

器件供电电压来源
STM32F103最小系统板3.3VAMS1117-3.3稳压
OV7670摄像头3.3VAMS1117-3.3稳压(需单独滤波)
舵机(如SD-5)5V-6V独立降压模块或电池直接(视舵机规格)
电机驱动逻辑部分5V/3.3V视模块型号
电机本体7.4V电池直接接驱动板

有个容易踩的坑:OV7670的FIFO芯片和摄像头的感光元件对电源纹波比较敏感,如果和舵机共用同一个5V降压,打舵瞬间可能导致图像出现横纹。建议摄像头的3.3V单独用一路LDO,并且串联一个10uF电容滤波。

2.2 核心接线:DVP接口与PWM通道

STM32F103和摄像头的连接,我用PA0-PA7做8位数据线,PC0接FIFO读时钟,PC1接FIFO读使能,PC2接FIFO写时钟,PC3接VSYNC场中断。这套方案的优点是8位数据线全部挤在PA口,读数据的时候可以直接用GPIOA->IDR一次读取,不需要逐bit拼凑,效率高很多。

PWM通道分配上,舵机用TIM2_CH1(PA0)输出50Hz的PWM信号,电机驱动用TIM3_CH1和TIM3_CH2(PA6、PA7)输出10kHz的PWM信号控制转速和方向。注意舵机是50Hz,电机是10kHz,两者频率完全不同,不能共用一个定时器的同一个通道。

2.3 摄像头安装角度与高度

这个细节课本上从不讲,但实际影响巨大。摄像头的高度和俯仰角直接决定了你“看得多远、看得多宽”。以我的车为例,CCD/CMOS摄像头安装在车头前方约15cm处,高度25cm,俯仰角约10度。这个角度下,图像底部的行对应车前的赛道(约10cm远处),顶部的行对应约1.2米远处。

为什么要这样设置?因为摄像头循迹和红外循迹最大的区别就是“预期性”。红外传感器只能检测车底的赛道线,而摄像头能看到前方1米多的赛道情况,为转向提前做出预判。俯仰角越小,看得越远,但底部盲区越大;俯仰角太大,视野太近,高速过弯时反应不过来。这个需要根据自己的赛道速度实际调试,没有绝对标准。

3. 图像处理与循迹算法核心

3.1 二值化:从灰度图到黑白图的阈值选择

摄像头采集回来的原始数据是灰度值(0-255),赛道是白的,背景是黑的(假设是深色背景),目标就是通过一个阈值把图像变成纯黑和白。大于阈值的置1,小于阈值的置0。

阈值怎么选是第一个大坑。固定阈值在室内固定灯光下勉强能用,但一旦环境光变化,比如太阳光照到赛道上,同样的阈值就废了。我试过几种方案,按推荐度排序:

  1. 大津法(OTSU):自动根据灰度直方图计算最优阈值。原理简单说就是让分割出的前景和背景类内方差最小、类间方差最大。计算量也不大,256个灰度级遍历一遍即可,在F103上跑一个160x120的图像,耗时约5ms,完全能接受。
  2. 动态阈值:只取图像底部几行的灰度平均值再加上一个固定偏移作为当前帧阈值。优点是速度快,缺点是对突然的光照变化反应迟缓。
  3. 固定阈值:仅仅是调试时的临时方案,不建议作为最终方案。

具体大津法的实现不复杂,核心就是统计灰度直方图,然后遍历阈值计算类间方差,取最大值对应的灰度级。在标准库工程里大概四五十行代码就能搞定。

3.2 边线提取:按行扫描与丢线处理

二值化之后,图像变成了一个0和1组成的二维数组。接下来要找到赛道的左右边线。

最常用的办法是从图像底部往顶部逐行扫描,对于每一行,从左往右找到第一个黑到白的跳变点作为左边界,从右往左找到第一个白到黑的跳变点作为右边界。两个边界之间的区域就是赛道。

但实际赛道不是平整的,会遇到各种“意外”情况。最常见的两种:

  • 丢线:摄像头只拍到赛道的一侧边界,另一侧超出了视场角。比如急弯的时候,左边界还在图像内,右边界找不到了。这时候的常规做法是“丢线补线”,用上一帧该侧边界的斜率推断当前帧的位置,或者直接镜像另一侧边界。更简单粗暴的做法是用图像边界作为丢失侧的边界。
  • 全白/全黑:整幅图像全是白色(比如到了十字路口中央全是赛道)或者全是黑色(比如跑出赛道)。这种时候不能靠图像直接算出偏差,必须有状态机逻辑处理。

在写边线提取函数的时候,我强烈建议每一行都记录边线是否有效。这不仅是为了防止数组越界,也是后面处理十字和环岛的必要条件。

3.3 十字路口与补线逻辑

十字路口是摄像头循迹的最大难点之一。小车接近十字时,图像底部是垂直的赛道线,左右两边都是赛道,边线提取算法会同时检测到多条边界,导致计算出的中线严重偏左或偏右,车就歪了。

处理策略是在上层增加赛道状态判断,常见做法是检测底部的赛道宽度突变。正常行驶时,底部赛道宽度大约在40到60像素之间;当头接近十字时,底部赛道宽度会突然超过80甚至达到满幅。一旦检测到这种情况,就进入“十字模式”,直接保持上一帧的舵机角度直行,直到底部赛道宽度恢复正常。

进阶一点的方案是补线逻辑。检测到十字之后,在拐角处人为补一条虚拟边线,让中线计算保持正常,车横穿十字的时候既不犹豫也不偏转。这就是热词里“十字补线”的来由。补线的实现说穿了就是在边线数组里把丢失的那一侧用另一侧镜像加上一个固定偏移量填充。总钻风摄像头因为图像质量高,补线效果会更好。

具体补线代码的逻辑大致是这样的:从底部往上扫描,当某一行某一侧边线丢失时,取该行另一侧有效边线,按赛道经验宽度(比如60像素)计算出一个虚拟边界。这样中线依然是两边界的平均值,算法流程完全不用改。

3.4 PID控制:从偏差到舵机打角

图像处理最终输出的是一个偏差值,通常定义为中心列坐标与实际中线列坐标的差值。假设图像宽160,中心列是80,算出中线在第70列,偏差就是-10,意味着车偏左了,要往右打。

舵机的控制用PD控制器就够了。P项让舵机角度正比于偏差,D项让舵机对偏差的变化率敏感,能有效抑制直道上的蛇形摆动。I项在舵机控制里通常不用,因为舵机本身有回中力,静态误差不严重。

实际参数整定我的经验是先给一个较小的P值(比如0.3-0.5),把D设为0,让车能在低速下跑起来;然后逐步加大P,直到出现左右摆动;再加D抑制摆动,直到高速过弯也不抖。

电机的速度控制则要看有没有编码器。如果有编码器,可以加一个速度闭环,让车在直道自动加速、弯道自动减速。如果没有编码器,做简单的开环控制:根据偏差大小实时调整PWM占空比,偏差大说明在弯道里,就减速;偏差小说明在直道,就加速。这种方案简单有效,赛道适应性稍差,但作为入门完全够用。

4. 核心代码解析与实战配置

4.1 摄像头初始化与DMA采集

工程里摄像头初始化一般是一个大函数,里面几十个寄存器配置。OV7670 默认输出RGB565,我们需要把它配置成YUV422或者RAW格式再手动转灰度。不过更省事的办法是直接用RGB565格式,读取时只取高字节(也就是Y分量),效果等效于灰度图。

这里有一个性能关键点:从FIFO读取数据时,如果用GPIO逐位读取再移位拼接,速度很慢,而且会占用大量CPU时间。推荐的做法是用DMA配合定时器触发,把FIFO的数据直接搬到内存数组里。但F103的DMA通道不多,配置也繁琐,很多人一开始搞不定。如果你也是初学者,可以退而求其次,直接靠GPIO读,160x120的分辨率下,一帧读取时间大约在20ms左右,帧率还能维持20fps以上,完全可以接受。

我最开始也是直接用GPIO读,等整个系统调通了才改的DMA,这样排查问题的时候范围更小。

4.2 图像处理主循环:一份可直接参考的框架

// 主循环伪代码,在一个while(1)里面 while (1) { // 1. 等待摄像头的VSYNC场中断标志 while (vsync_flag == 0); vsync_flag = 0; // 2. 从FIFO读取一帧图像到Image[120][160] Read_FIFO_To_Image(); // 3. 用OTSU计算阈值并二值化 uint8_t threshold = OTSU((uint8_t *)Image, 120 * 160); for (int row = 0; row < 120; row++) { for (int col = 0; col < 160; col++) { Binary[row][col] = (Image[row][col] > threshold) ? 1 : 0; } } // 4. 提取左右边线和有效标志 for (int row = IMG_H - 1; row >= 0; row--) { Find_Left_Right(row); } // 5. 补线、计算中线、计算偏差 Fill_Lost_Edge(); int error = Calc_Center_Error(); // 6. PID计算舵机角度 Set_Servo_PWM(PD_Controller(error)); // 7. 根据偏差和赛道形状控制电机速度 Set_Motor_PWM(Speed_Plan(error)); }

这套框架的好处是把图像采集、处理、控制完全解耦,每个环节都可以单独调试。我调车的时候,会先在串口上位机里把二值化图像打印出来看效果,确认边线提取没问题,再切到电机控制。

4.3 关键参数配置参考表

以我调通的这套参数为例(OV7670 FIFO、STM32F103C8T6、120x160分辨率):

参数数值说明
PCLK频率24MHz由外部XCLK 12MHz倍频而来
图像分辨率160x120QQVGA,兼顾视野和速度
二值化阈值OTSU自适应实测阈值约90-140之间波动
舵机PID系数P=0.35,D=0.8I=0,舵机回中良好
舵机PWM频率50Hz周期20ms,高电平0.5ms-2.5ms
电机PWM频率10kHz避免电机啸叫
直道目标速度1.8m/s开环PWM约65%占空比
弯道减速阈值偏差>25像素超过此值开始减速

这里特别提一下舵机的PWM频率:很多新手直接把电机PWM频率套到舵机上,结果舵机吱吱叫且行程不对。舵机必须用50Hz,脉宽1.5ms左右是回中位,这是舵机的固有规格,不要改。

5. 调试实录与常见问题排查

5.1 串口上位机调试:如何“看见”摄像头看到的画面

调试摄像头循迹最让人头疼的问题就是:车在路上跑,你根本看不到它“眼里”的画面到底是什么。解决办法是做一个简单的串口上位机。

我用的是匿名上位机或者自己写的一个Python小工具,通过串口接收STM32发回来的二值化数组,然后在电脑上以ASCII字符或者图形界面的方式还原出来。每行图像按160个像素打印成160个字符,黑色用空格、白色用#号,一帧120行。然后就能非常直观地看到算法到底把赛道处理成了什么样。

这个调试手段最大的价值在于,它能把图像处理和实际物理环境对应起来。比如某个弯道丢线严重,通过上位机就能看到是算法问题还是摄像头角度问题,而不是靠猜。

5.2 常见问题速查表

根据我自己的调试经历和帮别人调车的经验,整理了这份排查表:

现象可能原因解决思路
上电后上位机无图像摄像头SCCB初始化失败检查I2C引脚配置,用逻辑分析仪抓波形确认应答
图像全白阈值过高或曝光过度查看灰度值分布,降低OTSU上限或减少曝光时间
图像全黑FIOF读时序错误检查读时钟频率,不能超过FIFO最高允许频率
直道走S形P过大或D过小减小P,加大D
弯道直接冲出赛道看得太近或速度过快调大摄像头俯仰角,降低入弯速度
十字路口抖动状态机误判增加判断的连续帧数,比如连续3帧都满足条件才确认
阳光一照就乱跑固定阈值不适用换成OTSU或加三档手动切换

5.3 调试顺序建议

调试一定要有顺序,不要一上来就调PID。我的顺序是:

  1. 摄像头出图,确认看到的赛道清晰、无横纹。
  2. 二值化,保证赛道和背景能明确分开。
  3. 边线提取,上位机里确认左右边线基本贴合赛道。
  4. 舵机打角,用手推车在不同偏差下看舵机是否按预期转向。
  5. 最后才让车跑起来调速度。

前四步都是静态调试,不需要电池大电流放电,也不存在失控风险。只有最后一步才需要宽阔的场地和防撞准备。

5.4 一个容易被忽视的“坑”:陀螺仪融合

很多人做到这里会发现,纯靠图像PID,速度一上来就会甩尾。这是因为摄像头看到的偏差是位置偏差,缺少对车辆横摆角速度的感知。进阶方案是加一个陀螺仪(比如MPU6050只用Z轴角速度),把陀螺仪角速度和图像偏差做加权融合,相当于对转向做一个阻尼,能让车尾稳定很多。

我当时是加了陀螺仪的Z轴角速度作为舵机PID的D项补充,效果立竿见影:同样一段S弯,原来1.5m/s就甩尾,融合之后能稳稳跑到2.2m/s。这是我非常推荐的一个升级方向。

最后的几句经验

摄像头循迹这个项目,说难不难,说简单也不简单。我见过很多人拿到现成代码,烧进去能跑就觉得自己“会了”,结果一换场地、一换灯光就原形毕露。所以我特别想强调:代码只是载体,真正值钱的是你对图像处理流程的理解和对参数的调优能力。

如果你也是刚拿到这样的zip工程,我建议你第一件事不是烧代码,而是打开原理图和配置文件,把数据流捋明白——摄像头怎么把图像送进来、主控怎么处理、处理后怎么控制车。把这套“数据流”刻在脑子里,比背一百个API都有用。等你能做到不看代码就能口述整条处理链路,那这辆车就真正是你的车了。

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

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

不花3万报班!嵌入式自学路线全解析:从C语言到Linux驱动

这个标题我看了很久&#xff0c;心里挺有感触的。2026年&#xff0c;居然还能看到"3万块报嵌入式培训班"这种话题被反复讨论。一方面说明嵌入学这一行的热度确实还在&#xff0c;另一方面也说明信息差依然大得离谱。我见过太多被销售话术忽悠、背着贷款学完却找不到工…

作者头像 李华
网站建设 2026/9/8 22:08:15

RAID5阵列瘫痪全程复盘:从盘故障到数据恢复实战

那台机器是浪潮的&#xff0c;8块1.2TB SAS盘&#xff0c;RAID5&#xff0c;跑的是公司的文件服务器加一部分生产数据库的定期备份。接到电话的时候&#xff0c;对方的语气已经慌得不行&#xff1a;“小X&#xff0c;阵列崩了&#xff0c;所有盘都在报错&#xff0c;文件夹打不…

作者头像 李华