在 STM32 上輕鬆使用 scanf 函數

雖然與嵌入式系統互動的方法多種多樣,但其中最簡單、最通用的一種方法是使用來自序列控制台的使用者輸入。相較於手動解析傳入的 ASCII 字元並將它們轉換為適當的資料格式的做法,我們可以使用scanf() 函數自動完成此操作。然而,與 printf() 函數一樣,我們必須提供一些額外的程式碼才能利用 stdio 函式庫函數。作為「在 STM32 上輕鬆使用 printf 函數」貼文中的附屬說明,本貼文將深入介紹將 UART 周邊對應到 scanf() 函數的操作。

步驟

以下步驟可直接應用於任何 STM32CubeIDE 專案(在撰寫本文時,其版本為 1.9.0)。如果你偏好其他開發環境,可能需要稍作修改以適應存在的差異。

0.建立 UART 實例

作為預備步驟,請確保已將 UART(或 USART)周邊配置為與虛擬 COM 連接埠相互收發資料。在 ST開發板上,這通常意味著選擇連接到 ST-LINK 編程器/除錯器的 UART RX 和 TX 線路。如需了解完整詳情資料,請參閱「在 STM32 上輕鬆使用 printf 函數」貼文中的相應步驟。

1.重定向 scanf() 至 UART 實例

a. 在 main.c 檔案的「私有包含」(Private Includes)部分中,為 stdio 函式庫新增包含指令,如圖 1 所示。

image

圖 1 : 納入 stdio 函式庫

b. 若要僅重定向 scanf() 函數,請將清單 1 中提供的程式碼複製並貼上到 main.c 的「私有函數原型」部分(Private Function Prototypes Section)。 若要重定向 scanf()prinf() 函數(大多數應用程式都需要這樣做),請複製清單 2 中提供的程式碼。

列表 1 : 重定向 scanf() 的簡單程式碼

#ifdef __GNUC__
#define GETCHAR_PROTOTYPE int __io_getchar(void)
#else
#define GETCHAR_PROTOTYPE int fgetc(FILE *f)
#endif

GETCHAR_PROTOTYPE
{
  uint8_t ch = 0;

  /* Clear the Overrun flag just before receiving the first character */
  __HAL_UART_CLEAR_OREFLAG(&huart?);

  /* Wait for reception of a character on the USART RX line and echo this
   * character on console */
  HAL_UART_Receive(&huart?, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  HAL_UART_Transmit(&huart?, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  return ch;
}

列表 2 : 重定向 printf()scanf() 的簡單程式碼

#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#define GETCHAR_PROTOTYPE int __io_getchar(void)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#define GETCHAR_PROTOTYPE int fgetc(FILE *f)
#endif

PUTCHAR_PROTOTYPE
{
  HAL_UART_Transmit(&huart?, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  return ch;
}

GETCHAR_PROTOTYPE
{
  uint8_t ch = 0;

  /* Clear the Overrun flag just before receiving the first character */
  __HAL_UART_CLEAR_OREFLAG(&huart?);

  /* Wait for reception of a character on the USART RX line and echo this
   * character on console */
  HAL_UART_Receive(&huart?, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  HAL_UART_Transmit(&huart?, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  return ch;
}

請務必將上述清單中使用的虛擬 UART 控制代碼更改為你所需的 UART 周邊。 例如,圖 2 顯示了將huart2 用作目標 UART 實例。

圖 2:main.c 檔案新增重定向程式碼

c. 然而,STM32CubeIDE 自動產生的預設 syscalls.c 檔案在啟用輸入流的內部快取時會導致意外行為。解決此問題的最簡單方法是在呼叫 scanf() 之前停用快取。 將列表 3 中提供的程式碼行複製並貼上到 main() 函數的初始化部分中。

列表 3 : 停用輸入流的內部緩衝

setvbuf(stdin, NULL, _IONBF, 0);

現在,scanf() 函數應該可以正常運作了,除了浮點格式說明符。 若要啟用這些格式說明符,請繼續下一步。

2.啟用浮點支援(可選)

printf() 函數類似,如果應用程式需要從序列輸入讀取浮點數,則必須為 scanf() 明確啟用浮點支援。 否則,呼叫使用浮點格式說明符的任何 scanf() 都會出現意外行為。

a. 在「專案資源管理器(Project Explorer)」中以滑鼠右鍵按一下專案名稱,然後選擇「Properties 」 。 在「C/C++ Build」類別下選擇「Settings」,並在工具設定標籤下選擇 「MCU Settings 」。勾選「Use float with scanf from newlib-nano」旁的複選框,如下圖所示。 點擊「Apply and Close」。

圖 3 : 啟用浮點格式化支援

結論

使用 scanf() 參考頁面中的範例程式碼(參考列表 4)後,我們可以看到在圖 4 中,使用者的輸入被正確地讀取和格式化。

列表 4 : 測試範例程式碼

char str[80];
int i;

printf("Enter your family name: ");
scanf("%79s", str);
printf("Enter your age: ");
scanf("%d", &i);
printf("Mr. %s, %d years old.\n", str, i);
printf("Enter a hexadecimal number: ");
scanf("%x", &i);
printf("You have entered %#x (%d).\n", i, i);

example code output

圖 4 : 範例程式碼輸出