【STM32H7教程】第31章 STM32H7的USART應用之RS485


完整教程下載地址:http://www.armbbs.cn/forum.php?mod=viewthread&tid=86980

第31章       STM32H7的USART應用之RS485

本章教程為大家講解USART應用之485總線。雖然這幾年無線網絡的使用率有所上升,有線的串行網絡仍然提供最有力、最可靠的通信,特別是在惡劣的環境中。在需要抗噪、抗靜電、抗電壓故障的工業,建築自動化領域仍然是有線通信的天下。

31.1 初學者重要提示

31.2 RS485基礎知識

31.3 RS485硬件設計

31.4 RS485驅動設計

31.5 RS485板級支持包(bsp_uart_fifo.c)

31.6 RS485驅動移植和使用

31.7 使用例程設計框架

31.8 實驗例程說明(MDK)

31.9 實驗例程說明(IAR)

31.10 總結

 

 

31.1 初學者重要提示

  1.   學習本章節前,務必優先學習第30章,RS485用到的串口FIFO也是建立在30章的基礎上。
  2.   了解了本章31.2和31.3小節的基礎知識后,強烈推薦看此貼的兩個文檔,對RS485講解的比較透徹,中文版:http://www.armbbs.cn/forum.php?mod=viewthread&tid=90753
  3.   STM32H7支持RS485的硬件流控制,即有一個專門的引腳來控制485 PHY的收發狀態切換。V7開發板用的USART3,需要用PD12來控制,而這個引腳要用於FMC,所以用的是一個通用IO。
  4.   經常會有網友咨詢為什么程序里面收發切換沒有做延遲處理,這里就涉及到一個關鍵的知識點TXE發送空中斷和TC發送完成中斷的區別,詳細看教程中說明即可。

31.2 RS485的基礎知識

  •   背景知識(了解即可)

智能儀表是隨着80年代初單片機技術的成熟而發展起來的,現在世界儀表市場基本被智能儀表所壟斷。究其原因就是企業信息化的需要,企業在儀表選型時其中的一個必要條件就是要具有聯網通訊接口。最初是數據模擬信號輸出簡單過程量,后來儀表接口是RS232接口,這種接口可以實現點對點的通信方式,但這種方式不能實現聯網功能。隨后出現的RS485解決了這個問題。

EIA-485(過去叫做RS-485或者RS485)是隸屬於OSI模型物理層的電氣特性規定為2線、半雙工、平衡傳輸線多點通信的標准,是由電信行業協會(TIA)及電子工業聯盟(EIA)聯合發布的標准。實現此標准的數字通信網可以在有電子噪聲的環境下進行長距離有效率的通信。在線性多點總線的配置下,可以在一個網絡上有多個接收器。因此適用在工業環境中。

EIA一開始將RS(Recommended Standard)做為標准的前綴,不過后來為了便於識別標准的來源,已將RS改為EIA/TIA。電子工業聯盟(EIA)已結束運作,此標准目前是電信行業協會(TIA)維護,名稱為TIA-485,但工程師仍繼續用RS-485來稱呼此協議。

  •   RS485電氣特性

RS-485的數據最高傳輸速率為10Mbsp。

RS-485接口是采用平衡驅動器和差分接收器的組合,抗共模干擾能力增強,即抗干擾噪聲性好。

RS-485最大的通信距離約為1219m,最高傳輸速率為10Mbsp,傳輸速率與傳輸距離成反比,在100Kb/S的傳輸速率下,才可以達到最大的通信距離,如果需傳輸更長的距離,需要加485中繼器。RS-485總線一般最大支持32個節點,如果使用特制的485芯片,可以達到128個或者256個節點,最大的可以支持到400個節點。

關於RS485的邏輯狀態,不同廠家的芯片的定義可能不同,但不影響正常的數據收發,這里以TI的為例做個說明,TI的定義方式如下:

A表示非反向輸出non-inverting output,B表示反向輸出inverting output。

當VA > VB 的時候表示邏輯狀態0,被稱為ON。

當VA < VB 的時候表示邏輯狀態1,被稱為OFF。

 

對應到實際芯片框圖上就是下面這樣(DE發送使能,D是發送數據端,RE是接收使能,R是接收數據端):

 

當用戶在D(Driver)引腳輸入邏輯高電平時,將在485總線上實現邏輯狀態0,即ON狀態。接收端R(Receiver)將收到邏輯高電平。

當用戶在D(Driver)引腳輸入邏輯低電平時,將在485總線上實現邏輯狀態1,即OFF狀態。接收端R(Receiver)將收到邏輯低電平。

 

發送狀態下,大於|±1.5V |可以有效表示邏輯狀態1和邏輯狀態0:

 

接收狀態下,大於|±200mv|可以有效表示邏輯狀態1和邏輯狀態0:

 

31.3 RS485硬件設計

STM32H743XIH6最多可以支持8個獨立的串口。其中串口4和串口5和SDIO的GPIO是共用的,也就是說,如果要用到SD卡,那么串口4和串口5將不能使用。串口7和SPI3共用,串口8和RGB硬件接口共用。串口功能可以分配到不同的GPIO。我們常用的管腳分配如下:

串口USART1  TX = PA9,   RX = PA10

串口USART2  TX = PA2,   RX = PA3

串口USART3  TX = PB10,  RX = PB11

串口UART4   TX = PC10,  RX = PC11 (和SDIO共用)

串口UART5   TX = PC12,  RX = PD2  (和SDIO共用)

串口USART6  TX = PG14,  RX = PC7  

串口UART7   TX = PB4,   RX = PB3  (和SPI1/3共用)

串口UART8   TX = PJ8,   RX =PJ9   (和RGB硬件接口共用)

STM32-V7開發板使用了4個串口設備。

  •   串口1用於RS232接口,很多例子的pritnf結果就是輸出到串口1
  •   串口2用於GPS
  •   串口3用於RS485接口
  •   串口6 用於TTL串口插座,板子上有GPRS插座和串口WIFI插座。

下面是相關的原理圖:

串口3,RS485

 

關於485的PHY芯片SP3485E要注意以下幾個問題:

  •   SP3485E允許在同一總線上連接32個收發器。
  •   PB2(RS485_TX_EN)引腳控制收發狀態,高電平表示使能發送,低電平表示用於接收。
  •   由於 485的RO是輸出引腳,如果軟件上將PB2配置為輸出就會沖突(PB2輸出高,RO輸出低就會短路)。加電阻的話,PB2就可以做其他用途。主要是開發板才這樣弄,正式產品不加。
  •   電阻R15和R165的作用是避免CPU復位期間,TX為高阻時影響總線數據。
  •   電阻R4和R2是保證空閑時處於確定的邏輯狀態,提供可靠性。
  •   電阻R3是終端電阻。使用終端電阻是為了阻抗匹配,防止不匹配引起的噪聲干擾。一般在300米以下,19200bps不需要接終端電阻。終端電阻要接在傳輸總線的兩端。

 

31.4 RS485驅動設計

RS485的驅動實現是建立在第31章講解的串口FIFO基礎上,關鍵的知識點已經在第31章節做了詳細講解,這里把485驅動涉及到的兩個關鍵地方做個說明。

31.4.1 RS485驅動初始化

RS485驅動的初始化要對收發控制引腳進行配置,這點要注意,對應的代碼如下:

/*
*********************************************************************************************************
*    函 數 名: bsp_InitUart
*    功能說明: 初始化串口硬件,並對全局變量賦初值.
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
void bsp_InitUart(void)
{
    
    UartVarInit();     /* 必須先初始化全局變量,再配置硬件 */

    InitHardUart();    /* 配置串口的硬件參數(波特率等) */

    RS485_InitTXE();    /* 配置RS485芯片的發送使能硬件,配置為推挽輸出 */
}
/*
*********************************************************************************************************
*    函 數 名: RS485_InitTXE
*    功能說明: 配置RS485發送使能口線 TXE
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
void RS485_InitTXE(void)
{
    GPIO_InitTypeDef gpio_init;
    
    /* 打開GPIO時鍾 */
    RS485_TXEN_GPIO_CLK_ENABLE();
    
    /* 配置引腳為推挽輸出 */
    gpio_init.Mode = GPIO_MODE_OUTPUT_PP;            /* 推挽輸出 */
    gpio_init.Pull = GPIO_NOPULL;                 /* 上下拉電阻不使能 */
    gpio_init.Speed = GPIO_SPEED_FREQ_VERY_HIGH;    /* GPIO速度等級 */
    gpio_init.Pin = RS485_TXEN_PIN;
    HAL_GPIO_Init(RS485_TXEN_GPIO_PORT, &gpio_init);    
}

31.4.2 RS485驅動回調函數初始化

由於RS485是半雙工,收發要做切換,初始化的時候專門配套了回調函數:

/*
*********************************************************************************************************
*    函 數 名: UartVarInit
*    功能說明: 初始化串口相關的變量
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
static void UartVarInit(void)
{

#if UART3_FIFO_EN == 1
    g_tUart3.uart = USART3;                        /* STM32 串口設備 */
    g_tUart3.pTxBuf = g_TxBuf3;                    /* 發送緩沖區指針 */
    g_tUart3.pRxBuf = g_RxBuf3;                    /* 接收緩沖區指針 */
    g_tUart3.usTxBufSize = UART3_TX_BUF_SIZE;         /* 發送緩沖區大小 */
    g_tUart3.usRxBufSize = UART3_RX_BUF_SIZE;         /* 接收緩沖區大小 */
    g_tUart3.usTxWrite = 0;                        /* 發送FIFO寫索引 */
    g_tUart3.usTxRead = 0;                        /* 發送FIFO讀索引 */
    g_tUart3.usRxWrite = 0;                        /* 接收FIFO寫索引 */
    g_tUart3.usRxRead = 0;                        /* 接收FIFO讀索引 */
    g_tUart3.usRxCount = 0;                        /* 接收到的新數據個數 */
    g_tUart3.usTxCount = 0;                        /* 待發送的數據個數 */ g_tUart3.SendBefor = RS485_SendBefor;             /* 發送數據前的回調函數 */
    g_tUart3.SendOver = RS485_SendOver;            /* 發送完畢后的回調函數 */
    g_tUart3.ReciveNew = RS485_ReciveNew;             /* 接收到新數據后的回調函數 */
    g_tUart3.Sending = 0;                        /* 正在發送中標志 */
#endif

}

上面代碼中置紅的部分是專用於485總線的,對應的代碼如下:

/*
*********************************************************************************************************
*    函 數 名: RS485_SendBefor
*    功能說明: 發送數據前的准備工作。對於RS485通信,請設置RS485芯片為發送狀態,
*              並修改 UartVarInit()中的函數指針等於本函數名,比如 g_tUart2.SendBefor = RS485_SendBefor
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
void RS485_SendBefor(void)
{
    RS485_TX_EN();    /* 切換RS485收發芯片為發送模式 */
}

/*
*********************************************************************************************************
*    函 數 名: RS485_SendOver
*    功能說明: 發送一串數據結束后的善后處理。對於RS485通信,請設置RS485芯片為接收狀態,
*              並修改 UartVarInit()中的函數指針等於本函數名,比如 g_tUart2.SendOver = RS485_SendOver
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
void RS485_SendOver(void)
{
    RS485_RX_EN();    /* 切換RS485收發芯片為接收模式 */
}

/*
*********************************************************************************************************
*    函 數 名: RS485_ReciveNew
*    功能說明: 接收到新的數據
*    形    參: _byte 接收到的新數據
*    返 回 值: 無
*********************************************************************************************************
*/
//extern void MODH_ReciveNew(uint8_t _byte);
void RS485_ReciveNew(uint8_t _byte)
{
//    MODH_ReciveNew(_byte);
}
  •   函數RS485_SendBefor

發送數據前的准備工作。對於RS485通信,請設置RS485芯片為發送狀態。

  •   函數RS485_SendOver

發送一串數據結束后的善后處理。對於RS485通信,請設置RS485芯片為接收狀態。

  •   函數RS485_ReciveNew

接收到新的數據,用於Modbus通信協議。這里未用到Modbus,所以將對應的內容注釋掉了。

31.4.3 RS485發送處理

串口數據的發送主要涉及到下面四個函數,調用關系是如下:

RS485_SendStr –> RS485_SendBuf –> comSendBuf -> UartSend

實際應用中,大家調用函數RS485_SendStr,RS485_SendBuf或者comSendBuf均可。另外特別注意代碼中置紅的部分,用於設置485發送使能。

/*
*********************************************************************************************************
*    函 數 名: RS485_SendBuf
*    功能說明: 通過RS485芯片發送一串數據。注意,本函數不等待發送完畢。
*    形    參: _ucaBuf : 數據緩沖區
*              _usLen : 數據長度
*    返 回 值: 無
*********************************************************************************************************
*/
void RS485_SendBuf(uint8_t *_ucaBuf, uint16_t _usLen)
{
    comSendBuf(COM3, _ucaBuf, _usLen);
}

/*
*********************************************************************************************************
*    函 數 名: RS485_SendStr
*    功能說明: 向485總線發送一個字符串,0結束。
*    形    參: _pBuf 字符串,0結束
*    返 回 值: 無
*********************************************************************************************************
*/
void RS485_SendStr(char *_pBuf)
{
    RS485_SendBuf((uint8_t *)_pBuf, strlen(_pBuf));
}

/*
*********************************************************************************************************
*    函 數 名: comSendBuf
*    功能說明: 向串口發送一組數據。數據放到發送緩沖區后立即返回,由中斷服務程序在后台完成發送
*    形    參: _ucPort: 端口號(COM1 - COM8)
*              _ucaBuf: 待發送的數據緩沖區
*              _usLen : 數據長度
*    返 回 值: 無
*********************************************************************************************************
*/
void comSendBuf(COM_PORT_E _ucPort, uint8_t *_ucaBuf, uint16_t _usLen)
{
    UART_T *pUart;

    pUart = ComToUart(_ucPort);
    if (pUart == 0)
    {
        return;
    }

    if (pUart->SendBefor != 0)
    {
        pUart->SendBefor();        /* 如果是RS485通信,可以在這個函數中將RS485設置為發送模式 */
    }

    UartSend(pUart, _ucaBuf, _usLen);
}

/*
*********************************************************************************************************
*    函 數 名: UartSend
*    功能說明: 填寫數據到UART發送緩沖區,並啟動發送中斷。中斷處理函數發送完畢后,自動關閉發送中斷
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
static void UartSend(UART_T *_pUart, uint8_t *_ucaBuf, uint16_t _usLen)
{
    uint16_t i;

    for (i = 0; i < _usLen; i++)
    {
        /* 如果發送緩沖區已經滿了,則等待緩沖區空 */
        while (1)
        {
            __IO uint16_t usCount;

            DISABLE_INT();
            usCount = _pUart->usTxCount;
            ENABLE_INT();

            if (usCount < _pUart->usTxBufSize)
            {
                break;
            }
            else if(usCount == _pUart->usTxBufSize)/* 數據已填滿緩沖區 */
            {
                if((_pUart->uart->CR1 & USART_CR1_TXEIE) == 0)
                {
                    SET_BIT(_pUart->uart->CR1, USART_CR1_TXEIE);
                }  
            }
        }

        /* 將新數據填入發送緩沖區 */
        _pUart->pTxBuf[_pUart->usTxWrite] = _ucaBuf[i];

        DISABLE_INT();
        if (++_pUart->usTxWrite >= _pUart->usTxBufSize)
        {
            _pUart->usTxWrite = 0;
        }
        _pUart->usTxCount++;
        ENABLE_INT();
    }

    SET_BIT(_pUart->uart->CR1, USART_CR1_TXEIE);    /* 使能發送中斷(緩沖區空) */
}

函數UartSend的作用就是把要發送的數據填到發送緩沖區里面,並使能發送空中斷。

  •   如果要發送的數據沒有超過發送緩沖區大小,實現起來還比較容易,直接把數據填到FIFO里面,並使能發送空中斷即可。
  •   如果超過了FIFO大小,就需要等待有空間可用,針對這種情況有個重要的知識點,就是當緩沖剛剛填滿的時候要判斷發送空中斷是否開啟了,如果填滿了還沒有開啟,就會卡死在while循環中,所以多了一個剛填滿時的判斷,填滿了還沒有開啟發送空中斷,要開啟下。

 

注意:由於函數UartSend做了static作用域限制,僅可在bsp_uart_fifo.c文件中調用。函數RS485_SendStr,RS485_SendBuf或者comSendBuf是供用戶調用的。

31.4.4 RS485數據接收

下面我們再來看看接收的函數:

/*
*********************************************************************************************************
*    函 數 名: comGetChar
*    功能說明: 從接收緩沖區讀取1字節,非阻塞。無論有無數據均立即返回。
*    形    參: _ucPort: 端口號(COM1 - COM8)
*              _pByte: 接收到的數據存放在這個地址
*    返 回 值: 0 表示無數據, 1 表示讀取到有效字節
*********************************************************************************************************
*/
uint8_t comGetChar(COM_PORT_E _ucPort, uint8_t *_pByte)
{
    UART_T *pUart;

    pUart = ComToUart(_ucPort);
    if (pUart == 0)
    {
        return 0;
    }

    return UartGetChar(pUart, _pByte);
}

/*
*********************************************************************************************************
*    函 數 名: UartGetChar
*    功能說明: 從串口接收緩沖區讀取1字節數據 (用於主程序調用)
*    形    參: _pUart : 串口設備
*              _pByte : 存放讀取數據的指針
*    返 回 值: 0 表示無數據  1表示讀取到數據
*********************************************************************************************************
*/
static uint8_t UartGetChar(UART_T *_pUart, uint8_t *_pByte)
{
    uint16_t usCount;

    /* usRxWrite 變量在中斷函數中被改寫,主程序讀取該變量時,必須進行臨界區保護 */
    DISABLE_INT();
    usCount = _pUart->usRxCount;
    ENABLE_INT();

    /* 如果讀和寫索引相同,則返回0 */
    //if (_pUart->usRxRead == usRxWrite)
    if (usCount == 0)    /* 已經沒有數據 */
    {
        return 0;
    }
    else
    {
        *_pByte = _pUart->pRxBuf[_pUart->usRxRead];        /* 從串口接收FIFO取1個數據 */

        /* 改寫FIFO讀索引 */
        DISABLE_INT();
        if (++_pUart->usRxRead >= _pUart->usRxBufSize)
        {
            _pUart->usRxRead = 0;
        }
        _pUart->usRxCount--;
        ENABLE_INT();
        return 1;
    }
}

函數comGetChar是專門供用戶調用的,用於從接收FIFO中讀取1個數據。具體代碼的實現也比較好理解,主要是接收FIFO的調整。

注意:由於函數UartGetChar做了static作用域限制,僅可在bsp_uart_fifo.c文件中調用。

31.4.5 RS485驅動中斷服務程序的處理

串口中斷服務程序是實現RS485驅動的關鍵部分,主要實現如下三個功能:

  •   收到新的數據后,會將數據壓入RX_FIFO。
  •   檢測到發送緩沖區空后,會從TX_FIFO中取下一個數據並發送。
  •   對於RS485半雙工串口,發送前會設置一個GPIO=1控制RS485收發器進入發送狀態,當最后一個字節的最后一個bit傳送完畢后,設置這個GPIO=0讓RS485收發器進入接收狀態。

 

下面我們分析一下串口中斷處理的完整過程。

當產生串口中斷后,CPU會查找中斷向量表,獲得中斷服務程序的入口地址。入口函數為USART1_IRQHandler,這個函數在啟動文件startup_stm32h743xx.s匯編代碼中已經有實現。我們在c代碼中需要重寫一個同樣名字的函數就可以重載它。如果不重載,啟動文件中缺省的中斷服務程序就是一個死循環,等於 while(1);

我們將串口中斷服務程序放在bsp_uart_fifo.c文件,沒有放到 stm32h7xx_it.c。當應用不需要串口功能時,直接從工程中刪除bsp_uart_fifo.c接口,不必再去整理stm32h7xx_it.c這個文件。下面展示的代碼是8個串口的中斷服務程序,RS485用的USART3。

#if UART1_FIFO_EN == 1
void USART1_IRQHandler(void)
{
    UartIRQ(&g_tUart1);
}
#endif

#if UART2_FIFO_EN == 1
void USART2_IRQHandler(void)
{
    UartIRQ(&g_tUart2);
}
#endif

#if UART3_FIFO_EN == 1
void USART3_IRQHandler(void)
{
    UartIRQ(&g_tUart3);
}
#endif

#if UART4_FIFO_EN == 1
void UART4_IRQHandler(void)
{
    UartIRQ(&g_tUart4);
}
#endif

#if UART5_FIFO_EN == 1
void UART5_IRQHandler(void)
{
    UartIRQ(&g_tUart5);
}
#endif

#if UART6_FIFO_EN == 1
void USART6_IRQHandler(void)
{
    UartIRQ(&g_tUart6);
}
#endif

#if UART7_FIFO_EN == 1
void UART7_IRQHandler(void)
{
    UartIRQ(&g_tUart7);
}
#endif

#if UART8_FIFO_EN == 1
void UART8_IRQHandler(void)
{
    UartIRQ(&g_tUart8);
}
#endif

大家可以看到,這8個中斷服務程序都調用了同一個處理函數UartIRQ。我們只需要調通一個串口FIFO驅動,那么其他的串口驅動也就都通了。

下面,我們來看看UartIRQ函數的實現代碼。

/*
*********************************************************************************************************
*    函 數 名: UartIRQ
*    功能說明: 供中斷服務程序調用,通用串口中斷處理函數
*    形    參: _pUart : 串口設備
*    返 回 值: 無
*********************************************************************************************************
*/
static void UartIRQ(UART_T *_pUart)
{
    uint32_t isrflags   = READ_REG(_pUart->uart->ISR);
    uint32_t cr1its     = READ_REG(_pUart->uart->CR1);
    uint32_t cr3its     = READ_REG(_pUart->uart->CR3);
    
    /* 處理接收中斷  */
    if ((isrflags & USART_ISR_RXNE) != RESET)
    {
        /* 從串口接收數據寄存器讀取數據存放到接收FIFO */
        uint8_t ch;

        ch = READ_REG(_pUart->uart->RDR);               /* 讀串口接收數據寄存器 */
        _pUart->pRxBuf[_pUart->usRxWrite] = ch;         /* 填入串口接收FIFO */
        if (++_pUart->usRxWrite >= _pUart->usRxBufSize) /* 接收FIFO的寫指針+1 */
        {
            _pUart->usRxWrite = 0;
        }
        if (_pUart->usRxCount < _pUart->usRxBufSize)    /* 統計未處理的字節個數 */
        {
            _pUart->usRxCount++;
        }

        /* 回調函數,通知應用程序收到新數據,一般是發送1個消息或者設置一個標記 */
        //if (_pUart->usRxWrite == _pUart->usRxRead)
        //if (_pUart->usRxCount == 1)
        {
            if (_pUart->ReciveNew)
            {
                _pUart->ReciveNew(ch); /* 比如,交給MODBUS解碼程序處理字節流 */
            }
        }
    }

    /* 處理發送緩沖區空中斷 */
    if ( ((isrflags & USART_ISR_TXE) != RESET) && (cr1its & USART_CR1_TXEIE) != RESET)
    {
        //if (_pUart->usTxRead == _pUart->usTxWrite) 
        if (_pUart->usTxCount == 0)  /* 發送緩沖區已無數據可取 */
        {
        /* 發送緩沖區的數據已取完時, 禁止發送緩沖區空中斷 (注意:此時最后1個數據還未真正發送完畢)*/
            //USART_ITConfig(_pUart->uart, USART_IT_TXE, DISABLE);
            CLEAR_BIT(_pUart->uart->CR1, USART_CR1_TXEIE);

            /* 使能數據發送完畢中斷 */
            //USART_ITConfig(_pUart->uart, USART_IT_TC, ENABLE);
            SET_BIT(_pUart->uart->CR1, USART_CR1_TCIE);
        }
        Else  /* 還有數據等待發送 */
        {
            _pUart->Sending = 1;
            
            /* 從發送FIFO取1個字節寫入串口發送數據寄存器 */
            //USART_SendData(_pUart->uart, _pUart->pTxBuf[_pUart->usTxRead]);
            _pUart->uart->TDR = _pUart->pTxBuf[_pUart->usTxRead];
            if (++_pUart->usTxRead >= _pUart->usTxBufSize)
            {
                _pUart->usTxRead = 0;
            }
            _pUart->usTxCount--;
        }

    }
    /* 數據bit位全部發送完畢的中斷 */
    if (((isrflags & USART_ISR_TC) != RESET) && ((cr1its & USART_CR1_TCIE) != RESET))
    {
        //if (_pUart->usTxRead == _pUart->usTxWrite)
        if (_pUart->usTxCount == 0)
        {
            /* 如果發送FIFO的數據全部發送完畢,禁止數據發送完畢中斷 */
            //USART_ITConfig(_pUart->uart, USART_IT_TC, DISABLE);
            CLEAR_BIT(_pUart->uart->CR1, USART_CR1_TCIE);

            /* 回調函數, 一般用來處理RS485通信,將RS485芯片設置為接收模式,避免搶占總線 */
            if (_pUart->SendOver)
            {
                _pUart->SendOver();
            }
            
            _pUart->Sending = 0;
        }
        else
        {
            /* 正常情況下,不會進入此分支 */

            /* 如果發送FIFO的數據還未完畢,則從發送FIFO取1個數據寫入發送數據寄存器 */
            //USART_SendData(_pUart->uart, _pUart->pTxBuf[_pUart->usTxRead]);
            _pUart->uart->TDR = _pUart->pTxBuf[_pUart->usTxRead];
            if (++_pUart->usTxRead >= _pUart->usTxBufSize)
            {
                _pUart->usTxRead = 0;
            }
            _pUart->usTxCount--;
        }
    }
    
    /* 清除中斷標志 */
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_PEF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_FEF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_NEF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_OREF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_IDLEF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_TCF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_LBDF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_CTSF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_CMF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_WUF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_TXFECF);    
}

中斷服務程序的處理主要分為兩部分,接收數據的處理和發送數據的處理,詳情看程序注釋即可,已經比較詳細,下面重點把思路說一下:

  •   接收數據處理

接收數據的處理是判斷ISR寄存器的USART_ISR_RXNE標志是否置位,如果置位表示RDR接收寄存器已經存入數據。然后將數據讀入到接收FIFO空間。

特別注意里面的ReciveNew處理,這個在Modbus協議里面要用到。

  •   發送數據處理

發送數據主要是發送空中斷TEX和發送完成中斷TC的處理,當TXE=1時,只是表示發送數據寄存器為空了,此時可以填充下一個准備發送的數據了。當為TDR發送寄存器賦值后,硬件啟動發送,等所有的bit傳送完畢后,TC標志設置為1。如果是RS232全雙工通信,可以只用TXE標志控制發送過程。如果是RS485半雙工通通信,就需要利用TC標志了,因為在最后一個bit傳送完畢后,需要設置RS485收發器進入到接收狀態。

31.5 RS485板級支持包(bsp_uart_fifo.c)

串口驅動文件bsp_uart_fifo.c主要實現了如下幾個API供用戶調用:

  •   bsp_InitUart
  •   comSendBuf
  •   comSendChar
  •   comGetChar

31.5.1 函數bsp_InitUart

函數原型:

void bsp_InitUart(void)

函數描述:

此函數主要用於串口的初始化,使用所有其它API之前,務必優先調用此函數。

使用舉例:

串口的初始化函數在bsp.c文件的bsp_Init函數里面調用。

31.5.2 函數comSendBuf

函數原型:

void comSendBuf(COM_PORT_E _ucPort, uint8_t *_ucaBuf, uint16_t _usLen);

函數描述:

此函數用於向串口發送一組數據,非阻塞方式,數據放到發送緩沖區后立即返回,由中斷服務程序在后台完成發送。

函數參數:

  •   第1個參數_ucPort是端口號,范圍COM1 - COM8。
  •   第2個參數_ucaBuf是待發送的數據緩沖區地址。
  •   第3個參數_usLen是要發送數據的字節數。

注意事項:

  •   此函數的解讀在第30章30.3.5小節。
  •   發送的數據最好不要超過bsp_uart_fifo.h文件中定義的發送緩沖區大小,從而實現最優的工作方式。因為超過后需要在發送函數等待有發送空間可用。

使用舉例:

調用此函數前,務必優先調用函數bsp_InitUart進行初始化。

const char buf1[] = "接收到串口命令1\r\n";

comSendBuf(COM1, (uint8_t *)buf1, strlen(buf1));

31.5.3 函數RS485_SendBuf

函數原型:

void RS485_SendBuf(uint8_t *_ucaBuf, uint16_t _usLen)

函數描述:

此函數用於向RS485總線發送一組數據,非阻塞方式,數據放到發送緩沖區后立即返回,由中斷服務程序在后台完成發送。此函數是通過調用函數comSendBuf實現的。

函數參數:

  •   第1個參數_ucaBuf是數據緩沖區。
  •   第2個參數_usLen是數據長度。

注意事項:

調用此函數前,務必優先調用函數bsp_InitUart進行初始化

31.5.4 函數RS485_SendStr

函數原型:

void RS485_SendStr(char *_pBuf)

函數描述:

此函數用於向RS485總線發送一個字符串,非阻塞方式,數據放到發送緩沖區后立即返回,由中斷服務程序在后台完成發送。此函數是通過調用函數RS485_SendBuf實現的。

函數參數:

  •   第1個參數_pBuf是字符串地址,0結束。

注意事項:

調用此函數前,務必優先調用函數bsp_InitUart進行初始化

31.5.5 函數comGetChar

函數原型:

uint8_t comGetChar(COM_PORT_E _ucPort, uint8_t *_pByte)

函數描述:

此函數用於從接收緩沖區讀取1字節,非阻塞。無論有無數據均立即返回。

函數參數:

  •   第1個參數_ucPort是端口號,范圍COM1 - COM8。
  •   第2個參數_pByte用於存放接收到的數據。
  •   返回值,返回0表示無數據, 1 表示讀取到有效字節。

注意事項:

  •   此函數的解讀在第30章30.3.6小節。

使用舉例:

調用此函數前,務必優先調用函數bsp_InitUart進行初始化。

比如從串口1讀取一個字符就是:comGetChar(COM1, &read)

31.6 RS485驅動移植和使用

RS485移植步驟如下:

  •   第1步:復制bsp_uart_fifo.h和bsp_uart_fifo.c到自己的工程目錄,並添加到工程里面。
  •   第2步:根據485要使用的串口和收發緩沖大小,修改下面的宏定義即可。
#define    UART1_FIFO_EN    1
#define    UART2_FIFO_EN    0
#define    UART3_FIFO_EN    0
#define    UART4_FIFO_EN    0
#define    UART5_FIFO_EN    0
#define    UART6_FIFO_EN    0
#define    UART7_FIFO_EN    0
#define    UART8_FIFO_EN    0

/* 定義串口波特率和FIFO緩沖區大小,分為發送緩沖區和接收緩沖區, 支持全雙工 */
#if UART1_FIFO_EN == 1
    #define UART1_BAUD            115200
    #define UART1_TX_BUF_SIZE    1*1024
    #define UART1_RX_BUF_SIZE    1*1024
#endif

#if UART2_FIFO_EN == 1
    #define UART2_BAUD            9600
    #define UART2_TX_BUF_SIZE    10
    #define UART2_RX_BUF_SIZE    2*1024
#endif

#if UART3_FIFO_EN == 1
    #define UART3_BAUD            9600
    #define UART3_TX_BUF_SIZE    1*1024
    #define UART3_RX_BUF_SIZE    1*1024
#endif

#if UART4_FIFO_EN == 1
    #define UART4_BAUD            115200
    #define UART4_TX_BUF_SIZE    1*1024
    #define UART4_RX_BUF_SIZE    1*1024
#endif

#if UART5_FIFO_EN == 1
    #define UART5_BAUD            115200
    #define UART5_TX_BUF_SIZE    1*1024
    #define UART5_RX_BUF_SIZE    1*1024
#endif

#if UART6_FIFO_EN == 1
    #define UART6_BAUD            115200
    #define UART6_TX_BUF_SIZE    1*1024
    #define UART6_RX_BUF_SIZE    1*1024
#endif

#if UART7_FIFO_EN == 1
    #define UART7_BAUD            115200
    #define UART7_TX_BUF_SIZE    1*1024
    #define UART7_RX_BUF_SIZE    1*1024
#endif

#if UART8_FIFO_EN == 1
    #define UART8_BAUD            115200
    #define UART8_TX_BUF_SIZE    1*1024
    #define UART8_RX_BUF_SIZE    1*1024
#endif
  •   第3步:配置485 PHY的收發切換引腳
/* PB2 控制RS485芯片的發送使能 */
#define RS485_TXEN_GPIO_CLK_ENABLE()      __HAL_RCC_GPIOB_CLK_ENABLE()
#define RS485_TXEN_GPIO_PORT              GPIOB
#define RS485_TXEN_PIN                    GPIO_PIN_2

#define RS485_RX_EN()    RS485_TXEN_GPIO_PORT->BSRRH = RS485_TXEN_PIN
#define RS485_TX_EN()    RS485_TXEN_GPIO_PORT->BSRRL = RS485_TXEN_PIN
  •   第4步:根據使用的串口,配置下面函數的幾個參數成員,下面是以串口3為例說明(置紅部分):
static void UartVarInit(void)
{

#if UART3_FIFO_EN == 1
    g_tUart3.uart = USART3;                        /* STM32 串口設備 */
    g_tUart3.pTxBuf = g_TxBuf3;                    /* 發送緩沖區指針 */
    g_tUart3.pRxBuf = g_RxBuf3;                    /* 接收緩沖區指針 */
    g_tUart3.usTxBufSize = UART3_TX_BUF_SIZE;         /* 發送緩沖區大小 */
    g_tUart3.usRxBufSize = UART3_RX_BUF_SIZE;         /* 接收緩沖區大小 */
    g_tUart3.usTxWrite = 0;                        /* 發送FIFO寫索引 */
    g_tUart3.usTxRead = 0;                        /* 發送FIFO讀索引 */
    g_tUart3.usRxWrite = 0;                        /* 接收FIFO寫索引 */
    g_tUart3.usRxRead = 0;                        /* 接收FIFO讀索引 */
    g_tUart3.usRxCount = 0;                        /* 接收到的新數據個數 */
    g_tUart3.usTxCount = 0;                        /* 待發送的數據個數 */
    g_tUart3.SendBefor = RS485_SendBefor;             /* 發送數據前的回調函數 */
    g_tUart3.SendOver = RS485_SendOver;            /* 發送完畢后的回調函數 */
    g_tUart3.ReciveNew = RS485_ReciveNew;             /* 接收到新數據后的回調函數 */
    g_tUart3.Sending = 0;                        /* 正在發送中標志 */
#endif
}
  •   第5步:這幾個驅動文件主要用到HAL庫的GPIO和串口驅動文件,簡單省事些可以添加所有HAL庫.C源文件進來。
  •   第6步,應用方法看本章節配套例子即可。

31.7 實驗例程設計框架

通過程序設計框架,讓大家先對配套例程有一個全面的認識,然后再理解細節,本次實驗例程的設計框架如下:

 

  第1階段,上電啟動階段:

  •  這部分在第14章進行了詳細說明。

  第2階段,進入main函數:

  •   第1步,硬件初始化,主要是MPU,Cache,HAL庫,系統時鍾,滴答定時器,LED和串口。
  •   第2步,485應用程序設計部分,具體分為兩個部分,接收命令和發送命令。

31.8 實驗例程說明(MDK)

配套例子:

V7-016_RS485多機通訊

實驗目的:

  1. 學習RS485多機通訊的實現。

實驗內容:

  1. 由於通信距離較短,SP3485E芯片上缺省未貼的電阻不需要貼上,大家可以根據需要貼上電阻做測試。
  2. 本例子支持多個485節點,不需要設置主設備和從設備,所有節點下載此程序即可。
  3. 開發板的485-A端子連接到一起,485-B端子連接到一起,具體連接看工程Doc文件夾中的截圖。

實驗操作:

  1. 按下開發板上的K1鍵點亮LED1,松開熄滅LED1,同時打印按鍵事件到串口1。485總線上的其它開發板做相同的動作。
  2. 按下開發板上的K2鍵,啟動50ms的自動重裝定時器,每隔50ms翻轉LED2,並向485總線上的其它開發板發送按鍵K2按下消息,從而也實現每隔50ms翻轉LED2。
  3. 按下開發板上的K3按鍵,停止K2按鍵啟動的50ms自動重載定時器,485總線上的其它開發板做相同的動作。
  4. 按下開發板上的搖桿(上下左右,OK共5種),會通過串口1打印搖桿事件。485總線上的其它開發板做相同的動作。

注意事項:

  1. RS485 PHY的發送使能用的引腳PB11,測試前需要將板子J5處的跳線帽短接到PB11端。

多機接線效果:

 

 

上電后串口打印的信息:

波特率 115200,數據位 8,奇偶校驗位無,停止位 1

 

程序設計:

  系統棧大小分配:


  RAM空間用的DTCM:


  硬件外設初始化

硬件外設的初始化是在 bsp.c 文件實現:

/*
*********************************************************************************************************
*    函 數 名: bsp_Init
*    功能說明: 初始化所有的硬件設備。該函數配置CPU寄存器和外設的寄存器並初始化一些全局變量。只需要調用一次
*    形    參:無
*    返 回 值: 無
*********************************************************************************************************
*/
void bsp_Init(void)
{
    /* 配置MPU */
    MPU_Config();
    
    /* 使能L1 Cache */
    CPU_CACHE_Enable();

    /* 
       STM32H7xx HAL 庫初始化,此時系統用的還是H7自帶的64MHz,HSI時鍾:
       - 調用函數HAL_InitTick,初始化滴答時鍾中斷1ms。
       - 設置NVIV優先級分組為4。
     */
    HAL_Init();

    /* 
       配置系統時鍾到400MHz
       - 切換使用HSE。
       - 此函數會更新全局變量SystemCoreClock,並重新配置HAL_InitTick。
    */
    SystemClock_Config();

    /* 
       Event Recorder:
       - 可用於代碼執行時間測量,MDK5.25及其以上版本才支持,IAR不支持。
       - 默認不開啟,如果要使能此選項,務必看V7開發板用戶手冊第xx章
    */    
#if Enable_EventRecorder == 1  
    /* 初始化EventRecorder並開啟 */
    EventRecorderInitialize(EventRecordAll, 1U);
    EventRecorderStart();
#endif
    
    bsp_InitKey();        /* 按鍵初始化,要放在滴答定時器之前,因為按鈕檢測是通過滴答定時器掃描 */
    bsp_InitTimer();      /* 初始化滴答定時器 */
    bsp_InitUart();    /* 初始化串口 */
    bsp_InitExtIO();    /* 初始化FMC總線74HC574擴展IO. 必須在 bsp_InitLed()前執行 */    
    bsp_InitLed();        /* 初始化LED */    
}

  MPU配置和Cache配置:

數據Cache和指令Cache都開啟。配置了AXI SRAM區(本例子未用到AXI SRAM)和FMC的擴展IO區。

/*
*********************************************************************************************************
*    函 數 名: MPU_Config
*    功能說明: 配置MPU
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
static void MPU_Config( void )
{
    MPU_Region_InitTypeDef MPU_InitStruct;

    /* 禁止 MPU */
    HAL_MPU_Disable();

    /* 配置AXI SRAM的MPU屬性為Write back, Read allocate,Write allocate */
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x24000000;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_512KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL1;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_ENABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    
    
    /* 配置FMC擴展IO的MPU屬性為Device或者Strongly Ordered */
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x60000000;
    MPU_InitStruct.Size             = ARM_MPU_REGION_SIZE_64KB;    
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE;    
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER1;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_ENABLE;
    
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    /*使能 MPU */
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

/*
*********************************************************************************************************
*    函 數 名: CPU_CACHE_Enable
*    功能說明: 使能L1 Cache
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
static void CPU_CACHE_Enable(void)
{
    /* 使能 I-Cache */
    SCB_EnableICache();

    /* 使能 D-Cache */
    SCB_EnableDCache();
}

  主功能:

主功能的實現主要分為兩部分:

  •   獲取其它RS485設備發來的命令,並執行相應功能。

通過函數comGetChar(COM3, &ucDataTravel)以查詢、非阻塞方式獲取其它開發板發來的數據。

  •   根據不同的按鍵消息,向其它485設備發送命令

通過函數comSendChar(COM3, ucDataTravel)以非阻塞方式向其它485設備發送命令,以此來執行同樣的功能。

/*
*********************************************************************************************************
*    函 數 名: main
*    功能說明: c程序入口
*    形    參: 無
*    返 回 值: 錯誤代碼(無需處理)
*********************************************************************************************************
*/
int main(void)
{
    uint8_t ucKeyCode;        /* 按鍵代碼 */
    uint8_t ucDataTravel;    /* 發送變量 */
    uint8_t ucDataRec;         /* 接收變量 */    

    bsp_Init();        /* 硬件初始化 */
    
    PrintfLogo();    /* 打印例程名稱和版本等信息 */
    PrintfHelp();    /* 打印操作提示 */

    /* 進入主程序循環體 */
    while (1)
    {
        bsp_Idle();        /* 這個函數在bsp.c文件。用戶可以修改這個函數實現CPU休眠和喂狗 */
        
        /* 獲取其它開發板通過485總線發來的數據 */
        if(comGetChar(COM3, &ucDataRec))
        {
            switch (ucDataRec)
            {
                case KEY_DOWN_K1:            /* 獲得K1鍵按下消息 */
                    bsp_LedOn(1);
                    printf("K1鍵按下, LED1點亮\r\n");
                    break;

                case KEY_UP_K1:              /* 獲得K1鍵釋放消息 */
                    bsp_LedOff(1);
                    printf("K1鍵彈起, LED1熄滅\r\n");
                    break;

                case KEY_DOWN_K2:            /* 獲得K1鍵按下消息 */
                    bsp_LedToggle(2);
                    break;

                case JOY_DOWN_U:            /* 獲得搖桿UP鍵按下 */
                    printf("搖桿上鍵按下\r\n");
                    break;

                case JOY_DOWN_D:            /* 獲得搖桿DOWN鍵按下 */
                    printf("搖桿下鍵按下\r\n");
                    break;

                case JOY_DOWN_L:            /* 獲得搖桿LEFT鍵按下 */
                    printf("搖桿左鍵按下\r\n");
                    break;

                case JOY_DOWN_R:            /* 獲得搖桿RIGHT鍵按下 */
                    printf("搖桿右鍵按下\r\n");
                    break;

                case JOY_DOWN_OK:            /* 獲得搖桿OK鍵按下 */
                    printf("搖桿OK鍵按下\r\n");
                    break;

                case JOY_UP_OK:                /* 獲得搖桿OK鍵彈起 */
                    printf("搖桿OK鍵彈起\r\n");
                    break;

                default:
                    /* 其它的鍵值不處理 */
                    break;
            }
        }

        /* 判斷定時器超時時間 */
        if (bsp_CheckTimer(0))    
        {
            /* 每隔50ms 進來一次 */  
            bsp_LedToggle(2);
            /* 向其它開發板發送按鍵K2按下的消息 */
            ucDataTravel = KEY_DOWN_K2;
            comSendChar(COM3, ucDataTravel);
        }

        /* 按鍵濾波和檢測由后台systick中斷服務程序實現,我們只需要調用bsp_GetKey讀取鍵值即可。 */
        ucKeyCode = bsp_GetKey();    /* 讀取鍵值, 無鍵按下時返回 KEY_NONE = 0 */
        if (ucKeyCode != KEY_NONE)
        {
            switch (ucKeyCode)
            {
                case KEY_DOWN_K1:            /* K1鍵按下 */
                    ucDataTravel = KEY_DOWN_K1;
                    comSendChar(COM3, ucDataTravel);
                    bsp_LedOn(1);
                    printf("K1鍵按下, LED1點亮\r\n");
                    break;

                case KEY_UP_K1:            /* K1鍵彈起 */
                    ucDataTravel = KEY_UP_K1;
                    comSendChar(COM3, ucDataTravel);
                    bsp_LedOff(1);
                    printf("K1鍵彈起, LED1熄滅\r\n");
                    break;

                case KEY_DOWN_K2:            /* K2鍵按下 */
                    bsp_StartAutoTimer(0, 50);    /* 啟動1個50ms的自動重裝的定時器 */
                    break;

                case KEY_DOWN_K3:            /* K3鍵按下 */
                    bsp_StopTimer(0);       /* 停止自動重裝的定時器 */
                    break;

                case JOY_DOWN_U:            /* 搖桿UP鍵按下 */
                    ucDataTravel = JOY_DOWN_U;
                    comSendChar(COM3, ucDataTravel);
                    printf("搖桿上鍵按下\r\n");
                    break;

                case JOY_DOWN_D:            /* 搖桿DOWN鍵按下 */
                    ucDataTravel = JOY_DOWN_D;
                    comSendChar(COM3, ucDataTravel);
                    printf("搖桿下鍵按下\r\n");
                    break;

                case JOY_DOWN_L:            /* 搖桿LEFT鍵按下 */
                    ucDataTravel = JOY_DOWN_L;
                    comSendChar(COM3, ucDataTravel);
                    printf("搖桿左鍵按下\r\n");
                    break;

                case JOY_DOWN_R:            /* 搖桿RIGHT鍵按下 */
                    ucDataTravel = JOY_DOWN_R;
                    comSendChar(COM3, ucDataTravel);
                    printf("搖桿右鍵按下\r\n");
                    break;

                case JOY_DOWN_OK:            /* 搖桿OK鍵按下 */
                    ucDataTravel = JOY_DOWN_OK;
                    comSendChar(COM3, ucDataTravel);
                    printf("搖桿OK鍵按下\r\n");
                    break;

                case JOY_UP_OK:                /* 搖桿OK鍵彈起 */
                    ucDataTravel = JOY_UP_OK;
                    comSendChar(COM3, ucDataTravel);
                    printf("搖桿OK鍵彈起\r\n");
                    break;

                default:
                    /* 其它的鍵值不處理 */
                    break;
            }
        }
    }
}

31.9 實驗例程說明(IAR)

配套例子:

V7-016_RS485多機通訊

實驗目的:

  1. 學習RS485多機通訊的實現。

實驗內容:

  1. 由於通信距離較短,SP3485E芯片上缺省未貼的電阻不需要貼上,大家可以根據需要貼上電阻做測試。
  2. 本例子支持多個485節點,不需要設置主設備和從設備,所有節點下載此程序即可。
  3. 開發板的485-A端子連接到一起,485-B端子連接到一起,具體連接看工程Doc文件夾中的截圖。

實驗操作:

  1. 按下開發板上的K1鍵點亮LED1,松開熄滅LED1,同時打印按鍵事件到串口1。485總線上的其它開發板做相同的動作。
  2. 按下開發板上的K2鍵,啟動50ms的自動重裝定時器,每隔50ms翻轉LED2,並向485總線上的其它開發板發送按鍵K2按下消息,從而也實現每隔50ms翻轉LED2。
  3. 按下開發板上的K3按鍵,停止K2按鍵啟動的50ms自動重載定時器,485總線上的其它開發板做相同的動作。
  4. 按下開發板上的搖桿(上下左右,OK共5種),會通過串口1打印搖桿事件。485總線上的其它開發板做相同的動作。

注意事項:

  1. RS485 PHY的發送使能用的引腳PB11,測試前需要將板子J5處的跳線帽短接到PB11端。

多機接線效果:

 

 

上電后串口打印的信息:

波特率 115200,數據位 8,奇偶校驗位無,停止位 1

 

程序設計:

  系統棧大小分配:

 

  RAM空間用的DTCM:

 

  硬件外設初始化

硬件外設的初始化是在 bsp.c 文件實現:

/*
*********************************************************************************************************
*    函 數 名: bsp_Init
*    功能說明: 初始化所有的硬件設備。該函數配置CPU寄存器和外設的寄存器並初始化一些全局變量。只需要調用一次
*    形    參:無
*    返 回 值: 無
*********************************************************************************************************
*/
void bsp_Init(void)
{
    /* 配置MPU */
    MPU_Config();
    
    /* 使能L1 Cache */
    CPU_CACHE_Enable();

    /* 
       STM32H7xx HAL 庫初始化,此時系統用的還是H7自帶的64MHz,HSI時鍾:
       - 調用函數HAL_InitTick,初始化滴答時鍾中斷1ms。
       - 設置NVIV優先級分組為4。
     */
    HAL_Init();

    /* 
       配置系統時鍾到400MHz
       - 切換使用HSE。
       - 此函數會更新全局變量SystemCoreClock,並重新配置HAL_InitTick。
    */
    SystemClock_Config();

    /* 
       Event Recorder:
       - 可用於代碼執行時間測量,MDK5.25及其以上版本才支持,IAR不支持。
       - 默認不開啟,如果要使能此選項,務必看V7開發板用戶手冊第xx章
    */    
#if Enable_EventRecorder == 1  
    /* 初始化EventRecorder並開啟 */
    EventRecorderInitialize(EventRecordAll, 1U);
    EventRecorderStart();
#endif
    
    bsp_InitKey();        /* 按鍵初始化,要放在滴答定時器之前,因為按鈕檢測是通過滴答定時器掃描 */
    bsp_InitTimer();      /* 初始化滴答定時器 */
    bsp_InitUart();    /* 初始化串口 */
    bsp_InitExtIO();    /* 初始化FMC總線74HC574擴展IO. 必須在 bsp_InitLed()前執行 */    
    bsp_InitLed();        /* 初始化LED */    
}

  MPU配置和Cache配置:

數據Cache和指令Cache都開啟。配置了AXI SRAM區(本例子未用到AXI SRAM)和FMC的擴展IO區。

/*
*********************************************************************************************************
*    函 數 名: MPU_Config
*    功能說明: 配置MPU
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
static void MPU_Config( void )
{
    MPU_Region_InitTypeDef MPU_InitStruct;

    /* 禁止 MPU */
    HAL_MPU_Disable();

    /* 配置AXI SRAM的MPU屬性為Write back, Read allocate,Write allocate */
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x24000000;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_512KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL1;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_ENABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    
    
    /* 配置FMC擴展IO的MPU屬性為Device或者Strongly Ordered */
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x60000000;
    MPU_InitStruct.Size             = ARM_MPU_REGION_SIZE_64KB;    
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE;    
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER1;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_ENABLE;
    
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    /*使能 MPU */
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

/*
*********************************************************************************************************
*    函 數 名: CPU_CACHE_Enable
*    功能說明: 使能L1 Cache
*    形    參: 無
*    返 回 值: 無
*********************************************************************************************************
*/
static void CPU_CACHE_Enable(void)
{
    /* 使能 I-Cache */
    SCB_EnableICache();

    /* 使能 D-Cache */
    SCB_EnableDCache();
}

  主功能:

主功能的實現主要分為兩部分:

  •   獲取其它RS485設備發來的命令,並執行相應功能。

通過函數comGetChar(COM3, &ucDataTravel)以查詢、非阻塞方式獲取其它開發板發來的數據。

  •   根據不同的按鍵消息,向其它485設備發送命令

通過函數comSendChar(COM3, ucDataTravel)以非阻塞方式向其它485設備發送命令,以此來執行同樣的功能。

/*
*********************************************************************************************************
*    函 數 名: main
*    功能說明: c程序入口
*    形    參: 無
*    返 回 值: 錯誤代碼(無需處理)
*********************************************************************************************************
*/
int main(void)
{
    uint8_t ucKeyCode;        /* 按鍵代碼 */
    uint8_t ucDataTravel;    /* 發送變量 */
    uint8_t ucDataRec;         /* 接收變量 */    

    bsp_Init();        /* 硬件初始化 */
    
    PrintfLogo();    /* 打印例程名稱和版本等信息 */
    PrintfHelp();    /* 打印操作提示 */

    /* 進入主程序循環體 */
    while (1)
    {
        bsp_Idle();        /* 這個函數在bsp.c文件。用戶可以修改這個函數實現CPU休眠和喂狗 */
        
        /* 獲取其它開發板通過485總線發來的數據 */
        if(comGetChar(COM3, &ucDataRec))
        {
            switch (ucDataRec)
            {
                case KEY_DOWN_K1:            /* 獲得K1鍵按下消息 */
                    bsp_LedOn(1);
                    printf("K1鍵按下, LED1點亮\r\n");
                    break;

                case KEY_UP_K1:              /* 獲得K1鍵釋放消息 */
                    bsp_LedOff(1);
                    printf("K1鍵彈起, LED1熄滅\r\n");
                    break;

                case KEY_DOWN_K2:            /* 獲得K1鍵按下消息 */
                    bsp_LedToggle(2);
                    break;

                case JOY_DOWN_U:            /* 獲得搖桿UP鍵按下 */
                    printf("搖桿上鍵按下\r\n");
                    break;

                case JOY_DOWN_D:            /* 獲得搖桿DOWN鍵按下 */
                    printf("搖桿下鍵按下\r\n");
                    break;

                case JOY_DOWN_L:            /* 獲得搖桿LEFT鍵按下 */
                    printf("搖桿左鍵按下\r\n");
                    break;

                case JOY_DOWN_R:            /* 獲得搖桿RIGHT鍵按下 */
                    printf("搖桿右鍵按下\r\n");
                    break;

                case JOY_DOWN_OK:            /* 獲得搖桿OK鍵按下 */
                    printf("搖桿OK鍵按下\r\n");
                    break;

                case JOY_UP_OK:                /* 獲得搖桿OK鍵彈起 */
                    printf("搖桿OK鍵彈起\r\n");
                    break;

                default:
                    /* 其它的鍵值不處理 */
                    break;
            }
        }

        /* 判斷定時器超時時間 */
        if (bsp_CheckTimer(0))    
        {
            /* 每隔50ms 進來一次 */  
            bsp_LedToggle(2);
            /* 向其它開發板發送按鍵K2按下的消息 */
            ucDataTravel = KEY_DOWN_K2;
            comSendChar(COM3, ucDataTravel);
        }

        /* 按鍵濾波和檢測由后台systick中斷服務程序實現,我們只需要調用bsp_GetKey讀取鍵值即可。 */
        ucKeyCode = bsp_GetKey();    /* 讀取鍵值, 無鍵按下時返回 KEY_NONE = 0 */
        if (ucKeyCode != KEY_NONE)
        {
            switch (ucKeyCode)
            {
                case KEY_DOWN_K1:            /* K1鍵按下 */
                    ucDataTravel = KEY_DOWN_K1;
                    comSendChar(COM3, ucDataTravel);
                    bsp_LedOn(1);
                    printf("K1鍵按下, LED1點亮\r\n");
                    break;

                case KEY_UP_K1:            /* K1鍵彈起 */
                    ucDataTravel = KEY_UP_K1;
                    comSendChar(COM3, ucDataTravel);
                    bsp_LedOff(1);
                    printf("K1鍵彈起, LED1熄滅\r\n");
                    break;

                case KEY_DOWN_K2:            /* K2鍵按下 */
                    bsp_StartAutoTimer(0, 50);    /* 啟動1個50ms的自動重裝的定時器 */
                    break;

                case KEY_DOWN_K3:            /* K3鍵按下 */
                    bsp_StopTimer(0);       /* 停止自動重裝的定時器 */
                    break;

                case JOY_DOWN_U:            /* 搖桿UP鍵按下 */
                    ucDataTravel = JOY_DOWN_U;
                    comSendChar(COM3, ucDataTravel);
                    printf("搖桿上鍵按下\r\n");
                    break;

                case JOY_DOWN_D:            /* 搖桿DOWN鍵按下 */
                    ucDataTravel = JOY_DOWN_D;
                    comSendChar(COM3, ucDataTravel);
                    printf("搖桿下鍵按下\r\n");
                    break;

                case JOY_DOWN_L:            /* 搖桿LEFT鍵按下 */
                    ucDataTravel = JOY_DOWN_L;
                    comSendChar(COM3, ucDataTravel);
                    printf("搖桿左鍵按下\r\n");
                    break;

                case JOY_DOWN_R:            /* 搖桿RIGHT鍵按下 */
                    ucDataTravel = JOY_DOWN_R;
                    comSendChar(COM3, ucDataTravel);
                    printf("搖桿右鍵按下\r\n");
                    break;

                case JOY_DOWN_OK:            /* 搖桿OK鍵按下 */
                    ucDataTravel = JOY_DOWN_OK;
                    comSendChar(COM3, ucDataTravel);
                    printf("搖桿OK鍵按下\r\n");
                    break;

                case JOY_UP_OK:                /* 搖桿OK鍵彈起 */
                    ucDataTravel = JOY_UP_OK;
                    comSendChar(COM3, ucDataTravel);
                    printf("搖桿OK鍵彈起\r\n");
                    break;

                default:
                    /* 其它的鍵值不處理 */
                    break;
            }
        }
    }
}

31.10   總結

本章節就為大家講解這么多,485通信依然在實際項目中非常實用。有線的串行網絡仍然提供最有力、最可靠的通信,特別是在惡劣的環境中。

 


免責聲明!

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



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