---
# System prepended metadata

title: Xilinx
tags: [FPGA]

---

# Xilinx
## 版本配置
+ original(failed)
> 1. Vivado 2013.2(install from DVD) with [MATLAB R2012b](https://www.mathworks.com/downloads/)
     (MATLAB 版本 2011a / 2013b / 2014a / 2014b 不支援)
> 2. ISE(install from DVD)

+ now
> 1. [Vivado 2023.1](https://www.xilinx.com/support/download.html)
---

## ~~操作說明~~
1. ~~以系統管理員執行 C:\Xilinx_2023.1\Vivado\2023.1\bin\vivado.bat~~
---
### [Update Vivado Board Files](https://digilent.com/reference/programmable-logic/guides/installing-vivado-and-vitis)

+ ```step 1.``` 下載 [board files](https://github.com/Digilent/vivado-boards/archive/refs/heads/master.zip)

+ ```step 2.``` 解壓縮後將 ++new > board_files++ 複製至 ++Vivado 安裝路徑 > data > boards++ 下
  
  ![image](https://hackmd.io/_uploads/r1uB2wB06.png)

---
## [Zynq Workshop for Beginners](https://www.avnet.com/opasdata/d120001/medias/docus/3/SILICA_Xilinx_Zynq_ZedBoard_Vivado_Workshop_ver1.0.pdf)

### Exercise 1 - Getting something(anything!) working

  + ```step 1.``` 連接 UART 跟 PC
  + ```step 2.``` 連接 PROG 跟 PC
  + ```step 3.``` 把 PIN 腳 J18 設置在 2V5 的位置
  + ```step 4.``` 拔掉 SD card、連接電源線
  + ```step 5.``` 確保 jumper 設置如下    
    | Jumper | SIG connected to... |
    | -------| --------            | 
    | MIO6   | GND                 |
    | MIO5   | 3V3                 |
    | MIO4   | 3V3                 |
    | MIO3   | GND                 |
    | MIO2   | GND                 |
  + ```step 6.``` 省略
  + ```step 7.``` (Vivado)
    新建檔案 > 選擇檔案路徑(建議在 Xilinx 的目錄之下) > 選擇 RTL Project > target language 選擇 VHDL > (點選 Next 直到要選擇板子的地方) > 選擇 Boards 再選擇 Zynq 7000 ZC702 Evaluation Board(xc7z020clg484-1) > 點選 Finish 即建置完成
    ![](https://hackmd.io/_uploads/SyPhbmQxa.png)
  + ```step 8.``` 介面簡介
  + ```step 9.``` Create Block Design(左側 Flow Navigator 的 panel 下)
  + ```step 10.``` 
    點選 Add IP(在 Diagram 裡面的 + 圖示) 並選擇 ZYNQ7，之後點選 "Run Block Automation(亮綠色區塊)"，保留預設值(Apply Board Preset 已預設好)，直接點選 OK。
    + 可以看到圖中 DDR 和 FIXED_IO 處有所變化
      + Run Block Automation 前
        ![](https://hackmd.io/_uploads/r1JS-6be6.png)
    
      + Run Block Automation 後
        (DDR & Fixed IO connections are made automatically)
        ![](https://hackmd.io/_uploads/SyhGR3Zl6.png) 
  
    + 雙擊 ZYNQ7，之後會產生 Programmable Logic(PL) 圖，點選 "32b GP AXI Master Ports" 後取消勾選 "M AXI GP0 interface"，點選 OK 即完成
    + 點選 Validate Design☑ 圖示，即會顯示 Validation successful 的視窗
    + 點選 File > Save Block Design(Ctrl + S)
  + ```step 11.``` 省略
  + ```step 12.``` 右擊 Sources 裡 Design Sources 下的 .bd 檔案，然後選取 "Create HDL Wrapper"，點擊 OK 即完成 
  + ```step 13.``` 
    左側 Flow Navigator 的 panel 下，點擊 "Generate Bitstream"，保留預設值，直接點選 OK 即完成(過程需等待大約三分鐘)
    +   完成後如圖所示
    ![](https://hackmd.io/_uploads/rJgsJR-lT.png)

  + ```step 14.``` 因為是在 Zedboard 上實作，有很多的設定已經預設完成，所以不需做太多的更動
  + ```step 15.``` 省略
  + ```step 16.``` 
    + 點選 File > Export > Export Hardware...，照預設點選 Next 即完成。此步驟會將 .xsa 檔匯出 
    + 開啟 vitis(可從上方工具列 Tools > Launch Vitis IDE 打開)
    + (Vitis)
      Create new platform project
      ![](https://hackmd.io/_uploads/BJz8wlXxp.png)
    
    + 點選 Browse 開啟剛才 export 的 .xsa 檔
      ![](https://hackmd.io/_uploads/rJcUDlQep.png)

    + 開啟 .xsa 檔，會看到這塊板子所有的 Base Address 和 High Address 
      ![](https://hackmd.io/_uploads/BJ82c-Qea.png)

  + ```step 17.``` 創建 BSP(新版 SDK 已自動建立，詳細位置如圖，但要記得按下槌子🔨 重新 build 一下 project！)
    ![](https://hackmd.io/_uploads/rJ0JjZmx6.png)

  + ```step 18.``` 
    + 點選 File > New Application Project...，照預設點選 Next 直到要選擇 Templates 並選擇 "Hello World" 即完成
    + helloworld.c 檔案會在左側 Explorer panel 中 Exercise_01_stystem > Exercise_01 > src 裡
      ![](https://hackmd.io/_uploads/Hy5SQQXlT.png)
      
      ```c
      #include <stdio.h>
      #include "platform.h"
      #include "xil_printf.h"

      int main()
      {
          init_platform();

          print("Hello World\n\r");
          print("Successfully ran Hello World application");
          cleanup_platform();
          return 0;
      }
      ```
  + ```step 19.``` 確認開發板已連接電源(開發板上的 POWER LED 會亮)
  + ```step 20.``` 
    + 點選上工具列 Window > Show view... > Vitis > Vitis Serial Terminal，則 Terminal panel 即出現在畫面下方
    + 接著點擊在畫面右下角的 + 圖示，進行 serial port 連接參數設定(需注意設定完畢是否有出現連接成功的訊息)
      ![](https://hackmd.io/_uploads/r1p_rmXl6.png)
    
      ![](https://hackmd.io/_uploads/rkqxIXQxa.png)
  + ```step 21.``` 
    + 右擊左側 Explorer panel 中的專案(Exercise_01)，選取 Build Project
    + 接著再次右擊專案，選取 Run As... > Launch Hardware(Single Application Debug(GDB))
  + ```step 22.``` 待 Vitis Serial Terminal 出現 Hello World 等訊息即完成
  
---
### Exercise 2 - Using drivers to flash an LED
:::info
此次 Exercise 我們將探索使用軟體驅動程序來控制 Zynq 設備上的一個輸出腳位，以驅動一個 LED。我們將使用 "LD9"，它連接到 54 個 MIO 腳位中的腳位 7(PS_MIO7)，這些腳位可以在 Zynq 設備的 "PS" 側存取
:::

+ (Vitis)
  點選 File > New Application Project...，照預設點選 Next 直到要選擇 Templates 並選擇 "Hello World"（這個 Template 呼叫了 init_platform 函式，建議先選擇此 Template 之後再直接用它來進行修改為以下程式碼）  

  ```c
  #include <stdio.h>
  #include "platform.h"
  #include "xil_printf.h"
  
  //在我們 Exercise 1 中創建的 BSP 中找到合適的驅動程式
  //展開 Project Explorer 中的 BSP > include
  //所有的 Xilinx 驅動程式都以字母 "X" 開頭，然後有一個反映其功能的名稱
  //所以我們需要查找 "xgpiops.h"（X GPIO PS）
  #include "xgpiops.h" 
  
  //使得可以存取 Xilinx BSP 中提供的巨集
  #include "xparameters.h"

  int main()
  {
      XGpioPs_Config *GPIO_Config;
      XGpioPs my_Gpio;
      int Status;
      
      int blink;

      init_platform(); //包含 UART 的一些基本設定

      //XGpioPs_Config 結構的實體
      //XPAR_PS7_GPIO_0_DEVICE_ID 被定義於 xparameters.h 中
      GPIO_Config = XGpioPs_LookupConfig(XPAR_PS7_GPIO_0_DEVICE_ID); 

      //非常重要！！！一定要進行初始化
      Status = XGpioPs_CfgInitialize(&my_Gpio, GPIO_Config, GPIO_Config->BaseAddr); 

      //XGpioPs_SetDirectionPin 和 XGpioPs_WritePin 函式
      //允許我們將值傳送到 GPIO 內的各個暫存器外部設備
      //設定該 GPIO 腳位為 input(0) 或 output(1)
      XGpioPs_SetDirectionPin(&my_Gpio, 7, 1); //output
    
      for(int i = 0; i < 4; i++) //讓 LED 閃爍 4 次
      {   
          XGpioPs_WritePin(&my_Gpio, 7, 0); //對 GPIO 腳位寫入 '0'
          for(blink = 0; blink < 1001; blink++) //熄燈時間
          {
              printf("%d.\n", blink);
          }
          XGpioPs_WritePin(&my_Gpio, 7, 1); //對 GPIO 腳位寫入 '1'
          for(blink = 0; blink < 1001; blink++) //亮燈時間
          {
              printf("%d.\n", blink);
          }
      } 

      cleanup_platform();
      return 0;
  }
  ```
+ 在 Zynq 的 PS 中控制 GPIO 的是 XGpioPs 結構（在本次 Exercise 中宣告為 my_Gpio），其內部有四個變數

  ![](https://hackmd.io/_uploads/BJajYLKla.png)
  + 第一個是 "GpioConfig"，其資料型態為 "XGpioPs_Config"，它實際上是另一個結構（結構內部可以有結構！）
    > 其餘的三個變數都是不同的資料型態，詳細部分可以先不深入探討！
  + 如果手動在結構內部配置所有東西會是一項複雜的任務，因此 Xilinx 提供了一個 C 語言的 "XGpioPs_CfgInitialize" 函式來幫助我們完成這項工作，當結構被宣告時，其中的所有變數都是未初始化的，而它會自動為我們配置所有東西。
  + XGpioPs_CfgInitialize 函式
    <font color="gray">*XGpioPs_CfgInitialize(XGpioPs \*InstancePtr, const XGpioPs_Config \*ConfigPtr, u32 EffectiveAddr)*</font>
    
    + 函式需要三個輸入：
      + 完成宣告的 my_Gpio 實體
      + GPIO_Config 結構
      + Base address（這可以從 GPIO_Config 結構中輕鬆提取）
      
    + 函式的輸出是一個狀態值，可以讓我們知道初始化是否成功(0: Success)
    
  + 運作流程如下圖所示(==錯誤示範==) 
    ![](https://hackmd.io/_uploads/r14mnLKea.png)
    
    + 但此為 **錯誤示範**，事實上 GPIO_Config 結構尚未初始化，所以我們需要利用另外一個函式來達到初始化的工作
      + XGpioPs_LookupConfig 函式 
        <font color="gray">*XGpioPs_LookupConfig(u16 DeviceId)*</font>
      
      要呼叫函式，我們只需放入 Device_ID，而實際上 Device_ID 已被定義在 xparameters.h 中  
        
  + 正確的運作流程圖如下圖所示    
    ![](https://hackmd.io/_uploads/H1mRiBtla.png)
  
---
### Exercise 3 - Debugging

1. (Vitis)
   建立名為 Exercise_03 的 Application Project
3. 複製 exercise_02.c 為 exercise_03.c
4. 右擊左側 Explorer panel 中的專案(Exercise_03)，選取 Build Project
5. 接著再次右擊專案，選取 Debug As... > Launch  Hardware(Single Application Debug(GDB))
6. 成功完成後應該呈現如下圖(應該出現左上角的 Debug panel、被綠色標註的 init_platform()) 
   ![](https://hackmd.io/_uploads/Hk2pjVuxp.png)
6. 使用 "Step Into" 按鈕單步執行程式
   ![](https://hackmd.io/_uploads/r1g3TEOxp.png)
   
   直到進入 init_platform 函式內
   ![](https://hackmd.io/_uploads/SkPIyHOxT.png)

7. 在 "init_platform" 內部，單擊 "Step Into" 繼續深入，或使用 "Step Over" 執行程式碼的下一行
8. 在 exercise_03.c 中，找到執行 "XGpioPs_SetDirectionPin" 函式的那行，並在該行左側雙擊設置中斷點
9. 點擊綠色的 "Resume" 按鈕，讓 Debugger 執行程式直到設置中斷點的位置
10.	查看右上角的 "Variables" panel 查看函式相對應的 local variables。特別注意，其中應該會有一個 “Status” 變數，其值應為 0 以表示 "XGpioPs_CfgInitialize" 函式順利完成並無發生錯誤 
11.	在 exercise_03.c 中，將鼠標停在變數或結構的名稱上可以看到更詳細的內容
    ![](https://hackmd.io/_uploads/H1XsqBOg6.png)

12.	可以試試看在 for 迴圈的地方增加中斷點，接著使用 "Resume"、"Step Into"、"Step Over" 和 "Step Return" 按鈕，並於 "Variables" panel 中手動更改循環變數的值，觀察循環計數變數的改變

---
### Exercise 4 - Expanding your design into the programmable logic

:::info
在之前的 Exercise 中，我們僅限於 Zynq 設備的 processing system(PS) 部分，但 Zynq 不僅是一個處理器，它還提供了大量使用者可配置的 FPGA programmable logic(PL)。

PL 有多種使用模式：
+ 使用 VHDL 或 Verilog 等硬體描述語言編寫和設計自訂程式碼
+ 用 high level language synthesis tool(ex: Vivado HLS) 將 C code 轉換為硬體
+ 從目錄中新增現有的 IP **(本次我們選擇此方法)**
:::

1. 重新打開 Vivado，在 Sources 中，雙擊在 Exercise 1 中創建的 .bd 檔(design_1.bd)
2. 可以看到 Design 裡只包含 processing_system7_0，即 Zynq 設備的 PS 部分的 IP。接下來我們將在 Zynq 設備的 PL 部分中擴展此功能(添加功能來控制 8 個 LED、8 個 DIP 開關和 1 個 PMOD 擴展插座)
3. 雙擊 ZYNQ7，之後會產生 Programmable Logic(PL) 圖，點選 "32b GP AXI Master Ports" 後勾選 “M AXI GP0 interface，接著在視窗左側(Page Navigator) 選擇 "Interrupts"，將 "Fabric Interrupts" 勾選並展開，再展開 "PL-PS Interrupt Ports"，然後將 "IRQ_F2P[15:0]" 打勾，最後點選 OK 即完成
4. 在上方欄位中，點擊 "Add IP" 圖示。在搜索框中輸入 "gpio" 並雙擊 "AXI GPIO"，接著搜索 "spi"，再雙擊 "AXI Quad SPI" 
5. 雙擊 AXI GPIO，選擇 "IP Configuration"(注意周邊設備有兩個通道 "GPIO" 和 "GPIO 2"，這裡只需要對 "GPIO" 進行設置)，將 GPIO Width 設為 16 位（8 位用於 DIP 開關，8 位用於 LED）
6. 為 "AXI QUAD SPI" 周邊設備進行配置，需取消勾選 Enable STARTUP Primitive 
7. 使用上方綠色條中的 "Designer Assistance"，執行 "connection automation" ，並從對話框中選擇 "/axi_gpio_0/S_AXI"
8. 之後將三個項目皆設置為 "Auto" 並點擊 "確定"
9. 重複使用 "Designer Assistance" 對 "/axi_quad_spi_0/AXI_LITE" 進行設置
10. 執行 "Designer assistance"，選擇 "/axi_gpio_0/GPIO"，Options 裡的 Select Board Part Interface 選擇 "custom"，點擊 "確定" 
11. 執行 "Designer assistance"，並從對話框中選擇 "/axi_quad_spi_0/SPI_0"
12. SPI 周邊設備需要一個 "reference clock connection"，這必須手動連接。使用滑鼠從 SPI 區塊上的 "ext_SPI_clk" 畫一條連接到SPI區塊上的 "s_axi_aclk" 線 
13. 我們需要在 interrupt enabled mode 下使用 SPI controller，因此我們必須將來自周邊設備的中斷輸出連接到 interrupt controller。使用滑鼠從 SPI 區塊上的 "IP2INTC_Irpt" 畫一條線到 "Zynq7 Processing System" 區塊上的 "IRQ_F2P"
14. 點選左側 Flow Navigator panel 下 RTL ANALYSIS > Open Elaborated Design
15. 雙擊 Sources 裡 Design Sources 中的 .vhd 檔(design_1_wrapper.vhd)，打開後搜尋 "spi_rtl_ss_iobuf_0"，並於 spi_rtl_ss_o_0(0) 前加上 <font color="purple">**not**</font>，如下圖所示
    ```vhdl
    spi_rtl_ss_iobuf_0: component IOBUF
        port map (
            I => not spi_rtl_ss_o_0(0),
            IO => spi_rtl_ss_io(0),
            O => spi_rtl_ss_i_0(0),
            T => spi_rtl_ss_t
        );
    ```
    + 加上 <font color="purple">**not**</font> 後，應會出現以下錯誤訊息，於下方 Tcl Console 輸入指令: <font color="blue">set_property FILE_TYPE {VHDL 2008} [get_files <file>.vhd]</font> 即可解決(<file> 需填入 .vhd 的名稱)
      ![](https://hackmd.io/_uploads/ryjJUFdlp.png)

16. 點選左側 Flow Navigator panel 下 Project Manager > Add Sources，接著選擇 Add or create constraints，然後 Create File，File name 設為 my_constraints，點擊 OK 和 Finish 後即完成
17. 打開 Sources 裡的 Constraints 下的 my_constraints.xdc 檔，接著把以下內容複製進去即可
    ```tcl
    set_property PACKAGE_PIN AA11 [get_ports spi_rtl_io0_io]
    set_property PACKAGE_PIN Y10 [get_ports spi_rtl_io1_io]
    set_property PACKAGE_PIN AA9 [get_ports spi_rtl_sck_io]
    set_property PACKAGE_PIN Y11 [get_ports {spi_rtl_ss_io[0]}] 

    set_property PACKAGE_PIN F22 [get_ports {gpio_rtl_tri_io[0]}]
    set_property PACKAGE_PIN G22 [get_ports {gpio_rtl_tri_io[1]}]
    set_property PACKAGE_PIN H22 [get_ports {gpio_rtl_tri_io[2]}]
    set_property PACKAGE_PIN F21 [get_ports {gpio_rtl_tri_io[3]}]
    set_property PACKAGE_PIN H19 [get_ports {gpio_rtl_tri_io[4]}]
    set_property PACKAGE_PIN H18 [get_ports {gpio_rtl_tri_io[5]}]
    set_property PACKAGE_PIN H17 [get_ports {gpio_rtl_tri_io[6]}]
    set_property PACKAGE_PIN M15 [get_ports {gpio_rtl_tri_io[7]}]
    set_property PACKAGE_PIN T22 [get_ports {gpio_rtl_tri_io[8]}]
    set_property PACKAGE_PIN T21 [get_ports {gpio_rtl_tri_io[9]}]
    set_property PACKAGE_PIN U22 [get_ports {gpio_rtl_tri_io[10]}]
    set_property PACKAGE_PIN U21 [get_ports {gpio_rtl_tri_io[11]}]
    set_property PACKAGE_PIN V22 [get_ports {gpio_rtl_tri_io[12]}]
    set_property PACKAGE_PIN W22 [get_ports {gpio_rtl_tri_io[13]}]
    set_property PACKAGE_PIN U19 [get_ports {gpio_rtl_tri_io[14]}]
    set_property PACKAGE_PIN U14 [get_ports {gpio_rtl_tri_io[15]}]

    set_property IOSTANDARD LVCMOS25 [get_ports {spi_rtl_*}]
    set_property IOSTANDARD LVCMOS25 [get_ports {gpio_rtl_tri_io[*]}]

    set_property PULLDOWN true [get_ports {spi_rtl_*}]
    set_property PULLDOWN true [get_ports {gpio_rtl_tri_io[*]}]
    ```
    + 這些 constraints 中有 4 個與 SPI 介面的腳位位置相關；16 個與 GPIO 的腳位位置相關（8 個 DIP 開關和 8 個 LED），其餘 4 個指定了 2 個介面的 IO 標準和下拉電阻（使用 * 來概括）
18. 點選左側 Flow Navigator panel 下 SYNTHESIS > Run Synthesis，接著 Run implementation
19. 點選左側 Flow Navigator panel 下 PROGRAM AND DEBUG > Generate Bitstream
20. 點選 File > Export > Export Hardware...，接著選取 Include bitstream ，(建議將名稱改為 design_1_wrapper_bitstream)，最後點選 Next 即完成。此步驟會將 .xsa 檔匯出 
21. 現在已經有了一個新的硬體平台，可以根據它來開發應用程式

---
### Exercise 5 - Making your design interactive
:::info
+ 本練習的目標是學習如何讓處理器從 user input（這次練習中為一些 DIP 開關）讀取值，進而控制輸出（這次練習中為一些 LED 燈）。
    
+ 8 個 DIP 開關將連接到 GPIO 的 LSB（位元 7~0），LED 燈則連接到 GPIO 的 MSB（位元 15~8）。處理器會根據 DIP 開關的狀態按位輸出到 LED 燈。 
:::
    
(Vitis)
  
1. 建立名為 Exercise_05 的 Application Project ，在選擇平台的地方記得確認選取的是之前 Exercise_04 所匯出的 .xsa 檔(design_1_wrapper_bitstream) 
2. 將平台(design_1_wrapper_bitstream) build🔨 以更新 BSP  
3. 檢查 gpio driver 的相關設定
   + 在 Explorer panel 中的 design_1_wrapper_bitstream > ps7_cortexa9_0 > standalone_domain > bsp > ps7_cortexa9_0 > include 資料夾裡，確認是否存在 xgpio.h 檔案。若無出現則執行以下步驟：
     + 展開 Explorer panel 中的 design_1_wrapper_bitstream 資料夾，雙擊 platform.spr
     + 雙擊 standalone on ps7_cortexa9_0 > Board Support Package
     + 點選 Modify BSP Settings... 按鈕
     + 展開 drivers 列表後，將 axi_gpio_0 以及 axi_quad_spi_0 的 Driver 分別更改為 gpio 和 spi
       | Component      | Driver   |
       | --------       | -------- |
       | axi_gpio_0     | gpio     |
       | axi_quad_spi_0 | spi      |
   --- 
+ 程式碼：
  ```c
  #include <stdio.h>
  #include "platform.h"
  #include "xgpio.h"
  #include "xparameters.h"

  int main() 
  {
	  XGpio_Config *GPIO_Config;
	  XGpio my_Gpio;

	  // Declare some variables that we will use later
	  int Status;
	  unsigned int DIP_value;
	  unsigned int LED_value;

 
	  init_platform(); //初始化平台，包括一些 UART 的基本設定
	  printf("Exercise 5\n\r");
    
	  //XGpio_Config 結構的實體
	  GPIO_Config = XGpio_LookupConfig(XPAR_PS7_GPIO_0_DEVICE_ID);
      
      //將 GPIO 進行初始化，這步非常重要！
	  Status = XGpio_CfgInitialize(&my_Gpio, GPIO_Config, GPIO_Config->BaseAddress);

	  //設定該 GPIO 腳位為 input(0) 或 output(1)
	  //The lower(LSB) 8 bits of the GPIO are for the DIP Switches (inputs). //1
	  //The upper(MSB) 8 bits of the GPIO are for the LEDs (outputs). //0
	  XGpio_SetDataDirection(&my_Gpio, 1, 0x00ff); //0000 0000 1111 1111


	  //Go around in a loop for ever
	  while (1)
	  {
            //從 GPIO 讀取 DIP 開關的狀態
		  DIP_value = XGpio_DiscreteRead(&my_Gpio, 1);

		  //將讀取到的 DIP 值左移 8 位，並賦值予 LED_value 變數
		  LED_value = DIP_value << 8;

		  //印出變數值以幫助 debug
		  printf("DIP = 0x%04X, LED = 0x%04X\n\r", DIP_value, LED_value);
    
		  //把值寫回 GPIO
            XGpio_DiscreteWrite(&my_Gpio, 1, LED_value);
	  }

	  //Technically we should never reach this far!
      cleanup_platform();  
    
	  return(0);
  } 
  ```
+ 執行步驟：
  1. 右擊 Exercise_05 > Build Project
  2. 右擊 Exercise_05_system > Program Device(按下 Program 按鈕前建議先點選 Device > Select 按鈕 進行確認，查看是否有偵測到開發版，這部分常因偵測失敗報錯) 
    
     ![](https://hackmd.io/_uploads/H1nZZ6o-T.png)
  3. 右擊 Exercise_05 > Run As... > Launch Hardware(Single Application Debug(GDB))
  4. 接著便可以對 DIP 開關進行調整，控制 LED 的亮暗
    
+ 程式碼中特別需要注意的部分：
  1. 因為本次主要是對 PL 區塊進行控制，在之前 exercise 程式碼中為 "XGpioPs" 的部分皆改成了 "XGpio"，例如：
     + XGpioPs_Config *GPIO_Config; => XGpio_Config *GPIO_Config;
     + XGpioPs my_Gpio; => XGpio my_Gpio;
     + ... 等，
    
     而 XGpio 相關函式之詳細內容皆可於 "xgpio.h" 檔案中找到。
  2. 在 **XGpioPs_SetDirection** 函式中，0 表示 input、1 表示 output；然而在 XGpio 對應的 **XGpio_SetDataDirection** 函式中，0 表示的是 output、1 則表示 input。
     + XGpioPs_SetDirection
    <font color="gray">*void XGpioPs_SetDirection(const XGpioPs \*InstancePtr, u8 Bank, u32 Direction);*</font>
       
       ![](https://hackmd.io/_uploads/SJNqI1hbp.png)

     + XGpio_SetDataDirection
    <font color="gray">*void XGpio_SetDataDirection(XGpio \*InstancePtr, unsigned Channel, u32 DirectionMask);*</font>
       
       ![](https://hackmd.io/_uploads/SJTiwknWT.png)

---
### Exercise 6 – Reading from and writing to memory
:::info
+ 本練習將學習如何對記憶體進行讀取與寫入。
    
+ 記憶體除了在嵌入式系統中被用以儲存軟體之外；在 FPGA 設計中，使用者往往會使用一塊記憶體作為嵌入式處理器系統(embedded processor system) 和自定義硬體之間的介面，像是 Xilinx 的 dual port BlockRAMs 就經常以這種方式使用。
    
+ 另外，Zynq 中的 On Chip Memory (OCM) 使用方式和 dual port BlockRAMs 相似，而外部 （e.g. DDR） 記憶體的工作方式也大致相同，唯一的區別在於外部記憶體會由記憶體控制器來解釋來自處理器的請求並生成一個交易，該交易使用 DIMM 或設備所用的協議訊號來接收和回應存取請求。    
:::
   
(Vitis)
1. 建立名為 Exercise_06 的 Application Project ，在選擇平台的地方記得確認選取的是之前 Exercise_04 所匯出的 .xsa 檔(design_1_wrapper_bitstream) 
2. 將平台(design_1_wrapper_bitstream) build🔨 以更新 BSP  
3. 我們將再次使用由 Library Generator 生成的其中幾個 driver，這些 driver 沒有和任何周邊設備相關聯，但是仍然在所有處理器設計中提供給 EDK(Embedded Development Kit) 使用者
   + 在 Explorer panel 中的 design_1_wrapper_bitstream > ps7_cortexa9_0 > standalone_domain > bsp > ps7_cortexa9_0 > include 資料夾裡，找到 xil_io.h 檔案。其中包含專門設計用於從記憶體中讀取和寫入不同字節寬度資料的各種函式：
     + Reading
       + Xil_In8
       + Xil_In16
       + Xil_In32
     + Writing
       + Xil_Out8
       + Xil_Out16
       + Xil_Out32
    
  
+ 下面的例子說明了我們可以如何存取 Zynq 的 On-Chip Memory(OCM)：
  + 程式碼：  
    ```c
    #include <stdio.h>
    #include "platform.h"
    #include "xil_io.h"
    
    int main(void) 
    {  
        init_platform(); //初始化平台，包括一些 UART 的基本設定
        
        printf("Exercise 6\n\r");
    
        int result1; //integers are 32 bits wide!
        int result2; //integers are 32 bits wide!
	    
        //將 byte 大小的資料寫入從 DDR_BASEADDR 開始的記憶體區塊，連續四次
        Xil_Out8(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 0, 0x12); //寫入資料 0x12
	    Xil_Out8(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 1, 0x34); //寫入資料 0x34 
	    Xil_Out8(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 2, 0x56); //寫入資料 0x56
	    Xil_Out8(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 3, 0x78); //寫入資料 0x78 
	    
        //自 DDR_BASEADDR 開始的記憶體區塊讀取 word 大小的資料
        result1 = Xil_In32(XPAR_PS7_RAM_0_S_AXI_BASEADDR); //讀取資料並存至 result1 變數
	  
        //將 half-word 大小的資料寫入接續的記憶體區塊
        Xil_Out16(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 4, 0x9876); //寫入資料 0x9876
	    Xil_Out16(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 6, 0x5432); //寫入資料 0x5432
	    
        //自 DDR_BASEADDR + 4(byte) 的記憶體區塊讀取 word 大小的資料
        result2 = Xil_In32(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 4); //讀取資料並存至 result2 變數
	    
        //印出結果
        printf("result1 = 0x%08X, result2 = 0x%08X\n\r", result1, result2);
	    
        cleanup_platform();
    
        return(0);
    }
    ```
    + 執行結果：
      > result1 = 0x78563412, result2 = 0x54329876
+ Challenge:
  + 程式碼：
    
    ```c
    #include <stdio.h>
    #include "platform.h"
    #include "xil_io.h"
  
    int main(void)
    {
        init_platform(); //初始化平台，包括一些 UART 的基本設定
    
        printf("Exercise 6 Challenge\n\r");
	  
        unsigned long long result1; //integers are 64 bits wide!
	    unsigned long long result2; //integers are 64 bits wide!
	
        //Write eight data values, each one byte wide with the following values: 0xAB, 0xFF, 0x34, 0x8C, 0xEF, 0xBE, 0xAD, 0xDE.
        //The values should be written to consecutive addresses, starting at the base address of the Zynq OCM region.
        Xil_Out8(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 0, 0xAB);
	    Xil_Out8(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 1, 0xFF);
	    Xil_Out8(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 2, 0x34);
	    Xil_Out8(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 3, 0x8C);
	    Xil_Out8(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 4, 0xEF);
	    Xil_Out8(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 5, 0xBE);
	    Xil_Out8(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 6, 0xAD);
	    Xil_Out8(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 7, 0xDE);
	
        result1 = Xil_In64(XPAR_PS7_RAM_0_S_AXI_BASEADDR);
	
        //Leave a gap in the OCM memory of 2 "words" in size following the values you have just written. 
        //Then write four "half-word" data values at the next available addresses in the OCM, of values 0x1209, 0xFE31, 0x6587 and 0xAAAA.
        Xil_Out16(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 24, 0x1209);
	    Xil_Out16(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 26, 0xFE31);
	    Xil_Out16(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 28, 0x6587);
	    Xil_Out16(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 30, 0xAAAA);
	
        result2 = Xil_In64(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 24);
	  
        printf("result1 = 0x%llx, result2 = 0x%llx\n\r", result1, result2);

        //---
        //Write the value of 0x00000000(32 bits of data) to address 0xE000A204.
        Xil_Out32(0xE000A204, 0x00000000);
	
	    int word1; // integers are 32 bits wide!
        int word2; // integers are 32 bits wide!
    
        //Read two words of data starting from the base address of the OCM region.
        word1 = Xil_In32(XPAR_PS7_RAM_0_S_AXI_BASEADDR);
	    word2 = Xil_In32(XPAR_PS7_RAM_0_S_AXI_BASEADDR + 4);
    
        int word3; // integers are 32 bits wide!
      
        //Read a word(32 bits) of data from address 0xE000A064.
        word3 = Xil_In32(0xE000A064);
	  
        printf("word1 = 0x%08x\n", word1);
	    printf("word2 = 0x%08x\n", word2);
	    printf("word3 = 0x%08x\n\r", word3);

        cleanup_platform();
    
	    return(0);
    }  
    ```
  + 執行結果：
    > result1 = 0xdeadbeef8c34ffab, result2 = 0xaaaa6587fe311209
    >
    >
    > word1 = 0x8c34ffab
    >
    > word2 = 0xdeadbeef
    >
    > word3 = 0x0020c000
    
    ★ 位址 0xE000A064 的值(word3) 呈現了 push button PB1 和 PB2 的狀態。PB1 的狀態由 bit 18 表示；PB2 的狀態由 bit 19 表示：
    | <font color="blue">PB2</font> | <font color="red">PB1</font> | word3(binary)                                                                                | word3(heximal)
    | ------------------------------| ---------------------------- | ------------------------------------------------------------------------------------ | --------------|
    | 0                             | 0                            | 10 **<font color="blue">0</font>** **<font color="red">0</font>** 00 1100 0000 0000 0000 | 0x20c000 |
    | 0                             | 1                            | 10 **<font color="blue">0</font>** **<font color="red">1</font>** 00 1100 0000 0000 0000 | 0x24c000 |
    | 1                             | 0                            | 10 **<font color="blue">1</font>** **<font color="red">0</font>** 00 1100 0000 0000 0000 | 0x28c000 |
    | 1                             | 1                            | 10 **<font color="blue">1</font>** **<font color="red">1</font>** 00 1100 0000 0000 0000 |  0x2cc000 |
    
---
### Exercise 7 - Timers (Polled mode)  
:::info
* 查找初始化定時器所需的配置資訊
* 使用配置資訊初始化定時器
* 向定時器載入一個數值
* 啟動定時器
* 讀取定時器的 counter register 當前值
* 從初始值重新啟動定時器
:::
    
1. 透過<font color="red">**迴圈**</font>的方式來控制處理器速度的延遲
   ```c
   for (i=0; i<2000; i++)
   {
       print(“.”);
   }                    
   ```   
                     
   + 缺點:
     + 處理時間非常的低效
     + 無法準確的利用迴圈來控制時間，只能透過調整迴圈大小，慢慢進行調整
     + 處理器時間十分珍貴，不應該耗在無功用的迴圈當中，應該花在其他有功用的程式碼上
                   
2. 透過<font color="red">**定時器**</font>的方式來控制處理器速度的延遲       
   + 控制的時間較精準
   + 已經被添加到匯流排結構中的硬體
    
+ 程式碼：

  ```c
  #include <stdio.h>
  #include "platform.h"
  #include "xscutimer.h"
  #include "xparameters.h"

  int main()
  {
      init_platform();
    
      int Status;
      int timer_value;
      int counter = 0;

	  XScuTimer my_Timer;
	  XScuTimer_Config *Timer_Config; //用於配置定時器

	  //Timer_Config 結構的實體
	  Timer_Config = XScuTimer_LookupConfig(XPAR_PS7_SCUTIMER_0_DEVICE_ID);

	  //將定時器初始化
	  Status = XScuTimer_CfgInitialize(&my_Timer, Timer_Config, Timer_Config->BaseAddr);

	  //將定時器載入一個代表一秒的值
	  //SCU 定時器的時脈頻率是 CPU 頻率的一半
	  XScuTimer_LoadTimer(&my_Timer,XPAR_PS7_CORTEXA9_0_CPU_CLK_FREQ_HZ / 2);
    
	  //開始定時器的倒數（它是往下數）
	  XScuTimer_Start(&my_Timer);

	  while(1)
	  {
	      //讀取定時器的值
          timer_value = XScuTimer_GetCounterValue(&my_Timer);
          if (timer_value == 0) //如果數到 0
          {
              //重新載入原始值到定時器並重新開始
              XScuTimer_RestartTimer(&my_Timer);
              //印出 counter 的值
              printf("Timer has reached zero. %d times\n\r", counter++);
          }
          else 
          {
              //印出定時器的值
              printf("Timer is still running. (Timer value = %d)\n\r", timer_value);
          }
      }
    
      cleanup_platform();
    
      return 0;
    }
  ```
---
### Exercise 8 - Timers (Interrupt mode)
      
:::info
+ 中斷可以由許多不同的周邊裝置產生。在這次練習中，我們將從上一個練習中使用的 **Snoop Control Unit Timer** 周邊裝置產生中斷。
    
+ Zynq 的文件指出，每當**定時器計數到零**時，就會產生一個**中斷**，所以我們將利用這一功能來進行本次的練習。
    
+ 為了使用中斷，我們需要使用更多的**驅動函式**來啟用定時器上的中斷功能，啟用 **ARM 處理器**上的中斷，並設置中斷控制器。
    
+ 在 ARM Cortex-A9 這樣的處理器中，所有中斷都是由**通用中斷控制器(General Interrupt Controller, GIC)** 管理並連接的。
:::    
     
+ 軟體流程的中斷  
    
  ![1](https://hackmd.io/_uploads/rJ5G1RuEp.png)
  
  + **Main application:** 
    這代表正在運行的主程序，它在中斷事件發生時正在處理的任務。
    
  + **Xilinx supplied interrupt handler:**
    當中斷發生時，系統首先跳轉到由 Xilinx 提供的一個通用中斷處理器。這個處理器負責保存處理器狀態，包括執行階段和內部暫存器的內容。
    
  + **Your custom interrupt handler:**
    Xilinx 的中斷處理器會調用你為特定中斷事件編寫的自定義中斷處理器。在這裡，你可以定義當特定中斷發生時需要執行的任務。
    
  + **Any other functions you call from your interrupt handler:**
    你的中斷處理器可能會呼叫其他函式來完成處理中斷所需的特定工作。
    
  + **Return to Xillinx supplied interrupt handler:**
    一旦你的自定義中斷處理器和任何其他相關函式執行完畢，控制權會返回到 Xilinx 的中斷處理器，以便完成任何剩餘的清理工作。
    
  + **Continue with main application:**
    當所有中斷相關的任務完成後，處理器的狀態會恢復，然後主應用程式將繼續執行中斷發生之前的任務。

+ Block diagram 
  + A simplified view of a Zynq device at power-up.
    ![image](https://hackmd.io/_uploads/HkPhGk546.png)
  
  1. 在像 ARM Cortex-A9 這樣的處理器中，所有中斷都是由通用中斷控制器(GIC) 管理並連接的。因此，我們的第一項任務是創建通用中斷控制器和定時器結構的實體，並初始化它們。
  2. 接下來我們需要初始化 ARM 處理器上的例外處理功能。 
  3. 當發生中斷時，處理器首先必須查詢中斷控制器，以找出哪個周邊產生了中斷。
  4. 在中斷控制器上啟用定時器的中斷輸入。
  5. 我們需要啟用定時器上的中斷輸出。
  6. 最後，我們需要在 ARM 處理器上啟用中斷處理。
    
+ 程式碼：    
```c    
#include <stdio.h>
#include "platform.h"
#include "xscutimer.h"
#include "xparameters.h"
#include "xscugic.h"

#define INTERRUPT_COUNT_TIMEOUT_VALUE 20

static void my_timer_interrupt_handler(void *CallBackRef);

//global variable
int InterruptCounter = 0;

int main()
{
    init_platform(); //初始化平台

    int Status;

    XScuTimer my_Timer;
    XScuTimer_Config *Timer_Config; //用於配置定時器

    XScuGic my_Gic;
    XScuGic_Config *Gic_Config; //用於配置 GIC

    //Gic_Config 結構的實體
    Gic_Config = XScuGic_LookupConfig(XPAR_PS7_SCUGIC_0_DEVICE_ID);
    
    //初始化 GIC
    Status = XScuGic_CfgInitialize(&my_Gic, Gic_Config, Gic_Config->CpuBaseAddress);

    //Timer_Config 結構的實體
    Timer_Config = XScuTimer_LookupConfig(XPAR_PS7_SCUTIMER_0_DEVICE_ID);
    
    //初始化定時器
    Status = XScuTimer_CfgInitialize(&my_Timer, Timer_Config, Timer_Config->BaseAddr);
   
    
    //在 ARM 處理器上初始化異常處理機制
    Xil_ExceptionInit();

    //所有中斷都會通過中斷控制器，所以 ARM 處理器必須由中斷控制器得知哪個周邊發生了中斷
    //將 Xilinx 提供的通用中斷處理器(XScuGic_InterruptHandler) 連接到處理器中的中斷處理邏輯 
    Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_IRQ_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, &my_Gic);

    //將特定的中斷處理器函數(my_timer_interrupt_handler) 與特定的中斷源(SCU 計時器中斷 XPAR_SCUTIMER_INTR) 相連接
    Status = XScuGic_Connect(&my_Gic, XPAR_SCUTIMER_INTR, (Xil_ExceptionHandler)my_timer_interrupt_handler, (void *)&my_Timer);

    //在通用中斷控制器(GIC) 上啟用與 SCU 定時器相關聯的中斷輸入
    XScuGic_Enable(&my_Gic, XPAR_SCUTIMER_INTR);

    //啟用了定時器自身的中斷輸出
    XScuTimer_EnableInterrupt(&my_Timer);

    //在 ARM 處理器上啟用中斷
    Xil_ExceptionEnable();

    //向定時器加載一個表示一秒真實時間的值
    //SCU 定時器的時鐘頻率是 CPU 的一半
    //CPU clock 頻率為 666 MHZ，SCU Timer clock 頻率為 333 MHZ
    XScuTimer_LoadTimer(&my_Timer, XPAR_PS7_CORTEXA9_0_CPU_CLK_FREQ_HZ / 2);

    //啟用定時器上的自動重載模式。當它到期時，會自動重新加載原始值
    //這意味著計時間隔永遠不會因為中斷處理器運行所花的時間而偏差
    XScuTimer_EnableAutoReload(&my_Timer);

    //啟動 SCU 定時器（倒數計時）
    XScuTimer_Start(&my_Timer);

    while(1)
    {
        //此時處理器將只是閒置
        //如果有中斷發生我們會在 UART 上看到訊息
    
        //檢查我們是否已經服務了超過 20 次中斷
        if(InterruptCounter >= INTERRUPT_COUNT_TIMEOUT_VALUE)
        {
            break;
        }
    }

    //print 到 UART，顯示我們已經跳出 while 迴圈
    printf("如果我們看到這個訊息，那麼我們已經跳出了 while 迴圈\n\r");

    //在處理器中 disable 中斷
    Xil_ExceptionDisable();

    //斷開定時器的中斷連接
    XScuGic_Disconnect(&my_Gic, XPAR_SCUTIMER_INTR);

    cleanup_platform(); 

    return 0;
}

static void my_timer_interrupt_handler(void *CallBackRef)
{
    //Xilinx 驅動程式使用特殊參數 "CallBackRef"，自動將在中斷中生成的周邊實體傳入
    //宣告一個定時器的實體，並將其賦值給 CallBackRef
    XScuTimer *my_Timer_LOCAL = (XScuTimer *) CallBackRef;

    //檢查定時器計數器是否已經過期
    /*
         （嚴格來說，這個檢查不是必要的，
          我們首次進入這個 handler 的原因本來就是因為定時器已經過期了！
          這只是一個範例，用來說明如何使用 callback reference 作為過期定時器實體的指標）
    */
    //如果我們有兩個定時器，我們可以為兩者使用相同的 handler，
    //"CallBackRef" 將始終告訴我們哪個定時器產生了中斷
    
    
    if (XScuTimer_IsExpired(my_Timer_LOCAL))
    {
        //在定時器中清除中斷標誌，這樣我們就不會兩次服務相同的中斷
        XScuTimer_ClearInterruptStatus(my_Timer_LOCAL);

        //累加中斷次數
        InterruptCounter++;

        //print 一些東西到 UART，顯示我們正在中斷處理器中
        printf("\n\r** 這個訊息來自中斷處理器！ (%d) **\n\r\n\n\r", InterruptCounter);

        //檢查我們是否已經超過了定義的中斷數量
        if (InterruptCounter >= INTERRUPT_COUNT_TIMEOUT_VALUE)
        {
            //停止定時器自動重載，這樣我們就不會再得到任何中斷
            //如果我們沒有 CallBackRef，我們就無法做到這一點
            XScuTimer_DisableAutoReload(my_Timer_LOCAL);
        }
    }
}
```
      
---
### Exercise 9 - Talking to external components
:::info
在 Exercise 4 中我們對 PL 增加了兩個外部設備，一個是 GPIO、一個則是 SPI。GPIO 的部份我們在 Exercise 5 中已經練習使用了，本次練習將要學習如何使用 SPI 並用它來控制外部 PMOD 子板上的設備。      
::: 
    
+ SPI (Serial Peripheral Interface) 
    
  ![image](https://hackmd.io/_uploads/H1O7J0zr6.png)

  + 一種序列周邊介面，允許 master 和 slave 使用四條線進行通訊：
    + SCLK(++S++erial ++CL++oc++K++)
      + master 和 slave 共用同一個 clock
    + MOSI(++M++aster ++O++ut ++S++lave ++I++n)
    + MISO(++M++aster ++I++n ++S++lave ++O++ut)
    + SS(++S++lave ++S++elect)
      + 當 SS 為低電位時，表示該 slave 被選中進行通訊
  
  + 可分為單工/半雙工/全雙工
    + 單工：線路上的訊號只能做單向傳送，類似於廣播
    + 半雙工：線路上的訊號可以雙向傳送，但是不能同時傳送
    + 全雙工：線路上的訊號可以同時雙向傳送  
  + 實際上，SPI 並沒有「讀」和「寫」的具體概念，僅僅以「傳輸」的概念運行

+ PMOD(Peripheral MODule)
  
    ![image](https://hackmd.io/_uploads/S1awlAMSa.png)

  + 一種使用 6 或 12 接腳連接器連接擴展模組的規格，由 Digilent Inc. 設計
  + 由於提供了一種標準化的連接方式，使得開發者可以輕鬆地擴展其電路和系統。不同廠商生產的 PMOD 模組可以在支持相同標準的硬體平台上通用，促進了外部設備模組的可換性和相容性，並提高了擴展性  
  + 存在大量的 PMOD 擴展模組，涵蓋的應用範圍從簡單的七段顯示器到資料轉換器、GPS 接收器、SD 卡介面、搖桿、旋轉編碼器、實時時鐘、PS/2 連接器等各種應用  

\--    
    
+ 本次 Exercise 我們將使用 Maxim 的一個名為 "MAX31723PMB1" 的溫度感測 PMOD 板
  + 其由一個安裝在 6 pins PMOD 板上的 MAX31723 裝置組成
    ![image](https://hackmd.io/_uploads/ryjmuAfHp.png)
  + 其中 4 pins 用於 SPI 連接
  + 剩下的 2 個 pins 則用於 VCC 和 GND
  + (MAX31723 還可以在 3 pins I2C 模式下使用，我們這次選擇使用 SPI)   

+ MAX31723 有很多功能，但我們將僅使用其三個內部暫存器（TEMP~MSB~ register、TEMP~LSB~ register、configuration register）。它的每個暫存器都有一個地址，可以通過 SPI 連接發送兩個 byte 進行讀寫
  + write: 第一個 byte 包含要寫入的暫存器地址，第二個 byte 包含要寫入暫存器的值
  + read: 第一個 byte 包含要讀的暫存器地址，而第二個 byte 是一個 dummy byte。MAX31723 將對 master 在 byte 1 中提供的地址進行讀取，並忽略第二個進來的 byte；作為回應，當它收到 byte 1 時，將輸出一個 dummy value，並在 byte 2 發送暫存器中的資料    
    ![image](https://hackmd.io/_uploads/S1uYp0zS6.png)
    
    > 在全雙工的 SPI 中，為了保持通訊的同步，需要確保在每個資料傳輸週期中 master 和 slave 都能發送和接收相同數量的 byte，確保通訊的平衡和正確性。
    
+ 我們將從 MAX31723 讀取兩個暫存器，總共需要傳輸 4 個 byte（2 個地址 byte + 2 個 dummy byte）
  + TEMP~LSB~ 暫存器位於地址 0x01
  + TEMP~MSB~ 暫存器位於地址 0x02 
  
  使用這兩個暫存器的資料，可以計算出當前的溫度。TEMP~MSB~ 的 byte 以二進制補數形式表示溫度的整數部分（1 ℃、2 ℃ ... 100 ℃ 等），而 TEMP~LSB~ 表示溫度的小數部分（二進制字中的 bits 表示 1/2 ℃、1/4 ℃、1/8 ℃ 等）。 
    
+ 在讀取溫度暫存器之前，必須使用地址為 0x80 的配置暫存器來啟用設備。我們將寫入值 0x00，清除配置暫存器中的所有 bits

+ 我們在前一個之前添加到 .xdc 文件中的所有腳位 constraints 足以將 SPI 連接路由到 ZedBoard 上的 PMOD 插座 "JA1"（位於 DIP 開關旁邊）。所以只需將 MAX31723PMB1 模組插入 JA1 連接器的上排接腳，且模組的 jumper 朝上即可
    
+ 我們將會在中斷模式下使用這個裝置

+ 程式碼：
  ```c=
  /***************************** Include Files *********************************/

  #include "xparameters.h"
  #include "xscugic.h"
  #include "xil_exception.h"
  #include <stdio.h>
  #include "xspi.h"		/* SPI device driver */
  #include "math.h"

  #define MAX31723_CONFIG_REG_ADDRESS 0x80
  #define MAX31723_TEMP_LSB_REG_ADDRESS 0x01
  #define MAX31723_TEMP_MSB_REG_ADDRESS 0x02

  #define MAX31723_THERMOMETER_RESOLUTION_9BIT_MODE 0x00
  #define MAX31723_THERMOMETER_RESOLUTION_10BIT_MODE 0x02
  #define MAX31723_THERMOMETER_RESOLUTION_11BIT_MODE 0x04
  #define MAX31723_THERMOMETER_RESOLUTION_12BIT_MODE 0x06

  #define MAX31723_CONTINUOUS_TEMPERATURE_CONVERSION_MODE 0x00
  #define MAX31723_COMPARATOR_MODE 0x00
  #define MAX31723_DISABLE_ONE_SHOT_TEMPERATURE_CONVERSION 0x0

  // Define the temperature calibration offset here
  #define TEMPERATURE_CALIBRATION_OFFSET -3.30

  /*
   *  This is the size of the buffer to be transmitted/received in this example.
   */
  #define BUFFER_SIZE 4

  /**************************** Type Definitions *******************************/
  /*
   * The following data type is used to send and receive data on the SPI
   * interface.
   */
  typedef u8 DataBuffer[BUFFER_SIZE];

  /************************** Function Prototypes ******************************/

  void SpiIntrHandler(void *CallBackRef, u32 StatusEvent, u32 ByteCount);
  void display_buffers(void);
  void clear_SPI_buffers(void);
  float read_current_temperature(XSpi *SpiInstance);

  /************************** Variable Definitions *****************************/

  /*
   * The following variables are shared between non-interrupt processing and
   * interrupt processing such that they must be global.
   */
  volatile int SPI_TransferInProgress;
  int SPI_Error_Count;

  /*
   * The following variables are used to read and write to the  Spi device, they
   * are global to avoid having large buffers on the stack.
   */
  u8 ReadBuffer[BUFFER_SIZE];
  u8 WriteBuffer[BUFFER_SIZE];

  int main(void)
  {
	  XSpi_Config *SPI_ConfigPtr;
	  XScuGic_Config *IntcConfig;
	  XScuGic IntcInstance;		/* Interrupt Controller Instance */
	  static XSpi SpiInstance;	 /* The instance of the SPI device */

	  int Status;
	  float temperature;
	  float previous_temperature;

	  // Initialise the SPI driver so that it is ready to use.
	  SPI_ConfigPtr = XSpi_LookupConfig(XPAR_PS7_QSPI_0_DEVICE_ID);
	  if (SPI_ConfigPtr == NULL) return XST_DEVICE_NOT_FOUND;
	  Status = XSpi_CfgInitialize(&SpiInstance, SPI_ConfigPtr, SPI_ConfigPtr->BaseAddress);
	  if (Status != XST_SUCCESS) return XST_FAILURE;

	  // Reset the SPI peripheral
	  XSpi_Reset(&SpiInstance);

	  // Initialise the Interrupt controller so that it is ready to use.
	  IntcConfig = XScuGic_LookupConfig(XPAR_SCUGIC_0_DEVICE_ID);
	  if (NULL == IntcConfig) return XST_FAILURE;
	  Status = XScuGic_CfgInitialize(&IntcInstance, IntcConfig, IntcConfig->CpuBaseAddress);
	  if (Status != XST_SUCCESS) return XST_FAILURE;

	  // Initialise exceptions on the ARM processor
	  Xil_ExceptionInit();

	  // Connect the interrupt controller interrupt handler to the hardware interrupt handling logic in the processor.
	  Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_IRQ_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, &IntcInstance);

	  // Connect a device driver handler that will be called when an interrupt
	  // for the device occurs, the device driver handler performs the
	  // specific interrupt processing for the device.
	  Status = XScuGic_Connect(&IntcInstance, XPAR_FABRIC_AXI_QUAD_SPI_0_IP2INTC_IRPT_INTR, (Xil_ExceptionHandler)XSpi_InterruptHandler, (void *)&SpiInstance);
	  if (Status != XST_SUCCESS) return Status;

	  // Enable the interrupt for the SPI peripheral.
	  XScuGic_Enable(&IntcInstance, XPAR_FABRIC_AXI_QUAD_SPI_0_IP2INTC_IRPT_INTR);

	  // Enable interrupts in the Processor.
	  Xil_ExceptionEnable();

	  // Perform a self-test to ensure that the hardware was built correctly.
	  Status = XSpi_SelfTest(&SpiInstance);
	  if (Status != XST_SUCCESS) return XST_FAILURE;

	  printf("MAX31723PMB1 PMOD test\n\r\n\r");

	  // Run loopback test only in case of standard SPI mode.
	  if (SpiInstance.SpiMode != XSP_STANDARD_MODE) return XST_SUCCESS;

	  // Setup the handler for the SPI that will be called from the interrupt
	  // context when an SPI status occurs, specify a pointer to the SPI
	  // driver instance as the callback reference so the handler is able to
	  // access the instance data.
	  XSpi_SetStatusHandler(&SpiInstance, &SpiInstance, (XSpi_StatusHandler)SpiIntrHandler);

	  // Set the SPI device to the correct mode for this application
	  printf("Setting the SPI device into Master mode...");
	  Status = XSpi_SetOptions(&SpiInstance, XSP_MASTER_OPTION + XSP_MANUAL_SSELECT_OPTION + XSP_CLK_PHASE_1_OPTION);
	  if (Status != XST_SUCCESS) return XST_FAILURE;
	  printf("DONE!!\n\r");

	  // Select the SPI Slave.  This asserts the correct SS bit on the SPI bus
	  XSpi_SetSlaveSelect(&SpiInstance, 0x01);

	  // Start the SPI driver so that interrupts and the device are enabled.
	  printf("Starting the SPI driver, enabling interrupts and the device...");
	  XSpi_Start(&SpiInstance);
	  printf("DONE!!\n\r");

	  printf("\n\r\n\r");
	  printf("Writing to the MAX31723 Config Register...");

	  // Clear the SPI read and write buffers
	  clear_SPI_buffers();

	  // Put the commands for the MAX31723 device in the write buffer
	  WriteBuffer[0] = MAX31723_CONFIG_REG_ADDRESS;
	  WriteBuffer[1] = MAX31723_DISABLE_ONE_SHOT_TEMPERATURE_CONVERSION +
			  MAX31723_COMPARATOR_MODE +
			  MAX31723_THERMOMETER_RESOLUTION_12BIT_MODE +
			  MAX31723_CONTINUOUS_TEMPERATURE_CONVERSION_MODE;  // Set the CONFIG register to a basic state

	  // Transmit the data.
	  SPI_TransferInProgress = TRUE;
	  Status = XSpi_Transfer(&SpiInstance, WriteBuffer, NULL, 2);

	  while (SPI_TransferInProgress);  // Wait here until the SPI transfer has finished
	  printf("DONE!\n\r\n\r\n\r");

	  // An endless loop which reads and displays the current temperature
	  while(1)
	  {
		  temperature = read_current_temperature(&SpiInstance);

		  // Check to see if the temperature is different from the last reading.
		  // Only update the display on the UART if it is different.
		  if (previous_temperature != temperature)
		  {
			  printf("Temperature = %3.4f  {Temperature calibration offset = %3.4f}\n\r", temperature, TEMPERATURE_CALIBRATION_OFFSET);
			  previous_temperature = temperature;
		  }
	  }

	  // Disable and disconnect the interrupt system.
	  XScuGic_Disconnect(&IntcInstance, XPAR_FABRIC_AXI_QUAD_SPI_0_IP2INTC_IRPT_INTR);

	  return XST_SUCCESS;
  }

  void SpiIntrHandler(void *CallBackRef, u32 StatusEvent, u32 ByteCount)
  {
	  //printf("** In the SPI Interrupt handler **\n\r");
	  //printf("Number of bytes transferred, as seen by the handler = %d\n\r", ByteCount);

	  // Indicate the transfer on the SPI bus is no longer in progress
	  // regardless of the status event.
	  if (StatusEvent == XST_SPI_TRANSFER_DONE)
	  {
		  SPI_TransferInProgress = FALSE;
	  }
	  else	  // If the event was not transfer done, then track it as an error.
	  {
		  printf("\n\r\n\r ** SPI ERROR **\n\r\n\r");
		  SPI_Error_Count++;
	  }
  }

  void display_buffers(void)
  {
	  int i;
	  for(i=0; i<BUFFER_SIZE; i++)
	  {
		  printf("Index 0x%02X  -->  Write = 0x%02X  |  Read = 0x%02X\n\r", i, WriteBuffer[i], ReadBuffer[i]);
	  }
  }

  void clear_SPI_buffers(void)
  {
	  int SPI_Count;

	  // Initialize the write buffer and read buffer to zero
	  for (SPI_Count = 0; SPI_Count < BUFFER_SIZE; SPI_Count++)
	  {
		  WriteBuffer[SPI_Count] = 0;
		  ReadBuffer[SPI_Count] = 0;
	  }

  }

  float read_current_temperature(XSpi *SpiInstance)
  {
	  u8 Temperature_LSB = 0;
	  u8 Temperature_MSB = 0;
	  float Temperature_LSB_float = 0;
	  float Temperature_MSB_float = 0;
	  float Temperature_float = 0;
	  int Status = 0;
	  int i = 0;

	  // Clear the SPI read and write buffers
	  clear_SPI_buffers();

	  // Put the commands for the MAX31723 device in the write buffer
	  WriteBuffer[0] = MAX31723_TEMP_MSB_REG_ADDRESS;
	  WriteBuffer[1] = 0x00000000;

	  // Transmit the data.
	  SPI_TransferInProgress = TRUE;
	  Status = XSpi_Transfer(SpiInstance, WriteBuffer, ReadBuffer, 2);

	  while (SPI_TransferInProgress);  // Wait here until the SPI transfer has finished

	  // Fetch the byte of data from the ReadBuffer
	  Temperature_MSB = ReadBuffer[1];

	  // Clear the SPI read and write buffers
	  clear_SPI_buffers();

	  // Put the commands for the MAX31723 device in the write buffer
	  WriteBuffer[0] = MAX31723_TEMP_LSB_REG_ADDRESS;
	  WriteBuffer[1] = 0x00000000;

	  // Transmit the data.
	  SPI_TransferInProgress = TRUE;
	  Status = XSpi_Transfer(SpiInstance, WriteBuffer, ReadBuffer, 2);
	  if (Status != XST_SUCCESS) return XST_FAILURE;

	  while (SPI_TransferInProgress);  // Wait here until the SPI transfer has finished

	  // Fetch the byte of data from the ReadBuffer
	  Temperature_LSB = ReadBuffer[1];

	  if (Temperature_MSB & 0x80)  // If the sign bit is a '1'
	  {
		  Temperature_LSB_float = (float)Temperature_LSB;

		  Temperature_MSB = (~Temperature_MSB) + 1;
		  Temperature_MSB_float = 0 - (float)Temperature_MSB;
		  Temperature_LSB_float = 0;
		  for (i=0; i<4; i++)
		  {
			  if (Temperature_LSB & (0x80 >> i))
			  {
				  Temperature_LSB_float += 0.5 / pow(2, i);  // For this to work, the -lm switch must be added to the linker command line
			  }
		  }
	  }
	  else
	  {
		  Temperature_LSB_float = (float)Temperature_LSB / 256;
		  Temperature_MSB_float = (float)Temperature_MSB;
	  }
	  Temperature_float = Temperature_MSB_float + Temperature_LSB_float + TEMPERATURE_CALIBRATION_OFFSET;

	  return (Temperature_float);
  }  
  ```
+ 在程式碼中：    
  + 因為 SPI 可能因為多種原因（傳輸完成、錯誤條件等）生成中斷，所以 interrupt handler 必須在採取任何操作之前檢查導致中斷的事件。因此，這個練習中的 interrupt handler 略微先進，它必須先進行一些檢查：如果中斷條件是期望的結果（傳輸完成），則 handler 會將 global variable 設置回原始值，main() 就可以繼續運行了
    
  + 使用了 global variable 實現 SPI 的兩個 buffer： 1 個 read buffer 和 1 個 write buffer。然後，這些 buffer 被傳遞給 SPI drivers，在 SPI 傳輸期間，它們會一個 byte、一個 byte 地被發送/接收   

---
### Exercise 10 – Autonomous Boot
   
:::info
+ 本次練習將讓 ZedBoard 在通電後自動啟動。
+ 創建一個 First Stage Boot Loader （FSBL），並將應用程序軟件、FSBL 和 Programmable Logic hardware bitstream 合併成一個 flash image 檔案。
+ 需準備 SD 卡、SD 讀卡機
:::
    
+ ### xilffs libraries
    xilffs 是 Xilinx 提供用於嵌入式系統的文件系統庫，它是 Xilinx 軟件開發工具箱（SDK）的一部分，這些系統庫實現了 FAT 文件系統（File Allocation Table，文件分配表），使得 Xilinx 的處理器能夠讀寫存儲在 FAT 格式的存儲裝置（如 **SD 卡**、USB 驅動器等）上的文件。

    + xilffs 提供了以下幾個主要功能：
        + 文件讀寫：使處理器可以創建、讀取、寫入和刪除文件。
        + 目錄操作：支持在 FAT 文件系統中創建、讀取和管理目錄。
        + 與底層存儲介面的接口：xilffs 庫提供了與底層存儲設備（如 SD 卡）的接口，這通常涉及到與 SD 卡控制器或其他類型的存儲控制器的通訊。
        + 兼容性：支持常見的 FAT 格式，如 FAT16 和 FAT32，這使得與各種操作系統和設備的互操作性成為可能。
    
+ ### FSBL（First Stage Boot Loader）
    FSBL是用於在嵌入式系統中啟動過程的第一階段的關鍵軟件組件，特別是在使用像 Zynq-7000 系列這樣的可編程系統晶片（SoC）時，它在系統上電或重置後首先被執行，負責執行一系列初始化操作，為系統的後續啟動階段做準備。

+ ### 操作流程
    + Step 1. Creating the FSBL (First Stage Boot Loader)
    
    + Step 2. 先找到之前練習所建立的 design_1_wrapper_bitstream，找到 platform.spr 並將它開啟，點擊ps7_cortexa9_0 > standalone_ps7_cortexa9_0 > Board Support Package(BSP)，再點擊 Modify BSP Settings，之後將 Supported Libraries 裡的 xilffs 打勾  
    + Step 3. File > New > Application Project > 點擊 Next > 選擇design_1_wrapper_bitstream 後點擊 Next > 將 Application Project Name 命名為 FSBL 後點擊 Next > 再點擊 Next > 之後再Templates的頁面選擇最底下的 Zynq FSBL > 點擊 Finish
    + Step 4. 桌面新增空白資料夾(最後會作為儲存本次練習的位置)
    + Step 5. Create Zynq Boot Image
        + 點擊 Vitis 上方的 Vitis > Create boot image > Zynq and Zynq Ultrascale
    + Step 6. 選擇 Create new BIF file，在 Output BIF file path 開啟剛剛新建資料夾的位置(flash_images)
    + Step 7. 接下來在下方的 Boot image partitions 會需要新增三個路徑
        + 第一個： Vitis > 2023.1 > practice > design_1_wrapper_bitstream > zynq_fsbl > fsbl.elf，並確認 partition type 是 "bootloader"
        + 第二個： Vitis > 2023.1 > practice > design_1_wrapper_bitstream > hw > design_1_wrapper_bitstream.bit，並確認 partition type 是 "datafile" 
        + 第三個： Vitis > 2023.1 > practice > Exercise_02 > Debug > Exercise_02.elf，並確認 partition type 是 "datafile" 
    + Step 8. 在 Output path 一樣開啟剛剛新建資料夾的位置(flash_images)，並使用檔名 "boot.bin"，之後點擊 Create Image
    + Step 9. 將 SD 卡插入電腦，並將桌面 flash_images 資料夾裡 boot.bin 複製一分到 SD 卡裡
    + Step 10. 將 SD 卡插入 zedboard 並觀察，會發現 application 已經在板子上運行了


    
---    
## 參考資料
+ Bitstream
  https://ithelp.ithome.com.tw/m/articles/10266953
+ zybo board 開發記錄
  https://coldnew.github.io/7004ff00/  
  https://coldnew.github.io/dec85bd3/
+ 從底層結構開始學習 FPGA - Block RAM(BRAM)
  https://xilinx.eetrend.com/blog/2022/100562010.html 
+ Xilinx-ZYNQ7000 系列 - 學習筆記（9）：定時器中斷實驗      
  https://blog.csdn.net/qq_42826337/article/details/93136546