本文最后更新于 2025-05-30,学习久了要注意休息哟

第一章 通讯基础概念

通讯是指两个或多个设备之间进行信息传输的过程。在嵌入式系统中,常见的通讯方式有串行通信、并行通信、同步通信、异步通信等。本章将介绍通讯的基本原理及常用术语,为后续章节打下基础。

1.1 通讯的基本要素

一个完整的通讯过程通常包括以下几个基本要素:

  • 发送端:发出数据的设备。

  • 接收端:接收数据的设备。

  • 信道(介质):传输数据的物理通路,如导线、光纤或无线电波。

  • 协议:通信双方约定好的规则,如波特率、校验方式等。

常见通讯协议包括:

  • TCP/IP:网络通信协议,广泛应用于互联网。

  • Modbus:常用于工业控制系统中的通讯协议。

1.2 通讯方式

1.2.1 串行通讯和并行通讯

1、串行通讯

  • 数据一位一位按顺序传输,只需一条数据线。

  • 优点:线路简单,抗干扰强,适合远距离传输

  • 缺点:传输速度相对较慢。

常见应用:UART、USART、SPI、I2C 等。

2、并行通讯

  • 数据多位同时传输,每位使用一条数据线。

  • 优点:速度快,适合短距离、高速传输。

  • 缺点:线路多,布线复杂,干扰大。

常见应用:计算机总线、LCD 并口通信等。

1.2.2 同步通讯和异步通讯

1、同步通讯

  • 通信双方共享同一个时钟信号,数据与时钟同步。

  • 发送和接收严格按照时钟节拍进行。

  • 通常适用于速率较高的协议。

常见应用:USART、SPI、I2C、CAN 等。

2、异步通讯

  • 不依赖共享时钟,使用起始位、停止位标识数据帧。

  • 通信双方只需约定波特率即可。

  • 实现简单,适用于低速或控制类通信。

常见应用:UART 。

1.2.3 单工、半双工和全双工

1、单工通信

  • 数据只能单向传输,接收端不能发送数据。

  • 示例:电视广播、收音机。

2、半双工通信

  • 数据可以双向传输,但同一时刻只能一个方向传输

  • 需要轮流发送和接收。

  • 示例:对讲机、无线电台。

3、全双工通信

  • 数据可以同时双向传输,互不干扰。

  • 效率高,但实现复杂。

  • 示例:电话通信、USART 全双工模式。

1.4 通讯参数与术语

  • 波特率(Baud Rate):每秒传输的比特数,单位 bps。

  • 数据位:每帧数据中有效数据的位数,通常为 8 位。

  • 停止位:用于标识一帧数据的结束,常见为 1 位或 2 位。

  • 校验位:用于检查数据传输是否出错,可选奇偶校验。

1.4.1 波特率

波特率表示每秒钟传输的比特数(bps)。常见的波特率有:

  • 9600bps:常见的串口通信速率。

  • 115200bps:用于高速数据传输。

  • 250000bps:用于某些特定应用,如 CAN 总线。

1.4.2 数据位

数据位表示每个数据帧中有效数据的位数,通常为 8 位。常见的数据位设置有:

  • 5 位:适用于一些简单的通信协议。

  • 8 位:广泛应用于串行通信中。

  • 9 位:广泛应用于串行通信中。

1.4.3 停止位

停止位用于标识数据帧的结束,常见的停止位设置有:

  • 1 位:较为常见的设置。

  • 2 位:在高速通信时提高数据稳定性。

1.4.4 校验位

校验位用于检查数据传输是否出错,常见的校验方式有:

  • 无校验:不进行校验,传输速度较快。

  • 奇校验:在数据位中添加一个额外的校验位,使得数据帧中 1 的个数保持奇数

    • 如果数据中已经有奇数个 1,那么校验位设置为 0;
    • 如果已有偶数个 1,则校验位设置为 1。
  • 偶校验:与奇校验类似,但此时校验位的设置确保数据帧中 1 的个数保持偶数

    • 如果数据中已有偶数个 1,校验位设置为 0;
    • 如果已有奇数个 1,则校验位设置为 1。

1.4.5 硬件控制流

硬件流控制通过物理信号线动态管理数据传输节奏,避免因收发双方处理速度不一致导致的数据丢失或溢出。核心是通过 RTS(Request to Send) 和 CTS(Clear to Send) 信号实现。

1、关键概念

  • RTS(请求发送)
    由发送端发出,表示已准备好发送数据。
    高电平:允许发送;低电平:请求暂停(接收方未就绪)。

  • CTS(允许发送)
    由接收端发出,表示是否可接收数据。
    高电平:允许发送;低电平:要求发送方暂停。

2、工作流程

  1. 发送方置高 RTS,询问接收方是否就绪。

  2. 接收方若缓冲区有空闲,置高 CTS,允许发送。

  3. 发送方检测到 CTS 为高电平后开始传输数据。

  4. 若接收方缓冲区将满,拉低 CTS,发送方立即停止传输。

1.5 常见通讯方式

1.5.1 UART通用异步收发器

  • 特点:

    • 异步通信,无需时钟信号

    • 使用起始位、停止位、校验位进行帧同步

    • 通信稳定,适合低速传输与远距离通信

  • 用途:

    • 打印调试信息(如串口调试助手)

    • 连接蓝牙模块、GPS模块、GPRS模块、ESP8266 等

  • 引脚:

    • TX(发送)

    • RX(接收)

  • 通讯方向:

    • 单工 / 半双工 / 全双工(一对 TX 和 RX 实现全双工)
  • 同步/异步通讯:

    • 异步

1.5.2 USART 通用同步/异步收发器

  • 特点:

    • 可切换为同步或异步模式

    • 异步模式与 UART 相同

    • 同步模式支持共享时钟,通信速率更高

  • 用途:

    • 与外部高速设备同步通信(如无线模块、MP3 解码器)

    • 与其他 MCU 或 FPGA 的高速数据交换

  • 引脚:

    • TX(发送)

    • RX(接收)

    • SCK(同步模式下的时钟)

  • 通讯方向:

    • 全双工(异步)

    • 半双工 / 全双工(同步)

  • 同步/异步通讯:

    • 同步 / 异步均支持

1.5.3 SPI 串行外设接口

  • 特点:

    • 高速同步通信,支持多个从设备

    • 数据传输速率高,帧结构简单

    • 主从模式固定,由主机控制时序

  • 用途:

    • 连接外部 Flash、SD 卡、显示屏(如 OLED、TFT)

    • 与传感器、DAC、ADC、无线模块(如 NRF24L01)通信

  • 引脚:

    • SCLK:时钟线

    • MOSI:主机输出,从机输入

    • MISO:主机输入,从机输出

    • NSS(CS):从机片选

  • 通讯方向:

    • 全双工(MOSI 与 MISO 同时工作)
  • 同步/异步通讯:

    • 同步

1.5.4 I2C 集成电路通信

  • 特点:

    • 双线通信,节省引脚

    • 支持多个主从设备

    • 需设备地址进行通信管理

    • 总线仲裁机制,适合短距离通信

  • 用途:

    • 连接 EEPROM、RTC、温湿度传感器

    • 控制 OLED 屏、陀螺仪、加速度计等低速外设

  • 引脚:

    • SCL:时钟线

    • SDA:数据线

  • 通讯方向:

    • 半双工(主从共用 SDA)
  • 同步/异步通讯:

    • 同步

1.5.5 1-Wire 单总线

  • 特点:

    • 仅需一根数据线即可完成通信

    • 每个设备有唯一地址,可挂载多个设备

    • 通信速率较低,适合温度传感器等低速场合

  • 引脚:

    • DATA:数据与控制信号复用(需上拉电阻)
  • 通讯方向:

    • 半双工(主从共用一根线)
  • 同步/异步通讯:

    • 异步(时序严格控制)

第二章 USART 串口通讯基础

2.1 USART 简介

在 STM32 中,通用同步异步收发器(USART)能够灵活地与外部设备进行全双工数据交换,满足外部设备对工业标准 NRZ 异步串行数据格式的要求。

USART 通过小数波特率发生器提供了多种波特率。它支持同步单向通信和半双工单线通信;

还支持局部连接网络(Local Interconnect Network,LIN)、智能卡协议与红外线数据协会(Infrared Data Association,IrDA)的 SIRENDEC 规范,以及调制解调器操作(CTS/RTS)。

而且,它还支持多处理器通信。通过配置多个缓冲区使用 DMA 可实现高速数据通信。

2.2 功能框图

2.3 串口协议

2.4 寄存器

寄存器名说明
USART_SR状态寄存器:TXE、RXNE、TC 等位
USART_DR数据寄存器:发送/接收数据
USART_BRR波特率寄存器
USART_CR1控制寄存器1:发送/接收使能、字长、TXEIE 等
USART_CR2控制寄存器2:停止位设置、LIN、同步模式
USART_CR3控制寄存器3:流控、DMA使能等
USART_GTPR宽带调制和红外相关设置

2.5 库函数

2.5.1 初始化函数

/* 初始化和配置相关函数 **********************************************/

void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
// USART 初始化函数,根据结构体配置波特率、数据位、停止位等。

void USART_StructInit(USART_InitTypeDef* USART_InitStruct);
// 将 USART_InitTypeDef 结构体设置为默认参数。

void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState);
// 使能或失能 USART 外设(开启发送和接收功能)。

2.5.2 数据收发函数

/* 数据收发相关函数 **************************************************/

void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
// 发送一个字节(8位或9位)数据。

uint16_t USART_ReceiveData(USART_TypeDef* USARTx);
// 接收一个字节数据(从接收数据寄存器读取)。

2.5.4 标志位管理函数

/* 中断与标志位管理函数 **********************************************/

void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);
// 配置 USART 中断使能,如 RXNE、TXE 等。

FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);
// 获取指定标志位的状态,如:USART_FLAG_RXNE、USART_FLAG_TXE 等。

void USART_ClearFlag(USART_TypeDef* USARTx, uint16_t USART_FLAG);
// 清除指定的标志位。

ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);
// 获取中断事件状态(是否触发)。

void USART_ClearITPendingBit(USART_TypeDef* USARTx, uint16_t USART_IT);
// 清除中断待处理位(即中断标志)。

1、状态标志位

uint16_t USART_FLAG

表示 USART 的状态标志位,通过 USART_GetFlagStatus() 查询或 USART_ClearFlag() 清除。
常用取值及含义:

标志位描述应用场景
USART_FLAG_TXE发送数据寄存器空(Transmit Data Register Empty)检查是否可以写入新数据到发送寄存器
USART_FLAG_TC发送完成(Transmission Complete)确认数据已完全发送到物理线路上
USART_FLAG_RXNE接收数据寄存器非空(Receive Data Register Not Empty)检查是否有新数据到达接收寄存器
USART_FLAG_ORE溢出错误(Overrun Error)接收缓冲区溢出时触发
USART_FLAG_FE帧错误(Framing Error)检测到无效的停止位
USART_FLAG_NE噪声错误(Noise Error)数据在传输中受到干扰
USART_FLAG_IDLE空闲总线检测(Idle Line Detected)检测总线空闲状态(如 Modem 通信)
USART_FLAG_LBDLIN 总线断开检测(LIN Break Detection)LIN 总线协议中检测断开信号
USART_FLAG_CTSCTS 标志变化(CTS Flag Change)硬件流控制中 CTS 信号状态变化

2、中断标志位

uint16_t USART_IT

表示 USART 的中断事件类型,通过 USART_ITConfig() 使能或禁用中断,通过 USART_GetITStatus() 查询中断触发状态。
常用取值及含义:

中断类型描述触发条件
USART_IT_TXE发送寄存器空中断发送寄存器空时触发(可写入新数据)
USART_IT_TC发送完成中断数据完全发送到物理线路上时触发
USART_IT_RXNE接收寄存器非空中断接收寄存器中有新数据时触发
USART_IT_IDLE空闲总线中断检测到总线空闲时触发
USART_IT_ORE溢出错误中断接收溢出时触发
USART_IT_PE奇偶校验错误中断奇偶校验失败时触发
USART_IT_ERR综合错误中断(ORE/FE/NE)任意错误标志触发时生效
USART_IT_CTSCTS 信号变化中断硬件流控制中 CTS 信号变化时触发

3、关键区别与注意事项

  1. 标志位 vs 中断

    • 标志位(FLAG):直接反映硬件状态,轮询模式下通过 USART_GetFlagStatus() 查询。
    • 中断(IT):需先通过 USART_ITConfig() 使能中断,再结合 NVIC 配置,在 中断服务函数(ISR) 中使用 USART_GetITStatus() 检查中断源。
  2. 清除标志的顺序

    • 在中断服务函数中,应先处理数据,再清除标志位(如 USART_ClearFlag()USART_ClearITPendingBit()),避免丢失后续中断。
  3. 错误处理

    • 若启用错误中断(如 USART_IT_ERR),必须检查具体错误标志(如 USART_FLAG_ORE)并处理,否则可能导致通信阻塞。

2.5.2 结构体

typedef struct
{
  uint32_t USART_BaudRate;                // USART通信的波特率,例如9600、115200等
  uint16_t USART_WordLength;             // 数据位长度,通常为USART_WordLength_8b或USART_WordLength_9b
  uint16_t USART_StopBits;               // 停止位设置,如USART_StopBits_1、USART_StopBits_2
  uint16_t USART_Parity;                 // 奇偶校验,如USART_Parity_No(无校验)、USART_Parity_Even(偶)、USART_Parity_Odd(奇)
  uint16_t USART_Mode;                   // 工作模式,如USART_Mode_Rx(接收)、USART_Mode_Tx(发送)或二者组合
  uint16_t USART_HardwareFlowControl;    // 硬件流控设置,如USART_HardwareFlowControl_None、RTS、CTS等
} USART_InitTypeDef;

第三章 USART串口通讯实验

3.1 实验1:发送字符与发送字符串

3.1.1 原理图分析

2.6.3 配置步骤

  1. 使能GPIO和USART时钟。

  2. 配置并初始化GPIO引脚(TX和RX)为复用模式。

  3. 设置引脚的复用功能为USART6。

  4. 配置USART6的串口参数,如波特率、数据位、停止位等。

  5. 使能USART6外设,开始进行数据传输。

2.6.4 示例代码

1、配置函数

void USART_6_Init(uint32_t BaudRate)

{
// 启动时钟
    // 启动GPIOG
    RCC_AHB1PeriphClockCmd( RCC_AHB1Periph_GPIOG , ENABLE );  
    // 启动USART6
    RCC_APB2PeriphClockCmd( RCC_APB2Periph_USART6 , ENABLE );

// 配置GPIO
    GPIO_InitTypeDef GPIO_InitStruct;

    // 配置模式为 复用
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
    // 配置输出模式 推挽
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    // 配置上下拉
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
    // 配置速度
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    // 配置引脚
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_14;

    // 配置到外设
    GPIO_Init( GPIOG , &GPIO_InitStruct );

// 设置引脚复用功能
    GPIO_PinAFConfig( GPIOG , GPIO_PinSource9 , GPIO_AF_USART6 );
    GPIO_PinAFConfig( GPIOG , GPIO_PinSource14 , GPIO_AF_USART6 );

// 配置串口
    USART_InitTypeDef USART_InitStruct;
    // 设定波特率  由用户输入
    USART_InitStruct.USART_BaudRate = BaudRate;
    // 设定数据位  8位
    USART_InitStruct.USART_WordLength = USART_WordLength_8b;
    // 设定停止位  1位
    USART_InitStruct.USART_StopBits = USART_StopBits_1;
    // 设定奇偶校验 无校验
    USART_InitStruct.USART_Parity = USART_Parity_No;
    // 设定模式    输入 输出
    USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    // 硬件控制器  不使用
    USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;

// 配置到外设
    USART_Init( USART6 , &USART_InitStruct );

// 启动外设
    USART_Cmd( USART6 , ENABLE );

}

2、发送字符

	// 发送字符
	USART_SendData(USART6 , 'A');

3、发送字符串

// 发送字符串
void USART6_Send(char *str)
{
    while (*str)
    {
		// 等待发送缓冲区空闲(TXE:Transmit Data Register Empty)
		// 当 TXE 为 1,表示 USART_DR 寄存器已准备好发送新数据
        while (USART_GetFlagStatus(USART6, USART_FLAG_TXE) == RESET);
        
        // 将当前字符写入数据寄存器 USART_DR,开始发送
        USART_SendData(USART6, *str++);

		// 等待数据发送完成(TC:Transmission Complete)
		// TC 为 1 表示数据已完全移出发送移位寄存器
        while (USART_GetFlagStatus(USART6, USART_FLAG_TC) == RESET);
    }
}

3.2 实验2: 串口中断接收实验

3.2.1 配置步骤

  • 使能 GPIOG 和 USART6 时钟。

  • 初始化 GPIOG 的 TX(PG14)和 RX(PG9)为复用功能模式。

  • 配置引脚复用为 USART6 对应功能。

  • 初始化 USART6:设置波特率、数据位、停止位、校验方式、收发模式。

  • 使能 USART 接收中断 USART_IT_RXNE

  • 配置 NVIC,开启 USART6 中断,并设定优先级。

  • 编写 USART6 中断服务函数 USART6_IRQHandler,在中断中读取数据。

3.2.2 示例代码-接收中断

1、串口中断配置

void USART_6_NVIC_Init(void)
{
    NVIC_InitTypeDef NVIC_InitStruct;

    // 设置优先级分组(可根据 FreeRTOS 使用情况调整)
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

    NVIC_InitStruct.NVIC_IRQChannel = USART6_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;  // 抢占优先级
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;         // 响应优先级
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;

    NVIC_Init(&NVIC_InitStruct);

    // 开启 USART6 接收中断
    USART_ITConfig(USART6, USART_IT_RXNE, ENABLE);
}

2、串口中断服务函数

设计思路:在中断函数中接收数据,然后将数据写入到缓冲区中,当接收到数据遇到了/r或者/n后,设置标志位,主函数负责对数据进行处理,而不使用中断对数据进行处理,降低中断的运行时间,防止接收数据丢失。

#define RECV_MAX 32

char Recv_ch;
char Recv_str[RECV_MAX];

uint16_t Recv_Index = 0;
uint16_t Recv_Flag = 0;

void USART6_IRQHandler(void)
{
    // 判读接收寄存器非空
    if (USART_GetITStatus(USART6, USART_IT_RXNE) == SET)
    {
        // 清空中断标志位
        USART_ClearITPendingBit(USART6, USART_IT_RXNE);
        Recv_ch = USART_ReceiveData(USART6);
        
        if (Recv_Index < RECV_MAX) // 判断是否越界
        {
            Recv_str[Recv_Index++] = Recv_ch;

            if (Recv_ch == '\r')
            {
                // 添加尾0
                Recv_str[Recv_Index] = '\0';
                // 清空操作
                Recv_Index = 0;
                Recv_Flag = SET;
            }
        }
        else
        {
            // 数据越界 清空
            Recv_Index = 0;
        }
    }
}

3、中断接收程序

void USART_Recv_Cmd(void)
{
    if (RESET != Recv_Flag)
    {
        // 清除标志位
        Recv_Flag = RESET;
        // 处理指令
        if (!strcmp(Recv_str, "Beep\r"))
        {
            Beep_Toggle();
        }
        else if ((!strcmp(Recv_str, "LED_1\r")))
        {
            LED_Toggle(LED_ID_1);
        }
        else if ((!strcmp(Recv_str, "Moto\r")))
        {
            Moto_Toggle();
        }
        else if ((!strcmp(Recv_str, "FAN\r")))
        {
            Fan_Toggle();
        }
    }
}

3.2.3 示例代码-空闲中断

在串口中断函数中,对以下两个中断类型响应:USART_IT_RXNE 表示数据接收寄存器收到内容,那么将接收到的内容作为一个字符放入缓冲区中;USART_IT_IDLE 表示数据包接收完毕,在缓冲器结尾添加上一个空字符使其变为字符串,并将结束标志位置 1 。

注意在不接收中断时,串口空闲中断会一直产生,从而干扰程序运行;清除串口空闲中断标志位需要由软件完成,具体做法是通过程序先读取 USART_SR 寄存器,再读取 USART_DR 寄存器。

1、串口中断配置

void USART_6_NVIC_Init(void)
{
    NVIC_InitTypeDef NVIC_InitStruct;

    // 设置优先级分组(可根据 FreeRTOS 使用情况调整)
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

    NVIC_InitStruct.NVIC_IRQChannel = USART6_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;  // 抢占优先级
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;         // 响应优先级
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;

    NVIC_Init(&NVIC_InitStruct);

    // 开启 USART6 接收中断
    USART_ITConfig(USART6, USART_IT_RXNE, ENABLE);
    // 开启 USART6 空闲中断
    USART_ITConfig(USART6, USART_IT_IDLE, ENABLE);
}

2、中断服务函数

#define RECV_MAX 32

char Recv_ch;
char Recv_str[RECV_MAX];

uint16_t Recv_Index = 0;
uint16_t Recv_Flag = 0;

void USART6_IRQHandler(void)
{
    // 判读接收寄存器非空
    if (USART_GetITStatus(USART6, USART_IT_RXNE) != RESET)
    {
        // 清空中断标志位
        USART_ClearITPendingBit(USART6, USART_IT_RXNE);
        Recv_ch = USART_ReceiveData(USART6);

        if (Recv_Index < RECV_MAX) // 判断是否越界
        {
            Recv_str[Recv_Index++] = Recv_ch;

            if (Recv_ch == '\r')
            {
                // 添加尾0
                Recv_str[Recv_Index] = '\0';

                // 清空操作
                Recv_Index = 0;
                Recv_Flag = SET;
            }
        }
        else
        {
            // 数据越界 清空
            Recv_Index = 0;
        }
    }
    if (USART_GetITStatus(USART6 , USART_IT_IDLE) != RESET)
    {
        uint16_t temp;
        // 先读SR
        temp = USART3->SR;
        // 再读DR 清除中断标志
        temp = USART3->DR;

        // 调用处理函数
        USART_Recv_Cmd();
    }
}

3.3 实验3:收发定长字符串

本实验演示如何使用串口一次性收发固定长度的数据,适用于结构化数据传输场景,例如接收 10 个字节的传感器数据或一帧固定格式命令。

3.3.1 示例程序

1、定义接收缓冲与标志

#define USART6_FIXED_LEN  10     // 固定长度为 10 字节

char USART6_FixedBuf[USART6_FIXED_LEN];  // 接收缓冲区
uint8_t USART6_FixedIndex = 0;           // 当前接收索引
uint8_t USART6_FixedFlag = 0;            // 接收完成标志

2、 串口中断接收函数

void USART6_IRQHandler(void)
{
    if (USART_GetITStatus(USART6, USART_IT_RXNE) != RESET)
    {
        char recv = USART_ReceiveData(USART6);

        if (USART6_FixedIndex < USART6_FIXED_LEN)
        {
            USART6_FixedBuf[USART6_FixedIndex++] = recv;

            // 判断是否接收完成
            if (USART6_FixedIndex >= USART6_FIXED_LEN)
            {
                USART6_FixedFlag = 1;       // 设置接收完成标志
                USART6_FixedIndex = 0;      // 清空索引,准备下次接收
            }
        }
    }
}

3、 接收数据

void USART6_Recv(char *str)
{
	if (USART6_RxFlag)
	{
	// 处理标志位
        USART6_RxFlag = 0;  // 清除标志
        USART6_Recv(USART6_RxBuf);  // 处理收到的一整帧数据
    
    // 处理数据
	    // 示例:打印字符串内容
	    USART6_Send("接收到:");
	    USART6_Send(str);
	    USART6_Send("\r\n");
	
	    // 你可以在此添加关键字判断,如:
	    // if (strcmp(str, "led on") == 0) { 控制LED点亮 }
	}
}

4、 发送数据

void USART6_Send(char *str)
{
    while (*str)
    {
		// 等待发送缓冲区空闲(TXE:Transmit Data Register Empty)
		// 当 TXE 为 1,表示 USART_DR 寄存器已准备好发送新数据
        while (USART_GetFlagStatus(USART6, USART_FLAG_TXE) == RESET);
        
        // 将当前字符写入数据寄存器 USART_DR,开始发送
        USART_SendData(USART6, *str++);

		// 等待数据发送完成(TC:Transmission Complete)
		// TC 为 1 表示数据已完全移出发送移位寄存器
        while (USART_GetFlagStatus(USART6, USART_FLAG_TC) == RESET);
    }
}

3.4 实验4:收发不定长字符串

本实验用于演示如何通过串口接收长度不固定的一串字符数据,常见于命令解析、上位机协议通信等场景。
结束标志通常为 \r\n,接收完成后由主函数统一处理数据。

3.4.1 示例代码

1、串口中断配置

void USART_6_NVIC_Init(void)
{
    NVIC_InitTypeDef NVIC_InitStruct;

    // 设置优先级分组(可根据 FreeRTOS 使用情况调整)
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

    NVIC_InitStruct.NVIC_IRQChannel = USART6_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;  // 抢占优先级
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;         // 响应优先级
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;

    NVIC_Init(&NVIC_InitStruct);

    // 开启 USART6 接收中断
    USART_ITConfig(USART6, USART_IT_RXNE, ENABLE);
}

2、串口中断配置

设计思路:在中断函数中接收数据,然后将数据写入到缓冲区中,当接收到数据遇到了/r或者/n后,设置标志位,主函数负责对数据进行处理,而不使用中断对数据进行处理,降低中断的运行时间,防止接收数据丢失。

#define USART6_BUF_LEN  64

char USART6_RxBuf[USART6_BUF_LEN];   // 接收缓冲区
uint8_t USART6_RxIndex = 0;          // 当前接收下标
uint8_t USART6_RxFlag = 0;           // 接收完成标志位(1 表示收到一帧)


void USART6_IRQHandler(void)
{
    if (USART_GetITStatus(USART6, USART_IT_RXNE) != RESET)
    {
        char recv = USART_ReceiveData(USART6);

        if (recv == '\r' || recv == '\n')  // 一帧结束
        {
            USART6_RxBuf[USART6_RxIndex] = '\0';  // 添加字符串结束符
            USART6_RxFlag = 1;                    // 设置接收完成标志
            USART6_RxIndex = 0;                   // 重置接收下标
        }
        else
        {
            // 普通字符,存入缓冲区
            if (USART6_RxIndex < USART6_BUF_LEN - 1)
            {
                USART6_RxBuf[USART6_RxIndex++] = recv;
            }
            else
            {
                USART6_RxIndex = 0;  // 超出缓冲区,重新接收
            }
        }
    }
}


2、中断接收程序

void USART6_Recv(char *str)
{
	if (USART6_RxFlag)
	{
	// 处理标志位
        USART6_RxFlag = 0;  // 清除标志
        USART6_Recv(USART6_RxBuf);  // 处理收到的一整帧数据
    
    // 处理数据
	    // 示例:打印字符串内容
	    USART6_Send("接收到:");
	    USART6_Send(str);
	    USART6_Send("\r\n");
	
	    // 你可以在此添加关键字判断,如:
	    // if (strcmp(str, "led on") == 0) { 控制LED点亮 }
	}
}

3.5 实验5:重定向Printf

在 STM32 开发中,printf() 函数默认输出到标准输出设备(一般是显示器),而我们希望通过串口输出调试信息,就需要将 printf() 重定向到串口。

2.7.1 原理说明

  • printf() 最终调用的是 fputc() 函数来实现字符输出。

  • 所以只要 重写 fputc() 函数,将每个字符发送到串口,即可实现 printf 重定向。

  • 由于 fputc 属于 C 标准库,所以必须在开启 MicroLib(微库) 的情况下才能生效。

2.7.2 重定向 fputc

#include "stm32f4xx.h"
#include <stdio.h>

/**
 * @brief 重写 fputc 函数,将 printf 的输出重定向到 USART6
 * @param ch  需要输出的字符
 * @param f   文件结构体(可以忽略)
 * @retval 返回写入的字符
 */
int fputc(int ch, FILE *f)
{
    // 等待发送缓冲区为空(TXE = 1),确保上一个字符已发送完成
    while ((USART6->SR & USART_SR_TXE) == 0);

    // 写入数据寄存器,自动开始发送
    USART6->DR = (uint8_t)ch;

    return ch;
}
  • 请确认 USART6 已正确初始化(波特率、GPIO、模式等);

  • 若使用其他串口(如 USART1),请修改 USART6 为对应的串口;

  • printf() 在使用浮点数(如 %f)时,需要设置编译器支持浮点格式,避免报错或乱码。

3.6 实验6:基于ESP8266的TCP通讯

3.6.1 ESP8266 芯片介绍

ESP-12F 是由安信可科技开发的 Wi-Fi 模块,该模块核心处理器 ESP8266 在较小尺寸封装中集成了业界领先的 Tensilica L106 超低功耗 32 位微型 MCU,带有 16 位精简模式,主频支持 80 MHz 和 160 MHz,支持 RTOS,集成 Wi-Fi MAC/BB/RF/PA/LNA。

ESP-12F Wi-Fi 模块支持标准的 IEEE802.11 b/g/n 协议,完整的 TCP/IP 协议栈。用户可以使用该模块为现有的设备添加联网功能,也可以构建独立的网络控制器。

ESP8266 是高性能无线 SoC,以最低成本提供最大实用性,为 Wi-Fi 功能嵌入其他系统提供无限可能。

ESP8266 拥有完整的且自成体系的 Wi-Fi 网络功能,既能够独立应用,也可以作为从机搭载于其他主机 MCU 运行。当 ESP8266 独立应用时,能够直接从外接 flash 中启动。

内置的高速缓冲存储器有利于提高系统性能,并且优化存储系统。

另外⼀种情况是, ESP8266 只需通过 SPI/SDIO 接口或 UART 接口即可作为 Wi-Fi适配器,应用到基于任何微控制器设计中。

ESP8266 强大的片上处理和存储能力,使其可通过 GPIO 口集成传感器及其他应用的特定设备,大大地降低了前期开发的成本。

1、特性

  • 完整的 802.11b/g/n Wi-Fi SoC 模块
  • 内置 Tensilica L106 超低功耗 32 位微型 MCU,主频支持 80 MHz 和 160 MHz,支持 RTOS
  • 内置 1 路 10 bit 高精度 ADC
  • 支持 UART/GPIO/ADC/PWM/SPI/I2C 接口
  • 采用 SMD-22 封装
  • 集成 Wi-Fi MAC/ BB/RF/PA/LNA
  • 支持多种休眠模式,深度睡眠电流低至 20uA
  • 串口速率最高可达 4Mbps
  • 内嵌 Lwip 协议栈
  • 支持 STA/AP/STA+AP 工作模式
  • 支持安卓、IOS 的 Smart Config(APP)/AirKiss(微信) 一键配网
  • 支持串口本地升级和远程固件升级(FOTA)
  • 通用 AT 指令可快速上手
  • 支持二次开发,集成了 Windows、Linux 开发环境

2、引脚定义


3.6.2 指令介绍

1、什么是 AT 指令?

AT 指令(Attention Command)是一种串口控制协议,ESP8266 模块通过串口接收这些指令,来实现连接 WiFi、建立 TCP 连接、发送数据等操作。

2、 AT 指令分类与功能表

指令功能说明常用返回结果
AT测试模块是否正常工作OK
AT+RST重启模块OKready
AT+GMR查询固件版本版本号信息
AT+UART配置串口参数OK
AT+CWMODE设置 WiFi 模式(STA/AP/STA+AP)OK
AT+CWLAP扫描周围 WiFi 热点热点列表
AT+CWJAP连接指定的 WiFi 热点OKWIFI GOT IP
AT+CWQAP断开当前 WiFi 连接OK
AT+CIFSR查询当前模块 IP 地址+CIFSR:STAIP,...
AT+CIPSTART建立 TCP/UDP 连接CONNECT
AT+CIPSEND发送数据(或进入透传模式)>SEND OK
AT+CIPCLOSE关闭 TCP/UDP 连接CLOSED
AT+CIPMODE设置是否使用透传模式OK
AT+PING="IP"测试网络连通性(部分固件支持)OK 或响应时间

3. 常用 AT 指令详解

模块测试与重启

AT          // 测试模块状态
AT+RST      // 重启模块

配置串口波特率(慎用)

AT+UART=9600,8,1,0,0    // 设置为 9600 波特率,8位数据,1位停止,无校验

设置 WiFi 工作模式

AT+CWMODE=1     // 设置为 STA 模式(连接路由器)
AT+CWMODE=2     // 设置为 AP 模式(自建热点)
AT+CWMODE=3     // STA + AP 双模式

连接到 WiFi 热点

AT+CWJAP="TAN_ZHIPENG 8403","88888888"

获取模块 IP 地址

AT+CIFSR       // 查看分配到的 IP 地址

建立 TCP 连接并发送数据

AT+CIPSTART="TCP","192.168.1.100",8080
AT+CIPSEND=5
hello

开启透传模式通信

AT+CIPMODE=1
AT+CIPSTART="TCP","192.168.1.100",8080
AT+CIPSEND
// 出现 '>' 后,直接发送数据

退出透传模式

+++    // 前后1秒无其他数据(静默),否则会失败

查询当前连接状态

AT+CIPSTATUS

4、AT 指令返回值解析

返回值含义说明
OK指令成功执行
ERROR指令执行失败
WIFI CONNECTED成功连接 WiFi
WIFI GOT IP获取到 IP 地址,联网成功
FAIL执行失败(常见于连不上 WiFi)
SEND OK数据发送成功
busy p...模块忙,正在处理上一个命令
>表示可以开始透传数据

5、附录:建议的串口调试设置

参数建议配置
波特率115200 或 9600
数据位8
停止位1
校验位
流控
回车换行符\r\n(回车换行)

如你需要我为这个章节配上每条指令的效果图(串口截图)STM32 调用示例代码块,我也可以继续帮你丰富。如果需要我整理成 markdown 或 ppt 结构,也可以告诉我格式需求。

3.6.2 ESP8266 固件下载

注意:设备出厂一般自带固件,可以直接使用,不烧录也可以使用。

1、设备准备

核心板 + 底板

USB TypeC 线

拨动拨码开关,设定模式

将拨码开关拨动到
S2 USB
S3 USB
S4 下载

2、固件准备

固件下载地址:
https://docs.ai-thinker.com/

模组厂家-安信可官网:
https://docs.ai-thinker.com/esp8266

  1. 进入 WIFI 模组 进入ESP8266系列

  1. 滑到最下面,然后点击各类AT固件

  1. 选择固件下载

  1. 此外,也可以直接选择文件夹中的固件,是已经下载好的
路径为:\FS_STM32F407IGH6_开发资料\02_器件手册\ESP-12F模组官方资料\Ai-Thinker_ESP8266_AT_Firmware_DOUT_v1.5.4.1-a_20171130

3、工具准备

路径:FS_STM32F407IGH6_开发资料\02_器件手册\ESP-12F模组官方资料\flash_download_tool_3.9.2

4、开始下载

  1. 打开软件,然后选择ESP8266

  1. 开始下载

3.6.3 原理图分析

3.6.4 ESP8266 指令测试

本小结是通过安信可串口调试助手进行连接 TCP 测试。

1、设备准备

拨动ESP8266如下所示

S2 USB
S3 USB
S4 运行

此外,需要将 Type-C 线从原来的核心板连接换到底板连接。

2、软件准备

软件路径:FS_STM32F407IGH6_开发资料\01_工具软件\06_安信可串口调试助手\AiThinker_Serial_Tool_V1.2.3

启动软件

端口号的选择可以通过设备管理器进行查看,此外要提前安装CH340驱动。

还需要一个网络调试工具,我们使用vofa+进行调试,启动软件,配置如下:

3、启动热点

然后我们需要开启自己电脑的热点,需要注意ESP8266不支持5G网络,所以我们需要对电脑的热点做一些设定,设定如下:

主要是将网络频带设定为 2.4GHz

4、指令测试 TCP

1、测试响应

AT

响应

OK

2、配置 WIFI 模式

AT+CWMODE=3

响应

OK

4、连接网络
这里是我们电脑热点或者你的路由器的WiFi名称和WiFi密码

AT+CWJAP="SSID","password"

响应:

WIFI CONNECTED

WIFI GOT IP


OK

3、查询 ESP8266 设备的 IP 地址

AT+CIFSR

应答:

+CIFSR:APIP,"192.168.
4.1"
+CIFSR:APMAC,"ce:7b:5c:4e:4d:8d"
+CIFSR:STAIP,"192.168.137.218"        // IP地址
+CIFSR:STAMAC,"cc:7b:5c:4e:4d:8d"

OK

4、ESP8266 作为 TCP 客户端连接

需要提前启动一个TCP服务器,例如我们前面软件准备中启动网络调试助手的TCP服务器。

关于TCP服务器的地址需要通过cmd中的ipconfig指令获取,如果出现多个ip则选择和前面获取ESP8266同网段的地址。

AT+CIPSTART="TCP","192.168.137.1",8080

响应:

CONNECT

OK

5、ESP8266 发送数据到服务器

设置要发送的数据长度,例如16字节。

AT+CIPSEND=12

响应:

OK
> 

发送数据

小谭老师真帅

应答:

busy s...

Recv 12 bytes


SEND OK

6、接收数据
服务器发送数据ESP8266,ESP2866会收到如下数据

+IPD,18:小谭老师确实很帅

3.6.5 示例程序

1、串口配置和串口中断配置

void USART_ESP8266_Config(void)
{
	
}

2、中断服务函数

	

3、接收数据处理

	

4、等待接收结果

	

3、TCP连接请求

void ESP8266_TCP_Connect(void)
{
	
}

3、发送数据函数

3.7 实验7:基于ESP8266的UDP通讯

3.8 实验8:串口环形队列实验

第四章 DMA 直接内存访问

4.1 DMA 简介

直接内存访问(DMA)是一种允许外设直接与内存交换数据的技术,而无需通过 CPU 进行数据转移。DMA 的主要作用是提高数据传输效率,减少 CPU 的负担,从而提升系统的性能。

DMA 的工作原理如下:

  • 外设如 USART、ADC 等将数据传输到内存,而不需要 CPU 介入。

  • DMA 控制器管理数据的传输过程,确保数据从外设或内存传输到目标位置。

  • DMA 可以与不同外设(如 USART、SPI)协同工作,通过配置 DMA 通道来指定数据流动的方向与目标。

常见的 DMA 类型:

  • 内存到内存 DMA:用于在内存区域之间传输数据。

  • 外设到内存 DMA:外设数据传输到内存,常用于 ADC、USART 等外设。

  • 内存到外设 DMA:内存数据传输到外设,常见于 USART、DAC 等外设。

DMA 的优势:

  • 提高效率:减少了 CPU 的干预,解放了 CPU 的计算资源。

  • 降低延迟:DMA 可实现更快的数据传输。

  • 节省功耗:因为 CPU 的参与减少,节省了能量。

DMA 控制器的一般功能包括:启动传输、停止传输、控制数据传输方向、检查传输完成等。

在使用 DMA 时,需要对 DMA 控制器进行配置,指定源地址、目标地址、数据大小、传输模式等参数。每种外设可以关联不同的 DMA 通道,进行数据传输。

使用DMA 的优势

DMA的工作模式

4.2 DMA 功能框图

STM32F4 最多有 2 个 DMA 控制器(DMA1 和 DMA2),共 16 个数据流(每个控制器 8 个),每一个 DMA 控制器都用于管理一个或多个外设的存储器访问请求。每个数据流总共可以有多达 8个通道(或称请求)。每个数据流通道都有一个仲裁器,用于处理 DMA 请求间的优先级。

DMA 控制器执行直接存储器传输:因为采用 AHB 主总线,它可以控制 AHB 总线矩阵来启动 AHB 事务。
它可以执行下列事务:

  • 外设到存储器的传输

  • 存储器到外设的传输

  • 存储器到存储器的传输

DMA控制器提供了两个AHB 主端口:AHB存储器端口(用于连接存储区)AHB外设端口(用于连接外设),不过,要实现存储器到存储器的数据传输,AHB外设端口就必须能访问存储器。

对于有两个DMA的控制器的框图如下图。

需要注意的是,在小容量的一些芯片中是没有两个DMA控制器的,只有大容量的设备才具有两个DMA控制器,所以在进行编程的时候,一定要注意当前的芯片是否具有两组DMA控制器。

此外,DMA1 控制器的 AHB外设端口与DMA2控制器的连接方式不同,DMA2 的外设端口不连接到矩阵,所以,只有DMA2具备存储器到存储的直接数据访问。

3.2.1 DMA 事务

DMA的事务由给定数目的数据传输序列组成,可以通过软件控制其传输的数据项数目宽度(8位)、16位、32位

然后每个DMA包含了三个操作步骤:

  1. 通过 DMA_SxPAR 或者 DMA_SxMAR 寄存器寻址,从外设数据存储器存储单元中加载数据。
  2. 通过 DMA_SxAR 或者 DMA_SxMAR 寄存器寻址,将加载的数据到外设数据存储器或存储单元
  3. 通过 DMA_SxNDTR 计数器在数据存储结束后递减,该计数器包含的是后续任需要执行的事务数

DMA_SxPAR 和 DMA_SxMAR
DMA_SxPAR: P 代表 外设 peripheral
DMA_SxMAR: M 代表 内存 memory

在 DMA 请求发生时,外设向 DMA 控制器发送请求信号。DMA 控制器根据通道优先级处理该请求。当 DMA 控制器访问外设时,会向外设发送确认信号。外设收到确认信号后,便会释放请求。如果外设撤销请求,DMA 控制器会释放确认信号,等待外设发起新的事务请求。

3.2.2 MDA 外设通道选择

每个DMA数据流(用于外设和内存间传输数据)需要选择一个通道(共8个可选通道,编号0-7)。通过配置寄存器 DMA_SxCR 中的 CHSEL[2:0] 位(3个二进制位),可以指定使用哪个通道。

通过设置 CHSEL 选择通道号(0-7),让DMA数据流知道该响应哪个外设(如ADC或SPI)的传输请求。具体外设和通道的对应关系需查芯片手册。

关于STM32F407芯片中的每个通道连接的是那个外设,可以通过下表进行查看。

DMA 1

DMA 2

3.2.3 仲裁器

DMA仲裁器负责管理8个数据流的传输请求,根据优先级决定哪个数据流先执行传输。它连接两个主端口:\

  • 存储器端口(如内存)

  • 外设端口(如ADC、SPI等)

优先级规则

  1. 软件优先级(可配置):
    在寄存器 DMA_SxCR 中设置优先级等级,共4级:

    • 非常高 ➔ 高 ➔ 中 ➔ 低
      示例: 若数据流1设为“非常高”,数据流2设为“低”,则数据流1优先执行。
  2. 硬件优先级(固定规则):

    • 如果两个数据流的软件优先级相同,编号小的数据流优先。
      示例: 数据流2和4的优先级同为“高”,则数据流2先执行。

3.2.4 数据流

数据流的作用
每个DMA控制器有8个数据流,每个数据流负责在源(Source)目标(Target) 之间建立单向数据传输通道。

支持两种传输模式

  1. 标准传输模式

    • 支持以下三种场景:
      • 内存 ↔ 外设(如从内存搬运数据到SPI)
      • 外设 ↔ 内存(如ADC采集数据存到内存)
      • 内存 ↔ 内存(快速复制数据块)
  2. 双缓冲模式

    • 使用两个内存区域交替传输,提升效率:
      • DMA读写一个缓冲区时,CPU可操作另一个缓冲区。
      • 避免传输过程中CPU与DMA争用同一内存区域。

关键特性

  • 传输数据量可编程:单次传输最多支持 65535 项数据(如65535字节、字等,具体单位由外设决定)。
  • 动态调整:每完成一次传输,剩余数据量会自动减少,直到全部传完。

3.2.5 FIFO

FIFO的作用
每个DMA数据流自带一个四级深度的32位缓冲区(FIFO),用于临时存储数据,解决传输中的效率和数据宽度匹配问题。

两种传输模式

  1. 直接模式

    • 外设一发出请求,DMA立即传输数据。
    • 示例: 内存到外设传输时,DMA先将1个数据存入FIFO,外设请求一到,数据立刻被送出。
  2. FIFO模式

    • 数据先缓存在FIFO中,积累到设定阈值后再批量传输。
    • 阈值控制:通过寄存器 DMA_SxFCRFTH[1:0] 设置触发传输的阈值:
      • 1/4满 ➔ 1/2满 ➔ 3/4满 ➔ 全满

FIFO的核心用途

  1. 数据宽度转换

    • 当源和目标的数据宽度不同时(如源是字节,目标是32位字),FIFO会先将数据缓存,再按目标宽度重组。
    • 示例: 4个8位字节拼成1个32位字传输,避免频繁操作。
  2. 突发传输(Burst)支持

    • 一次性传输大量数据,减少频繁请求的开销,提升效率。
    • 类比: 快递批量打包送货,比单件多次运输更快。
  3. 平滑数据传输

    • 源数据速率不稳定时,FIFO作为“蓄水池”,确保目标端按稳定速率接收数据。

FIFO是DMA的智能缓冲区,解决数据宽度不匹配、突发传输需求,还能通过阈值控制优化传输效率。

3.3 DMA 相关函数

3.3.1 常用库函数

1、DMA 初始化和重置函数

/* ************ DMA 初始化和重置函数 *************/

/* 
 * 功能:将 DMA 配置重置为默认状态
 * 参数:DMAy_Streamx - 需要重置的 DMA 流
 */
void DMA_DeInit(DMA_Stream_TypeDef* DMAy_Streamx);


/* 
 * 功能:初始化 DMA 流,并根据传入的配置结构进行配置
 * 参数:
 *   DMAy_Streamx - 需要初始化的 DMA 流
 *   DMA_InitStruct - DMA 配置结构体
 */
void DMA_Init(DMA_Stream_TypeDef* DMAy_Streamx, DMA_InitTypeDef* DMA_InitStruct);


/* 
 * 功能:将 DMA 配置结构体初始化为默认值
 * 参数:
 *   DMA_InitStruct - 需要初始化的 DMA 配置结构体
 */
void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct);


/* 
 * 功能:使能或禁用 DMA 流
 * 参数:
 *   DMAy_Streamx - 需要配置的 DMA 流
 *   NewState - 新状态(ENABLE 或 DISABLE)
 */
void DMA_Cmd(DMA_Stream_TypeDef* DMAy_Streamx, FunctionalState NewState);

2、可选配置函数

/* ************ 可选配置函数 *************/

/* 
 * 功能:配置 DMA 外设地址递增偏移量大小
 * 参数:
 *   DMAy_Streamx - DMA 流
 *   DMA_Pincos - 外设地址递增偏移大小
 */
void DMA_PeriphIncOffsetSizeConfig(DMA_Stream_TypeDef* DMAy_Streamx, uint32_t DMA_Pincos);


/* 
 * 功能:配置 DMA 流控制器
 * 参数:
 *   DMAy_Streamx - DMA 流
 *   DMA_FlowCtrl - 流控制方式
 */
void DMA_FlowControllerConfig(DMA_Stream_TypeDef* DMAy_Streamx, uint32_t DMA_FlowCtrl);


/* ************ 数据计数器函数 *************/

/* 
 * 功能:设置 DMA 当前数据计数器值
 * 参数:
 *   DMAy_Streamx - DMA 流
 *   Counter - 数据计数器的值
 */
void DMA_SetCurrDataCounter(DMA_Stream_TypeDef* DMAy_Streamx, uint16_t Counter);


/* 
 * 功能:获取 DMA 当前数据计数器值
 * 参数:
 *   DMAy_Streamx - DMA 流
 * 返回值:当前的数据计数器值
 */
uint16_t DMA_GetCurrDataCounter(DMA_Stream_TypeDef* DMAy_Streamx);

3、双缓冲模式函数

/* ************ 双缓冲模式函数 *************/

/* 
 * 功能:配置 DMA 双缓冲模式
 * 参数:
 *   DMAy_Streamx - DMA 流
 *   Memory1BaseAddr - 第二缓冲区的基地址
 *   DMA_CurrentMemory - 当前内存选择(Memory0 或 Memory1)
 */
void DMA_DoubleBufferModeConfig(DMA_Stream_TypeDef* DMAy_Streamx, uint32_t Memory1BaseAddr, uint32_t DMA_CurrentMemory);


/* 
 * 功能:启用或禁用 DMA 双缓冲模式
 * 参数:
 *   DMAy_Streamx - DMA 流
 *   NewState - 新状态(ENABLE 或 DISABLE)
 */
void DMA_DoubleBufferModeCmd(DMA_Stream_TypeDef* DMAy_Streamx, FunctionalState NewState);


/* 
 * 功能:配置 DMA 目标内存地址
 * 参数:
 *   DMAy_Streamx - DMA 流
 *   MemoryBaseAddr - 目标内存基地址
 *   DMA_MemoryTarget - 目标内存选择(Memory0 或 Memory1)
 */
void DMA_MemoryTargetConfig(DMA_Stream_TypeDef* DMAy_Streamx, uint32_t MemoryBaseAddr, uint32_t DMA_MemoryTarget);


/* 
 * 功能:获取当前的 DMA 内存目标(Memory0 或 Memory1)
 * 参数:
 *   DMAy_Streamx - DMA 流
 * 返回值:当前的 DMA 内存目标
 */
uint32_t DMA_GetCurrentMemoryTarget(DMA_Stream_TypeDef* DMAy_Streamx);


4、中断与标志管理函数

/* ************ 中断与标志管理函数 *************/

/* 
 * 功能:获取 DMA 流的使能状态
 * 参数:
 *   DMAy_Streamx - DMA 流
 * 返回值:DMA 流的使能状态(ENABLE 或 DISABLE)
 */
FunctionalState DMA_GetCmdStatus(DMA_Stream_TypeDef* DMAy_Streamx);


/* 
 * 功能:获取 DMA 流的 FIFO 状态
 * 参数:
 *   DMAy_Streamx - DMA 流
 * 返回值:DMA 流的 FIFO 状态
 */
uint32_t DMA_GetFIFOStatus(DMA_Stream_TypeDef* DMAy_Streamx);


/* 
 * 功能:获取 DMA 标志状态
 * 参数:
 *   DMAy_Streamx - DMA 流
 *   DMA_FLAG - 标志位
 * 返回值:标志位的状态(SET 或 RESET)
 */
FlagStatus DMA_GetFlagStatus(DMA_Stream_TypeDef* DMAy_Streamx, uint32_t DMA_FLAG);


/* 
 * 功能:清除 DMA 标志位
 * 参数:
 *   DMAy_Streamx - DMA 流
 *   DMA_FLAG - 需要清除的标志位
 */
void DMA_ClearFlag(DMA_Stream_TypeDef* DMAy_Streamx, uint32_t DMA_FLAG);


/* 
 * 功能:使能或禁用 DMA 中断
 * 参数:
 *   DMAy_Streamx - DMA 流
 *   DMA_IT - 中断类型(DMA_IT_TC、DMA_IT_HT 等)
 *   NewState - 新状态(ENABLE 或 DISABLE)
 */
void DMA_ITConfig(DMA_Stream_TypeDef* DMAy_Streamx, uint32_t DMA_IT, FunctionalState NewState);


/* 
 * 功能:获取 DMA 中断状态
 * 参数:
 *   DMAy_Streamx - DMA 流
 *   DMA_IT - 中断类型(DMA_IT_TC、DMA_IT_HT 等)
 * 返回值:中断状态(SET 或 RESET)
 */
ITStatus DMA_GetITStatus(DMA_Stream_TypeDef* DMAy_Streamx, uint32_t DMA_IT);


/* 
 * 功能:清除 DMA 中断挂起位
 * 参数:
 *   DMAy_Streamx - DMA 流
 *   DMA_IT - 中断类型(DMA_IT_TC、DMA_IT_HT 等)
 */
void DMA_ClearITPendingBit(DMA_Stream_TypeDef* DMAy_Streamx, uint32_t DMA_IT);

3.3.2 配置结构体

typedef struct
{
    // 基本配置
    uint32_t DMA_Channel;               // DMA通道
    uint32_t DMA_Mode;                  // DMA工作模式
    uint32_t DMA_Priority;              // DMA优先级
    uint32_t DMA_FIFOMode;              // FIFO模式使能
    uint32_t DMA_FIFOThreshold;         // FIFO阈值

    // 地址配置
    uint32_t DMA_PeripheralBaseAddr;   // 外设基地址
    uint32_t DMA_Memory0BaseAddr;      // 内存基地址

    // 地址增量配置
    uint32_t DMA_PeripheralInc;        // 外设地址增量
    uint32_t DMA_MemoryInc;            // 内存地址增量

    // 数据大小配置
    uint32_t DMA_PeripheralDataSize;   // 外设数据宽度
    uint32_t DMA_MemoryDataSize;       // 内存数据宽度

    // 传输方向与缓冲区大小
    uint32_t DMA_DIR;                  // 数据传输方向
    uint32_t DMA_BufferSize;           // 数据传输大小

    // 数据突发模式
    uint32_t DMA_MemoryBurst;          // 内存突发传输模式
    uint32_t DMA_PeripheralBurst;      // 外设突发传输模式
} DMA_InitTypeDef;

1、基本配置

    uint32_t DMA_Channel;               // DMA通道
    uint32_t DMA_Mode;                  // DMA工作模式
    uint32_t DMA_Priority;              // DMA优先级
    uint32_t DMA_FIFOMode;              // FIFO模式使能
    uint32_t DMA_FIFOThreshold;         // FIFO阈值
  • DMA通道:DMA 请求通道选择,可选通道 0 至通道 7,每个外设对应固定的通道,具体设置值需要查表 DMA1 各个通道的请求映像 和表 DMA2 各个通道的请求映像 ;它设定 DMA_SxCR寄存器的 CHSEL[2:0] 位的值。

  • DMA工作模式:DMA 传输模式选择,可选一次传输或者循环传输,它设定 DMA_SxCR 寄存器的 CIRC 位的值。对于串口

  • DMA优先级:软件设置数据流的优先级,有 4 个可选优先级分别为非常高、高、中和低,它设定 DMA_SxCR 寄存器的 PL[1:0] 位的值。

  • FIFO模式使能:FIFO 模式使能,如果设置为 DMA_FIFOMode_Enable 表示使能 FIFO 模式功能;它设定 DMA_SxFCR 寄存器的 DMDIS 位。

  • FIFO阈值:FIFO 阈值选择,可选 4 种状态分别为 FIFO 容量的 1/4、1/2、3/4 和满;它设定 DMA_SxFCR 寄存器的 FTH[1:0] 位;DMA_FIFOMode 设置为 DMA_FIFOMode_Disable,那 DMA_FIFOThreshold 值无效。

2、地址配置

    uint32_t DMA_PeripheralBaseAddr;   // 外设基地址
    uint32_t DMA_Memory0BaseAddr;      // 内存基地址
  • 外设基地址:外设地址,设定 DMA_SxPAR 寄存器的值;一般设置为外设的数据寄存器地址,如果是存储器到存储器模式则设置为其中一个存储区地址。

  • 内存基地址:存储器 0 地址,设定 DMA_SxM0AR 寄存器值;一般设置为我们自定义存储区的首地址。

3、地址增量配置

    uint32_t DMA_PeripheralInc;        // 外设地址增量
    uint32_t DMA_MemoryInc;            // 内存地址增量
  • 外设地址增量:如果配置为 DMA_PeripheralInc_Enable,使能外设地址自动递增功能,它设定 DMA_SxCR 寄存器的 PINC 位的值;一般外设都是只有一个数据寄存器,所以一般不会使能该位。

  • 内存地址增量:如果配置为 DMA_MemoryInc_Enable,使能存储器地址自动递增功能,它设定 DMA_SxCR 寄存器的 MINC 位的值;

4、数据大小配置

    uint32_t DMA_PeripheralDataSize;   // 外设数据宽度
    uint32_t DMA_MemoryDataSize;       // 内存数据宽度
  • 外设数据宽度:外设数据宽度,可选字节 (8 位)、半字 (16 位) 和字 (32 位),它设定DMA_SxCR 寄存器的 PSIZE[1:0] 位的值。

  • 内存数据宽度:存储器数据宽度,可选字节 (8 位)、半字 (16 位) 和字 (32 位),它设定DMA_SxCR 寄存器的 MSIZE[1:0] 位的值。

5、传输方向与缓冲区大小

    uint32_t DMA_DIR;                  // 数据传输方向
    uint32_t DMA_BufferSize;           // 数据传输大小
  • 数据传输方向:传输方向选择,可选外设到存储器、存储器到外设以及存储器到存储器。它设定DMA_SxCR 寄存器的 DIR[1:0] 位的值。ADC 采集显然使用外设到存储器模式。
  • 数据传输大小:设定待传输数据数目,初始化设定 DMA_SxNDTR 寄存器的值。

6、数据突发模式

    uint32_t DMA_MemoryBurst;          // 内存突发传输模式
    uint32_t DMA_PeripheralBurst;      // 外设突发传输模式
  • 内存突发传输模式:存储器突发模式选择,可选单次模式、4 节拍的增量突发模式、8 节拍的增量突发模式或 16 节拍的增量突发模式,它设定 DMA_SxCR 寄存器的 MBURST[1:0] 位的值。

  • 外设突发传输模式:外设突发模式选择,可选单次模式、4 节拍的增量突发模式、8 节拍的增量突发模式或 16 节拍的增量突发模式,它设定 DMA_SxCR 寄存器的 PBURST[1:0] 位的值。

3.3.2 循环模式和一次传输

3.3.3 突发传输

3.3.4 FIFO模式和直接模式

FIFO模式 有缓冲区
直接模式 无缓冲区

3.3.5 数据大小和数据宽度

如果把 数据比作一个数组 那么 数据大小就是数据类型 char 字节 short 半字 int 字
那么 数据宽度 代表 这个数组多大

DMA_PeripheralDataSize arr[DMA_BufferSize]
数据大小 DMA_BufferSize
数据宽度
DMA_PeripheralDataSize
DMA_MemoryDataSize

3.3.6 DMA中断

对于每个 DMA 数据流,可在发生以下事件时产生中断:

  • 达到半传输
  • 传输完成
  • 传输错误
  • FIFO 错误(上溢、下溢或 FIFO 级别错误)
  • 直接模式错误

第五章 DMA存储实验

5.1 实验:存储器到存储器传输

3.4.1 初始化配置

// 定义源数据和目标数据缓冲区
uint32_t srcBuffer[BUFFER_SIZE];
uint32_t destBuffer[BUFFER_SIZE];

void DMA_M_M_Init(void)
{
    // 启动 DMA 时钟(DMA2 总线时钟)
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);

    DMA_InitTypeDef DMA_InitStruct;
    // 配置 DMA 通道(这里选择 DMA2 流0,通道0)
    DMA_InitStruct.DMA_Channel = DMA_Channel_0;
    // 配置 DMA 模式(此处为正常传输)
    DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
    // 配置 DMA 优先级(优先级高)
    DMA_InitStruct.DMA_Priority = DMA_Priority_High;
    // 配置 FIFO 模式(禁用 FIFO)
    DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable;
    // 配置 FIFO 阈值(此处不用设置,因为 FIFO 已禁用)
    DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
    // 设置外设和内存的基地址
    DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)srcBuffer;    // 源内存地址
    DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)destBuffer;       // 目标内存地址
    // 设置地址增量(这里使用地址递增模式)
    DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Enable;     // 外设地址递增
    DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;             // 内存地址递增
    // 设置数据大小(内存和外设数据宽度都为字(32位))
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;  // 外设数据宽度为字
    DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;          // 内存数据宽度为字
    // 设置数据传输方向(从内存到内存)
    DMA_InitStruct.DMA_DIR = DMA_DIR_MemoryToMemory;
    
    // 设置数据传输的大小(缓冲区的大小)
    DMA_InitStruct.DMA_BufferSize = BUFFER_SIZE;
    
    // 设置内存和外设的突发模式(使用单次传输)
    DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single;            // 内存突发模式
    DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;    // 外设突发模式
    
    // 初始化 DMA 配置
    DMA_Init(DMA2_Stream0, &DMA_InitStruct);
    // 使能 DMA 流
    DMA_Cmd(DMA2_Stream0, ENABLE);
}

3.4.2 轮询操作

extern uint32_t srcBuffer[BUFFER_SIZE];
extern uint32_t destBuffer[BUFFER_SIZE];

// 轮询 DMA 完成标志
void Polling_DMA_Transfer(void)
{
    // 轮询等待传输完成
    while (DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_TCIF0) == RESET)
    {
        // 可以添加其他处理逻辑(例如,LED指示、计时等)
    }
    // 清除 DMA 传输完成标志
    DMA_ClearFlag(DMA2_Stream0, DMA_FLAG_TCIF0);
    // 启动 DMA 传输 必须要启动 才能开启二次传输
    DMA_Cmd(DMA2_Stream0, ENABLE);
    // 处理目标数据,比如打印传输结果
    printf("轮询完成, destBuffer[0] = %d\n", destBuffer[0]);
}

int main()
{
    // 初始化
    Main_Init();
    // 初始化源数据
    for (int i = 0; i < BUFFER_SIZE; i++)
    {
        srcBuffer[i] = 100 + i;  // 填充源缓冲区数据
    }
    int i = 0;
    while (1)
    {
        srcBuffer[0] = 90 + i++;
        Polling_DMA_Transfer();
        Delay_ms(500);  // 延时 500ms
    }
}

3.4.3 中断操作

1. 中断配置

void DMA_NVIC_Init(void)
{
    // 启用 DMA 传输完成中断
    DMA_ITConfig(DMA2_Stream0, DMA_IT_TC, ENABLE);
    
    // 配置 DMA 中断优先级
    NVIC_InitTypeDef NVIC_InitStruct;
    NVIC_InitStruct.NVIC_IRQChannel = DMA2_Stream0_IRQn;    // DMA2 流0中断
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;   // 优先级
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;          // 子优先级
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;             // 使能中断
    NVIC_Init(&NVIC_InitStruct);
}

2. 中断服务函数

// DMA2 流0中断服务函数
void DMA2_Stream0_IRQHandler(void)
{
    // 检查 DMA 完成标志
    if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TCIF0))
    {
        // 清除 DMA 完成中断标志
        DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TCIF0);
        // 在此处处理 DMA 完成后的操作,例如:打印、处理数据等
        printf("中断打印 destBuffer = %d \n", destBuffer[0]);
    }
}

``

3、注意事项

在编程时,需要确保数据的发送端准备好数据后,启动 DMA 传输才能进行数据的传输。

// 清除 DMA 传输完成标志,确保没有残留的标志位
DMA_ClearFlag(DMA2_Stream0, DMA_FLAG_TCIF0);
// 启动 DMA 传输
DMA_Cmd(DMA2_Stream0, ENABLE);

3.5 DMA实验:存储器到外设传输

3.5.1 初始化配置

#include "Bsp_DMA_M_P.h"

#define BUFFER_SIZE 128

char txBuffer[BUFFER_SIZE];  // 用于存储待发送的数据

void DMA_M_P_Init(void)
{
    // 启动 DMA 时钟(DMA2 总线时钟)
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);

    DMA_InitTypeDef DMA_InitStruct;

    // 配置 DMA 通道(选择 DMA2 流 6,通道 5 对应 USART6)
    DMA_InitStruct.DMA_Channel = DMA_Channel_5;  // USART6 使用 DMA2 流6,通道5

    // 配置 DMA 模式(此处为正常传输)
    DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;

    // 配置 DMA 优先级(优先级高)
    DMA_InitStruct.DMA_Priority = DMA_Priority_High;

    // 配置 FIFO 模式(禁用 FIFO)
    DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable;

    // 配置 FIFO 阈值(此处不用设置,因为 FIFO 已禁用)
    DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;

    // 设置外设和内存的基地址
    DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART6->DR;  // 外设地址:USART6 数据寄存器
    DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)txBuffer;         // 内存地址:txBuffer

    // 设置地址增量(外设地址不递增,内存地址递增)
    DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;      // 外设地址不递增
    DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;                // 内存地址递增

    // 设置数据大小(8位数据)
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;  // 外设数据宽度为字节
    DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;          // 内存数据宽度为字节

    // 设置数据传输方向(从内存到外设)
    DMA_InitStruct.DMA_DIR = DMA_DIR_MemoryToPeripheral;

    // 设置数据传输的大小(缓冲区的大小)
    DMA_InitStruct.DMA_BufferSize = BUFFER_SIZE;

    // 设置内存和外设的突发模式(使用单次传输)
    DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single;              // 内存突发模式
    DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;      // 外设突发模式

    // 初始化 DMA 配置
    DMA_Init(DMA2_Stream6, &DMA_InitStruct);  // 使用 DMA2 流6

    // 使能 DMA 流
    DMA_Cmd(DMA2_Stream6, ENABLE);

    // 使能串口和DMA
    USART_DMACmd(USART6, USART_DMAReq_Tx, ENABLE);
}

3.5.2 轮询操作

需要注意的是,轮询和中断是由冲突的,所以我们在轮询的时候,就不要使用中断。

int main()
{
    // 初始化
    Main_Init();

    while (1)
    {
        // 初始化源数据
        strcpy(txBuffer, "张三\n");

        // 启动 DMA 传输 必须要启动 才能开启二次传输
        DMA_Cmd(DMA2_Stream6, ENABLE);

        // 处理目标数据,比如打印传输结果
        DMA_ClearFlag(DMA2_Stream6, DMA_FLAG_TCIF6);

        Delay_ms(1000);
    }
}

3.5.3 中断操作

1、中断配置

void DMA_Stream6_NVIC_Init(void)
{
    // 启用 DMA 传输完成中断
    DMA_ITConfig(DMA2_Stream6, DMA_IT_TC, ENABLE);

    // 配置 DMA 中断优先级
    NVIC_InitTypeDef NVIC_InitStruct;

    NVIC_InitStruct.NVIC_IRQChannel = DMA2_Stream6_IRQn;  // DMA2 流6中断
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;  // 优先级
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;         // 子优先级
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;            // 使能中断

    NVIC_Init(&NVIC_InitStruct);
}

2、中断服务函数

// DMA2 流6中断服务函数
void DMA2_Stream6_IRQHandler(void)
{
    // 检查 DMA 完成标志
    if (DMA_GetITStatus(DMA2_Stream6, DMA_IT_TCIF6))
    {
        // 清除 DMA 完成中断标志
        DMA_ClearITPendingBit(DMA2_Stream6, DMA_IT_TCIF6);

        // 打印信息,表示数据发送完成
        printf("发信息咯\n");
    }
}

3.6 DMA实验:外设到存储器传输

3.6.1 初始化配置

#include "Bsp_DMA_M_P.h"

#define BUFFER_SIZE 128

char rxBuffer[BUFFER_SIZE];  // 用于存储接收到的数据

void DMA_M_P_Init(void)
{
    // 启动 DMA 时钟(DMA2 总线时钟)
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);

    DMA_InitTypeDef DMA_InitStruct;

    // 配置 DMA 通道(选择 DMA2 流 0,通道 5 对应 USART6)
    DMA_InitStruct.DMA_Channel = DMA_Channel_5;  // USART6 使用 DMA2 流0,通道5

    // 配置 DMA 模式(此处为正常传输)
    DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;

    // 配置 DMA 优先级(优先级高)
    DMA_InitStruct.DMA_Priority = DMA_Priority_High;

    // 配置 FIFO 模式(禁用 FIFO)
    DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable;

    // 配置 FIFO 阈值(此处不用设置,因为 FIFO 已禁用)
    DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;

    // 设置外设和内存的基地址
    DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART6->DR;   // 外设地址:USART6 数据寄存器
    DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)rxBuffer;         // 内存地址:rxBuffer

    // 设置地址增量(外设地址不递增,内存地址递增)
    DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;      // 外设地址不递增
    DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;               // 内存地址递增

    // 设置数据大小(8位数据)
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;  // 外设数据宽度为字节
    DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;         // 内存数据宽度为字节

    // 设置数据传输方向(从外设到内存)
    DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralToMemory;

    // 设置数据传输的大小(缓冲区的大小)
    DMA_InitStruct.DMA_BufferSize = BUFFER_SIZE;

    // 设置内存和外设的突发模式(使用单次传输)
    DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single;           // 内存突发模式
    DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;   // 外设突发模式

    // 初始化 DMA 配置
    DMA_Init(DMA2_Stream0, &DMA_InitStruct);  // 使用 DMA2 流0

    // 使能 DMA 流
    DMA_Cmd(DMA2_Stream0, ENABLE);

    // 使能串口和 DMA
    USART_DMACmd(USART6 , USART_DMAReq_Rx , ENABLE);
}

3.6.2 轮询操作

int main()
{
    // 初始化
    Main_Init();

    while (1)
    {
        // 启动 DMA 传输
        DMA_Cmd(DMA2_Stream0, ENABLE);
        
        // 等待 DMA 完成数据接收
        while (DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_TCIF0) == RESET)
        {
            // 等待直到 DMA 完成
        }

        // 处理接收到的数据,比如打印接收到的内容
        printf("接收到的数据:%s\n", rxBuffer);

        // 清除 DMA 完成标志
        DMA_ClearFlag(DMA2_Stream0, DMA_FLAG_TCIF0);

        Delay_ms(1000);
    }
}

3.6.3 中断操作

1、中断配置

void DMA_Stream0_NVIC_Init(void)
{
    // 启用 DMA 传输完成中断
    DMA_ITConfig(DMA2_Stream0, DMA_IT_TC, ENABLE);

    // 配置 DMA 中断优先级
    NVIC_InitTypeDef NVIC_InitStruct;

    NVIC_InitStruct.NVIC_IRQChannel = DMA2_Stream0_IRQn;  // DMA2 流0中断
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;  // 优先级
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;         // 子优先级
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;            // 使能中断

    NVIC_Init(&NVIC_InitStruct);
}

2、中断服务函数

// DMA2 流0中断服务函数
void DMA2_Stream0_IRQHandler(void)
{
    // 检查 DMA 完成标志
    if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TCIF0))
    {
        // 清除 DMA 完成中断标志
        DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TCIF0);

        // 处理接收到的数据
        printf("接收到的信息:%s\n", rxBuffer);
    }
}

第四章 SPI 通讯协议

4.1 SPI 概述

4.1.1 SPI 的定义与功能

1、SPI 的定义

SPI(Serial Peripheral Interface,串行外设接口)是一种用于微控制器与外部设备之间的同步串行通信协议。它由摩托罗拉公司在 1980 年代初期提出,广泛用于嵌入式系统中,特别是在需要快速、可靠的短距离数据传输的场合。

SPI 通常用于连接微控制器与外设,如传感器、存储器、显示屏、数码管等。它是全双工通信协议,即在同一时刻可以同时进行数据的发送与接收。

2、SPI 的主要功能

  • 全双工数据传输:SPI 支持同时发送和接收数据,这使得它在数据传输效率上具有较大的优势。

  • 主从模式:SPI 工作时有一个主设备和一个或多个从设备,主设备负责控制数据的传输时序。

  • 高速通信:SPI 可以提供较高的传输速度,适合需要快速数据传输的应用。

  • 同步通信:SPI 的数据传输由时钟信号(SCK)同步,确保发送和接收数据的时序一致。

4.1.2 SPI 工作原理

SPI 通信使用四根主要的信号线来传输数据,它们分别是:

  • MOSI(Master Out Slave In):主设备输出,数据由主设备传输到从设备。

  • MISO(Master In Slave Out):主设备输入,数据由从设备传输到主设备。

  • SCK(Serial Clock):时钟信号,由主设备生成,用于同步数据传输。

  • SS(Slave Select):片选信号,用于选择从设备进行通信。

SPI 使用的是同步串行通信的方式,数据传输过程完全由时钟信号控制。具体的工作流程如下:

  1. 时钟同步:主设备生成时钟信号(SCK),从设备根据该时钟信号同步接收和发送数据。

  2. 数据传输:数据通过 MOSI 和 MISO 两条线同时传输。主设备发送数据的同时,接收来自从设备的数据。

  3. 片选信号(SS):当主设备要与某个从设备通信时,需要通过片选信号(SS)选择特定的从设备。片选信号通常是低电平有效。

  4. 数据传输的顺序:数据通常是以字节为单位进行传输,传输过程中每个时钟周期传输一个比特位,所有比特位按顺序传输。

数据传输流程:

  1. 主设备首先拉低片选信号(SS),激活从设备。

  2. 主设备通过 MOSI 线发送数据,数据位随着时钟信号的变化而发送。

  3. 同时,主设备通过 MISO 线接收从设备的数据。

  4. 当数据传输完成后,主设备将片选信号(SS)置为高电平,结束与从设备的通信。

4.1.3 SPI 的优缺点

1、优点

  1. 高速度:由于 SPI 是同步的全双工协议,可以在较高的频率下进行数据传输,适合需要快速数据传输的应用。

  2. 简单易实现:SPI 的硬件和协议结构相对简单,易于实现。

  3. 全双工通信:SPI 支持全双工数据传输,可以同时进行数据的接收与发送,提高了通信效率。

  4. 灵活性:SPI 支持多主机和多从设备配置,方便进行多设备的通信。

  5. 低延迟:由于数据传输完全由时钟控制,传输时延较低,适合实时性要求较高的应用。

2、缺点:

  1. 需要更多的引脚:相较于其他串行通信协议(如 I2C),SPI 需要更多的引脚(SCK、MOSI、MISO、SS),对于引脚资源有限的嵌入式系统可能不太适用。

  2. 无法支持多主设备:SPI 设计时并不支持多个主设备之间的通信,如果要实现多个主设备,需要通过额外的控制机制。

  3. 没有设备地址机制:SPI 通常通过片选信号(SS)来选择从设备,因此在大规模设备网络中,需要较多的片选引脚,不如 I2C 那样可以通过地址来管理设备。

  4. 数据传输距离有限:由于 SPI 使用并行信号线,数据传输的距离较短,通常只能在短距离的设备间使用。

4.1.4 SPI 与其他通讯协议的对比

特性SPII2CUSART
传输模式全双工半双工全双工
最大设备数无限制(需多个片选信号)支持多个设备(通过地址)单设备通信
数据传输速度高速(一般可达到数 Mbps)较低(一般为几百 kbps)较低(可达数 Mbps)
引脚需求4 根引脚(SCK、MOSI、MISO、SS)2 根引脚(SCL、SDA)2 根引脚(TX、RX)
应用场景高速数据传输、设备间点对点通信低速设备通信、多个设备管理通用数据通信、串行接口
设备选择通过片选信号(SS)选择从设备通过地址选择设备无设备选择机制
优点高速、全双工、简单低引脚需求、多设备管理通用、灵活
缺点引脚需求高、无地址机制速度较慢、设备选择较为复杂速度较慢、距离较短
  • SPI 是一种高速、全双工的同步串行通信协议,适合用于需要高速数据传输的应用场合,尤其在短距离内进行点对点通信时表现优越。

  • I2C 在设备数量较多时非常有优势,但速度较慢,适用于传输量较小的设备。

  • USART 作为一种通用的串行通信协议,广泛应用于各种串行数据传输,但速度相对较慢,适合长距离通信。

选择适合的通信协议需要根据具体的应用需求、数据传输速率和硬件资源来决定。

4.2 SPI 总线结构

4.2.1 SPI 主从模式

SPI(Serial Peripheral Interface)是一种全双工的同步串行通信协议,它采用主从式结构,在一条总线上由一个主设备(Master)控制一个或多个从设备(Slave)进行数据通信。

1、主设备(Master)

  • 负责产生 时钟信号(SCK),用于驱动数据同步。

  • 控制 片选信号(SS),选择一个具体的从设备进行通信。

  • 发送数据至从设备(通过 MOSI 线),同时接收从设备的数据(通过 MISO 线)。

2、从设备(Slave)

  • 接收主设备的时钟信号,按时钟同步进行数据传输。

  • 只有当自己的片选信号(SS)被拉低时,才会响应主设备。

  • 接收来自主设备的数据(MOSI),也可以将数据发送给主设备(MISO)。

3、通信流程示意

  1. 主设备将片选信号(SS)拉低,选中目标从设备。

  2. 主设备通过 MOSI 向从设备发送数据,同时接收从设备通过 MISO 返回的数据。

  3. 每次数据传输都由 SCK 时钟信号驱动,确保主从同步。

  4. 传输完成后,主设备将 SS 拉高,断开通信。

4、主从模式特点

  • 一个主设备 可以控制 多个从设备,但同一时间只能与一个从设备通信。

  • 所有设备共享 SCK、MOSI、MISO,每个从设备独占一个 SS 线

  • 数据传输过程中,主从设备在 每个时钟周期 同时发送与接收一个字节。

4.2.2 SPI 数据线说明

SPI 通信协议采用四条主要的数据线来实现主从设备之间的数据交换与同步,分别是:

1、SCK(Serial Clock 串行时钟)

SCL、SCLK、CLK

  • 由主设备产生的时钟信号,用于同步数据的发送与接收。

  • 所有从设备共享该时钟线。

  • 数据在 SCK 上升沿或下降沿变化或采样(由极性相位决定)。

2、MOSI(Master Out Slave In 主出从入)

  • 主设备输出数据,从设备接收数据。

  • 数据从主设备通过该线发送到从设备。

  • 所有从设备共享该线,但只有被片选的从设备响应数据。

3、MISO(Master In Slave Out 主入从出)

  • 从设备输出数据,主设备接收数据。

  • 数据从从设备通过该线发送到主设备。

  • 当有多个从设备时,只有被选中的从设备可驱动该线。

4、SS(Slave Select 从设备选择)

NSS、NCS、CC、CS

  • 片选信号线,由主设备控制,用于选择与哪个从设备进行通信。

  • 每个从设备对应一根独立的 SS 线。

  • 当主设备将 SS 拉低时,所选从设备被激活,进入通信状态;否则从设备忽略总线信号。

4.2.3 SPI 时钟配置

SPI 的时钟信号由主设备提供,决定数据传输的节奏与时序。SPI 中有两个重要的参数会影响数据的采样和传输时机:

  • CPOL(Clock Polarity,时钟极性)

  • CPHA(Clock Phase,时钟相位)

这两个参数共同决定了 SPI 的四种工作模式。

1、 CPOL(时钟极性)

CPOL 控制空闲时钟线(SCK)的电平状态:

  • CPOL = 0:空闲时,SCK 保持低电平。

  • CPOL = 1:空闲时,SCK 保持高电平。

2、 CPHA(时钟相位)

CPHA 决定数据在哪一个时钟边沿采样:

  • CPHA = 0:第一个边沿采样数据(采样边沿为第一个有效边沿)

  • CPHA = 1:第二个边沿采样数据(采样边沿为第二个有效边沿)

注意:边沿是相对于 CPOL 设置下的 SCK 边沿。

3、 SPI 的四种模式

由 CPOL 和 CPHA 组合可形成 4 种 SPI 模式:

模式CPOLCPHASCK 空闲状态采样边沿传输边沿
000低电平上升沿下降沿
101低电平下降沿上升沿
210高电平下降沿上升沿
311高电平上升沿下降沿

主机与从机的 SPI 模式(CPOL/CPHA)必须一致,否则会导致通信失败或数据错误。

4、模式0

CPOL=0:空闲状态时,SCK为低电平
CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

5、模式1

CPOL=0:空闲状态时,SCK为低电平
CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
采样

6、模式2

CPOL=1:空闲状态时,SCK为高电平
CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

7、模式3

交换一个字节(模式3)
CPOL=1:空闲状态时,SCK为高电平
CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

4.2.4 SPI 数据传输顺序(MSB/LSB)

在 SPI 通信中,数据通常以字节为单位进行发送,而每一个字节中的 比特(bit)传输顺序可以配置为:

  • MSB 先传输(Most Significant Bit,最高有效位)

  • LSB 先传输(Least Significant Bit,最低有效位)

这一配置关系到 主设备与从设备的比特对齐方式是否一致,如果不一致将会导致接收到的数据错误。

1、 MSB 优先(默认方式)

  • 数据的最高位(bit7)先从 MOSI/MISO 线上开始传输。

  • 是大多数 SPI 器件默认的传输顺序。

例子:

要发送的数据为 0b10110001,传输顺序如下:

MSB优先:1 → 0 → 1 → 1 → 0 → 0 → 0 → 1

2、 LSB 优先

  • 数据的最低位(bit0)先开始传输。

  • 部分特定外设或协议需要使用此模式。

例子:

要发送的数据为 0b10110001,传输顺序如下:

LSB优先:1 → 0 → 0 → 0 → 1 → 1 → 0 → 1

主设备与从设备的数据传输顺序必须保持一致,否则接收方将解析出错误的数据。

模式优点注意事项
MSB 优先符合大多数芯片默认协议默认选择,兼容性好
LSB 优先某些自定义协议使用使用前需确保主从设备都支持此模式

4.4 SPI 库函数

4.4.1 初始化与配置函数

// 将 SPI 外设寄存器恢复为默认复位状态
void SPI_I2S_DeInit(SPI_TypeDef* SPIx);

// 初始化 SPI 外设
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);

// 结构体初始化为默认参数
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);

// 使能或禁用 SPI 外设
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);

// 设置 SPI 数据帧宽度(8 位或 16 位)
void SPI_DataSizeConfig(SPI_TypeDef* SPIx, uint16_t SPI_DataSize);

// 设置双向数据模式下的方向(只用于半双工)
void SPI_BiDirectionalLineConfig(SPI_TypeDef* SPIx, uint16_t SPI_Direction);

// 软件方式设置 NSS 引脚的状态
void SPI_NSSInternalSoftwareConfig(SPI_TypeDef* SPIx, uint16_t SPI_NSSInternalSoft);

// 控制 NSS 输出引脚使能(主模式下)
void SPI_SSOutputCmd(SPI_TypeDef* SPIx, FunctionalState NewState);

// 启用 TI 模式(与 TI 从设备通信)
void SPI_TIModeCmd(SPI_TypeDef* SPIx, FunctionalState NewState);

4.4.2 数据收发函数

// 向 SPI 发送一个数据帧(8 或 16 位)
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);

// 从 SPI 接收一个数据帧(8 或 16 位)
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);

4.4.3 DMA 管理函数

// 启用 SPI 的 DMA 请求(发送或接收)
void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);

4.4.4 中断与标志位管理函数

// 使能或禁用 SPI 中断源
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);

// 获取指定标志位的状态(SET/RESET)
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);

// 清除指定标志位
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);

// 获取中断状态(是否触发)
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);

// 清除中断挂起位
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);

4.5.5 SPI 结构体

/**
  * @brief SPI 初始化结构体定义(分组说明)  
  */
typedef struct
{
  // ==== 通信方向与模式配置 ====
  uint16_t SPI_Direction;           // 设置数据方向:全双工、单线双向、只接收等
  uint16_t SPI_Mode;                // 设置 SPI 工作模式:主模式或从模式

  // ==== 数据格式配置 ====
  uint16_t SPI_DataSize;            // 设置数据帧长度:8 位或 16 位
  uint16_t SPI_FirstBit;            // 设置先发送的位:MSB 或 LSB

  // ==== 时钟相关配置 ====
  uint16_t SPI_CPOL;                // 设置时钟极性:空闲时为高电平或低电平
  uint16_t SPI_CPHA;                // 设置时钟相位:第一个边沿或第二个边沿采样
  uint16_t SPI_BaudRatePrescaler;   // 设置波特率分频系数(影响通信速度)
                                     // 示例:SPI_BaudRatePrescaler_8 表示主频/8

  // ==== 片选 NSS 配置 ====
  uint16_t SPI_NSS;                 // 设置 NSS 管理方式:硬件管理或软件管理

  // ==== CRC 配置(如使用 CRC 校验) ====
  uint16_t SPI_CRCPolynomial;       // 设置 CRC 校验使用的多项式值(若启用 CRC)
} SPI_InitTypeDef;

4.5.6 中断标识

4.5.7 初始化配置注意事项

在配置 SPI 时,GPIO 引脚的模式设置非常关键,直接影响通信是否正常。

1、 引脚模式设置建议

引脚模式说明
SCK复用推挽输出主机产生时钟信号
MOSI复用推挽输出主机发送数据
MISO复用上拉输入主机接收从机数据
NSS普通推挽输出(主机)或复用输入(从机)软件控制更灵活
  • 推挽输出:信号稳定,适用于 SCK/MOSI/NSS。

  • 上拉输入:防止 MISO 悬空,避免杂波干扰。

2、 NSS 配置建议

  • 主机模式:建议使用 软件控制 NSS,配置为普通 GPIO 输出,手动拉低/拉高。

  • 从机模式:可使用 硬件 NSS,自动响应主机的片选信号。

SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 主机用得最多

2、 小结

  1. 所有 SPI 引脚要用复用功能(AF);

  2. 主机输出引脚(SCK/MOSI)设为推挽;

  3. MISO 输入建议上拉;

  4. NSS 推荐软件控制,方便灵活多设备通信。

第六章 SPI通讯实验

4.5 SPI 实验:数码管

4.5.1 器件介绍:数码管

1、数码管

数码管(LED Segment Display)是一种由 8 个发光二极管 组成的显示器件。它们按照特定的图形封装,其中 7 个 LED 构成数字或字母显示的基本笔画,第 8 个 LED 用作小数点,因此又称 八段数码管

数码管的位数通常有:半位、1 位、2 位、3 位、4 位、5 位、6 位、8 位、10 位等;显示颜色常见有红色、绿色、蓝色、黄色等。

理解数码管的工作原理对于后续编程控制非常重要——不同类型的数码管不仅电路结构不同,控制方法也会有差异。

2、基本结构

数码管按内部连接方式主要分为 共阴极型共阳极型 两类:

  • 共阴极数码管:所有 LED 的阴极(负极)共用,连接至地。要点亮某段 LED,需要在对应段脚上施加高电平。

  • 共阳极数码管:所有 LED 的阳极(正极)共用,连接至电源。要点亮某段 LED,需要在对应段脚上施加低电平。

它们的电气连接方式不同,但基本的发光原理一致。如下图为 1 位数码管的原理图:

3、显示原理

要控制数码管正常显示,需满足两个条件:

  1. 为公共端提供正确电源电压,同时在每个发光二极管前串联限流电阻;

  2. 控制对应段脚的电平,使所需的 LED 被点亮。

共阴极数码管 为例,公共端需接地,各段(a~g, dp)接单片机引脚。当某段需要点亮时,单片机输出高电平。

如下图所示,单片机可通过设置输出引脚的高低电平,控制各段的亮灭,从而显示指定的字符或数字。

为了简化程序设计,我们通常将数字和字母的显示编码预先存储在一个数组中。例如:

// 共阴极数码管编码表(a~g, dp)
0x3F,  // "0"
0x06,  // "1"
0x5B,  // "2"
0x4F,  // "3"
0x66,  // "4"
0x6D,  // "5"
0x7D,  // "6"
0x07,  // "7"
0x7F,  // "8"
0x6F,  // "9"
0x77,  // "A"
0x7C,  // "B"
0x39,  // "C"
0x5E,  // "D"
0x79,  // "E"
0x71,  // "F"
0x76,  // "H"
0x38,  // "L"
0x40,  // "-"
0x00   // 全灭

对于共阳极数码管,只需将以上编码逐位取反即可得到对应控制码。

4.5.2 器件介绍:74HC595芯片

1、芯片简介

74HC595 是一款常用的 串行输入/并行输出(SIPO) 类型的移位寄存器芯片,属于 74 系列高性能 CMOS 器件。该芯片具备 8 位移位寄存器8 位锁存寄存器,可以通过少量的引脚实现对多个输出口的控制,常用于 数码管控制、LED矩阵驱动、IO扩展等场景

在 STM32 等单片机开发中,74HC595 的作用如下:

  • IO口扩展:只需通过3个IO(数据、时钟、锁存)就能控制多个输出引脚,节省宝贵的单片机引脚资源;

  • 串行控制并行显示:非常适合驱动数码管、LED等需要并行控制的设备;

  • 多芯片级联:多个 74HC595 可通过串行级联组成更多位的控制系统(如2片组合控制16位数据);

  • 锁存输出数据:内部锁存寄存器可保证显示内容在更新过程中保持稳定,避免闪烁;

结构特点

  • 串行输入(SER):每次输入一位数据;

  • 移位时钟(SRCLK):输入一个脉冲,数据向左移位一位;

  • 存储时钟(RCLK):将移位寄存器中的内容锁存到输出端 Q0~Q7;

  • 输出使能(OE,低电平有效):控制是否允许输出;

  • 串行输出(QH'):用于级联下一个 74HC595 的 SER 引脚。

通过合理编程控制 SER、SRCLK 和 RCLK,可以轻松实现对 74HC595 输出状态的全面控制,是嵌入式系统中非常实用的显示与控制芯片。

2、引脚说明

74HC573 有多种封装形式,常见的为 DIP-20(20引脚)SOIC-20(20引脚贴片) 封装。但在某些精简版本中,也存在 16引脚封装。如下图所示:

在本项目中,我们使用的是 16引脚版本的 74HC573 芯片

下表为 74HC573 各引脚的功能简要说明:

注:引脚上方带有一条横线,表示“低电平有效”或“低电平使能”。

引脚名说明
QA,QB,QC,QD,QE,QF,QG,QH并行数据输出
QH`串行数据输出
SRCLR主复位引脚,为0时复位移位寄存器
SRCLK SPI CLK移位寄存器时钟输入,上升沿时SER

上的数据会移入移位寄存器
RCLK存储寄存器时钟输入,上升沿时移位寄存器的数据传输到存储寄存器
OE SPI CS输出使能,为0时存储器中的数据并行输出到QA-QH引脚,为1时,输出为高阻态
SER SPI MOSI串行数据输入
名称对应正式引脚中文名称功能说明
OEOE(13脚)输出使能(低电平有效)控制 Q0~Q7 输出是否有效。低电平时输出生效,高电平时为高阻态,常接地(0)使能输出。
RCLKSTCP(12脚)存储时钟(锁存时钟)也叫锁存时钟。上升沿时将移位寄存器的数据“锁存”到输出寄存器,使 Q0~Q7 显示最新数据。
SERSER(14脚)串行数据输入这是数据输入引脚,数据一位一位通过这个引脚送入。
SRCLKSHCP(11脚)移位时钟每来一个上升沿,SER 口上的数据就进入移位寄存器,旧数据向左移动一位。
SRCLRMR(10脚)移位寄存器清除(低电平有效)当此引脚为低电平时,移位寄存器内容清零。正常使用时应接高电平(1)
QHQ7(7脚)第八位并行输出第 8 位的并行输出,对应一个输出控制位,可控制 LED 或数码管某一段。
`QH``Q7'(9脚)串行输出(用于级联)把当前芯片移位寄存器中最后一位输出,用于连接下一个 74HC595 的 SER,实现级联扩展。

3、工作原理

74HC595数据发送过程

74HC595级联数据发送流程

4.5.3 电路分析

SPI2_NSS PB12
SPI2_SCK PB13
SPI2_MOSI PB15

### 4.5.5 程序设计

1、段码表 & 位码表

// 共阴极数码管段码表(0~9)
const uint8_t seg_code[10] = {
    0x3F, // 0 -> 0011 1111
    0x06, // 1 -> 0000 0110
    0x5B, // 2 -> 0101 1011
    0x4F, // 3 -> 0100 1111
    0x66, // 4 -> 0110 0110
    0x6D, // 5 -> 0110 1101
    0x7D, // 6 -> 0111 1101
    0x07, // 7 -> 0000 0111
    0x7F, // 8 -> 0111 1111
    0x6F  // 9 -> 0110 1111
};

// 位码控制(哪一位数码管亮),Q0-Q3 控制四个数码管
const uint8_t pos_code[4] = {
    0x01, // 第一位
    0x02, // 第二位
    0x04, // 第三位
    0x08  // 第四位
};

2、 SPI2 初始化函数

/**
 * @brief 初始化 SPI2 接口以及相关 GPIO(PB12、PB13、PB15)
 */
void Nixie_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    SPI_InitTypeDef SPI_InitStructure;

    // 开启时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);

    // 配置 PB13(SCK)、PB15(MOSI) 为复用推挽上拉输出
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    GPIO_PinAFConfig(GPIOB, GPIO_PinSource13, GPIO_AF_SPI2); // SCK
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource15, GPIO_AF_SPI2); // MOSI

    // 配置 PB12(RCLK)为普通推挽输出
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    GPIO_SetBits(GPIOB, GPIO_Pin_12); // 初始状态拉高

    // SPI2 模块配置
    SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx;   // 单线发送
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;               // 主模式
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;           // 8位数据
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;                  // 空闲低电平
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;                // 第一个边沿采样
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;                   // 软件 NSS
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64;
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
    SPI_Init(SPI2, &SPI_InitStructure);

    SPI_Cmd(SPI2, ENABLE); // 启动 SPI2
}

3、 SPI2 发送一个字节函数

/**
 * @brief 使用 SPI2 发送一个字节
 * @param data 要发送的数据
 */
void SPI2_SendByte(uint8_t data)
{
    while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET); // 等待缓冲区空
    SPI_I2S_SendData(SPI2, data);                                   // 发送数据
    while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_BSY) == SET);   // 等待发送完成
}

4、 发送段码和位码到 74HC595

/**
 * @brief 向 2 片级联的 74HC595 芯片发送段码和位码
 * @param seg 段码(显示数字)
 * @param pos 位码(选择哪一位数码管)
 */
void HC595_Send(uint8_t seg, uint8_t pos)
{
    GPIO_ResetBits(GPIOB, GPIO_Pin_12); // RCLK 置低,准备发送
    SPI2_SendByte(pos);                 // 先发后级(控制位码)
    SPI2_SendByte(seg);                 // 后发前级(控制段码)
    GPIO_SetBits(GPIOB, GPIO_Pin_12);   // RCLK 上升沿,锁存
}

5、 数码管数字显示函数

/**
 * @brief 显示一个 0~9999 的数字
 * @param num 要显示的数字
 */
void Nexie_Show(uint16_t num)

{

	uint8_t seg_buf[4]; // 存储四位的段码
	
	// 限制最大值为 9999,防止越界
	if (num > 9999) num = 9999;
	
	// 拆分出每一位(千、百、十、个)
	seg_buf[0] = seg_code[num / 1000]; // 千位
	seg_buf[1] = seg_code[(num / 100) % 10]; // 百位
	seg_buf[2] = seg_code[(num / 10) % 10]; // 十位
	seg_buf[3] = seg_code[num % 10]; // 个位
	
	// 动态刷新四位数码管
	for (int i = 0; i < 4; i++)
	{
		HC595_Send(seg_buf[i], pos_code[i]);
		Delay_ms(2); // 每位延时 2ms
	}

}

课堂练习 :自动去除前导 0014

第七章 I2C 通讯协议

5.1 I2C 总体概述

5.1.1 什么是 I2C?

  • 定义:I2C(Inter-Integrated Circuit)是一种同步串行通信协议,采用半双工方式传输数据。

  • 历史背景:由 Philips(现 NXP)于 1982 年提出,旨在简化不同 IC 之间的通信。

  • 特点

    • 多主从结构:可以有多个主设备和从设备。

    • 低成本:通过两根信号线(SDA 和 SCL)进行通信,降低硬件成本。

    • 两线制:只需要两根信号线(数据线 SDA 和时钟线 SCL)即可完成设备间的数据交换。

5.1.2 应用场景

  • 传感器通信:常用于温湿度传感器、加速度计、陀螺仪等传感器的数据采集。

  • 小容量存储器:如 EEPROM(电可擦可编程只读存储器)等存储器的访问。

  • 低速外设控制:包括 LCD 显示屏、实时时钟(RTC)、GPIO 扩展等设备的控制。

5.2 I2C 物理层特性

5.2.1 I2C 总线结构

I2C 总线由两条信号线构成:数据线 SDA(Serial Data Line)和时钟线 SCL(Serial Clock Line)。这两条线用于传输数据和时钟信号。以下是一些关键概念:

  • 数据线 SDA:用于传输双向数据。主设备和从设备可以在 SDA 上进行数据的读写操作。

  • 时钟线 SCL:用于提供时钟信号,确保数据传输的同步。

I2C 总线采用 开漏输出模式,即设备不会直接驱动信号线的高电平,而是通过拉低信号线来表示数据的传输,信号线的高电平由外部的上拉电阻提供。

5.2.2 多设备连接

I2C 协议支持多主从架构,多个设备可以连接到同一条总线上。所有 I2C 设备的 SCL 和 SDA 信号线是共享的:

  • SCL 线连接:所有 I2C 设备的 SCL 线连接在一起,由主设备控制时钟信号的发出。

  • SDA 线连接:所有 I2C 设备的 SDA 线也连接在一起,用于数据传输。主设备和从设备通过在 SDA 线上发送特定的数据来进行通信。

5.2.3 开漏输出与上拉电阻

  • 开漏输出:I2C 的 SCL 和 SDA 信号线采用开漏输出模式,意味着设备只能拉低信号线,无法直接拉高。拉高电平由 上拉电阻 提供。

  • 上拉电阻:SDA 和 SCL 信号线需要连接上拉电阻,一般选择 4.7KΩ 的电阻。这些电阻确保当没有设备在拉低信号线时,信号线能恢复到高电平状态。

5.2.4 电气特性

  • 电源电压:I2C 总线支持的电压范围通常为 1.8V 到 5V,不同的电压水平决定了 I2C 总线的通信速率和电气特性。

  • 信号电平:I2C 总线的高电平由上拉电阻决定,低电平由设备主动拉低信号线产生。

5.2.5 总线拓扑

I2C 总线拓扑采用 树型结构,每个设备通过唯一的地址进行识别。主设备负责管理总线,而从设备则响应主设备的命令。多个设备可以连接到同一总线,但每个设备的地址必须唯一。

5.3 I2C 协议层详解

5.3.1 起始与停止

I2C 通信通过“起始”和“停止”信号来标识一次通信过程的开始与结束。

  • 起始条件(START)
    SCL 为高电平 时,SDA 从高电平变为低电平,表示通信开始。

  • 停止条件(STOP)
    SCL 为高电平 时,SDA 从低电平变为高电平,表示通信结束。

5.3.2 读取和写入

I2C 通信以“字节”为单位,每个字节为 8 位,高位在前,低位在后

1、 写数据流程(主机 → 从机):

  1. 主机在 SCL 为低电平 时将一个数据位写入 SDA。

  2. 然后拉高 SCL(从机读取此位),保持 SDA 稳定。

  3. 重复 8 次,发送 1 个完整字节。

2、 读数据流程(从机 → 主机):

  1. 主机释放 SDA,由从机输出数据位。

  2. SCL 为低电平 时,从机准备好 SDA 电平。

  3. 主机拉高 SCL,读取 SDA 上的电平。

  4. 重复 8 次,接收 1 个完整字节。

⚠️ 注意:数据在 SCL 高电平期间必须保持稳定,不能改变!

信号0 和 信号1 的表示方式如下:

8 个连续的数据位构成完整数据字节,如下图:

5.3.3 发送应答和接收应答

I2C 采用应答机制来确认每个字节是否被成功接收。

1、 主机发送完一个字节后:

  • 从机会在下一个时钟周期拉低 SDA(ACK),表示“收到”。

  • 如果 SDA 保持高电平(NACK),表示“未收到”或“拒绝”。

2、 主机接收完一个字节后:

  • 主机会在下一个时钟周期主动拉低 SDA,表示“我接收到了”。

  • 若发送 NACK(SDA 为高),表示“我不再接收”。

💡 主机在接收前要先释放 SDA,让从机驱动。

5.3.4 设备地址与读写数据位

I2C 通信中,每个从设备都有一个唯一的地址。

  • 地址通常为 7 位(也有 10 位扩展格式)。

  • 紧接地址后面的第 8 位是 读写控制位

    • 0 表示写操作(Master → Slave)

    • 1 表示读操作(Slave → Master)

5.3.4 读数据帧

读数据流程如下:

  1. 主机发送起始信号 + 设备地址 + 写标志

  2. 发送寄存器地址

  3. 再次发送起始信号 + 设备地址 + 读标志

  4. 从机连续发送数据,主机读取

  5. 主机通过 ACK/NACK 控制是否继续读取

  6. 最后发送停止信号结束通信

5.3.5 写数据帧

写数据流程如下:

  1. 主机发送起始信号 + 设备地址 + 写标志

  2. 主机发送寄存器地址

  3. 主机发送要写入的数据(可连续写多个)

  4. 每次数据后从机会发送 ACK

  5. 写完后主机发送停止信号

5.3.6 主机发送

5.3.7 主机接收

5.4 库函数

5.4.1 库函数解释

1、复位与初始化函数

void I2C_DeInit(I2C_TypeDef* I2Cx);                         
// 将指定的 I2C 外设寄存器重置为默认值,通常用于系统初始化或错误恢复。

void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);  
// 根据配置结构体初始化 I2C 外设,包括时钟速度、地址模式等。

void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);       
// 给 I2C_InitStruct 结构体赋默认值,用户可在其基础上进行修改。

void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);  
// 启动或关闭 I2C 外设。NewState 为 ENABLE 表示使能,DISABLE 表示关闭。

2、模式与地址配置函数

void I2C_DigitalFilterConfig(I2C_TypeDef* I2Cx, uint16_t I2C_DigitalFilter);
// 配置 I2C 数字滤波器,增强抗干扰能力。

void I2C_AnalogFilterCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
// 启用或禁用模拟滤波器,默认建议启用以滤除电气噪声。

void I2C_FastModeDutyCycleConfig(I2C_TypeDef* I2Cx, uint16_t I2C_DutyCycle);
// 设置快速模式下的占空比(如 16/9 或 2),影响数据传输速度。

void I2C_NACKPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_NACKPosition);
// 配置 NACK(非应答)信号的位置,用于接收操作的终止控制。

void I2C_SMBusAlertConfig(I2C_TypeDef* I2Cx, uint16_t I2C_SMBusAlert);
// 设置 SMBus 警报配置,用于与 SMBus 设备通信。

void I2C_ARPCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
// 启用或禁用地址解析协议(ARP)功能,仅用于 SMBus。

void I2C_OwnAddress2Config(I2C_TypeDef* I2Cx, uint8_t Address);
// 设置第二个从设备地址,用于双地址应答模式。

void I2C_DualAddressCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
// 启用或禁用双地址模式。

void I2C_GeneralCallCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
// 启用或禁用对通用呼叫地址的应答功能(地址 0x00)。

void I2C_SoftwareResetCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
// 启用软件复位,常用于复位 I2C 状态机。

void I2C_StretchClockCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
// 启用或禁用时钟拉伸(Clock Stretching),允许从设备延迟主设备。

void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);
// 设置应答位,决定是否对接收到的数据自动应答。

3、通信控制函数

void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
// 发送或停止 START 条件,启动一次通信。

void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);
// 发送或取消 STOP 条件,结束通信。

void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);
// 发送从设备 7 位地址,并指定读写方向(I2C_Direction)。

4、数据传输函数

void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);       
// 向 I2C 数据寄存器写入一个字节的数据,开始发送。

uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);               
// 从 I2C 数据寄存器读取接收到的一个字节数据。

5、PEC 管理函数(PEC Management)

void I2C_TransmitPEC(I2C_TypeDef* I2Cx, FunctionalState NewState);
// 使能或禁用 PEC(Packet Error Checking)发送功能。

void I2C_PECPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_PECPosition);
// 配置 PEC 字节的发送位置(当前字节或下一个字节)。

void I2C_CalculatePEC(I2C_TypeDef* I2Cx, FunctionalState NewState);
// 启用或禁用 PEC 校验计算。

uint8_t I2C_GetPEC(I2C_TypeDef* I2Cx);
// 获取当前计算的 PEC 值,用于错误校验。

6、DMA 管理函数

void I2C_DMACmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
// 启用或禁用 I2C DMA 功能,便于大批量数据传输。

void I2C_DMALastTransferCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
// 设置是否为 DMA 的最后一次传输,用于传输控制。

7. 中断和标志位管理

void I2C_ITConfig(I2C_TypeDef* I2Cx, uint16_t I2C_IT, FunctionalState NewState);
// 开启或关闭指定的中断类型(如事件中断、错误中断等)。

uint16_t I2C_ReadRegister(I2C_TypeDef* I2Cx, uint8_t I2C_Register);
// 读取 I2C 外设指定寄存器的值,用于调试或状态获取。

FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);
// 获取指定标志位当前状态,如 BUSY、TXE、RXNE 等。

void I2C_ClearFlag(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);
// 清除指定标志位(主要用于某些特殊用途的标志)。

ITStatus I2C_GetITStatus(I2C_TypeDef* I2Cx, uint32_t I2C_IT);
// 获取中断标志位状态,判断是否发生了对应中断。

void I2C_ClearITPendingBit(I2C_TypeDef* I2Cx, uint32_t I2C_IT);
// 清除中断挂起位,通常在中断服务函数中调用。

8. 状态监控函数

Basic 状态监控
ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);
// 检查指定事件是否已经发生,常用于判断通信是否成功。
Advanced 状态监控
uint32_t I2C_GetLastEvent(I2C_TypeDef* I2Cx);
// 获取最近一次发生的事件值,便于分析通信状态。
Flag 状态监控
FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);
// 判断指定标志位当前是否置位,用于轮询或状态判断。

5.4.2 结构体介绍

typedef struct  
{  
  //===============================  
  // 1. 时钟配置  
  //===============================  
  uint32_t I2C_ClockSpeed;          /**< I2C 通信时钟频率(单位:Hz),必须 ≤400kHz */  
  
  //===============================  
  // 2. 工作模式与时序
  //===============================  
  uint16_t I2C_Mode;                /**< I2C 工作模式选择,例如:主模式/从模式,参见 @ref I2C_mode */  
  uint16_t I2C_DutyCycle;           /**< 快速模式下的时钟占空比,可选 2:1 或 16:9,参见 @ref I2C_duty_cycle_in_fast_mode */  
  
  //===============================  
  // 3. 地址配置  
  //===============================  
  uint16_t I2C_OwnAddress1;         /**< 设备的自身地址(支持 7 位或 10 位格式) */  
  uint16_t I2C_AcknowledgedAddress; /**< 主机对从机地址的应答模式(7 位或 10 位),参见 @ref I2C_acknowledged_address */  
  
  //===============================  
  // 4. 应答控制  
  //===============================  
  uint16_t I2C_Ack;                 /**< 使能或禁用数据接收时的自动应答机制,参见 @ref I2C_acknowledgement */  
} I2C_InitTypeDef;  

5.4.3 中断标识

5.4.4 IIC 初始化配置

#include "stm32f4xx.h"

/**
 * @brief  初始化 I2C1 外设,使用 PB6 作为 SCL,PB7 作为 SDA
 * @note   使用标准外设库配置 GPIO 和 I2C 外设
 * @retval 无
 */
void I2C_1_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    I2C_InitTypeDef  I2C_InitStruct;

    /************ 1. 使能 GPIOB 和 I2C1 的时钟 ************/
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); // 开启 GPIOB 时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);  // 开启 I2C1 时钟

    /************ 2. 配置 PB6 和 PB7 为复用功能(AF) ************/
    // 将 PB6 连接到 I2C1 的 SCL 功能
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_I2C1);

    // 将 PB7 连接到 I2C1 的 SDA 功能
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_I2C1);

    /************ 3. 配置 GPIO 引脚属性 ************/
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;      // 选择 PB6 和 PB7
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;                // 设置为复用功能模式
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;           // 设置 IO 口速率为 50MHz
    GPIO_InitStruct.GPIO_OType = GPIO_OType_OD;              // 设置为开漏输出(I2C要求)
    GPIO_InitStruct.GPIO_PuPd  = GPIO_PuPd_UP;               // 设置为上拉模式(I2C总线需要)
    GPIO_Init(GPIOB, &GPIO_InitStruct);                      // 初始化 GPIOB 的 6 和 7 引脚

    /************ 4. 配置 I2C1 外设参数 ************/
    I2C_InitStruct.I2C_ClockSpeed = 100000;                  // 设置时钟频率为 100kHz(标准模式)
    I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;                  // 设置 I2C 模式
    I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;          // 设置占空比为 2(标准模式)
    I2C_InitStruct.I2C_OwnAddress1 = 0x00;                   // 设置主机地址(可任意)
    I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;                 // 启用应答功能(ACK)
    I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; // 使用 7 位地址
    I2C_Init(I2C1, &I2C_InitStruct);                         // 初始化 I2C1

    /************ 5. 启动(使能)I2C1 ************/
    I2C_Cmd(I2C1, ENABLE);                                   // 启动 I2C1 外设
}

1、问题1:占空比为啥选择2?

在 STM32 的 I2C 初始化中,这个参数:

I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;

表示选择 占空比为 2。这个参数的意义,要从 I2C 快速模式(Fast mode,400kHz)谈起。

什么是 I2C 占空比 DutyCycle?

I2C 的 SCL(时钟线)是由主机控制的,在快速模式下,它有两种占空比可选:

参数占空比含义
I2C_DutyCycle_21:1高电平时间 = 低电平时间
I2C_DutyCycle_16_916:9高电平时间比低电平时间短一些(时钟频率更高)

为什么大多数情况选择 I2C_DutyCycle_2

  1. 兼容性好
    1:1 的占空比更标准、容易被各种 I2C 从机识别,不容易出问题。

  2. 不容易出错
    相对简单、时序清晰,调试也容易。

  3. 足够用了
    如果不追求极限的传输速度,1:1 足够支撑 400kHz 的 I2C 快速模式通信。

什么时候用 I2C_DutyCycle_16_9

如果你需要:

  • 更快的通信(接近 400kHz 极限)

  • 更高的吞吐率

  • 从机能稳定支持非对称时序

这时才考虑使用 I2C_DutyCycle_16_9

总结一句话:

选择 I2C_DutyCycle_2 是为了稳定、通用、兼容性好,而 I2C_DutyCycle_16_9 适合对性能要求高但从设备也必须支持的情况。

第八章 IIC通讯实验

5.5 I2C实验:SHT20 温湿度传感器

5.5.1 传感器介绍

1、SHT20 传感器概述

SHT20 是由 Sensirion 公司推出的一款高精度数字温湿度传感器,内部集成了温湿度感测元件、模数转换器、校准数据存储及 I2C 接口逻辑,具备功耗低、体积小、稳定性好等优点,广泛应用于环境监测、智能家居、工业控制等场景。

2、支持的通信协议

SHT20 使用标准的 I²C 通信协议 与主控芯片进行数据交换,通信速度支持 标准模式(100kHz)快速模式(400kHz),通过两个信号线(SCL 和 SDA)完成命令发送与数据读取。通信过程符合 I2C 主从结构,主机发起通信,传感器作为从机响应。

3、测量范围与精度

参数范围精度(典型)
温度-40°C ~ +125°C±0.3°C
相对湿度0%RH ~ 100%RH±3%RH
  • 解析度:12bit(湿度)、14bit(温度)

  • 采样时间:湿度约 29ms,温度约 85ms

  • 输出为未经补偿的原始值,需进行处理转为实际数值

4、封装与引脚说明

SHT20 常见为 DFN 封装(3mm x 3mm),引脚排列如下:

引脚名称说明
1VDD电源输入(2.1~3.6V)
2SDAI2C 数据线
3GND电源地
4SCLI2C 时钟线

注意:SDA 和 SCL 需要外接上拉电阻(一般为 4.7kΩ)

5.5.2 SHT20 协议

本章节结合命令表、寄存器配置表、测量时间表以及通信时序图,全面描述 SHT2x 系列温湿度传感器的配置方法、操作流程与通信机制。通过这些技术资料,开发者可高效实现对传感器的配置、数据采集与系统集成。

1、 命令表

SHT2x 传感器支持多种操作命令,用于启动测量、配置寄存器、执行复位等核心功能。每条命令以 8 位二进制编码发送。

常用命令

操作类型模式/说明命令码(8位)十六进制
触发温度测量Hold master 模式 保持模式111000110xE3
触发湿度测量Hold master 模式111001010xE5
触发温度测量No Hold master 模式 不保持111100110xF3
触发湿度测量No Hold master 模式111101010xF5
写用户寄存器配置传感器功能参数111001100xE6
读用户寄存器读取当前配置参数111001110xE7
软件复位恢复出厂设置111111100xFE

模式说明

  • Hold Master:传感器测量期间占用 I²C 总线,主机必须等待完成。

  • No Hold Master:主机发送命令后释放总线,可轮询等待数据准备好。

2、 用户寄存器配置表

用户寄存器用于调整传感器的工作特性,包括分辨率、加热器开关、电池状态指示等。

Bit位含义说明
7、0分辨率设置00(RH 12位 / T 14位)、01(RH 8位 / T 12位)等。影响测量时间。
6电池状态指示1 表示 VDD < 2.25V ± 0.1V(低电警告)。
5-3保留不可更改,写入可能导致不可预测行为。
2加热器控制1 启用片上加热器,可防结露但耗电高。
1禁用 OTP 重载1 表示使用用户设置;0 表示每次测量恢复默认值。

3、 测量时间表

不同分辨率下的典型和最大测量时长,用于系统响应优化与功耗管理。

分辨率设置温度典型时间温度最大时间湿度典型时间湿度最大时间
T: 14 位66 ms85 ms--
RH: 12 位--22 ms29 ms
T: 12 位13 ms20 ms--
RH: 8 位--3 ms4 ms

应用建议:

  • 低延迟应用:选用低分辨率(如 T: 12位,RH: 8位)。

  • 高精度测量:选用高分辨率(如 T: 14位,RH: 12位),但功耗与延迟较高。

4、 Hold Master 模式通信 时序图

Figure 15

流程描述

  • 主机发送起始位 → 地址字节(0x40)→ 命令(如0xE3)

  • 传感器锁定总线进行测量

  • 返回数据(MSB + LSB)+ CRC(可选)

  • 主机发送停止位

特点: 适合需要同步数据采集的实时控制系统。

5、 No Hold Master 模式通信 时序图

Figure 16

流程描述

  • 主机发送命令 → 释放总线

  • 定时读取数据准备状态(轮询)

  • 数据就绪后接收 MSB + LSB + CRC(可选)

  • 主机发送停止位

特点: 适合总线上有多个设备或主机需并行处理其他任务的情况。

6、工作流程

  1. 初始化配置

    • 使用 Write user register 命令设定测量分辨率、加热器开关等参数。
  2. 触发测量

    • 根据应用需求选择 HoldNo Hold 模式的命令发起测量。
  3. 数据读取

    • 遵循通信时序,读取传感器返回的温湿度数据与 CRC 校验。
  4. 错误与系统管理

    • 通过 Bit 6 监控电池状态,异常时使用 Soft reset 命令恢复默认状态。

5.4.5 SHT20 驱动程序设计

1、温湿度转换

传感器内部设置的默认分辨率为相对湿度12位和温度14位。SDA的输出数据被转换成两个字节的数据包,高字节 MSB在前(左对齐)。

1、湿度转换

2、温度转换

2、程序设计

#include "Bsp_I2C_SHT20.h"  // SHT20 传感器驱动头文件(包含地址、命令等定义)
#include "Delay.h"          // 延迟函数头文件,用于测量等待


/**
* @brief SHT20 传感器的 I2C 从机地址(7位地址)
* 实际使用中需要左移一位用于写/读位(最后一位为读写标志位)
*/

#define SHT20_ADDR 0x40

// 无保持模式下触发温度测量命令
#define SHT20_TRIG_TEMP_MEAS_NOHOLD 0xF3

// 无保持模式下触发湿度测量命令
#define SHT20_TRIG_HUMI_MEAS_NOHOLD 0xF5



//---------------------------------------------
// 静态函数,仅在本 .c 文件中使用,不对外暴露
//---------------------------------------------

/**
 * @brief 向 SHT20 发送命令字节
 * @param cmd 要发送的命令,如温度或湿度测量命令
 */
static void SHT20_WriteCmd(uint8_t cmd);

/**
 * @brief 从 SHT20 接收多个字节数据(一般为 3 字节:高+低+CRC)
 * @param buf 数据接收缓冲区
 * @param len 要接收的字节数
 */
static void SHT20_ReadBytes(uint8_t* buf, uint8_t len);

/**
 * @brief 启动一次 I2C 通讯,并选择读/写模式
 * @param address 从设备地址(7 位地址)
 * @param direction 通讯方向:发送或接收(I2C_Direction_Transmitter / Receiver)
 */
static void I2C_StartTransmission(uint8_t address, uint8_t direction) {
    // 1. 等待总线空闲,确保没有其他设备在通信
    while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));

    // 2. 发送 START 起始信号
    I2C_GenerateSTART(I2C1, ENABLE);

    // 3. 等待主模式选择成功
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    // 4. 发送从设备地址 + 读写位(左移1位后添加方向位)
    I2C_Send7bitAddress(I2C1, address << 1, direction);

    // 5. 根据方向判断是否成功进入发送或接收模式
    if (direction == I2C_Direction_Transmitter) {
        while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
    } else {
        while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
    }
}


/**
 * @brief 向 SHT20 发送指令(例如启动温度/湿度测量)
 * @param cmd SHT20 指令码
 */
static void SHT20_WriteCmd(uint8_t cmd) {
    // 1. 启动 I2C,并进入发送模式
    I2C_StartTransmission(SHT20_ADDR, I2C_Direction_Transmitter);

    // 2. 发送具体命令字节
    I2C_SendData(I2C1, cmd);

    // 3. 等待命令发送完成
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    // 4. 通信结束,发送 STOP 信号
    I2C_GenerateSTOP(I2C1, ENABLE);
}


/**
 * @brief 从 SHT20 读取多个字节的数据(一般包含 CRC 校验)
 * @param buf 接收缓冲区指针
 * @param len 要读取的字节数(通常为 3)
 */
static void SHT20_ReadBytes(uint8_t* buf, uint8_t len) {
    // 1. 启动 I2C 并进入接收模式
    I2C_StartTransmission(SHT20_ADDR, I2C_Direction_Receiver);

    // 2. 循环读取数据
    for (uint8_t i = 0; i < len; i++) {
        // 最后一个字节前,关闭应答信号,表示读取结束(NACK)
        if (i == len - 1)
            I2C_AcknowledgeConfig(I2C1, DISABLE);

        // 等待字节接收完成
        while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));

        // 存入缓冲区
        buf[i] = I2C_ReceiveData(I2C1);
    }

    // 3. 通信结束,发送 STOP 信号
    I2C_GenerateSTOP(I2C1, ENABLE);

    // 4. 恢复 ACK 状态,便于下次通信
    I2C_AcknowledgeConfig(I2C1, ENABLE);
}


/**
 * @brief 读取温度值(单位:℃)
 * @return 返回转换后的摄氏度温度
 */
float SHT20_ReadTemperature(void) {
    uint8_t data[3];   // 缓冲区:2字节数据 + 1字节 CRC
    uint16_t raw;      // 原始温度数据

    SHT20_WriteCmd(SHT20_TRIG_TEMP_MEAS_NOHOLD);  // 向 SHT20 发送温度测量命令
    Delay_ms(85);                                  // 等待测量完成(最长85ms)

    SHT20_ReadBytes(data, 3);                      // 读取3字节数据

    // 合并高低字节
    raw = ((uint16_t)data[0] << 8) | data[1];

    raw &= ~0x0003;  // 清除最低2位状态位(非数据)

    // 数据手册提供的温度换算公式
    return -46.85 + 175.72 * raw / 65536.0;
}


/**
 * @brief 读取湿度值(单位:%RH)
 * @return 返回转换后的相对湿度值
 */
float SHT20_ReadHumidity(void) {
    uint8_t data[3];
    uint16_t raw;

    SHT20_WriteCmd(SHT20_TRIG_HUMI_MEAS_NOHOLD);  // 发送湿度测量命令
    Delay_ms(29);                                 // 等待测量完成(最长29ms)

    SHT20_ReadBytes(data, 3);                     // 接收数据

    raw = ((uint16_t)data[0] << 8) | data[1];
    raw &= ~0x0003;  // 清除状态位

    // 湿度换算公式来自官方手册
    return -6.0 + 125.0 * raw / 65536.0;
}