大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家分享的是嵌入式裡串列埠(UART)自動波特率識別程式設計與實現。
串列埠(UART)是嵌入式裡最基礎最常用也最簡單的一種通訊(資料傳輸)方式,可以說是工程師入門通訊領域的啟蒙老師,同時串列埠列印也是嵌入式專案裡非常經典的除錯與互動方式。
最精簡的串列埠僅使用兩根單向訊號線:TXD、RXD,這兩根訊號線是獨立工作的,因此資料收發既可分開也可同時進行,這就是所謂的全雙工。串列埠沒有主從機概念,並且沒有專門的時鐘訊號 SCK,所以串列埠通訊也屬於非同步傳輸。
說到非同步傳輸,這就不得不提波特率(每秒鐘傳輸bit數)的問題了,通訊雙方必須使用一致的波特率才能完成正確的資料傳輸。正常情況下,我們都是為兩個串列埠裝置事先約定好波特率,比如 MCU 與上位機通訊,在 MCU 程式裡按 115200 的波特率去初始化 UART 外設,然後上位機串列埠除錯助手也設定 115200 波特率,雙方再聯合工作。
有時候,我們也希望能有一種靈活的波特率約定方式,比如建立通訊前,在上位機串列埠除錯助手裡隨意設定一種波特率,然後按這個波特率傳送資料,MCU 端能自動識別出這個波特率,並用識別出來的波特率去初始化 UART 外設,然後再進行後續資料傳輸,這種方式就叫自動波特率識別。痞子衡今天要分享的就是在 MCU 裡實現自動波特率識別的程式設計:
一、串列埠(UART)自動波特率識別程式設計
1.1 函式介面定義
首先是設計自動波特率識別程式標頭檔案:autobaud.h ,這個標頭檔案裡直接定義如下 3 個介面函式原型。涵蓋必備的初始化流程 init()、deinit(),以及最核心的波特率識別功能 get_rate()。
//! @brief 初始化波特率識別
void autobaud_init(void);
//! @brief 檢測波特率識別是否已完成,並獲取波特率值
bool autobaud_get_rate(uint32_t *rate);
//! @brief 關閉波特率識別
void autobaud_deinit(void);
1.2 識別設計思想
關於識別,因為上位機資料是從 RXD 引腳過來的,所以在 MCU 裡需要先將 RXD 引腳配置成普通數字輸入 GPIO(這個引腳需要上拉,預設保持高電平),然後檢測這個 GPIO 的電平跳變(一般用下降沿)並計時。
下圖是典型的 UART 單位元組傳輸時序,I/O 空閒狀態是高電平,傳輸時總是由 1bit 低電平起始位開啟,然後是從 LSB 到 MSB 的 8bit 資料位,校驗位是可選項(我們暫不開啟),最後由 1bit 高電平停止位結束,I/O 迴歸高電平空閒狀態。
- Note 1:檢測下降沿跳變,是因為 I/O 空閒為高,起始位的存在保證了每 Byte 傳輸週期總是從下降沿開始。
- Note 2:起始位和停止位兩個 bit 的存在還兼有波特率容錯的功能,通訊雙方波特率在 3% 的誤差內資料傳輸均可以正常進行。
雖然我們不需要約定上位機波特率,但是要想實現波特率自動識別,上位機初始傳輸的資料卻必須要事先約定好(可理解為接頭暗號),這涉及到 MCU 裡檢測電平跳變次數與相應計時計算。這個接頭暗號是雙向的,MCU 端根據接頭暗號識別出波特率後,再將同樣的接頭暗號通過 UART_TXD 傳送給上位機以確認(這部分邏輯不在自動波特率識別程式設計範疇裡,應放在專案整體設計裡)。
痞子衡設計的接頭暗號是 0x5A, 0xA6 兩個位元組,兩位元組暗號相比單位元組暗號容錯性更好一些(以防 I/O 上有干擾,導致誤識別),根據指定的暗號和 UART 傳輸時序圖,我們很容易得到如下常量定義:
enum _autobaud_counts
{
//! 0x5A 位元組對應的下降沿個數
kFirstByteRequiredFallingEdges = 4,
//! 0xA6 位元組對應的下降沿個數
kSecondByteRequiredFallingEdges = 3,
//! 0x5A 位元組(從起始位到停止位)第一個下降沿到最後一個下降沿之間的實際bit數
kNumberOfBitsForFirstByteMeasured = 8,
//! 0xA6 位元組(從起始位到停止位)第一個下降沿到最後一個下降沿之間的實際bit數
kNumberOfBitsForSecondByteMeasured = 7,
//! 兩個下降沿之間允許的最大超時(us)
kMaximumTimeBetweenFallingEdges = 80000,
//! 對實際檢測出的波特率值做對齊處理,以便於更好地配置UART模組
kAutobaudStepSize = 1200
};
上述常量定義裡,kMaximumTimeBetweenFallingEdges 指定了兩個下降沿之間允許的最大時間間隔,超過這個時間,自動波特率程式將丟掉前面統計的下降沿個數,重頭開始識別,這個設計也是為了防止 I/O 上有電平干擾,導致誤識別。
kAutobaudStepSize 常量是為了對檢測出的波特率值做對齊處理,公式是 rounded = stepSize * (value/stepSize + 0.5),其中 value 是實際檢測出的波特率值,rounded 是對齊後的波特率值,用對齊後的波特率值能更好地配置UART外設(這跟UART模組裡波特率發生器SBR設計有關)。
最後就是 I/O 電平下降沿檢測方法設計,這裡既可以用軟體查詢(就是迴圈讀取 I/O 輸入電平,比較當前值與上一次值的差異),也可以使用GPIO模組自帶的邊沿中斷功能。推薦使用後者,一方面計時更精確,另外也不用阻塞系統。檢測到下降沿發生就呼叫一次如下 pin_transition_callback() 函式,在這個函式裡統計跳變次數以及計時。
//! @brief 管腳下降沿跳變回撥函式
static void pin_transition_callback(void);
1.3 主程式碼實現
根據上一小節描述的設計思想,我們很容易寫出下面的主程式碼(autobaud_irq.c),程式碼裡痞子衡都做了詳細註釋。有一點要提的是關於其中系統計時,可參考痞子衡舊文 《嵌入式裡通用微秒(microseconds)計時函式框架設計與實現》 。
//! @brief 使能GPIO管腳中斷
extern void enable_autobaud_pin_irq(pin_irq_callback_t func);
//! @brief 關閉GPIO管腳中斷
extern void disable_autobaud_pin_irq(void);
//!< 已檢測到的下降沿個數
static uint32_t s_transitionCount;
//!< 0x5A 位元組檢測期間內對應計數值
static uint64_t s_firstByteTotalTicks;
//!< 0xA6 位元組檢測期間內對應計數值
static uint64_t s_secondByteTotalTicks;
//!< 上一次下降沿發生時系統計數值
static uint64_t s_lastToggleTicks;
//!< 下降沿之間最大超時對應計數值
static uint64_t s_ticksBetweenFailure;
void autobaud_init(void)
{
s_transitionCount = 0;
s_firstByteTotalTicks = 0;
s_secondByteTotalTicks = 0;
s_lastToggleTicks = 0;
// 計算出下降沿之間最大超時對應計數值
s_ticksBetweenFailure = microseconds_convert_to_ticks(kMaximumTimeBetweenFallingEdges);
// 使能GPIO管腳中斷,並註冊中斷處理回撥函式
enable_autobaud_pin_irq(pin_transition_callback);
}
void autobaud_deinit(void)
{
// 關閉GPIO管腳中斷
disable_autobaud_pin_irq();
}
bool autobaud_get_rate(uint32_t *rate)
{
if (s_transitionCount == (kFirstByteRequiredFallingEdges + kSecondByteRequiredFallingEdges))
{
// 計算出實際檢測到的波特率值
uint32_t calculatedBaud =
(microseconds_get_clock() * (kNumberOfBitsForFirstByteMeasured + kNumberOfBitsForSecondByteMeasured)) /
(uint32_t)(s_firstByteTotalTicks + s_secondByteTotalTicks);
// 對實際檢測出的波特率值做對齊處理
// 公式:rounded = stepSize * (value/stepSize + .5)
*rate = ((((calculatedBaud * 10) / kAutobaudStepSize) + 5) / 10) * kAutobaudStepSize;
return true;
}
else
{
return false;
}
}
void pin_transition_callback(void)
{
// 獲取當前系統計數值
uint64_t ticks = microseconds_get_ticks();
// 計數這次檢測到的下降沿
s_transitionCount++;
// 如果本次下降沿與上次下降沿之間間隔過長,則從頭開始檢測
uint64_t delta = ticks - s_lastToggleTicks;
if (delta > s_ticksBetweenFailure)
{
s_transitionCount = 1;
}
switch (s_transitionCount)
{
case 1:
// 0x5A 位元組檢測時間起點
s_firstByteTotalTicks = ticks;
break;
case kFirstByteRequiredFallingEdges:
// 得到 0x5A 位元組檢測期間內對應計數值
s_firstByteTotalTicks = ticks - s_firstByteTotalTicks;
break;
case (kFirstByteRequiredFallingEdges + 1):
// 0xA6 位元組檢測時間起點
s_secondByteTotalTicks = ticks;
break;
case (kFirstByteRequiredFallingEdges + kSecondByteRequiredFallingEdges):
// 得到 0xA6 位元組檢測期間內對應計數值
s_secondByteTotalTicks = ticks - s_secondByteTotalTicks;
// 關閉GPIO管腳中斷
disable_autobaud_pin_irq();
break;
}
// 記錄本次下降沿發生時系統計數值
s_lastToggleTicks = ticks;
}
二、串列埠(UART)自動波特率識別程式實現
前面講的都是硬體無關設計,但最終還是要落實到具體 MCU 平臺上的,其中 GPIO 中斷部分是跟 MCU 緊相關的。我們以恩智浦 i.MXRT1011 為例來介紹硬體實現。
2.1 管腳中斷方式實現(基於i.MXRT1011)
恩智浦 MIMXRT1010-EVK 有板載偵錯程式 DAPLink,這個 DAPLink 中也整合了 USB 轉串列埠的功能,對應的 UART 引腳是 IOMUXC_GPIO_09_LPUART1_RXD 和 IOMUXC_GPIO_10_LPUART1_TXD,我們就選用這個管腳 GPIO1[9] 做自動波特率檢測,實現程式碼如下:
typedef void (*pin_irq_callback_t)(void);
static pin_irq_callback_t s_pin_irq_func;
//! @brief UART引腳功能切換函式
void uart_pinmux_config(bool setGpio)
{
if (setGpio)
{
IOMUXC_SetUartAutoBaudPinMode(IOMUXC_GPIO_09_GPIOMUX_IO09, GPIO1, 9);
}
else
{
IOMUXC_SetUartPinMode(IOMUXC_GPIO_09_LPUART1_RXD);
IOMUXC_SetUartPinMode(IOMUXC_GPIO_10_LPUART1_TXD);
}
}
//! @brief 使能GPIO管腳中斷
void enable_autobaud_pin_irq(pin_irq_callback_t func)
{
s_pin_irq_func = func;
// 開啟GPIO1_9下降沿中斷
GPIO_SetPinInterruptConfig(GPIO1, 9, kGPIO_IntFallingEdge);
GPIO1->IMR |= (1U << 9);
NVIC_SetPriority(GPIO1_Combined_0_15_IRQn, 1);
NVIC_EnableIRQ(GPIO1_Combined_0_15_IRQn);
}
//! @brief GPIO中斷處理函式
void GPIO1_Combined_0_15_IRQHandler(void)
{
uint32_t interrupt_flag = (1U << 9);
// 僅當GPIO1_9中斷髮生時
if ((GPIO_GetPinsInterruptFlags(GPIO1) & interrupt_flag) && s_pin_irq_func)
{
//執行一次回撥函式
s_pin_irq_func();
GPIO_ClearPinsInterruptFlags(GPIO1, interrupt_flag);
}
}
2.2 在MIMXRT1010-EVK上實測
一切就緒,我們現在來實測一下,主函式流程很簡單,測試結果也表明達到了預期效果,每次將 MCU 程式復位執行後,串列埠除錯助手裡可任意設定波特率。
int main(void)
{
// 略去系統時鐘配置...
// 初始化定時器
microseconds_init();
// 將GPIO1_9先配成輸入GPIO
bool setGpio = true;
uart_pinmux_config(setGpio);
// 初始化波特率識別
autobaud_init();
// 檢測波特率識別是否已完成,並獲取波特率值
uint32_t baudrate;
while (!autobaud_get_rate(&baudrate));
// 關閉波特率識別
autobaud_deinit();
// 配置UART1引腳
setGpio = false;
uart_pinmux_config(setGpio);
// 初始化UART1外設
uint32_t uartClkSrcFreq = BOARD_DebugConsoleSrcFreq();
DbgConsole_Init(1, baudrate, kSerialPort_Uart, uartClkSrcFreq);
PRINTF("Autobaud test success\r\n");
PRINTF("Detected baudrate is %d\r\n", baudrate);
while (1);
}
至此,嵌入式裡串列埠(UART)自動波特率識別程式設計與實現痞子衡便介紹完畢了,掌聲在哪裡~~~
歡迎訂閱
文章會同時釋出到我的 部落格園主頁、CSDN主頁、知乎主頁、微信公眾號 平臺上。
微信搜尋"痞子衡嵌入式"或者掃描下面二維碼,就可以在手機上第一時間看了哦。