资讯详情

STM32F103驱动TSL1401线性CCD:时序、ADC采样与上位机联调

📅 2026/10/19 16:45:40 | 华诺云谱 👁 阅读
STM32F103驱动TSL1401线性CCD:时序、ADC采样与上位机联调
简介这份资源面向使用STM32F103驱动TSL1401线性CCD模块的嵌入式开发者与智能车竞赛选手提供从底层驱动到上位机调试的完整参考方案。包内共437个文件以136个h头文件与115个c源文件为核心配合Keil、IAR等工程配置uvprojx、ewp、icf、cmd、上位机可执行程序与ini配置、6份pdf指导手册及若干批处理脚本压缩包约12.56MB。项目基于正点原子MINI板与逐飞科技红孩儿CCD搭建上位机采用拉普兰德线性CCD调试助手代码同样适用于任意STM32F103单片机针对红孩儿模块停产的情况资料中亦给出龙邱科技、平衡小车之家等替代选型思路。读者可据此快速完成CCD采集、二值化与边线提取的移植调试并借助上位机观察波形、定位阈值与曝光问题。目前已有2084人学习下载适合需要系统掌握线性CCD循迹方案的进阶学习者。1. 从一颗“老 CCD”说起STM32F103 驱动 TSL1401 到底能解决什么如果你手头有一块 TSL1401 线性 CCD又正好在用 STM32F103 做控制大概率会遇到一个尴尬局面模块本身不贵资料却散得到处都是时序图看得懂代码跑起来却全是噪声。这份STM32F103驱动TSL1401线性CCD模块.rar就是冲着这个场景来的——它把底层驱动、采样时序和上位机联调打包在一起省去你从零翻数据手册的时间。TSL1401 是 128 像素的线性 CCD输出是模拟量靠 CLK 和 SI 两个信号控制曝光与读出。STM32F103 没有专用 CCD 接口只能用 GPIO 加 ADC 配合定时器去“抠”时序。这套资源适合做循迹小车、简易光谱采集、边缘检测的开发者尤其是想用拉普兰德上位机看波形的朋友。它不解决高精度成像但能把“能不能读出稳定 128 个点”这件事讲清楚。2. 时序与硬件链路为什么 GPIO 模拟比想象中挑剔2.1 TSL1401 的读出逻辑与 STM32F103 的匹配点TSL1401 的读出分两步先给 SI 一个高电平脉冲把像素电荷转移到采样电容然后在 CLK 的每个上升沿输出一个模拟电压顺序是像素 1 到 128。关键参数是 CLK 频率手册标称典型 1 MHz但实际用 STM32F103 的 GPIO 翻转能稳定跑到 500 kHz 已经不错。原因在于 GPIO 翻转有函数调用开销中断一多就抖。我一般会把 CLK 挂在 TIM 的 PWM 输出上用硬件产生方波SI 用另一个定时器通道或者 GPIO 在精确时刻拉高。这样 CPU 只负责在 CLK 下降沿后触发 ADC 采样。STM32F103 的 ADC 采样时间要设得足够长因为 TSL1401 的输出阻抗不低采样保持电容需要充电时间。常见做法是 ADC 时钟 14 MHz采样周期设 55.5 个周期对应约 4 微秒刚好在 500 kHz 的半个周期内完成。曝光时间由 SI 脉冲之间的间隔决定也就是两次读出的时间差。间隔越长输出幅值越大但容易饱和。这个参数没有固定值得根据你的光源和镜头调。资源里应该带了可调的宏定义改一个数就能试。2.2 硬件连接与最小系统搭建接线本身不复杂但电源和地没处理好波形就是一团毛刺。TSL1401 的 AO 输出是 0 到 VDD 的模拟电压STM32F103 的 ADC 参考电压也是 VDD所以直接连就行。但要注意CCD 的 CLK 和 SI 是数字信号走线尽量短远离电机驱动和电源电感。我习惯用杜邦线先搭一遍确认能读出数据再画板。供电用 3.3VSTM32F103 的 ADC 在 3.3V 下满量程是 4095。如果你用 5V 给 CCD 供电AO 输出可能超过 3.3V必须分压或者用 3.3V 供电。TSL1401 支持 3V 到 5V所以统一 3.3V 最省事。下面是一个最小连接的参考表TSL1401 引脚STM32F103 引脚说明VDD3.3V电源正GNDGND共地SIPA1定时器通道或 GPIOCLKPA0TIM2_CH1 或 GPIOAOPA2ADC_IN2注意如果你用 TIM2 的 PWM 输出 CLKPA0 要配置为复用推挽SI 如果也用定时器注意两个通道的相位关系。2.3 用定时器产生 CLK 和 SI 的代码骨架直接上代码这是初始化 TIM2 产生 CLK 的部分SI 用 TIM3 做单脉冲。代码基于标准外设库HAL 库逻辑类似改一下函数名就行。// 系统时钟 72MHzTIM2 产生 500kHz 方波 void TIM2_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA0 复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 72MHz / (711) 1MHz 计数频率 TIM_TimeBaseStructure.TIM_Period 1; // 自动重装载2 分频得 500kHz TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // PWM 模式 1占空比 50% TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 1; // 比较值 1周期 2占空比 50% TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_Cmd(TIM2, ENABLE); }这段代码把 TIM2 的计数频率设成 1 MHz自动重装载值设为 1所以 PWM 周期是 2 个计数输出 500 kHz。比较值设为 1占空比 50%。PA0 自动输出方波不需要 CPU 干预。SI 信号我一般用 TIM3 的单脉冲模式在每次读出前产生一个高电平脉冲宽度大约 1 到 2 微秒。具体代码资源里有这里不重复贴。逻辑说明CLK 由硬件产生CPU 只需要在 CLK 下降沿后启动 ADC。可以用 TIM2 的更新中断或者触发 ADC 的 EXTI 来实现。参数上Prescaler 和 Period 决定了 CLK 频率改这两个值就能调。如果发现波形失真先检查 GPIO 速度设置50MHz 是必须的。3. ADC 采样与 DMA 搬运把 128 个点稳稳收进数组3.1 ADC 触发源选择与采样时间计算STM32F103 的 ADC 支持外部触发可以用 TIM2 的 CC1 事件触发。但 TSL1401 的输出是在 CLK 上升沿后有效所以最好在 CLK 下降沿触发 ADC这样采样点落在像素电压稳定区。我一般用 TIM2 的 CC2 通道设置成 PWM 模式但反向或者直接用软件在中断里启动 ADC。软件触发简单但 128 个点逐个启动中断开销大500 kHz 下 CPU 基本被占满。更稳的方案是 ADC 连续扫描加 DMA。把 ADC 设成由 TIM2 的 TRGO 触发每次触发转换一个通道DMA 自动把数据搬到数组。但 TSL1401 只有一个模拟输出所以 ADC 只采一个通道DMA 传输 128 次。这里有个细节ADC 的规则组只有一个通道但需要转换 128 次所以要用 DMA 的循环模式传输长度设 128。采样时间前面说了55.5 周期在 14 MHz ADC 时钟下约 4 微秒。但如果你把 CLK 降到 250 kHz半周期是 2 微秒采样时间就得缩短到 1.5 微秒对应 28.5 周期。这个换算关系是采样时间 (采样周期数 12.5) / ADC 时钟。12.5 是逐次逼近的固定开销。3.2 DMA 配置与双缓冲思路DMA 配置不复杂但要注意 ADC 的 DMA 请求在每次转换结束后产生如果 DMA 传输完成中断里不及时处理下一轮数据会覆盖。我一般用双缓冲DMA 目标地址设成两个数组传输完成一半和全部完成时分别触发中断在中断里标记缓冲区就绪。这样主循环处理上一帧数据时DMA 已经在填下一帧。// ADC DMA 初始化采样 128 个点 #define CCD_PIXELS 128 uint16_t ccd_buf[2][CCD_PIXELS]; // 双缓冲 void ADC_DMA_Config(void) { ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // PA2 模拟输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)ccd_buf[0]; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize CCD_PIXELS; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; // 循环模式自动重填 DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, DMA_InitStructure); DMA_ITConfig(DMA1_Channel1, DMA_IT_TC | DMA_IT_HT, ENABLE); // 传输完成和半完成中断 DMA_Cmd(DMA1_Channel1, ENABLE); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; // 单通道 ADC_InitStructure.ADC_ContinuousConvMode DISABLE; // 由外部触发 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_CC2; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 1, ADC_SampleTime_55Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_ExternalTrigConvCmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); // ADC 校准 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); }这段代码把 ADC1 的通道 2 配置成外部触发触发源是 TIM2 的 CC2 事件。DMA 循环模式缓冲区大小 128半完成和完成中断都打开。注意ADC_ExternalTrigConv_T2_CC2这个触发源要求 TIM2 的 CC2 通道配置成比较输出但不需要实际引脚输出只要在 TIM2 里设置好比较值就行。我一般把 CC2 的比较值设成 1这样它在每个周期都会产生事件。参数说明ADC_SampleTime_55Cycles5是采样时间改小可以适应更高的 CLK 频率但太小会导致采样不足读数偏低。DMA_Mode_Circular让 DMA 自动重装不用每次手动重启。双缓冲数组ccd_buf[2][128]在中断里切换主循环读另一块。3.3 中断服务函数与数据就绪标志DMA 中断里只做一件事切换缓冲区索引置位标志。不要在中断里做滤波或串口发送否则会丢数据。volatile uint8_t ccd_ready 0; volatile uint8_t ccd_idx 0; void DMA1_Channel1_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC1)) { DMA_ClearITPendingBit(DMA1_IT_TC1); ccd_idx ^ 1; // 切换到另一个缓冲区 ccd_ready 1; } if (DMA_GetITStatus(DMA1_IT_HT1)) { DMA_ClearITPendingBit(DMA1_IT_HT1); // 半完成也可以用来做流水线这里不用 } }主循环里检查ccd_ready然后处理ccd_buf[ccd_idx ^ 1]的数据。注意ccd_idx在中断里翻转主循环读的是另一块不会冲突。这个结构在 500 kHz 下跑CPU 占用率不到 20%剩下的时间可以做滤波和上位机通信。提示DMA 传输完成中断里不要调用printf或浮点运算STM32F103 没有 FPU浮点会拖慢中断响应。4. 上位机联调与波形显示拉普兰德怎么用才不翻车4.1 串口协议与数据帧格式拉普兰德上位机这里指常见的串口波形显示工具一般支持两种模式文本模式和二进制模式。文本模式发 ASCII 码方便调试但速度慢二进制模式发原始字节速度快但需要上位机解析。128 个点用文本发每个点 4 个字符加分隔符一帧大约 600 字节115200 波特率下要 50 毫秒根本跟不上 500 kHz 的采样率。所以必须用二进制。我一般把 12 位 ADC 值拆成两个字节高字节在前低字节在后帧头加两个 0xAA 0x55帧尾加校验和。上位机那边设置成二进制模式数据位 8停止位 1无校验。拉普兰德的具体配置界面因版本而异但核心是波特率要匹配STM32 这边用 115200 或者 256000 都行看你的 USB 转串口芯片支持到多少。// 发送一帧 CCD 数据二进制格式 void CCD_SendFrame(uint16_t *buf) { uint8_t header[2] {0xAA, 0x55}; uint8_t tail 0x0D; uint8_t checksum 0; uint8_t i; UART_SendBytes(header, 2); for (i 0; i CCD_PIXELS; i) { uint8_t high (buf[i] 8) 0xFF; uint8_t low buf[i] 0xFF; UART_SendByte(high); UART_SendByte(low); checksum high low; } UART_SendByte(checksum); UART_SendByte(tail); }这段代码把 128 个 16 位数据拆成 256 个字节发送加上帧头、校验和和帧尾。上位机收到后按同样格式解析。校验和是简单的累加够用。如果你发现上位机波形跳动先检查波特率是否准确STM32F103 的串口在 72MHz 下 115200 的误差大约 0.2%可以接受。4.2 曝光时间与波形幅值的调参经验TSL1401 的输出幅值和曝光时间成正比但曝光时间太长会饱和太短则信噪比差。我一般先设一个中间值比如 1 毫秒然后看波形。如果全是 4095说明饱和了把曝光时间减半如果峰值只有几百就加倍。这个调整在代码里就是一个延时或者定时器周期改起来很快。另一个坑是环境光。TSL1401 对红外也敏感如果你用红外光源波形会偏。最好加一个可见光滤光片或者用调制光源。资源里可能带了简单的阈值二值化代码但阈值不是固定的得根据现场光强调。我习惯在初始化时采一帧全黑和全亮算一个动态阈值。4.3 常见波形异常与排查顺序波形异常无非几种全是零、全是满量程、有规律毛刺、随机跳动。排查顺序我总结成一张表现象可能原因解决全是 0SI 没脉冲或 CLK 没输出用示波器看 PA0 和 PA1确认波形全是 4095曝光太长或 AO 短路到 VDD缩短曝光检查焊接有规律毛刺电源纹波或 CLK 串扰加 100nF 去耦CLK 线远离 AO随机跳动地线环路或 ADC 采样时间太短单点接地增加采样周期注意用示波器看 AO 信号时探头地线要尽量短否则会引入额外噪声让你误判。5. 避坑与排查那些让我熬夜的 CCD 驱动问题5.1 SI 脉冲宽度不够导致首像素丢失现象读出的 128 个点里第一个点总是接近 0后面的正常。原因SI 脉冲太窄电荷转移不完整。TSL1401 要求 SI 高电平至少 1 微秒我一开始用 GPIO 直接拉高拉低编译器优化后只有几百纳秒。解决用定时器产生 SI或者加__NOP()延时。后来我统一用 TIM3 的单脉冲模式宽度设成 2 微秒问题消失。5.2 CLK 频率过高导致 ADC 采样跟不上现象波形整体偏小且随 CLK 频率升高而减小。原因ADC 采样时间固定CLK 快了之后采样窗口落在电压建立之前。解决降低 CLK 到 250 kHz或者缩短 ADC 采样周期。但缩短采样周期会增加源阻抗影响所以优先降 CLK。我一般把 CLK 设在 200 到 500 kHz 之间看具体需求。5.3 电源噪声让暗电流漂移现象没有光照时读数在 100 到 300 之间跳。原因TSL1401 的模拟输出对电源敏感尤其是 3.3V 稳压器质量一般时。解决在 CCD 的 VDD 引脚旁加 10uF 钽电容和 100nF 陶瓷电容模拟地和数字地单点连接。如果还跳用线性稳压器单独给 CCD 供电。这个坑我踩过两次后来每次画板都强制加这两个电容。5.4 DMA 缓冲区覆盖导致数据错位现象上位机波形偶尔出现半帧正常半帧乱。原因DMA 循环模式下主循环还没处理完DMA 已经填了下一轮。解决用双缓冲并且在 DMA 完成中断里切换。如果不用双缓冲至少把 DMA 设成正常模式每次手动重启但这样会丢时间。双缓冲是标准做法代码在前面已经给了。5.5 串口发送阻塞导致采样丢帧现象波形更新慢或者偶尔卡住。原因在主循环里用阻塞方式发串口115200 下发 600 字节要 50 毫秒这期间 DMA 可能已经覆盖了缓冲区。解决用串口 DMA 发送或者降低发送频率每两帧发一次。我一般用串口 DMA把发送也交给硬件CPU 只负责打包。6. 进阶技巧用中值滤波和动态阈值把波形“洗干净”6.1 中值滤波的快速实现CCD 数据常见的噪声是椒盐噪声中值滤波比均值滤波更有效。但 128 个点做 3 点中值计算量不大可以直接在中断外做。我一般用滑动窗口每次取三个连续点排序后取中间值。代码很简单// 3 点中值滤波输入输出都是 128 点数组 void MedianFilter3(uint16_t *in, uint16_t *out, uint8_t len) { uint8_t i; out[0] in[0]; out[len - 1] in[len - 1]; for (i 1; i len - 1; i) { uint16_t a in[i - 1], b in[i], c in[i 1]; uint16_t t; // 三个数排序取中 if (a b) { t a; a b; b t; } if (b c) { t b; b c; c t; } if (a b) { t a; a b; b t; } out[i] b; } }这个函数没有用除法全是比较和交换在 STM32F103 上跑 128 点大约几十微秒完全跟得上。参数len是数组长度固定 128。注意输入输出不能是同一个数组否则会覆盖未处理的数据。6.2 动态阈值二值化与循迹应用如果你做循迹小车最终需要的是黑线位置不是原始波形。动态阈值比固定阈值靠谱因为环境光会变。我一般取一帧数据的最大值和最小值阈值设成(max min) / 2然后找低于阈值的连续区间取中心。这个方法在白色赛道和黑色线之间很稳。但要注意如果视野里全是白色max 和 min 会很接近阈值就没意义。所以加一个判断如果max - min 200认为没有黑线返回上一次的位置。这个 200 是经验值根据你的 ADC 满量程 4095 来调。6.3 验证方法用已知图案反推时序调完代码后怎么确认时序真的对了我一般拿一张黑白条纹纸放在 CCD 前面看波形是不是方波。如果边缘陡峭说明采样点对齐了如果边缘圆滑说明采样时间太长或者 CLK 太快。另一个方法是遮住一半镜头波形应该一半高一半低分界点对应像素位置。这个验证比看数据手册直观。从那以后我每次调 CCD 都强制走一遍先看电源纹波再看 SI 和 CLK 波形最后看 AO 输出。这三步过了基本不会翻车。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑