博客
返回文章列表
技术

嵌入式外设

·32

嵌入式外设(hal库)

在hal库里面,带中断和dma的api,都是只开启一次的,你要循环着用的话,还要再次开启

iic读写

一、 核心 API 速查表(mem模式)

模式读操作 API (完整参数签名)写操作 API (完整参数签名)特点
轮询 (Polling)HAL_I2C_Mem_Read(hi2c, DevAddr, MemAddr, MemSize, pData, Size, Timeout)HAL_I2C_Mem_Write(hi2c, DevAddr, MemAddr, MemSize, pData, Size, Timeout)阻塞:自带 Timeout 参数,超时退出。
中断 (IT)HAL_I2C_Mem_Read_IT(hi2c, DevAddr, MemAddr, MemSize, pData, Size)HAL_I2C_Mem_Write_IT(hi2c, DevAddr, MemAddr, MemSize, pData, Size)非阻塞:无 Timeout,完成后进中断回调。
DMAHAL_I2C_Mem_Read_DMA(hi2c, DevAddr, MemAddr, MemSize, pData, Size)HAL_I2C_Mem_Write_DMA(hi2c, DevAddr, MemAddr, MemSize, pData, Size)非阻塞:性能最强,由硬件直接搬运数据。

hi2c: I2C 句柄指针。比如你用的 I2C2,这里就是 &hi2c2

DevAddr: 设备地址。MPU6050 的 7 位地址通常是 0x68,但 HAL 库要求左移一位,即 0xD0

MemAddr: 寄存器地址。比如 WHO_AM_I0x75,加速度起始地址是 0x3B

MemSize: 寄存器地址的长度。MPU6050 是 8 位寄存器地址,固定填 I2C_MEMADD_SIZE_8BIT

pData: 数据缓冲区。传入一个 uint8_t 类型的数组或变量地址。

Size: 传输的数据字节数。读 WHO_AM_I1,读完整的加速度+角速度填 14

Timeout (仅轮询有): 最大等待时间(单位 ms)。通常给个 10100


二、三种模式的使用(mem模式)

1. 轮询模式 (Polling)

c
uint8_t id = 0;
// 最稳的方式:等 10ms,读不到就报错
HAL_I2C_Mem_Read(&hi2c2, 0xD0, 0x75, I2C_MEMADD_SIZE_8BIT, &id, 1, 10);

2. 中断模式 (Interrupt)

c
uint8_t id_it = 0;
// 潇洒的方式:命令发完 CPU 直接走人,传输完了硬件会叫你
HAL_I2C_Mem_Read_IT(&hi2c2, 0xD0, 0x75, I2C_MEMADD_SIZE_8BIT, &id_it, 1);

// 你需要在回调函数里接收结果
void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c) {
    if (hi2c->Instance == I2C2) {
        // 这时候 id_it 里的值才是 0x68
    }
}

3. DMA 模式 (DMA)

c
uint8_t id_dma = 0;
// 最强悍的方式:CPU 连个招呼都不打,DMA 直接把 0x68 搬到 id_dma 的内存里
HAL_I2C_Mem_Read_DMA(&hi2c2, 0xD0, 0x75, I2C_MEMADD_SIZE_8BIT, &id_dma, 1);

// 回调函数和中断共用同一个
void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c) {
    if (hi2c->Instance == I2C2) {
        // id_dma 已准备就绪
    }
}

三、核心 API 速查表(master模式)

模式读操作 API (完整参数签名)写操作 API (完整参数签名)特点
轮询 (Polling)HAL_I2C_Master_Receive(hi2c, DevAddr, pData, Size, Timeout)HAL_I2C_Master_Transmit(hi2c, DevAddr, pData, Size, Timeout)阻塞:简单直接,没有寄存器地址参数。
中断 (IT)HAL_I2C_Master_Receive_IT(hi2c, DevAddr, pData, Size)HAL_I2C_Master_Transmit_IT(hi2c, DevAddr, pData, Size)非阻塞:需在回调函数中处理后续逻辑。
DMAHAL_I2C_Master_Receive_DMA(hi2c, DevAddr, pData, Size)HAL_I2C_Master_Transmit_DMA(hi2c, DevAddr, pData, Size)非阻塞:最省 CPU,适合大数据量传输。

参数和mem模式相同


四、三种模式的使用(master模式)

特别注意:由于 Master 模式不识别寄存器,所以读取 MPU6050 的 WHO_AM_I (0x75) 必须分成两步走:先“告诉”地址,再“接收”数据。

1. 轮询模式 (Polling)

c
uint8_t reg_addr = 0x75;
uint8_t id = 0;

// 第一步:写。发送我们要读的寄存器地址
HAL_I2C_Master_Transmit(&hi2c2, 0xD0, &reg_addr, 1, 10);
// 第二步:读。接收传感器吐回来的数据
HAL_I2C_Master_Receive(&hi2c2, 0xD0, &id, 1, 10);

2. 中断模式 (Interrupt)

这是 Master 模式最麻烦的地方,你需要手动在“发送完”的回调里去“启动读”。

c
uint8_t reg_addr = 0x75;
uint8_t id_it = 0;

// 启动第一步:发送寄存器地址
HAL_I2C_Master_Transmit_IT(&hi2c2, 0xD0, &reg_addr, 1);

// 你需要处理两个回调:
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c) {
    if (hi2c->Instance == I2C2) {
        // 第一步发完了,赶紧启动第二步:接收数据
        HAL_I2C_Master_Receive_IT(hi2c, 0xD0, &id_it, 1);
    }
}

void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c) {
    if (hi2c->Instance == I2C2) {
        // 这时候 id_it 终于拿到值了 (0x68)
    }
}

3. DMA 模式 (DMA)

逻辑与中断类似,但搬运过程由 DMA 完成。

c
uint8_t reg_addr = 0x75;
uint8_t id_dma = 0;

// 启动第一步:DMA 发送
HAL_I2C_Master_Transmit_DMA(&hi2c2, 0xD0, &reg_addr, 1);

// 回调函数处理(代码与 IT 模式几乎一致,只需把启动函数换成 _DMA 后缀)
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c) {
    if (hi2c->Instance == I2C2) {
        HAL_I2C_Master_Receive_DMA(hi2c, 0xD0, &id_dma, 1);
    }
}

五、两种模式的区别

1. 核心逻辑差异:谁在干活?

维度Master 模式 (Master_Transmit/Receive)Mem 模式 (Mem_Read/Write)
设计逻辑面向数据流。它不知道“寄存器”为何物,只负责搬运字节。面向传感器。它理解“先发地址、再读数据”的硬件逻辑。
通讯连贯性断开式。写地址和读数据是两个独立的任务。一气呵成。发送寄存器地址后,直接发送 Repeated Start
回调逻辑多级跳转。发完地址进一次回调,读完数据再进一次回调。单次完成。无论中间切了几次方向,只在最终完成后进一次回调。

2. 通讯时序对比(以读取 WHO_AM_I 为例)

这是它们在总线(SDA/SCL)上跑出来的真实样子:

  • Master 模式 (分两步):

    1. [Start] + [设备写地址] + [寄存器地址 0x75] + [Stop]
    2. [Start] + [设备读地址] + [读取数据 0x68] + [Stop]

    缺点:中间产生了一个 Stop。在复杂总线上,这一瞬间总线控制权可能被抢走。

  • Mem 模式 (一步到位):

    1. [Start] + [设备写地址] + [寄存器地址 0x75] + [Repeated Start] + [设备读地址] + [读取数据 0x68] + [Stop]

    优点:使用重复起始信号,全程霸占总线,通讯极其可靠。

3. 异步模式(中断/DMA)下的开发难度对比

这是决定你笔记含金量的部分:

Master 模式:手动状态机

你要在代码里自己处理“接力棒”。

c
// 1. 发起写地址任务
HAL_I2C_Master_Transmit_IT(&hi2c2, 0xD0, &reg, 1);

// 2. 你必须写两个回调函数来衔接
void HAL_I2C_MasterTxCpltCallback(...) {
    // 第一次中断:地址发完了,现在赶紧手动启动读任务
    HAL_I2C_Master_Receive_IT(...); 
}
void HAL_I2C_MasterRxCpltCallback(...) {
    // 第二次中断:数据终于读完了,可以处理数据了
}
Mem 模式:全自动托管

你只需要发起一次任务,HAL 库内部的硬件状态机会帮你跑完所有流程。

c
// 1. 发起读寄存器任务
HAL_I2C_Mem_Read_IT(&hi2c2, 0xD0, 0x75, 1, &data, 1);

// 2. 你只需要一个回调函数
void HAL_I2C_MemRxCpltCallback(...) {
    // 唯一的一次中断:整个“发地址+读数据”的过程全完了,直接拿数据
}

六、回调函数

事件函数名触发条件常用场景
主机发送完成HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)_IT_DMA 发送完毕Master 模式两步读(发地址后启动读)
主机接收完成HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c)_IT_DMA 接收完毕Master 模式收到数据
内存读完成HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c)Mem_Read _IT_DMA 完成直接读取传感器寄存器
内存写完成HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c)Mem_Write _IT_DMA 完成配置传感器寄存器后处理
传输错误HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c)NACK / 超时 / 总线错误错误恢复逻辑

判断是哪个外设

c
void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c) {
    if (hi2c->Instance == I2C1) {
        // OLED 那边的回调
    } else if (hi2c->Instance == I2C2) {
        // MPU6050 那边的回调
    }
}

SPI读写

注意:SPI 传输速度极快,但 HAL 库默认不控制 CS(片选)引脚。在调用读写函数前,必须手动拉低 CS,结束后手动拉高。

一、 核心 API 速查表

模式发送 API (Transmit)接收 API (Receive)同时收发 API (TransmitReceive)特点
轮询 (Polling)HAL_SPI_TransmitHAL_SPI_ReceiveHAL_SPI_TransmitReceive阻塞:死等传输完成。
中断 (IT)HAL_SPI_Transmit_ITHAL_SPI_Receive_ITHAL_SPI_TransmitReceive_IT非阻塞:完成后进 Tx/Rx/TxRx 回调。
DMAHAL_SPI_Transmit_DMAHAL_SPI_Receive_DMAHAL_SPI_TransmitReceive_DMA性能王:大数据量首选,CPU 占用极低。

参数解析

  • hspi: SPI 句柄。如 &hspi1
  • pData / pTxData / pRxData: 数据缓冲区地址。
  • Size: 传输的字节数。
  • Timeout: 仅轮询模式有,超时时间(ms)。

二、 三种模式的使用

1. 轮询模式 (Polling)

这是读写 SPI 传感器(如 W25Q128 存储器或 SPI 接口的 MPU6050)最常用的方式。

uint8_t tx_data = 0x9F; // 读取 ID 指令
uint8_t rx_data[3];

// 1. 拉低片选 (开始通讯)
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);

// 2. 发送指令
HAL_SPI_Transmit(&hspi1, &tx_data, 1, 10);
// 3. 接收数据
HAL_SPI_Receive(&hspi1, rx_data, 3, 10);

// 4. 拉高片选 (结束通讯)
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);

2. 中断模式 (Interrupt)

适合在后台传输少量数据。

// 发起异步传输
HAL_SPI_TransmitReceive_IT(&hspi1, tx_buf, rx_buf, 10);

// 回调函数
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) {
    if (hspi->Instance == SPI1) {
        // 传输完成,拉高 CS 引脚
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
    }
}

3. DMA 模式 (DMA)

刷屏(SPI LCD)或读取大量存储数据时的神技。

// 发起 DMA 传输
HAL_SPI_Transmit_DMA(&hspi1, long_buffer, 1024);

// 回调函数
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
    // 逻辑同上
}

三、 SPI 的精髓:全双工 (TransmitReceive)

这是新手最容易懵的地方。SPI 的硬件特性是:每发送一个字节,必然会同时收到一个字节。

  • 如果你只调用 HAL_SPI_Receive,HAL 库内部其实是在发送“垃圾数据(0xFF 或 0x00)”来换取从机的数据。
  • 如果你想在发送指令的同时观察从机的反馈,必须使用 HAL_SPI_TransmitReceive

四、 两种接口模式对比(Transmit/Receive vs TransmitReceive)

维度Transmit / ReceiveTransmitReceive (全双工)
操作方式先发完,再接收(半双工逻辑)。边发边收(物理层逻辑)。
效率略低,总线方向切换有微小间隔。最高,充分利用 SPI 双向带宽。
使用难度简单,符合人类直觉。略难,需处理发送与接收缓冲区的对应关系。
推荐场景读写简单的传感器寄存器。两个单片机通讯、高速数据交换。

五、回调函数

事件函数名触发条件常用场景
发送完成HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)_IT_DMA 发送完毕发完命令后拉高 CS
接收完成HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)_IT_DMA 接收完毕收到数据后拉高 CS
收发完成HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)TransmitReceive _IT_DMA 完成全双工通讯(最常用)
传输错误HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi)溢出 / CRC错误错误恢复

💡 避坑准则

  1. CS 引脚:一定要记得手动控制!如果 CS 一直拉低,很多 SPI 芯片会进入错误状态。
  2. SPI 模式 (Phase/Polarity):SPI 有 4 种模式(Mode 0, 1, 2, 3)。如果读出来全是 0xFF0x00,90% 是时钟极性配错了,请对照手册修改 CubeMX 里的 CPOLCPHA
  3. 速度限制:SPI 速度极快,但如果杜邦线太长(超过 20cm),请把 Baud Rate 调低,否则信号会失真。

UART读写

UART 没有"寄存器地址"概念,不像 I2C 的 mem 模式。它只关心收发字节流。

接收能力比 I2C 多一个特有模式:DMA + IDLE 空闲检测,适合接收不定长数据(比如蓝牙命令、串口调试)。

一、 核心 API 速查表

接收

模式接收 API特点
轮询 (Polling)HAL_UART_Receive(huart, pData, Size, Timeout)阻塞:死等数据。
中断 (IT)HAL_UART_Receive_IT(huart, pData, Size)非阻塞:收完 Size 个字节进回调。只触发一次,要重新装弹。
DMAHAL_UART_Receive_DMA(huart, pData, Size)非阻塞:DMA 自动搬。
DMA+IDLEHAL_UARTEx_ReceiveToIdle_DMA(huart, pData, Size)最推荐:收到数据进入闲状态自动进回调,不用等满 Size。

发送

模式发送 API特点
轮询 (Polling)HAL_UART_Transmit(huart, pData, Size, Timeout)阻塞:简单直接。
中断 (IT)HAL_UART_Transmit_IT(huart, pData, Size)非阻塞:发完进回调。
DMAHAL_UART_Transmit_DMA(huart, pData, Size)非阻塞:大量 log 发送首选。

参数解析

  • huart: UART 句柄。比如你用的 USART1,就是 &huart1
  • pData: 数据缓冲区地址。
  • Size: 传输的字节数。
  • Timeout: 仅轮询有,超时时间(ms)。

二、三种模式的使用

1. 轮询模式 (Polling)

最简单的方式:发完/收完才走人。

c
// 发送(阻塞,发完才返回)
char msg[] = "Hello";
HAL_UART_Transmit(&huart1, (uint8_t *)msg, 5, 100);

// 接收(阻塞,收满指定字节数才返回)
uint8_t buf[10];
HAL_UART_Receive(&huart2, buf, 10, 100);  // 等 10 个字节,超时 100ms

2. 中断模式 (Interrupt)

适合接收不定时来的数据(比如蓝牙命令)。注意:每收完一轮要重新装弹

c
static uint8_t rx_byte;  // 必须 static!

// 启动接收(只收 1 个字节,收完自动停)
HAL_UART_Receive_IT(&huart2, &rx_byte, 1);

// 回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART2) {
        // rx_byte 里有数据了
        ringbuffer_write(&ring, &rx_byte, 1);  // 存到缓冲区

        // 重新装弹!不装的话后面的数据就丢了
        HAL_UART_Receive_IT(&huart2, &rx_byte, 1);
    }
}

3. DMA + IDLE 模式 (DMA)

适合大量不定长数据(调试口收 log、串口协议帧)。这是 UART 特有的模式,I2C/SPI 没有。

IDLE 空闲检测的原理:硬件检测到总线上连续 1 个字节时间没有新数据 → 判定一帧结束 → 进回调。

c
#define DMA_BUF_SIZE 128
#define RING_BUF_SIZE 256

static uint8_t dma_rx_buf[DMA_BUF_SIZE];  // DMA 写入的缓冲区(必须 static)
static uint8_t ring_mem[RING_BUF_SIZE];
static ringbuffer_st rx_ring;

extern DMA_HandleTypeDef hdma_usart1_rx;

// 启动
void APP_UART_Init(void)
{
    ringbuffer_init(&rx_ring, ring_mem, RING_BUF_SIZE);

    HAL_UARTEx_ReceiveToIdle_DMA(&huart1, dma_rx_buf, DMA_BUF_SIZE);

    // 关闭半传输中断(只要完整帧,不要半帧通知)
    __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);
}

// 回调
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if (huart->Instance == USART1)
    {
        // Size = 本次实际收到的字节数
        ringbuffer_write(&rx_ring, dma_rx_buf, Size);

        // 重新装弹
        HAL_UARTEx_ReceiveToIdle_DMA(&huart1, dma_rx_buf, DMA_BUF_SIZE);
        __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);
    }
}

DMA 发送

c
// 发送(立即返回,DMA 后台搬运)
char msg[] = "data";
HAL_UART_Transmit_DMA(&huart1, (uint8_t *)msg, 4);

// 注意:DMA 还在发的时候,不能修改 msg 的内容!
// 如果连续发,要等上一次完成
while (huart1.gState != HAL_UART_STATE_READY);  // 等 DMA 空闲
// 现在可以写新的数据了

三、两种接收方式对比

维度HAL_UART_Receive_ITHAL_UARTEx_ReceiveToIdle_DMA
触发条件收满 Size 个字节空闲(1 个字节时间无新数据)
帧长度必须知道不需要知道
回调函数HAL_UART_RxCpltCallbackHAL_UARTEx_RxEventCallback
数据搬运CPU(中断里执行 LDR/STR)DMA(硬件自动搬)
常用方式每次收 1 字节,堆到 ring buffer收一轮自动进回调
典型场景蓝牙模块调试口、GPS 模块

四、回调函数

事件函数名触发条件常用场景
发送完成HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)_IT_DMA 发送完毕DMA 发送后释放 buffer
接收完成HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)_IT 收满 Size 字节单字节中断接收
空闲+DMAHAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)_ReceiveToIdle_DMA 检测到空闲收到不定长数据帧
传输错误HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)帧错误 / 噪声 / 溢出错误恢复

判断是哪个外设

c
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
    if (huart->Instance == USART1) {
        // 调试口
    } else if (huart->Instance == USART2) {
        // 蓝牙
    }
}

💡 避坑准则

  1. 中断接收是一次性的HAL_UART_Receive_IT 收完 Size 个字节就停了,必须在回调里重新调用。否则后面的数据全部丢失。
  2. DMA 发送要等上次完成while (huart->gState != HAL_UART_STATE_READY)。不然 buffer 会被覆盖,发出混合数据。
  3. 关闭半传输中断:DMA + IDLE 模式下,调用 __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT)。否则 buffer 填到一半就触发一次回调,数据被提前读走。
  4. 单字节接收用 1 字节 bufferHAL_UART_Receive_IT(&huart, &rx_byte, 1),不要贪心收多个字节。收到一个扔进 ring buffer,再装弹,简单可靠。
  5. IDLE 检测是"字节间无数据":如果波特率 115200,一个字节约 0.087ms。总线空闲超过 0.087ms 就会触发回调。所以不需要手动判帧尾

相关文章