【7】串口編程三種模式(查詢/中斷/DMA)韋東山老師學習筆記(課程聽不懂的話試著來看看我的學習筆記吧)

<1>前置概念補充

在深入拆解三種模式前,先通過提供的?“函數對比表”?建立整體認知:這張表是串口收發的「武器庫索引」,清晰標注了查詢、中斷、DMA 三種模式下,收發 / 回調函數的對應關系。后續會結合實際代碼,講透每個函數怎么用、何時觸發,先記住這張表的核心關聯👇

功能查詢模式中斷模式DMA 模式
發送HAL_UART_TransmitHAL_UART_Transmit_IT
HAL_UART_TxCpltCallback
HAL_UART_Transmit_DMA
HAL_UART_TxHalfCpltCallback
HAL_UART_TxCpltCallback
接收HAL_UART_ReceiveHAL_UART_Receive_IT
HAL_UART_RxCpltCallback
HAL_UART_Receive_DMA
HAL_UART_RxHalfCpltCallback
HAL_UART_RxCpltCallback
錯誤處理-HAL_UART_ErrorCallbackHAL_UART_ErrorCallback

簡單說:

  • 查詢模式:用「阻塞函數」收發,CPU 全程等待
  • 中斷模式:用「啟動函數 + 完成回調」,收發完自動通知 CPU
  • DMA 模式:用「DMA 啟動函數 + 半完成 / 完成回調」,數據自動傳輸,CPU 完全解放

查詢模式的簡單,是靠「犧牲 CPU 效率」實現的。對比另外兩種模式的函數邏輯,差異一目了然:

模式收發邏輯CPU 參與度典型函數
查詢函數阻塞,CPU 全程等待收發完成100% 占用HAL_UART_Transmit/Receive
中斷啟動后立即返回,收發完成回調通知僅中斷觸發時參與HAL_UART_Transmit_IT?+ 回調
DMA數據自動傳輸,CPU 無需參與0 參與(純硬件搬運)HAL_UART_Transmit_DMA?+ 回調

一、STM32 串口通信_查詢方式

(1)開篇引言

這篇教程專為?0 基礎嵌入式初學者?打造,用最通俗的語言拆解 STM32 串口通信核心函數?HAL_UART_Transmit?的用法,從硬件接線、工具使用到代碼邏輯,一步步帶大家實現 “STM32 發數據,電腦串口助手收數據”。后續還會擴展中斷、DMA 等高級用法。

(2)硬件連接:串口接線邏輯(核心!接錯沒數據)

1. 接線原理

串口通信遵循?“發送端連接收端,接收端連發送端”?的規則,簡單說:

  • STM32 開發板的?TX 引腳(如 DshanMCU-F103 底板的?PA9),要連接 USB 串口模塊的?RX 引腳
  • STM32 開發板的?RX 引腳(如 DshanMCU-F103 底板的?PA10),要連接 USB 串口模塊的?TX 引腳
  • 兩者的?GND 引腳?必須相連(保證電平參考一致)

同時,ST-Link 要保持連接,負責給開發板供電、燒錄程序和調試

2. 實物接線參考(搭配你的硬件圖)

下圖清晰展示了 DshanMCU-F103 底板與 USB 串口模塊的接線方式:

(3)驅動安裝:讓電腦識別串口

如果你用的是?CH340 串口模塊,需安裝 CH340 驅動(對應你提供的?8_CH340_CH341驅動程序?文件夾),步驟如下:

  1. 解壓?8_CH340_CH341驅動程序,找到并運行?CH341SER.EXE,按提示完成安裝。
  2. 插入 USB 串口模塊,打開?設備管理器,查看 “端口(COM 和 LPT)” 列表。若出現類似?USB-SERIAL CH340 (COMxx)(如 COM38),說明驅動安裝成功。

(4)串口助手:收發數據的 “窗口”

我們用?sscom5.13.1收發數據,操作步驟:

  1. 解壓運行?sscom5.13.1.exe,在 “通訊端口” 選擇識別到的串口(如 COM38 )。
  2. 設置?波特率為 115200(必須與代碼配置一致!),數據位 8、停止位 1、無校驗。
  3. 點擊 “打開串口”,即可開始收發數據。

(5)代碼解析:HAL_UART_Transmit 怎么用?

以下是核心代碼邏輯:

#include "main.h"
#include "usart.h"  // 串口相關頭文件
#include "gpio.h"   // GPIO 相關頭文件/* 全局變量定義(根據需求使用,此處保留核心邏輯) */
extern UART_HandleTypeDef huart1;  
char c;  // 存儲串口接收的字符/*** @brief  主函數:程序入口* @retval int 返回值(一般無實際意義)*/
int main(void)
{// 1. 初始化 HAL 庫、系統時鐘、串口、GPIO 等(CubeMX 自動生成,無需深究)HAL_Init();           SystemClock_Config(); MX_USART1_UART_Init();MX_GPIO_Init();       // 2. 主循環:不斷收發數據while (1){// ① 發送提示信息:“Please enter a char: \r\n”HAL_UART_Transmit(&huart1, "Please enter a char: \r\n", 20, 1000);  // ② 接收數據:循環嘗試接收 1 個字節,直到成功(超時時間 100ms )while(HAL_OK != HAL_UART_Receive(&huart1, &c, 1, 100));  // ③ 處理數據:收到的字符 +1(如輸入 'a' 變成 'b' )c = c + 1;  // ④ 發送處理后的數據:把 +1 后的字符發回電腦HAL_UART_Transmit(&huart1, &c, 1, 1000);  // ⑤ 發送換行符:讓串口助手顯示更整潔HAL_UART_Transmit(&huart1, "\r\n", 2, 1000);  }
}

關鍵函數:HAL_UART_Transmit?解析

HAL_UART_Transmit(&huart1, &c, 1, 1000);
  • &huart1:串口句柄,指定用 USART1 發數據(由 CubeMX 配置生成,直接用即可 )。
  • &c:要發送的數據地址。發送單個字符用?&c,發送字符串則用字符串數組名(如?str?)。
  • 1:發送數據的長度。發送 1 個字符填?1,發送字符串填?strlen(str)(自動計算長度 )。
  • 1000:超時時間(毫秒)。如果 1 秒內沒發出去,函數返回錯誤。

(6)效果驗證:收發數據測試

1. 正常情況:輸入?1?輸出?2

  • 操作:串口助手收到提示?Please enter a char:?后,輸入?1?并發送。
  • 預期:開發板回發?2(因為代碼里?c = c + 1?)。

2. 異常情況:輸入?123?無正確響應

  • 問題:代碼里是?單字節接收HAL_UART_Receive(&huart1, &c, 1, 100)?),一次只能收 1 個字符。若輸入?123,實際會分 3 次接收(123?),但代碼邏輯未處理多字節連續輸入,導致 “輸入?123?看似沒反應”。
  • 解決思路(后續優化方向 ):
    • 用?數組 + 長度判斷?接收多字節數據。
    • 結合?中斷 / DMA?實現 “數據自動緩存,無需 CPU 一直等待”。

(7)常見問題排查

1. 串口助手收不到數據?

  • 檢查?接線:TX/RX 是否交叉連接,GND 是否共地。
  • 檢查?波特率:代碼和串口助手的波特率是否均為?115200
  • 檢查?驅動:設備管理器是否識別到串口,驅動是否安裝成功。

2. 發送字符串怎么改?

若要發送字符串(如?Hello?),可修改代碼:

char str[] = "Hello";  // 定義字符串數組
// 發送字符串:長度用 strlen(str) 自動計算
HAL_UART_Transmit(&huart1, str, strlen(str), 1000);  

(8)后續優化預告(進階方向)

當前代碼用的是?查詢方式(發數據要等待、收數據要循環詢問 ),缺點是 “CPU 一直忙等,無法做其他事”。后續會擴展:

  1. 中斷方式:數據到來自動觸發,CPU 可并行處理其他任務(適合實時性高的場景 )。
  2. DMA 方式:數據直接在內存和外設間傳輸,完全無需 CPU 參與(適合大數據量場景 )。

?二、不實用的官方中斷模式

STM32 串口中斷深度解析:從硬件原理到代碼實戰(以 HAL 庫為例)

<2>中斷模式完整函數鏈

中斷模式的收發,是「啟動函數 → 中斷觸發 → 回調函數」的完整鏈條,用表格串聯更清晰:

階段發送流程(中斷)接收流程(中斷)
啟動HAL_UART_Transmit_IT?啟動發送HAL_UART_Receive_IT?啟動接收
中斷觸發發完 1 字節 → TXE 中斷;發完所有字節 → TC 中斷收到 1 字節 → RXNE 中斷;收完所有字節 → 接收完成中斷
回調通知發完所有字節 →?HAL_UART_TxCpltCallback收完所有字節 →?HAL_UART_RxCpltCallback
錯誤處理統一走?HAL_UART_ErrorCallback統一走?HAL_UART_ErrorCallback

(1)開篇:為什么需要串口中斷?

在之前的查詢方式串口通信中,CPU 需要不斷 “詢問” 串口是否有數據,就像一個人不停地問 “你有數據嗎?有數據嗎?”,這會讓 CPU 無法去做其他更有意義的事情。而串口中斷就像給串口裝了一個 “門鈴”,當有數據到來或者數據發送完成時,串口主動 “按門鈴” 通知 CPU,這樣 CPU 就可以在等待串口的空閑時間去處理其他任務,大大提高了系統效率。

(2)串口中斷的硬件基礎

(一)STM32 串口中斷相關寄存器

  1. 狀態寄存器(USART_SR)
    • TXE(Transmit Data Register Empty)位:當發送數據寄存器為空時,該位被置 1。這意味著可以往發送數據寄存器中寫入新的數據了。在中斷模式下,我們可以使能 TXE 中斷,當 TXE 位置 1 時觸發中斷,去發送下一個數據。
    • TC(Transmission Complete)位:當一幀數據發送完成時,該位被置 1。可以利用 TC 中斷來判斷一次發送是否完全結束。
    • RXNE(Read Data Register Not Empty)位:當接收數據寄存器中有數據時,該位被置 1,可使能 RXNE 中斷來觸發接收操作。
  2. 控制寄存器(USART_CR1)
    • TXEIE 位:用于使能 TXE 中斷。當該位被置 1,且 TXE 位置 1 時,會觸發串口發送中斷。
    • TCIE 位:用于使能 TC 中斷。當該位被置 1,且 TC 位置 1 時,會觸發串口發送完成中斷。
    • RXNEIE 位:用于使能 RXNE 中斷。當該位被置 1,且 RXNE 位置 1 時,會觸發串口接收中斷。

(二)串口中斷的硬件觸發流程

USART1 等外設可以通過 DMA 請求與系統進行數據交互,同時也可以通過中斷的方式。當使能了串口的某個中斷(如 TXE 中斷)后:

  1. 當發送數據寄存器為空(TXE=1)且 TXEIE=1 時,硬件會觸發中斷請求,這個請求會通過總線矩陣等到達 CPU 的中斷控制器。
  2. CPU 響應中斷后,會跳轉到對應的中斷處理函數(如 USART1_IRQHandler)去執行相應的操作。
  3. 對于接收來說,當接收數據寄存器非空(RXNE=1)且 RXNEIE=1 時,同樣會觸發中斷請求,進入接收中斷處理流程。

(3)HAL 庫串口中斷函數解析

(一)中斷處理函數入口:USART1_IRQHandler

void USART1_IRQHandler(void)
{HAL_UART_IRQHandler(&huart1);
}

這是串口 1 的中斷處理函數入口,當串口 1 觸發中斷時,CPU 會首先跳轉到這里。然后調用HAL_UART_IRQHandler(&huart1)函數,這個函數是 HAL 庫中處理串口中斷的核心函數,它會去檢查中斷源(是 TXE 中斷、TC 中斷還是 RXNE 中斷等),并調用相應的處理邏輯。

(二)HAL_UART_IRQHandler 函數關鍵邏輯

/* UART in mode Transmitter */
if ((((isrflags & USART_SR_TXE) != RESET) && ((cr1its & USART_CR1_TXEIE) != RESET)))
{UART_Transmit_IT(huart);return;
}
/* UART in mode Transmitter end */
if ((((isrflags & USART_SR_TC) != RESET) && ((cr1its & USART_CR1_TCIE) != RESET)))
{UART_EndTransmit_IT(huart);return;
}
  1. TXE 中斷處理:當檢測到狀態寄存器中的 TXE 位為 1(發送數據寄存器為空),并且控制寄存器中的 TXEIE 位為 1(使能了 TXE 中斷)時,會調用UART_Transmit_IT(huart)函數,這個函數會去處理發送過程中的數據填充等操作,繼續發送下一個數據。
  2. TC 中斷處理:當檢測到 TC 位為 1(發送完成),并且 TCIE 位為 1(使能了 TC 中斷)時,會調用UART_EndTransmit_IT(huart)函數,用于處理發送完成后的一些收尾工作,比如標記發送完成狀態等。

(三)HAL_UART_Transmit_IT 函數

HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)
{/* Check that a Tx process is not already ongoing */if (huart->gState == HAL_UART_STATE_READY){if ((pData == NULL) || (Size == 0U)){return HAL_ERROR;}huart->pTxBuffPtr = pData;huart->TxXferSize = Size;huart->TxXferCount = Size;huart->ErrorCode = HAL_UART_ERROR_NONE;huart->gState = HAL_UART_STATE_BUSY_TX;/* Enable the UART Transmit data register empty Interrupt */__HAL_UART_ENABLE_IT(huart, UART_IT_TXE);return HAL_OK;}else{return HAL_BUSY;}
}
  1. 函數作用:這個函數用于以中斷模式啟動串口發送。首先檢查串口當前狀態是否為HAL_UART_STATE_READY,如果是,就對串口句柄中的發送緩沖區指針、發送數據大小、發送計數器等進行初始化,然后將串口的狀態設置為HAL_UART_STATE_BUSY_TX表示正在發送,最后使能 TXE 中斷(通過__HAL_UART_ENABLE_IT(huart, UART_IT_TXE)),這樣當發送數據寄存器為空時就會觸發中斷,進入發送中斷處理流程。
  2. 參數解析huart是串口句柄,pData是要發送的數據緩沖區指針,Size是要發送的數據長度。

(四)UART_Transmit_IT 函數

static HAL_StatusTypeDef UART_Transmit_IT(UART_HandleTypeDef *huart)
{const uint16_t *tmp;/* Check that a Tx process is ongoing */if (huart->gState == HAL_UART_STATE_BUSY_TX){if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE)){tmp = (const uint16_t *)huart->pTxBuffPtr;huart->Instance->DR = (uint16_t)(*tmp & (uint16_t)0x01FF);huart->pTxBuffPtr += 2U;}else{huart->Instance->DR = (uint8_t)(*huart->pTxBuffPtr++ & (uint8_t)0x00FF);}if (--huart->TxXferCount == 0U){/* Disable the UART Transmit Data Register Empty Interrupt */__HAL_UART_DISABLE_IT(huart, UART_IT_TXE);/* Enable the UART Transmit Complete Interrupt */__HAL_UART_ENABLE_IT(huart, UART_IT_TC);}return HAL_OK;}else{return HAL_BUSY;}
}
  1. 數據發送處理:當進入這個函數時,首先檢查串口是否處于HAL_UART_STATE_BUSY_TX狀態(即正在發送過程中)。然后根據串口配置的字長和校驗位情況,從發送緩沖區中取出數據寫入到串口的數據寄存器(huart->Instance->DR)中。如果是 9 位數據且無校驗,就按 16 位處理;否則按 8 位處理。
  2. 中斷切換:每發送一個數據,發送計數器TxXferCount減 1。當TxXferCount減到 0 時,說明當前要發送的數據已經全部放到數據寄存器了,這時候需要關閉 TXE 中斷(因為不需要再觸發 “發送數據寄存器為空” 的中斷了),并使能 TC 中斷(等待發送完成中斷),這樣當一幀數據發送完成后就會觸發 TC 中斷。

(4)中斷模式完整函數鏈

中斷模式的收發,是「啟動函數 → 中斷觸發 → 回調函數」的完整鏈條,用表格串聯更清晰:

階段發送流程(中斷)接收流程(中斷)
啟動HAL_UART_Transmit_IT?啟動發送HAL_UART_Receive_IT?啟動接收
中斷觸發發完 1 字節 → TXE 中斷;發完所有字節 → TC 中斷收到 1 字節 → RXNE 中斷;收完所有字節 → 接收完成中斷
回調通知發完所有字節 →?HAL_UART_TxCpltCallback收完所有字節 →?HAL_UART_RxCpltCallback
錯誤處理統一走?HAL_UART_ErrorCallback統一走?HAL_UART_ErrorCallback

(5)中斷接收與發送完整代碼流程

(一)全局變量定義

extern UART_HandleTypeDef huart1;
int key_cut=0;
void key_timeout_func(void *args);
struct soft_timer key_timer ={~0,NULL,key_timeout_func};static uint8_t g_data_buf[100];
static circle_buf g_key_bufs;static volatile int g_tx_cplt = 0;

這里定義了串口句柄(通過extern引用)、一些用于按鍵處理的變量(雖然在串口中斷中可能暫時沒用到,但屬于整個工程的變量)以及用于標記發送完成的 volatile 變量g_tx_cplt(因為在中斷回調函數和主函數中都會訪問,需要用 volatile 保證其可見性)。

(二)發送完成回調函數

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{g_tx_cplt =1;
}

當串口發送完成中斷觸發并處理完成后,會調用這個回調函數,將g_tx_cplt置 1,用于在主函數中判斷發送是否完成。

(三)等待發送完成函數

void Wait_Tx_Complete(void)
{while(g_tx_cplt ==0);g_tx_cplt =0;
}

這個函數會在主函數中被調用,用于等待發送完成。它會一直循環檢查g_tx_cplt是否為 1,當為 1 時說明發送完成,然后將其置 0,為下一次發送做準備。

(四)主函數中的中斷發送流程

while (1)
{/*enable txe interrupt*/HAL_UART_Transmit_IT(&huart1,str2,strlen(str2));/*wait for tc*/Wait_Tx_Complete();while(HAL_OK !=HAL_UART_Receive(&huart1,&c,1,100));c=c+1;HAL_UART_Transmit(&huart1,&c,1,1000);HAL_UART_Transmit(&huart1,"\r\n",2,1000);
}
  1. 發送流程:在主循環中,首先調用HAL_UART_Transmit_IT(&huart1,str2,strlen(str2))以中斷模式發送字符串str2,然后調用Wait_Tx_Complete()等待發送完成(通過檢查g_tx_cplt)。
  2. 接收流程:發送完成后,調用HAL_UART_Receive(&huart1,&c,1,100)以查詢方式接收一個字節的數據(這里也可以改為中斷接收方式,后續優化方向),接收完成后對數據進行簡單處理(c=c+1),然后再用查詢方式發送回傳數據和換行符。

(6)中斷模式優缺點與適用場景

(一)優點

  1. 效率高:不需要像查詢方式那樣一直占用 CPU 去等待串口狀態,CPU 可以在等待串口中斷的時間去處理其他任務,比如進行按鍵掃描、傳感器數據處理等,提高了系統的整體效率。
  2. 實時性好:當串口有數據到來或者發送完成時能及時觸發中斷進行處理,對于一些對實時性要求較高的應用場景(如工業控制中的快速指令響應)非常合適。

(二)缺點

  1. 代碼復雜度高:相比查詢方式,中斷模式需要處理中斷函數、回調函數、中斷使能與禁用、中斷標志判斷等,代碼邏輯相對復雜,對于初學者來說理解和調試難度較大。
  2. 資源占用:雖然 CPU 利用率提高了,但中斷本身也會帶來一定的開銷,比如中斷上下文切換等,如果中斷過于頻繁,也可能會影響系統性能。

(三)適用場景

適用于對實時性要求較高、CPU 需要同時處理多個任務的場景,比如多傳感器數據實時上傳、工業設備的遠程控制指令接收與響應等。而對于一些簡單的、對實時性要求不高的小項目,查詢方式可能因為代碼簡單更容易實現。

(7)常見問題與調試方法

(一)中斷不觸發問題

  1. 可能原因
  • 1.中斷使能不正確:比如在HAL_UART_Transmit_IT中沒有正確使能 TXE 中斷,或者在HAL_UART_IRQHandler中相關中斷源的使能和標志判斷有問題。
  • 2.串口配置錯誤:在 STM32CubeMX 中配置串口時,沒有正確使能對應的中斷(如在 NVIC 設置中沒有使能 USART1 的中斷)。

  • 3.全局中斷未使能:即使串口的中斷使能了,但如果 CPU 的全局中斷沒有使能(比如沒有調用__enable_irq()函數,不過在 HAL 庫中一般會自動處理,但也可能因為某些配置被關閉),也無法響應中斷。
  1. 調試方法
    • 檢查 CubeMX 配置:查看串口的中斷是否在 NVIC 中正確使能,優先級是否設置合理。
    • 檢查代碼中的中斷使能函數:在HAL_UART_Transmit_IT中查看__HAL_UART_ENABLE_IT是否正確調用,使能的中斷類型是否正確。
    • 在中斷處理函數入口加斷點:在USART1_IRQHandler函數中加斷點,看是否能進入中斷處理函數,如果進不去,說明中斷觸發有問題;如果能進去,再逐步檢查HAL_UART_IRQHandler中的邏輯。

(二)數據發送不完整或錯誤

  1. 可能原因
    • 發送緩沖區處理問題:在UART_Transmit_IT中,數據從緩沖區取出和指針移動的邏輯有錯誤,導致數據沒有正確發送或者發送了錯誤的數據。
    • 中斷切換邏輯問題:TXE 中斷和 TC 中斷的切換沒有正確處理,導致數據發送到一半就停止或者發送完成后沒有正確標記狀態。
    • 數據長度設置錯誤:在調用HAL_UART_Transmit_IT時,strlen(str2)計算的長度不正確,導致發送的數據長度錯誤。
  2. 調試方法
    • UART_Transmit_IT函數中加斷點,檢查每次從緩沖區取出的數據是否正確,指針移動是否正確。
    • 檢查中斷切換時的計數器TxXferCount,看其遞減是否正確,以及中斷使能和禁用是否在正確的時機。
    • 打印發送的數據長度和實際發送的數據(可以通過串口助手配合,或者在代碼中使用調試串口打印中間變量),檢查數據是否正確。

(三)回調函數不執行

  1. 可能原因
    • 沒有正確實現回調函數:在 HAL 庫中,回調函數需要按照規定的名稱和參數實現,比如HAL_UART_TxCpltCallback,如果函數名寫錯或者參數不匹配,就不會被正確調用。
    • 中斷沒有正確觸發到發送完成階段:可能在數據發送過程中出現了錯誤,導致沒有觸發 TC 中斷,所以回調函數不會執行。
  2. 調試方法
    • 檢查回調函數名稱和參數是否正確。

三、中斷改造方法

(1)核心需求:“串口收發數據不丟” 要解決啥問題?

想象你用串口給設備發消息,比如連續快速發 10 個字符。如果設備處理慢,或者中斷響應不及時,數據就會 “擠在一起” 丟包。環形緩沖區?就像一個 “臨時倉庫”,先把收到的數據存起來,主程序慢慢取;中斷?負責 “一收到數據就通知倉庫存數據”,兩者配合就能解決丟包問題。

(2)代碼角色分工:誰在干啥?

// usart.c:專門處理串口中斷、環形緩沖區,收數據存緩沖區、取數據邏輯
#include "main.h"        // 包含HAL庫等
#include "usart.h"       // 串口相關聲明
#include "circle_buffer.h"  // 環形緩沖區頭文件// 靜態全局變量:僅usart.c內部用,避免全局污染
static volatile int g_tx_cplt = 0;  // 發送完成標志(0=未完成,1=完成)
static volatile int g_rx_cplt = 0;  // 接收完成標志(同理)
static uint8_t g_c;                 // 臨時存“剛收到的1個字節”
static uint8_t g_recvbuf[100];      // 環形緩沖區的“物理存儲數組”
static circle_buf g_uart1_rx_bufs;  // 環形緩沖區的“控制結構體”(存讀寫位置等)// 串口發送完成回調函數:HAL庫規定名,發送中斷完成后自動調用
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{g_tx_cplt = 1;  // 標記“發送完成”,告訴主程序可以繼續了
}// 【關鍵】串口接收完成回調函數:收到1個字節后,HAL庫自動調用
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{// 1. 把剛收到的1個字節(存在g_c里)存進環形緩沖區cirble_buf_write(&g_uart1_rx_bufs, g_c);  // 2. 重新使能接收中斷!否則只能收1個字節,收完就不監聽了HAL_UART_Receive_IT(&huart1, &g_c, 1);  
}// 等待發送完成:給主程序用,阻塞直到發送完成
void Wait_Tx_Complete(void)
{while (g_tx_cplt == 0) { /* 循環等,直到g_tx_cplt被回調函數設為1 */ }g_tx_cplt = 0;  // 清零,下次發送再用
}// 啟動串口接收中斷:主程序初始化時調用,開啟“收數據觸發中斷”
void startuart1recv(void)
{// 1. 初始化環形緩沖區:告訴它用g_recvbuf數組存數據,大小100circld_buf_init(&g_uart1_rx_bufs, 100, g_recvbuf);  // 2. 使能接收中斷:收到1個字節就觸發HAL_UART_RxCpltCallbackHAL_UART_Receive_IT(&huart1, &g_c, 1);  
}// 從環形緩沖區取1個字節:給主程序用,取“存好的數據”
int UART1getchar(uint8_t *pVal)
{// 調用環形緩沖區的讀取函數,把數據放到pVal里return circle_buf_read(&g_uart1_rx_bufs, pVal);  
}

先看代碼里的關鍵模塊,像 “部門分工” 一樣理解:

代碼部分作用類比核心任務
circle_buf(環形緩沖區)快遞臨時倉庫存收到的數據,主程序按需取
HAL_UART_RxCpltCallback(接收中斷回調)倉庫管理員一收到串口數據,就存進倉庫
startuart1recv啟動 “倉庫監聽”打開串口中斷,讓它能觸發回調
UART1getchar取快遞的人從倉庫里拿數據給主程序用

(3)流程拆解:從 “啟動程序” 到 “收發數據” 全步驟

1. 初始化:給 “倉庫” 和 “串口” 搭好架子
  • 環形緩沖區初始化(對應?circld_buf_init(&g_uart1_rx_bufs,100,g_recvbuf);):

    • 就像給倉庫劃分區域:g_recvbuf?是實際存數據的數組(倉庫貨架),g_uart1_rx_bufs?是管理這個數組的 “倉庫規則”(比如:數據存在哪、存了多少、從哪取)。
    • 你可以理解為:circld_buf_init?幫你 “建了一個 100 字節的臨時倉庫,準備存串口數據”。
  • 串口中斷啟動(對應?startuart1recv();):

    • 調用?HAL_UART_Receive_IT(&huart1,&g_c,1);,意思是:“串口 1 啊,你開啟‘接收中斷’吧!只要收到 1 個字節,就觸發回調函數!”
    • 這一步是 “打開倉庫管理員的監聽開關”,讓串口一收到數據,就通知程序處理。
2. 數據接收:“倉庫管理員” 怎么存數據?

當串口收到數據時(比如電腦發了一個字符?'A'),會觸發?HAL_UART_RxCpltCallback?中斷回調,流程像這樣:

  1. 觸發條件:串口硬件收到 1 個字節(比如?'A'),自動觸發這個函數。
  2. 存數據到環形緩沖區(對應?cirble_buf_write(&g_uart1_rx_bufs,g_c);):
    • g_c?里存著剛收到的字節(比如?'A'?的 ASCII 碼)。
    • 調用?cirble_buf_write,就像 “管理員把剛收到的快遞(數據)放進倉庫貨架(數組?g_recvbuf)”。
  3. 重新開啟中斷HAL_UART_Receive_IT(&huart1,&g_c,1);):
    • 因為中斷觸發一次后會關閉,必須重新調用它,才能繼續監聽下一個字節。
    • 相當于 “管理員存完這個快遞,趕緊打開監聽,等下一個快遞”。
3. 主程序取數據:從 “倉庫” 拿數據處理
// main.c:主程序,負責初始化、循環收發數據
#include "main.h"        // 包含HAL庫等基礎頭文件
#include "i2c.h"
#include "usart.h"       // 假設usart相關聲明在這里(實際工程需單獨.h)
#include "gpio.h"
#include "circle_buffer.h"  // 環形緩沖區頭文件// 軟件定時器結構體(按鍵消抖用,和串口主邏輯關聯弱,初學可先關注串口)
struct soft_timer
{uint32_t timeout;      // 超時時間戳(毫秒)void *args;            // 回調參數void (*func)(void *);  // 回調函數
};// 全局變量聲明(實際工程建議用static+訪問函數,這里簡化)
extern UART_HandleTypeDef huart1;  // 串口1句柄,usart.c里會用到
int key_cut = 0;
void key_timeout_func(void *args); 
struct soft_timer key_timer = {~0, NULL, key_timeout_func};// 環形緩沖區相關(按鍵邏輯,初學可先跳過,關注串口部分)
static uint8_t g_data_buf[100];     
static circle_buf g_key_bufs;// 串口收發相關函數聲明(實際應放usart.h,這里簡化)
void startuart1recv(void);   // 啟動串口接收中斷
int UART1getchar(uint8_t *pVal); // 從環形緩沖區取數據
void Wait_Tx_Complete(void);  // 等待發送完成
void Wait_Rx_Complete(void);  // 等待接收完成(實際未用到,演示用)// 主函數:程序入口
int main(void)
{char *str = "Please enter a char: \r\n"; // 發送的提示字符串char *str2 = "www.100ask.net\r\n";       // 歡迎信息char c;  // 存儲接收到的字符// 1. 初始化HAL庫、系統時鐘HAL_Init();          SystemClock_Config(); // 2. 初始化環形緩沖區(按鍵邏輯,初學先記住串口也有環形緩沖區)circld_buf_init(&g_key_bufs, 100, g_data_buf); // 3. 初始化外設:GPIO、I2C、串口MX_GPIO_Init();      MX_I2C1_Init();      MX_USART1_UART_Init(); // 4. 初始化OLED(如果有的話,演示用,和串口核心邏輯無關)OLED_Init();OLED_Clear();OLED_PrintString(0, 0, "cnt     : ");int len = OLED_PrintString(0, 2, "key val : ");// 5. 先發送一條歡迎信息HAL_UART_Transmit(&huart1, str2, strlen(str2), 1000); // 6. 啟動串口接收中斷:讓串口一收到數據就觸發中斷存緩沖區startuart1recv();  // 主循環:一直運行while (1){// 7. 非阻塞發送提示信息:告訴串口“后臺發,發完通知我”HAL_UART_Transmit_IT(&huart1, str, strlen(str)); // 8. 等待發送完成(阻塞等待,確保發完再干別的)Wait_Tx_Complete();  // 9. 從環形緩沖區取數據:循環取,直到取到數據(0表示成功)while (0 != UART1getchar(&c)) { /* 沒數據就循環等 */ }// 10. 處理數據:收到的字符+1,再發回去c = c + 1; HAL_UART_Transmit(&huart1, &c, 1, 1000); HAL_UART_Transmit(&huart1, "\r\n", 2, 1000); // 以下是按鍵消抖邏輯(和串口主流程關聯弱,初學可暫時注釋掉)// key_timeout_func相關邏輯...(演示用,不影響串口理解)}
}// 按鍵消抖回調(和串口主邏輯無關,初學可跳過)
void key_timeout_func(void *args)
{uint8_t key_val;key_cut++;key_timer.timeout = ~0;if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_14) == GPIO_PIN_RESET)key_val = 0x1;elsekey_val = 0x81;cirble_buf_write(&g_key_bufs, key_val);
}// 定時器修改函數(和串口主邏輯無關,初學可跳過)
void mod_timer(struct soft_timer *pTimer, uint32_t timeout)
{pTimer->timeout = HAL_GetTick() + timeout;
}// 定時器檢查函數(和串口主邏輯無關,初學可跳過)
void cherk_timer(void)
{if (key_timer.timeout <= HAL_GetTick()){key_timer.func(key_timer.args);}
}// GPIO中斷回調(和串口主邏輯無關,初學可跳過)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{if (GPIO_PIN_14 == GPIO_Pin){mod_timer(&key_timer, 10);}
}

主程序里有個循環?while(0 != UART1getchar(&c));,它的作用是?“一直從倉庫里取數據”,流程:

  1. 調用?UART1getchar(&c)(對應?return circle_buf_read(&g_uart1_rx_bufs,pVal);):
    • 這是 “取快遞的人” 去倉庫找數據。circle_buf_read?會檢查環形緩沖區里有沒有數據。
    • 如果有數據,就把數據放到?c?里(比如剛才存的?'A');如果沒數據,就等下次再取。
  2. 主程序處理數據(比如?c=c+1; HAL_UART_Transmit(&huart1,&c,1,1000);):
    • 拿到?c(比如?'A')后,主程序可以修改它(比如?c+1?變成?'B'),再通過?HAL_UART_Transmit?發回去。
4. 發送數據:“非阻塞” 發送是咋回事?

代碼里用了?HAL_UART_Transmit_IT(&huart1,str,strlen(str));?+?Wait_Tx_Complete();?發送數據:

  • HAL_UART_Transmit_IT:“告訴串口:你后臺慢慢發數據,別阻塞主程序!發完了通知我。”
  • Wait_Tx_Complete():“主程序在這等著,直到串口發完數據(g_tx_cplt?變成 1),再繼續干別的。”
    (類比:你點了個外賣配送,不用一直盯著,配送完 App 通知你 —— 但你可以選擇 “等配送完再干別的”)

(4)環形緩沖區核心邏輯(白話版)

很多同學不懂 “環形緩沖區” 咋循環存數據,用?“快遞貨架” 比喻?講清楚:

  1. 存數據(cirble_buf_write

    • 貨架有 100 個格子(g_recvbuf[100]),管理員存數據時,按順序往后放。
    • 存滿了怎么辦?環形?的關鍵:從最后一個格子跳回第一個格子繼續存(像操場跑圈),這樣不用清空數組,反復利用空間。
  2. 取數據(circle_buf_read

    • 取數據的人按 “存數據的順序” 拿,存的時候從格子 0→1→2…,取的時候也 0→1→2…,保證數據順序不亂。
    • 如果存的比取的快,倉庫會暫時存著;如果取的比存的快,就等新數據進來。

(5)“不丟數據” 的核心秘密:中斷 + 環形緩沖區配合

  • 中斷?保證 “一收到數據就存”:不管主程序在干啥,只要串口有數據,立刻觸發回調存進倉庫,不會因為主程序忙別的就丟數據。
  • 環形緩沖區?保證 “數據有地方存”:即使主程序處理慢,數據先存在倉庫里,不會因為 “沒及時取” 就丟失,主程序啥時候有空啥時候取。

(6)新手常見疑問解答

Q1:HAL_UART_RxCpltCallback?里為啥要重新調用?HAL_UART_Receive_IT

A:因為串口中斷觸發一次后,會自動關閉。重新調用才能繼續監聽下一個字節,保證 “收到一個存一個”,不斷觸發中斷。

Q2:環形緩沖區和普通數組有啥區別?

A:普通數組存滿了必須清空才能繼續存;環形緩沖區像 “循環跑道”,存滿了從開頭繼續存,不用清空,效率更高。

Q3:主程序里?while(0 != UART1getchar(&c));?是死循環嗎?

A:不是死循環!UART1getchar?里,沒數據時會返回非 0(比如 -1),主程序會一直循環嘗試取;一旦取到數據(返回 0),就跳出循環繼續處理。

(7)總結:完整流程串起來

  1. 初始化:建環形緩沖區倉庫,打開串口中斷監聽。
  2. 收數據:串口收到數據 → 觸發中斷回調 → 數據存進環形緩沖區 → 重新開啟中斷,等下一個數據。
  3. 取數據:主程序循環從環形緩沖區取數據,處理后可以再發回去。
  4. 發數據:用中斷方式后臺發送,主程序不用阻塞等待,發完再繼續干活。

這樣配合,就能實現?“串口收發數據不丟失”,不管數據來多快、主程序多忙,都能穩穩接住~

四、STM32 DMA 串口收發教程(結合中斷,從 0 講透)

<3>DMA 模式的回調函數(此次核心代碼)

DMA 模式比中斷模式多了「半完成回調」,適合大數據量的 “邊傳邊處理”,用表格對比差異:

回調類型觸發時機典型用途
完成回調數據全部收發完成后觸發最終數據處理(如校驗、存儲完整數據)
半完成回調數據收發到一半時觸發實時處理(如顯示傳輸進度、臨時緩存)
錯誤回調收發過程中出現錯誤時觸發錯誤恢復(如重發、報錯提示)

DMA 發送有?HAL_UART_TxHalfCpltCallback(發一半觸發)、HAL_UART_TxCpltCallback(發完觸發);接收同理。

/發送
HAL_UART_Transmit_DMA
HAL_UART_TxHalfCpltCallback
HAL_UART_TxCpltCallback
//接收
HAL_UART_Receive_DMA
HAL_UART_RxHalfCpltCallback
HAL_UART_RxCpltCallback
//錯誤回調
HAL_UART_ErrorCallback

(1)DMA 是啥?解決啥問題?

1. 白話理解 DMA

  • DMA 全稱:直接存儲器訪問(Direct Memory Access)
  • 作用:讓數據 “自己搬家”,不用 CPU 盯著!
    比如串口發 1000 個字符:
    • 不用 DMA:CPU 得逐個把字符 “抱” 到串口寄存器,期間不能干別的(像被 “拴在串口旁當苦力”)。
    • 用 DMA:CPU 說 “我要發這 1000 個字符,地址是 xx”,然后就去干別的。DMA 控制器自己把數據逐個搬到串口,搬完告訴 CPU(通過中斷)。

2. 解決的核心問題

  • 解放 CPU:讓 CPU 能同時處理其他任務(比如按鍵掃描、LED 控制),不用卡在 “收發數據” 上。
  • 適合大數據:發 1000 個字符、收 1000 個字節時,DMA 效率碾壓 “CPU 逐個搬”。

(2)和之前 “中斷收發” 的區別(對比理解)

方式核心邏輯適合場景缺點
普通中斷收 / 發 1 個字節就觸發中斷,CPU 處理數據量小、需要實時響應數據量大時,CPU 被中斷 “累死”
DMA + 中斷DMA 自動搬數據,搬完(或搬一半)觸發中斷告訴 CPU大數據收發(比如發 1000 字符)接收時需配合 IDLE 中斷才好用(下文講)

(3)代碼改造思路(把 “中斷收發” 改成 “DMA 收發”)

1. 發送改造:把?HAL_UART_Transmit_IT?換成?HAL_UART_Transmit_DMA

  • 原來的中斷發送:發 1 個字節觸發一次中斷,CPU 得管 “發完一個,下一個咋發”。
  • DMA 發送
    1. CPU 告訴 DMA:“我要發數組?str,長度?strlen(str),目標是串口 TDR 寄存器”。
    2. DMA 自動循環搬數據,全部搬完后觸發?HAL_UART_TxCpltCallback?中斷,告訴 CPU “發完了”。

2. 接收改造:HAL_UART_Receive_DMA?+ (可選 IDLE 中斷)

  • DMA 接收
    1. CPU 告訴 DMA:“我要收數據,存到數組?RxBuf,最多收?len?個字節”。
    2. DMA 自動把串口收到的數據搬到?RxBuf收滿?len?個?觸發?HAL_UART_RxCpltCallback收一半?觸發?HAL_UART_RxHalfCpltCallback
  • 為啥需要 IDLE 中斷
    串口收數據時,可能 “斷斷續續”(比如對方一次發 5 個,又發 3 個)。DMA 只會在 “收滿指定長度” 才觸發中斷,沒法知道 “對方已經發完一批”。這時候需要?IDLE 中斷:串口 “空閑” 時(沒數據來)觸發中斷,告訴 CPU“對方暫時不發了,你可以處理收到的數據了”。

四、保姆級代碼流程拆解(結合你發的代碼改造)

1. 關鍵函數對應(看第四張圖?DMA 模式?函數)

函數名觸發時機作用
HAL_UART_Transmit_DMA主程序調用,啟動 “DMA 發送”告訴 DMA 開始發數據
HAL_UART_TxCpltCallbackDMA 把數據全部發完后觸發通知 CPU “發送完成”
HAL_UART_Receive_DMA主程序調用,啟動 “DMA 接收”告訴 DMA 開始收數據
HAL_UART_RxCpltCallbackDMA 把數據全部收滿后觸發通知 CPU “收滿指定長度了”
HAL_UART_RxHalfCpltCallbackDMA 收了一半數據后觸發(可選)收一半時提前處理數據
HAL_UART_ErrorCallback收發出錯時觸發(比如總線錯誤)處理錯誤

2. 改造后的?main.c?核心代碼(發送部分)

// main.c 主程序#include "main.h"
#include "usart.h"  // 包含串口、DMA 相關聲明// 全局變量
UART_HandleTypeDef huart1;  // 串口1句柄(CubeMX 配置生成)
char *str = "Hello DMA! 這是用 DMA 發的數據\r\n";  // 要發的字符串// 主函數
int main(void)
{HAL_Init();             // 初始化 HAL 庫SystemClock_Config();   // 配置系統時鐘MX_USART1_UART_Init();  // 初始化串口(CubeMX 生成,包含 DMA 配置)MX_DMA_Init();          // 初始化 DMA(CubeMX 生成)// 1. 啟動 DMA 發送:把 str 的內容,通過 DMA 發給串口HAL_UART_Transmit_DMA(&huart1, (uint8_t *)str, strlen(str));  // 2. 主循環:發完后,DMA 會觸發 HAL_UART_TxCpltCallback 中斷while (1){// 發完后,這里可以干別的(比如掃按鍵、閃 LED)// 不用卡在“等發送完成”,因為 DMA 自己在后臺發}
}// 【關鍵】DMA 發送完成回調函數:HAL 庫規定名稱,發完自動調用
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{if (huart == &huart1)  // 確認是串口1的回調{// 可以在這里做“發送完成后的操作”:// 比如再發一次數據、切換 LED 狀態HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);  // 假設 LED 宏定義好了}
}

(5)結合“DMA 模式圖” 詳細講流程??

1. 發送流程(對應第二張圖?RAM → DMA → USART)?

?

  1. CPU 下達指令
    執行?HAL_UART_Transmit_DMA(&huart1, str, len),CPU 告訴 DMA:

    • 源地址str(RAM 里的字符串數組)。
    • 目標地址:串口的?TDR 寄存器(Transmit Data Register,發送數據寄存器)。
    • 長度len(要發多少個字節)。
  2. DMA 自動搬數據
    DMA 控制器開始工作,逐個把?str?里的字節,從 RAM 搬到?USART 的 TDR

    • 這一步?不需要 CPU 參與!CPU 可以去干別的(比如?while(1)?里的按鍵掃描)。
  3. 發送完成觸發中斷
    DMA 把?len?個字節全部搬完后,觸發?HAL_UART_TxCpltCallback?中斷。

    • CPU 暫停當前任務,執行回調函數里的邏輯(比如 “再發一次數據”“翻轉 LED”)。

2. 接收流程(?USART → DMA → RAM

假設要收數據到數組?RxBuf[100],流程:

  1. CPU 下達指令
    執行?HAL_UART_Receive_DMA(&huart1, (uint8_t *)RxBuf, 100),CPU 告訴 DMA:

    • 源地址:串口的?RDR 寄存器(Receive Data Register,接收數據寄存器)。
    • 目標地址RxBuf(RAM 里的數組)。
    • 長度100(最多收 100 個字節)。
  2. DMA 自動搬數據
    串口收到數據,存到?RDR 寄存器?→ DMA 自動把 RDR 的數據搬到?RxBuf

  3. 觸發中斷的兩種情況

    • 收滿 100 個字節:觸發?HAL_UART_RxCpltCallback,告訴 CPU“收滿了,來處理”。
    • 收了 50 個字節(一半):觸發?HAL_UART_RxHalfCpltCallback,告訴 CPU“收了一半,可以提前處理”(可選)。
  4. 為啥需要 IDLE 中斷
    如果對方發的數據?不足 100 個(比如只發 30 個),DMA 不會觸發?RxCpltCallback(因為沒收滿 100)。這時候需要?IDLE 中斷

    • 串口 “空閑” 時(沒數據來),觸發 IDLE 中斷,告訴 CPU“對方暫時不發了,你可以處理?RxBuf?里的 30 個數據”。

(6)關鍵函數詳解(保姆級逐行講)

1.?HAL_UART_Transmit_DMA:啟動 DMA 發送

HAL_UART_Transmit_DMA(&huart1, (uint8_t *)str, strlen(str));
  • 參數 1&huart1?→ 操作的串口(串口 1)。

  • 參數 2(uint8_t *)str?→ 要發的數據在 RAM 里的地址。

  • 參數 3strlen(str)?→ 要發的字節數(比如字符串長度)。

  • 底層干了啥(結合第二張圖):

    1. 配置 DMA 的?源地址?為?str目標地址?為?huart1->Instance->TDR(串口 TDR 寄存器)。
    2. 配置 DMA 傳輸長度為?strlen(str)
    3. 啟動 DMA 傳輸,開始自動搬數據。

2.?HAL_UART_TxCpltCallback:發送完成回調

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{if (huart == &huart1){// 發送完成!可以在這里做后續操作HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);  // 比如翻轉 LED}
}
  • 觸發時機:DMA 把所有數據搬完(TDR 發完)后,HAL 庫自動調用。
  • 作用:告訴 CPU “發送結束”,可以執行 “發完后的邏輯”(比如再發一批、記錄日志)。

3.?HAL_UART_Receive_DMA:啟動 DMA 接收

uint8_t RxBuf[100];  // 存接收的數據
HAL_UART_Receive_DMA(&huart1, RxBuf, 100);
  • 參數 1&huart1?→ 操作的串口(串口 1)。

  • 參數 2RxBuf?→ 數據接收后,存到 RAM 里的數組。

  • 參數 3100?→ 最多收 100 個字節。

  • 底層干了啥(結合第三張圖):

    1. 配置 DMA 的?源地址?為?huart1->Instance->RDR(串口 RDR 寄存器)。
    2. 配置 DMA 的?目標地址?為?RxBuf
    3. 配置 DMA 傳輸長度為?100
    4. 啟動 DMA 傳輸,串口收到數據后,DMA 自動把 RDR 的數據搬到?RxBuf

4.?HAL_UART_RxCpltCallback:接收完成回調(收滿長度觸發)

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{if (huart == &huart1){// 收滿 100 個字節了!可以處理 RxBuf 里的數據// 比如打印、解析HAL_UART_Transmit(&huart1, RxBuf, 100, 1000);  // 把收到的發回去}
}
  • 觸發時機:DMA 把?RxBuf?收滿 100 個字節后,自動調用。
  • 注意:如果對方發的數據不足 100,不會觸發這個回調!這時候需要配合?IDLE 中斷(下文補講)。

(7)接收的 “坑”:DMA 接收需配合 IDLE 中斷(初學者必看)

1. 問題:DMA 接收 “收不滿長度,不觸發回調”

比如你讓 DMA 收 100 個字節,但對方只發 30 個。DMA 沒收滿 100,不會觸發?HAL_UART_RxCpltCallback,主程序就 “不知道啥時候該處理這 30 個數據”。

五、STM32 USART 進階:DMA + IDLE 中斷收發保姆級教程(含代碼修復)

(1)前言:為什么需要 DMA + IDLE 中斷?

在 STM32 串口通信中,普通中斷收發適合小數據量,但面對連續不定長數據時:

  • 純中斷:頻繁觸發中斷,CPU 負擔重
  • 純 DMA:無法判斷 “數據何時接收完成”(比如對方發 50 字節后突然停止)

而?DMA + IDLE 中斷?完美解決這兩個痛點:

  • DMA 自動搬運數據,解放 CPU
  • IDLE 中斷精準識別 “數據傳輸暫停”,讓我們知道 “該處理已收數據了”

(2)核心問題拆解(老師代碼的坑)

1. 問題 1:忘記使能接收通道

  • 現象:DMA 能發數據,但收不到任何內容
  • 原因:串口接收的 DMA 通道未正確使能,數據無法從串口寄存器搬運到內存

2. 問題 2:IDLE 中斷后未重啟 DMA

  • 現象:只能接收一次數據,之后無響應
  • 原因:IDLE 中斷觸發后,未重新啟動 DMA 接收,導致后續數據無法被捕獲

(3)DMA + IDLE 中斷完整流程(從硬件到代碼)

1. 硬件配置(CubeMX 關鍵步驟)

(1)串口配置
  • 模式:Asynchronous(異步模式)
  • 波特率:根據需求設置(如 115200)
  • 使能 DMA:
    • TX:DMA1 Channel 4Memory To Peripheral
    • RX:DMA1 Channel 5Peripheral To Memory
(2)NVIC 配置
  • 使能?USART1 global interrupt
  • 使能?DMA1 Channel 4/5 interrupt(可選,部分場景需 DMA 半傳輸 / 傳輸完成中斷)

2. 代碼流程拆解

(1)文件結構
  • usart.c:核心邏輯(DMA 收發、中斷回調)
(2)usart.c?完整代碼(修復版)
/* USER CODE BEGIN 1 */
// 發送完成標志(用于非阻塞發送時等待發送結束)
static volatile int  g_tx_cplt = 0;
// 接收完成標志(用于普通中斷接收模式,此處DMA模式較少用)
static volatile int  g_rx_cplt = 0;
// 臨時接收緩沖區(普通中斷模式下存儲單個字節)
static uint8_t g_c;
// DMA接收緩沖區(一次接收10個字節)
static uint8_t g_buf[10];
// 環形緩沖區的物理存儲空間(用于存儲所有接收到的數據)
static uint8_t g_recvbuf[100];
// 環形緩沖區控制結構(管理讀寫位置等信息)
static circle_buf g_uart1_rx_bufs;/*** 發送完成回調函數(DMA模式)* 當DMA將所有數據從內存發送到串口后,由HAL庫自動調用*/
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{// 標記發送完成(用于Wait_Tx_Complete函數判斷)g_tx_cplt = 1;// 注意:此處注釋提到"放入環形緩沖區",但發送完成無需操作緩沖區// 發送完成回調主要用于喚醒等待的任務或處理發送后邏輯
}/*** 等待發送完成(阻塞函數)* 調用后會一直等待,直到DMA發送完成(g_tx_cplt被置1)*/
void Wait_Tx_Complete(void)
{// 循環等待發送完成標志while (g_tx_cplt == 0);// 清除標志,為下一次發送做準備g_tx_cplt = 0;
}/*** 接收完成回調函數(普通中斷模式)* 當使用HAL_UART_Receive_IT時,每收到1個字節觸發一次* 注意:在DMA+IDLE模式下,主要使用HAL_UARTEx_RxEventCallback*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{// 標記接收完成(普通中斷模式下使用)g_rx_cplt = 1;// 將收到的單個字節存入環形緩沖區for(int i = 0; i < 10; i++){// 此處邏輯有誤!普通中斷每次只收1字節,應直接存g_c// 正確寫法:cirble_buf_write(&g_uart1_rx_bufs, g_c);// 但代碼中錯誤地循環寫入g_buf(DMA緩沖區),可能導致數據異常cirble_buf_write(&g_uart1_rx_bufs, g_buf[i]);}// 重新啟動接收中斷(很重要!否則只能接收一次)// 注意:此處使用了錯誤的函數!在DMA模式下應使用HAL_UARTEx_ReceiveToIdle_DMA// 正確寫法:HAL_UARTEx_ReceiveToIdle_DMA(&huart1, g_buf, 10);HAL_UART_Receive_IT(&huart1, &g_c, 1);
}/*** 啟動串口接收(初始化函數)* 配置環形緩沖區并開啟接收中斷*/
void startuart1recv(void)
{// 初始化環形緩沖區(指定緩沖區大小和物理存儲數組)circld_buf_init(&g_uart1_rx_bufs, 100, g_recvbuf);// 啟動接收中斷(注意:此處使用了普通中斷模式!)// 正確寫法:HAL_UARTEx_ReceiveToIdle_DMA(&huart1, g_buf, 10);HAL_UART_Receive_IT(&huart1, &g_c, 1);
}/*** 從環形緩沖區讀取一個字節* 返回0表示成功讀取,非0表示緩沖區為空*/
int UART1getchar(uint8_t *pVal)
{return circle_buf_read(&g_uart1_rx_bufs, pVal);
}/*** IDLE事件+DMA接收回調函數(關鍵!)* 當DMA接收完成或串口空閑時觸發*/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{// 將DMA緩沖區中的數據存入環形緩沖區for(int i = 0; i < Size; i++){cirble_buf_write(&g_uart1_rx_bufs, g_buf[i]);}// 重新啟動DMA接收(關鍵!否則只能接收一次)// 注意:老師忘記在IDLE中斷中調用此函數,導致只能接收一次數據HAL_UARTEx_ReceiveToIdle_DMA(&huart1, g_buf, 10);
}
/* USER CODE END 1 */

(4)核心邏輯分步詳解(從 “啟動” 到 “收數據”)

1. 啟動流程(startuart1recv?做了什么?

  • 環形緩沖區g_recvbuf?作為 “臨時倉庫”,存零散收到的數據
  • DMA 配置:告訴硬件 “把串口收到的數據,自動搬到?g_buf,一次搬 10 字節”

2. 接收流程(DMA + IDLE 如何配合?)

(1)正常接收(數據連續)

?

?(2)觸發 IDLE 事件(數據暫停

?????

  • 為什么要重啟 DMA
    DMA 傳輸一旦完成(或觸發 IDLE),會自動停止。必須重新調用?HAL_UARTEx_ReceiveToIdle_DMA,才能繼續接收后續數據。

3. 發送流程(DMA 發送如何工作?)

(5)常見問題與解決方案(初學者必看)

1. 問題:DMA 接收后,環形緩沖區無數據

  • 原因
    • DMA 通道未正確使能(CubeMX 配置問題)
    • HAL_UARTEx_RxEventCallback?中未正確重啟 DMA
  • 解決
    • 檢查 CubeMX 的 DMA 配置(確保?USART1_RX?通道使能)
    • 確認?HAL_UARTEx_RxEventCallback?中調用了?HAL_UARTEx_ReceiveToIdle_DMA(&huart1, g_buf, 10);

2. 問題:只能接收一次數據,之后無響應

  • 原因:IDLE 事件觸發后,未重啟 DMA 接收
  • 解決:在?HAL_UARTEx_RxEventCallback?中添加重啟代碼
    HAL_UARTEx_ReceiveToIdle_DMA(&huart1, g_buf, 10);
    

3. 問題:IDLE 中斷頻繁觸發

  • 原因:串口線干擾或波特率配置錯誤,導致誤判 “空閑”
  • 解決
    • 檢查硬件接線(確保 GND 共地,串口線無松動)
    • 重新校準波特率(確保收發雙方波特率一致)

(6)總結:DMA + IDLE 中斷的價值

  • 效率:DMA 自動搬運數據,CPU 可同時處理其他任務
  • 靈活性:IDLE 事件精準識別 “數據暫停”,完美支持不定長數據收發
  • 可靠性:環形緩沖區緩沖零散數據,避免丟包

這套方案是 STM32 串口通信的 “進階標配”,掌握后可輕松應對串口調試助手連續發數據、上位機批量傳文件等場景!

六、完善UART程序與stdio(最終結果)

// 引入頭文件:頭文件相當于工具包,包含了各種已經寫好的函數和定義,方便我們直接使用
#include "main.h"         // 主頭文件,包含系統初始化、核心函數的定義
#include "dma.h"          // DMA相關工具:用于高速數據傳輸(不用CPU參與)
#include "i2c.h"          // I2C通信工具:用于和OLED等I2C設備通信
#include "usart.h"        // UART串口工具:用于串口收發數據(比如和電腦通信)
#include "gpio.h"         // GPIO工具:用于控制引腳高低電平(比如按鍵、LED)
#include "circle_buffer.h"// 環形緩沖區工具:一種特殊的存儲結構,適合臨時存數據
#include <stdio.h>        // 標準輸入輸出工具:包含printf、scanf等函數// 定義一個"軟定時器"結構體:用軟件實現定時功能(類似鬧鐘,到時間了就做指定的事)
struct soft_timer
{uint32_t timeout;       // 超時時間點(單位:毫秒):記錄"什么時候響鈴"void * args;            // 回調參數:給定時任務傳的數據(這里暫時不用)void (*func)(void *);   // 回調函數:超時后要執行的任務("響鈴后要做的事")
};// 聲明外部變量:huart1是UART1的"句柄"(可以理解為UART1的身份證,操作它必須用這個句柄)
extern UART_HandleTypeDef huart1;int key_cut = 0;  // 按鍵計數:記錄按鍵被有效按下的次數// 聲明按鍵超時處理函數(后面會具體實現)
void key_timeout_func(void *args);// 創建一個按鍵專用的軟定時器:初始狀態為"未激活"(timeout=~0表示無窮大)
struct soft_timer key_timer = {~0, NULL, key_timeout_func};// 聲明幾個函數(先告訴編譯器有這些函數,后面再實現)
void Wait_Tx_Complete(void);    // 等待串口發送完成
void Wait_Rx_Complete(void);    // 等待串口接收完成
void startuart1recv(void);      // 啟動UART1接收功能
int UART1getchar(uint8_t *pVal);// 從UART1讀取一個字符// 環形緩沖區相關變量:用來臨時存儲按鍵數據(防止按鍵觸發太快處理不過來)
static uint8_t g_data_buf[100];  // 實際存儲數據的空間(可以存100個字節)
static circle_buf g_key_bufs;    // 環形緩沖區的管理結構(負責讀寫數據)/*** 按鍵超時處理函數:軟定時器到時間后執行(用于按鍵消抖后確認狀態)* 為什么需要消抖?按鍵按下時金屬觸點會抖動(10ms內可能通斷多次),10ms后再讀才準確*/
void key_timeout_func(void *args)
{uint8_t key_val;  // 存儲按鍵的狀態值(0x1表示按下,0x81表示松開)key_cut++;        // 按鍵計數+1(每有效觸發一次,計數加1)key_timer.timeout = ~0;  // 重置定時器:處理完后暫時關閉,下次按鍵再激活// 讀取GPIO_PIN_14引腳的電平(這個引腳接了按鍵)// GPIO_PIN_RESET表示低電平(按鍵按下,因為按鍵通常接下拉電阻)// GPIO_PIN_SET表示高電平(按鍵松開)if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_14) == GPIO_PIN_RESET)key_val = 0x1;   // 按鍵按下,存0x1elsekey_val = 0x81;  // 按鍵松開,存0x81(用不同值區分兩種狀態)// 把按鍵狀態存入環形緩沖區(先存起來,后面慢慢處理)circle_buf_write(&g_key_bufs, key_val);
}/*** 設置定時器超時時間(激活定時器)* @param pTimer:要設置的定時器* @param timeout:要等待的時間(單位:毫秒)*/
void mod_timer(struct soft_timer *pTimer, uint32_t timeout)
{// HAL_GetTick():獲取系統從啟動到現在的毫秒數(比如啟動后1秒,返回1000)// 超時時間點 = 當前時間 + 等待時間(比如現在1000ms,等10ms,超時點就是1010ms)pTimer->timeout = HAL_GetTick() + timeout;
}/*** 檢查定時器是否超時(軟定時器的核心邏輯)* 相當于"看鬧鐘有沒有響",需要在主循環里反復調用*/
void check_timer(void)
{// 如果當前時間 >= 定時器的超時時間點,說明"鬧鐘響了"if(key_timer.timeout <= HAL_GetTick()){// 執行定時器綁定的任務(調用回調函數)key_timer.func(key_timer.args);}
}/*** GPIO中斷回調函數:當引腳電平變化時自動調用(比如按鍵按下時)* @param GPIO_Pin:觸發中斷的引腳編號*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{// 判斷是不是GPIO_PIN_14引腳觸發的中斷(我們的按鍵接在這個引腳)if(GPIO_Pin == GPIO_PIN_14){// 激活按鍵定時器,10ms后執行超時處理(用于消抖)mod_timer(&key_timer, 10);}
}/*** 聲明系統時鐘配置函數(由STM32CubeMX自動生成,負責設置CPU等的工作頻率)*/
void SystemClock_Config(void);/*** 主函數:程序的入口,所有代碼從這里開始執行* @retval int:返回值(嵌入式程序通常不返回,這里只是標準格式)*/
int main(void)
{int len;  // 臨時變量,用來存字符串長度// 定義要發送的字符串:\r\n是換行符(串口通信中換行需要這兩個字符)char *str = "Please enter a char: \r\n";char *str2 = "www.100ask.net\r\n";char c;   // 用來存從串口接收的字符// 初始化HAL庫:重置所有外設,初始化Flash和系統滴答定時器(用于延時)HAL_Init();// 配置系統時鐘:設置CPU、外設的工作頻率(比如72MHz)SystemClock_Config();// 初始化按鍵專用的環形緩沖區:指定大小100,用g_data_buf作為存儲區circle_buf_init(&g_key_bufs, 100, g_data_buf);// 初始化各個外設(由STM32CubeMX自動生成)MX_GPIO_Init();    // 初始化GPIO(配置引腳功能)MX_DMA_Init();     // 初始化DMAMX_I2C1_Init();    // 初始化I2C1(用于OLED通信)MX_USART1_UART_Init();  // 初始化UART1(配置波特率等參數)// 初始化OLED屏幕并清屏OLED_Init();OLED_Clear();// 在OLED上顯示固定文本:第一行顯示"cnt     : "(用于顯示按鍵計數)// 第三行顯示"key val : "(用于顯示按鍵狀態值)OLED_PrintString(0, 0, "cnt     : ");len = OLED_PrintString(0, 2, "key val : ");// 通過UART1發送str2字符串到電腦:// 參數:UART句柄、要發的字符串、長度、超時時間(1000ms發不出去就放棄)HAL_UART_Transmit(&huart1, str2, strlen(str2), 1000);// 啟動UART1的接收功能:讓UART1準備好接收數據,收到后會觸發中斷startuart1recv();// 主循環:程序啟動后會一直在這里循環執行(無限循環)while (1){// 通過printf發送str2到串口(printf已被設置為通過UART1發送)printf("%s", str2);// 內層循環:等待用戶輸入一個有效字符(不是'r'也不是換行符)while(1){// 從串口接收一個字符(scanf已被設置為通過UART1接收)scanf("%c", &c);// 如果接收的字符不是'r'也不是換行符('\n'),就處理并退出內層循環if(c != 'r' && c != '\n'){c = c + 1;  // 字符加1(比如輸入'a'變成'b',輸入'1'變成'2')printf("%c\r\n", c);  // 把處理后的字符發回串口break;  // 退出內層循環,回到外層循環重新等待輸入}}}
}
// 發送/接收完成標志,使用volatile確保編譯器不優化
static volatile int  g_tx_cplt=0;
static volatile int  g_rx_cplt=0;
// 臨時存儲接收數據的變量和緩沖區
static uint8_t g_c;
static uint8_t g_buf[10];
static uint8_t g_recvbuf[100];
// UART1接收緩沖區(環形緩沖區)
static circle_buf g_uart1_rx_bufs;/*** UART發送完成回調函數* 當UART發送完成時,此函數會被自動調用*/
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{if(huart == &huart1){g_tx_cplt=1; // 設置發送完成標志// 注釋中提到"放入環形緩沖區",但此處未實現}
}/*** 等待UART發送完成* 阻塞函數,會一直等待直到發送完成*/
void Wait_Tx_Complete(void)
{while (g_tx_cplt == 0 ); // 等待發送完成標志g_tx_cplt=0; // 清除標志,準備下一次發送
}/*** UART接收完成回調函數* 當使用HAL_UART_Receive_IT()接收到指定數量的數據后,此函數會被調用*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{if(huart == &huart1){g_rx_cplt=1; // 設置接收完成標志// 將接收到的數據(10字節)存入環形緩沖區for(int i=0;i<10;i++){cirble_buf_write(&g_uart1_rx_bufs,g_buf[i]);}// 重新啟動接收,準備接收下一組數據HAL_UARTEx_ReceiveToIdle_DMA(&huart1,g_buf,10);}
}/*** 啟動UART1接收* 初始化環形緩沖區并開啟接收中斷*/
void startuart1recv(void)
{// 初始化環形緩沖區,大小為100字節circld_buf_init(&g_uart1_rx_bufs,100,g_recvbuf);// 啟動中斷接收,每次接收1字節HAL_UART_Receive_IT(&huart1,&g_c,1);
}/*** 從UART1接收緩沖區獲取一個字符* 返回0表示成功獲取,非0表示緩沖區為空*/
int UART1getchar(uint8_t *pVal)
{return circle_buf_read(&g_uart1_rx_bufs,pVal);
}/*** UART接收空閑回調函數* 當檢測到UART接收線路空閑時,此函數會被調用*/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{if(huart == &huart1){// 將接收到的數據存入環形緩沖區for(int i=0;i<Size;i++){cirble_buf_write(&g_uart1_rx_bufs,g_buf[i]);}// 重新啟動接收,準備接收下一組數據HAL_UARTEx_ReceiveToIdle_DMA(&huart1,g_buf,10);}
}// 回退處理相關變量
static int g_last_char;
static int g_backspace=0;/*** 重定向fputc函數* 實現printf通過UART1發送數據*/
int fputc(int ch,FILE* stream)
{HAL_UART_Transmit(&huart1,(const uint8_t *)&ch,1,10);return ch;
}/*** 重定向fgetc函數* 實現scanf通過UART1接收數據*/
int fgetc(FILE *f)
{int ch;if (g_backspace){g_backspace=0;return g_last_char; // 返回上一個字符(回退功能)}// 阻塞等待,直到從環形緩沖區讀取到數據while(0 != UART1getchar((uint8_t *)&ch));g_last_char = ch; // 保存當前字符,用于回退功能return ch;
}/*** 實現回退功能* 允許程序"撤銷"上一次讀取的字符*/
int __backspace(FILE *stream)
{g_backspace = 1;return 0;
}

?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/91105.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/91105.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/91105.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【Kubernetes 指南】基礎入門——Kubernetes 201(二)

二、滾動升級- 滾動升級&#xff08;Rolling Update&#xff09;通過逐個容器替代升級的方式來實現無中斷的服務升級&#xff1a;- 在滾動升級的過程中&#xff0c;如果發現了失敗或者配置錯誤&#xff0c;還可以隨時回滾&#xff1a;- 需要注意的是&#xff0c; kubectl rolli…

網絡資源模板--基于Android Studio 實現的圖書商城App

目錄 一、測試環境說明 二、項目簡介 三、項目演示 四、部設計詳情&#xff08;部分) 登錄注冊頁 首頁 五、項目源碼 一、測試環境說明 電腦環境 Windows 11 編寫語言 JAVA 開發軟件 Android Studio (2020) 開發軟件只要大于等于測試版本即可(近幾年官網直接下載…

JavaWeb 進階:Vue.js 與 Spring Boot 全棧開發實戰(Java 開發者視角)

作為一名 Java 開發工程師&#xff0c;當你掌握了 HTML、CSS 和 JavaScript 的基礎后&#xff0c;是時候接觸現代前端框架了。Vue.js 以其簡潔的 API、漸進式的設計和優秀的中文文檔&#xff0c;成為眾多 Java 開發者入門前端框架的首選。Vue.js 讓你能快速構建響應式、組件化的…

智能體產品化的關鍵突破:企業智能化轉型的“最后一公里”如何邁過?

智能體產品化的關鍵突破&#xff1a;企業智能化轉型的“最后一公里”如何邁過&#xff1f; 在人工智能迅猛發展的當下&#xff0c;智能體&#xff08;Agent&#xff09;成為企業數字化轉型的新引擎。無論是市場分析、客戶服務&#xff0c;還是自動化辦公&#xff0c;智能體都被…

Rust × Elasticsearch官方 `elasticsearch` crate 上手指南

1 為什么選擇官方 Rust 客戶端&#xff1f; 語義化兼容&#xff1a;客戶端 主版本 與 ES 主版本 嚴格對應&#xff0c;8.x 客戶端可對接任何 8.x 服務器&#xff1b;不存在跨主版本兼容承諾 (docs.rs)100% API 覆蓋&#xff1a;穩定 API 全量映射&#xff0c;Beta/實驗特性可按…

怎樣畫流程圖?符號與流程解構教程

在數字化辦公和項目管理日益復雜的當下&#xff0c;流程圖早已不是工程師、項目經理的專屬工具&#xff0c;它正快速成為每一位職場人提升表達效率、理清工作邏輯的利器。無論是軟件開發中的流程規范、產品設計階段的用戶路徑&#xff0c;還是企業內部的審批流程、團隊協作機制…

vue3 + vite || Vue3 + Webpack創建項目

1.vue3 vite搭建項目方法 &#xff08;需要提前裝node,js&#xff09; 1. 使用官方 create-vite 工具&#xff08;推薦&#xff09; 1.使用npm----------------------------- npm create vuelatest2.使用pnpm----------------------------- pnpm create vuelatest3.使用yarn--…

Vue2-封裝一個含所有表單控件且支持動態增減行列的表格組件

效果1. 無編輯權限&#xff1a;顯示普通表格2. 有編輯權限&#xff1a;根據配置顯示編輯控件3. 可以動態新增行&#xff0c;也可以動態新增列 核心代碼無權限情況的核心代碼<!-- 無編輯權限時顯示普通表格 --><el-tablev-if"!hasEditPermission"ref"ta…

網絡原理 - TCP/IP(一)

目錄 1. 應用層&#xff1a;用戶與網絡的 “交互窗口” 1.1 應用層協議&#xff1a;規范交互的 “通用語言” 1.2 自定義協議&#xff1a;適配特殊需求的 “專屬規則” 1.3 應用層數據格式&#xff1a;讓數據 “說得明白” 1.3.1 XML&#xff1a;結構化但繁瑣的 “老…

Orange的運維學習日記--16.Linux時間管理

Orange的運維學習日記–16. Linux時間管理 文章目錄Orange的運維學習日記--16. Linux時間管理系統與硬件時鐘時鐘類型對比查看內核支持的時鐘源本地時間調整使用 date 查看與設置一次性同步&#xff1a;ntpdate同步到硬件時鐘&#xff1a;hwclock基于 systemd 的 timedatectl交…

Git 與 GitHub 的對比與使用指南

Git 與 GitHub 的對比與使用指南 在軟件開發中&#xff0c;Git 和 GitHub 是兩個密切相關但本質不同的工具。下面我將逐步解釋它們的定義、區別、核心概念以及如何協同使用&#xff0c;確保內容真實可靠&#xff0c;基于廣泛的技術實踐。 1. 什么是 Git&#xff1f; Git 是一個…

20250726-4-Kubernetes 網絡-Service DNS名稱解析_筆記

一、Service DNS名稱 ?1. 例題:通信需求 通信場景:項目A中的Pod需要與項目B中的Pod進行通信,直接使用Pod IP不可行,因為Pod IP會隨著Pod生命周期變化。 解決方案:通過Service提供的穩定IP地址進行通信,不受Pod重建、擴容/縮容等操作影響。 2. CoreDNS介紹 ?? 基本功能…

vscode 登錄ssh記住密碼直接登錄設置

第一種情況在系統已經生成密鑰對的情況下&#xff1a;點擊這里的設置第二步&#xff1a;第三步&#xff1a;沒有填寫的給填寫一下第四步驟&#xff1a;保存后進入選擇這個點開第五步&#xff1a;去Linux終端下輸入這個命令就OK了echo "ssh-rsa內容" >> ~/.ssh/…

Nginx 動靜分離配置(詳細版)

本文介紹了Nginx 動靜分離相關配置&#xff0c;主要包括了配置文件創建、配置示例、配置原理解析以及重新啟用配置文件等等 本文目錄1. 創建 Nginx 配置文件2. 配置示例3. 配置原理解析4. 啟用配置文件并重新加載 Nginx1. 創建 Nginx 配置文件 在 /etc/nginx/sites-available …

C# CAN通信上位機系統設計與實現

C# CAN通信上位機系統設計與實現 C# CAN通信上位機程序&#xff0c;支持多種CAN適配器&#xff0c;提供數據收發、協議解析、數據可視化等功能。 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; usi…

Ubuntu20.04子系統

常用 # 導出分發版到 E盤 wsl --export Ubuntu-20.04 E:\wsl-ubuntu20.04.tar # 注銷原有分發版 wsl --unregister Ubuntu-20.04 # 導入到 E盤的新路徑&#xff08;例如 E:\WSL\Ubuntu-20.04&#xff09; wsl --import Ubuntu-20.04 E:\WSL\Ubuntu-20.04 E:\wsl-ubuntu20.04.t…

【設計模式】狀態模式 (狀態對象(Objects for States))

狀態模式&#xff08;State Pattern&#xff09;詳解一、狀態模式簡介 狀態模式&#xff08;State Pattern&#xff09; 是一種 行為型設計模式(對象行為型模式)&#xff0c;它允許一個對象在其內部狀態改變時改變其行為。換句話說&#xff0c;對象看起來好像修改了它的類。 你…

工業前端組件庫重構心法:如何讓開發效率提升60%的交互模塊設計邏輯

工業前端組件庫重構心法&#xff1a;如何讓開發效率提升60%的交互模塊設計邏輯內容摘要在工業項目開發中&#xff0c;前端組件庫是提升開發效率的關鍵。然而&#xff0c;許多團隊的組件庫存在設計不合理、維護困難等問題&#xff0c;導致開發效率低下。如果能夠重構組件庫&…

leetcode 74. 搜索二維矩陣

二分查找經典題目&#xff1b;根據矩陣的特點&#xff0c;不需要把矩陣拉成一維&#xff0c;二維轉成一維映射關系為a[i]matrix[?i//n?][i%n]&#xff1b;然后開始二分查找&#xff0c;一直二分的收縮區間&#xff1b;class Solution:def searchMatrix(self, matrix: List[Li…

26考研英語詞匯的邏輯筆記(Unit31-43)

行為UNIT 31詞匯數量&#xff1a;274 詞群數量&#xff1a;16 詞群邏輯&#xff1a;行為舉止 | 行為標準與原則 給予、收回 | 接受、允許、讓步、拒絕 促進、鼓勵 | 支持、幫助、資助 破壞相關 | 錯誤、改正 阻礙、打擾相關 | 禁止、阻止、限制 值得、有利、不利相關 | 有意、故…