STM32线性CCD寻迹测速智能车:双闭环PID与编码器实践
2026/9/16 2:41:12 网站建设 项目流程

简介:基于STM32与线性CCD的寻迹测速智能车工程,面向嵌入式初学者、毕业设计或课程设计人群,完整覆盖硬件驱动、传感器采集与控制算法等关键环节。项目利用线性CCD提取赛道黑线,配合霍尔直流减速电机和编码器测速,通过PID运算完成寻迹控制,并借助电机PI算法实现速度闭环,是一套可直接运行和调试的完整方案。压缩包共包含170个文件,以C语言源码、头文件及Keil工程文件为主,兼顾编译中间文件、配置脚本和说明文档,整体大小仅7.15兆字节,结构清晰,便于导入工程后快速定位模块。目前已有三百八十余人学习下载,对于希望进阶STM32应用、理解PID控制或准备智能车竞赛的读者,能够提供从原理到代码的直观参考,尤其适合作为毕设选题或实训项目的起步模板。

1. 基于STM32的线性CCD寻迹测速智能车,先明确这条技术路径能解决什么

全国大学生智能车竞赛的竞速组和不少毕设都选“STM32 + 线性CCD + 编码器”这套方案:TSL1401 一类的线性图像传感器负责找赛道线,电机尾部编码器负责告诉主控轮子在真实物理世界里转得够不够快。多数人上手时只盯着 CCD 图像折腾二值化和阈值,车一上跑道,弯道丢线、直道冲线,才发现测速闭环才是“压得住车速”的另一半。

这条技术路径的核心是双闭环控制:外环用线性CCD算转向误差,内环用编码器测速做电机速度闭环,两个环共享同一份误差信息。本文直接给出从接线、像素处理、测速折算到两个 PID 协调的常规做法,适合手上有 STM32F103 开发板、正准备搭车架或做毕设的读者。看完能按章节复现一台能稳定巡线、速度可预期的智能车。

2. 线性CCD与STM32的硬件接线:TSL1401采集、电机驱动与编码器接口

线性CCD 与面阵摄像头最大的区别是只有一行感光单元。以常用的 TSL1401CL 为例,它内部是128个光电二极管排成一条线,输出一路模拟电压AO,像素值越大表示照得越亮。它不像摄像头那样自带帧同步,SI、CLK 两条控制线全靠主控用 GPIO 模拟,积分时间也由主控决定“多久去读一次”,所以先接线、再对齐时序,后面所有寻迹处理才有意义。

2.1 STM32F103 的最小外设资源与引脚分配

跑通一台寻迹测速车,STM32 端需要 ADC、定时器、GPIO 三类资源。以最常见的 STM32F103C8T6 为例,引脚分配建议如下:

外设功能建议引脚/定时器用途
CCD 信号线 SI、CLK任意两个 GPIO,如 PB0/PB1输出帧同步与移位时钟
CCD 模拟输出 AO任意 ADC 通道,如 PA0ADC + DMA 连续采样
左/右电机 PWMTIM1 CH1(PA8)、CH2(PA9)输出两路 PWM 占空比
电机方向普通 GPIO 两个控制正反转
左/右编码器TIM4 CH1/CH2、TIM3 CH1/CH2编码器模式读转速

选型理由:TIM1 是高级定时器,后续要加互补输出或刹车控制比较方便;编码器必须接在定时器的两个输入通道上,并且不能和 PWM 定时器共用,否则计数会干扰占空比更新。留一路 USART 给调试用,跑车时通过串口把 CCD 图像和速度数据发回电脑,这是后面调参的基本手段。

2.2 TSL1401 的帧读出时序与 ADC 采样对齐

TSL1401 的一个完整读帧过程是:先把 SI 拉高,产生一个 CLK 上升沿锁存当前积分结果,再拉低 SI,然后连续输出 128 个 CLK 脉冲,每来一个上升沿,AO 脚就移出下一个像素的模拟电压。这里最容易被忽视的问题是“先发完时钟再采数”顺序反了,导致采到的数据整体错位一个像素。

工程上的常规写法是 ADC 开 DMA,每个 CLK 上升沿之后由软件调用一次采样触发,DMA 自动把结果搬进内存,128 个值收完后产生中断或置一个完成标志:

#define CCD_PIXELS 128 uint16_t ccd_buf[CCD_PIXELS]; volatile uint8_t ccd_done = 0; void CCD_ReadFrame(void) { HAL_ADC_Start_DMA(&hadc1, (uint32_t*)ccd_buf, CCD_PIXELS); // SI=1,CLK 上升沿,锁存一帧 HAL_GPIO_WritePin(CCD_SI_GPIO_Port, CCD_SI_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(CCD_CLK_GPIO_Port, CCD_CLK_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(CCD_CLK_GPIO_Port, CCD_CLK_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(CCD_SI_GPIO_Port, CCD_SI_Pin, GPIO_PIN_RESET); // 剩余 127 个 CLK,每拍移出一个像素 for (int i = 1; i < CCD_PIXELS; i++) { HAL_GPIO_WritePin(CCD_CLK_GPIO_Port, CCD_CLK_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(CCD_CLK_GPIO_Port, CCD_CLK_Pin, GPIO_PIN_RESET); } ccd_done = 1; }

逻辑说明:DMA 在HAL_ADC_Start_DMA时就开始等待转换完成,CLK 每变化一次,ADC 采一个点。ccd_done放在主循环里被查询,主循环看到标志后再做阈值计算和找线。

参数说明:CCD_PIXELS必须和 DMA 传输长度一致;如果以后换用 256 像素的线性 CCD,只改宏和缓冲区大小不够,还要核对几张表是否同步。两个HAL_GPIO_WritePin调用之间的软件开销约几微秒,对 TSL1401 要求的几百 kHz 时钟完全够用。若在两帧读取之间主动延时几百微秒,等效于延长积分时间,画面整体变亮,这个延时就是 CCD 曝光调节的第一手段。

2.3 TB6612 电机驱动与编码器接线要点

驱动芯片选 TB6612 或 DRV8833 都行,两者都是小封装、逻辑电平直驱,不需要额外光耦。PWM 频率常见设 10~20 kHz,避开人耳敏感频段,电机噪音小,占空比线性度也比 1 kHz 好。方向控制用两个 GPIO 决定正反转,PWM 脚负责调速。

编码器接线要特别说明:带霍尔编码器的直流减速电机输出 A、B 两相方波,两者相位差 90°,A 超前 B 为正转。把 A、B 接到同一个定时器的两个输入通道,配置成TIM_ENCODERMODE_TI12,STM32 硬件会自动对两个通道的上升沿和下降沿都计数,得到 4 倍频计数,低速时测速分辨率最高。

提示:编码器的 5V 和 GND 从电机电源处分出来单独走线,信号线尽量短且避开 PWM 线;刹车瞬间电流冲击很容易让 CCD 的 AO 模拟电平出现毛刺,表现为图像上某几个像素突然异常亮。

3. 线性CCD寻迹核心算法:滤波、二值化与转向误差提取

接线正确只能保证 AD 值跳变,真正决定车能不能走直线的是主循环里这段“图像理解”代码。128 个像素的灰度曲线里,黑线表现为一个低谷,赛道底表现为平台,但实际环境会有反光、阴影和杂物,所以处理顺序一般是滤波、找阈值、提取线中心、最后转成转向误差。

3.1 先做轻量滤波:为什么不能直接拿原始像素找线

TSL1401 模拟输出本身噪声不大,真正的问题来自曝光不均和个别像素点受干扰。单独一个像素跳变就会让“找最小值”的算法瞬间把线中心指到错误位置,因此滤波必须做,又不能太重。128 点数据做 3 点均值滤波是竞赛里最常见的折中:

void CCD_Filter3(uint16_t *src, uint16_t *dst) { dst[0] = src[0]; dst[CCD_PIXELS - 1] = src[CCD_PIXELS - 1]; for (int i = 1; i < CCD_PIXELS - 1; i++) { dst[i] = (uint16_t)((src[i - 1] + 2 * src[i] + src[i + 1]) >> 2); } }

逻辑说明:这是一个窗口大小为 3 的加权均值滤波,中心像素权重为 2,两侧各为 1,右移两位相当于除以 4。它没有中值滤波那样彻底的尖峰去除能力,但计算量极小,在 128 个点上通常 1~2 微秒完成,不挤占主循环时间。

参数说明:如果跑车环境光线稳定,甚至可以去掉滤波只做一次“最小值邻域比较”——连续三个像素都比左右低,才认定为中心。滤波窗口不要加到 7 以上,黑线在图像里经常只有 5~10 个像素宽,窗口太大直接把线“抹平”了。

3.2 动态阈值与二值化的两种做法

阈值的作用是把灰度图像变成“白底、黑线”二值图。固定阈值在竞赛现场换一个灯光环境就要重新标定,不太实用。动态阈值的简单实现是统计整帧最大值和最小值后取中值:

uint8_t threshold_map[CCD_PIXELS]; void CCD_Binarize(uint16_t *pixel, uint16_t threshold) { for (int i = 0; i < CCD_PIXELS; i++) { threshold_map[i] = (pixel[i] < threshold) ? 1 : 0; } }

这个threshold推荐在每帧数据里现场计算,常用公式是threshold = (max_pixel + min_pixel) / 2。逻辑很简单,因为 CCD 视野中只有赛道底和黑线两类物体,两端灰度区分明显;但要注意,当车冲上坡道,整个视野变暗时,最大值下降、最小值也下降,动态阈值会跟着变,比固定阈值稳得多。

如果比赛场地光照闪烁严重,中值法会失效,这时可以用大津法(Otsu)。128 个像素统计灰度直方图后计算类间方差,耗时约几百微秒,放到 10 ms 的主循环里完全可接受。工程上更常见的做法是先固定曝光时间,再在每帧里根据上次的阈值做增量修正,既能避免 Otsu 的抖动,又能适应光照渐变。

3.3 线中心提取:极值法与边沿法

这是寻迹算法里最直接影响转向的部分。极值法认为“最暗的点就是赛道线中心”,实现最简单,对单根黑线效果好;边沿法则先找到黑带的左边界和右边界,取中点作为线中心。极值法代码如下:

int CCD_GetCenterByMin(uint16_t *pixel) { uint16_t minv = 0xFFFF; int minpos = CCD_PIXELS / 2; for (int i = 0; i < CCD_PIXELS; i++) { if (pixel[i] < minv) { minv = pixel[i]; minpos = i; } } return minpos; }

逻辑说明:把整帧最暗像素的下标当作赛道线位置,返回范围 0~127。转向误差用error = center - 64计算,正值表示线在右侧,负值表示线在左侧。

极值法的盲区在黑线附近出现反光或杂色时,比如白色反光点把某个像素拉得很亮没关系,但如果出现一块 случайный黑色污渍,中心会被吸过去。边沿法更抗这类问题:

int CCD_GetCenterByEdge(uint16_t *pixel, uint16_t threshold) { int left = -1, right = -1; int last_black = 0; for (int i = 1; i < CCD_PIXELS - 1; i++) { int black = (pixel[i] < threshold); if (black && !last_black) { left = i; // 检测到由白到黑的跳变 } if (!black && last_black && left >= 0) { right = i; // 检测到由黑到白的跳变 break; } last_black = black; } if (left < 0 || right < 0) return -1; return (left + right) / 2; }

逻辑说明:代码先找第一段“白到黑”的边界,再找紧接着的“黑到白”边界,两个边界取平均就是黑带中心。返回 -1 表示这一帧没有找到一组完整边沿,也就是丢线。

3.4 丢线处理与转向 PD 输出

丢线是最常见也最致命的场景,车在急弯或大倾角下视野里可能完全没有黑线。正确的做法是维持上一帧的误差方向,而不是直接把误差清零:

int last_error = 0; int valid_count = 0; int GetTrackError(uint16_t *pixel) { int center = CCD_GetCenterByEdge(pixel, threshold); int error; if (center < 0) { valid_count++; if (valid_count > 5) { error = 0; // 连续 5 帧丢线,回到直行 } else { error = last_error; // 短暂丢线,沿原方向保持 } } else { valid_count = 0; error = center - 64; last_error = error; } return error; }

转向通道不直接给 PWM 值,而是把误差转换成“左右轮目标速度的差值”。常见映射是比例项加微分项:

float turn_pd = 0.0f; float last_err = 0.0f; #define TURN_KP 3.2f #define TURN_KD 0.6f float UpdateTurn(int error) { turn_pd = TURN_KP * error + TURN_KD * (error - last_err); if (turn_pd > 50.0f) turn_pd = 50.0f; if (turn_pd < -50.0f) turn_pd = -50.0f; last_err = error; return turn_pd; }

参数说明:TURN_KP的数值与误差量纲强相关。误差是像素数,最大约 64,乘 3.2 后最大输出约 205,但被限幅在 ±50,这个限幅值代表左右轮目标速度的最大差值。TURN_KD不要设太大,CCD 每帧数据本身有噪声,微分项放大噪声后会让转向抖动,表现为车头高频摆动。调转向参数时先只留 P,等直道稳定了再加一点点 D。

4. 编码器测速与速度闭环控制:把转速变成可控制的 PWM 输出

测速部分的难点不在于读编码器,而在于把脉冲数换算成物理量,再让 PID 输出平滑地作用到 PWM 上。很多代码跑起来车一抖一抖,问题都出在速度量纲混乱、控制周期不稳、输出限幅不合适这三件事上。

4.1 编码器脉冲与轮轴转速的换算关系

常见带霍尔编码器的直流减速电机,编码器装在电机轴上。以一款常见电机为例:霍尔编码器 13 线,减速比 1:30,STM32 编码器模式 4 倍频。每圈脉冲数的计算方式是:

每轴圈脉冲 = 编码器线数 × 倍频 × 减速比 = 13 × 4 × 30 = 1560 脉冲/圈

也就是说,轮轴转一圈,定时器计数增加 1560。控制周期如果用 10ms,每周期读到的计数值除以 1560,就是 10ms 内轮轴转过的圈数。不同批次电机参数可能有差异,正确做法不是信规格书,而是上电后手动转车轮一定圈数,观察计数是否吻合,误差超过 2% 就要修正参数。

电机配置典型值对测速的影响
霍尔编码器线数13~20 线线数越多,低速脉冲越密
减速比1:30 或 1:45决定轮轴端每圈脉冲数
定时器编码器模式TI12 双通道硬件 4 倍频,分辨率最高
控制周期10~20ms周期越短,速度环响应越快

速度计算可以直接使用“每周期计数增量”作为控制量,不换算成 m/s,这样可以省掉浮点除法,目标速度也用计数增量表示即可。如果需要在屏幕上显示实际车速,再乘一圈周长换算。

4.2 增量式 PID 在定时器中断里的实现

速度环必须在固定时间周期里执行,不能用主循环里的软件延时凑合。工程上最稳妥的做法是配置一个 10ms 定时器中断,中断里读取编码器计数值、清零计数器,然后调用 PID 并更新 PWM 占空比。增量式 PID 避免了位置式 PID 的积分饱和问题:

float speed_pid_out = 0.0f; float e_last = 0.0f, e_prev = 0.0f; float SpeedPID(int target_pulse, int current_pulse) { float e = (float)(target_pulse - current_pulse); float delta = KP_SPEED * (e - e_last) + KI_SPEED * e + KD_SPEED * (e - 2.0f * e_last + e_prev); speed_pid_out += delta; if (speed_pid_out > PWM_MAX) speed_pid_out = PWM_MAX; if (speed_pid_out < -PWM_MAX) speed_pid_out = -PWM_MAX; e_prev = e_last; e_last = e; return speed_pid_out; }

逻辑说明:target_pulse是 10ms 内期望的编码器计数增量,current_pulse是实际增量。误差缩小到零时,增量式 PID 输出不再变化,因此它能自然保持当前 PWM 状态,不会像位置式那样一直累计误差。输出限幅PWM_MAX通常取 700~900(对应 16 位定时器 ARR 值),防止启动瞬间占空比直接拉满。

参数说明:KI_SPEED是积分项系数,这里由于是增量式,积分项实际作用在误差本身上,系数过大会在匀速阶段产生周期性波动,常见表现为电机声音忽高忽低。KD_SPEED对转速噪声特别敏感,编码器在低速时脉冲间隔不均匀,微分项会把这种不均匀放大,所以速度环一般KD给很小或者干脆给 0。

4.3 转向误差如何干预目标速度

如果转向和速度两个环完全独立,弯道会非常难控制:转向环拼命把轮子掰向一边,速度环却固执地要把速度维持在直道水平,结果就是侧滑丢线。竞赛方案里通常用弯道降速函数把两部分耦合起来:

int speed_target = BASE_SPEED - (int)(SPEED_DROP_K * fabs(error)); if (speed_target < MIN_SPEED) speed_target = MIN_SPEED;

error就是第 3 章算出的转向误差,误差越大说明赛道弯曲越明显,目标速度越低。SPEED_DROP_K一般取 2~8,具体要看赛道最大曲率。取值太大会让直道上一点点微抖就造成明显减速,取值太小则高速入弯时速度降不下来。BASE_SPEEDMIN_SPEED都用“每周期编码器计数增量”这个量纲,与速度 PID 保持一致。

速度环收敛后,整车的动态行为会变得“听话”:直道误差小,目标速度高;入弯误差变大,目标速度自动下降;出弯后误差回落,速度再提上去。这就是外环目标给内环、内环执行目标的级联控制,也是整套寻迹测速方案最标准的结构。

5. 调参顺序与实测技巧:先盯曝光和阈值,再动 PID 增益

把代码烧进去只完成一半,剩下的时间都在调参。最忌讳的是上来就改速度 PID,三四个参数一起动,车跑起来完全不知道是谁的问题。我的做法是固定一套调试工序,每一步只换一个变量。

5.1 用串口把 CCD 曲线和速度搬到电脑上

先不要着急跑车,把车架空,轮子悬空,在主循环里用串口把一帧像素数据和两个电机的转速吐出来:

void DebugOutput(uint16_t *pixel, int error, int speed_l, int speed_r) { printf("E,%d\r\n", error); printf("S,%d,%d\r\n", speed_l, speed_r); for (int i = 0; i < CCD_PIXELS; i += 8) { printf("C,%d,%d\r\n", i, pixel[i]); } }

逻辑说明:串口助手里能看到E开头的是转向误差,S开头的是左右轮速度,C开头的是 CCD 像素值。上位机用一个简单的数据可视化工具,或者直接用串口助手自带的曲线功能,就能看到 128 点像素曲线里黑线的位置。

提示:串口波特率建议 115200,控制周期 10ms 时每秒打印约 100 帧,数据量完全够用。不要在定时器中断里打印,串口阻塞会直接破坏控制周期,造成速度环抖动。

5.2 一套可行的调车顺序

顺序调整项典型现象调整方向
1CCD 曝光时间黑线不明显或整幅全黑增加或减少两帧间隔延时
2动态阈值找线中心跳动检查 max/min 是否被反光点干扰
3转向 KP直道走 S 形减小 KP,或加一点 KD
4转向 KD弯道甩尾、高频摆动减小 KD,检查微分项是否放大噪声
5速度 KI匀速阶段电机声音波动减小 KI,改小积分系数
6弯道降速系数入弯冲出去增大 SPEED_DROP_K 或降低 BASE_SPEED

优先调转向、后调速度的逻辑是:转向环作用在目标速度上,速度环只是执行者。如果转向误差本身不稳定,速度环调得再完美,车也会因为目标速度乱变而发飘。反过来,速度环没调好,转向正常也白搭,PWM 输出抖动会让 CCD 画面跟着晃动。

5.3 最后看数据,不靠感觉

车跑完一圈,别只说“感觉好多了”,把记录下来的误差和速度数据拉出来看。转向误差曲线应该是平滑起伏的,如果出现等间距锯齿,说明 CCD 采样噪声没滤干净;速度误差曲线稳态波动应该在 ±3% 以内,否则检查编码器接线和 PID 周期是否稳定。每次改完参数都要保留一版上一轮的调试记录,对比同一赛道段的误差峰值和超调量,这样 10 次调试迭代后车况会明显收敛。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询