爲樹莓派添加一個強實時性前端[原創cnblogs.com/helesheng]

     樹莓派是最近流行嵌入式平臺,其自由的開源特性以及低廉的價格,吸引了來 自全球的大量極客和計算機大咖的關注。來自各大樹莓派社區的幕後英雄,無私地在這個開源硬件平臺上作了大量的工做,將其打形成了世界上通用性最好,也最自由的計算機學習平臺之一。我本人感興趣的學習主題是Linux操做系統和Python編程,在流連於各大樹莓派社區向各位大神學習的過程當中感受獲益良多。結合本身擅長的實時信號處理工做,也作了一些小小的嘗試。不能說作了什麼首創性工做,希望意分享給各位後來者。如下原創內容歡迎網友轉載,但請註明出處:cnblogs.com/helesheng前端

1、樹莓派Raspbian系統的實時性python

   Raspbian是樹莓派最經常使用的Debian Linux操做系統,也是樹莓派官方推薦的系統。這個系統集成了Debian系統的良好看操做性和易用性,具備很是成熟的開源支持。但Linux系統內核並不是實時操做系統,在對系統硬件進行操做時很難保證系統的實時性。git

用如下shell命令安裝Python GPIO,對其實時性進行測試。算法

sudo apt-get install Python-dev
sudo apt-get install Python-rpi.gpio
樹莓派安裝Python GPIO

測試儀器是邏輯分析儀,簡單鏈接BCM模式下的#17號引腳以下圖所示。shell

圖1 用邏輯分析儀分析Raspbian的實時性編程

注:若是不清楚樹莓派GPIO的引腳位置能夠經過在Linux終端輸入指令:gpio readall 來查詢BCM和wirePi模式下引腳的位置。數組

編輯如下簡單Python測試腳本:緩存

 1 #coding: utf-8
 2 import RPi.GPIO as GPIO
 3 import time
 4 GPIO.setmode(GPIO.BCM) #引腳採用BCM編碼
 5 GPIO.setup(17, GPIO.OUT) #將對應的GPIO配置爲輸出
 6 DLY_TM = 0.001#延遲時間單位爲秒
 7 try:
 8     while True:
 9         GPIO.output(17,GPIO.HIGH)
10         time.sleep(DLY_TM)
11         GPIO.output(17,GPIO.LOW)
12         time.sleep(DLY_TM)
13 except KeyboardInterrupt:
14         print("It is over!")
15         GPIO.cleanup()
樹莓派Python實時性測試代碼

用邏輯分析儀測試#17引腳輸出的波形如左下圖所示。數據結構

            

圖2a Python代碼輸出的1ms延時波形                                         圖2b Python代碼輸出的100us延時波形多線程

 

   由上圖可知,實際的延遲時間爲1.08ms(紫色標籤M1和黃色標籤M2之間的時間差),實時偏差約爲80us。

   將上面代碼中的延遲時間DLY_TM改成0.0001(100us),測試結果以下圖。可見實際的延遲時間爲180us,實時偏差仍爲約爲80us。

   這個80us的延時偏差應該是由Linux內核調度器和Python解釋器共同形成的,很難進一步下降。且上述測試是在樹莓派空載狀況下進行的——當Linux內核調度更多線程時這個延遲時間不但將進一步增長,並且可能變成一個隨機時間。

   80us數量級的實時偏差,對於控制自動小車、3D打印機這類應用已經綽綽有餘,但對於須要精確控制時間的任務顯然是不夠的。

   因爲Python具備很是強大的數字信號處理能力,但樹莓派不含有A/D轉換器,我決定爲樹莓派添加一個強實時性的高速A/D,D/A轉換裝置,在樹莓派上實現Python實時數字信號處理

   根據孔徑(Aperture Jitter)抖動理論,兩次採樣間時間間隔的隨機變化,將形成A/D和D/A轉換信噪比(SNR)和有效分辨率(ENOB)的下降,這種採樣間隔之間的隨機變化稱爲孔徑抖動。這裏計劃爲樹莓派設計一個轉換率爲1MSPS,包含和A/D和D/A轉換功能,辨率爲12bits的模擬前端。根據孔徑抖動和信噪比之間的計算公式[1]: 

    其中tj是孔徑抖動時間。根據上式獲得採樣頻率、信噪比和要求的孔徑抖動之間的關係圖[2]

 

圖3 採樣頻率、信噪比和孔徑抖動的關係 

     由上圖可知爲達到1MSPS下10~12bits的有效分辨率ENOB(或60Db以上的信噪比),應將孔徑抖動時間控制在100ps如下,遠遠小於樹莓派(運行Linux系統條件下)可以提供的80us的時間分辨率,爲此必須採用實時性更強的模擬前端控制器。

2、整體設計思路

   常見的實時控制方案有MCU和FPGA兩種,FPGA實時性最好,但開發難度較大,成本也高,與樹莓派的開源和低成本精神不徹底吻合,比較合理的方案是用MCU實現。但若是採用傳統的MCU定時器軟件中斷法來實現轉換定時控制,則定時精度受中斷服務程序入口的影響,孔徑抖動在MCU指令週期數量級。以72MHz的STM32F103系列爲例,定時器中斷法產生的孔徑抖動在1/72MHz≈13.9ns數量級,遠高於12bits@1MSPS的A/D和D/A轉換要求。但STM32爲它的ADC模塊提供了強有力的DMA支持,DMA對轉換結果的轉存不受指令影響,能夠實現極佳的採樣定時控制,將孔徑抖動下降到1ns如下。

   採用STM32做爲實時模擬前端的控制器,還要實現樹莓派和STM32之間的數據交互——樹莓派發送數據給STM32來進行D/A轉換;接收STM32進行A/D轉換的結果。樹莓派擴展接口提供了GPIO、SPI、I2C(SMBUS)等幾種接口,爲下降傳輸延遲我採用了速度最快的SPI接口來鏈接STM32實時前端。傳輸過程當中樹莓派做爲SPI主機,用戶經過用戶界面驅動SPI口發起通訊;STM32做爲SPI從機被動進行通訊,以上傳A/D轉換結果和接受D/A轉換數據。

   當樹莓派不發起通訊的時候,STM32經過DMA1通道1不停地將轉換結果寫入其內部RAM中的A/D轉換循環緩衝區中,同時不斷地將D/A循環緩衝區中的數據從D/A轉換器中輸出。當樹莓派接收到用戶命令進行通訊時,首先經過GPIO通知STM32。STM32在收到命令後,找到A/D緩衝區最後放入循環隊列中的數據,並將整個隊列中的數據按時間順序搬運到發送緩衝區,再經過GPIO告訴樹莓派「能夠開始通訊了」。樹莓派在收到STM32發來的確認信息後發起連續的SPI通訊,一方面經過MOSI引腳將但願D/A轉換器轉換的數據隊列發送給STM32,另外一方面從MISO口接收STM32發送緩衝區中的A/D轉換數據。其結構框圖以下圖所示。

 

圖4 樹莓派和實時性前端功的能框圖

    根據上述思路,我設計了下圖左側所示的PCB:模擬信號從最左側的單排針接插件進入;STM32的SPI和GPIO接口則經過下圖中部的標準的樹莓派擴展接口鏈接到樹莓派上。其中STM32使用了集成A/D和D/A轉換器的Cortex-M3系列芯片STM32F103RC。

圖5 樹莓派和實時性前端功的實物圖

3、在樹莓派上用Python NumPy和Matplotlib編寫信號處理算法

   NumPy和Matplotlib是Python上著名的數值計算和圖形擴展庫,提供了豐富而強大的信號處理和顯示功能。其使用方法相似經常使用的Matlab,但幸運的是在開源的Linux和Python世界裏,它們都是免費的!在樹莓派上沒有安裝它們的小夥伴們能夠用如下指令安裝。 

sudo apt-get install python-numpy python-scipy python-matplotlib

   樹莓派上安裝matplotlib極可能因爲缺乏Cario圖形庫沒法運行,若是出現這種狀況請執行如下指令。

    sudo apt-get install python-gi-cairo 

   在Python腳本中以下方式導入上述兩個模塊,就能夠在樹莓派上開心的玩耍數字信號處理了。

import numpy as np
import matplotlib.pyplot as plt

1、產生D/A輸出所需的信號

利用NumPy產生正弦信號的Python腳本以下:

1 index = np.arange(D_LEN)
2 s = 1000*np.sin(2*np.pi*index*2/D_LEN) + 2048;

熟悉Matlab的小夥伴看起來是否是很是親切。還能夠爲D/A產生的信號增長几個高次諧波,將第二句改成:

 1 s = 1000*np.sin(2*np.pi*index*2/D_LEN) + 200*np.sin(2*np.pi*index*20/D_LEN) + 40*np.sin(2*np.pi*index*50/D_LEN) +2048 

最後爲方便Python和實時信號前端的數據傳輸,將s強制類型轉換爲16位無符號整型:

1 s=s.astype(np.uint16)#將numpy對象s強制類型轉換爲16位無符號整形

2、對A/D採集到的數據進行簡單處理

爲了演示NumPy和Matplotlib的信號處理和繪圖功能,我對A/D採集獲得的數據進行了簡單的處理。

1)繪製採集到數據的波形,Python腳本代碼以下。

 1 plt.subplot(211)
 2 delta_t = 1/Sample_rate#兩點之間的時間間隔
 3 t_scale=np.linspace(0,delta_t*D_LEN,num=D_LEN)*(10**6)
 4 #計算x軸,也就是時間軸的數值,最後乘與10**6是將時間單位折算爲us
 5 plt.plot(t_scale,res_float, '-r')
 6 plt.grid(True)
 7 plt.title("Time Domain WaveForm")
 8 plt.xlabel("t(us)")
 9 plt.ylabel("A(V)")
10 plt.show()
Python-Matplotlib繪製時域波形

    其中,Sample_rate是A/D轉換的採樣率;t_scale是一個NumPy數組,內容是顯示的X軸數值;res_float也是一個數組,內容是折算爲電壓值的A/D轉換結果。subplot()方法將打開一個2行1列的繪圖窗口,這個時域波形被繪製在第1行第1列的波形圖中。

2)計算和繪製FFT產生的幅頻特性

   爲減小數據時域截斷形成的能量泄露,先對數據進行加窗處理,再將其顯示在上面開啓的繪圖窗口的第2行的波形圖中。代碼以下:

 1 sfa = np.abs(sfc)
 2 sfa_half = sfa[0:int(D_LEN/2)]#因爲FFT結果的對稱性,只須要取一半數據。
 3 sfa_lg_half = np.log10(sfa_half)*20
 4 sfa_lg_half = sfa_lg_half - np.max(sfa_lg_half)#將最高能量點折算爲0dB
 5 plt.subplot(212)
 6 delta_f = Sample_rate/(D_LEN)    #FFT結果兩點之間的頻率間隔
 7 f_scale=np.linspace(0,delta_f*D_LEN/2,num=D_LEN/2)/1000
 8 #計算x軸,也就是頻率軸數值,最後除以1000,表示將頻率折算爲KHz
 9 plt.plot(f_scale,sfa_lg_half,'-b')
10 plt.title("Frequency Domain WaveForm")
11 plt.xlabel("f(KHz)")
12 plt.ylabel("A(dB)")
13 plt.grid(True)
14 plt.show()
Python NumPy MatPlotlib繪製頻域波形

    其中sw是通過加窗,且去除直流份量後的信號;D_LEN是以字節爲單位的數據傳輸的長度,每一個採樣點對應兩個字節,所以信號的長度爲D_LEN/2;f_scale是繪圖後X軸,也就是頻率軸的數值;NumPy中的fft()方法輸出快速傅里葉變換的結果,是個複數數組,sfa_lg是頻率折算爲dB後的數值。

4、STM32構成的實時性前端

   如圖4所示,由STM32構成的實現前端控制器主要完成如下工做:

  • 經過DMA1的通道1(CH1)控制ADC完成固定採樣率的A/D採集,並將數據存入到循環緩衝區ADC_DMA_BUF。
  • 經過DMA1的通道4(CH4)和5(CH5)控制SPI口和樹莓派通訊:接收樹莓派發送的D/A數據到緩衝區SPI_RX_DMA_BUF;向樹莓派發送緩衝區SPI_TX_DMA_BUF中的A/D轉換數據。

    另外,爲了在樹莓派人機交互界面的同步下,有序的完成:採集、數據搬運和傳輸工做,實時前端要在兩對GPIO鏈接:SHK_IN(樹莓派輸入/STM32輸出)和SHK_OUT(樹莓派輸出/STM32輸入)的同步下工做。

一、    由DMA1 CH1控制的A/D轉換

   A/D採集在STM32復位後不斷的循環進行,DMA1的CH1被配置爲循環模式,數據將採用循環隊列的數據結構存儲到寬度爲半字(HalfWord,16bits)的ADC_DMA_BUF中。配置代碼以下所示:

 1 DMA_DeInit(DMA1_Channel1);
 2 DMA_InitStructure.DMA_PeripheralBaseAddr = ADC1_DR_Address;//傳輸的源頭地址
 3 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&ADC_DMA_BUF;//目標地址
 4 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //外設做源頭
 5 DMA_InitStructure.DMA_BufferSize = (BUFF_SIZE - HEAD_SIZE)/2; //數據長度BUFF_SIZE
 6 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外設地址寄存器不遞增
 7 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//內存地址遞增
 8 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //外設傳輸以半字爲單位
 9 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//內存以半字爲單位
10 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;//循環模式
11 DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;//4優先級之一的
12 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //非內存到內存
13 DMA_Init(DMA1_Channel1,&DMA_InitStructure);//根據以上參數初始化DMA_InitStructure
14 DMA_Cmd(DMA1_Channel1, ENABLE);//使能DMA1
控制A/D轉換的DMA1CH1初始化

   其中BUFF_SIZE是以字節爲單位的傳輸緩衝區長度,可設爲1024。HEAD_SIZE是以字節爲單位傳輸數據包頭長度,可設爲24。採集緩衝區ADC_DMA_BUF的長度就是(BUFF_SIZE-HEAD_SIZE)/2。

   A/D轉換的配置代碼以下所示:

 1 ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//ADC1工做在獨立模式
 2 ADC_InitStructure.ADC_ScanConvMode = ENABLE;//模數轉換工做在掃描模式
 3 ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;//模數轉換工做在連續模式
 4 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; 
 5 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//ADC數據右對齊
 6 ADC_InitStructure.ADC_NbrOfChannel = 1;//轉換的ADC通道的數目爲1
 7 ADC_Init(ADC1, &ADC_InitStructure);
 8 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_1Cycles5); //ADC1通道2轉換順序爲1,
 9 RCC_ADCCLKConfig(RCC_PCLK2_Div4);   //設置ADC分頻因子4,56MHz/4=14 MHz
10 ADC_DMACmd(ADC1, ENABLE); //使能ADC1的DMA傳輸方式
11 ADC_Cmd(ADC1, ENABLE); //使能ADC1
A/D的DMA配置

   上述代碼配置STM32的ADC的採樣時間爲1.5個ADC時鐘週期,加上一次完整的逐次逼近過程所需的12.5個週期,共14個時鐘週期。ADC時鐘爲外設時鐘56MHz的四分之一,恰好14MHz,這樣進行一次完整A/D轉換的時間恰好爲1us,即實現了1MSPS的採樣率。ADC模塊被配置爲連續掃描通道1,並在轉換完成後直接觸發一次DMA1的數據傳輸。這樣整個採集和存儲工做由純硬件來完成,無需軟件干預,嚴格的控制了A/D轉換的孔徑抖動時間,有效的提高了A/D轉換的實時性。

二、    由DMA2的CH3控制的D/A轉換

   DMA2的CH3也被配置爲循環模式,程序運行過程當中會不斷的將SPI_RX_DMA_BUF中的數據發送到D/A轉換器中,從而造成連續的波形。而DMA2 CH3向DAC發送數據的時間間隔就是兩次D/A轉換的間隔,因此DMA2的CH3需由額外的定時器(TMR)來觸發傳輸。DMA2 CH3的配置代碼以下所示:

 1 DMA_DeInit(DMA2_Channel3); //根據默認設置初始化DMA2
 2 DMA_InitStructure.DMA_PeripheralBaseAddr = DAC_DHR12R1_Address;//外設地址
 3 DMA_InitStructure.DMA_MemoryBaseAddr = (u32)&SPI_RX_DMA_BUF; //內存地址
 4 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
 5 //外設DAC做爲數據傳輸的目的地
 6 DMA_InitStructure.DMA_BufferSize = (BUFF_SIZE-HEAD_SIZE)/2;
 7 //數據長度爲BUFF_SIZE
 8 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外設地址寄存器不遞增
 9 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//內存地址遞增
10 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
11 //外設傳輸以半字爲單位
12 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
13 //內存以半字爲單位
14 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;//循環模式
15 DMA_InitStructure.DMA_Priority = DMA_Priority_High;//4優先級之一的(高優先級)
16 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//非內存到內存
17 DMA_Init(DMA2_Channel3, &DMA_InitStructure);//根據以上參數初始化
18 DMA_Cmd(DMA2_Channel3, ENABLE);//使能DMA2的通道3
控制D/A轉換的DMA2CH3初始化代碼

   觸發DMA2的定時器爲TMR2,其初始化代碼以下所示:

 1 TIM_PrescalerConfig(TIM2,7-1,TIM_PSCReloadMode_Update);//設置TIM2預分頻值
 2 TIM_SetAutoreload(TIM2, 8-1);//設置定時器計數器值
 3 TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);
 4 //TIM2觸發模式選擇,這裏爲定時器2溢出更新觸發
 5 DAC_InitStructure.DAC_Trigger = DAC_Trigger_T2_TRGO;//定時器2觸發
 6 DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None;//無波形產生
 7 DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Disable;//DAC_OutputBuffer_Enable;//不使能輸出緩存
 8 DAC_Init(DAC_Channel_1, &DAC_InitStructure);//根據以上參數初始化DAC結構體
 9 DAC_Cmd(DAC_Channel_1, ENABLE);// 使能DAC通道1
10 DAC_DMACmd(DAC_Channel_1, ENABLE);//使能DAC通道1的DMA
11 TIM_Cmd(TIM2, ENABLE);//使能定時器2
觸發DMA2的TMR2配置

   TMR2的定時的溢出計數值被設置爲7*8=56,在56MHz主頻下將產生1MHz的溢出率,即D/A轉換器的刷新率也是1MSPS。如前所述,若是採用在TMR2中斷中由軟件來刷新DAC,將會提升形成D/A輸出間隔的孔徑抖動。所以這裏選擇了經過定時器硬件觸發DMA傳輸的方式來實現D/A數據的刷新的方式,大大提升了D/A輸出波形的信噪比。

三、    由DMA1的CH4和CH5控制的SPI數據交互

   A/D和D/A轉換由硬件控制,並自動定時進行的,但與樹莓派的數據交互倒是由樹莓派發起的,與STM32中的程序運行不一樣步。如圖4所示,雙方在握手信號SHK_IN和SHK_OUT的控制下,經過SPI口的雙向交互數據。其中樹莓派發起通訊,做爲SPI主機;STM32做爲從機。因爲從機沒法預知主機什麼時候發起通訊,所以也經過DMA來實現自動收發數據,其中DMA1 CH4負責接收D/A轉換數據,DMA1 CH5負責發送A/D數據。DMA1 CH4和CH5的配置代碼以下所示:

 1 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//使能DMA1時鐘
 2 DMA_DeInit(DMA1_Channel4);//DMA1的通道4是SPI2的接收通道
 3 DMA_InitStructure.DMA_PeripheralBaseAddr = ((uint32_t)(SPI2_BASE+0x0C));
 4 //外設地址,SPI1的基地址加上SPI_DR的偏移地址0X0C
 5 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&SPI_RX_DMA_BUF;
 6 //存儲器地址
 7 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //外設做爲數據源
 8 DMA_InitStructure.DMA_BufferSize = BUFF_SIZE;//數據長BUFF_SIZE
 9 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
10 //外設地址寄存器不遞增
11 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//內存地址遞增
12 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
13 //外設傳輸以字節爲單位
14 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
15 //內存以字節爲單位
16 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;//循環模式
17 DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//4優先級之一的(高優先)
18 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //非內存到內存
19 DMA_Init(DMA1_Channel4, &DMA_InitStructure);
20 DMA_Cmd(DMA1_Channel4, ENABLE);//使能DMA1通道2
21 //DMA1的通道5配置爲spi2輸出
22 DMA_DeInit(DMA1_Channel5);//DMA1的通道5是SPI2的發送通道
23 DMA_InitStructure.DMA_PeripheralBaseAddr = ((uint32_t)(SPI2_BASE+0x0C));
24 //外設地址,SPI1的基地址加上SPI_DR的偏移地址0X0C
25 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&SPI_TX_DMA_BUF;
26 //存儲器地址
27 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //外設做爲數據目的
28 DMA_InitStructure.DMA_BufferSize = BUFF_SIZE;//數據長BUFF_SIZE
29 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
30 //外設地址寄存器不遞增
31 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//內存地址遞增
32 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
33 //外設傳輸以字節爲單位
34 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
35 //內存以字節爲單位
36 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;//循環模式
37 DMA_InitStructure.DMA_Priority = DMA_Priority_High;//4優先級之一的
38 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //非內存到內存
39 DMA_Init(DMA1_Channel5, &DMA_InitStructure);
40 DMA_Cmd(DMA1_Channel5, ENABLE);//使能DMA1通道3
控制SPI收發數據的DMA1CH4和CH5的配置

   因爲A/D轉換數據在不斷的刷新中,樹莓派發起通訊的時A/D轉換數據可能存放到了緩衝區ADC_DMA_BUF的任意位置。若是直接將ADC_DMA_BUF中的數據發送給樹莓派,則樹莓派獲得的將是一個首地址指針錯誤的循環隊列,沒法解讀爲正確的數據。所以,我採用了圖4所示的雙緩衝數據結構,樹莓派發起通訊時首先讀取DMA1 CH1的當前位置,再根據這個首地址指針將ADC_DMA_BUF中的數據按順序從新搬運到發送緩衝區SPI_TX_DMA_BUF中。而後再啓動DMA2 CH4和CH5的SPI通訊,將數據發送給樹莓派。控制ADC_DMA_BUF和SPI_TX_DMA_BUF數據結構調整的代碼以下所示:

 1 Curr_point = (BUFF_SIZE - HEAD_SIZE)/2-DMA_GetCurrDataCounter(DMA1_Channel1);
 2 //讀取當前DMA正在操做的數據點,函數DMA_GetCurrDataCounter返回的是剩餘待傳輸的數據,因此要求當前地址應該用緩衝區的長度減去這個值
 3 Curr_point = Curr_point*2;    //每次DMA存儲兩個字節,變換爲字節地址
 4 j=0;
 5 for(i=Curr_point;i<(BUFF_SIZE - HEAD_SIZE);i++){
 6     SPI_TX_DMA_BUF[j] = *((char*)ADC_DMA_BUF+i);
 7     j++;}
 8 for(i=0;i<Curr_point;i++){
 9     SPI_TX_DMA_BUF[j] = *((char*)ADC_DMA_BUF+i);
10     j++;}
A/D緩衝區的數據結構調整

雖然D/A轉換也是不斷循環進行的,但對於D/A數據緩衝區的刷新卻沒有A/D數據緩衝區的問題:樹莓派能夠在任什麼時候刻總體刷新D/A緩衝區,輸出波形將在一個DMA週期後輸出正確的新數據波形。

5、用樹莓派的SPI和GPIO控制實時前端

爲實現樹莓派和實時前端的數據交互,須要使用樹莓派擴展接口中的兩個GPIO口和一個SPI設備。

一、    用Python控制GPIO

   GPIO的安裝使用較簡單,如本文第一部分所述在Linux的shell命令行中安裝控制GPIO模塊後就能夠在Python腳本中導入GPIO模塊來使用。本程序只用到3個GPIO,分別用於讀取按鍵和與STM32的握手信號,它們的初始化代碼以下所示:

1 GPIO.setmode(GPIO.BCM)#將GPIO的引腳編號設置爲BCM模式
2 RP_SHK_IN = 6    #樹莓派的輸入握手引腳,鏈接RT_STM32的輸出握手引腳
3 RP_SHK_OUT = 5    #樹莓派的輸出握手引腳,鏈接RT_STM32的輸入握手引腳
4 KEY_C = 25    #樹莓派的按鍵輸入引腳,用於接收數據交互的啓動信號
5 GPIO.setup(RP_SHK_IN, GPIO.IN, pull_up_down = GPIO.PUD_UP)
6 GPIO.setup(RP_SHK_OUT, GPIO.OUT)
7 GPIO.setup(KEY_C, GPIO.IN,pull_up_down=GPIO.PUD_UP)
8 GPIO.output(RP_SHK_OUT,True)    
與前端握手的GPIO配置

  其中須要注意的是GPIO.setpu()方法的第三個參數:pull_up_down = GPIO.PUD_UP,這個參數用於將這個GPIO配置爲弱上拉模式,以保證在沒有輸入信號的時候,這個GPIO是高電平。接下來只須要經過GPIO.input()方法來讀取GPIO狀態,和GPIO.output()來設置輸出電平便可。這裏就再也不贅述了。

二、    用Python控制SPI口

   樹莓派的SPI配置相對較麻煩,首先須要開啓這個功能,能夠在命令行中用:

sudo raspi-config

   命令來開啓命令行下的樹莓派配置程序,並從中開啓SPI功能。若是你安裝了圖形界面則簡單得多,Raspbian系統的開始菜單中打開Preferences菜單下的Raspberry Pi Configuration,就能夠在下圖所示的圖形界面中開啓SPI功能。

圖6 開啓Raspibian的SPI功能

    在https://pypi.python.org/pypi/spidev/3.1下載樹莓派的SPI模塊spidev,並經過如下命令安裝這個模塊:

tar –zxvf spidev-3.1.tar.gz
cd spidev
sudo python setup.py install
安裝spidiv

  安裝成功後,以下圖所示能夠在/dev下看到spidev0.0和spidev0.1兩個設備, 這兩個SPI設備擁有一樣的時鐘和數據傳輸引腳,只是片選引腳不一樣。

圖7 SPI設備

spidev模塊在Python下的使用並不複雜,首先導入模塊:

import spidev

其次初始化SPI口,Python代碼以下:

1 spi = spidev.SpiDev()
2 bus = 0
3 device = 0
4 spi.open(bus , device)
5 spi.max_speed_hz = 10000000
6 spi.mode = 0b00 
7 #[CPOL|CPHA]CPOL是SCK空閒時的電平;CPHA是時鐘的第幾個邊沿讀數
初始化SPI口

   其中open()方法的兩個參數分別是SPI口的編號和片選引腳的編號。max_speed_hz是以Hz爲單位的SPI同步時鐘頻率,這裏使用了10MHz的通訊頻率。而mode屬性只有兩個位,第一個位CPOL表示通訊空閒時SCK的電平——0爲低電平,1爲高電平;第二個位CPHA表示在時鐘SCK的第幾個邊沿讀取SPI數據線上的數據——0爲在空閒狀態恢復的第一個邊沿讀取SPI數據,而1表示在空閒恢復後的第二個邊沿讀取數據。這裏將這兩個位都設置爲0,表示SCK在空閒狀態處於低電平,而進入通訊後在SCK的第一個邊沿,也就是上升沿開始讀取數據。

Spidev中讀寫SPI口的方法爲xfer(),使用示例代碼爲: 

rx_data = spi.xfer(tx_data)

其中tx_data是由待發送數據構成的Python列表,長度不限。返回rx_data是和tx_data長度相同的列表,存放了SPI收到的數據。

三、    樹莓派主程序

與實時前端進行通訊的樹莓派Python主程序負責完成:發送D/A轉換數據包,接收A/D轉換數據包,以及和用戶實時交互的工做。其代碼以下所示: 

 1 try:
 2 while True:
 3 time.sleep(0.01)
 4 if(GPIO.input(KEY_C) == False):
 5  #如下開始控制RT_STM32模塊
 6         GPIO.output(RP_SHK_OUT,False)#啓動一次實時採集
 7         print("Beginning a A/D&D/A processing...")
 8         while (GPIO.input(RP_SHK_IN) == True):
 9 #RT_STM32模塊輸出爲高電平表示AD轉換等操做尚未完成,須要等待
10             time.sleep(0.001)
11         print("The data is transporting!")#如下進行讀取數據的工做
12         rx_data = spi.xfer(tx_data)#調用spidev模塊進行連續數據收發
13         #列表tx_data中存放的是發送給STM32的D/A輸出的數據
14         #列表rx_data獲得的是STM32的A/D採集到的數據
15         GPIO.output(RP_SHK_OUT,True)#結束本次採集和數據交換
16         #如下將以字節爲單位收發的數據拼接爲16bits的數據
17         rx_short = []
18         for i in range(int(D_LEN)):    #將以字節存儲的數據轉換爲字形式
19             rx_short = rx_short + [rx_data[i*2] + rx_data[i*2+1]*256]
20 #每次添加一個數,被以列表的形式添加在原有列表的最後
21         rx_head = rx_data[D_LEN*2::]#後面的數據是數據包的頭信息
22         #如下將整型數據轉換爲0-3.3V的電壓值
23         res_float=[]
24         for x in rx_short:
25             temp_float = x*3.3/4096#將數據折算爲電壓
26             res_float = res_float + [temp_float]
27         np.savetxt("last_data.csv",res_float,delimiter = ',')#保存測試數據。
28         plot_time_frq_wave(res_float)#繪製時域和頻域波形
29         print("This A/D&D/A processing is complete!")
30         while(GPIO.input(KEY_C) == False):        #等待按鍵釋放
31             time.sleep(0.01)
32         print(".....")
33         print("Please press the KEY to start a A/D&D/A processing!")
34 except KeyboardInterrupt:
35         print("Program is over.")
36         GPIO.cleanup()#關閉用到的GPIO
Python主流程控制代碼

   整個程序的最外層是一個異常檢測、處理程序:當終端收到「CTL+C」時終止程序,不然不斷循環交互數據和顯示結果。

   第二層是一個無條件循環,用於檢測和樹莓派25號GPIO相連的按鍵,並消除按鍵上的抖動:若是有按鍵就開始一輪新的數據交互和顯示,若是沒有就繼續循環和等待。

   第三層代碼在檢測到按鍵後啓動,用於和STM32交互數據而後顯示結果:首先經過RP_SHK_OUT拉低來啓動STM32的數據交互,待STM32準備好後經過將RP_SHK_IN拉低來通知樹莓派,樹莓派接到消息後經過xfer()方法來啓動SPI數據傳輸。數據交互完成後存放在返回列表中的是以高低字節存放的8位數據,程序首先將它們拼接在一塊兒,再將數據轉換爲0-3.3V的實際電壓信號,並保存爲CSV格式的數據文件。隨後程序對這些數據進行前述的數據處理,隨機顯示數據和處理結果。最後,程序將等待本次按鍵釋放,而後退回上一層代碼等待按鍵來啓動下一次數據交互和結果顯示。

 

6、測試結果

一、    A/D採集的結果

用函數信號發生器分別產生20KHz、50KHz、100KHz和200KHz的正弦信號,並利用上述實時前端,以1MSPS採樣率進行500次採樣。樹莓派中運行的Python程序調用NumPy模塊進行FFT變換後獲得的信號的頻譜後,再調用matplotlib模塊繪圖,結果以下圖8-圖11所示。其中上部的紅色波形是時域數據,下部的藍色波形是紅色數據的頻譜圖。

圖8 對10KHz正弦信號採樣和FFT變換的結果

 

圖9 對50KHz正弦信號採樣和FFT變換的結果

圖10 對100KHz正弦信號採樣和FFT變換的結果

圖11 對200KHz正弦信號採樣的結果

    對20KHz的三角波進行採樣,結果以下圖所示。能夠明顯的看到,做爲一種對稱的周期函數,三角波的存在能量較大的奇次諧波。

 

圖12 對20KHz三角波信號採樣的結果

二、    對A/D轉換結果的進一步分析

   文獻[3]指出,FFT頻譜的理論噪底(噪聲平面)等於:

QNLdB = -(SNR +  10*log10(M/2))                                 (2)

   其中,SNR爲理論信噪比,M是進行FFT的數據點數。理論信噪比SNR的計算公式爲[4]

SNR = 6.02*N+1.76                                              (3)

   N爲轉換器位數,STM32的A/D轉換器爲12bits,對於圖8-圖11所示的500點的FFT,SNR的理論值爲74dB,噪聲平面爲-94dB。顯然,圖8-圖11所示的對正弦信號的測試結果噪聲平面有效值在-60dB左右——遠高於理論值。

   進一步嘗試計算信納比(SINAD),來評估採樣結果。信納比定義爲:實際輸入信號的均方根值與奈奎斯特頻率如下包括諧波但直流除外的全部其它頻譜成分的均方根和之比[4]。在樹莓派上用Python和NumPy模塊實現信納比的計算,代碼以下。 

 1 def cal_sinad(sfa,w):#根據FFT的幅值結果,計算信納比SINAD的函數
 2     #第一個參數是FFT的結果
 3     sfa=sfa**2#將信號折算成能量
 4     s_max = max(sfa)#查找最大值
 5       max_index = list(sfa).index(s_max)
 6     #查找最大值所在的位置,但index()方法只有列表有,因此先將其轉回爲列表再查找
 7     index_low=max_index-w#選取窗口的下限
 8     index_high=max_index+w#選取窗口的上限
 9     signal_pow=sum(sfa[index_low:index_high])#選取窗口內的信號之和
10     noise_pow=sum(sfa)-signal_pow#計算噪聲能量
11     sinad=10*np.log10(signal_pow/noise_pow)
12     return sinad  
計算SINAD的Python代碼

   編程的基本思路是找到能量最高的頻點,並將其附近的兩個w內的能量值都做爲信號的能量,用信號能量與其餘全部點的噪聲能量相除從而獲得信納比。在主程序中的調用方式以下。

1 SEL_WIDE = 2#選擇單頻信號的窗口寬度,真實窗口的寬度爲SEL_WIDE*2+1
2 sinad = cal_sinad(sfa_half,SEL_WIDE)#根據FFT的幅值結果,計算信納比SINAD
3 print("The SINAD is: %f dB"%sinad) #輸出顯示採集信號的信納比

   經計算獲得圖8-圖11所示信號的信納比在44-46dB左右,低於理論值74dB(在理想狀況下。理論信納比SINAD等於理論信噪比SNR)。形成信納比低於信噪比的緣由可能有:

  1)從實物圖5中能夠看到,函數信號發生器和實時性前端的模擬輸入採用了鱷魚夾和單股導線鏈接,極可能形成了信號的失真。週期性的失真將形成諧波干擾,而這一點能夠在圖8-圖11的頻譜圖中均可以觀測到——信號的二次諧波頻率點上都有明顯的能量突出。

  2)STM32的A/D轉換模塊自己屬於SoC的一部分,因爲模數隔離等緣由,其模擬性能可能不如單獨的ADC芯片,距離SINAD的理論值更是存在必定差距。

  3)STM32佈線時沒有嚴格區分模擬電源、模擬地和數字電源、數字地,並分別對模擬電源——模擬地,以及數字電源——數字地去耦。

  4)STM32鎖相環所產生的系統時鐘可能存在較大孔徑抖動,從而形成信納比下降。

三、    D/A輸出的結果

   在每次數據交互前,須要將D/A輸出的數據存入列表tx_data中,可用numpy模塊產生一個單頻的正弦信號。其中,計算產生的正弦值被增長了211的直流偏置,以將全部數值轉換爲正數。因爲SPI通訊的基本單位是1個字節,所以數據最後要分解爲高低兩個字節。 

1 index = np.arange(D_LEN)
2 s = 1000*np.sin(2*np.pi*index*k/D_LEN)+2**11#k個週期的正弦波形
3 s = s.astype(np.uint16)#將numpy對象s強制類型轉換爲16位無符號整形
4 tx_data = [] #將數據存放在列表中,且不超過1個字節
5 for dt in s:
6     tx_data.append(int(dt%256))
7     tx_data.append(int(dt/256))
Pyhton產生D/A數據

   用spi.xfer()方法啓動一次雙向通訊後,用示波器觀察D/A輸出的信號以下圖所示,其中,考上的綠色部分是D/A輸出的時域波形,靠下的紅色部分是示波器對時域波形進行FFT獲得的頻譜圖。

圖12 示波器觀測D/A產生的單頻信號

   也能夠用Python NumPy產生更復雜的波形,如包含三個頻率點的信號:

s = 1000*np.sin(2*np.pi*index*2/D_LEN) + 200*np.sin(2*np.pi*index*20/D_LEN) + 40*np.sin(2*np.pi*index*50/D_LEN) +2048

用示波器觀察上面代碼產生的波形及其頻譜以下圖所示。

圖13 示波器觀測D/A產生的三頻信號

參考文獻:

[1] Brad Brannon, "Aperture Uncertainty and ADC System Performance" Application Note AN-501, Analog Devices, Inc., January 1998.

[2] Walt Kester, "孔徑時間、孔徑抖動、孔徑延遲時間——正本清源" MT-007 TUTORIAL, Analog Devices, Inc., October 2008.

[3] Walt Kester, "瞭解SINAD、ENOB、SNR、THD、THD + N、SFDR,不在噪底中迷失" MT-003 TUTORIAL, Analog Devices, Inc., October 2008.

[4] Walt Kester, "Analog-Digital Conversion" Analog Devices, Inc., ISBN 0-916550-27-3, 2004.

 

相關文章
相關標籤/搜索