11. 串列埠通訊

星光樱梦發表於2024-03-09

一、串列埠通訊簡介

  串列埠通訊是一種裝置間常用的序列通訊方式,串列埠按位(bit)傳送和接收位元組。串列埠通訊的資料包由 傳送裝置的 TXD 介面傳輸到接收裝置的 RXD 介面。在串列埠通訊的協議層中,規定了資料包的內容,它由 起始位主體資料校驗位 以及 停止位 組成,通訊雙方的資料包格式要約定一致才能正常收發資料。在串列埠通訊中,常用的協議包括 RS-232、RS-422 和 RS-485 等。

  隨著科技的發展,RS-232 在工業上還有廣泛的使用,但是在商業技術上,已經慢慢的使用USB 轉串列埠取代了 RS-232 串列埠。我們只需要在電路中新增一個 USB 轉串列埠晶片,就可以實現 USB 通訊協議和標準 UART 序列通訊協議的轉換,而開發板上的 USB 轉串列埠晶片是 CH340C 這個晶片。

二、串列埠通訊協議

  串列埠通訊協議資料包組成可以分為 波特率資料幀格式 兩部分。

串列埠通訊協議資料幀格式

【1】、波特率

  非同步通訊是不需要時鐘訊號的,但是這裡需要我們約定好兩個裝置的波特率。波特率表示每秒鐘傳送的碼元符號的個數,所以它決定了資料幀裡面每一個位的時間長度。兩個要通訊的裝置的波特率一定要設定相同,我們常見的波特率是 4800、9600、115200 等。

【2】、起始位和停止位

  串列埠通訊的一個資料幀是從起始位開始,直到停止位。資料幀中的起始位是由一個邏輯 0 的資料位表示,而資料幀的停止位可以是 0.5、1、1.5 或 2 個邏輯 1 的資料位表示,只要雙方約定一致即可。

【3】、有效資料位

  資料幀的起始位之後,就接著是資料位,也稱有效資料位,這就是我們真正需要的資料,有效資料位通常會被約定為 5、6、7 或者 8 個位長。有效資料位是低位(LSB)在前,高位(MSB)在後。

【4】、校驗位

  校驗位可以認為是一個特殊的資料位。校驗位一般用來判斷接收的資料位有無錯誤,檢驗方法有:奇檢驗偶檢驗0 檢驗1 檢驗 以及 無檢驗

  • 奇校驗 是指有效資料為和校驗位中 “1” 的個數為奇數
  • 偶校驗 與奇校驗要求剛好相反,要求幀資料和校驗位中 “1” 的個數為偶數
  • 0 校驗 是指不管有效資料中的內容是什麼,校驗位總為 “0”.
  • 1 校驗 是指不管有效資料中的內容是什麼,校驗位總為 “1”
  • 無校驗 是指資料幀中不包含校驗位

三、串列埠簡介

  STM32407ZGT6 最多可提供 6 路串列埠,有分數波特率發生器、支援同步單線通訊和半雙工單線通訊、支援 LIN、支援調變解調器操作、智慧卡協議和 IrDA SIR ENDEC 規範、具有 DMA 等。

  STM32F4 的串列埠分為兩種:USART(即通用同步非同步收發器)和 UART(即通用非同步收發器)。UART 是在 USART 基礎上裁剪掉了同步通訊功能,只剩下非同步通訊功能。簡單區分同步和非同步就是看通訊時需不需要對外提供時鐘輸出,我們平時用串列埠通訊基本都是非同步通訊。

  STM32F4 有 4 個 USART 和 2 個 UART,其中 USART1 和 USART6 的時鐘源來於 APB2 時鐘,其最大頻率為 84MHz,其他 4個串列埠的時鐘源可以來於 APB1時鐘,其最大頻率為 42MHz。

四、USART框圖

USART框圖

USART簡化版框圖

①、USART 訊號引腳

  • TX:傳送資料輸出引腳
  • RX:接收資料輸入引腳
  • SCLK:傳送器時鐘輸出,適用於同步傳輸
  • SW_RX:資料接收引腳,屬於內部引腳,用於智慧卡模式
  • IrDA_RDI:IrDA 模式下的資料輸入
  • IrDA_TDO:IrDA 模式下的資料輸出
  • nRTS:傳送請求,若是低電平,表示 USART 準備好接收資料
  • nCTS:清除傳送,若是高電平,在當前資料傳輸結束時阻斷下一次的資料傳送

②、資料暫存器

  USART_DR 包含了已傳送或接收到的資料。由於它本身就是兩個暫存器組成的,一個專門給傳送用的(TDR),一個專門給接收用的(RDR),該暫存器具備讀和寫的功能。TDR 暫存器提供了內部匯流排和輸出移位暫存器之間的並行介面。RDR 暫存器提供了輸入移位暫存器和內部匯流排之間的並行介面。當進行資料傳送操作時,往 USART_DR 中寫入資料會自動儲存在 TDR內;當進行讀取操作時,向 USART_DR 讀取資料會自動提去 RDR 資料。

  USART 資料暫存器(USART_DR)低 9 位資料有效,其他資料位保留。USART_DR 的第 9 位資料是否有效跟 USART_CR1 的 M 位設定有關,當 M 位為 0 表示 8 位資料字長;當 M 位為 1 時表示 9 位資料字長,一般使用 8 位資料字長。

  當使能校驗位(USART_CR1 中 PCE 位被置位)進行傳送時,寫到 MSB 的值(根據資料的長度不同,MSB 是第 7 位或者第 8 位)會被後來的校驗位取代。

③、控制器

  USART 有專門控制傳送的傳送器,控制接收的接收器,還有喚醒單元、中斷控制等等。

④、時鐘與波特率

  波特率,即每秒鐘傳輸的碼元個數,在二進位制系統中(串列埠的資料幀就是二進位制的形式),波特率與波特率的數值相等。波特率透過以下公式得出:

\[band = \frac{f_{ck}}{8 * (2 - OVER8) * USARTDIV} \]

  其中,fck 是給串列埠的時鐘(USART2\3 和 UART4\5 的時鐘源為 PCLK1,USART1\6 的時鐘源為PCLK2),過取樣設定為 16 倍過取樣,即 OVER8 = 0,USARTDIV 是一個無符號的定點數,存放在波特率暫存器(USART_BRR)的低 16位,DIV_Mantissa[11:0] 存放的是 USARTDIV 的整數部分,DIV_Fractionp[3:0] 存放的是 USARTDIV 的小數部分。

  當串列埠 1 設定需要得到 115200 的波特率,fck = 84MHz,那麼可得:

\[115200 = \frac{84000000}{16 ∗ USARTDIV} \]

  得到 USARTDIV ≈ 45.5729,分離 USARTDIV 的整數部分與小數部分,整數部分為 45,即 0x2D,那麼 DIV_Mantissa = 0x2D;小數部分為 0.5729,轉化為十六進位制即 0.5729 * 16 ≈ 9,所以 DIV_Fractionp = 0x9,USART_BRR 暫存器應該賦值為 0x2D9,成功設定波特率為 115200。值得注意 USARTDIV 是允許有餘數的,我們用四捨五入進行取整,這樣會導致波特率會有所偏差,而這樣的小誤差是可以被允許的。

五、UASRT常用暫存器

5.1、狀態暫存器(USART_SR)

狀態暫存器

  這裡我們關注一下兩個位,第 5、6 位 RXNE 和 TC。

  位 5 RXNE(讀資料暫存器非空),當該位被置 1 的時候,就是提示已經有資料被接收到了,並且可以讀出來了。這時候我們要做的就是儘快去讀取 USART_DR,透過讀 USART_DR 可以將該位清零,也可以向該位寫 0,直接清除。

  位 6 TC(傳送完成),當該位被置位的時候,表示 USART_DR 內的資料已經被髮送完成了。如果設定了這個位的中斷,則會產生中斷。該位也有兩種清零方式:①、讀 USART_SR,寫 USART_DR;②、直接向該位寫 0

5.2、資料暫存器(USART_DR)

  STM32 的傳送與接收是透過資料暫存器 USART_DR 來實現的,這是一個雙暫存器,包含了 TDR 和 RDR。當向該暫存器寫資料的時候,串列埠就會自動傳送,當收到資料的時候,也是存在該暫存器內。

資料暫存器

5.3、波特率暫存器(USART_BRR)

  每個串列埠都有一個自己獨立的波特率暫存器 USART_BRR,透過設定該暫存器就可以達到配置不同波特率的目的。

波特率暫存器

5.4、控制暫存器(USART_CRx)

  STM32F407 每個串列埠都有 3 個控制暫存器 USART_CR1~3,串列埠的很多配置都是透過這 3個暫存器來設定的。USART_CR1 暫存器的描述如下圖所示:

控制暫存器1

  UASRT_CR1 暫存器的高 16 位沒有用到,低 16 位用於串列埠的功能設定。

  位 13 UE 為串列埠使能位,透過該位置 1,使能串列埠。

  位 12 M 為字長,當該位為 0 的時候設定串列埠為 8 個字長外加 n 個停止位,停止位的個數(n)是根據 USART_CR2 的 [13:12] 位設定來決定的,預設為 0。

  位 10 PCE 為校驗使能位,設定為 0,即禁止校驗,否則使能校驗。位 9 PS 為校驗位選擇,設定為 0 為偶校驗,否則奇校驗。

  位 7 TXEIE 為傳送緩衝區空中斷使能位,設定該位為 1,當 USART_SR 中的 TXE 位為 1 時,將產生串列埠中斷。位 6 TCIE 為傳送完成中斷使能位,設定該位為 1,當 USART_SR 中的 TC 位為 1時,將產生串列埠中斷。位 5 RXNEIE 為接收緩衝區非空中斷使能,設定該位為 1,當 USART_SR 中的 ORE 或者 RXNE 位為 1 時,將產生串列埠中斷。位 3 TE 為傳送使能位,設定為 1,將開啟串列埠的傳送功能。位 2 RE 為接收使能位,用法同 TE。

控制暫存器2

  這裡,我們使用 USART_CR2 暫存器的位 [13:12] STOP 設定停止位個數。

控制暫存器3

  這裡,我們使用 USART_CR3 暫存器的位 3 HDSEL 設定工作模式。

六、IO引腳複用功能

【1】、USART1 IO 引腳複用及其重對映功能

功能引腳 複用引腳 重對映引腳
TXD PA9 PB6
RXD PA10 PB7

【2】、USART2 IO 引腳複用及其重對映功能

功能引腳 複用引腳 重對映引腳
TXD PA2 PD5
RXD PA3 PD6

【3】、USART3 IO 引腳複用及其重對映功能

功能引腳 複用引腳 重對映引腳
TXD PB10 PD8/PC10
RXD PB11 PD9/PC11

【4】、UART4 IO 引腳複用及其重對映功能

功能引腳 複用引腳 重對映引腳
TXD PA0 PC10
RXD PA1 PC11

【5】、UART5 IO 引腳複用及其重對映功能

功能引腳 複用引腳 重對映引腳
TXD PC12
RXD PD2

【6】、USART6 IO 引腳複用及其重對映功能

功能引腳 複用引腳 重對映引腳
TXD PC6 PG9
RXD PAC7 PG14

七、串列埠通訊配置步驟

7.1、使能對應的時鐘

  使能對應的串列埠時鐘。

#define __HAL_RCC_USART1_CLK_ENABLE()   do { \
                                        __IO uint32_t tmpreg = 0x00U; \
                                        SET_BIT(RCC->APB2ENR, RCC_APB2ENR_USART1EN);\
                                        /* Delay after an RCC peripheral clock enabling */ \
                                        tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_USART1EN);\
                                        UNUSED(tmpreg); \
                                          } while(0U)
#define __HAL_RCC_USART2_CLK_ENABLE()     do { \
                                        __IO uint32_t tmpreg = 0x00U; \
                                        SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USART2EN);\
                                        /* Delay after an RCC peripheral clock enabling */ \
                                        tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_USART2EN);\
                                        UNUSED(tmpreg); \
                                          } while(0U)
#define __HAL_RCC_USART6_CLK_ENABLE()   do { \
                                        __IO uint32_t tmpreg = 0x00U; \
                                        SET_BIT(RCC->APB2ENR, RCC_APB2ENR_USART6EN);\
                                        /* Delay after an RCC peripheral clock enabling */ \
                                        tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_USART6EN);\
                                        UNUSED(tmpreg); \
                                          } while(0U)
#define __HAL_RCC_USART3_CLK_ENABLE() do { \
                                      __IO uint32_t tmpreg = 0x00U; \
                                      SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USART3EN);\
                                      /* Delay after an RCC peripheral clock enabling */ \
                                      tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_USART3EN);\
                                      UNUSED(tmpreg); \
                                      } while(0U)
#define __HAL_RCC_UART4_CLK_ENABLE()  do { \
                                      __IO uint32_t tmpreg = 0x00U; \
                                      SET_BIT(RCC->APB1ENR, RCC_APB1ENR_UART4EN);\
                                      /* Delay after an RCC peripheral clock enabling */ \
                                      tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_UART4EN);\
                                      UNUSED(tmpreg); \
                                      } while(0U)
#define __HAL_RCC_UART5_CLK_ENABLE()  do { \
                                      __IO uint32_t tmpreg = 0x00U; \
                                      SET_BIT(RCC->APB1ENR, RCC_APB1ENR_UART5EN);\
                                      /* Delay after an RCC peripheral clock enabling */ \
                                      tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_UART5EN);\
                                      UNUSED(tmpreg); \
                                      } while(0U)

  使能對應的 GPIO 的時鐘。

#define __HAL_RCC_GPIOA_CLK_ENABLE()   do { \
                                        __IO uint32_t tmpreg = 0x00U; \
                                        SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
                                        /* Delay after an RCC peripheral clock enabling */ \
                                        tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
                                        UNUSED(tmpreg); \
                                          } while(0U)
#define __HAL_RCC_GPIOB_CLK_ENABLE()   do { \
                                        __IO uint32_t tmpreg = 0x00U; \
                                        SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
                                        /* Delay after an RCC peripheral clock enabling */ \
                                        tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
                                        UNUSED(tmpreg); \
                                          } while(0U)
#define __HAL_RCC_GPIOC_CLK_ENABLE()  do { \
                                        __IO uint32_t tmpreg = 0x00U; \
                                        SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
                                        /* Delay after an RCC peripheral clock enabling */ \
                                        tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
                                        UNUSED(tmpreg); \
                                          } while(0U)
#define __HAL_RCC_GPIOD_CLK_ENABLE()   do { \
                                        __IO uint32_t tmpreg = 0x00U; \
                                        SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIODEN);\
                                        /* Delay after an RCC peripheral clock enabling */ \
                                        tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIODEN);\
                                        UNUSED(tmpreg); \
                                      } while(0U)

#define __HAL_RCC_GPIOG_CLK_ENABLE()   do { \
                                       __IO uint32_t tmpreg = 0x00U; \
                                       SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOGEN);\
                                       /* Delay after an RCC peripheral clock enabling */ \
                                       tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOGEN);\
                                       UNUSED(tmpreg); \
                                       } while(0U)

7.2、配置串列埠工作引數

  要使用一個外設首先要對它進行初始化,串列埠的初始化函式,其宣告如下:

HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart);

  形參 huart 是串列埠的控制代碼,UART_HandleTypeDef 結構體型別,其定義如下:

typedef struct __UART_HandleTypeDef
{
    USART_TypeDef *Instance;                    // UART暫存器基地址
    UART_InitTypeDe Init;                       // UART初始化結構體
    const uint8_t *pTxBuffPtr;                  // UART的傳送資料緩衝區
    uint16_t TxXferSize;                        // UART傳送資料大小
    __IO uint16_t TxXferCount;                  // UART傳送端計數器
    uint8_t *pRxBuffPtr;                        // UART的接收資料緩衝區
    uint16_t RxXferSize;                        // UART接收資料大小
    __IO uint16_t RxXferCount;                  // UART接收端計數器
    __IO HAL_UART_RxTypeTypeDef ReceptionType;  
    DMA_HandleTypeDef *hdmatx;                  // UART傳送引數設定(DMA)
    DMA_HandleTypeDef *hdmarx;                  // UART接收引數設定(DMA)
    HAL_LockTypeDef Lock;                       // 鎖物件
    __IO HAL_UART_StateTypeDef gState;          // UART傳送狀態結構體
    __IO HAL_UART_StateTypeDef xState;          // UART接收狀態結構體
    __IO uint32_t  ErrorCode;                   // UART操作錯誤資訊
} UART_HandleTypeDef;

  Instance指向 UART 暫存器基地址。實際上這個基地址 HAL 庫已經定義好了,可以選擇範圍:USART1~ USART3、USART6、UART4、UART5。

#define USART1              ((USART_TypeDef *) USART1_BASE)
#define USART2              ((USART_TypeDef *) USART2_BASE)
#define USART3              ((USART_TypeDef *) USART3_BASE)
#define UART4               ((USART_TypeDef *) UART4_BASE)
#define UART5               ((USART_TypeDef *) UART5_BASE)
#define USART6              ((USART_TypeDef *) USART6_BASE)

  InitUART 初始化結構體,用於配置通訊引數,如波特率、資料位數、停止位等等。

  Lock:對資源操作增加操作 鎖保護,可選 HAL_UNLOCKED 或者 HAL_LOCKED 兩個引數。如果 gState 的值等於 HAL_UART_STATE_RESET,則認為串列埠未被初始化,此時,分配鎖資源,並且呼叫 HAL_UART_MspInit() 函式來對串列埠的 GPIO 和時鐘進行初始化。

  gStateRxState:分別是 UART 的傳送狀態、工作狀態的結構體和 UART 接受狀態的結構體。HAL_UART_StateTypeDef 是一個列舉型別,列出串列埠在工作過程中的狀態值,有些值只適用於 gState,如 HAL_UART_STATE_BUSY。

  ErrorCode串列埠錯誤操作資訊。主要用於存放串列埠操作的錯誤資訊。

  UART_InitTypeDef 這個結構體型別,該結構體用於配置 UART 的各個通訊引數,包括波特率,停止位等,具體說明如下:

typedef struct
{
    uint32_t BaudRate;              // 位元率
    uint32_t WordLength;            // 字長
    uint32_t StopBits;              // 停止位
    uint32_t Parity;                // 奇偶校驗
    uint32_t Mode;                  // 模式
    uint32_t HwFlowCtl;             // 硬體流設定
    uint32_t OverSampling;          // 過取樣設定
} UART_InitTypeDef;

  BaudRate波特率設定。一般設定為 2400、9600、19200、115200。

  WordLength資料幀字長,可選 8 位或 9 位。這裡我們設定為 8 位字長資料格式。

#define UART_WORDLENGTH_8B                  0x00000000U
#define UART_WORDLENGTH_9B                  ((uint32_t)USART_CR1_M)

  StopBits停止位設定,可選 1 個或 2 個停止位,一般我們選擇 1 個停止位。

#define UART_STOPBITS_1                     0x00000000U
#define UART_STOPBITS_2                     ((uint32_t)USART_CR2_STOP_1)

  Parity奇偶校驗控制選擇,我們可以設定為 無奇偶校驗位偶校驗奇校驗

#define UART_PARITY_NONE                    0x00000000U
#define UART_PARITY_EVEN                    ((uint32_t)USART_CR1_PCE)
#define UART_PARITY_ODD                     ((uint32_t)(USART_CR1_PCE | USART_CR1_PS))

  ModeUART 模式選擇,可以設定為 只收模式只發模式,或者 收發模式。這裡我們設定為全雙工收發模式。

#define UART_MODE_RX                        ((uint32_t)USART_CR1_RE)
#define UART_MODE_TX                        ((uint32_t)USART_CR1_TE)
#define UART_MODE_TX_RX                     ((uint32_t)(USART_CR1_TE | USART_CR1_RE))

  HwFlowCtl硬體流控制選擇,一般我們設定為無硬體流控制。

#define UART_HWCONTROL_NONE                  0x00000000U
#define UART_HWCONTROL_RTS                   ((uint32_t)USART_CR3_RTSE)
#define UART_HWCONTROL_CTS                   ((uint32_t)USART_CR3_CTSE)
#define UART_HWCONTROL_RTS_CTS               ((uint32_t)(USART_CR3_RTSE | USART_CR3_CTSE))

  OverSampling過取樣選擇,選擇 8 倍過取樣或者 16 過取樣,一般選擇 16 過取樣。

#define UART_OVERSAMPLING_16                    0x00000000U
#define UART_OVERSAMPLING_8                     ((uint32_t)USART_CR1_OVER8)

  該函式的返回值是 HAL_StatusTypeDef 列舉型別的值,有 4 個,分別是 HAL_OK 表示 成功HAL_ERROR** 表示 錯誤HAL_BUSY 表示 忙碌HAL_TIMEOUT 表示 超時

typedef enum 
{
    HAL_OK = 0x00U,             // 成功
    HAL_ERROR = 0x01U,          // 錯誤
    HAL_BUSY = 0x02U,           // 忙碌
    HAL_TIMEOUT = 0x03U         // 超時
} HAL_StatusTypeDef;

7.3、串列埠底層初始化

  HAL 庫中,提供 HAL_GPIO_Init() 函式用於配置 GPIO 功能模式,初始化 GPIO。該函式的宣告如下:

void HAL_GPIO_Init(GPIO_TypeDef  *GPIOx, GPIO_InitTypeDef *GPIO_Init);

  該函式的第一個形參 GPIOx 用來指定埠號,可選值如下:

#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD               ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)

  第二個引數是 GPIO_InitTypeDef 型別的結構體變數,用來設定 GPIO 的工作模式,其定義如下:

typedef struct
{
  uint32_t Pin;         // 引腳號
  uint32_t Mode;        // 模式設定
  uint32_t Pull;        // 上下拉設定
  uint32_t Speed;       // 速度設定
  uint32_t Alternate;   // 複用功能設定
}GPIO_InitTypeDef;

  成員 Pin 表示 引腳號,範圍:GPIO_PIN_0 到 GPIO_PIN_15。

#define GPIO_PIN_0                 ((uint16_t)0x0001)  /* Pin 0 selected    */
#define GPIO_PIN_1                 ((uint16_t)0x0002)  /* Pin 1 selected    */
#define GPIO_PIN_2                 ((uint16_t)0x0004)  /* Pin 2 selected    */
#define GPIO_PIN_3                 ((uint16_t)0x0008)  /* Pin 3 selected    */
#define GPIO_PIN_4                 ((uint16_t)0x0010)  /* Pin 4 selected    */
#define GPIO_PIN_5                 ((uint16_t)0x0020)  /* Pin 5 selected    */
#define GPIO_PIN_6                 ((uint16_t)0x0040)  /* Pin 6 selected    */
#define GPIO_PIN_7                 ((uint16_t)0x0080)  /* Pin 7 selected    */
#define GPIO_PIN_8                 ((uint16_t)0x0100)  /* Pin 8 selected    */
#define GPIO_PIN_9                 ((uint16_t)0x0200)  /* Pin 9 selected    */
#define GPIO_PIN_10                ((uint16_t)0x0400)  /* Pin 10 selected   */
#define GPIO_PIN_11                ((uint16_t)0x0800)  /* Pin 11 selected   */
#define GPIO_PIN_12                ((uint16_t)0x1000)  /* Pin 12 selected   */
#define GPIO_PIN_13                ((uint16_t)0x2000)  /* Pin 13 selected   */
#define GPIO_PIN_14                ((uint16_t)0x4000)  /* Pin 14 selected   */
#define GPIO_PIN_15                ((uint16_t)0x8000)  /* Pin 15 selected   */

  成員 Mode 是 GPIO 的 模式選擇,有以下選擇項:

#define  GPIO_MODE_AF_PP                        0x00000002U     // 推輓式複用

  成員 Pull 用於 配置上下拉電阻,有以下選擇項:

#define  GPIO_NOPULL        0x00000000U     // 無上下拉
#define  GPIO_PULLUP        0x00000001U     // 上拉
#define  GPIO_PULLDOWN      0x00000002U     // 下拉

  成員 Speed 用於 配置 GPIO 的速度,有以下選擇項:

#define  GPIO_SPEED_FREQ_LOW         0x00000000U    // 低速
#define  GPIO_SPEED_FREQ_MEDIUM      0x00000001U    // 中速
#define  GPIO_SPEED_FREQ_HIGH        0x00000002U    // 高速
#define  GPIO_SPEED_FREQ_VERY_HIGH   0x00000003U    // 極速

  成員 Alternate 用於 配置具體的複用功能,不同的 GPIO 口可以複用的功能不同,具體可參考資料手冊。

#define GPIO_AF7_USART1        ((uint8_t)0x07)  /* USART1 Alternate Function mapping     */
#define GPIO_AF7_USART2        ((uint8_t)0x07)  /* USART2 Alternate Function mapping     */
#define GPIO_AF7_USART3        ((uint8_t)0x07)  /* USART3 Alternate Function mapping     */

#define GPIO_AF8_UART4         ((uint8_t)0x08)  /* UART4 Alternate Function mapping  */
#define GPIO_AF8_UART5         ((uint8_t)0x08)  /* UART5 Alternate Function mapping  */

#define GPIO_AF8_USART6        ((uint8_t)0x08)  /* USART6 Alternate Function mapping */

7.4、設定中斷

7.4.1、設定中斷優先順序分組

  HAL_NVIC_SetPriorityGrouping() 函式是設定中斷優先順序分組函式。其宣告如下:

void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);

  其中,引數 PriorityGroup中斷優先順序分組號,可以選擇範圍如下:

#define NVIC_PRIORITYGROUP_0         0x00000007U /*!< 0 bits for pre-emption priority
                                                      4 bits for subpriority */
#define NVIC_PRIORITYGROUP_1         0x00000006U /*!< 1 bits for pre-emption priority
                                                      3 bits for subpriority */
#define NVIC_PRIORITYGROUP_2         0x00000005U /*!< 2 bits for pre-emption priority
                                                      2 bits for subpriority */
#define NVIC_PRIORITYGROUP_3         0x00000004U /*!< 3 bits for pre-emption priority
                                                      1 bits for subpriority */
#define NVIC_PRIORITYGROUP_4         0x00000003U /*!< 4 bits for pre-emption priority
                                                      0 bits for subpriority */

  這個函式在一個工程裡基本只呼叫一次,而且是在程式 HAL 庫初始化函式里面已經被呼叫,後續就不會再呼叫了。因為當後續呼叫設定成不同的中斷優先順序分組時,有可能造成前面設定好的搶佔優先順序和響應優先順序不匹配。如果呼叫了多次,則以最後一次為準。

7.4.2、設定中斷優先順序

  HAL_NVIC_SetPriority() 函式是設定中斷優先順序函式。其宣告如下:

void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);

  其中,引數 IRQn中斷號,可以選擇範圍:IRQn_Type 定義的列舉型別,定義在 stm32f407xx.h。

typedef enum
{
  USART1_IRQn                 = 37,     /*!< USART1 global Interrupt                                           */
  USART2_IRQn                 = 38,     /*!< USART2 global Interrupt                                           */
  USART3_IRQn                 = 39,     /*!< USART3 global Interrupt                                           */
  UART4_IRQn                  = 52,     /*!< UART4 global Interrupt                                            */
  UART5_IRQn                  = 53,     /*!< UART5 global Interrupt                                            */
  USART6_IRQn                 = 71,     /*!< USART6 global interrupt                                           */
} IRQn_Type;

  引數 PreemptPriority搶佔優先順序,可以選擇範圍:0 到 15,具體根據中斷優先順序分組決定。

  引數 SubPriority響應優先順序,可以選擇範圍:0 到 15,具體根據中斷優先順序分組決定。

7.4.3、使能中斷

  HAL_NVIC_EnableIRQ() 函式是中斷使能函式。其宣告如下:

void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);

  其中,引數 IRQn中斷號,可以選擇範圍:IRQn_Type 定義的列舉型別,定義在 stm32f407xx.h。

7.5、設計中斷服務函式

  每開啟一箇中斷,就必須編寫其對應的中斷服務函式,否則將導致當機(CPU 將找不到中斷服務函式)。中斷服務函式介面廠家已經在 startup_stm32f407xx.s 中寫好了。

void USART1_IRQHandler();
void USART2_IRQHandler();  
void USART3_IRQHandler();
void UART4_IRQHandler();   
void UART5_IRQHandler();
void USART6_IRQHandler();

  HAL 庫為了使用者使用方便,提供了一箇中斷通用入口函式 HAL_UART_IRQHandler()。

void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
{
    // 如果沒有錯誤發生
    errorflags = (isrflags & (uint32_t)(USART_SR_PE | USART_SR_FE | USART_SR_ORE | USART_SR_NE));
    if (errorflags == RESET)
    {
        // UART接收模式。RXNE:讀資料暫存器非空 RXNEIE:接收緩衝區非空中斷使能
        if (((isrflags & USART_SR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET))
        {
            // 在該函式里清除相關中斷標誌位並呼叫HAL_UART_RxCpltCallback()
            UART_Receive_IT(huart);
            return;
        }
    }

    // 如果發生了錯誤
    if ((errorflags != RESET) && (((cr3its & USART_CR3_EIE) != RESET) || ((cr1its & (USART_CR1_RXNEIE | USART_CR1_PEIE)) != RESET)))
    {
        // pass
    }


    // UART傳送模式。TXE:傳送資料暫存器空 TXEIE:傳送緩衝區空中斷使能
    if (((isrflags & USART_SR_TXE) != RESET) && ((cr1its & USART_CR1_TXEIE) != RESET))
    {
        UART_Transmit_IT(huart);
        return;
    }

    // UART傳送模式結束。TC:傳送完成 TCIE:傳送完成中斷使能
    if (((isrflags & USART_SR_TC) != RESET) && ((cr1its & USART_CR1_TCIE) != RESET))
    {
        // 在該函式里清除相關中斷標誌位並呼叫HAL_UART_TxCpltCallback()
        UART_EndTransmit_IT(huart);
        return;
    }
}

  HAL 庫常用的回撥函式如下:

__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);                // 傳送完成回撥函式
__weak void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);            // 半傳送完成回撥函式
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);                // 接收完成回撥函式
__weak void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);            // 半接收完成回撥函式
__weak void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);                 // 錯誤回撥函式
__weak void HAL_UART_AbortCpltCallback(UART_HandleTypeDef *huart);             // 中止回撥函式
__weak void HAL_UART_AbortTransmitCpltCallback(UART_HandleTypeDef *huart);     // 傳送中止回撥函式
__weak void HAL_UART_AbortReceiveCpltCallback(UART_HandleTypeDef *huart);      // 接收中止回撥函式

7.6、串列埠接收資料

  HAL 庫提供 HAL_UART_Receive_IT() 用於開啟以中斷的方式接收指定位元組。資料接收在中斷處理函式里面實現。其宣告如下:

HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

  形參 huartUART_HandleTypeDef 結構體指標型別的串列埠控制代碼。形參 pData 是要接收的資料地址。形參 Size 是要接收的資料大小,以位元組為單位。當接收到 Size 個位元組之後,會執行對應的中斷接收完成回撥函式。

  該函式的返回值是 HAL_StatusTypeDef 列舉型別的值,有 4 個,分別是 HAL_OK 表示 成功HAL_ERROR 表示 錯誤HAL_BUSY 表示 忙碌HAL_TIMEOUT 表示 超時

7.7、串列埠傳送資料

  HAL 庫提供了以阻塞方式傳送指定位元組資料的函式。該函式的宣告如下:

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout);

  形參 huartUART_HandleTypeDef 結構體指標型別的串列埠控制代碼。形參 pData 是要傳送的資料地址。形參 Size 是要傳送的資料大小,以位元組為單位。形參 Timeout 設定超時時間,以毫秒為單位。

  該函式的返回值是 HAL_StatusTypeDef 列舉型別的值,有 4 個,分別是 HAL_OK 表示 成功HAL_ERROR 表示 錯誤HAL_BUSY 表示 忙碌HAL_TIMEOUT 表示 超時

八、原始碼實現

8.1、原理圖

USART1原理圖

  透過原理圖,我們看出 USART1 的 TXD 連線到 PA9 引腳,RXD 連線到 PA10 引腳,工作模式為複用推輓輸出。

8.2、程式原始碼

  USART1 初始化函式內容如下:

UART_HandleTypeDef g_usart1_handle;                                             // USART1控制代碼

uint8_t g_usart_rx_buffer[1];                                                   // HAL庫使用的串列埠接收資料緩衝區
/**
 * @brief 串列埠1初始化函式
 * 
 * @param band 波特率
 */
void USART1_Init(uint32_t band)
{
    g_usart1_handle.Instance = USART1;                                          // 暫存器基地址
    g_usart1_handle.Init.BaudRate = band;                                       // 波特率
    g_usart1_handle.Init.WordLength = UART_WORDLENGTH_8B;                       // 資料位
    g_usart1_handle.Init.StopBits = UART_STOPBITS_1;                            // 停止位
    g_usart1_handle.Init.Parity = UART_PARITY_NONE;                             // 奇偶校驗位
    g_usart1_handle.Init.Mode = UART_MODE_TX_RX;                                // 收發模式
    g_usart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;                       // 硬體流控制
    g_usart1_handle.Init.OverSampling = UART_OVERSAMPLING_16;                   // 過取樣
    HAL_UART_Init(&g_usart1_handle);

    HAL_UART_Receive_IT(&g_usart1_handle, (uint8_t *)g_usart_rx_buffer, 1);     // 開啟接收中斷
}

  USART1 底層初始化函式如下:

/**
 * @brief 串列埠底層初始化函式
 * 
 * @param huart 串列埠控制代碼
 */
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    if (huart->Instance == USART1)                                              // 初始化的串列埠是否是USART1
    {
        __HAL_RCC_USART1_CLK_ENABLE();                                          // 使能USART1時鐘
        __HAL_RCC_GPIOA_CLK_ENABLE();                                           // 使能對應GPIO的時鐘

        // PA9 -> USART TXD
        GPIO_InitStruct.Pin = GPIO_PIN_9;                                       // USART1 TXD的引腳
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;                                 // 推輓式複用
        GPIO_InitStruct.Pull = GPIO_NOPULL;                                     // 不使用上下拉
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;                           // 輸出速度
        GPIO_InitStruct.Alternate = GPIO_AF7_USART1;                            // 複用功能
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

        // PA10 -> USART RXD
        GPIO_InitStruct.Pin = GPIO_PIN_10;                                      // USART1 RXD的引腳
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

        HAL_NVIC_EnableIRQ(USART1_IRQn);                                        // 使能USART1中斷
        HAL_NVIC_SetPriority(USART1_IRQn, 2, 0);                                // 設定中斷優先順序
    }
}

  該函式主要實現底層的初始化,事實上這個函式的程式碼還可以直接放到 USART1_Init() 函式里面,但是 HAL 庫為了程式碼的功能分層初始化,定義這個函式方便使用者使用。所以我們也按照 HAL 庫的這個結構來初始化外設。這個函式首先是呼叫 if(huart->Instance == USART1) 判斷是要初始化那個串列埠是否是 USART1,因為每個串列埠初始化都會呼叫 HAL_UART_MspInit() 這個函式,所以需要判斷是哪個串列埠要初始化才做相應的處理。

  首先就是使能串列埠以及 PA9 和 PA10 的時鐘,PA9 和 PA10 需要用做複用功能,複用功能模式有兩個選擇:GPIO_MODE_AF_PP 推輓式複用和 GPIO_MODE_AF_OD 開漏式複用,我們選擇的是推輓式複用。然後,我們需要將 PA9 和 PA10 的複用功能配置為 ,GPIO_AF7_USART1 。然後就是呼叫 HAL_GPIO_Init() 函式進行 IO 口的初始化。

  串列埠中斷服務函式內容如下:

/**
 * @brief USART1中斷服務函式
 * 
 */
void USART1_IRQHandler(void)
{
    HAL_UART_IRQHandler(&g_usart1_handle);                                      // 呼叫HAL庫公共處理函式
    HAL_UART_Receive_IT(&g_usart1_handle, (uint8_t *)g_usart_rx_buffer, 1);     // 再次開啟接收中斷
}

  從程式碼邏輯可以看出,在中斷服務函式內部透過呼叫接收回撥函式 HAL_UART_RxCpltCallback() 進行處理。然後,再呼叫 UART_Receive_IT() 函式重新開啟中斷。UART_Receive_IT() 函式的作用就是把每次中斷接收到的字元儲存在串列埠控制代碼的快取指標 g_usart_rx_buffer 中,同時每次接收一個字元,其計數器 RxXferCount 減 1,直到接收完成 RxXferSize 個字元之後 RxXferCount 設定為 0。這裡,我們直接設定了串列埠控制代碼成員變數 RxXferSize 為 1。

  重寫串列埠接收回撥函式:

#define USART_RECEIVE_LENGTH 200

uint8_t g_usart1_rx_buffer[USART_RECEIVE_LENGTH];                               // 接收資料緩衝區
uint16_t g_usart1_rx_status = 0;                                                // 接收狀態標記

/**
 * @brief USART接收中斷回撥函式
 * 
 * @param huart 串列埠控制代碼
 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1)
    {
        printf("%c", g_usart_rx_buffer[0]);
        if ((g_usart1_rx_status & 0x8000) ==  0)                                // 接收未完成
        {
            if (g_usart1_rx_status & 0x4000)                                    // 接收到了0x0D,即Enter鍵
            {
                if (g_usart_rx_buffer[0] != 0x0A)                               // 接收到的不是0x0A,即不是換行符
                {
                    g_usart1_rx_status = 0;                                     // 接收錯誤,重新開始
                }
                else                                                            // 接收到的是0x0A,即換行符
                {
                    g_usart1_rx_status |= 0x8000;                               // 接收完成  
                }
            }
            else                                                                // 還沒接收到0x0D,即還沒接收到Enter鍵
            {
                if (g_usart_rx_buffer[0] == 0x0D)                               // 接收到的是0x0D,即Enter鍵
                {
                    g_usart1_rx_status |= 0x4000;
                }
                else                                                            // 如果沒有接收到回車
                {
                    // 接收到的資料存入接收緩衝區
                    g_usart1_rx_buffer[g_usart1_rx_status & 0x3FFF] = g_usart_rx_buffer[0];
                    g_usart1_rx_status++;
                    if (g_usart1_rx_status > (USART_RECEIVE_LENGTH - 1))        // 接收到資料大於接收緩衝區大小
                    {
                        g_usart1_rx_status = 0;                                 // 接收資料錯誤,重新開始接收
                    }
                }
            }
        }
    }
}

  因為我們設定了串列埠控制代碼成員變數 RxXferSize 為 1,那麼每當 USART1 接收到一個字元後觸發接收完成中斷,便會在中斷服務函式中引導執行該回撥函式。當串列埠接受到一個字元後,它會儲存在快取 g_usart_rx_buffer中,由於我們設定了快取大小為 1,而且 RxXferSize=1,所以每次接受一個字元,會直接儲存到 g_usart_rx_buffer[0]中,我們直接透過讀取 g_usart_rx_buffer[0] 的值就是本次接收到的字元。

  這裡我們設計了一個簡單的接收協議:透過這個函式,配合一個陣列 g_usart1_rx_buffer,一個接收狀態標誌位 g_usart1_rx_status 實現對串列埠資料的接收管理。陣列 g_usart1_rx_buffer 的大小由 USART_RECEIVE_LENGTH 定義,也就是一次接收的資料最大不能超過 USART_RECEIVE_LENGTH 個位元組。其中,g_usart1_rx_status 的位 15 用來表示接收完成標誌,位 14 用來表示接收到 0x0D 標誌,位 0 ~ 13 用來表示接收到的有效位元組個數。

  當接收到從電腦發過來的資料,把接收到的資料儲存在陣列 g_usart_rx_buffer 中,同時在接收狀態標誌位(g_usart1_rx_status)中計數接收到的有效資料個數,當收到回車(回車的表示由 2 個位元組組成:0x0D 和 0x0A)的第一個位元組 0x0D 時,計數器將不再增加,等待 0x0A 的到來,而如果 0x0A 沒有來到,則認為這次接收失敗,重新開始下一次接收。如果順利接收到 0x0A,則標記 g_usart1_rx_status 的第 15 位,這樣完成一次接收,並等待該位被其他程式清除,從而開始下一次的接收,而如果遲遲沒有收到 0x0D,那麼在接收資料超過 USART_RECEIVE_LENGTH 的時候,則會丟棄前面的資料,重新接收。

  如果想要使用 printf() 函式,我們需要重寫 _write() 函式實現 printf() 函式。

int _write(int fd, char *ptr, int length)
{
    HAL_UART_Transmit(&g_usart1_handle, (uint8_t *)ptr, length, 0xFFFF);        // g_usart1_handle是對應串列埠
    return length;
}

注意:不同的編譯器實現 printf() 函式要重寫的函式不同;

  預設情況下,只有一個串列埠可以使用 priintf() 函式。如果,我們想要多個串列埠使用 printf() 函式,可以透過如下方法:

/**
 * @brief 多串列埠使用printf()函式
 * 
 * @param huart 串列埠控制代碼
 * @param fmt 格式化字串
 * @param ... 格式化引數
 */
void UART_Printf(UART_HandleTypeDef *huart, char *fmt, ...)
{
    char buff[USART_RECEIVE_LENGTH+1];                                          // 用來存放轉換後的資料
    uint16_t i = 0;

    va_list args;
    va_start(args, fmt);
    vsnprintf(buff, USART_RECEIVE_LENGTH+1, fmt, args);                         // 將格式化字串轉換為字元陣列

    i = (strlen(buff) > USART_RECEIVE_LENGTH) ? USART_RECEIVE_LENGTH : strlen(buff);

    HAL_UART_Transmit(huart, (uint8_t *)buff, i, 1000);                         // 串列埠傳送資料

    va_end(args);
}

  有關時鐘配置函式請在 STM32 的時鐘系統 篇章檢視。

  main() 函式內容如下:

int main(void)
{
    uint32_t length = 0;

    HAL_Init();

    System_Clock_Init(8, 336, 2, 7);
    HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);                         // 設定中斷優先順序分組

    USART1_Init(115200);

    printf("這是一個串列埠測試程式!\r\n");

    while (1)
    {
        if (g_usart1_rx_status & 0x8000)                                        // 接收到了資料
        {
            printf("接收到資料:\r\n");
            length = g_usart1_rx_status & 0x3FFF;
            HAL_UART_Transmit(&g_usart1_handle, (uint8_t *)g_usart1_rx_buffer, length, 1000);     // 傳送資料
            while (__HAL_USART_GET_FLAG(&g_usart1_handle, USART_FLAG_TC) != SET);                 // 等待傳送完成
            printf("\r\n");
            g_usart1_rx_status = 0;
        }
    }
  
    return 0;
}

相關文章