十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TSL1401线性CCD智能车:MC9S12XS128阈值分割与PD/PID

TSL1401线性CCD智能车:MC9S12XS128阈值分割与PD/PID 简介面向智能车竞赛与嵌入式控制方向的一份技术论文型PDF围绕基于线性CCD的智能小车控制系统设计展开适合参加NXP杯等智能汽车竞赛的学生、负责循迹小车项目的开发者以及需要参考赛道识别与控制算法实现思路的读者。文档以MC9S12XS128单片机为控制核心系统梳理硬件与软件两大部分硬件涉及电源、TSL1401线性CCD图像采集、$3010舵机转向、欧姆龙编码器测速和MOS管双电机驱动等模块软件则讲解固定阈值与跳变沿边缘提取、分段PD转向控制以及增量式PID速度闭环等方法并给出电路组成与算法流程。资源包仅含1个PDF文件约139KB轻量便于快速查阅与打印。目前已有122人学习可作为竞赛方案设计、课程设计或智能系统开发的参考文献帮助读者把握从传感器采集到执行控制的完整链路与调参要点。1. 一条黑线、128 个像素为什么阈值比分辨率更重要跑过 NXP 杯的人大多有过这样的体验拿摄像头方案调了半个月图像处理代码越写越厚车反而越跑越飘换成线性 CCD 之后代码量砍掉一大半圈速却上来了。TSL1401 这种 128×1 的线性 CCD 只输出单行灰度值没有形状、没有坐标信息量比起摄像头少得可怜但它的前瞻能做到一米开外帧率轻松拉到几百赫兹几乎不占 CPU 时间。真正决定成败的不是传感器本身的像素数而是你从这 128 个数字里怎么把车现在偏了多少抠出来。这份以 MC9S12XS128 为核心、在白色赛道上识别两侧黑色引导线的设计用的是一条很朴素的路线固定阈值分割、跳变沿搜索边界、非线性位置式 PD 打方向、增量式 PID 控速度。它适合做智能车、循迹小车以及任何需要低成本、高实时性视觉反馈的控制类项目。下面按硬件链路、图像处理、控制算法、实车调试的顺序拆开讲。2. MC9S12XS128 与 TSL1401 的硬件链路搭建与信号隔离智能小车的硬件问题八成会在软件上以数据莫名其妙抖动的形式出现。CCD 灰度值整行漂移、编码器脉冲偶尔多计一个、舵机在电机换向瞬间抽一下这些现象追到根上基本都是电源和地的问题而不是算法问题。所以在写第一行图像处理代码之前先把电源域和信号链理顺。2.1 电源域划分5V、6V、7.2V 各走各的小车用 7.2V 电池组供电但车上其实有三个电压域混在一起就是灾难。单片机、CCD、编码器要 5V舵机要 6V电机驱动直接吃 7.2V。供电对象电压稳压器件关键注意点单片机、CCD、编码器5VLAS1117M3 滤波电容模拟地与数字地单点汇接舵机转向模块6VAS1117M3-Adj堵转电流大单独走粗线电机驱动模块7.2V电池直供回流路径不要穿过 5V 地平面常见做法是把舵机和电机的地回流单独拉一条线回到电池负极在靠近电池的位置和单片机地单点汇接而不是让大电流从 CCD 的地线上过。舵机堵转那一瞬间的电流突变会在共用地线上产生几十毫伏的压降如果 CCD 的模拟地正好落在这段上采样出来的 128 个灰度值会整行跳几个 LSB阈值判据立刻失效。2.2 TSL1401 的 SI/CLK 时序与曝光时间标定TSL1401 内部是 128 个光电二极管加电荷放大电路还有一个像素数据保持功能。它对外的接口极其简单一个串行输入 SI一个时钟 CLK一个模拟输出 AO。SI 配合第一个 CLK 把积分电容复位之后每来一个时钟上升沿就把下一个像素的电压推到 AO 上。// TSL1401 采集: 128 像素, SI 启动积分, CLK 逐个读出 #define EXPOSURE_US 800 // 积分时间, 强光场地 500~1000us 之间调 #define PIX_NUM 128 uint8_t pixel[PIX_NUM]; void ccd_capture(void) { uint8_t i; SI 1; // 拉高 SI, 配合第一个 CLK 复位积分电容 CLK 1; SI 0; // SI 只保持一个时钟周期的高电平 CLK 0; delay_us(EXPOSURE_US); // 积分时间, 直接决定整行灰度的基线 for (i 0; i PIX_NUM; i) { CLK 1; // 上升沿把第 i 个像素推到 AO delay_us(1); // 等 AO 建立稳定 pixel[i] adc_read(CH0); // CLK 高电平期间采样 CLK 0; } }逻辑上SI与第一个CLK的配合决定了积分起点之后CLK的个数就是像素个数多打一个时钟就多读一个无效数据少打一个就少读一列。EXPOSURE_US是最需要现场标定的参数曝光越长整行数值越大。这份设计的场地里白色路面灰度稳定在 200 上下、黑色引导线稳定在 70 左右这两个数就是判断曝光是否合适的标尺——如果白路面采样值只有 120说明曝光太短或者环境光不足往上加如果白路面直接饱和到 255说明曝光过头往下减。adc_read()的采样点必须落在CLK高电平期间落在低电平区间会读到上一列残留的电荷。2.3 电机驱动、74LVC245 隔离与编码器测速链路电机驱动用 MOS 管搭双电机 H 桥单片机输出四路 PWM 控制正反转。这里有一个容易被省掉但很值钱的器件74LVC245 总线驱动芯片。它干了三件事——提高单片机 IO 的驱动能力让 MOS 栅极能被快速充放电把 MOS 与单片机隔开万一 MOS 击穿电池电压会先打在 245 上而不是直接灌进单片机引脚烧芯片。省掉这颗芯片一次 MOS 损坏就可能带走整块最小系统板。PWM 频率的选择也有讲究。舵机是 50Hz 的标准信号周期 20ms靠 0.9~2.1ms 的高电平宽度对应转角电机 PWM 至少要在 10kHz 以上落在人耳不敏感的区域否则电机全程啸叫。// 舵机 50Hz(周期 20ms), 电机 10kHz(周期 100us) void pwm_init(void) { PWME 0x00; // 配置寄存器前先关通道 PWMPRCLK 0x03; // 时钟预分频, 决定 PWMPER 的单位 PWMPOL 0xFF; // 高电平有效 PWMCAE 0x00; // 左对齐输出 PWMPER0 200; // 舵机通道: 周期计数, 对应 20ms PWMDTY0 15; // 1.5ms 高电平 - 舵机中值 PWMPER1 100; // 电机通道: 周期计数, 对应 100us PWMDTY1 0; // 初始占空比 0, 上电不冲车 PWME 0x07; // 使能通道 0/1/2 }占空比等于PWMDTY/PWMPER所以PWMDTY0 15得到的就是 1.5ms 中值。上电初始把电机占空比写成 0是一条用血换来的经验很多车第一次上电就窜出去撞墙就是因为 PWM 寄存器复位值不确定。测速用欧姆龙编码器输出轴齿轮和电机齿轮啮合转速与电机转速严格成正比。读取方式用 DMA 计数让定时器在固定周期比如 10ms自动搬运脉冲计数值CPU 完全不参与省下来的时间全部留给图像处理。3. 128 像素灰度图像的固定阈值分割与跳变沿边界提取图像处理这一层的输入是一行 128 个字节输出是一个数车相对于赛道中线的偏差。中间所有动作都要在这个约束下完成——不能有大数组、不能有浮点、不能有几十毫秒的耗时。3.1 为什么用固定阈值而不是动态阈值看到光照会变很多人的第一反应是上动态阈值每帧统计一次灰度分布取个中间值当门限甚至上大津法。在室内比赛场地里这个思路往往是负优化。场地光照均匀且强度高白路面和黑线的灰度分布极其稳定而动态阈值的门限是每帧单独算的一旦某一帧出现一小块高亮反光或者镜头边缘的暗角门限就被这几个畸点带偏本来该判白的路面被判成黑边界瞬间错位十几列。下一帧门限又回正舵机就跟着抽一下。固定阈值把边界判断的稳定性从每帧自适应变成了整场恒定代价是换场地要重新标定。这个代价完全值得因为标定只需要在上电前看一眼 128 点波形。更进一步的用法是准备多个固定阈值针对直道、起跑线、障碍、十字这些不同赛道元素各用一套判定门限让小车的识别在不同元素前都有明确的反应而不是靠一条门限硬扛所有情况。3.2 跳变沿法只在上一帧中心附近找边沿有了阈值接下来是从 128 个值里找黑线的左右边界。最直接的做法是全行扫描找到所有白到黑的下降沿和黑到白的上升沿。这个方法在十字路口会彻底失效——十字中间那条垂直的黑色引导线会在图像里贡献出额外的两个边沿全行扫描分不清哪个是自己要跟的那条线。解决办法是限制搜索范围。上一次引线中心在哪下一次就在那个位置附近找窗口取正负 40 个像素。十字的垂直黑线距离本车引导线通常在这个窗口之外天然被滤掉引线外围偶然出现的黑色噪点也进不了窗口。#define PIX_NUM 128 #define CENTER 63.5f // 128 像素的几何中线 #define WIN 40 // 搜索窗口半宽 int16_t last_center 63; // 上一帧引线中心, 初值取中线 uint8_t last_valid 0; /* 从上一帧中心附近向左/右搜索跳变沿, 返回本帧引线中心 */ int16_t trace_edge(const uint8_t *px, uint8_t th) { int16_t i, left -1, right -1; int16_t lstart last_center - WIN; int16_t rstart last_center WIN; if (lstart 0) lstart 0; if (rstart PIX_NUM - 1) rstart PIX_NUM - 1; /* 向左: 白 - 黑的下降沿, 即左边线 */ for (i lstart; i 0; i--) { if (px[i - 1] th px[i] th) { left i; break; } } /* 向右: 黑 - 白的上升沿, 即右边线 */ for (i rstart; i PIX_NUM - 1; i) { if (px[i] th px[i 1] th) { right i; break; } } if (left 0 right 0 right left) { last_center (left right) 1; // 两边都找到才更新中心 last_valid 1; } /* 边沿丢失(十字/盲区)时不更新, 沿用上次中心保持直行 */ return last_center; }WIN是整个算法最敏感的参数。窗口太窄急弯时黑线跑出搜索范围算法直接丢线窗口太宽十字的垂直线会进入视野被误判成边界。40 这个值是在前瞻约一米、弯道最小曲率半径已知的前提下试出来的实际调车时可以从 30 开始往大放放到十字开始误判就退回来两格。丢边沿时的处理同样关键left_valid标记的作用是让算法在找不到边沿时沿用上一次的中心而不是返回 -1 让上层去做异常处理——十字路口本来就该直行通过。3.3 偏差量计算与赛道元素判定拿到左右边界后取平均就是当前引线的中心位置与 63.5 相减得到偏差量err。这个数同时被两个环使用给舵机做转向输入给速度环做赛道类型判断。赛道元素判定依据建议阈值处理策略直道连续若干帧 |err| 小于 8150目标速度拉满弯道|err| 持续单向增大150降速, 切远场 PD 系数十字左右边沿同时丢失超过 3 帧170沿用上次中心, 直行不改向起跑线全行黑像素占比超过 0.8110维持当前速度, 标记一圈障碍边沿位置出现固定宽度缺口的跳变170按缺口方向绕行阈值列的数字就是判黑的门限不同元素用不同门限的原因在于 CCD 对黑线的响应会随距离变化近距离黑线压得很低70 左右远距离因为积分和散射黑线灰度会抬到 110 甚至 130用同一个 150 去判远端边界会提前被切掉转弯时就会觉得车提前反应了。把远端的门限单独抬高等于给前瞻做了一次距离补偿。4. 非线性位置式 PD 转向与增量式 PID 速度环整定控制这一层是整个系统里最需要反复试的部分。图像处理给出一个偏差值控制算法要把它变成一个舵机 PWM 和一个电机 PWM而这两个输出之间还会互相影响车转得急了速度掉得快速度掉下来转向响应又变慢。4.1 非线性位置式 PD为什么偏差要平方标准的位置式 PD 是out center kp*err kd*(err - err_last)。它的毛病在于比例项是线性的直道上偏差本身很小可能只有一两个像素但传感器噪声也有一个像素kp 一大舵机就抖kp 一小弯道又打不动。直道平稳和弯道灵敏这两个需求用一组线性系数调不出来。这份设计用偏差的平方代替偏差本身形式上是out center kp*err²*sign(err) kd*(err - err_last)。偏差小的时候平方项小得几乎不起作用噪声被压掉偏差大的时候平方项迅速放大舵机大幅度打角。非线性的本质是把小偏差不敏感、大偏差敏感这个需求直接写进了算法形式里而不是靠调参去折中。#define STEER_MID 1500 // 舵机中值: 1.5ms 对应计数 #define STEER_MAX 2100 #define STEER_MIN 900 /* 分段非线性位置式 PD, err 单位为像素 */ int16_t steer_pd(int16_t err) { static int16_t err_last 0; int16_t kp, kd; int32_t out; /* 分段: 按偏差绝对值切换比例/微分系数 */ if (err 25 || err -25) { kp 45; kd 14; } // 离场: 大增益抢方向 else { kp 20; kd 6; } // 近场: 小增益保平稳 /* 平方项保留符号, 4 是缩放, 避免乘完溢出 */ out STEER_MID kp * (((int32_t)err * err) 4) * (err 0 ? 1 : -1) kd * (err - err_last); err_last err; if (out STEER_MAX) out STEER_MAX; // 舵机机械行程限幅 if (out STEER_MIN) out STEER_MIN; return (int16_t)out; }这段代码有两个坑必须说明。第一err * err一定要先转成int32_t再乘偏差最大能到 60 左右平方是 3600再乘 kp 之后远超 16 位范围用int16_t计算会静默溢出成负数表现出来就是弯道往反方向打。第二4这个缩放系数不是可有可无的修饰它决定了平方项的量纲能落进舵机行程范围里改 kp 之前先把这个缩放系数定下来否则 kp 的物理意义就乱了。中值 1500、上下限 900/2100 对应的是 0.9ms 到 2.1ms超出这个范围舵机会顶到机械限位长时间堵转烧舵机。4.2 分段 PD切换点要做滞回分段系数的思路是把偏差分成近场和远场两段近场用温柔系数保直道平稳远场用激进系数保弯道灵敏。落地时最容易犯的错是切换点没有滞回偏差在 25 附近上下浮动时系数在两组值之间来回跳舵机输出就会出现周期性的抖动。工作区偏差范围kpkd适用场景近场|err| 22206直道、微调过渡带22 ≤ |err| 28保持上一次保持上一次防抖不切换远场|err| ≥ 284514入弯、急弯过渡带这一行的意思是进入某个区用的是门槛值22 或 28退出某个区用的是另一个门槛22中间这 6 个像素的宽度就是滞回带。实现上用一个静态变量记住当前处于哪个区只有偏差越过对应的进出阈值时才改状态。4.3 增量式 PID 速度环只输出 PWM 增量速度环的输入是编码器脉冲数输出是电机 PWM。这里不用位置式 PID 而用增量式主要原因有两个。位置式 PID 需要累加积分项一旦目标速度从 2300 切到 1500入弯降速累加出来的积分项还在高位PWM 要过好几十毫秒才能降下来表现出来就是入弯刹不住。增量式 PID 每次只算 PWM 的增量天然不存在积分饱和切目标速度时输出立刻跟着走。#define TARGET_STRAIGHT 2300 // 编码器脉冲数/10ms, 对应约 2.3 m/s #define TARGET_CURVE 1500 // 弯道目标速度 /* 增量式 PID, 返回本次电机 PWM 值 */ int16_t speed_inc(int16_t target, int16_t actual) { static int16_t e0 0, e1 0, e2 0; static int16_t pwm 0; int16_t delta; e2 e1; e1 e0; // 偏差序列后移 e0 target - actual; // 本次速度偏差 delta 12 * (e0 - e1) // Kp: 比例增量 3 * e0 // Ki: 积分项, 消除稳态误差 1 * (e0 - 2 * e1 e2); // Kd: 微分项, 抑制超调 pwm delta; if (pwm 8000) pwm 8000; // 占空比上限, 保护 MOS 和电机 if (pwm 0) pwm 0; // 不允许反向, 换向交给方向引脚 return pwm; }三个系数的初始值我一般按 12/3/1 起步先把 Kp 单独加上去到速度能跟上但不振荡再加 Ki 把稳态误差压掉最后用 Kd 削超调。Ki 是唯一能把速度真正拉到目标值的项只有 Kp 的话稳态会差一个固定量因为摩擦和风阻需要固定的 PWM 去抵消。目标速度的切换由舵机调节量预判转向输出越偏离中值说明越接近弯道目标速度就越低这样车会在入弯前就开始减速而不是等偏差很大了才反应。5. 实车调试灰度波形、PWM 回读与分段验证调车最耗时间的不是写代码是分清一个抖动到底是图像问题、控制问题还是机械问题。有效的做法是把中间量全部吐出来看而不是只盯着车跑得直不直。5.1 把 128 点灰度值打到上位机在固件里开一个串口通道每帧把 128 个灰度值加一个帧头打出去波特率 115200占用时间可以接受。上位机用几十行 Python 就能画出实时波形。import serial import matplotlib.pyplot as plt ser serial.Serial(COM3, 115200, timeout1) # 波特率与固件一致 while True: line ser.readline().decode(ascii, ignore).strip() if not line.startswith($PIX): continue vals [int(v) for v in line[4:].split(,)] # 解析 128 个灰度值 if len(vals) ! 128: continue # 丢帧直接跳过 plt.cla() plt.plot(vals) plt.axhline(150, colorr, linestyle--) # 与固件 WHITE_TH 保持一致 plt.ylim(0, 255) plt.pause(0.01)axhline那条红线必须和固件里的阈值常量取同一个值否则你看到的边界位置和车实际算出来的不是一回事。波形上有三类故障一眼能认出来整行数值整体抬高或压低是曝光时间漂了或者环境光变了改EXPOSURE_US某几个点单独冒尖是地线干扰串进了 ADC 参考去查电机地和模拟地的汇接点黑线边沿在波形上是一条很宽的缓坡而不是陡降说明阈值正好卡在过渡带上把门限往上或往下挪十个 LSB 就干净了。5.2 按顺序验证别一步到位上电顺序错了出的问题会互相掩盖。我习惯分四步走第一步车架起来轮子不落地看 128 点波形是否稳定标定曝光和阈值第二步手动推着车沿赛道走看串口打印的err曲线是否和车的实际横向偏移同向、量级合理这一步能验证边界提取和符号方向第三步落地低速跑目标速度设到 800 左右只开转向环确认车能沿线走但不追求速度第四步再放速度同时把速度环的 Ki 从小往大加。验证转向极性有个不用跑车的办法把车放在赛道中线偏左的位置err应该是负的舵机输出应该小于中值 1500。如果符号相反问题要么在偏差计算要么在舵机安装方向先改软件符号别急着拆机械因为拆了之后图像这边的坐标系也跟着变会陷进来回改的循环。最后一章再给一个具体技巧每次改完参数把当次的kp/kd、TARGET_STRAIGHT、WHITE_TH三个值和当圈的实测数据一起记下来写在一张纸上贴在调车桌边。参数不是孤立的最优解它们是互相耦合的——提高目标速度往往要同步降低近场 kp抬高阈值往往要同步缩短曝光。没有这份记录改到第三天你就不知道自己昨天那套跑得不错的参数到底是什么了。本文还有配套的精品资源点击获取
返回列表