STM32通訊介面RS485

2021-05-15 03:00:01

18.1關於 RS485

RS485是美國電子工業協會(Electronic Industries Association,EIA)於1983年釋出的序列通訊介面標準,經通訊工業協會(TIA)修訂後命名為TIA/EIA-485-A

RS485具有支援多節點(32個節點)、傳輸距離遠(最大1219m)、接收靈敏度高(200mV電壓)、連線簡單(在構成通訊網路時,僅需要一對雙絞線作傳輸線)、能抑制共模干擾(差分傳輸)、成本低廉等特點,在多站、遠距離通訊等多種工控環境中獲得了廣泛應用。

RS485比RS232晚出現20多年,很多RS232的缺點,在RS485上有了改進。

RS232的電平從-15V至+15V,較高的電平值易損壞介面電路的晶片,而RS485採用差分訊號後,電平範圍為-6V至+6V,相對不易損壞介面電路晶片,同時RS485介面訊號電平與TTL訊號電平相容,便於連線TTL電路。

RS232傳輸速率比較低,傳輸速率為20Kbps,而RS485最高傳輸速率達10Mbps。過高的傳輸速率會降低傳輸距離,在實際應用中,RS485傳輸速率往往設定為9600bps或更低。

RS232採用邏輯電平,共地傳輸容易產生共模干擾,抗噪聲干擾性弱,傳輸距離有限,常用傳輸距離就幾十米左右。而RS485採用平衡傳送和差分接收方式,具有抑制共模干擾的能力,加之匯流排收發器具有高靈敏度,能檢測低至200mV的電壓,因此RS485的傳輸距離達到千米以外。

RS232在匯流排上只允許連線1個收發器,即單站能力,而RS485在匯流排上允許連線多達128個收發器,即具有多站能力,可以利用單一的RS485方便地建立起裝置網路,如圖 18.1.1 所示,為RS485通訊網路結構。

在RS485通訊網路中,通常使用485收發器將TTL電平轉換成RS485的差分訊號。MCU的串列埠控制器TxD傳送資料,經485收發器轉換成差分訊號,傳輸到匯流排上。接收資料時,485收發器將匯流排上的差分訊號轉化成TTL訊號由RxD到串列埠控制器。整個通訊網路中,通常只有一個主機,剩下的全部為從機。在RS485匯流排中,通常還需要在匯流排起止端分別加上約120Ω的終端匹配電阻,以保證RS485匯流排的穩定性。
在這裡插入圖片描述
RS485同樣可以使用DB9介面將訊號引腳引出,實際工程中通常使用接線端子引出,如圖 18.1.2 所示。圖中左邊的為螺釘式接線端子,適合固定連線的場合,圖中右邊為插拔式接線端子,適合需要調整的場合。本開發板使用的插拔式接線端子,如上圖 3.3.1 中編號10部分所示。

在這裡插入圖片描述

18.2 硬體設計

如圖 18.2.1 為開發板RS485部分的原理圖,U16為3.3V低功耗半雙工收發器,滿足RS-485和RS-422標準。 USART的RX和TX,經過U16轉換,變為RS485的A、B。

U16的2腳RE����為接收使能,上劃線表示低電平有效,即當U16的2腳為低電平時,U16接收資料。U16的3 腳DE為輸出使能,高電平有效,即當U16的3腳為高電平是,U16傳送資料。

因此,RS485除了USART,還多了一個收發控制引腳,該引腳使用的PC5。R64為終端匹配電阻,阻值為120Ω。
在這裡插入圖片描述
結合前面圖 17.2.1 電路和表格可知,如果需要將USART2分配給RS485使用,還要將J11(藍色撥碼開關)的1號腳撥到ON位置。

18.3軟體設計

18.3.1軟體設計思路

實驗目的:RS485是差分訊號,收發資料時,A、B都在工作。開發板也只提供了一個RS485介面,因此不能自發自收實驗,需要至少兩個RS485裝置進行實驗。這裡假設兩個開發板進行RS485通訊,一個做主機,一個做從機,主機傳送資料給從機,從機收到資料再發給主機,實現兩個裝置的收發資料,供讀者參考和方便移植。

  1. 初始化USART1、2:設定波特率,收發選擇,有效資料位等;
  2. 將所使用的串列埠引腳初始化:USART使能、GPIO埠時鐘使能、GPIO引腳設定為USART複用;
  3. RS485採用中斷方式傳送,編寫中斷回撥函數;
  4. 主函數編寫控制邏輯:按下按鍵KEY1(KEY_U),主機RS485傳送一次資料,從機RS485接收到資料並列印,然後從機RS485傳送資料,主機RS485接受到資料並列印;
  5. 在軟體方面,RS485的本質跟串列埠沒有差別,不同的地方在於:RS485在傳送、接收之前,需要設定收發控制引腳。

本實驗配套程式碼位於「5_程式原始碼\10_通訊—RS485\」。

18.3.2軟體設計講解

  1. GPIO 引腳選擇與串列埠選擇
    本實驗會用到兩個串列埠,USART1用於偵錯、USART2用於RS485,在程式碼框架上,將每個串列埠都單獨放在「.c」檔案裡,方便修改裁剪。

程式碼段 18.3.1 偵錯串列埠 USART1 相關宏定義(driver_usart1.h)

/*********************
* 引腳宏定義
**********************/
#define DEBUG_USART USART1
#define DEBUG_USART_RX_PIN GPIO_PIN_10
#define DEBUG_USART_TX_PIN GPIO_PIN_9
#define DEBUG_USART_PORT GPIOA
#define DEBUG_USART_GPIO_CLK_EN() __HAL_RCC_GPIOA_CLK_ENABLE()
#define DEBUG_USART_CLK_EN() __HAL_RCC_USART1_CLK_ENABLE()
#define DEBUG_USART_CLK_DIS() __HAL_RCC_USART1_CLK_DISABLE()
#define DEBUG_USART_IRQn USART1_IRQn

程式碼段 18.3.2 偵錯串列埠 USART2 相關宏定義(driver_usart2.h)

/*********************
* 引腳宏定義
**********************/
#define RS485 USART2
#define RS485_RX_PIN GPIO_PIN_3
#define RS485_TX_PIN GPIO_PIN_2
#define RS485_PORT GPIOA
#define RS485_GPIO_CLK_EN() __HAL_RCC_GPIOA_CLK_ENABLE()
#define RE_DE_PIN GPIO_PIN_5
#define RE_DE_PORT GPIOC
#define RE_DE_GPIO_CLK_EN() __HAL_RCC_GPIOC_CLK_ENABLE()
/*********************
* 函數宏定義
**********************/
// 此引腳高電平是傳送有效接收無效;低電平時接收有效傳送無效
#define RE_DE_TX() HAL_GPIO_WritePin(RE_DE_PORT, RE_DE_PIN, GPIO_PIN_SET)
#define RE_DE_RX() HAL_GPIO_WritePin(RE_DE_PORT, RE_DE_PIN, GPIO_PIN_RESET)
#define RS485_IRQn USART2_IRQn
#define RS485_IRQHandler USART2_IRQHandler
#define RS485_CLK_ENABLE() __HAL_RCC_USART2_CLK_ENABLE()
#define RS485_CLK_DISABLE() __HAL_RCC_USART2_CLK_DISABLE()

分別定義了兩個串列埠、對應GPIO、時鐘使能,方便程式碼複用,同時定義了RS485的收發控制引腳。

  1. 初始化USART
    USART初始化包含兩部分:協定部分和硬體部分。
    協定部分放在各自「.c」檔案裡,硬體部分都是呼叫「HAL_UART_Init()」,單獨建立一個「.c」檔案處理。USART1作為偵錯串列埠,初始化和前面的實驗一樣,這裡直接跳過。USART2作為RS485,初始化如程式碼段 18.3.3 所示。

程式碼段 18.3.3 USART2 初始化(driver_usart2.c)

/*
* 函數名:void RS485_Init(uint32_t baudrate)
* 輸入引數:baudrate-串列埠波特率
* * 輸出引數:無
* 返回值:無
* 函數作用:初始化 USART 的波特率,收發選擇,有效資料位等
*/
void RS485_Init(uint32_t baudrate)
{
husart2.Instance = RS485; // 選擇 USART2
husart2.Init.BaudRate = baudrate; // 設定波特率
husart2.Init.WordLength = USART_WORDLENGTH_8B; // 設定資料有效位為 8bit
husart2.Init.StopBits = USART_STOPBITS_1; // 設定一位停止位
husart2.Init.Parity = USART_PARITY_NONE; // 不設校驗位
husart2.Init.Mode = USART_MODE_TX_RX; // 可收可發
husart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
// 使用庫函數初始化 USART2 的引數
if (HAL_UART_Init(&husart2) != HAL_OK)
{
Error_Handler(); } }

RS485的本質還是串列埠,串列埠的初始化和之前的基本一樣。RS485通常也遵循「96-N-8-1」格式,96指波特率9600,N指無校驗,8指8bits資料位,1指1bit停止位。

串列埠協定初始化完後,都呼叫「HAL_UART_Init()」進行設定,在「HAL_UART_Init()」呼叫
「HAL_UART_MspInit()」初始化串列埠硬體部分。

程式碼段 18.3.4 USART MSP 初始化(driver_msp_usart.c)

/*
* 函數名:void AL_USART_MspInit(USART_HandleTypeDef* husart)
* 輸入引數:husart-USART 控制程式碼
* 輸出引數:無
* 返回值:無
* 函數作用:使能 USART1、2 的時鐘,使能引腳時鐘,並設定引腳的複用功能
*/
void HAL_UART_MspInit(UART_HandleTypeDef* husart)
{
// 定義 GPIO 結構體物件
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(husart->Instance==DEBUG_USART) {
// 使能 USART1 的時鐘
DEBUG_USART_CLK_EN();
// 使能 USART1 的輸入輸出引腳的時鐘
DEBUG_USART_GPIO_CLK_EN();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
GPIO_InitStruct.Pin = DEBUG_USART_TX_PIN; // 選擇 USART1 的 TX 引腳
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 設定為複用推輓功能
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 引腳翻轉速率快
HAL_GPIO_Init(DEBUG_USART_PORT, &GPIO_InitStruct); // 初始化 TX 引腳
GPIO_InitStruct.Pin = DEBUG_USART_RX_PIN; // 選擇 RX 引腳
GPIO_InitStruct.Mode = GPIO_MODE_AF_INPUT; // 設定為輸入
HAL_GPIO_Init(DEBUG_USART_PORT, &GPIO_InitStruct); // 初始化 RX 引腳
}
else if(husart->Instance==RS485) {
// 使能 USART2 的時鐘
RS485_CLK_ENABLE();
// 使能 USART2 的輸入輸出和方向引腳的時鐘
RS485_GPIO_CLK_EN();
RE_DE_GPIO_CLK_EN();
/**USART2 GPIO Configuration
PA2 ------> USART2_TX
PA3 ------> USART2_RX
*/
GPIO_InitStruct.Pin = RS485_TX_PIN; // 選擇 USART2 的 TX 引腳
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 設定為複用推輓功能
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 引腳翻轉速率快
HAL_GPIO_Init(RS485_PORT, &GPIO_InitStruct); // 初始化 TX 引腳
GPIO_InitStruct.Pin = RS485_RX_PIN; // 選擇 RX 引腳
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 設定為輸入
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不上拉
HAL_GPIO_Init(RS485_PORT, &GPIO_InitStruct); // 初始化 RX 引腳
GPIO_InitStruct.Pin = RE_DE_PIN; // 選擇方向引腳
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 設定為輸出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不上拉
HAL_GPIO_Init(RE_DE_PORT, &GPIO_InitStruct); // 初始化方向引腳
RE_DE_RX(); // 初始化後預設處於接收狀態
HAL_NVIC_SetPriority(RS485_IRQn, 1, 1); // 設定 USART2 的中斷等級(0-15)(0-15)
// 規則:(0,0)最高,(0,1)次之依次由高到低排序到(15,15)
HAL_NVIC_EnableIRQ(RS485_IRQn); // 使能 USART2 的中斷
} }

先後初始化了USART1和USART2的硬體部分,其中USART2設定了中斷優先順序和使能了中斷,便可以使用「HAL_UART_Receive_IT()」和「HAL_UART_Transmit_IT()」收發資料。接著將RS485的收發函數進行封裝,如程式碼段 18.3.5 所示。

程式碼段 18.3.5 RS485 收發函數(driver_usart2.c)

/*
* 函數名:void RS485_Tx(uint8_t *pdata, uint16_t sz)
* 輸入引數:pdata->指向傳送資料所儲存的首地址
sz->傳送資料個數
* 輸出引數:無
* 返回值:無
* 函數作用:USART2 的傳送函數
*/
void RS485_Tx(uint8_t *pdata, uint16_t sz)
{
usart2_tx_finish = 0;
RE_DE_TX();
HAL_UART_Transmit_IT(&husart2, pdata, sz); }
/*
* 函數名:void RS485_Rx(uint8_t *pdata, uint16_t sz)
* 輸入引數:pdata->指向接收資料所儲存的首地址
sz->接收資料個數
* 輸出引數:無
* 返回值:無
* 函數作用:USART2 的接收函數
*/
void RS485_Rx(uint8_t *pdata, uint16_t sz)
{
usart2_rx_finish = 0;
HAL_UART_Receive_IT(&husart2, pdata, sz); }
  • 11行:usart2_tx_finish為一個全域性變數,用來標記USART2是否傳送完成。這裡將其設定為0,USART2傳送完成後,在中斷函數將其置為1,通過該標記便可得知USART2是否傳送完成;
  • 13行:RS485裝置通常預設為接收狀態,以方便接收資料。這裡傳送資料,需要手動臨時改為傳送狀態;
  • 14行:呼叫串列埠中斷函數傳送資料;
  • 27行:usart2_rx_finish為一個全域性變數,用來標記USART2是否接收完成。這裡將其設定為0,USART2接收資料完成後,在中斷函數將其置為1,通過該標記便可得知USART2是否接收到資料;
  • 29行:RS485裝置通常預設為接收狀態,這裡無需其它設定,直接呼叫串列埠中斷函數接收資料;
  1. 中斷回撥函數
    當USART2發生中斷時,將自動呼叫「USART2_IRQHandler()」,「USART2_IRQHandler()」又呼叫「HAL_UART_IRQHandler()」,最後呼叫「HAL_UART_TxCpltCallback()」或「HAL_UART_RxCpltCallback()」, 在這兩個回撥函數裡修改USART2接收/傳送完成標誌,以便後面查詢是否收發成功。

程式碼段 18.3.6 USART2 中斷回撥函數(driver_msp_usart.c)

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == RS485) {
usart2_tx_finish = 1;
RE_DE_RX(); } }
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == RS485) {
usart2_rx_finish = 1; } }

在傳送完成回撥函數裡,需要將RS485設定為預設的接收模式,以方便隨時接收資料。

  1. 按鍵中斷函數
    因為是通過按鍵來控制RS485主機傳送資料,這裡還需要編寫按鍵中斷函數。參考前面的按鍵中斷實驗,首先初始化按鍵引腳、設定中斷優先順序、使能中斷,便可在發生按鍵事件時,自動呼叫中斷回撥函數「HAL_GPIO_EXTI_Callback()」,在中斷回撥函數裡,修改按鍵標誌,以便隨時查詢是否該按鍵按下,按鍵中斷回撥函數如程式碼段 18.3.7 所示。

程式碼段 18.3.7 按鍵中斷回撥函數(driver_key.c)

/*
* 函數名:void HAL_GPIO_EXTI_Callback(void)
* 輸入引數:無
* 輸出引數:無
* 返回值:無
* 函數作用:外部中斷處理常式的回撥函數,用以處理不同引腳觸發的中斷服務最終函數
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == KEY_UP_GPIO_PIN) {
step = 0; }
  1. 主函數控制邏輯
    RS485主機和RS485從機,通常是兩套獨立程式碼工程。但本實驗中,除了主函數,其它的驅動程式碼都一樣,因此將兩個套程式碼公共部分共用,讀者通過如圖 18.3.1 所示下拉選擇對應工程,即可切換RS485主機工程和RS485從機工程。
    在這裡插入圖片描述
    在RS485主機的主函數如程式碼段 18.3.8 所示。
    程式碼段 18.3.8 RS485 主機主函數(master_main.c)
// 初始化 USART1,設定波特率為 115200 bps
DEBUG_USART_Init(115200);
// 初始化 USART2,設定波特率為 9600 bps
RS485_Init(9600);
// 初始化按鍵
KeyInit();
// 在 windows 下字串\n\r 表示回車
// 如果工程在編譯下面這句中文的時候報錯,請在「Option for target」->"C/C++"->"Misc Controls"新增「 --locale=english」
printf("百問科技 www.100ask.net\n\r");
printf("RS485 收發實驗\n\r");
printf("當前裝置:主機\n\r");
printf("\n\r");
// 初始化 RS485 CAN 的傳送資訊
RS485_Msg.ID = 0x305;
RS485_Msg.length = 8;
for(i=0; i<8; i++) {
RS485_Msg.tx_data[i] = i;
}
RS485_Rx((uint8_t*)&RS485_Msg.rx_data[0], RS485_Msg.length);
while(1) {
if(step == 0) // KEY1 按下
{
RS485_Tx((uint8_t*)&RS485_Msg.tx_data[0], RS485_Msg.length);
printf("主機 ----> 從機 資料:\n\r");
for(i=0; i<8; i++) {
printf("0x%x ", RS485_Msg.tx_data[i]); }
printf("\n\r");
step = 0xFF; }
if(usart2_rx_finish == 0x01) {
usart2_rx_finish = 0;
printf("主機 <---- 從機 資料:\n\r");
for(i=0; i<8; i++) {
printf("0x%x ", RS485_Msg.rx_data[i]); }
printf("\n\r");
RS485_Rx((uint8_t*)&RS485_Msg.rx_data[0], RS485_Msg.length); } }
  • 1~8行:初始化偵錯串列埠、RS485和按鍵;
  • 17~23行:建立要傳送的資料,RS485_Msg為自己建立的資料結構,包含ID、資料長度、資料內容,其中ID可用於裝置識別,整個資料資料結構,可方便資料傳輸;
  • 25行:先接收資料,將裝置設定為預設的接收模式;
  • 29~40行:如果按鍵按下,則按鍵標誌step變為0,此時呼叫「RS485_Tx()」傳送資料;
  • 42~53行:如果接到資料,則接收標誌usart2_rx_finish變為1,此時將接收資料列印處理,並再次呼叫「RS485_Rx()」接收資料;

在RS485從機的主函數如程式碼段 18.3.9 所示。
程式碼段 18.3.9 RS485 從機主函數(slave_main.c)

// 初始化 USART1,設定波特率為 115200 bps
DEBUG_USART_Init(115200);
// 初始化 USART2,設定波特率為 9600 bps
RS485_Init(9600);
// 初始化按鍵
KeyInit();
// 在 windows 下字串\n\r 表示回車
// 如果工程在編譯下面這句中文的時候報錯,請在「Option for target」->"C/C++"->"Misc Controls"新增「 --locale=english」
printf("百問科技 www.100ask.net\n\r");
printf("RS485 收發實驗\n\r");
printf("當前裝置:從機\n\r");
printf("\n\r");
// 初始化 RS485 CAN 的傳送資訊
RS485_Msg.ID = 0x305;
RS485_Msg.length = 8;
for(i=0; i<8; i++) {
RS485_Msg.tx_data[i] = i ^ 0xAB; }
RS485_Rx((uint8_t*)&RS485_Msg.rx_data[0], RS485_Msg.length);
while(1) {
if(usart2_rx_finish == 0x01) {
usart2_rx_finish = 0;
printf("從機 <---- 主機 資料:\n\r");
for(i=0; i<8; i++) {
printf("0x%x ", RS485_Msg.rx_data[i]); }
printf("\n\r");
RS485_Tx((uint8_t*)&RS485_Msg.tx_data[0], RS485_Msg.length);
printf("從機 ----> 主機 資料:\n\r");
for(i=0; i<8; i++) {
printf("0x%x ", RS485_Msg.tx_data[i]); }
printf("\n\r");
RS485_Rx((uint8_t*)&RS485_Msg.rx_data[0], RS485_Msg.length); } }
  • 1~8行:初始化偵錯串列埠、RS485和按鍵;
  • 17~23行:建立要傳送的資料;
  • 25行:先接收資料,將裝置設定為預設的接收模式;
  • 29~49行:如果接收到資料,先列印接收的資料,然後呼叫「RS485_Tx()」傳送資料,最後再呼叫「RS485_Rx()」接收資料;

18.4實驗效果

本實驗對應配套資料的「5_程式原始碼\ 10_通訊—RS485\」。準備兩個開發板,連線好下載器。在Keil中,分別切換到RS485主機工程和RS485從機工程,編譯,分別給兩個開發板下載RS485主機程式和RS485從機程式。

將兩個開發板的J11(藍色撥碼開關)的1腳撥為ON,使用配套的插拔式接線端子將兩個板子的RS485介面連線,注意RS485不需要交叉,即兩個開發板RS485的A對A,B對B,最後連線好兩個開發板串列埠和電源,如圖 18.4.1 所示。
在這裡插入圖片描述

啟動電源後,串列埠會有列印當前裝置是主機還是從機。按下主機的KEY1_U,主機將資料傳送給從機,從機接收到資料後,傳送新資料給主機,如圖 18.4.2 所示。

在這裡插入圖片描述

【總結】
偵錯串列埠、RS232、RS485本質都是一樣的,不同的部分由轉換晶片實現,使用者幾乎不用關心轉換實現。因此,使用者只需要控制串列埠收發資料即可。

串列埠的程式設計,可分為三步:

  1. 初始化串列埠,包含串列埠協定的設定和硬體的初始化(引腳、中斷等);
  2. 使用對應模式(超時管理模式、中斷模式、DMA模式)的串列埠收發函數,進行收發資料;
  3. 主函數控制收發邏輯;

百問網技術論壇:
http://bbs.100ask.net/

百問網嵌入式視訊官網:
https://www.100ask.net/index

百問網開發板:
淘寶:https://100ask.taobao.com/
天貓:https://weidongshan.tmall.com/

技術交流群(鴻蒙開發/Linux/嵌入式/驅動/資料下載)
QQ群:752871361

微控制器-嵌入式Linux交流群:
QQ群:536785813