4、图像采集与预处理移植(上):CIS驱动时序适配、DMA传输配置、图像数据格式转换(RGB565转灰度图)
各位工程师,大家好。欢迎来到第四章。
这一章,我们开始真正碰硬件了。图像采集,说白了就是让CIS(接触式图像传感器)老老实实地把钞票的影像“吐”出来。这一步要是没做好,后面算法再牛也白搭。我个人习惯把这一章分成三块:CIS的时序怎么伺候好、DMA怎么把数据高效搬走、以及拿到RGB565数据后怎么转成灰度图。咱们一个一个来。
4.1 CIS驱动时序适配:让传感器“听话”
CIS这玩意儿,说简单也简单,说复杂也复杂。它本质上就是一排感光单元,外加一个模拟前端。你需要给它提供正确的时钟、行同步信号,它才会乖乖地输出一行一行的像素数据。
核心信号就那么几个:
- CLK(主时钟):CIS的“心跳”。频率决定了每行像素的读出速度。我见过不少新手上来就把CLK拉到最高,结果图像全是噪点。为什么?因为模拟前端跟不上。
- SI(行起始信号):告诉CIS“开始新的一行”。这个脉冲的宽度和时序位置,每个厂家、每个型号都有细微差别。
- SP(采样时钟):在SI之后,每个CLK周期输出一个像素。SP的边沿要对齐像素数据的有效窗口。
- VOUT(模拟视频输出):这是CIS输出的模拟信号,需要经过ADC转换成数字量。
嗯,这里要注意。不同厂家的CIS,比如Toshiba、Hamamatsu、或者国产的一些品牌,它们的时序参数表(Datasheet)里,对SI的建立时间、保持时间要求完全不同。我曾经在一个项目中,因为SI的脉冲宽度差了0.1微秒,导致整张图像出现了周期性的条纹。排查了整整两天,最后用示波器一量,才发现是时序没对齐。
我建议的适配步骤:
- 读Datasheet:找到CIS的时序图,把CLK频率、SI脉宽、SP与SI的关系画出来。
- 配置MCU的定时器:用定时器的PWM或比较输出模式,精确生成SI和SP信号。别用软件延时去模拟,精度不够。
- 示波器验证:把SI和SP接到示波器上,调整定时器的参数,直到波形和Datasheet上的图一模一样。
- 看输出:接上ADC,看看VOUT引脚上有没有稳定的模拟波形。如果没有,回头检查供电和复位。
4.2 DMA传输配置:让CPU“歇着”
CIS一旦开始工作,数据是源源不断的。以一张A4大小的钞票为例,假设CIS分辨率是200DPI,一行就有约1700个像素。如果每个像素是16位(RGB565),一行就是3400字节。如果每秒采集100行,那就是340KB/s的数据量。
如果让CPU一个字节一个字节地去读ADC的结果,那CPU就别干别的了。所以,必须用DMA(直接存储器访问)。
DMA配置的几个关键点:
- 源地址:ADC的数据寄存器地址。注意,有些ADC是16位的,有些是8位的。要配置对数据宽度。
- 目的地址:内存中的缓冲区地址。我习惯用双缓冲(ping-pong buffer)。一个缓冲区在接收数据,另一个缓冲区让CPU处理。这样流水线作业,效率最高。
- 传输长度:一行像素的字节数。每次CIS输出一行,DMA就传输一次。传输完成后触发中断。
- 触发源:DMA的传输由谁触发?通常是ADC的转换完成事件。ADC每转换完一个像素,就触发DMA搬运一次。
一个典型的DMA配置代码片段(伪代码):
// 假设ADC数据寄存器地址为 ADC1_DR
// 缓冲区为 uint16_t buffer[LINE_WIDTH]
DMA_InitTypeDef DMA_InitStruct;
// 使能DMA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 配置DMA通道
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)buffer;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStruct.DMA_BufferSize = LINE_WIDTH;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不变
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 16位
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal; // 每次传输一行,用循环模式也行
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel1, &DMA_InitStruct);
// 使能DMA传输完成中断
DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);
// 使能DMA通道
DMA_Cmd(DMA1_Channel1, ENABLE);
你想想看,配置好DMA之后,CPU只需要在中断里处理一帧完整的数据,中间的过程完全不用管。这才是嵌入式开发的优雅之处。
4.3 图像数据格式转换:RGB565转灰度图
CIS输出的原始数据,通常是RGB565格式。也就是每个像素用16位表示,高5位是红色,中间6位是绿色,低5位是蓝色。为什么是565?因为人眼对绿色最敏感,所以绿色多给1位。
但我们的识别算法,大部分是基于灰度图的。因为灰度图计算量小,而且对光照变化不那么敏感。所以,第一步就是把RGB565转成灰度图。
转换公式(标准加权法):
Gray = 0.299 * R + 0.587 * G + 0.114 * B
这个公式模拟了人眼对亮度的感知。但注意,在嵌入式平台上,浮点运算太慢了。我们要用整数运算来近似。
我常用的整数近似方法:
// 输入:RGB565像素值 (uint16_t)
// 输出:灰度值 (uint8_t)
uint8_t RGB565ToGray(uint16_t rgb565)
{
// 提取RGB分量
uint8_t r = (rgb565 >> 11) & 0x1F; // 5位
uint8_t g = (rgb565 >> 5) & 0x3F; // 6位
uint8_t b = rgb565 & 0x1F; // 5位
// 扩展到8位(左移高位补0)
r = (r << 3) | (r >> 2);
g = (g << 2) | (g >> 4);
b = (b << 3) | (b >> 2);
// 整数加权:Gray = (77*R + 150*G + 29*B) >> 8
// 系数来源:0.299*256 ≈ 76.5,取77;0.587*256 ≈ 150.3,取150;0.114*256 ≈ 29.2,取29
uint16_t gray = (77 * r + 150 * g + 29 * b) >> 8;
return (uint8_t)gray;
}
为什么不用平均值法(R+G+B)/3?
因为人眼对绿色更敏感。平均值法会把绿色和红色、蓝色等同对待,导致图像看起来“发灰”,对比度不够。你想想看,如果一张钞票上的红色图案和绿色图案亮度一样,平均值法就分不出来了。而加权法能保留更多的细节。
好了,这一章的内容就到这里。我们搞定了CIS的时序、DMA的配置、以及RGB565到灰度图的转换。下一章,我们会继续深入图像预处理,聊聊滤波和边缘增强。这些东西,在识别脏污、残损的钞票时特别有用。
记住,硬件驱动是地基。地基不稳,楼盖得再高也得塌。动手去调吧,别怕示波器上的波形乱,乱了才能学会怎么把它调顺。