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


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

一、樹莓派Raspbian系統的實時性

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

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

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

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

圖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的時間分辨率,為此必須采用實時性更強的模擬前端控制器。

二、總體設計思路

   常見的實時控制方案有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 樹莓派和實時性前端功的實物圖

三、在樹莓派上用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后的數值。

四、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輸入)的同步下工作。

1、    由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轉換的實時性。

2、    由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輸出波形的信噪比。

3、    由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周期后輸出正確的新數據波形。

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

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

1、    用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()來設置輸出電平即可。這里就不再贅述了。

2、    用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收到的數據。

3、    樹莓派主程序

與實時前端進行通信的樹莓派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格式的數據文件。隨后程序對這些數據進行前述的數據處理,隨機顯示數據和處理結果。最后,程序將等待本次按鍵釋放,然后退回上一層代碼等待按鍵來啟動下一次數據交互和結果顯示。

 

六、測試結果

1、    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三角波信號采樣的結果

2、    對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鎖相環所產生的系統時鍾可能存在較大孔徑抖動,從而造成信納比降低。

3、    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.

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM