关于TCS34725颜色识别传感器的彩球分拣装置
TCS34725简介
本模块是基于AMS的TCS3472XFN彩色光数字转换器为核心的颜色传感器,传感器提供红色,绿色,蓝色(RGB)和清晰光感应值的数字输出。集成红外阻挡滤光片可最大限度地减少入射光的红外光谱成分,并可精确地进行颜色测量。具有高灵敏度,宽动态范围和红外阻隔滤波器。最小化IR和UV光谱分量效应,以产生准确的颜色测量。并且带有环境光强检测和可屏蔽中断。通过I2C接口通信。本设计使用的是双孔版本,如下图所示布局了2个LED灯对于物体进行补光。
TCS34725颜色识别传感器

本次使用双孔版本:

接口说明:

传感器原理框图

颜色传感器吸收物体反射回来的光由红光、绿光、蓝光和清晰(未滤光)光电二极管吸收,产生光电效应,并且产生光电流,四个积分ADC同时将放大的光电二极管电流转换为16位数字值。转换周期完成后,结果被传输到数据寄存器,数据寄存器采用双缓冲方式,以确保数据的完整性。
积分时间设置

RGBC定时寄存器以2.4ms的增量获得一组RGBC的数据,例如积分时间取24ms,即每次取10组数据,整个通道最大值就变为10倍,而实际取RGB(0-255)的算法为R*255/C,在整个算法下理论上积分时间越长数据越精确,但是转盘的旋转速度做不到太慢,太慢的PWM值下驱动不了转盘,即采集数据的速度较快,积分时间不宜过高。(本次使用24ms)
传感器增益相关

在上面提到的算法中若结果数值较小或者误差较大请调高增益,若数值过大请调低增益。(本次使用4x)
所测彩球

初版彩球分拣器

从图中可以看出初版的彩球分拣装置并没有在TCS34725传感器周围遮光,仅由下方的LED灯进行补光,周围光线对传感器的数据影响太大,数据波动严重,且坏数据占大部分,甚至能只用清晰光感应值分辨球该去哪一个盒子(球的不透明度不一样,下方的LED灯透过球反馈给传感器的数据不同),不提他本身偏离我们的目的——通过RGB数值判断球该去哪里,透光度受外部光线影响严重,不能满足我们此次制作的要求,于是就来到了第二版。
第二版彩球分拣器

第二版其实修改了很多,一方面是加了TCS34725传感器附近的遮光设备,另一方面改变了漏斗下方的搅拌轮的轮齿,先前的轮齿在搅拌的时候会有两颗小球因为尺寸的原因配合着他的旋转不掉下去。我们这个彩球分拣器原版是由白色的材料3D打印而成的,该材料有一定的透光性,会一定程度上干扰颜色识别,在第二版选择黑色的材料,打印的时候产生了一定的加工偏差,导致本该水平放于彩球上方的传感器产生倾斜,并且阻碍了下方圆盘的旋转。于是便决定重新制作各个部件,这便到了终版。
终版彩球分拣器

如图所示终版的彩球分拣装置排除了大部分的环境光线的影响,并且在运行期间的正确数据基本可控,到此外形设计部分基本完成。
接线
接线图

CH549 | 功能引脚 | 功能 |
P0.5 | TCS34725 SCL | 传感器I2C时钟输入 |
P0.6 | TCS34725 SDA | 传感器I2C数据输入 |
P1.4 | SSD1036 CS | OLED屏CS片选 |
P1.5 | SSD1036 D1 | OLED屏MOSI数据 |
P1.7 | SSD1036 D0 | OLED屏SCLK时钟信号 |
P2.2 | MG90S 黄线 | MG90S舵机PWM控制 |
P2.4 | MX1508 电机1+ | 转盘PWM控制 |
P2.7 | SSD1036 DC | OLED屏数据/命令控制 |
P3.5 | SSD1036 RST | OLED屏LED复位 |
PS:传感器下方补光的LED灯记得连电阻;其余VCC和GND不再给出;搅拌轮电机的PWM控制正极直接连VCC。
若使用USB 转 TTL 模块与串口调试助手则改变以下几个引脚接线
CH549引脚 | 串口引脚 |
P1.6 | TXD |
P1.7 | RXD |
GND | GND |
串口调试工具波特率为125000
模拟I2C
I2C通信协议简介
I2C(Inter Integrated Circuit)总线是PHILIPS公司开发的一种半双工、双向二线制同步串行总线。I2C总线传输数据时只需两根信号线,一根是双向数据线SDA(serial data),另一根是双向时钟线SCL(serial clock)。SPI总线有两根线分别用于主从设备之间接收数据和发送数据,而I2C总线只使用一根线进行数据收发。
I2C和SPI一样以主从的方式工作,不同于SPI一主多从的结构,它允许同时有多个主设备存在,每个连接到总线上的器件都有唯一的地址,主设备启动数据传输并产生时钟信号,从设备被主设备寻址,同一时刻只允许有一个主设备。
I2C总线在传送数据过程中共有三种类型信号:开始信号、结束信号和应答信号。

时序
开始信号:SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。
结束信号:SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。
应答信号:接收数据的IC在接收到8bit数据后,向发送数据的IC发出特定的低电平脉冲,表示已收到数据I2C写时序。
I2C写时序:

首先主机会发送一个开始信号,然后将其I2C的7位地址与写操作位组合成8位的数据发送给从机,从机接收到后会响应一个应答信号,主机此时将命令寄存器地址发送给从机,从机接收到发送响应信号,此时主机发送命令寄存的值,从机回应一个响应信号,直到主机发送一个停止信号,此次I2C写数据操作结束。
I2C读时序:

首先主机会发送一个开始信号,然后将其I2C的7位地址与写操作位组合成8位的数据发送给从机,从机接收到后会响应一个应答信号,主机此时将命令寄存器地址发送给从机,从机接收到发送响应信号,此时主机重新发送一个开始信号,并且将其7位地址和读操作位组合成8位的数据发送给从机,从机接收到信号后发送响应信号,再将其寄存器中的值发送给主机,主机端给予响应信号,直到主机端发送停止信号,此次通信结束。
I2C地址:I2C设备地址为0x29。
PS:0x29为7位的设备地址,8位设备地址需要向高位移一位为0x52。
CH549_IIC.h
#ifndef __CH549_IIC_H__
#define __CH549_IIC_H__
#include "CH549_sdcc.h"
#include "CH549_DEBUG.h"
#include "CH549_GPIO.h"
/****************** 参数定义 ******************/
#define SCL_UP (P0_5 = 1)
#define SCL_DOWN (P0_5 = 0)
#define SDA_UP (P0_6 = 1)
#define SDA_DOWN (P0_6 = 0)
#define SDA_READ P0_6
/****************** 外部调用子函数 ****************************/
extern void IIC_Init();
extern void IIC_Start();
extern void IIC_Stop();
extern UINT8 IIC_Wait_Ack();
extern void IIC_Nack();
extern void IIC_Ack();
extern void IIC_Send_Byte(UINT8 byte);
extern UINT8 IIC_Read_Byte(UINT8 ack);
#endif
此处为本次小制作中模拟I2C的头文件,定义P0.5
为SCL,定义P0.6
为SDA。
CH549_IIC.c
#include "CH549_IIC.h"
//初始化I2C
void IIC_Init()
{
GPIO_Init(PORT0,PIN5,MODE2);
GPIO_Init(PORT0,PIN6,MODE2);//CH549电压为5V,而非3.3V
}
//开始
void IIC_Start()
{
SDA_UP;
SCL_UP;
mDelayuS(4);
SDA_DOWN;
mDelayuS(4);
SCL_DOWN;
}
//结束
void IIC_Stop()
{
SCL_DOWN;
SDA_DOWN;
mDelayuS(4);
SCL_UP;
SDA_UP;
mDelayuS(4);
}
//返回值:1.接收应答失败;0.接收应答成功。
UINT8 IIC_Wait_Ack()
{
UINT32 t=0;
SDA_UP;
mDelayuS(1);
SCL_UP;
mDelayuS(1);
while(SDA_READ)
{
t++;
if(t > 250)
{
IIC_Stop();
return 1;
}
}
SCL_DOWN;
return 0;
}
//不产生ACK应答
void IIC_Nack()
{
SCL_DOWN;
SDA_UP;
mDelayuS(2);
SCL_UP;
mDelayuS(2);
SCL_DOWN;
}
//产生ACK应答
void IIC_Ack()
{
SCL_DOWN;
SDA_DOWN;
mDelayuS(2);
SCL_UP;
mDelayuS(2);
SCL_DOWN;
}
//I2C发送一个字节
void IIC_Send_Byte(UINT8 byte)
{
UINT8 i;
SCL_DOWN;
for( i = 0 ; i < 8 ; i++ )
{
if(byte&0x80)
{
SDA_UP;
}else
{
SDA_DOWN;
}
byte <<= 1;
mDelayuS(2);
SCL_UP;
mDelayuS(2);
SCL_DOWN;
mDelayuS(2);
}
}
//读1个字节,ack=1时,发送ack;ack=0时发送nack
UINT8 IIC_Read_Byte(UINT8 ack)
{
UINT8 i = 0;
UINT8 receive = 0;
for( i = 0 ; i < 8 ; i++ )
{
SCL_DOWN;
mDelayuS(2);
SCL_UP;
receive <<= 1;
if(SDA_READ)
{
receive++;
}
mDelayuS(1);
}
if (!ack)
{
IIC_Nack();
}else
{
IIC_Ack();
}
return receive;
}
CH549_TCS34725.c
#include "CH549_TCS34725.h"
#define max3v(v1, v2, v3) ((v1)<(v2)? ((v2)<(v3)?(v3):(v2)):((v1)<(v3)?(v3):(v1)))
#define min3v(v1, v2, v3) ((v1)>(v2)? ((v2)>(v3)?(v3):(v2)):((v1)>(v3)?(v3):(v1)))
/*******************************************************************************
* @brief Writes data to a slave device.
*
* @param slaveAddress - Adress of the slave device.
* @param dataBuffer - Pointer to a buffer storing the transmission data.
* @param bytesNumber - Number of bytes to write.
* @param stopBit - Stop condition control.
* Example: 0 - A stop condition will not be sent;
* 1 - A stop condition will be sent.
*******************************************************************************/
void TCS34725_I2C_Write(UINT8 slaveAddress, UINT8 * dataBuffer,UINT8 bytesNumber, UINT8 stopBit)
{
UINT8 i = 0;
IIC_Start();
IIC_Send_Byte((slaveAddress << 1) | 0x00); //发送从机地址写命令
IIC_Wait_Ack();
for(i = 0; i < bytesNumber; i++)
{
IIC_Send_Byte(*(dataBuffer + i));
IIC_Wait_Ack();
}
if(stopBit == 1) IIC_Stop();
}
/*******************************************************************************
* @brief Reads data from a slave device.
*
* @param slaveAddress - Adress of the slave device.
* @param dataBuffer - Pointer to a buffer that will store the received data.
* @param bytesNumber - Number of bytes to read.
* @param stopBit - Stop condition control.
* Example: 0 - A stop condition will not be sent;
* 1 - A stop condition will be sent.
*******************************************************************************/
void TCS34725_I2C_Read(UINT8 slaveAddress, UINT8 * dataBuffer, UINT8 bytesNumber, UINT8 stopBit)
{
UINT8 i = 0;
IIC_Start();
IIC_Send_Byte((slaveAddress << 1) | 0x01); //发送从机地址读命令
IIC_Wait_Ack();
for(i = 0; i < bytesNumber; i++)
{
if(i == bytesNumber - 1)
{
dataBuffer[i] = IIC_Read_Byte(0);//读取的最后一个字节发送NACK
}
else
{
dataBuffer[i] = IIC_Read_Byte(1);
}
}
if(stopBit == 1) IIC_Stop();
}
/*******************************************************************************
* @brief Writes data into TCS34725 registers, starting from the selected
* register address pointer.
*
* @param subAddr - The selected register address pointer.
* @param dataBuffer - Pointer to a buffer storing the transmission data.
* @param bytesNumber - Number of bytes that will be sent.
*
* @return None.
*******************************************************************************/
void TCS34725_Write(UINT8 subAddr, UINT8 * dataBuffer, UINT8 bytesNumber)
{
UINT8 sendBuffer[10] = {0, };
UINT8 byte = 0;
sendBuffer[0] = subAddr | TCS34725_COMMAND_BIT;
for(byte = 1; byte <= bytesNumber; byte++)
{
sendBuffer[byte] = dataBuffer[byte - 1];
}
TCS34725_I2C_Write(TCS34725_ADDRESS, sendBuffer, bytesNumber + 1, 1);
}
/*******************************************************************************
* @brief Reads data from TCS34725 registers, starting from the selected
* register address pointer.
*
* @param subAddr - The selected register address pointer.
* @param dataBuffer - Pointer to a buffer that will store the received data.
* @param bytesNumber - Number of bytes that will be read.
*
* @return None.
*******************************************************************************/
void TCS34725_Read(UINT8 subAddr, UINT8 * dataBuffer, UINT8 bytesNumber)
{
subAddr |= TCS34725_COMMAND_BIT;
TCS34725_I2C_Write(TCS34725_ADDRESS, (UINT8 *)&subAddr, 1, 0);
TCS34725_I2C_Read(TCS34725_ADDRESS, dataBuffer, bytesNumber, 1);
}
/*******************************************************************************
* @brief TCS34725设置积分时间
*
* @return None
*******************************************************************************/
void TCS34725_SetIntegrationTime(UINT8 time)
{
TCS34725_Write(TCS34725_ATIME, &time, 1);
}
/*******************************************************************************
* @brief TCS34725设置增益
*
* @return None
*******************************************************************************/
void TCS34725_SetGain(UINT8 gain)
{
TCS34725_Write(TCS34725_CONTROL, &gain, 1);
}
/*******************************************************************************
* @brief TCS34725使能
*
* @return None
*******************************************************************************/
void TCS34725_Enable(void)
{
UINT8 cmd = TCS34725_ENABLE_PON;
TCS34725_Write(TCS34725_ENABLE, &cmd, 1);
cmd = TCS34725_ENABLE_PON | TCS34725_ENABLE_AEN;
TCS34725_Write(TCS34725_ENABLE, &cmd, 1);
//delay_s(600000);//delay_ms(3);//延时应该放在设置AEN之后
}
/*******************************************************************************
* @brief TCS34725失能
*
* @return None
*******************************************************************************/
void TCS34725_Disable(void)
{
UINT8 cmd = 0;
TCS34725_Read(TCS34725_ENABLE, &cmd, 1);
cmd = cmd & ~(TCS34725_ENABLE_PON | TCS34725_ENABLE_AEN);
TCS34725_Write(TCS34725_ENABLE, &cmd, 1);
}
/*******************************************************************************
* @brief TCS34725初始化
*
* @return ID - ID寄存器中的值
*******************************************************************************/
UINT8 TCS34725_Init(void)
{
UINT8 id=0;
IIC_Init();
TCS34725_Read(TCS34725_ID, &id, 1); //TCS34725 的 ID 是 0x44 可以根据这个来判断是否成功连接,0x4D是TCS34727;
if(id==0x4D | id==0x44)
{
TCS34725_SetIntegrationTime(TCS34725_INTEGRATIONTIME_50MS);
TCS34725_SetGain(TCS34725_GAIN_1X);
TCS34725_Enable();
return 1;
}
return 0;
}
/*******************************************************************************
* @brief TCS34725获取单个通道数据
*
* @return data - 该通道的转换值
*******************************************************************************/
UINT16 TCS34725_GetChannelData(UINT8 reg)
{
UINT8 tmp[2] = {0,0};
UINT16 data;
TCS34725_Read(reg, tmp, 2);
data = (tmp[0] << 8) | tmp[1];
return data;
}
/*******************************************************************************
* @brief TCS34725获取各个通道数据
*
* @return 1 - 转换完成,数据可用
* 0 - 转换未完成,数据不可用
*******************************************************************************/
UINT8 TCS34725_GetRawData(COLOR_RGBC *rgbc)
{
UINT8 status = TCS34725_STATUS_AVALID;
TCS34725_Read(TCS34725_STATUS, &status, 1);
if(status & TCS34725_STATUS_AVALID)
{
rgbc->c = TCS34725_GetChannelData(TCS34725_CDATAL);
rgbc->r = TCS34725_GetChannelData(TCS34725_RDATAL);
rgbc->g = TCS34725_GetChannelData(TCS34725_GDATAL);
rgbc->b = TCS34725_GetChannelData(TCS34725_BDATAL);
return 1;
}
return 0;
}
/******************************************************************************/
//RGB转HSL
void RGBtoHSL(COLOR_RGBC *Rgb, COLOR_HSL *Hsl)
{
UINT8 maxVal,minVal,difVal;
UINT8 r = Rgb->r*100/Rgb->c; //[0-100]
UINT8 g = Rgb->g*100/Rgb->c;
UINT8 b = Rgb->b*100/Rgb->c;
maxVal = max3v(r,g,b);
minVal = min3v(r,g,b);
difVal = maxVal-minVal;
//计算亮度
Hsl->l = (maxVal+minVal)/2; //[0-100]
if(maxVal == minVal)//若r=g=b,灰度
{
Hsl->h = 0;
Hsl->s = 0;
}
else
{
//计算色调
if(maxVal==r)
{
if(g>=b)
Hsl->h = 60*(g-b)/difVal;
else
Hsl->h = 60*(g-b)/difVal+360;
}
else
{
if(maxVal==g)Hsl->h = 60*(b-r)/difVal+120;
else
if(maxVal==b)Hsl->h = 60*(r-g)/difVal+240;
}
//计算饱和度
if(Hsl->l<=50)Hsl->s=difVal*100/(maxVal+minVal); //[0-100]
else
Hsl->s=difVal*100/(200-(maxVal+minVal));
}
}
该段程序基本上沿用STM32中关于TCS34725的设置,将其中STM32与CH549不同的地方做了适应CH549修改。
主程序main.c
#include <CH549_TCS34725.h> //TCS34725颜色识别相关设定
#include <CH549_IIC.h> //I2C相关设定
#include <CH549_PWM.h> //CH549的PWM相关设定
#include <CH549_GPIO.h> //GPIO相关设定
#include <CH549_BMP.h> //用于显示图片的头文件
#include <CH549_OLED.h> //其中有驱动屏幕使用的函数
#include <CH549_SPI.h> //CH549官方提供库的头文件,定义了一些关于SPI初始化,传输数据等函数
#include <CH549_DEBUG.h> //CH549官方提供库的头文件,定义了一些关于主频,延时,串口设置,看门口,赋值设置等基础函数
#include <CH549_sdcc.H> //ch549的头文件,其中定义了单片机的一些特殊功能寄存器
#include <CH549_UART.h>
// #define numLEDs 10 //灯的个数
#define max3v(v1, v2, v3) ((v1) < (v2) ? ((v2) < (v3) ? (v3) : (v2)) : ((v1) < (v3) ? (v3) : (v1))) // 三个数中最大值
#define min3v(v1, v2, v3) ((v1) > (v2) ? ((v2) > (v3) ? (v3) : (v2)) : ((v1) > (v3) ? (v3) : (v1))) // 三个数中最小值
UINT8 box[4] = {60, 80, 95, 110}; // 控制分向舵机的PWM参数三组,例如第一个数值即为第一个盒子
COLOR_RGBC rgb; // RGB相关结构体
COLOR_HSL hsl; // HSL相关结构体
int oled_colum; // oled列
int oled_row; // oled行
void setCursor(int column, int row); // 屏幕输出函数
/********************************************************************
* 函 数 名 : main
* 函数功能 : 主函数
* 输 入 : 无
* 输 出 : 无
*********************************************************************/
void main(void)
{
UINT16 r, g, b;
UINT32 M = 0;
UINT16 m = 0, n = 0, l = 0, i = 0, x = 0, y = 0, z = 0;
CfgFsys(); // CH549时钟选择配置
mDelaymS(20);
SPIMasterModeSet(3); //SPI主机模式设置,模式3
SPI_CK_SET(12); //设置spi sclk 时钟信号为12分频
OLED_Init(); //初始化OLED
OLED_Clear(); //将oled屏幕上内容清除
setFontSize(8); //设置文字大小
TCS34725_Init(); // 初始化TCS34725
TCS34725_SetGain(TCS34725_GAIN_4X); // 设置TCS34725的增益
TCS34725_SetIntegrationTime(TCS34725_INTEGRATIONTIME_24MS); // 设置TCS34725的积分时间,即通道最大值
SetPWMClkDiv(255); // PWM时钟配置,Fsys/255,Fsys为12Mhz
SetPWMCycle256Clk(); // PWM周期 Fsys/255/256
PWM_SEL_CHANNEL(PWM_CH1, Enable); // 使能CH1,即P2.4
PWM_SEL_CHANNEL(PWM_CH3, Enable); // 使能CH3,即P2.2
SetPWM1Dat(18);
/* 主循环 */
while (1)
{
TCS34725_GetRawData(&rgb); // 读取当前颜色数据,即RGBC
RGBtoHSL(&rgb, &hsl); // 转RGB为HSL
r = (long)rgb.r * 255.0 / rgb.c; // 将实际的RGB值读取出来,范围为0-255
g = (long)rgb.g * 255.0 / rgb.c;
b = (long)rgb.b * 255.0 / rgb.c;
mDelaymS(20);
setCursor(0,0);//设置printf到屏幕上的字符串起始位置
printf_fast_f("x : %d ",r);
setCursor(0,2);//设置printf到屏幕上的字符串起始位置
printf_fast_f("y : %d ",g);
setCursor(0,4);//设置printf到屏幕上的字符串起始位置
printf_fast_f("z : %d ",b);
setCursor(0,6);//设置printf到屏幕上的字符串起始位置
printf_fast_f("m : %d ",hsl.h);
if (rgb.c > 100) // 能检测到亮度时开始读取颜色值
{
if (3 >= i && i >= 2) // 2次循环后再读取防抖动
{
if ((max3v(r, g, b) > 240) || (max3v(r, g, b) < 60)) // 经过防抖动之后可能还是有数据异常的情况,再检测一次
{
i--;
}
else
{
if (i == 2)
{
m = hsl.h; // 读取当前HSL值
n = hsl.s;
l = hsl.l;
x = r; // 读取当前RGB值
y = g;
z = b;
}
else
{
m = (m + hsl.h) / 2; // 取平均值
n = (n + hsl.s) / 2;
l = (l + hsl.l) / 2;
x = (x + r) / 2;
y = (y + g) / 2;
z = (z + b) / 2;
}
}
}
i++;
}
if (rgb.c < 100) // 亮度归零意味着小球经过监测点,开始根据读取到的RGB值进行判断
{
if (i != 0) // 仅判断一次
{
if (x > z && x > y)
{
M = M << 8 | box[0]; // 红球
}
else
{
if (m > 190 && m < 200 && z > x && z > y)
{
M = M << 8 | box[3]; // 白球
}
else
{
if (m >= 200 && m < 220)
{
M = M << 8 | box[2]; // 蓝球
}
else if (m > 155 && m <= 195)
{
M = M << 8 | box[1]; // 绿球
}
}
}
i = 0;
}
SetPWM3Dat((unsigned char)(M >> 8 * 2)); // 将第三个数据发送给舵机
}
}
}
/********************************************************************
* 函 数 名 : putchar
* 函数功能 : 将printf映射到OLED屏幕输出上
* 输 入 : 字符串
* 输 出 : 字符串
********************************************************************/
int putchar(int a)
{
在光标处显示文字 a
OLED_ShowChar(oled_colum,oled_row,a);
将光标右移一个字的宽度,以显示下一个字
oled_colum+=6;
/*当此行不足以再显示一个字时,换行.
同时光标回到最边(列坐标=0).
*/
if (oled_colum>122){oled_colum=0;oled_row+=1;}
return(a);
}
/********************************************************************
* 函 数 名 : setCursor
* 函数功能 : 设置光标(printf到屏幕上的字符串起始位置)
* 输 入 : 行坐标 列坐标(此处一行为8个像素,一列为1个像素,所以屏幕上共有8行128列)
* 输 出 : 无
********************************************************************/
void setCursor(int column, int row)
{
oled_colum = column;
oled_row = row;
}
PWM相关
本次主函数中N20电机和MG90S舵机使用到了PWM控制。
UINT8 box[4] = {60,80,95,110}; //控制分向舵机的PWM参数三组,例如第一个数值即为第一个盒子
SetPWMClkDiv(255); //PWM时钟配置,Fsys/255,Fsys为12Mhz
SetPWMCycle256Clk(); //PWM周期 Fsys/255/256
PWM_SEL_CHANNEL(PWM_CH1,Enable); //使能CH1,即P2.4
PWM_SEL_CHANNEL(PWM_CH3,Enable); //使能CH3,即P2.2
SetPWM1Dat(18);
MG90S舵机
舵机的控制信号为周期是20ms的脉宽调制(PWM)信号,其中脉冲宽度从0.5ms-2.5ms,相对应舵盘的位置为0-180度,呈线性变化。也就是说,给它提供一定的脉宽,它的输出轴就会保持在一个相对应的角度上,无论外界转矩怎样改变,直到给它提供一个另外宽度的脉冲信号,它才会改变输出角度到新的对应的位置上。一般而言,舵机的基准信号都是周期为20ms,宽度为1.5ms。这个基准信号定义的位置为中间位置。其中间位置的脉冲宽度是一定的,那就是1.5ms。

角度是由来自控制线的持续的脉冲所产生。这种控制方法叫做脉冲调制。脉冲的长短决定舵机转动多大角度。当舵机接收到一个小于1.5ms的脉冲,输出轴会以中间位置为标准,逆时针旋转一定角度。接收到的脉冲大于1.5ms情况相反。不同品牌,甚至同一品牌的不同舵机,都会有不同的最大值和最小值。一般而言,最小脉冲为1ms,最大脉冲为2ms。

本次设计中采用等等PWM时钟配置的Fsys为12MHz,为了达到目标的20ms的周期,将值均取最大,勉强达到周期5.4ms,可以进行操作。
舵机示意图
安装时选取如图中90°的线作为中心线。

N20电机
用MX1508控制两台N20电机,电机1是旋转盘,经测试18/255的占空比设置刚好满足要求,搅拌轮的正极直接接VCC全速旋转,防止玻璃珠卡在漏斗上。
TCS34725颜色识别相关
TCS34725_GetRawData(&rgb); // 读取当前颜色数据,即RGBC
RGBtoHSL(&rgb, &hsl); // 转RGB为HSL
r = (long)rgb.r * 255.0 / rgb.c; // 将实际的RGB值读取出来,范围为0-255
g = (long)rgb.g * 255.0 / rgb.c;
b = (long)rgb.b * 255.0 / rgb.c;
mDelaymS(20);
setCursor(0,0);//设置printf到屏幕上的字符串起始位置
printf_fast_f("r : %d ",x);
setCursor(0,2);//设置printf到屏幕上的字符串起始位置
printf_fast_f("g : %d ",y);
setCursor(0,4);//设置printf到屏幕上的字符串起始位置
printf_fast_f("b : %d ",z);
setCursor(0,6);//设置printf到屏幕上的字符串起始位置
printf_fast_f("h : %d ",hsl.h);
if (rgb.c > 100) // 能检测到亮度时开始读取颜色值
{
if (3 >= i && i >= 2) // 2次循环后再读取防抖动
{
if ((max3v(r, g, b) > 240) || (max3v(r, g, b) < 60) ) // 经过防抖动之后可能还是有数据异常的情况,再检测一次
{
i--;
}
else
{
if (i == 2)
{
m = hsl.h; // 读取当前HSL值
n = hsl.s;
l = hsl.l;
x = r; // 读取当前RGB值
y = g;
z = b;
}
else
{
m = (m + hsl.h) / 2; // 取平均值
n = (n + hsl.s) / 2;
l = (l + hsl.l) / 2;
x = (x + r) / 2;
y = (y + g) / 2;
z = (z + b) / 2;
}
}
}
i++;
}
if (rgb.c < 100) // 亮度归零意味着小球经过监测点,开始根据读取到的RGB值进行判断
{
if (i != 0) // 仅判断一次
{
if (x > z && x > y)
{
M = M << 8 | box[0]; // 红球
}
else
{
if (m > 190 && m < 200 && z > x && z > y)
{
M = M << 8 | box[3]; // 白球
}
else
{
if (m >= 200 && m < 220)
{
M = M << 8 | box[2]; // 蓝球
}
else if (m > 155 && m <= 195)
{
M = M << 8 | box[1]; // 绿球
}
}
}
}
i = 0;
}
SetPWM3Dat((unsigned char)(M >> 8 * 2)); // 将第三个数据发送给舵机
其中:
TCS34725_GetRawData(&rgb); // 读取当前颜色数据,即RGBC
RGBtoHSL(&rgb, &hsl); // 转RGB为HSL
r = (long)rgb.r * 255.0 / rgb.c; // 将实际的RGB值读取出来,范围为0-255
g = (long)rgb.g * 255.0 / rgb.c;
b = (long)rgb.b * 255.0 / rgb.c;
这里为RGBC的数据获取和转RGB的算法部分。
/******************************************************************************/
//RGB转HSL
void RGBtoHSL(COLOR_RGBC *Rgb, COLOR_HSL *Hsl)
{
UINT8 maxVal,minVal,difVal;
UINT8 r = (long)Rgb->r*100/Rgb->c; //[0-100]
UINT8 g = (long)Rgb->g*100/Rgb->c;
UINT8 b = (long)Rgb->b*100/Rgb->c;
maxVal = max3v(r,g,b);
minVal = min3v(r,g,b);
difVal = maxVal-minVal;
//计算亮度
Hsl->l = (maxVal+minVal)/2; //[0-100]
if(maxVal == minVal)//若r=g=b,灰度
{
Hsl->h = 0;
Hsl->s = 0;
}
else
{
//计算色调
if(maxVal==r)
{
if(g>=b)
Hsl->h = 60*(g-b)/difVal;
else
Hsl->h = 60*(g-b)/difVal+360;
}
else
{
if(maxVal==g)Hsl->h = 60*(b-r)/difVal+120;
else
if(maxVal==b)Hsl->h = 60*(r-g)/difVal+240;
}
//计算饱和度
if(Hsl->l<=50)
Hsl->s=difVal*100/(maxVal+minVal); //[0-100]
else
Hsl->s=difVal*100/(200-(maxVal+minVal));
}
}
/******************************************************************************/
这里为RGB转HSL的算法部分。
if (rgb.c > 100) // 能检测到亮度时开始读取颜色值
{
if (3 >= i && i >= 2) // 2次循环后再读取防抖动
{
if ((max3v(r, g, b) > 240) || (max3v(r, g, b) < 60) ) // 经过防抖动之后可能还是有数据异常的情况,再检测一次
{
i--;
}
else
{
if (i == 2)
{
m = hsl.h; //读取当前HSL值
n = hsl.s;
l = hsl.l;
x = r; //读取当前RGB值
y = g;
z = b;
}
else
{
m = (m + hsl.h) / 2;//取平均值
n = (n + hsl.s) / 2;
l = (l + hsl.l) / 2;
x = (x + r) / 2;
y = (y + g) / 2;
z = (z + b) / 2;
}
}
}
i++;
}
这里为TCS34725颜色识别传感器筛选符合条件的有效数据后取平均值部分,能让采集到的数据更加精确,提高小球的识别度。
1.完全遮光后Clear的数据在非检测区为0,只有经过1处检测区时,通过下方的LED灯的照射才会有数值,以此作为开始检测的标志(rgb.c > 100)
;
2.在转盘旋转时,因为球需要在4处落下经过滑道进入盒子,所以球在运输过程中并非卡死在检测区有缝隙,即随着他的运动会产生LED灯直接照射传感器或者是反光影响颜色识别的现象,尤其是在刚开始检测和快结束检测的时候;
3.一次检测大概过7次循环左右,所以选定符合条件的第二三次循环的数据的平均值为准,以防数据仍有问题,加入防抖判断,即RGB三个数值有一个大于240或者都小于60时即判断为坏数据,取下一次的数据为准。
颜色检测示意图

由图中可以看出1准备检测时刚好是4落下时,所以设定32位的数,将每一次的数据左移进去,而最后读取的时候将第三个数据发给舵机,完美实现彩球分拣。
判断彩球代码:
if (rgb.c < 100) // 亮度归零意味着小球经过监测点,开始根据读取到的RGB值进行判断
{
if (i != 0) // 仅判断一次
{
if (x > z && x > y)
{
M = M << 8 | box[0]; // 红球
}
else
{
if (m > 190 && m < 200 && z > x && z > y)
{
M = M << 8 | box[3]; // 白球
}
else
{
if (m >= 200 && m < 220)
{
M = M << 8 | box[2]; // 蓝球
}
else if (m > 155 && m <= 195)
{
M = M << 8 | box[1]; // 绿球
}
}
}
}
i = 0;
}
SetPWM3Dat((unsigned char)(M >> 8 * 2)); // 将第三个数据发送给舵机
这里为判断彩球颜色部分,所使用的判断条件中的x,y,z,m
分别为采集的r,g,b,hsl.h
的有效数据平均值,再通过不同颜色小球数据间的不同加以区分来达到更为精确的分拣效果。
成品展示

总结
可能遇到的问题:
1.电路接线问题,补光灯亮度影响所测数据(补光LED灯是否已烧);
2.传感器增益相关问题(算法中若结果数值较小或者误差较大请调高增益,若数值过大请调低增益);
TCS34725_SetGain(TCS34725_GAIN_4X); // 设置TCS34725的增益
3.数据的调试,通过对所测数据(r = x, g = y, b = z)
(hsl.h = m, hsl.s = n, hsl.l = l)
的对比区分来找出区别彩球条件;
if (x > z && x > y)
{
M = M << 8 | box[0]; // 红球
}
else
{
if (m > 190 && m < 200 && z > x && z > y) // 白球
{
M = M << 8 | box[3];
}
else
if (m >= 200 && m < 220)
{
M = M << 8 | box[2]; // 蓝球
}
else if (m > 155 && m <= 195)
{
M = M << 8 | box[1]; // 绿球
}
}
4.对数据的统计时注意强制类型转换。
r = (long)rgb.r * 255.0 / rgb.c; // 将实际的RGB值读取出来,范围为0-255
g = (long)rgb.g * 255.0 / rgb.c;
b = (long)rgb.b * 255.0 / rgb.c;