串口最全資料 ZYNQ "HELLO,WORLD!"背後的故事

轉載來自 http://blog.chinaaet.com/detail/30143.html

http://www.myir-tech.com/bbs/forum.php?mod=viewthread&tid=7224

https://blog.csdn.net/FPGADesigner/article/details/88852990

 

之前看到一篇比較好的博文,轉載了。

我等電子愛好者拿到一塊開發板當然首先就是讓他輸出HELLO,WORLD的啦。ZYNQ作爲XILINX推出的最新的ALL PROGRAMME平臺自然也無法逃離此等“厄運”。  

      讓ZYNQ輸出"HELLO,WORLD"非常簡單,ZEDBOARD.ORG網站上已有ZedBoard_CTT_v14.1文檔,大家按照文檔中的步驟就能通過串行接口看到輸出了。如果不太明白也可以到BAIDU上搜索ZEDBOARD,很多前輩已經把輸出"HELLO,WORLD"的步驟圖文並茂的一步一步給出了。圖1就是main函數的截圖,可以看到該函數非常的簡單,首先初始化 平臺,然後利用“重定向把printf”函數的輸出定位到串口上。

 

注意到代碼中把init_platform和cleanup_platform函數已經被註釋掉了,難道只有

一個printf就能輸出嗎?當然,查看Init_platform()和cleanup_platform()源碼我們會發現其實這兩個函數和串口輸出並沒有任何關係,這兩個函數的作用就是打開系統的CACHE和關閉系統CACHE(Init_platform函數中包含有init_uart函數,但仔細觀察發現只有在系統中定義了STDOUT_IS_16550宏,init_uart函數中的語句才起作用,否則該函數就是一個空函數。而在本例子中我們是並沒有用到STDOUT,所以根本沒有定義 STDOUT_IS_16550宏 )。

     看到這個估計很多人都有個疑問:串口在使用之前不是應該初始化的嗎?至少要設置一下波特率啥的吧?看代碼中,似乎沒有對ZYNQ的串口進行初始化就使用了,難道ZYNQ太先進了不用初始化?

     在討論這個問題之前讓我們先來看看ZYNQ的結構以及ARM的啓動過程。ZYNQ中包含有2個ARM CORTEX-A9的核(PS)和PL邏輯單元。ZYNQ支持多種啓動模塊,總的來說包括安全和不安全的兩種引導方式。引導主要是通過PS部分來完成。對於安全引導來說,PL部分必須啓動以便使用其中的安全模塊,通過這些模塊可以完成256位AES和SHA授權。在系統復位後,系統將會檢測模式引腳上的電平狀態,以此來決定從哪個設備(NOR,NAND,QUAD-SPI或JTAG)來引導系統。當然,JTAG方式只能工作在不安全引導方式下,通常該模式是用來調試系統的。在決定從哪個設備引導系統後,其中一個ARM A9的核開始執行片外ROM中的代碼並拷貝第一階段BOOT LOADER(FSBL)到OCM中。拷貝完成後,處理器開始執行FSBL,系統開始對PS部分進行初始化並可以開始配置PL模塊(當然,你也可以不配置PL模塊)。通常,在FSBL中需要包含對第二階段代碼(SSBL)的載入(如UBOOT)。SSBL繼續對處理器進行配置直到系統完全完成初始化。

     既然在執行MAIN函數之前還有這麼多步驟,那麼這些代碼到底寫在哪裏呢?打開工程目錄下的SDK\SDK_Export\hello_world_bsp_0\ps7_cortexa9_0\libsrc\standalone_v3_05_a\路徑下的SRC文件夾,裏面有asm_vectors.S

boot.S

cpu_init.S

translation_table.s

xil-crt0.S等5個文件,它們都是.s結尾的彙編文件,根據以往的經驗,這些都應該是最底層的,系統啓動後首先執行的文件。我們知道ARM啓動都是從0x00地址開始的,而在這個地址上放的應該是中斷向量表,根據這個線索,我們首先查看asm_vectors.S文件,文件的最開頭如圖2所示:

ZYNQ "HELLO,WORLD!"背後的故事_第1張圖片

 

這裏就是中斷向量表,ZYNQ啓動後所運行的第一句話就是B  _boot,這是一個跳轉語句,直接跳轉到boot.s文件中的  _boot  標號處開始執行後面的語句,由於後面的代碼比較長,在此就不贅述了。boot.s中的代碼主要完成MMU、CACHE的初始化以及ARM各種模式下棧基地址的配置,最後通過

b _start

 語句跳轉到_start標號中繼續執行。_start標號在xil-crt0.S文件中被實現,代碼如下:

 

_start:

bl      __cpu_init /* Initialize the CPU first (BSP provides this) */

 

mov r0, #0

 

/* clear sbss */

ldr  r1,.Lsbss_start /* calculate beginning of the SBSS */

ldr r2,.Lsbss_end /* calculate end of the SBSS */

 

.Lloop_sbss:

cmp r1,r2

bge .Lenclsbss /* If no SBSS, no clearing required */

str r0, [r1], #4

b .Lloop_sbss

 

.Lenclsbss:  

/* clear bss */

ldr r1,.Lbss_start /* calculate beginning of the BSS */

ldr r2,.Lbss_end /* calculate end of the BSS */

 

.Lloop_bss:

cmp r1,r2

bge .Lenclbss /* If no BSS, no clearing required */

str r0, [r1], #4

b .Lloop_bss

 

.Lenclbss:

 

/* set stack pointer */

ldr r13,.Lstack /* stack address */

 

/* Initialize STDOUT to 115200bps */

ldr r0, =UART_BAUDRATE

bl Init_Uart

#ifdef PROFILING /* defined in Makefile */

/* Setup profiling stuff */

bl _profile_init

#endif /* PROFILING */

 

 

/* Initialize the SMC interfaces for NOR and SRAM */

#ifndef USE_AMP

bl XSmc_NorInit

 

bl XSmc_SramInit

#endif

 

/* make sure argc and argv are valid */

mov r0, #0

mov r1, #0

 

/* Let her rip */

bl main

 

 

代碼首先通過語句bl      __cpu_init 跳轉到_cpu_init標號處(該標號在cpu_init.S文件中,其中的代碼主要對cp15寄存器進行操作),隨後代碼對SBSS段、BSS段以及棧指針R13進行初始化,緊接着通過語句bl Init_Uart跳轉到UART.C文件中對串口進行初始化;通過語句bl XSmc_NorInit和bl XSmc_SramInit對NOR和SRAM存儲器進行初始化,最後調用bl main跳轉到我們的MAIN函數中開始執行用戶的程序。

        這下我們就可以解釋開始所提出的問題了, 實際中ZYNQ的串口並不是不需要初始化,而是在進入main函數之前就已經被初始化過了,其所用的波特率在xil-crt0.S文件的頂頭被宏定義過,默認爲115200BPS。如果我們要用其它的波特率的話,理論上來說在該處更改即可。爲什麼說理論上呢?因爲在實際中更改此處是不行的,究其原因我認爲是由於xil-crt0.S是系統生成,如果我們更改後不編譯該文件那麼自然不能更改波特率;如果編譯該文件則EDK會自動從系統中重新拷一個xil-crt0.S文件到項目中, 這也就把我們更改過的文件覆蓋了。所以,如果我們需要更改串口的波特率的話最簡便的方法就是在main函數中再調用一次Init_Uart函數,指定其輸入參數爲我們所需要的波特率,如9600.

 

總結一下,其實ZYNQ中的ARM和我們平時所用的ARM啓動方式上並沒有任何差別,也是從中斷向量表開始然後經過多次跳轉最終調用用戶程序的MAIN函數。用流程圖表示如下所示:

 

 

總結如下:

實際中ZYNQ的串口並不是不需要初始化,而是在進入main函數之前就已經被初始化過了,如果我們需要更改串口的波特率的話最簡便的方法就是在main函數中再調用一次Init_Uart函數,指定其輸入參數爲我們所需要的波特率,如9600.

 

 

寫在前面:
本人純屬菜鳥,不敢保證自己的觀點正確,各位看官多指教。

  • 本文討論的主題是zynq啓動過程。
  • 閱讀本文要對Zyna EPP有那麼一點點了解,最好使用過Xilinx的PlanAhead、EDK、XPS、SDK開發工具,我用的版本是14.2。
  • 建議閱讀本文前先看一下pkilllo前輩的一篇文章《ZYNQ "HELLO,WORLD!"背後的故事》,鏈接爲:http://bbs.myir-tech.com/thread-7225-1-1.html
  • 文章最後有文中提到的文檔的下載鏈接。
  • 這篇博文沒有編輯格式,附件給出word版本下載。
     


接觸zc702和z-turn Board也有一陣子了,最大的感觸就是開發工具做的工作實在太多。以前都是用Altera的片子,用Quartus II,從來沒有用過Xilinx的東西,是一個不折不扣的新手,剛接觸Xilinx就是他們最新產品Zynq,結果可想而知:相當摸不到頭腦。
幾位前輩做了簡單的測試之後,都已經開始做比較大的方案,這對於我這個還沒有入門的人跳躍太大,所以在打算將自己規劃的學習過程記錄下來,希望對和我同樣是門外漢的人有所幫助。

跟着官方的教程一直next,超級終端就能打印出“HelloWorld”,測試串口發送的目的是達到了,可是自己除了知道去點哪些菜單之外,對整個平臺到底在做什麼真的是一無所知!
爲了讓自己不那麼白癡,只能硬着頭皮肯官方文檔。那時候我還不知道ChinaAET上開展了ZedBoard體驗的活動,一直在閉門造車的研究Xilinx和ZedBoard的官方英文文檔,直到昨天才在ChinaAET上發現很多有用的資料。其中pkilllo前輩的一篇文章《ZEDBOARD入手體驗------ZYNQ "HELLO,WORLD!"背後的故事》跟我正在研究的東西很類似,令我受益匪淺,但是我覺得這篇文章對Zynq的啓動過程分析稍微缺了一部分,故將自己的心得寫下來。

廢話不多說了,直奔主題。
首先提一下Xilinx官方提供的文檔《Zynq-7000 Extensible Processing Platform Technical Reference Manual(ug585)》,這篇文檔總共有1700左右,可想而知它的信息量有多豐富,可以說是搞Zynq離不開的資料。這篇文檔第6章Boot and Configuration對Zynq的啓動過程進行了描述。文中指出:
------------------------------------------------翻譯並引用原文-------------------------------------------------
配置Zynq-7000系列可擴展器件需要多個步驟,最少要2個階段,通常需要3個階段,如下:

  • Stage0:稱爲BootROM,這一階段控制最初的器件啓動,BootROM是不可改動的可執行代碼,處理器在上電覆位和熱重啓之後執行。
  • Stage1:通常稱爲第一階段引導程序(First Stage Boot Loader,FSBL),它可以是任何用戶可控的代碼。關於FSBL的細節請參考UG821:《Zynq-7000 EPP Software Developers Guide》。
  • Stage2:該階段通常開始執行用戶所設計的處理系統,但是它也可以是第二階段引導程序(Second Stage Boot Loader,SSBL)。這一階段完全在用戶的控制之下,本章並不予以詳述。請參考UG821:《Zynq-7000 EPP Software Developers Guide》瞭解FSBL和Stage 2 images。

-----------------------------------------------------引用結束-----------------------------------------------------
由上述描述可以看出,Zynq上電後最先執行的是BootROM啓動,由於它不可更改,所以我們可以跳到Stage1,FSBL。查閱ug821相關章節:
------------------------------------------------翻譯並引用原文-------------------------------------------------
第一階段引導程序(FSBL)在啓動後開始。它由BootROM加載到OCM(On Chip Memory)或者在BootROM header所指明的加密位置執行。
FSBL負責:

  • 利用XPS提供的PS配置數據進行初始化。(見Zynq PS 配置)
  • 將比特流(bitstream)下載到PL。將第二階段引導程序(SSBL)或者是“裸奔”應用代碼加載到內存。
  • 開始執行SSBL或是“裸奔”應用程序。
     

……
查看SDK提供的FSBL代碼可以瞭解FSBL如何初始化CPU和FSBL所用到的外設,以及它如何使用一個簡單的C run-time library。
……

Zynq PS 配置
利用Zynq配置UI,XPS生成MIO和SLCR寄存器的初始化代碼。這些文件在XPS工程文件夾下:

  • Ps7_init.c和ps7_init.h,用於初始化CLK,DDR和MIO。Ps7_init.tcl完成的初始化和ps7_init.c代碼完成的初始化是相同的。
  • Ps7_init.tcl文件,可以用來初始化CLK,DDR和MIO。它所完成的初始化和ps7_init.c代碼完成的初始化是相同的。
     

注意:當使用XMD調試應用程序的時候tcl文件是很有用的。例如,你可以執行Ps7_init.tcl文件然後將應用程序加載到DDR中並調試。在這種情況下,就不需要進行FSBL的全過程。

  • Ps7_init.html,描述了初始化數據。
     

重要:在將來的版本中,可以改變PS初始化數據的位置和形式。

注意:XPS保持PL比特流和這些初始化數據的同步。不建議手動更改這些設置。
-----------------------------------------------------引用結束-----------------------------------------------------

相信大家看了上面兩段引文,對Zynq的啓動過程已經有了大致的瞭解。另外,還可以看出,FSBL階段不僅調用了pkilllo前輩提到的BSP提供的ARM啓動代碼,而且使用ps7_init.c中的代碼對設備進行了初始化工作。這兩部分的初始化又是哪個在前,哪個在後呢?

從理論上來講,假設先後改動這兩部分對串口波特率的初始化數據,如果改動之後波特率發生變化,那麼就可以知道被改動的部分是後執行的。但是正如pkilllo前輩所說的,這只是理論上的想法,在改動BSP提供的源文件時發現一旦重新build,SDK又會重新生成源碼,而不會編譯改動後的文件;另外,在改動ps7_init.c和ps7_init.tcl後,波特率也沒有發生變化,我想這是由於這些初始化數據是由XPS生成,並且和bitstream同步,應該是PFGA內部硬件連線實現的初始化,所以我手動更改也是無效的。不過,按照ug821的說法,在以後的版本中或許可以在XPS中對初始化數據進行設置。
鬧了這半天,到頭來發現你考慮的這些玩意目前只能由開發工具完成,想隨心所欲八成是沒戲的,但如果能夠自己開發BSP的話,應該可以按照自己的想法做一些事情。對於我這個菜鳥來說,BSP還太遙遠,目前只能利用SDK提供的源碼學習如果操作各種外設。

經過一番周折,雖然沒有能夠改變串口通信的波特率,但回過頭來再看,我對開發工具到底做了哪些工作已經有所瞭解,這讓我的思路清晰了不少。我打算在下一篇文章結合利用PS在串口打印“Hello ZedBoard!”的工程,講一下自己對Xilinx一些列開發工具所做工作的理解。

UG585 Zynq-7000 Extensible Processing Platform Technical Reference Manual 下載鏈接:
http://china.xilinx.com/support/documentation/user_guides/ug585-Zynq-7000-TRM.pdf
UG821 Zynq-7000 EPP Software Developers Guide 下載鏈接:
http://china.xilinx.com/support/documentation/user_guides/ug821-zynq-7000-swdg.pdf

 

 

上文對Zynq中的UART控制器做了簡單介紹。從本文開始將以實例的方式詳細講述UART的各種使用方法。本文是UART最基礎的使用方法,每秒發送一個“hello world”,實現的功能與printf或xil_printf相同。但後面介紹UART更復雜特性的文章,都是在本文設計的基礎上進行改動。
SDK程序設計

Vivado中配置Zynq時啓用開發板提供的UART接口。SDK中user_uart.h文件代碼如下:

#ifndef SRC_USER_UART_H_
#define SRC_USER_UART_H_

#include "xparameters.h"
#include "xuartps.h"
#include "xil_printf.h"
#include "sleep.h"

#define UART_DEVICE_ID                  XPAR_XUARTPS_0_DEVICE_ID

int Uart_Send(XUartPs* Uart_Ps, u8 *sendbuf, int length);
int Uart_Init(XUartPs* Uart_Ps, u16 DeviceId);

#endif /* SRC_USER_UART_H_ */

 

user_uart.c文件的代碼如下:

#include "user_uart.h"

// UART格式
XUartPsFormat uart_format =
{
    9600,
    //XUARTPS_DFT_BAUDRATE,   //默認波特率 115200
    XUARTPS_FORMAT_8_BITS,
    XUARTPS_FORMAT_NO_PARITY,
    XUARTPS_FORMAT_1_STOP_BIT,
};

//--------------------------------------------------------------
//                     UART初始化函數
//--------------------------------------------------------------
int Uart_Init(XUartPs* Uart_Ps, u16 DeviceId)
{
    int Status;
    XUartPs_Config *Config;

    /*  初始化UART設備    */
    Config = XUartPs_LookupConfig(DeviceId);
    if (NULL == Config) {
        return XST_FAILURE;
    }
    Status = XUartPs_CfgInitialize(Uart_Ps, Config, Config->BaseAddress);
    if (Status != XST_SUCCESS) {
        return XST_FAILURE;
    }
    
    /*  UART設備自檢  */
    Status = XUartPs_SelfTest(Uart_Ps);
    if (Status != XST_SUCCESS) {
        return XST_FAILURE;
    }

    /*  設置UART模式與參數   */
    XUartPs_SetOperMode(Uart_Ps, XUARTPS_OPER_MODE_NORMAL); //正常模式
    XUartPs_SetDataFormat(Uart_Ps, &uart_format);    //設置UART格式

    return XST_SUCCESS;
}

//--------------------------------------------------------------
//                     UART數據發送函數
//--------------------------------------------------------------
int Uart_Send(XUartPs* Uart_Ps, u8 *sendbuf, int length)
{
    int SentCount = 0;

    while (SentCount < length) {
        SentCount += XUartPs_Send(Uart_Ps, &sendbuf[SentCount], 1);
    }

    return SentCount;
}

main.c文件的代碼如下:

#include "user_uart.h"

XUartPs Uart_Ps;        /* The instance of the UART Driver */

int main(void)
{
    int Status;
    u8 sendbuf[] = "Hello World!\r\n";
    /* 串口初始化 */
    Status = Uart_Init(&Uart_Ps, UART_DEVICE_ID);
    if (Status == XST_FAILURE) {
        xil_printf("Uartps Failed\r\n");
        return XST_FAILURE;
    }

    while (1)
    {
        sleep(1);
        Uart_Send(&Uart_Ps, sendbuf, 14);
    }

    return Status;
}

SDK Terminal中添加串口,波特率設置爲程序制定的9600,運行程序,將看到每隔1s打印一次“Hello World!”。
在這裏插入圖片描述
相關API函數
1.UART初始化

對UART設備初始化操作和前面GPIO設備、中斷設備、定時器設備、XADC設備的初始化過程一樣,不再贅述。接着使用XUartPs_SelfTest函數對UART設備自檢。

    s32 XUartPs_SelfTest(XUartPs *InstancePtr)

    1

這個函數執行一次本地迴環,驗證可以正常發送和接受數據。返回XST_UART_TEST_FAIL表示測試失敗;XST_SUCCESS表示測試成功。我們可以用這個函數檢查硬件工作是否正常。
2.模式配置

初始化函數中還用XUartPs_SetOperMode函數設置了UART的工作模式。工作模式在xuartps.h文件中宏定義。

    void XUartPs_SetOperMode(XUartPs *InstancePtr, u8 OperationMode)

    1

四種工作模式的作用請參考第24篇。下表給出每種模式的宏定義:
宏定義     實際值     工作模式
XUARTPS_OPER_MODE_NORMAL     0x00U     正常模式
XUARTPS_OPER_MODE_AUTO_ECHO     0x01U     自動echo模式
XUARTPS_OPER_MODE_LOCAL_LOOP     0x02U     本地迴環模式
XUARTPS_OPER_MODE_REMOTE_LOOP     0x03U     遠程迴環模式
3.格式配置

接下來使用XUartPs_SetDataFormat函數設置UART的數據格式,包括波特率、數據位數、停止位數和奇偶校驗。調用此函數時應確保UART沒有收發數據。

    s32 XUartPs_SetDataFormat(XUartPs *InstancePtr, XUartPsFormat * FormatPtr)

    1

如果設置成功則返回XST_SUCCESS;如果該波特率在當前參考時鐘頻率下無法實現,則返回XST_UART_BAUD_ERROR表示無法設置波特率;函數的任意一個輸入參數無效時返回XST_INVALI_PARAM。

我們絕大多數情況下都會使用“8位數據、1位停止、無奇偶校驗”,因此如果想進一步提高程序效率,可以僅使用XUartPs_SetBaudRate函數來設置波特率。

    s32 XUartPs_SetBaudRate(XUartPs *InstancePtr, u32 BaudRate)

    1

使用該函數也會檢查輸入的波特率值是否有效。檢查的依據是xuartps.c中下面這個宏定義,即最大允許的波特率錯誤率:

    #define XUARTPS_MAX_BAUD_ERROR_RATE         3U    /* max % error allowed */

 

其實質上是波特率生成器產生的實際波特率與設置的波特率之間的差值。如果不滿足這個條件,便會返回XST_UART_BAUD_ERROR,保持原有波特率不變。
4.數據格式

上面的函數中使用了XuartPsFormat類型的結構體來設置UART格式。該結構體原型如下:

typedef struct {
        u32 BaudRate;    /**< In bps, ie 1200 */
        u32 DataBits;    /**< Number of data bits */
        u32 Parity;        /**< Parity */
        u8 StopBits;    /**< Number of stop bits */
} XUartPsFormat;

 

下表總結了與數據格式相關的宏定義,使用時要將其填到結構體變量的對應位置。一般波特率可以寫成數字形式,其餘三個成員都要用宏定義的形式。
宏定義     實際值     工作模式
    數據位     
XUARTPS_FORMAT_8_BITS     0U     8-bits數據位
XUARTPS_FORMAT_7_BITS     2U     7-bits數據位
XUARTPS_FORMAT_6_BITS     3U     6-bits數據位
    奇偶校驗     
XUARTPS_FORMAT_NO_PARITY     4U     無奇偶校驗
XUARTPS_FORMAT_MARK_PARITY     3U     校驗位始終爲1
XUARTPS_FORMAT_SPACE_PARITY     2U     校驗位時鐘爲0
XUARTPS_FORMAT_ODD_PARITY     1U     奇校驗
XUARTPS_FORMAT_EVEN_PARITY     0U     偶校驗
    停止位     
XUARTPS_FORMAT_2_STOP_BIT     2U     2-bits停止位
XUARTPS_FORMAT_1_5_STOP_BIT     1U     1.5-bits停止位
XUARTPS_FORMAT_1_STOP_BIT     0U     1-bit停止位
    波特率     
XUARTPS_MAX_RATE     921600U     最大波特率
XUARTPS_MIN_RATE     110U     最小波特率
XUARTPS_DFT_BAUDRATE     115200U     默認波特率
5.數據發送

程序中使用XUartPs_Send函數發送數據。這個函數是非阻塞的,輪詢模式和中斷驅動模式下都可以使用。它會盡可能地想TxFIFO填充數據,並返回發送的字節數;如果無法填充,會返回0表示發送了0字節,便於用戶處理。

中斷模式下,該函數會發送指定的緩衝區(Buffer)中的內容,中斷處理程序負責將所有數據全部發送完。此時會調用綁定的回調函數,標識發送完成。關於中斷的用法在後面文章中專門介紹。

    u32 XUartPs_Send(XUartPs *InstancePtr, u8 *BufferPtr, u32 NumBytes)

 

第二個參數是指向要發送的數據緩衝區的指針;第三個參數是發送的字節數;返回值標識實際發送的字節數。本例程序中就是利用返回值確保所有數據都依次發送(雖然本例的數量不大,但要學習這個用法)。

這個函數還有個特殊用法,如果將第三個參數設爲0,則會停止正在進行的發送操作,並將已經在TxFIFO中的所有數據都發送出去。可以用這個用法實現某些特殊功能。
————————————————
版權聲明:本文爲CSDN博主「FPGADesigner」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/FPGADesigner/article/details/88852990

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