TCS3200顏色識別與LCD1602顯示工程解析)
簡介這是一套基于C51單片機(jī)的TCS3200顏色識別與LCD1602顯示完整軟件源碼面向正在學(xué)習(xí)單片機(jī)接口編程、傳感器數(shù)據(jù)采集與顯示驅(qū)動的開發(fā)者尤其適合課程設(shè)計或電子競賽的軟硬件聯(lián)調(diào)階段。代碼中main函數(shù)完成系統(tǒng)初始化后開啟總中斷INT0下降沿觸發(fā)計數(shù)定時器T0負(fù)責(zé)計時利用TCS3200輸出頻率與顏色分量的關(guān)系計算RGB數(shù)值再通過LCD1602顯示出對應(yīng)顏色信息中斷服務(wù)與主循環(huán)配合緊密適合理解傳感器測頻與顯示刷新的完整流程。壓縮包共16個文件容量僅46KB主要包含C語言源文件、Keil工程文件uvproj/uvopt、編譯生成的HEX燒錄文件、LST列表文件及工程備份能夠直接打開工程重新編譯也可用HEX文件燒錄驗(yàn)證。另有PLG、M51等輔助文件便于查看鏈接映射和項(xiàng)目構(gòu)建日志。目前已有278人學(xué)習(xí)下載對于需要快速搭建TCS3200顏色識別顯示系統(tǒng)的讀者具有直接參考和復(fù)用價值。1. 手頭這份源代碼包到底在做什么手頭這份『基于C51單片機(jī)設(shè)計的TCS3200顏色識別LCD10602顯示的軟件源代碼.zip』是典型的單片機(jī)課程設(shè)計或軟件綜合實(shí)踐選題用8051系列單片機(jī)讀一顆顏色傳感器再把識別結(jié)果送到一塊字符屏上。標(biāo)題里的LCD10602按市面上能買到的器件習(xí)慣對應(yīng)的是最常見的LCD1602字符液晶下文的驅(qū)動和接線都以LCD1602為準(zhǔn)。整包源碼的價值不在傳感器本身的電路而在C51工程里怎么把TCS3200輸出的頻率信號量化成RGB三個數(shù)再把這個過程嵌進(jìn)一個不卡死的顯示循環(huán)。適合正在做課程設(shè)計、剛接觸Keil C51的讀者也適合想把手頭的C51開發(fā)板快速接到TCS3200的老手。2. TCS3200顏色傳感器的頻率輸出原理與C51定時器計數(shù)2.1 引腳配置S0/S1決定頻率縮放S2/S3決定通道顏色TCS3200內(nèi)部把光電二極管排列成8x8陣列等效為16個紅色、16個綠色、16個藍(lán)色、16個無濾光通道。芯片對外只有一個頻率輸出引腳OUT頻率高低與所選通道的光強(qiáng)近似線性。選哪個通道由S2/S3兩個引腳決定頻率縮放倍率由S0/S1兩個引腳決定這四根線正好接在C51的四個普通IO上。S0S1輸出頻率縮放適用場景LL關(guān)斷不建議用于測量LH2%弱光環(huán)境、需要更精細(xì)的低頻測量HL20%常規(guī)室內(nèi)光源推薦起始檔HH100%強(qiáng)光、高速采樣S2S3選通通道LL紅色濾波通道HH綠色濾波通道LH藍(lán)色濾波通道HL無濾波通帶通道我一般把S0/S1設(shè)為20%檔起步。原因很直接C51的T0計數(shù)器是16位如果直接用100%檔強(qiáng)光下輸出頻率可達(dá)數(shù)百kHz10ms窗口內(nèi)計數(shù)可能逼近65535容易溢出2%檔又會讓暗色物體讀數(shù)太小。20%檔在室內(nèi)光照下通常落在10kHz到120kHz之間10ms窗口計數(shù)范圍是100到1200分辨率足夠判斷顏色大類。接線方式也很固定TCS3200的OUT接P3.4也就是T0外部計數(shù)輸入引腳S0接P1.0S1接P1.1S2接P1.2S3接P1.3。這樣做的原因是T0專門負(fù)責(zé)數(shù)脈沖T1專門負(fù)責(zé)開時間窗口兩個定時器分工不占用CPU計算頻率。2.2 T0外部計數(shù)加T1時間窗口的測量代碼C51測量頻率的常見做法是“定時計數(shù)”T1產(chǎn)生一個固定寬度的時間窗口T0在這個窗口內(nèi)對外部脈沖計數(shù)。窗口結(jié)束時讀出TH0和TL0拼接出來的計數(shù)值再除以窗口時間就得到頻率。下面這段是可運(yùn)行的骨架我會把它獨(dú)立成tcs3200_core.c。/* tcs3200_core.c - 適用于STC89C52/AT89S52等C51單片機(jī) */ #include reg52.h sbit S0 P1^0; sbit S1 P1^1; sbit S2 P1^2; sbit S3 P1^3; #define TCS_SCALE_20 0 /* S01,S10: 輸出頻率為原頻率的20% */ #define TCS_SCALE_2 1 /* S00,S11: 輸出頻率為原頻率的2% */ void Delay_Ms(unsigned int ms); void TCS_Init(unsigned char scale) { if (scale TCS_SCALE_20) { S0 1; S1 0; } else { S0 0; S1 1; } } unsigned int TCS_ReadChannel(unsigned char ch) { unsigned int cnt; /* 切換光電二極管通道: 0紅, 1綠, 2藍(lán), 3通帶 */ switch (ch) { case 0: S2 0; S3 0; break; /* 紅 */ case 1: S2 1; S3 1; break; /* 綠 */ case 2: S2 0; S3 1; break; /* 藍(lán) */ default: S2 1; S3 0; break; /* 無濾波 */ } Delay_Ms(2); /* 通道切換后留2ms讓輸出穩(wěn)定 */ /* T0作16位外部計數(shù), T1作16位定時器, 開一個10ms窗口 */ TMOD 0x15; /* 0001 0101: T116位定時器, T016位計數(shù)器 */ TH0 0; TL0 0; /* 11.0592MHz晶振, 12時鐘/機(jī)器周期, 10ms窗口需9216個計數(shù) */ TH1 (65536 - 9216) / 256; TL1 (65536 - 9216) % 256; TR0 1; TR1 1; while (TF1 0); /* 等待T1溢出, 10ms窗口結(jié)束 */ TR0 0; TR1 0; TF1 0; cnt ((unsigned int)TH0 8) | TL0; return cnt; }這段代碼的操作邏輯很直觀先切換通道等輸出穩(wěn)定清空T0給T1裝上10ms窗口的初值然后同時啟動兩個定時器。T0在P3.4引腳上數(shù)外部脈沖T1在內(nèi)部按機(jī)器周期跑。T1溢出時窗口結(jié)束停止計數(shù)后把TH0和TL0拼成一個16位無符號整數(shù)返回。參數(shù)上要注意三個點(diǎn)。第一TMOD0x15的低4位0101表示T0是16位計數(shù)器高4位0001表示T1是16位定時器如果把T0配成定時器而T1配成計數(shù)器讀出來會是完全無規(guī)律的數(shù)字。第二9216這個數(shù)字只在11.0592MHz晶振、10ms窗口下成立換成12MHz晶振時要改成10000因?yàn)?2MHz的機(jī)器周期正好是1us。第三返回的cnt除以0.01秒就是OUT引腳的實(shí)際頻率單位Hz如果要做絕對光譜分析再除以0.2得到傳感器的原始頻率但單純比顏色時直接用cnt就夠。2.3 三通道輪詢與RGB歸一化顏色識別最少要讀紅綠藍(lán)三個通道。我一般把三個讀數(shù)放進(jìn)一個結(jié)構(gòu)體里然后按最大值歸一化到0到255這樣光源強(qiáng)弱變化時顏色比例仍然穩(wěn)定。typedef struct { unsigned char r; unsigned char g; unsigned char b; } RGB_Value; void TCS_ReadRGB(RGB_Value *rgb) { unsigned int cr, cg, cb, cmax; cr TCS_ReadChannel(0); cg TCS_ReadChannel(1); cb TCS_ReadChannel(2); /* 取三通道最大值做歸一化基準(zhǔn), 防止暗場下三個數(shù)都偏小 */ cmax (cr cg) ? cr : cg; cmax (cb cmax) ? cb : cmax; if (cmax 0) cmax 1; rgb-r (unsigned char)((unsigned long)cr * 255 / cmax); rgb-g (unsigned char)((unsigned long)cg * 255 / cmax); rgb-b (unsigned char)((unsigned long)cb * 255 / cmax); }為什么不除以固定的校準(zhǔn)值而用動態(tài)最大值因?yàn)門CS3200讀到的絕對計數(shù)值受環(huán)境光強(qiáng)度影響很大同一個紅色物體在白天和晚上可能差三倍但紅綠藍(lán)之間的相對比例變化不大。按當(dāng)天當(dāng)前光源下的最大值歸一化相當(dāng)于把亮度信息丟掉只保留色度比例這對識別“偏紅還是偏綠”最有效。需要提醒的是三個通道不是同一時刻采樣的紅綠藍(lán)各占一個10ms窗口再加上通道切換延時一組數(shù)據(jù)實(shí)際跨越了大約40ms。對靜止物體這沒有問題對快速移動的物體三通道會受運(yùn)動模糊影響。更穩(wěn)的做法是每個通道連續(xù)讀三次取平均或者把窗口時間從10ms拉到50ms后者適合弱光但會降低刷新率。3. LCD1602LCD10602在C51上的4位驅(qū)動與顯示格式3.1 4線接線與引腳分配LCD1602的標(biāo)準(zhǔn)接口有16個引腳但控制字符顯示只需要數(shù)據(jù)線、RS、RW、E四類。8位模式要占8根數(shù)據(jù)線4位模式只占4根而且速度差異在字符屏上完全感受不到。我一般直接省略RW把它接地這樣能再省一個IO只保留RS和E兩個控制腳。這里采用一組比較省線的分配LCD的D4到D7接P0.4到P0.7RS接P2.5RW接P2.6E接P2.7。P0口是開漏輸出必須外接10k上拉排阻否則高電平驅(qū)動能力不足字符會忽亮忽暗。背光LED的限流電阻用一個100歐到330歐的貼片電阻視供電電壓而定。LCD引腳接C51引腳說明D4~D7P0.4~P0.74位數(shù)據(jù)線高4位有效RSP2.50寫命令1寫數(shù)據(jù)RWP2.6只寫模式接地EP2.7下降沿鎖存數(shù)據(jù)選4位模式還有一層原因C51的P0口經(jīng)常同時掛外部存儲器或其它外設(shè)8位全占用會讓系統(tǒng)擴(kuò)展性變得很差。4位模式只占半個端口剩下的引腳可以留給按鍵、蜂鳴器或者串口。3.2 4位初始化時序和讀寫函數(shù)LCD1602的4位初始化有一個固定套路先發(fā)三次0x30讓液晶無論處于什么狀態(tài)都回到8位模式的初始位置第四次發(fā)0x20切換成4位模式。之后才能用完整的命令字節(jié)配置顯示參數(shù)。我把它寫成一個單獨(dú)的lcd1602.c核心函數(shù)如下。/* lcd1602.c - 4位總線驅(qū)動, 適用于STC89C52/AT89S52 */ #include reg52.h #define LCD_DB P0 sbit LCD_RS P2^5; /* 寄存器選擇: 0命令 1數(shù)據(jù) */ sbit LCD_RW P2^6; /* 讀寫選擇: 0寫 1讀 */ sbit LCD_E P2^7; /* 使能脈沖 */ void Delay_Ms(unsigned int ms); static void LCD_WriteHalf(unsigned char dat) { LCD_DB (LCD_DB 0x0F) | (dat 0xF0); /* 只改高4位 */ LCD_E 1; Delay_Ms(1); LCD_E 0; } static void LCD_WriteByte(unsigned char dat) { LCD_WriteHalf(dat); /* 先高4位 */ LCD_WriteHalf(dat 4); /* 后低4位 */ } void LCD_WriteCmd(unsigned char cmd) { LCD_RS 0; LCD_RW 0; LCD_WriteByte(cmd); } void LCD_WriteData(unsigned char dat) { LCD_RS 1; LCD_RW 0; LCD_WriteByte(dat); } void LCD_Init(void) { Delay_Ms(20); LCD_RS 0; LCD_RW 0; /* 三次0x30握手進(jìn)入4位模式 */ LCD_WriteHalf(0x30); Delay_Ms(5); LCD_WriteHalf(0x30); Delay_Ms(1); LCD_WriteHalf(0x30); LCD_WriteHalf(0x20); /* 正式切到4位 */ LCD_WriteCmd(0x28); /* 4位, 2行, 5x7點(diǎn)陣 */ LCD_WriteCmd(0x0C); /* 開顯示, 不顯示光標(biāo) */ LCD_WriteCmd(0x01); /* 清屏 */ LCD_WriteCmd(0x06); /* 地址自動加一 */ Delay_Ms(2); }LCD_WriteHalf每次只向P0的高4位寫數(shù)據(jù)低4位用原來的值兜住避免影響同一端口上其它外設(shè)。寫命令和寫數(shù)據(jù)唯一的區(qū)別就是RS的電平在命令狀態(tài)時RS拉低在數(shù)據(jù)狀態(tài)時RS拉高。E引腳用下降沿鎖存所以先拉高再拉低形成一個完整的使能脈沖。初始化時前三次0x30看起來是同一個值實(shí)際是在逐步確認(rèn)8位模式下的同步邊界。如果直接發(fā)0x28液晶在上電時序不完整時可能停留在2行模式的某一側(cè)導(dǎo)致后面每行只顯示半個字符。0x28把我前面發(fā)送的半個字節(jié)拆成兩次0x30中的一次然后在4位模式下正確配對。3.3 把RGB值轉(zhuǎn)成顏色名稱的映射判據(jù)歸一化之后接下來要做的不是打印三個數(shù)字而是給出一個人類能讀的顏色名。判據(jù)基于一個簡單事實(shí)純紅色物體的r值會明顯大于g和b純綠色物體的g值會明顯大于另外兩個藍(lán)色同理。亮度很低的物體一律歸為黑色三個值接近且都偏大的歸為白色或灰色。unsigned char code *MapColor(unsigned char r, unsigned char g, unsigned char b) { unsigned char max; max (r g) ? r : g; max (b max) ? b : max; if (max 60) return BLACK; /* 三通道接近且都大于180, 視為白或淺灰 */ if (max 180 max - r 30 max - g 30 max - b 30) return WHITE; /* 用整數(shù)比較代替浮點(diǎn), 避免C51的浮點(diǎn)庫開銷 */ if (r max (unsigned int)g * 10 (unsigned int)max * 6 (unsigned int)b * 10 (unsigned int)max * 6) return RED; if (g max (unsigned int)r * 10 (unsigned int)max * 6 (unsigned int)b * 10 (unsigned int)max * 6) return GREEN; if (b max (unsigned int)r * 10 (unsigned int)max * 6 (unsigned int)g * 10 (unsigned int)max * 6) return BLUE; return MIX; }這里的閾值6/10表示次要通道不超過主通道的60%是室內(nèi)光源下比較寬松的判斷。如果你把物體放在暗處60%可能會把棕色誤判成紅色這時把閾值收緊到45%左右。比較時用g * 10 max * 6而不是g max * 0.6是因?yàn)镃51的浮點(diǎn)運(yùn)算要額外鏈接浮點(diǎn)庫在8位機(jī)上會拖慢顯示刷新。值得注意的是上面函數(shù)返回的字符串常量在C51里默認(rèn)放在code段所以返回類型寫成unsigned char code *。如果你的Keil工程報“指針類型不匹配”多半是因?yàn)樽址环旁赾ode段而函數(shù)聲明成了普通的char *這是C51和標(biāo)準(zhǔn)C差異最明顯的地方。4. 完整軟件工程的文件劃分與Keil5工程配置4.1 main.c、tcs3200.c、lcd1602.c的接口職責(zé)拿到一個軟件源代碼包先看文件劃分是否合理比先看代碼更重要。我一般會把C51工程拆成四個文件main.c負(fù)責(zé)主循環(huán)tcs3200.c負(fù)責(zé)傳感器采樣lcd1602.c負(fù)責(zé)顯示delay.c提供延時。頭文件只放函數(shù)聲明和宏定義不放全局變量定義。文件對外接口內(nèi)部職責(zé)main.cmain()初始化、輪詢、顯示刷新節(jié)奏tcs3200.cTCS_Init、TCS_ReadRGB通道切換、定時器窗口、歸一化lcd1602.cLCD_Init、LCD_ShowRGB4位驅(qū)動、字符格式化delay.cDelay_Ms毫秒延時軟實(shí)現(xiàn)這樣拆分有個看得見的好處調(diào)試顏色識別時不需要翻液晶代碼調(diào)顯示時不需要碰傳感器。壓縮包里如果只有一個巨長的main.c建議先按這個結(jié)構(gòu)拆開后再去改算法否則每一個小改動都要重新編譯整個文件驗(yàn)證周期會拖長。4.2 主循環(huán)的采樣節(jié)奏與刷新節(jié)流主循環(huán)不需要寫成復(fù)雜狀態(tài)機(jī)顏色識別和顯示天然是順序關(guān)系。為了讓屏幕上的數(shù)字可讀而不是飛快跳變我一般把每次完整刷新的間隔控制在300ms左右也就是大約3幀每秒。#include tcs3200.h #include lcd1602.h void main(void) { RGB_Value rgb; unsigned char code *color_name; Delay_Ms(100); LCD_Init(); TCS_Init(TCS_SCALE_20); LCD_WriteStr(0, 0, TCS3200 Ready); Delay_Ms(500); LCD_WriteCmd(0x01); Delay_Ms(2); while (1) { TCS_ReadRGB(rgb); color_name MapColor(rgb.r, rgb.g, rgb.b); LCD_ShowRGB(rgb, color_name); Delay_Ms(300); } }這段循環(huán)里TCS_ReadRGB本身會消耗三組10ms窗口加兩次通道切換延時大概40ms剩下的300ms是純顯示節(jié)流。如果你把Delay_Ms去掉屏幕會以每秒25幀以上刷新但由于LCD1602本身沒有硬件緩沖快速刷新時字符會閃爍人的眼睛反而不舒服。主循環(huán)里沒有中斷這是刻意為之。TCS3200的采樣靠定時器同步等待LCD1602的寫操作也依賴延時整個流程是順序的。如果給采樣加中斷反而會出現(xiàn)中斷里改通道、主循環(huán)里讀計數(shù)值的競態(tài)問題。對這個項(xiàng)目來說輪詢比中斷更不容易踩坑。4.3 Keil5中添加C51芯片包與鏈接器控制文件Keil5默認(rèn)安裝時只勾選ARM工具鏈的話新建工程會找不到C51芯片。解決路徑不是重新下載整個IDE而是打開Pack Installer找到對應(yīng)芯片廠商的設(shè)備包后點(diǎn)擊Install或者在安裝Keil5時勾上C51 Compiler組件。同一臺機(jī)器上同時裝C51和ARMSTM32兩套工具鏈?zhǔn)峭耆R姷乃鼈児灿靡粋€uVision界面工程文件后綴都是.uvprojx創(chuàng)建工程時靠芯片型號區(qū)分編譯工具鏈。芯片選型后在Options for Target里有兩處必須確認(rèn)。第一是Memory Model一般選Small變量放data區(qū)這樣訪問速度快但data區(qū)總共只有128字節(jié)如果程序里RGB緩沖區(qū)不多Small完全夠用。第二是Code Rom Size選Large 64K避免鏈接器因?yàn)榇a超過默認(rèn)段而產(chǎn)生L55類錯誤。Keil C51會為每個工程自動生成一個鏈接器控制文件STARTUP.A51它負(fù)責(zé)在main之前把可重定位段清零、設(shè)置堆棧指針。如果程序里用了XDATA的大數(shù)組鏈接器會在映射文件.M51里報告XDATA段的起始地址和大小調(diào)試RAM溢出時先看這個文件。不要手動去改STARTUP.A51里的堆棧位置除非你確定XDATA或IDATA的地址沖突改了反而容易把中斷向量頂?shù)簟?. 白平衡校準(zhǔn)與三處易錯點(diǎn)定位5.1 黑白兩點(diǎn)標(biāo)定與歸一化公式動態(tài)最大值歸一化能扛住光源強(qiáng)度波動但扛不住光源色溫偏移。白熾燈偏黃、LED偏藍(lán)同樣一張白紙在不同光源下讀到的RGB比例會變。如果要讓顏色名稱穩(wěn)定我會上電后先做黑白兩點(diǎn)校準(zhǔn)白參考采白紙黑參考采鏡頭蓋或直接把傳感器貼近不透光物體。unsigned int cal_white[3] {200, 200, 200}; /* 白參考 */ unsigned int cal_black[3] {10, 10, 10}; /* 黑參考 */ unsigned char ScaleChannel(unsigned int raw, unsigned int white, unsigned int black) { unsigned long v; if (raw black) return 0; v (unsigned long)(raw - black) * 255 / (white - black); if (v 255) v 255; return (unsigned char)v; }ScaleChannel做的是線性拉伸把黑色讀數(shù)當(dāng)作0白色讀數(shù)當(dāng)作255中間的讀數(shù)按比例映射。這樣處理后即使換了一個色溫完全不同的環(huán)境只要重新校準(zhǔn)白參考和黑參考顏色名稱映射就能保持不變。校準(zhǔn)值建議放在main函數(shù)里在開機(jī)時提示用戶把白紙貼上傳感器延時兩秒后讀取并暫存。5.2 采樣穩(wěn)定不下來怎么辦如果屏幕上的數(shù)值在固定場景下仍然跳變優(yōu)先懷疑三個地方環(huán)境光頻閃、采樣窗口太短、物體距離遠(yuǎn)近變化。熒光燈每半個工頻周期亮度變化一次10ms窗口正好可能橫跨暗段和亮段解決辦法是把窗口從10ms改成50ms讓讀數(shù)覆蓋多個頻閃周期。距離問題更常見TCS3200沒有透鏡傳感器表面和被測物體距離超過2cm后雜散光會明顯抬高三個通道我一般盡量把傳感器貼在物體表面或者用黑色遮光套管固定1cm左右的工作距離。讀數(shù)徹底為零時先檢查OUT有沒有接到P3.4而不是P3.5再確認(rèn)S0/S1組合不是關(guān)斷狀態(tài)。5.3 字符顯示異常的定位順序LCD1602顯示亂碼或白屏按順序排查先量E引腳有沒有下降沿再確認(rèn)P0上拉到VCC的電阻已經(jīng)接好最后看初始化時序里三次0x30是否被精簡成一次。只發(fā)一次0x30的代碼看起來省事但遇到上電慢的液晶屏就會漏同步表現(xiàn)為第一行正常、第二行錯位或者全是方塊。另一個常見問題是RS和E的引腳定義和實(shí)際接線不一致代碼里寫P2^5板上卻接到了P2^4這類問題看代碼看不出直接拿示波器量引腳電平最快。如果只是刷新時新字符覆蓋殘留在寫第二行前先輸出16個空格清行再把顏色名居中就不會出現(xiàn)長短名交替時末尾殘留的情況。本文還有配套的精品資源點(diǎn)擊獲取