STM32-HAL--F429-HAL 库驱动框架(2026/7/22)
目录
HAL 库驱动框架(完整代码示例)
第一层:句柄结构体
第二层:初始化(Init → MspInit)
2.1 HAL_UART_MspInit —— 你写的"硬件接线"
2.2 My_UART1_Init —— 你的总初始化
2.3 My_UART1_DMA_Init —— 用 DMA 模式时才调
第三层:三种操作模式
③-① 阻塞轮询 Polling
③-② 中断 IT
③-③ DMA
③-④ 辅助操作
main 函数总入口
三模式速查
句柄六成员速查
HAL 库驱动框架(完整代码示例)
平台:STM32F429 + HAL 库 | 外设:USART1(PA9=TX, PA10=RX)
覆盖脑图三个分支:句柄 → 初始化 → 三种操作模式
第一层:句柄结构体
xx_HandleTypeDef= Instance + Init + Hdma + Lock + State(6 个成员,你填 2 个 + HAL 管 3 个 + DMA 时填 1 个)
UART_HandleTypeDef huart1; // 一张"点菜单",6 个成员一览: // huart1.Instance → USART_TypeDef * ← 【你要填】指向 USART1 寄存器基址 0x40011000 // huart1.Init → UART_InitTypeDef ← 【你要填】波特率/停止位/校验等参数 // huart1.hdmatx → DMA_HandleTypeDef * ← 【用 DMA 才填】DMA 发送通道句柄 // huart1.hdmarx → DMA_HandleTypeDef * ← 【用 DMA 才填】DMA 接收通道句柄 // huart1.Lock → HAL_LockTypeDef ← 【HAL 库自己管】你没填过,但它一直在变 // huart1.State → HAL_UART_StateTypeDef ← 【HAL 库自己管】READY/BUSY/ERROR 自己流转 /* DMA 句柄——只有用 DMA 模式时才需要 */ DMA_HandleTypeDef hdma_usart1_tx; // TX 用的 DMA 通道 DMA_HandleTypeDef hdma_usart1_rx; // RX 用的 DMA 通道 /* 数据缓冲 */ uint8_t tx_buf[64]; uint8_t rx_buf[64];
第二层:初始化(Init → MspInit)
调用链:你调HAL_UART_Init→ 库内部调HAL_UART_MspInit→ 库内部设 Lock/State
2.1 HAL_UART_MspInit —— 你写的"硬件接线"
放在stm32f4xx_hal_msp.c。你不需要手动调它——HAL_UART_Init内部会自动调用。
void HAL_UART_MspInit(UART_HandleTypeDef *huart) { if (huart != &huart1) return; GPIO_InitTypeDef GPIO_Init = {0}; /* 1. 开时钟 */ __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_DMA2_CLK_ENABLE(); // DMA 也要开时钟! /* 2. 配引脚 */ // PA9 — TX GPIO_Init.Pin = GPIO_PIN_9; GPIO_Init.Mode = GPIO_MODE_AF_PP; GPIO_Init.Pull = GPIO_PULLUP; GPIO_Init.Speed = GPIO_SPEED_FREQ_HIGH; GPIO_Init.Alternate = GPIO_AF7_USART1; HAL_GPIO_Init(GPIOA, &GPIO_Init); // PA10 — RX GPIO_Init.Pin = GPIO_PIN_10; GPIO_Init.Alternate = GPIO_AF7_USART1; HAL_GPIO_Init(GPIOA, &GPIO_Init); /* 3. 配中断(IT 和 DMA 模式都需要) */ HAL_NVIC_SetPriority(USART1_IRQn, 0, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); /* 4. 配 DMA 中断(DMA 模式才需要) */ HAL_NVIC_SetPriority(DMA2_Stream7_IRQn, 0, 0); // USART1_TX 用的 DMA 流 HAL_NVIC_EnableIRQ(DMA2_Stream7_IRQn); HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 0, 0); // USART1_RX 用的 DMA 流 HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn); }2.2 My_UART1_Init —— 你的总初始化
HAL_UART_Init内部除了调MspInit,还偷偷设了Lock=UNLOCKED、State=READY。这两个字段你一行没写,但 HAL 靠它们保证不乱套。
void My_UART1_Init(void) { /* ===== 第一步:填 Instance ===== */ huart1.Instance = USART1; // "管的是 USART1 这个硬件" // 等价于:把这个句柄和 USART1 的寄存器基址(0x40011000)绑定 /* ===== 第二步:填 Init ===== */ huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; // 这些值最终会被 HAL_UART_Init 写进 USART1 的控制寄存器(CR1/CR2/CR3) /* HAL_UART_Init 内部做了: * ① 调 HAL_UART_MspInit → 开时钟、配引脚、配中断 * ② 写 Init 参数到寄存器 → 波特率/停止位等生效 * ③ 设 Lock = UNLOCKED → 初始状态"没锁" * ④ 设 State = READY → 标记外设"就绪,可以干活" */ HAL_UART_Init(&huart1); // 至此,Lock 和 State 已由 HAL 库设好,你完全不需要关心它们的初始值 }2.3 My_UART1_DMA_Init —— 用 DMA 模式时才调
填的是 DMA 句柄自己的 Instance 和 Init,然后把它的指针挂到huart1.hdmatx/hdmarx上。
void My_UART1_DMA_Init(void) { /* ===== TX DMA ===== */ hdma_usart1_tx.Instance = DMA2_Stream7; // USART1_TX → DMA2 流 7 hdma_usart1_tx.Init.Channel = DMA_CHANNEL_4; // 通道 4 hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;// 内存→外设 hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不变 hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE; // 内存地址递增 hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_usart1_tx.Init.Mode = DMA_NORMAL; // 单次模式 hdma_usart1_tx.Init.Priority = DMA_PRIORITY_MEDIUM; HAL_DMA_Init(&hdma_usart1_tx); // 内部也会调 HAL_DMA_MspInit /* ===== RX DMA ===== */ hdma_usart1_rx.Instance = DMA2_Stream2; // USART1_RX → DMA2 流 2 hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4; hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; // 外设→内存 hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE; hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_usart1_rx.Init.Mode = DMA_NORMAL; hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_usart1_rx); /* 把 DMA 句柄挂到 UART 句柄上——这就是"什么时候填 Hdma"的答案 */ huart1.hdmatx = &hdma_usart1_tx; huart1.hdmarx = &hdma_usart1_rx; __HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx); // 本质就是指针赋值 __HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx); }第三层:三种操作模式
选哪种?数据量小 + 时间短 → Polling | 不定长/不想等 → IT | 大批量搬运 → DMA
③-① 阻塞轮询 Polling
CPU 发出命令 → 原地死等 → 等到完或超时才走。 特征:函数名无_IT无_DMA,必带Timeout参数。
void Polling_Example(void) { HAL_StatusTypeDef ret; /* 发送 6 字节 */ strcpy((char *)tx_buf, "Hello\r\n"); ret = HAL_UART_Transmit(&huart1, tx_buf, 6, 1000); // 句柄 数据 长度 超时(ms) // // CPU 卡在这 1000ms 内,等所有字节发完才走。 // 正常发完 → 返回 HAL_OK, State 恢复 READY // 超时 → 返回 HAL_TIMEOUT, State 恢复 READY /* 接收 10 字节——还是死等 */ ret = HAL_UART_Receive(&huart1, rx_buf, 10, 1000); // 等 1000ms 内收齐 10 个字节,收不齐就超时。 // 收齐后直接读 rx_buf 就行——数据已经到了。 }HAL 内部对 Lock/State 的操作(你看不到,但它在做):
┌─ HAL_UART_Transmit 入口 ────────────────┐ │ if (huart->State != READY) return BUSY;│ ← Lock/State 起作用了 │ huart->State = BUSY_TX; │ ← HAL 自己改的 │ while (没发完) { │ ← 这儿等 Timeout │ 往 DR 寄存器塞一个字节; │ │ if (超时) { State=READY; 返回; } │ │ } │ │ huart->State = READY; │ ← 发完,恢复就绪 └─────────────────────────────────────────┘③-② 中断 IT
CPU 发起 → 立刻返回干别的 → 硬件触发中断 → HAL 搬数据 → 回调通知你。 特征:函数名以_IT结尾,没有Timeout参数。
启动(主循环里调一次):
void IT_Example_Start(void) { HAL_StatusTypeDef ret; ret = HAL_UART_Receive_IT(&huart1, rx_buf, 10); // 句柄 缓冲 收 10 字节 // // 注意:没有 Timeout! // 这行执行完 CPU 就自由了,什么时候收齐,什么时候进回调。 if (ret != HAL_OK) { // 启动失败(上次还没收完——State 不是 READY,被 HAL 挡了) } }中断服务函数(放在 stm32f4xx_it.c):
void USART1_IRQHandler(void) { /* HAL 库内部做了这些事(你不用管): * - 检查 RXNE(收到一个字节?) * - 读 DR 寄存器 → 放进 rx_buf * - 计数器-1,看收够了没 * - 收够了 → 关中断 → State=READY → RxCpltCallback() */ HAL_UART_IRQHandler(&huart1); }回调(数据到手,在这儿处理):
/* 接收完成 */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart1) { // 数据已在 rx_buf,处理完再启动下一轮 HAL_UART_Receive_IT(&huart1, rx_buf, 10); } } /* 发送完成 */ void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart1) { // 上一批发完了,可以发下一批 } } /* 出错 */ void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { if (huart == &huart1) { // State 已被 HAL 改成 ERROR,重启接收即可 HAL_UART_Receive_IT(&huart1, rx_buf, 10); } }IT 模式 HAL 内部 State 流转:
主循环: Receive_IT → State=BUSY_RX → 开 RXNE 中断 → 立刻返回 中断里: IRQHandler → 清标志/搬数据 → 收够了 → 关中断 → State=READY → RxCpltCallback()
③-③ DMA
CPU 发起 → DMA 自己搬数据(CPU 完全不管)→ 搬完触发 DMA 中断 → 回调。 特征:函数名以_DMA结尾,没有Timeout。
和 IT 的关键区别:IT 每个字节打断 CPU 一次;DMA 整批搬完只打断一次。
void DMA_Example_Start(void) { /* 前提:My_UART1_DMA_Init() 已经把 hdmatx/hdmarx 挂到 huart1 上了 */ /* DMA 发送 */ strcpy((char *)tx_buf, "Large block via DMA...\r\n"); HAL_UART_Transmit_DMA(&huart1, tx_buf, 64); // HAL 内部: // ① 检查 State==READY && Lock==UNLOCKED // ② 设 State=BUSY_TX, Lock=LOCKED // ③ 配 DMA(源=tx_buf, 目标=USART1->DR, 长度=64) // ④ 启动 DMA → 立即返回, CPU 自由 // ⑤ 搬完 → DMA 中断 → IRQHandler → TxCpltCallback // ⑥ 回调里 State=READY, Lock=UNLOCKED /* DMA 接收 */ HAL_UART_Receive_DMA(&huart1, rx_buf, 64); }DMA 中断服务函数(stm32f4xx_it.c):
void DMA2_Stream7_IRQHandler(void) // TX DMA { HAL_DMA_IRQHandler(&hdma_usart1_tx); // 搬完 → 调 HAL_UART_TxCpltCallback } void DMA2_Stream2_IRQHandler(void) // RX DMA { HAL_DMA_IRQHandler(&hdma_usart1_rx); // 搬完 → 调 HAL_UART_RxCpltCallback(和 IT 共用同一个回调!) }回调——DMA 和 IT 共用的,不用区分:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart1) { // 不管 IT 还是 DMA 搬完的,数据都在 rx_buf,统一处理 HAL_UART_Receive_DMA(&huart1, rx_buf, 64); // 再开下一轮 } }③-④ 辅助操作
void Misc_Operations(void) { /* 查询标志 */ if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) { /* 有数据来了 */ } if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC)) { /* 发送完成 */ } /* 清除标志 */ __HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_TC); /* 开关中断 */ __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE); // 开接收中断 __HAL_UART_DISABLE_IT(&huart1, UART_IT_TC); // 关发送完成中断 /* 查询状态(读的就是句柄里的 State 字段) */ if (huart1.State == HAL_UART_STATE_READY) { /* 空闲,可以干活 */ } }main 函数总入口
int main(void) { HAL_Init(); SystemClock_Config(); /* 初始化——填表 + 硬件接线一步到位 */ My_UART1_Init(); /* 如果要测 DMA,再加这行 */ My_UART1_DMA_Init(); /* * 三种模式选一种: * Polling_Example(); // 简单快速,CPU 等着 * IT_Example_Start(); // CPU 不卡,收完进回调 * DMA_Example_Start(); // 大批量搬运,CPU 完全不管 */ IT_Example_Start(); // 假设选中断模式——启动一次即可 while (1) { // 主循环跑业务逻辑 // 数据来了自动进回调,不用在这儿轮询 } }三模式速查
| 你调的函数 | 谁搬数据 | 你什么时候拿到数据 | Timeout | CPU 占用 | |
|---|---|---|---|---|---|
| Polling | Transmit/Receive | CPU 等着搬 | 函数返回时 | ✅ 有 | 全程占着 |
| IT | xxx_IT | HAL 在 ISR 里搬 | 回调里 | ❌ 无 | 每字节打断一次 |
| DMA | xxx_DMA | DMA 自己搬 | 回调里(同 IT) | ❌ 无 | 几乎不占 |
句柄六成员速查
| 成员 | 谁填 | 什么时候 |
|---|---|---|
Instance | 你 | 初始化时 |
Init | 你 | 初始化时 |
hdmatx/hdmarx | 你(DMA 时) | My_DMA_Init里挂上 |
Lock | HAL 库 | 每次操作前后自动改 |
State | HAL 库 | 每次操作前后自动改 |
