目录
[HAL 库驱动框架(完整代码示例)](#HAL 库驱动框架(完整代码示例))
[第二层:初始化(Init → MspInit)](#第二层:初始化(Init → MspInit))
[2.1 HAL_UART_MspInit ------ 你写的"硬件接线"](#2.1 HAL_UART_MspInit —— 你写的"硬件接线")
[2.2 My_UART1_Init ------ 你的总初始化](#2.2 My_UART1_Init —— 你的总初始化)
[2.3 My_UART1_DMA_Init ------ 用 DMA 模式时才调](#2.3 My_UART1_DMA_Init —— 用 DMA 模式时才调)
[③-① 阻塞轮询 Polling](#③-① 阻塞轮询 Polling)
[③-② 中断 IT](#③-② 中断 IT)
[③-③ DMA](#③-③ DMA)
[③-④ 辅助操作](#③-④ 辅助操作)
[main 函数总入口](#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 库 | 每次操作前后自动改 |