DIY製作示波器的超詳細教程:(三)給你的電路注入靈魂——程序設計

沒有程序那一堆堆電路起不到任何作用,完全是一堆廢板子!就像一臺沒有操作系統的電腦一樣,只能廢電。程序設計是整個示波器製作中的難點,本文將詳細講解程序的設計。

該示波器中的程序全部是用 c 語言編寫的,開發環境爲 CodeVisionAVR C,原程序在附件中,下面就各個重要的子程序的設計一一敘述,其它程序見原程序。MCU2 MCU1 程序流程圖分別見圖 1 和圖 2

1.同步觸發的軟實現

細心的朋友會發現這個示波器電路中缺少一部分電路,就是硬件觸發電路,爲了降低電路的複雜性我在做這個示波器時沒有做這個電路,而是用軟件實現同步觸發的,這樣做有個弊端就是幾乎無法實現單次觸發,因爲我基本不用這個功能,需用這個功能的朋友只需在程控放大器部分加上一個由高速比較器構成的遲滯比較器然後將輸出端接到一個外部中斷的輸入口即可。當然程序和電路就要做相應的變化,這裏就不多講了。軟件觸發的好處是觸發條件更易調整,只需調整比較語句中的參數即可。保證可以用軟件觸發的條件是要有足夠大的存儲空間,顯示一屏的數據爲 240 個,但每次讀進單片機的數據爲 500 個,多餘 260 個數據就是作爲不滿足觸發條件的捨棄餘量,爲了以防萬一,當從 500 個數據中以經讀出超過260 個數據但還沒有符合觸發條件的數據時,將跳出觸發比較循環,重新從 FIFO 存儲器中讀出 500 個數據,因爲 FIFO 存儲器爲 4K 容量,最多可以這樣重複讀取 8 次數據,所以軟觸發可以非常穩定的工作,在該示波器的 MCU2 中控制觸發的語句見以下程序段:

read: 
for(i=0;i<500;i++) //從 FIFO 存儲器中讀 500 個數據
{ 
 FIFO_R=0; 
add[i]=FIFO_bus; 
FIFO_R=1; 
} 
while(!(add[q]<=m&&add[q+1]>=m)) //滿足幅度爲 m 且爲上升沿則觸發
 { 
 q+=1; 
 if(q>=260) //若存儲數據不足則重新讀數據
goto read; 
 }

程序的意思是隻有當此時採樣信號的數值是 m 且爲上升沿時纔可以觸發,改變觸發沿只需改變運算符,改變觸發電壓只需改變 m 的值即可,m 的取值範圍是 0~255

2. 從採樣數據中測信號峯峯值

本示波器就能夠測量輸入信號電壓的峯峯值,並顯示在屏幕上。這個功能由峯峯值測量子程序完成,見下面的程序段。

在程序開始時給a中賦值128,即基線電壓值。因爲一屏幕的顯示數據爲240個,所以用for()循環將if…else…判斷語句執行240次,在a中存放最大值,在b中存放最小值。對每個數據進行比較,如果該數據比a大則將這個數據存入a,如果小於a,則將這個數據與b進行比較,比b大則拋棄,比b小則存入b。故當240個循環執行完後a中存放的是這一屏幕數據中的最大值,b中存放的是這一屏幕顯示數據中的最小值。在比較完後用a減去b,得到差值存入c中,則c中保存的值就是電壓的峯峯值,調用電壓計算顯示子程序根據當前的垂直靈敏度給c乘以不同的倍數,得到實際的峯峯值。當前垂直靈敏度的判斷由一個switch()選擇結構完成。biao寄存器中的數據是當前的垂直靈敏度,case 4:後面沒有運算是因爲程控放大器在此狀態下的放大倍數爲1,即沒有放大也沒有衰減。

在計算完峯峯值後,設置LCD顯示器,使其工作在文本模式(因爲只有在文本模式下對字庫的調用纔有效),然後設置屏幕上顯示電壓峯峯的座標(對該LCD模塊的控制是先送命令,後送參數。例如設置X座標“SdCmd(0x60);SdCmd(30);”中,第一個SdCmd()送的0x60是設置X座標的命令,第二個SdCmd()送的30是X軸的座標,其他設置相同。具體見光盤中LCD顯示屏的資料。),在設置完LCD後約定顯示格式,小數點後保留2爲有效數字,顯示單位爲Vpp,顯示完畢後需重新設置LCD工作狀態使其工作在圖形模式用於波形顯示。

a=128; 
for(i=0;i<240;i++) //取數據中的最大值與最小值 
 { 
 if(add[i]>a) 
 { 
 a=add[i]; 
 } 
 else if(add[i]<b) 
 { 
 b=add[i]; 
 } 
 } //取得最大值存於a中,最小值存於b中 
 c=a-b; //取差值存入c中 
 if(e>5) //避免頻繁換數據 
 { 
 disp_volt(); //調用電壓值計算顯示子程序 
 e=0; 
 } 
 e++; 
void disp_volt() //電壓值計算顯示子程序 
{ 
c=c*0.667; 
 switch(biao)//根據不同的垂直靈敏度計算峯峯值 
 { 
 case 0:c=c*25;break; 
 case 1:c=c*10;break; 
 case 2:c=c*5;break; 
 case 3:c=c*2.5;break; 
 case 4:;break; 
 case 5:c=c*0.5;break; 
 case 6:c=c*0.25;break; 
 case 7:c=c*0.1;break; 
 case 8:c=c*0.05;break; 
 default:break; 
 } 
SdCmd(0x00);SdCmd(0xcd); //設置WLCR 寄存器,使LCD工作與顯示文本狀態 
SdCmd(0xf1);SdCmd(0x1f); //字型水平、垂直方向各放大2 倍 
SdCmd(0x60);SdCmd(30); //設置顯示X座標 
SdCmd(0x70);SdCmd(50); //設置顯示Y座標 
sprintf(lcd_buffer,"%3d.%02dVpp",c/100,c%100); //約定顯示格式,小數點後保留兩位
ShowText(lcd_buffer); //有效數字
SdCmd(0x00);SdCmd(0xc5); //設置WLCR 寄存器,使LCD工作於圖形顯示模式 
}

3.將採樣數據轉換成顯示數據

LCD顯示屏爲320×240點陣的圖形顯示模塊,內置RA8803 控制器。模塊不僅可以顯示單一的文本、圖形,而且可以實現雙圖層的(“或”、“異或”、“同或”、“與”四種邏輯關係)合成顯示。在本示波器中顯示格線與波形是在不同的層上顯示,顯示關係爲“或”,畫方格線的程序見原程序,比較簡單就不多說了,着重解釋一下如何將採樣數據轉換成顯示數據。

顯示屏的地址結構見圖3,由圖可知對顯示數據的操作最小單位爲字節,因爲Mega32的內存爲2K字節,顯示波形的區域爲240*240,顯示一屏波形所需處理的數據爲7.2K,故Mega32不可能同時處理一屏波形的全部數據,所以將一屏波形按字節分爲30列,每次處理一列,處理完後直接顯示,然後處理下一列。將AD轉換所得的數據作爲給LCD顯示器寫數據的列地址,因爲一列數據位240字節,所以定義一個容量爲240字節的數組lcd_buffer[240],lcd_buffer[]在初始時數據全爲00H,因爲每次對數據的操作至少是一個字節,而每次處理數據處理的是所顯示一個點,所以對每列數據處理8次,定義一個變量m,在一列數據處理之前將其賦值爲m=10000000B,處理該列第1個點時讓該點垂直地址所對應的數組中的數據(00H)與m相或並將結果存入數組,再將變量m右移一位,即m=01000000B。讓第2點垂直地址所對應的數組中的數據與m相或並將結果存入數組,再將變量m右移一位,即m=00100000B ……,這樣直到一列數據中的8個點全處理完,重新給m賦值爲m=10000000B,然後送顯示。爲了有較好顯示效果,將顯示相鄰的點用線連接起來,在處理第一個點時預讀出第二個點的垂直座標,與第一個點的垂直座標進行比較,如果比第一個點的垂直座標小則從第一個點向第二個點拉線,如果比第一個點的垂直座標大則從第二個點向第一個點拉線。

具體程序如下所示:

for(j=0;j<30;j++) //將一屏數據分爲30列 
 { 
 m=0b10000000; // 
 for(i=j*8;i<(j+1)*8;i++) //處理每列中的8個點 
 { 
 k=add[i]; // 讀出採樣數據作爲垂直座標 
 lcd_buffer[k]=(lcd_buffer[k]|m); //讓該座標對應數據與m相或並原位保存 
 if(add[i+q]<add[i+q+1]) //判斷拉線方向 
 { 
 for(k=add[i+q];k<add[i+q+1];k++) 
{ 
lcd_buffer[k]=(lcd_buffer[k]|m); 
} 
 } 
 else 
 { 
 for(k=add[i+q];k>add[i+q+1];k--) 
{ 
lcd_buffer[k]=(lcd_buffer[k]|m); 
} 
 } 
 m>>=1; //將m的值右移一位 
 } 
 for(h=0;h<240;h++) //送顯示 
 { 
 SdCmd(0x60);SdCmd(j); //設置顯示X座標 
 SdCmd(0x70);SdCmd(h); //設置顯示Y座標 
 SdData(lcd_buffer[h]); //傳送顯示數據 
 lcd_buffer[h]=0; //將已送出數據的存儲器單元清零 
 } 
}

4.用MCU1頻率測量

用Mega8測量頻率使用了其中的兩個計數器/定時器。設置TCCR1B=6使16位計數器/定時器T/C1工作在計數器方式,對外部T1(PD5)引腳輸入的脈衝信號進行計數(下降沿觸發)。 設置TCCR2=15使T/C2工作在CTC模式,內部時鐘1024分頻(16M/1024=15.625KHz),設置 OCR2=124使中斷時間爲(124+1)/15.625=8ms,在低水平掃速時每隔8ms中斷一次,在高水平 掃速時通過重新設置TCCR2=14則每隔2ms中斷一次,在這裏以低水平掃速時爲例,每次T/C2 的中斷中都首先記錄下T/C1寄存器TCNT1當前的計數值,因此前後兩次寄存器TCNT1的差值 (time1_new-time1_old)或(65536-time1_old+time1_new)就是8ms時間內T1引腳輸入的 脈衝個數,爲了提高測量精度程序對125個8ms內的脈衝個數進行累計,將累計值存入變量 freq中,即可知限定時間爲1s內有多少個脈衝,這樣就將T1腳上的脈衝頻率測量出來了,而 T1腳上的頻率是經過4分頻後的所以真正的頻率是將測量的頻率的4倍。具體程序如下所示:

interrupt [TIM2_COMP] void timer2_comp_isr(void) 
{ 
 time1_new = TCNT1; // 8ms到,記錄當前T/C1的計數值 
time_8ms_ok = 1; 
} 
void time() 
{ 
 if (time_8ms_ok) 
 { // 累計T/C1的計數值 
 if (time1_new >= time1_old) freq = freq + (time1_new – time1_old); 
 else freq = freq + (65536 – time1_old + time1_new); 
 time1_old = time1_new; 
 if (++freq_time >=125) 
 { 
 freq_time = 0; // 1s到, 
 freq_to_spibuff();// 將1s內的脈衝數送計算並通過SPI發送 
 freq = 0; 
 } 
 time_8ms_ok = 0; 
 } 
}

5.將兩個單片機聯繫起來

 

將兩個單片機聯繫起來就是實現兩個單片機之間的通信,在這裏實際就是讓 MCU1 控制 MCU2,爲了完成這一功能所以使用 SPI 通信。

首先介紹一下 SPI 的通信協議:

 

SPI(串行外設接口)總線系統是一種同步串行外設接口,允許 MCU 與各種外圍設備以串行方式進行通信、數據交換,廣泛應用於各種工業控制領域。基於此標準,SPI 系統可以直接於各個廠家生產的多種標準外圍器件直接接口。SPI 接口通常包含有 4 根線:串行時鍾(SCK)、主機輸入/從機輸出數據線(MISO)、主機輸出/從機輸入數據線(MOSI)和低電平有效的從機選擇線 SS。在從機選擇線 SS 使能的前提下,主機的 SCK 脈衝將在數據線上傳輸主/從機的串行數據。主/從機的典型連接圖如圖 4 所示:

串行外設接口 SPI 允許 AVR 單片機和外設之間進行高速的同步數據傳輸。AVR 單片機 SPI的特點如下:全雙工,3 線同步數據傳輸,主/從機操作,LSB 首先發送或 MSB 首先發送,7 種可編程的比特率,傳送中斷結束,寫碰撞標誌檢測,可以從閒置模式喚醒。SPI 主機-從機的互連如圖 5 所示,系統包括兩個移位寄存器和一個主時鐘發生器。通過將需要的從機的 SS 引腳拉低,主機啓動一次通信過程。主機和從機將需要的數據放到相應的移位寄存器,主機在 SCK 引腳上產生時鐘脈衝以交換數據。主機的數據從 MOSI 移出,從從機 MISO 移入。從機的數據從 MISO 移出,從從機 MOSI 移入。主機通過將從機的 SS 拉高實現與從機的同步。

下面將介紹 SPI 的幾個特殊寄存器:

1SPI 的控制寄存器—SPCR

SPIE SPI 中斷使能,置位後,只要 SPSR 寄存器的 SPIF SREG 寄存器的全局中斷使能位置位,就會引發 SPI 中斷。SPE 置位將使能 SPIDORD 置位時數據的 LSB 首先發送;否則數據的 MSB 首先發送。MSTR 置位時選擇主機模式,否則爲從機。CPOL 置位表示空閒 SCK 爲高電平;否則空閒時 SCK 爲低電平。CPHA 決定數據是在 SCK 的起始沿採樣還是在 SCK 的結束沿採樣。通過對 SPR1SPR0 進行設計,確定主機的 SCK 速率。

2SPI 的狀態寄存器—SPSR

SPIF 爲中斷標誌位,串行發送結束後,SPIF 置位。若此時寄存器 SPCR SPIE 和全局中斷使能位置位,SPI 中斷即產生。進入中斷例程後 SPIF 將自動清零。在發送當中對 SPI 數據寄存器 SPDR 寫數據將置位 WCOLSPI2X 置位後 SPI 的速度加倍。

3SPI 的數據寄存器—SPDR

SPDR 數據寄存器爲讀/寫寄存器,用來在寄存器文件 SPI 移位寄存器之間傳輸數據。寫寄存器將啓動數據傳輸,讀寄存器將讀取寄存器的接收緩衝器。SPI 系統的發送方向只有一個緩衝器,而在接收方向有兩個緩衝器。也就是說,在發送時一定要等到移位過程全部結束後才能對 SPI 數據寄存器執行寫操作。而在接收數據時,需要在下一個字符移位過程結束之前通過訪問 SPI 數據寄存器讀取當前接收到的字符。否則第一個字節將丟失。

本示波器中只用 MCU1 控制 MCU2,所以 MCU1 只用於發送控制數據,而 MCU2 只用於接收控制數據,所以將 MCU1 配製成 SPI 主機,將 MCU2 配製成 SPI 從機即可。在實 際的程序設計中由於 MCU1 啓動 SPI 通信是在中斷服務程序中完成,所以在執行完後相應寄存器會被清零,導致數據錯誤,所以 MCU1 並沒有使用其中的 SPI 控制器,而是使用一個子程序模擬 SPI 通信,解決了控制寄存器被清零的問題。MCU2 則使用了本身的 SPI 制器進行數據接收。具體程序見以下程序段:

1MCU1 模擬 SPI 主機程序段:

spi_out()爲 SPI 發送子程序,帶有參數 j,即 j 爲要發送的數據,發送數據時先拉低ss,讓從機開始接收數據,然後用 for()循環將數據按由左至右的順序(即高位先發送) 發送給從機,具體方法是將 j 與 0b10000000 相與,屏蔽低 7 位,是 1 則將 dat 拉高,否則 置低,然後拉高 clk,延時 1us 再置低 clk,模擬時鐘信號,再將 j 左移一位,再與 0b1000 0000 相與,然後判斷髮送……直到 8 位數據發送完畢,拉高 ss 告訴從機數據發送完畢進行 數據存儲。發送數據時約定數據格式,即兩個單片機之間的通信協議:每次發送 9 個字節, 前 4 個字節是測得的頻率數據,且高位在前;第 5 個字節爲垂置靈敏度數據;第 6 個字節爲 觸發控制數據;第 7 個字節爲同步控制數據;第 8 個字節爲水平掃速數據;第 9 個字節爲功 能複用鍵的當前功能標誌。從機再接收到數據後按照這樣的順序對數據進行處理,實現相應 的功能。spi_out()這個子程序還可以用於其他需要 SPI 控制的芯片,只需在調用前對 IO 口進行定義即可。

spi_out(unsigned char j) 
{ 
 unsigned char u; 
 ss=0; 
 for(u=0;u<8;u++) 
 { 
 if(j&0b10000000) { dat=1; } 
 else {dat=0;} 
 delay_us(1); 
 clk=1; 
 delay_us(1); 
 clk=0; 
 delay_us(1); 
 j<<=1; 
 } 
 delay_us(1); 
 ss=1; 
} 
void freq_to_disbuff() 
{ if(fr==0) 
 { 
 freq=freq*4; 
 } 
 eep=freq>>24;//取頻率高 8 位 
 spi_out(eep); 
 delay_us(10); 
 eep=(freq>>16)&0xff; 
 spi_out(eep); 
 delay_us(10); 
 eep=(freq>>8)&0xff; 
 spi_out(eep); 
 delay_us(10); 
 eep=freq&0xff;//取頻率低 8 位 
 spi_out(eep); 
 delay_us(10); 
 spi_out(w[i]); //垂直靈敏度數據 
 delay_us(10); 
 spi_out(tri); //觸發數據 
 spi_out(hold); //同步數據 
 delay_us(10); 
 spi_out(kr); //掃速數據 
 delay_us(10); 
 spi_out(zhi); //複用鍵功能標誌數據 
 delay_us(10); 
 }

2MCU2 從機 SPI 程序段:

init_spi()函數是將 MCU2 配製成 SPI 從機,每接收一個字節的數據中斷一次,中斷服務程序中將接收到的數據存入數組,並將數組地址加 1,然後判斷 9 個字節是否接收完畢, 若沒接收完則繼續等待接收,接收完後則將數據按約定格式處理顯示。大家可以根據自己的 需求改變這些格式爲其增加新的功能。

void init_spi() //SPI 初始化子函數
{ 
DDRB.7=0; 
PORTB.7=1; 
DDRB.5=0; 
PORTB.5=1; 
DDRB.4=0; 
PORTB.4=1; //將 SPI 端口設置成輸入
SPCR=0b11000101; //設置 SPI 爲從機
SPSR=0X00; //清零 SPSR 寄存器
} 
interrupt[SPI_STC] void spi_isr(void) //SPI 接收數據中斷
{ 
 if(j!=9) 
 { 
 i[j]=SPDR; //將接收到的數據存入數組
 j++; //給數組地址加 1 } 
} 
 if(j>=9) //判斷 9 個字節數據是否接受完畢
 { eep=i[0]; 
 freq=eep; 
 freq=freq<<8; 
 eep=i[1]; 
 freq=freq|eep; 
 freq=freq<<8; 
 eep=i[2]; 
 freq=freq|eep; 
 freq=freq<<8; 
 eep=i[3]; 
 freq=freq|eep; //將頻率值整和到 freq 寄存器
 disp_freq(); //顯示頻率值
 eep=i[4]; 
 disp_volt(); //顯示垂直靈敏度
 tri=i[5]; //顯示觸發方式
 hold=i[6]; //顯示同步
 biao=i[7]; //顯示掃速
 disp_time(); 
 zhi=i[8]; //顯示覆用鍵當前功能
 disp_cond(); 
 j=0;}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章