资讯动态

线性CCD智能小车赛道感知:TSL1401采样、中线提取与闭环控制

发布时间:2026/9/17 18:11:57 来源:尧图企业网站定制
简介这份PDF面向参加NXP杯智能汽车竞赛的高校学生、嵌入式与自动控制方向的学习者围绕线性CCD循迹智能小车给出较完整的设计与实现参考。内容以MC9S12X S128单片机为控制核心硬件部分梳理电源、CCD图像采集、舵机转向、速度测量与MOS管双电机驱动等模块的选型与电路思路软件部分讲解图像滤波与阈值处理、黑线边缘提取、位置式PD转向控制以及增量式PID速度闭环调速并讨论直道、起跑线、十字等赛道元素的识别与抗干扰处理适合作为课程设计、竞赛备赛或系统开发阶段的技术参考文献。资源为一个PDF文档压缩包约139KB单篇篇幅紧凑便于快速通读与检索关键算法。目前已有122人学习可为搭建循迹小车整体方案、理解CCD图像处理与PID整定思路提供直接借鉴。1. 线性CCD为什么值得成为智能小车的赛道感知方案智能小车的赛道感知常见三条路红外对管、摄像头、线性CCD。红外对管便宜输出只有几个开关量线宽和曲率细节全丢摄像头信息最全可对一块跑在几十到一百多兆主频的 MCU 来说一帧图像的取回、二值化、边缘搜索会吃掉大半个时间片。线性CCD 卡在两者中间TSL1401 这类 128 像素线阵传感器一次曝光吐出一整行灰度输出只有一路模拟电压配 ADC 加 DMA 就能整行搬进内存交给控制系统的是连续、可微的中线偏移量而不是几个离散跳变点。做电磁三轮循迹、工创赛智能物流小车、51 或 STM32 平台的循迹车都适合从它入手链路短、变量少调参时每个现象都能对回一段波形。2. 线性CCD的驱动时序与采样链路怎么搭线性CCD 用得顺不顺九成取决于驱动时序和采样链路搭得对不对算法反而是后面的事。这一章把 TSL1401 的三根信号线讲清楚再给一套 STM32 上用定时器 PWM 加 ADC 加 DMA 的完整采样链路最后落到曝光时间这个最容易被忽略的参数上。链路上任何一环没对齐后面无论怎么调 PID看到的都是抖动的输入和莫名其妙的丢线。2.1 TSL1401的SI、CLK、AO三根线怎么配合TSL1401 对外真正要接的信号只有三根SI 启动一次积分与读出CLK 提供时钟AO 输出模拟电压。它没有寄存器、没有器件地址你给时序它吐电压仅此而已。一次完整读出按下面的顺序走SI 拉高同时保持 CLK 为低电平给出第一个 CLK 上升沿像素阵列的电荷被锁进移位寄存器同时把积分起点重置到这一刻SI 拉低继续给出 128 个 CLK每个下降沿之后 AO 依次输出第 1 到第 128 个像素的电压第 128 个像素读完后 AO 进入无效区一直保持到下一次 SI 脉冲。两次 SI 上升沿之间的时间差就是曝光时间。它的倒数关系要记牢曝光越长输出幅度越大、动态范围越好但车在高速下这一行数据就越陈旧。这是整个系统里第一个必须权衡的参数。引脚方向接到 MCU说明SI输入普通 GPIO一次曝光给一个正脉冲宽度几个微秒即可CLK输入定时器 PWM 通道或 GPIO频率决定 128 像素的读出耗时AO输出ADC 通道或比较器输入0 到 VDD 之间的模拟电压VDD / GND电源3.3V 或 5V与 MCU 同电平采集更省事2.2 用定时器触发ADC加DMA把128个像素搬进内存STM32 上比较省 CPU 的做法是让 CLK 由 PWM 硬件产生同时用这个定时器的更新事件去触发 ADC再由 DMA 把转换结果循环搬走。这样一条流水线上 CPU 只负责发号施令。/* TSL1401 一次完整读出TIM3_CH1 输出 CLK更新事件触发 ADC1DMA 搬 128 个半字 */ #define CCD_PIXELS 128 static uint16_t ccd_buf[CCD_PIXELS]; void ccd_init(void) { /* PA6 TIM3_CH1 输出 CLKPA0 ADC1_IN0 接 AO */ HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); HAL_ADC_Start_DMA(hadc1, (uint32_t *)ccd_buf, CCD_PIXELS); } void ccd_read_row(void) { CCD_SI_HIGH(); __NOP(); /* 满足 SI 相对 CLK 的建立时间 */ CCD_CLK_PULSE(); /* 第一个上升沿锁存像素电荷并复位积分 */ CCD_SI_LOW(); /* 剩余 127 个 CLK 由 PWM 连续输出DMA 同步把 AO 结果写进 ccd_buf */ ccd_wait_dma_done(); HAL_TIM_PWM_Stop(htim3, TIM_CHANNEL_1); }逻辑说明PWM 的每个周期产生一次更新事件ADC 在这个事件上启动一次转换转换点落在 CLK 上升沿之后此时 AO 已经完成切换采到的不是跳变沿。DMA 用循环模式长度设成 128读满一行后回调里置标志位。参数说明ADC 采样时间取 7.5 或 13.5 个 ADC 周期比较合适太短会引入通道串扰CLK 占空比 50%频率压在 200kHz 到 500kHz给传感器手册上限留出余量定时器的 TRGO 必须选更新事件如果误选比较事件采样点会落在 PWM 高电平中间采到的值随占空比漂移。提示AO 走线尽量短远离电机驱动和舵机信号线必要时在 ADC 输入端并一个几十皮法的小电容采到的灰度会干净很多。2.3 曝光时间、CLK频率与整行读出时间的三角关系128 像素的读出耗时等于 128 除以 CLK 频率曝光时间等于 SI 周期减去这个读出时间。三者互相牵制先定 CLK 频率再按车速选 SI 周期。CLK 频率128 像素读出耗时单次曝光可给的时长适用场景50 kHz约 2.6 ms3 到 10 ms暗环境、低速车200 kHz约 0.64 ms1 到 3 ms常规室内赛道400 kHz约 0.32 ms0.5 到 1.5 ms高速车、强光环境固定曝光在不同场地会翻车自适应曝光更省心每行读出后统计灰度最大值和最小值对比度不够就加长 SI 周期整体偏亮就缩短。/* 粗调曝光目标是把灰度动态范围压在 40 到 220 之间 */ static uint16_t si_period_us 3000; uint16_t ccd_auto_exposure(const uint8_t *gray) { uint8_t min_v 255, max_v 0, i; for (i 0; i CCD_PIXELS; i) { if (gray[i] min_v) min_v gray[i]; if (gray[i] max_v) max_v gray[i]; } if (max_v - min_v 60) si_period_us 200; /* 对比度不够多积一点 */ else if (min_v 200) si_period_us - 200; /* 整体太亮减曝光 */ if (si_period_us 800) si_period_us 800; if (si_period_us 8000) si_period_us 8000; return si_period_us; }逻辑说明每行只调一小步避免曝光量在相邻行之间来回跳导致中线摆动。参数说明60 这个对比度门限按你 ADC 的位数标定12 位 ADC 要放大到几百上下限 800 和 8000 微秒对应最快和最慢的两端按实际车速留出手动覆盖接口调试时先用固定值找感觉再切自动。3. 从128个灰度值提取赛道中线二值化与质心算法采样链路给到手的是一行 128 个灰度值控制系统要的却是一个数字中线相对车头正前方的偏移。这一步的转换质量直接决定后面控制环路的输入噪声。阈值选错中线上蹿下跳中线算法选错弯道里会突然跳一个像素宽的平台。把这两件事分开处理先定阈值再谈质心。3.1 固定阈值必然翻车动态阈值怎么算才不抖白底黑线的赛道看起来对比强烈实际灰度受三件事影响环境光在场地各处的分布、镜头到地面的距离、电池电压下降带来的满量程收缩。同一辆车上午在窗边能跑下午灯一开就丢线根子都在固定阈值。常见做法是行内最大最小值法取阈值等于最小值加上动态范围的一个比例。uint8_t ccd_dynamic_thresh(const uint8_t *gray) { uint8_t min_v 255, max_v 0, i; for (i 0; i CCD_PIXELS; i) { if (gray[i] min_v) min_v gray[i]; if (gray[i] max_v) max_v gray[i]; } if (max_v - min_v 30) return 128; /* 对比度太低退回默认值 */ return (uint8_t)(min_v (max_v - min_v) * 0.55f); }逻辑说明系数 0.55 决定把多暗的部分算作黑线。取值偏大时只有真正黑的核心被算进来抗阴影能力强但细线可能被切掉取值偏小时灰边和反光点也会被吃进来中线会被拉向噪声一侧。参数说明30 这个门限用于判断整行几乎没有黑白差异多半是线跑出视野或者车悬空了此时直接返回上一行的阈值更合理。要让阈值不抖还得加一道一阶滤波static float th_filt 128.0f; th_filt 0.85f * th_filt 0.15f * ccd_dynamic_thresh(gray);0.85 的保持系数意味着阈值变化的时间常数在七行左右既能跟上光照的缓慢漂移又不会被单行噪声带跑。3.2 加权质心法求中线的公式与C代码拿到阈值之后中线提取有三种常见做法差别不小。方法优点缺点适用场景左右边缘取中点计算量小抗灰度不均虚线、断线直接失效中线有像素级跳变连续实线赛道加权质心输出连续容忍断线需要标定阈值与权重大多数场景最大值位置最快一行只比较 128 次精度只有 1 像素应急备用加权质心的公式是中线位置等于所有有效像素的灰度权重乘以像素索引之和再除以权重之和。权重取超出阈值的部分这样底板和灰边被压到接近零贡献。#define CCD_PIXELS 128 #define LINE_LOST 0x7FFF int16_t ccd_centroid(const uint8_t *gray, uint8_t thresh) { uint32_t sum_w 0, sum_wx 0; uint8_t i; for (i 0; i CCD_PIXELS; i) { uint8_t w gray[i]; if (w thresh) continue; /* 低于阈值当背景丢掉 */ w - thresh; /* 只用超出阈值的部分做权重抑制底板噪声 */ sum_w w; sum_wx (uint32_t)w * i; } if (sum_w 200) return LINE_LOST; /* 有效像素太少判丢线 */ return (int16_t)(sum_wx / sum_w) - CCD_PIXELS / 2; }逻辑说明返回的是相对视野中心的像素偏移正负号对应线在右侧或左侧。参数说明sum_w 小于 200 是丢线判据这个数必须按自己的曝光量标定——曝光充足时整行权重和可能到几千曝光压得很短时只有几百门限定高了会把正常行误判成丢线。整型运算里 sum_wx 最大约 255 乘以 128 再乘以 128用 32 位不会有溢出风险。3.3 双边缘检测与丢线补线策略断线、十字路口、赛道尽头都会让中线算法瞬间失去输入。边缘法的做法是在视野内分别找左右边缘由于断线处边缘会消失必须先判断边缘数量再决定输出。加权质心本身对断线有一定容忍但权值和会明显下降用这个特征做丢线判据最自然。static int16_t last_err 0; static uint8_t lost_cnt 0; int16_t ccd_get_error(const uint8_t *gray) { uint8_t th ccd_dynamic_thresh(gray); int16_t err ccd_centroid(gray, th); if (err LINE_LOST) { lost_cnt; if (lost_cnt 8) return last_err; /* 短时丢线沿用上次偏差 */ return last_err 0 ? 20 : -20; /* 长时丢线朝上次方向小幅找线 */ } lost_cnt 0; last_err err; return err; }逻辑说明短时丢线直接沿用上一行的偏差等于让车按原方向滑过去长时丢线说明线真的不在视野里给一个固定的小偏差让舵机朝上次见到线的方向打一点给车一个找回赛道的机会。参数说明8 次这个计数按控制周期换算5ms 周期对应 40ms刚好够过一个小断口如果你车速快、断口宽可以加到 12 到 15 次。4. 中线偏差到执行机构智能小车控制系统的闭环实现前两章解决的是感知这一章把偏差变成舵机和电机的动作。舵机负责方向电机负责速度两者共用同一个偏差输入但参数整定的思路完全不同。整定顺序错了你会一直以为算法有问题实际上是环路在打架。4.1 舵机PD控制的参数整定顺序舵机是位置执行机构给 PWM 占空比它就转到对应角度所以控制量直接是角度修正量用 PD 足够积分项通常不用——赛道曲率是变化的积分只会让出弯时回正过冲。#define SERVO_CENTER 1500 /* 中值单位微秒按舵机型号标定 */ static float err_prev 0; int16_t servo_calc(float err) { float d err - err_prev; err_prev err; return SERVO_CENTER - (int16_t)(KP * err KD * d); }逻辑说明偏差为正表示线在视野右侧车需要右转所以这里用减号符号方向取决于舵机的安装朝向上车前先手动给两个极限值确认。参数说明d 项用的是相邻两次偏差的差没有除以时间因此 KP 和 KD 的数值只在固定控制周期下有意义换了周期必须重新整定。整定顺序是先 P 后 D。KP 从 0.3 起步每次翻倍加到直线出现小幅等幅摆动再回退到当前值的六成接着 KD 从 1 开始加它压制摆动加到舵机开始有细碎的吱吱声就说明给大了。参数作用偏大表现起步参考值KP回正力度直线画龙、等幅摆动0.8 到 1.5KD抑制超调、提前打舵高频抖动、舵机发热2 到 64.2 差速小车的增量式PID与限幅电机控制的是速度速度环里积分项才有意义它负责在你想要的速度和实际能跑的速度之间补齐差距。增量式 PID 输出的是这一拍速度的增量天然带限幅需求也天然不需要处理积分饱和。typedef struct { float kp, ki, kd; float e1, e2; /* e[k-1], e[k-2] */ int16_t out; /* 当前速度补偿量 */ int16_t out_max; /* 限幅 */ } inc_pid_t; int16_t inc_pid_calc(inc_pid_t *p, float e) { float delta p-kp * (e - p-e1) p-ki * e p-kd * (e - 2 * p-e1 p-e2); p-e2 p-e1; p-e1 e; p-out (int16_t)delta; if (p-out p-out_max) p-out p-out_max; if (p-out -p-out_max) p-out -p-out_max; return p-out; }逻辑说明左右轮分别写成 base 加 out 和 base 减 outout 就是这里的补偿量。参数说明out_max 要卡在两个地方之内——一是不能大到让内侧轮反转二是不能超过电机驱动在目标占空比下的余量一般取 base 的 30% 到 50%。注意入弯时如果只靠差速、不做方向控制车会在弯中推头。常见做法是让舵机先吃到偏差差速只在偏差超过某个阈值后再开始介入两个执行机构错开工作区间。4.3 控制周期与采样周期错配会带来什么最容易被忽略的坑是把控制放在 CCD 读完的同一个循环里。CCD 一次读出加二值化加质心计算要一到两毫秒若曝光量随环境变化这个周期就在变PID 里的 D 项变成噪声放大器——它算的其实是偏差变化率采样间隔一变同样的偏差差对应完全不同的物理含义。正确做法是把采样和控制解耦CCD 采样可以放在主循环或一个低频定时器任务里控制固定在 5ms 的定时器中断中执行每次取最近一次有效偏差作为输入。可以按下面的结构组织volatile int16_t g_err 0; volatile uint8_t g_err_ready 0; void TIM4_IRQHandler(void) /* 5ms 硬周期只做控制 */ { if (TIM4-SR TIM_SR_UIF) { TIM4-SR ~TIM_SR_UIF; if (g_err_ready) { int16_t s servo_calc(g_err); servo_set(s); g_err_ready 0; } } } void main_loop(void) /* 主循环周期随曝光变化 */ { uint16_t t ccd_auto_exposure(gray); ccd_set_exposure(t); ccd_read_row(); g_err ccd_get_error(gray); g_err_ready 1; }逻辑说明控制周期固定采样周期浮动两者通过一个标志位交接。参数说明如果 CCD 采样比控制周期快标志位会被覆盖这是可以接受的控制只用最新值如果比控制周期慢舵机在两次更新之间保持上一次输出这要求舵机的响应速度跟得上否则会出现阶梯式的转向手感。偏差量化到 1 像素时相邻两行的差可能全是量化噪声喂给 D 项之前可以用一个 0.7 的保持系数做一阶滤波。5. 弯道预判与实车调试线性CCD小车的进阶技巧5.1 前瞻权重曲线的取法标准加权质心把近处和远处一视同仁等效于把视线放在车头正下方结果就是入弯晚、出弯急。改进办法是在权重上乘一条距离曲线远端权重大、近端权重小把等效视线前移。像素区间相对车头建议权重作用0 到 31远前瞻1.6提前感知弯道减少入弯滞后32 到 95中1.0兼顾直线循迹精度96 到 127近车头0.5抑制远端噪声对近端判断的干扰/* 前瞻权重远端 1.6中间 1.0近端 0.5以整数放大十倍参与运算 */ static uint16_t ccd_weight_x(uint8_t i) { if (i 32) return 16; if (i 96) return 10; return 5; }把权重乘进质心公式后分母要保持一致否则除法的量纲会乱sum_w (uint32_t)w * ccd_weight_x(i); sum_wx (uint32_t)w * ccd_weight_x(i) * i;参数说明如果丢线门限原来按 sum_w 的绝对值定加了权重之后必须重新标定否则门限会整体偏小正常行被误判成丢线。权重比例也不是越大越好远端权重超过 2.0 之后镜头畸变和远端灰度衰减会开始主导结果车会在直线末端提前打舵。注意像素 0 到底在远端还是近端取决于镜头的安装朝向。上车前拿一张白纸挡住视野的一半看哪一头先变黑把权重曲线和实际朝向对齐很多前瞻不起作用的问题都是这里搞反了。5.2 用串口波形把中线提取过程摊开看调车最有效的手段是把中间量打出来看波形。gray 数组、阈值、偏差三个量画在同一张时间轴上丢线、阈值误判、舵机滞后一眼就能分辨。麻烦的是带宽115200 波特率下一行 128 字节要 11ms远超 5ms 的控制周期直接全量打印会把主循环拖垮。常见做法是压缩后再发每四个像素取最大值压成 32 个字节加上帧头和关键状态一帧 37 字节占用约 3.2ms放在空闲时发。/* 每帧0xAA 0x55 thresh err(2B) 32 个压缩像素 */ void ccd_send_frame(const uint8_t *gray, uint8_t th, int16_t err) { uint8_t i, buf[37], *p buf; *p 0xAA; *p 0x55; *p th; *p (uint8_t)(err 8); *p (uint8_t)(err 0xFF); for (i 0; i 128; i 4) { uint8_t m gray[i]; if (gray[i 1] m) m gray[i 1]; if (gray[i 2] m) m gray[i 2]; if (gray[i 3] m) m gray[i 3]; *p m; } uart_send_dma(buf, 37); /* DMA 发送不阻塞控制中断 */ }逻辑说明帧头 0xAA 0x55 用来做上位机的帧同步阈值和偏差放在像素数据前面画波形时可以在同一时间轴上对齐。参数说明压缩取样用最大值而不是均值是因为黑线在压缩后的小值特征必须保住均值会把细线抹平发送走 DMA 而不是阻塞式写寄存器否则 3.2ms 的发送时间会直接吃掉一次控制中断的余量。波形对上之后重点看两件事一是 err 曲线在过弯时有没有出现平台或者阶跃出现平台通常说明质心的有效像素数掉到了丢线门限附近二是把 err 和舵机 PWM 值画在一起如果 err 已经回正而 PWM 还在往反方向走说明 D 项在拖后腿砍掉三成再看。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价