嵌入式外设
嵌入式外设(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,完成后进中断回调。 |
| DMA | HAL_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_I 是 0x75,加速度起始地址是 0x3B。
MemSize: 寄存器地址的长度。MPU6050 是 8 位寄存器地址,固定填 I2C_MEMADD_SIZE_8BIT。
pData: 数据缓冲区。传入一个 uint8_t 类型的数组或变量地址。
Size: 传输的数据字节数。读 WHO_AM_I 填 1,读完整的加速度+角速度填 14。
Timeout (仅轮询有): 最大等待时间(单位 ms)。通常给个 10 或 100。
二、三种模式的使用(mem模式)
1. 轮询模式 (Polling)
uint8_t id = 0;
// 最稳的方式:等 10ms,读不到就报错
HAL_I2C_Mem_Read(&hi2c2, 0xD0, 0x75, I2C_MEMADD_SIZE_8BIT, &id, 1, 10);
2. 中断模式 (Interrupt)
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)
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) | 非阻塞:需在回调函数中处理后续逻辑。 |
| DMA | HAL_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)
uint8_t reg_addr = 0x75;
uint8_t id = 0;
// 第一步:写。发送我们要读的寄存器地址
HAL_I2C_Master_Transmit(&hi2c2, 0xD0, ®_addr, 1, 10);
// 第二步:读。接收传感器吐回来的数据
HAL_I2C_Master_Receive(&hi2c2, 0xD0, &id, 1, 10);
2. 中断模式 (Interrupt)
这是 Master 模式最麻烦的地方,你需要手动在“发送完”的回调里去“启动读”。
uint8_t reg_addr = 0x75;
uint8_t id_it = 0;
// 启动第一步:发送寄存器地址
HAL_I2C_Master_Transmit_IT(&hi2c2, 0xD0, ®_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 完成。
uint8_t reg_addr = 0x75;
uint8_t id_dma = 0;
// 启动第一步:DMA 发送
HAL_I2C_Master_Transmit_DMA(&hi2c2, 0xD0, ®_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 模式 (分两步):
[Start]+[设备写地址]+[寄存器地址 0x75]+[Stop][Start]+[设备读地址]+[读取数据 0x68]+[Stop]
缺点:中间产生了一个
Stop。在复杂总线上,这一瞬间总线控制权可能被抢走。 -
Mem 模式 (一步到位):
[Start]+[设备写地址]+[寄存器地址 0x75]+[Repeated Start]+[设备读地址]+[读取数据 0x68]+[Stop]
优点:使用重复起始信号,全程霸占总线,通讯极其可靠。
3. 异步模式(中断/DMA)下的开发难度对比
这是决定你笔记含金量的部分:
Master 模式:手动状态机
你要在代码里自己处理“接力棒”。
// 1. 发起写地址任务
HAL_I2C_Master_Transmit_IT(&hi2c2, 0xD0, ®, 1);
// 2. 你必须写两个回调函数来衔接
void HAL_I2C_MasterTxCpltCallback(...) {
// 第一次中断:地址发完了,现在赶紧手动启动读任务
HAL_I2C_Master_Receive_IT(...);
}
void HAL_I2C_MasterRxCpltCallback(...) {
// 第二次中断:数据终于读完了,可以处理数据了
}
Mem 模式:全自动托管
你只需要发起一次任务,HAL 库内部的硬件状态机会帮你跑完所有流程。
// 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 / 超时 / 总线错误 | 错误恢复逻辑 |
判断是哪个外设
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_Transmit | HAL_SPI_Receive | HAL_SPI_TransmitReceive | 阻塞:死等传输完成。 |
| 中断 (IT) | HAL_SPI_Transmit_IT | HAL_SPI_Receive_IT | HAL_SPI_TransmitReceive_IT | 非阻塞:完成后进 Tx/Rx/TxRx 回调。 |
| DMA | HAL_SPI_Transmit_DMA | HAL_SPI_Receive_DMA | HAL_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 / Receive | TransmitReceive (全双工) |
|---|---|---|
| 操作方式 | 先发完,再接收(半双工逻辑)。 | 边发边收(物理层逻辑)。 |
| 效率 | 略低,总线方向切换有微小间隔。 | 最高,充分利用 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错误 | 错误恢复 |
💡 避坑准则
- CS 引脚:一定要记得手动控制!如果 CS 一直拉低,很多 SPI 芯片会进入错误状态。
- SPI 模式 (Phase/Polarity):SPI 有 4 种模式(Mode 0, 1, 2, 3)。如果读出来全是
0xFF或0x00,90% 是时钟极性配错了,请对照手册修改 CubeMX 里的CPOL和CPHA。 - 速度限制: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 个字节进回调。只触发一次,要重新装弹。 |
| DMA | HAL_UART_Receive_DMA(huart, pData, Size) | 非阻塞:DMA 自动搬。 |
| DMA+IDLE | HAL_UARTEx_ReceiveToIdle_DMA(huart, pData, Size) | 最推荐:收到数据进入闲状态自动进回调,不用等满 Size。 |
发送
| 模式 | 发送 API | 特点 |
|---|---|---|
| 轮询 (Polling) | HAL_UART_Transmit(huart, pData, Size, Timeout) | 阻塞:简单直接。 |
| 中断 (IT) | HAL_UART_Transmit_IT(huart, pData, Size) | 非阻塞:发完进回调。 |
| DMA | HAL_UART_Transmit_DMA(huart, pData, Size) | 非阻塞:大量 log 发送首选。 |
参数解析
huart: UART 句柄。比如你用的 USART1,就是&huart1。pData: 数据缓冲区地址。Size: 传输的字节数。Timeout: 仅轮询有,超时时间(ms)。
二、三种模式的使用
1. 轮询模式 (Polling)
最简单的方式:发完/收完才走人。
// 发送(阻塞,发完才返回)
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)
适合接收不定时来的数据(比如蓝牙命令)。注意:每收完一轮要重新装弹。
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 个字节时间没有新数据 → 判定一帧结束 → 进回调。
#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 发送
// 发送(立即返回,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_IT | HAL_UARTEx_ReceiveToIdle_DMA |
|---|---|---|
| 触发条件 | 收满 Size 个字节 | 空闲(1 个字节时间无新数据) |
| 帧长度 | 必须知道 | 不需要知道 |
| 回调函数 | HAL_UART_RxCpltCallback | HAL_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 字节 | 单字节中断接收 |
| 空闲+DMA | HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) | _ReceiveToIdle_DMA 检测到空闲 | 收到不定长数据帧 |
| 传输错误 | HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) | 帧错误 / 噪声 / 溢出 | 错误恢复 |
判断是哪个外设
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
if (huart->Instance == USART1) {
// 调试口
} else if (huart->Instance == USART2) {
// 蓝牙
}
}
💡 避坑准则
- 中断接收是一次性的:
HAL_UART_Receive_IT收完 Size 个字节就停了,必须在回调里重新调用。否则后面的数据全部丢失。 - DMA 发送要等上次完成:
while (huart->gState != HAL_UART_STATE_READY)。不然 buffer 会被覆盖,发出混合数据。 - 关闭半传输中断:DMA + IDLE 模式下,调用
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT)。否则 buffer 填到一半就触发一次回调,数据被提前读走。 - 单字节接收用 1 字节 buffer:
HAL_UART_Receive_IT(&huart, &rx_byte, 1),不要贪心收多个字节。收到一个扔进 ring buffer,再装弹,简单可靠。 - IDLE 检测是"字节间无数据":如果波特率 115200,一个字节约 0.087ms。总线空闲超过 0.087ms 就会触发回调。所以不需要手动判帧尾。