控制軟件源碼解析:從溫度采樣到繼電器輸出)
簡(jiǎn)介這份資源是面向工業(yè)自動(dòng)化初學(xué)者與嵌入式開(kāi)發(fā)者的空壓機(jī)控制軟件工程包基于C8051微控制器與C語(yǔ)言實(shí)現(xiàn)重點(diǎn)解決壓縮機(jī)運(yùn)行中溫度、壓力實(shí)時(shí)監(jiān)測(cè)與安全保護(hù)問(wèn)題。包內(nèi)共151個(gè)文件以69個(gè)h頭文件、39個(gè)inc包含文件、12個(gè)c源文件為核心代碼另有obj、lst編譯中間文件、lib庫(kù)文件、uvproj與uvopt工程配置、hex燒錄文件及少量jpg截圖壓縮包約2.66MB結(jié)構(gòu)完整可直接導(dǎo)入Keil環(huán)境編譯研究。軟件通過(guò)ADC采集溫度與壓力傳感器信號(hào)設(shè)定閾值觸發(fā)過(guò)溫保護(hù)與加載卸載調(diào)節(jié)并借助UART、SPI及LCD或LED實(shí)現(xiàn)人機(jī)交互與遠(yuǎn)程數(shù)據(jù)交換還包含F(xiàn)lash讀寫(xiě)、按鍵處理、系統(tǒng)調(diào)度等模塊便于理解實(shí)際項(xiàng)目代碼組織方式。目前已有634人學(xué)習(xí)下載適合希望掌握C8051外設(shè)驅(qū)動(dòng)、傳感器集成與壓縮機(jī)控制邏輯的讀者參考借鑒。1. 空壓機(jī)控制軟件從傳感器采樣到繼電器輸出的完整閉環(huán)空壓機(jī)控制軟件聽(tīng)起來(lái)像是工業(yè)現(xiàn)場(chǎng)才用得上的東西但真把它拆開(kāi)看核心邏輯并不復(fù)雜一顆 C8051 單片機(jī)定時(shí)讀溫度傳感器跟設(shè)定閾值比較決定壓縮機(jī)啟停、風(fēng)扇轉(zhuǎn)停同時(shí)把狀態(tài)碼刷到數(shù)碼管或串口屏上。這套邏輯跑在螺桿機(jī)、活塞機(jī)、渦旋機(jī)的控制板上管的是排氣溫度、油溫、電機(jī)過(guò)載這些要命的參數(shù)。我手上這份源碼包就是一套典型的空壓機(jī)控制器固件C8051F 系列 MCU 打底NTC 熱敏電阻做溫度采集繼電器組驅(qū)動(dòng)接觸器和風(fēng)扇帶 RS485 上傳。適合誰(shuí)看做工業(yè)控制固件的、接非標(biāo)設(shè)備改造的、想拿一套能跑通的 C8051 項(xiàng)目練手的都能從里面摳出能直接復(fù)用的模塊。溫度采樣這塊的玄學(xué)在于 NTC 非線性查表還是算 Steinhart-Hart代碼里怎么處理后面細(xì)說(shuō)。2. 硬件抽象層拆解C8051 外設(shè)初始化與溫度采樣鏈路2.1 系統(tǒng)時(shí)鐘與 ADC 配置C8051F 系列跟 STM32 不一樣它的交叉開(kāi)關(guān)和優(yōu)先權(quán)交叉開(kāi)關(guān)是繞不開(kāi)的坎。這份代碼里系統(tǒng)時(shí)鐘配到 24.5MHz 內(nèi)部振蕩器ADC0 用定時(shí)器 2 溢出觸發(fā)采樣率設(shè)在 1kHz 左右。為什么不用軟件查詢(xún)因?yàn)榭諌簷C(jī)現(xiàn)場(chǎng)干擾大軟件查詢(xún)?nèi)菀撞傻介_(kāi)關(guān)噪聲定時(shí)器觸發(fā)能保證采樣間隔均勻后面做滑動(dòng)濾波才有意義。// 系統(tǒng)時(shí)鐘與ADC初始化C8051F340 void SysClk_Init(void) { OSCICN 0x83; // 使能內(nèi)部振蕩器不分頻24.5MHz CLKSEL 0x00; // 時(shí)鐘源選內(nèi)部振蕩器 while((OSCICN 0x10) 0); // 等待振蕩器穩(wěn)定 } void ADC0_Init(void) { REF0CN 0x03; // 使能內(nèi)部基準(zhǔn)VREF2.4V AMX0P 0x0C; // P3.0作為ADC0正輸入接NTC分壓 ADC0CF 0xF8; // 轉(zhuǎn)換時(shí)鐘系統(tǒng)時(shí)鐘/8增益1 ADC0CN 0x02; // 定時(shí)器2溢出觸發(fā)右對(duì)齊 EIE1 | 0x08; // 使能ADC0轉(zhuǎn)換結(jié)束中斷 AD0EN 1; // 使能ADC0 }這里有個(gè)參數(shù)要盯住ADC0CF 0xF8把 SAR 時(shí)鐘設(shè)成 3MHz 左右24.5MHz 系統(tǒng)時(shí)鐘下轉(zhuǎn)換一次大概 2.5us留足了采樣保持時(shí)間。AMX0P 0x0C選的是 P3.0 通道對(duì)應(yīng) NTC 分壓點(diǎn)。如果你換板子這個(gè)通道號(hào)得跟著原理圖改改錯(cuò)了采回來(lái)就是滿量程或者零別問(wèn)我是怎么知道的。2.2 NTC 溫度換算查表加線性插值NTC 10K 的阻值-溫度曲線是非線性的直接套公式算浮點(diǎn)對(duì) C8051 來(lái)說(shuō)太奢侈。代碼里用的是分段查表加線性插值表按 5℃ 一檔存 ADC 碼值實(shí)際溫度落在兩檔之間就插值。這樣既省 CPU精度也能做到 ±0.5℃ 以?xún)?nèi)對(duì)空壓機(jī)排氣溫度保護(hù)來(lái)說(shuō)夠用了。// NTC溫度查表-20℃到150℃5℃一檔 const unsigned int NTC_Table[] { 3720, 3510, 3280, 3040, 2790, 2540, 2290, 2050, 1820, 1600, 1400, 1220, 1060, 920, 790, 680, 580, 495, 420, 355, 300, 253, 213, 180, 152, 128, 108, 91, 77, 65, 55, 47, 40, 34, 29 }; int Get_Temperature(unsigned int adc_val) { unsigned char i; int temp; // 超出表格范圍直接返回邊界值 if(adc_val NTC_Table[0]) return -20; if(adc_val NTC_Table[34]) return 150; for(i 0; i 34; i) { if(adc_val NTC_Table[i1] adc_val NTC_Table[i]) { // 線性插值溫度 低檔溫度 比例 * 5℃ temp -20 i * 5; temp (int)((long)(NTC_Table[i] - adc_val) * 5 / (NTC_Table[i] - NTC_Table[i1])); return temp; } } return 0; }NTC_Table數(shù)組是按 ADC 碼值從大到小排的因?yàn)闇囟仍礁?NTC 阻值越小分壓點(diǎn)電壓越低ADC 碼值越小。插值那行用long強(qiáng)轉(zhuǎn)是為了防止乘法溢出C8051 的 int 是 16 位(NTC_Table[i] - adc_val) * 5最大能到 18000 多不轉(zhuǎn) long 會(huì)翻車(chē)。這個(gè)表你得根據(jù)自己用的 NTC 型號(hào)重新標(biāo)定B 值不同曲線差很多直接抄表溫度能偏出十幾度。2.3 滑動(dòng)濾波與斷線檢測(cè)ADC 采回來(lái)的原始值不能直接用現(xiàn)場(chǎng)接觸器吸合那一下能干擾出幾十個(gè)碼值的跳動(dòng)。代碼里做了 8 點(diǎn)滑動(dòng)平均同時(shí)判斷采樣值是否在合理范圍內(nèi)超出范圍認(rèn)為傳感器斷線直接報(bào)故障停機(jī)。#define FILTER_LEN 8 unsigned int Adc_Buf[FILTER_LEN]; unsigned char Buf_Index 0; unsigned int Filter_Adc(unsigned int new_val) { unsigned long sum 0; unsigned char i; Adc_Buf[Buf_Index] new_val; Buf_Index (Buf_Index 1) % FILTER_LEN; for(i 0; i FILTER_LEN; i) { sum Adc_Buf[i]; } return (unsigned int)(sum / FILTER_LEN); } // 斷線檢測(cè)ADC值持續(xù)為滿量程或零 bit Check_Sensor_Open(unsigned int adc_val) { static unsigned char open_cnt 0; if(adc_val 4000 || adc_val 20) { open_cnt; if(open_cnt 50) { // 連續(xù)50次異常約50ms open_cnt 0; return 1; // 傳感器斷線 } } else { open_cnt 0; } return 0; }濾波長(zhǎng)度 8 是我試出來(lái)的平衡點(diǎn)再長(zhǎng)響應(yīng)就遲鈍了排氣溫度漲得快的時(shí)候保護(hù)會(huì)滯后。斷線檢測(cè)的閾值 4000 和 20 對(duì)應(yīng)分壓電路的上拉和下拉極限你要是改了分壓電阻這兩個(gè)數(shù)得跟著調(diào)。open_cnt 50是防抖避免干擾脈沖誤報(bào)。3. 控制邏輯實(shí)現(xiàn)閾值比較、狀態(tài)機(jī)與繼電器輸出3.1 壓縮機(jī)啟停狀態(tài)機(jī)空壓機(jī)的啟停不是簡(jiǎn)單的大于開(kāi)小于關(guān)得有回差、有延時(shí)、有故障鎖死。代碼里用了一個(gè)四狀態(tài)機(jī)待機(jī)、啟動(dòng)延時(shí)、運(yùn)行、故障。溫度超過(guò)上限進(jìn)入啟動(dòng)延時(shí)延時(shí)到了才吸合接觸器溫度降到下限以下再延時(shí)停機(jī)防止頻繁啟停把接觸器燒了。typedef enum { STATE_IDLE 0, STATE_START_DELAY, STATE_RUNNING, STATE_FAULT } SystemState; SystemState sys_state STATE_IDLE; unsigned int delay_cnt 0; void Control_Logic(int temp) { switch(sys_state) { case STATE_IDLE: if(temp TEMP_START) { // 超過(guò)啟動(dòng)閾值 sys_state STATE_START_DELAY; delay_cnt 0; } break; case STATE_START_DELAY: delay_cnt; if(delay_cnt START_DELAY_MS) { // 啟動(dòng)延時(shí)到 RELAY_MAIN 1; // 吸合主接觸器 RELAY_FAN 1; // 風(fēng)扇同時(shí)啟動(dòng) sys_state STATE_RUNNING; } if(temp TEMP_START - TEMP_HYST) { // 溫度回落取消啟動(dòng) sys_state STATE_IDLE; } break; case STATE_RUNNING: if(temp TEMP_STOP) { // 低于停機(jī)閾值 RELAY_MAIN 0; RELAY_FAN 0; sys_state STATE_IDLE; } if(temp TEMP_FAULT) { // 超溫故障 RELAY_MAIN 0; RELAY_FAN 0; sys_state STATE_FAULT; } break; case STATE_FAULT: RELAY_MAIN 0; RELAY_FAN 0; // 需要手動(dòng)復(fù)位或斷電重啟才能退出 if(FAULT_RESET_BTN 0 temp TEMP_START) { sys_state STATE_IDLE; } break; } }TEMP_START、TEMP_STOP、TEMP_FAULT這三個(gè)閾值一般設(shè) 85℃、75℃、105℃回差TEMP_HYST給 5℃。啟動(dòng)延時(shí)START_DELAY_MS通常 3000 到 5000ms看電機(jī)功率。故障狀態(tài)必須手動(dòng)復(fù)位這是血淚教訓(xùn)——自動(dòng)復(fù)位的話傳感器真壞了會(huì)反復(fù)啟停把機(jī)頭干廢。3.2 繼電器輸出與互鎖繼電器輸出這塊有個(gè)坑C8051 的 IO 口上電默認(rèn)是高阻如果繼電器驅(qū)動(dòng)電路是低電平有效上電瞬間會(huì)全部吸合一下。代碼里在初始化最前面就把所有繼電器口置高硬件上再加下拉電阻。// 繼電器引腳定義低電平有效 sbit RELAY_MAIN P1^0; // 主接觸器 sbit RELAY_FAN P1^1; // 風(fēng)扇 sbit RELAY_ALARM P1^2; // 報(bào)警輸出 void Relay_Init(void) { P1MDOUT | 0x07; // P1.0-P1.2推挽輸出 P1 | 0x07; // 先全部置高繼電器不動(dòng)作 RELAY_MAIN 1; RELAY_FAN 1; RELAY_ALARM 1; } // 互鎖風(fēng)扇故障時(shí)禁止啟動(dòng)主電機(jī) bit Check_Interlock(void) { if(RELAY_FAN 0 FAN_FEEDBACK 1) { return 0; // 風(fēng)扇沒(méi)轉(zhuǎn)起來(lái)不允許啟動(dòng)主機(jī) } return 1; }P1MDOUT設(shè)推挽是為了驅(qū)動(dòng)能力夠直接推光耦或者三極管?;ユi邏輯里FAN_FEEDBACK是風(fēng)扇接觸器的輔助觸點(diǎn)沒(méi)有這個(gè)反饋風(fēng)扇皮帶斷了主機(jī)還在轉(zhuǎn)機(jī)頭幾分鐘就過(guò)熱。這個(gè)互鎖在代碼里是硬判斷不經(jīng)過(guò)狀態(tài)機(jī)任何情況下風(fēng)扇沒(méi)反饋都不讓主機(jī)吸合。3.3 故障碼存儲(chǔ)與串口上傳故障碼存在片內(nèi) Flash 里掉電不丟。C8051F340 有 4KB 的 Flash 可以當(dāng) EEPROM 用代碼里劃了最后 512 字節(jié)存故障記錄每條記錄 8 字節(jié)循環(huán)覆蓋。#define FAULT_BASE 0xF000 // 故障記錄起始地址 #define FAULT_MAX 64 // 最多存64條 typedef struct { unsigned char code; // 故障碼 int temp; // 故障時(shí)溫度 unsigned int runtime; // 累計(jì)運(yùn)行時(shí)間 unsigned char reserved; } FaultRecord; void Save_Fault(unsigned char code, int temp) { unsigned char i; FaultRecord rec; unsigned int addr; rec.code code; rec.temp temp; rec.runtime total_runtime; // 找空位或覆蓋最舊 for(i 0; i FAULT_MAX; i) { addr FAULT_BASE i * sizeof(FaultRecord); if(Flash_Read_Byte(addr) 0xFF) { break; } } if(i FAULT_MAX) i 0; // 滿了從頭覆蓋 addr FAULT_BASE i * sizeof(FaultRecord); Flash_Write(addr, (unsigned char *)rec, sizeof(FaultRecord)); }Flash_Write是底層擦寫(xiě)函數(shù)C8051 寫(xiě) Flash 前必須先擦除整個(gè)扇區(qū)所以實(shí)際代碼里是先把整個(gè)扇區(qū)讀到 RAM改完再擦再寫(xiě)回去。這個(gè)操作耗時(shí)大概 20ms期間不能斷電否則記錄全丟。我一般會(huì)在寫(xiě)之前先把看門(mén)狗喂了再關(guān)中斷寫(xiě)完再開(kāi)。4. 避坑與排查C8051 空壓機(jī)控制器的常見(jiàn)翻車(chē)點(diǎn)4.1 溫度跳變導(dǎo)致頻繁啟?,F(xiàn)象是空壓機(jī)運(yùn)行幾分鐘就停停了沒(méi)一會(huì)又啟接觸器啪啪響。原因多半是 NTC 采樣線跟電機(jī)線走在一起了變頻器或者接觸器線圈的干擾耦合進(jìn)來(lái)ADC 值跳變超過(guò)回差。解決分兩步硬件上把 NTC 線換成屏蔽線屏蔽層單端接地軟件上把滑動(dòng)濾波長(zhǎng)度從 8 加到 16同時(shí)在狀態(tài)機(jī)里加一個(gè)“溫度必須連續(xù) 3 次超過(guò)閾值才動(dòng)作”的判斷。4.2 上電繼電器瞬間吸合前面提過(guò)C8051 的 IO 上電默認(rèn)高阻如果驅(qū)動(dòng)電路是低有效上電瞬間繼電器會(huì)抖一下?,F(xiàn)象是每次上電空壓機(jī)“啪”一聲接觸器吸合又?jǐn)嚅_(kāi)。解決是在main函數(shù)最開(kāi)頭、任何外設(shè)初始化之前先把繼電器口置高同時(shí)硬件上加 10K 下拉電阻到地。代碼順序不能反先配P1MDOUT再置高否則推挽沒(méi)開(kāi)置高也沒(méi)驅(qū)動(dòng)能力。4.3 Flash 寫(xiě)故障記錄導(dǎo)致看門(mén)狗復(fù)位C8051 擦寫(xiě) Flash 的時(shí)候 CPU 會(huì)停頓如果看門(mén)狗沒(méi)處理直接復(fù)位?,F(xiàn)象是每次存故障記錄就重啟故障碼還存不進(jìn)去。解決是在Flash_Write前后加WDT_Feed()并且把看門(mén)狗超時(shí)設(shè)到最長(zhǎng)。更穩(wěn)妥的做法是存故障前先關(guān)看門(mén)狗存完再開(kāi)但這樣萬(wàn)一寫(xiě) Flash 卡死就沒(méi)救了我一般還是喂狗。4.4 RS485 通信在電機(jī)啟動(dòng)時(shí)丟包現(xiàn)象是空壓機(jī)一啟動(dòng)上位機(jī)就報(bào)通信中斷。原因是 RS485 收發(fā)器跟電機(jī)共地地電位跳變導(dǎo)致差分信號(hào)超范圍。解決是給 RS485 加隔離模塊或者至少把 A/B 線換成雙絞屏蔽線屏蔽層接大地。軟件上把通信超時(shí)從 1s 放寬到 3s給重傳留時(shí)間。4.5 溫度查表精度不夠有人直接抄了 10K B3950 的表結(jié)果實(shí)際用的 NTC 是 B3435溫度差出 8℃ 以上?,F(xiàn)象是排氣溫度顯示 95℃實(shí)際才 87℃保護(hù)提前動(dòng)作。解決是拿恒溫槽標(biāo)定從 20℃ 到 120℃ 每隔 10℃ 記 ADC 值重新生成表格。沒(méi)有恒溫槽的話至少用冰水混合物和沸水標(biāo)兩個(gè)點(diǎn)反推 B 值修正。5. 二次開(kāi)發(fā)改造從單機(jī)控制到多機(jī)聯(lián)控的擴(kuò)展思路拿到這份源碼想改造成自己的東西最直接的路子是先把溫度采樣和繼電器輸出這兩個(gè)模塊吃透然后按自己的硬件改引腳和閾值。但如果你要做多臺(tái)空壓機(jī)聯(lián)控就得動(dòng)架構(gòu)了。我一般會(huì)這么干把單機(jī)控制邏輯封裝成一個(gè)獨(dú)立的任務(wù)函數(shù)主循環(huán)里用時(shí)間片輪詢(xún)每臺(tái)機(jī)器一個(gè)狀態(tài)機(jī)實(shí)例通過(guò) RS485 或者 CAN 交換啟停請(qǐng)求和故障狀態(tài)。// 多機(jī)聯(lián)控的調(diào)度框架 #define MAX_UNITS 4 typedef struct { SystemState state; int temp; unsigned int delay_cnt; unsigned char fault_code; unsigned char addr; // 從站地址 } CompressorUnit; CompressorUnit units[MAX_UNITS]; void Main_Loop(void) { unsigned char i; static unsigned int tick 0; for(i 0; i MAX_UNITS; i) { // 每10ms調(diào)度一臺(tái)4臺(tái)輪一圈40ms if((tick % MAX_UNITS) i) { units[i].temp Get_Temperature( Filter_Adc(ADC_Read(units[i].addr))); Control_Logic_Unit(units[i]); } } tick; // 聯(lián)控邏輯根據(jù)總管壓力決定啟停臺(tái)數(shù) if(tick % 100 0) { Cascade_Control(units, MAX_UNITS); } }這個(gè)框架的關(guān)鍵是把Control_Logic改成帶實(shí)例參數(shù)的版本所有狀態(tài)變量從全局挪到結(jié)構(gòu)體里。Cascade_Control負(fù)責(zé)讀總管壓力壓力低于下限就按運(yùn)行時(shí)間最短的優(yōu)先啟動(dòng)高于上限就按運(yùn)行時(shí)間最長(zhǎng)的優(yōu)先停。輪詢(xún)周期 40ms 對(duì)空壓機(jī)來(lái)說(shuō)綽綽有余溫度變化是秒級(jí)的。改造時(shí)驗(yàn)證方法很簡(jiǎn)單先用信號(hào)發(fā)生器模擬 NTC 分壓從 0.5V 掃到 2.0V看每臺(tái)機(jī)器的狀態(tài)切換和繼電器動(dòng)作是否跟單機(jī)版一致。然后接實(shí)際傳感器用熱風(fēng)槍加熱看聯(lián)控邏輯能不能正確加減載。最后跑 72 小時(shí)老化重點(diǎn)盯 Flash 故障記錄有沒(méi)有丟、RS485 有沒(méi)有累積錯(cuò)誤。從那以后我每次改完控制邏輯都強(qiáng)制走一遍“模擬傳感器全量程掃描 繼電器動(dòng)作記錄 故障注入”這三步少一步都不敢上機(jī)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取