当前位置: 首页 > news >正文

嵌入式软件知识点汇总(day6):常见通信方式(上)

串口通信

1. 串口通信基本概念

串口通信是一种串行通信方式,数据逐位顺序传输,只需要少数几根线即可实现双向通信。

一般四根线(VCC、GND、RX、TX),部分场景下只需要两根线(RX、TX)(RX、GND)(TX、GND)

  • 串行通信:数据逐位依次传输,只需要少数几根线。

  • 并行通信:数据多个位同时传输,需要多根数据线。

  • 全双工:可以同时进行发送和接收。

  • 半双工:可以发送或接收,但不能同时进行。

  • 单工:只能在一个方向上传输。

    主要特点

  • 异步通信:不需要时钟信号同步

  • 全双工:可同时发送和接收

  • 点对点:通常两个设备间通信

  • 距离较长:相比I2C/SPI可传输更远距离

串口通信通常是全双工的。

2. 硬件接口

  • RS-232:常见于计算机串口,使用负逻辑,电压范围较大(-15V~+15V),传输距离较长(可达15米)。

  • RS-485:差分信号,抗干扰能力强,支持多点通信,传输距离更长(可达1200米)。

  • TTL UART:单片机常用,电压为0V和3.3V/5V,传输距离短(通常不超过1米)。

3. 串口通信协议

  • 起始位:表示一帧数据的开始,通常为1位低电平。

  • 数据位:实际传输的数据,通常为5、6、7、8位。

  • 校验位:用于错误检测,可选奇校验、偶校验或无校验。

  • 停止位:表示一帧数据的结束,通常为1、1.5或2位高电平。

4. 关键参数

  • 波特率:每秒传输的符号数,常见的波特率有9600、115200等。

  • 数据位:5~8位,通常为8位。

  • 停止位:1、1.5、2位,通常为1位。

  • 校验位:奇校验、偶校验、无校验。

  • 流控制:硬件流控制(RTS/CTS)和软件流控制(XON/XOFF)。

波特率(Baud Rate)计算

// 波特率计算(以STM32为例)
void calculate_baudrate(void) {// 波特率 = 系统时钟 / (USARTDIV × 16)// USARTDIV = 系统时钟 / (波特率 × 16)uint32_t system_clock = 72000000;  // 72MHzuint32_t desired_baud = 115200;uint32_t usartdiv = system_clock / (desired_baud * 16);printf("USARTDIV = %lu\n", usartdiv);
}

数据帧格式

串口数据帧格式:
+----------+----------+-------------+----------+----------+----------+
| 起始位   | 数据位   | 校验位      | 停止位   |          |          |
| (1 bit)     | (5-9 bit) | (可选)       | (1-2 bit)  |          |          |
+----------+----------+-------------+----------+----------+----------+

示例:8N1格式 = 1起始位 + 8数据位 + 无校验 + 1停止位

5. 流控制

  • 硬件流控制:使用RTS(Request to Send)和CTS(Clear to Send)信号线来控制数据流,避免缓冲区溢出。

  • 软件流控制:使用特殊字符XON(0x11)和XOFF(0x13)来控制数据流。

6. 错误检测

  • 奇偶校验:检测单比特错误。

  • 溢出错误:接收缓冲区已满,又收到新数据。

  • 帧错误:停止位不在预期的时刻出现。

  • 校验错误:奇偶校验失败。

7. 串口通信编程

7.1 初始化步骤

  1. 配置GPIO引脚为串口功能。

  2. 使能串口时钟。

  3. 配置波特率、数据位、停止位、校验位、流控制。

  4. 使能串口和中断(如果需要)。

7.2 数据发送和接收

  • 轮询方式:程序不断检查状态寄存器,直到发送完成或接收到数据。

    // 简单的轮询收发
    void uart_send_byte(USART_TypeDef *uart, uint8_t data) {// 等待发送缓冲区空while(!(uart->ISR & USART_ISR_TXE));// 发送数据uart->TDR = data;// 等待发送完成while(!(uart->ISR & USART_ISR_TC));
    }uint8_t uart_receive_byte(USART_TypeDef *uart) {// 等待接收到数据while(!(uart->ISR & USART_ISR_RXNE));// 读取数据return (uint8_t)(uart->RDR);
    }// 发送字符串
    void uart_send_string(USART_TypeDef *uart, const char *str) {while(*str) {uart_send_byte(uart, *str++);}
    }
  • 中断方式:使用中断处理发送和接收,提高效率。

    #include "stm32f1xx_it.h"// 接收缓冲区
    #define RX_BUFFER_SIZE 256
    static uint8_t uart_rx_buffer[RX_BUFFER_SIZE];
    static volatile uint16_t uart_rx_index = 0;
    static volatile bool uart_rx_complete = false;// 中断初始化
    void uart_interrupt_init(USART_TypeDef *uart) {// 使能接收中断uart->CR1 |= USART_CR1_RXNEIE;// 配置NVICIRQn_Type irq;if(uart == USART1) irq = USART1_IRQn;else if(uart == USART2) irq = USART2_IRQn;// 其他USART...HAL_NVIC_SetPriority(irq, 0, 0);HAL_NVIC_EnableIRQ(irq);
    }// 中断服务函数
    void USART1_IRQHandler(void) {// 接收中断if(USART1->ISR & USART_ISR_RXNE) {uint8_t data = (uint8_t)(USART1->RDR);// 简单的协议处理:以换行符结束if(data == '\n' || uart_rx_index >= RX_BUFFER_SIZE - 1) {uart_rx_buffer[uart_rx_index] = '\0';uart_rx_complete = true;uart_rx_index = 0;} else {uart_rx_buffer[uart_rx_index++] = data;}}// 发送中断处理...if(USART1->ISR & USART_ISR_TXE) {// 发送缓冲区空,可以发送下一个字节}
    }// 获取接收到的数据
    bool uart_get_received_data(char *buffer, uint16_t size) {if(uart_rx_complete) {uint16_t len = strlen((char*)uart_rx_buffer);if(len < size) {strcpy(buffer, (char*)uart_rx_buffer);uart_rx_complete = false;return true;}}return false;
    }

  • DMA方式:使用DMA传输大量数据,减少CPU占用。

    // DMA接收配置
    void uart_dma_init(USART_TypeDef *uart, DMA_Channel_TypeDef *dma_ch) {// 配置DMAdma_ch->CCR = 0;dma_ch->CCR |= DMA_CCR_MINC;    // 内存地址递增dma_ch->CCR |= DMA_CCR_CIRC;    // 循环模式dma_ch->CCR |= DMA_CCR_TCIE;    // 传输完成中断// 设置外设地址(UART数据寄存器)dma_ch->CPAR = (uint32_t)&(uart->RDR);// 设置内存地址dma_ch->CMAR = (uint32_t)uart_rx_buffer;// 数据数量dma_ch->CNDTR = RX_BUFFER_SIZE;// 使能DMAdma_ch->CCR |= DMA_CCR_EN;// 使能UART的DMA接收uart->CR3 |= USART_CR3_DMAR;
    }// DMA发送函数
    void uart_dma_send(USART_TypeDef *uart, DMA_Channel_TypeDef *dma_ch, uint8_t *data, uint16_t length) {// 等待上次DMA传输完成while(dma_ch->CNDTR != 0);// 配置DMA发送dma_ch->CCR &= ~DMA_CCR_EN;dma_ch->CMAR = (uint32_t)data;dma_ch->CNDTR = length;dma_ch->CCR |= DMA_CCR_EN;// 使能UART的DMA发送uart->CR3 |= USART_CR3_DMAT;
    }

8. 常见应用

  • 调试输出:通过串口打印调试信息。

  • 与传感器通信:读取传感器数据。

  • 与计算机通信:实现嵌入式设备与上位机的数据交换。

  • 设备间通信:多个嵌入式设备通过串口进行数据交换。

9. 通信协议设计

简单的文本协议

// 命令行协议示例
typedef enum {CMD_GET_TEMP,      // 获取温度CMD_SET_LED,       // 设置LEDCMD_GET_STATUS,    // 获取状态CMD_RESET,         // 复位CMD_UNKNOWN        // 未知命令
} uart_command_t;// 协议解析
uart_command_t parse_command(const char *cmd_str) {if(strcmp(cmd_str, "GET_TEMP") == 0) return CMD_GET_TEMP;if(strcmp(cmd_str, "SET_LED") == 0) return CMD_SET_LED;if(strcmp(cmd_str, "GET_STATUS") == 0) return CMD_GET_STATUS;if(strcmp(cmd_str, "RESET") == 0) return CMD_RESET;return CMD_UNKNOWN;
}// 命令处理
void process_uart_command(const char *command) {uart_command_t cmd = parse_command(command);switch(cmd) {case CMD_GET_TEMP:{float temp = read_temperature();char response[32];snprintf(response, sizeof(response), "TEMP:%.2f\r\n", temp);uart_send_string(USART1, response);}break;case CMD_SET_LED:// 解析参数并设置LEDbreak;case CMD_GET_STATUS:{char status[64];get_system_status(status);uart_send_string(USART1, status);}break;default:uart_send_string(USART1, "ERROR: Unknown command\r\n");break;}
}

二进制协议(更高效)

#pragma pack(push, 1)// 协议头
typedef struct {uint8_t start_flag;     // 起始标志 0xAAuint8_t command;        // 命令字uint16_t length;        // 数据长度uint8_t checksum;       // 头校验和
} uart_protocol_header_t;// 数据包
typedef struct {uart_protocol_header_t header;uint8_t data[256];      // 数据负载uint8_t crc16[2];       // CRC16校验
} uart_packet_t;#pragma pack(pop)// 协议处理状态机
typedef enum {STATE_WAIT_HEADER,STATE_READ_LENGTH,STATE_READ_DATA,STATE_READ_CRC,STATE_PROCESS_PACKET
} uart_parser_state_t;// 协议解析器
typedef struct {uart_parser_state_t state;uart_packet_t packet;uint16_t data_index;uint16_t expected_length;
} uart_parser_t;uint8_t calculate_checksum(const uint8_t *data, uint16_t length) {uint8_t sum = 0;for(uint16_t i = 0; i < length; i++) {sum += data[i];}return sum;
}// 协议解析状态机
bool uart_parse_byte(uart_parser_t *parser, uint8_t byte) {switch(parser->state) {case STATE_WAIT_HEADER:if(byte == 0xAA) {parser->packet.header.start_flag = byte;parser->state = STATE_READ_LENGTH;parser->data_index = 0;}break;case STATE_READ_LENGTH:if(parser->data_index == 0) {parser->packet.header.command = byte;parser->data_index++;} else {parser->packet.header.length = byte;parser->expected_length = byte;parser->data_index = 0;// 验证头校验和uint8_t checksum = calculate_checksum((uint8_t*)&parser->packet.header, sizeof(uart_protocol_header_t) - 1);if(checksum == parser->packet.header.checksum) {parser->state = (parser->expected_length > 0) ? STATE_READ_DATA : STATE_READ_CRC;} else {parser->state = STATE_WAIT_HEADER;  // 校验失败}}break;case STATE_READ_DATA:parser->packet.data[parser->data_index++] = byte;if(parser->data_index >= parser->expected_length) {parser->state = STATE_READ_CRC;parser->data_index = 0;}break;case STATE_READ_CRC:parser->packet.crc16[parser->data_index++] = byte;if(parser->data_index >= 2) {parser->state = STATE_PROCESS_PACKET;return true;  // 完整数据包接收完成}break;default:parser->state = STATE_WAIT_HEADER;break;}return false;
}

10. 错误处理和流控制

错误检测和处理

// 错误状态定义
typedef enum {UART_ERROR_NONE = 0,UART_ERROR_OVERRUN,     // 溢出错误UART_ERROR_FRAMING,     // 帧错误UART_ERROR_PARITY,      // 校验错误UART_ERROR_NOISE,       // 噪声错误UART_ERROR_TIMEOUT      // 超时错误
} uart_error_t;// 错误处理
uart_error_t uart_check_errors(USART_TypeDef *uart) {if(uart->ISR & USART_ISR_ORE) {uart->ICR |= USART_ICR_ORECF;  // 清除溢出标志return UART_ERROR_OVERRUN;}if(uart->ISR & USART_ISR_FE) {uart->ICR |= USART_ICR_FECF;   // 清除帧错误标志return UART_ERROR_FRAMING;}if(uart->ISR & USART_ISR_PE) {uart->ICR |= USART_ICR_PECF;   // 清除校验错误标志return UART_ERROR_PARITY;}if(uart->ISR & USART_ISR_NE) {uart->ICR |= USART_ICR_NECF;   // 清除噪声错误标志return UART_ERROR_NOISE;}return UART_ERROR_NONE;
}// 带错误处理的接收函数
bool uart_receive_byte_safe(USART_TypeDef *uart, uint8_t *data, uint32_t timeout) {uint32_t start_time = HAL_GetTick();while(!(uart->ISR & USART_ISR_RXNE)) {// 检查超时if((HAL_GetTick() - start_time) > timeout) {return false;}// 检查错误uart_error_t error = uart_check_errors(uart);if(error != UART_ERROR_NONE) {// 记录错误日志uart_log_error(error);return false;}}*data = (uint8_t)(uart->RDR);return true;
}

硬件流控制

// RTS/CTS流控制实现
void uart_flow_control_init(void) {// 配置RTS为输出,CTS为输入GPIO_InitTypeDef gpio_init = {0};// RTS引脚配置gpio_init.Pin = UART_RTS_PIN;gpio_init.Mode = GPIO_MODE_OUTPUT_PP;gpio_init.Speed = GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init(UART_RTS_PORT, &gpio_init);// CTS引脚配置gpio_init.Pin = UART_CTS_PIN;gpio_init.Mode = GPIO_MODE_INPUT;gpio_init.Pull = GPIO_NOPULL;HAL_GPIO_Init(UART_CTS_PORT, &gpio_init);// 初始状态:可以接收数据HAL_GPIO_WritePin(UART_RTS_PORT, UART_RTS_PIN, GPIO_PIN_RESET);
}// 检查是否可以发送(CTS信号)
bool uart_can_send(void) {return (HAL_GPIO_ReadPin(UART_CTS_PORT, UART_CTS_PIN) == GPIO_PIN_RESET);
}// 设置是否可以接收(RTS信号)
void uart_set_ready_to_receive(bool ready) {HAL_GPIO_WritePin(UART_RTS_PORT, UART_RTS_PIN, ready ? GPIO_PIN_RESET : GPIO_PIN_SET);
}// 带流控制的发送函数
bool uart_send_with_flow_control(USART_TypeDef *uart, uint8_t *data, uint16_t length) {for(uint16_t i = 0; i < length; i++) {// 等待CTS信号允许发送uint32_t timeout = 1000;  // 1秒超时uint32_t start_time = HAL_GetTick();while(!uart_can_send()) {if((HAL_GetTick() - start_time) > timeout) {return false;  // 超时}}// 发送字节uart_send_byte(uart, data[i]);}return true;
}

11. 实际应用案例

嵌入式日志系统

// 串口日志系统
typedef enum {LOG_LEVEL_DEBUG,LOG_LEVEL_INFO,LOG_LEVEL_WARNING,LOG_LEVEL_ERROR
} log_level_t;void uart_log(log_level_t level, const char *format, ...) {// 添加日志级别前缀const char *level_str[] = {"DEBUG", "INFO", "WARN", "ERROR"};char buffer[256];int pos = snprintf(buffer, sizeof(buffer), "[%s] ", level_str[level]);// 格式化消息va_list args;va_start(args, format);vsnprintf(buffer + pos, sizeof(buffer) - pos, format, args);va_end(args);// 添加换行strcat(buffer, "\r\n");// 发送日志uart_send_string(USART1, buffer);
}// 使用示例
#define LOG_DEBUG(...) uart_log(LOG_LEVEL_DEBUG, __VA_ARGS__)
#define LOG_ERROR(...) uart_log(LOG_LEVEL_ERROR, __VA_ARGS__)void system_monitor_task(void) {LOG_DEBUG("系统启动完成");LOG_INFO("温度: %.1f°C", read_temperature());if(system_error_detected) {LOG_ERROR("系统错误: %s", error_message);}
}

传感器数据采集

// 通过串口读取传感器数据
typedef struct {float temperature;float humidity;uint16_t pressure;uint8_t battery_level;
} sensor_data_t;bool read_sensor_data_via_uart(sensor_data_t *data) {// 发送读取命令const uint8_t read_cmd[] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x04};uart_send_data(USART2, read_cmd, sizeof(read_cmd));// 等待响应uint8_t response[32];if(!uart_receive_data(USART2, response, sizeof(response), 1000)) {return false;}// 解析响应数据if(response[0] == 0x01 && response[1] == 0x03) {uint16_t temp_raw = (response[3] << 8) | response[4];uint16_t hum_raw = (response[5] << 8) | response[6];data->temperature = temp_raw / 10.0f;data->humidity = hum_raw / 10.0f;return true;}return false;
}

总结

串口通信关键知识点:

  1. 基本参数:波特率、数据位、停止位、校验位

  2. 硬件接口:TTL、RS232、RS485电平标准

  3. 工作模式:轮询、中断、DMA

  4. 协议设计:文本协议、二进制协议

  5. 错误处理:溢出、帧错误、校验错误

  6. 流控制:硬件RTS/CTS、软件XON/XOFF

  7. 实际应用:调试日志、传感器通信、设备控制

嵌入式开发建议:

  1. 选择合适的波特率:平衡速度和可靠性

  2. 使用DMA处理大数据:减少CPU占用

  3. 实现完整的错误处理:提高系统稳定性

  4. 设计清晰的通信协议:便于调试和维护

  5. 考虑硬件流控制:在高速通信时很重要

IIC总线通信

我们通常说的IIC(Inter-Integrated Circuit)总线是一种同步、多主从、半双工的串行通信总线,由Philips公司(现NXP)开发。它只需要两根线:串行数据线(SDA)和串行时钟线(SCL)。IIC总线在嵌入式系统中广泛应用,用于连接微控制器和各种外设,如传感器、存储器、IO扩展器等。

  1. IIC总线特点

    • 总线长度较短,一般用于板内通信

    • 两线制:SCL(时钟线)和SDA(数据线)

    • 多主多从:支持多个主设备和从设备(但同一时刻只能有一个主设备)

    • 地址寻址:每个设备有唯一地址,通过地址选择设备

    • 速度分级:标准模式(100kbps)、快速模式(400kbps)、高速模式(3.4Mbps)

    • 半双工:同一时间只能单向传输

  2. 总线拓扑结构

    I2C总线拓扑:+-----+     +-----+     +-----+| MCU |     | RTC |     | EEPROM||主设备|     |从设备|     |从设备 |+-----+     +-----+     +-----+|           |           ||           |           |
    SCL -----+-----------+-----------+
    SDA -----+-----------+-----------+|           |           |+-----+     +-----+     +-----+| 上拉 |    | 上拉 |     | 上拉 || 电阻 |    | 电阻 |     | 电阻 |+-----+     +-----+     +-----+

  3. IIC总线信号

    • 起始条件(START):SCL为高电平时,SDA由高变低

    • 停止条件(STOP):SCL为高电平时,SDA由低变高

    • 数据有效性:在SCL高电平期间,SDA必须保持稳定。数据变化发生在SCL低电平期间。

    • 应答(ACK):每传输完一个字节(8位),接收方需要发送一个应答位(低电平为ACK,高电平为NACK)

    • 非应答(NACK):当接收方不想再接收数据或无法接收时,会发送NACK

  4. IIC总线协议

    • 数据传输以起始条件开始,停止条件结束。

    • 数据传输的基本单位是一个字节,高位在前。

    • 每个字节后必须跟一个应答位。

    • 数据帧格式:
      起始条件 + 从设备地址(7位或10位) + 读写位(1位,0写1读) + 应答 + 数据字节 + 应答/非应答 + ... + 停止条件

  5. 7位地址和10位地址

    • 7位地址:常用,地址范围0x08~0x77(其中0x00~0x07和0x78~0x7F为保留地址)

    • 10位地址:扩展地址范围,但使用较少

  6. IIC总线操作模式

    • 主设备发送模式:主设备向从设备写数据

    • 主设备接收模式:主设备从从设备读数据

    • 组合模式:先写后读,例如先发送要读的寄存器地址,再读取数据

  7. 时钟同步和仲裁

    • 时钟同步:多个主设备可以同时拉低SCL,但只有所有主设备都释放SCL后,SCL才变高。因此,时钟由高电平持续时间最长的设备决定。

    • 仲裁:当多个主设备同时发送数据时,通过SDA上的数据仲裁,确保只有一个主设备获胜。仲裁失败的主设备会退出并转为从设备。

  8. 常见IIC设备

    • EEPROM(如24C02)

    • 温度传感器(如LM75)

    • 实时时钟(如DS1307)

    • IO扩展器(如PCF8574)

    • 数字陀螺仪/加速度计(如MPU6050)

  9. 软件模拟IIC和硬件IIC

    硬件IIC:使用微控制器自带的IIC控制器,效率高,但不一定兼容所有设备。软件模拟IIC:使用普通GPIO模拟IIC时序,灵活,但占用CPU资源。
  10. 上拉电阻

    IIC总线为开漏输出,因此SDA和SCL都需要上拉电阻,典型值在2.2k~10k之间,具体根据总线速度和总线电容决定。

I2C硬件接口和电气特性

电气特性

// I2C电平标准
#define I2C_VOLTAGE_LEVEL_3V3     33
#define I2C_VOLTAGE_LEVEL_5V      50// 上拉电阻计算
typedef struct {uint32_t vdd;           // 电源电压(mV)uint32_t tr_max;        // 最大上升时间(ns)uint32_t bus_capacitance; // 总线电容(pF)
} i2c_electrical_params_t;uint32_t calculate_pullup_resistance(i2c_electrical_params_t *params) {// 上拉电阻 = tr_max / (0.8473 × bus_capacitance)return params->tr_max / (0.8473 * params->bus_capacitance);
}// 常用上拉电阻值
#define I2C_PULLUP_1K     1000
#define I2C_PULLUP_2K2    2200
#define I2C_PULLUP_4K7    4700
#define I2C_PULLUP_10K    10000

GPIO配置

// I2C引脚配置结构体
typedef struct {GPIO_TypeDef *scl_port;uint16_t scl_pin;GPIO_TypeDef *sda_port;uint16_t sda_pin;uint32_t pullup_resistor;  // 上拉电阻值
} i2c_pin_config_t;// STM32 I2C引脚配置示例
void i2c_gpio_init(i2c_pin_config_t *config) {GPIO_InitTypeDef gpio_init = {0};// SCL引脚配置gpio_init.Pin = config->scl_pin;gpio_init.Mode = GPIO_MODE_AF_OD;        // 开漏输出gpio_init.Pull = GPIO_PULLUP;            // 内部上拉(可选)gpio_init.Speed = GPIO_SPEED_FREQ_HIGH;gpio_init.Alternate = GPIO_AF4_I2C1;     // I2C复用功能HAL_GPIO_Init(config->scl_port, &gpio_init);// SDA引脚配置gpio_init.Pin = config->sda_pin;HAL_GPIO_Init(config->sda_port, &gpio_init);printf("I2C GPIO初始化完成\n");printf("SCL: Port=0x%p, Pin=%d\n", config->scl_port, config->scl_pin);printf("SDA: Port=0x%p, Pin=%d\n", config->sda_port, config->sda_pin);
}

I2C通信协议详解

基本通信时序

// I2C时序定义
typedef struct {uint32_t start_condition;    // 起始条件uint32_t stop_condition;     // 停止条件uint32_t data_setup;         // 数据建立时间uint32_t data_hold;          // 数据保持时间uint32_t clock_low;          // 时钟低电平时间uint32_t clock_high;         // 时钟高电平时间
} i2c_timing_t;// I2C帧格式
void i2c_frame_format_demo(void) {printf("=== I2C通信帧格式 ===\n");printf("1. 起始条件(S)\n");printf("2. 从设备地址(7位) + 读写位(1位)\n");printf("3. 应答位(ACK/NACK)\n");printf("4. 数据字节(8位)\n");printf("5. 应答位(ACK/NACK)\n");printf("6. ... 更多数据字节 ...\n");printf("7. 停止条件(P)\n");
}

软件模拟I2C实现

#include "stm32f1xx_hal.h"// 软件I2C延时函数
void i2c_delay(void) {for(volatile int i = 0; i < 10; i++);  // 调整延时时间
}// I2C起始条件
void i2c_start(i2c_pin_config_t *config) {// SDA高电平期间,SCL从高变低HAL_GPIO_WritePin(config->sda_port, config->sda_pin, GPIO_PIN_SET);HAL_GPIO_WritePin(config->scl_port, config->scl_pin, GPIO_PIN_SET);i2c_delay();HAL_GPIO_WritePin(config->sda_port, config->sda_pin, GPIO_PIN_RESET);i2c_delay();HAL_GPIO_WritePin(config->scl_port, config->scl_pin, GPIO_PIN_RESET);i2c_delay();
}// I2C停止条件
void i2c_stop(i2c_pin_config_t *config) {// SDA低电平期间,SCL从低变高HAL_GPIO_WritePin(config->sda_port, config->sda_pin, GPIO_PIN_RESET);HAL_GPIO_WritePin(config->scl_port, config->scl_pin, GPIO_PIN_SET);i2c_delay();HAL_GPIO_WritePin(config->sda_port, config->sda_pin, GPIO_PIN_SET);i2c_delay();
}// 发送一个字节
bool i2c_send_byte(i2c_pin_config_t *config, uint8_t data) {for(int i = 7; i >= 0; i--) {// 设置数据位HAL_GPIO_WritePin(config->sda_port, config->sda_pin, (data & (1 << i)) ? GPIO_PIN_SET : GPIO_PIN_RESET);i2c_delay();// 时钟脉冲HAL_GPIO_WritePin(config->scl_port, config->scl_pin, GPIO_PIN_SET);i2c_delay();HAL_GPIO_WritePin(config->scl_port, config->scl_pin, GPIO_PIN_RESET);i2c_delay();}// 读取应答位HAL_GPIO_WritePin(config->sda_port, config->sda_pin, GPIO_PIN_SET); // 释放SDAi2c_delay();HAL_GPIO_WritePin(config->scl_port, config->scl_pin, GPIO_PIN_SET);i2c_delay();GPIO_PinState ack = HAL_GPIO_ReadPin(config->sda_port, config->sda_pin);HAL_GPIO_WritePin(config->scl_port, config->scl_pin, GPIO_PIN_RESET);i2c_delay();return (ack == GPIO_PIN_RESET);  // ACK为低电平
}// 接收一个字节
uint8_t i2c_receive_byte(i2c_pin_config_t *config, bool ack) {uint8_t data = 0;HAL_GPIO_WritePin(config->sda_port, config->sda_pin, GPIO_PIN_SET); // 释放SDAfor(int i = 7; i >= 0; i--) {HAL_GPIO_WritePin(config->scl_port, config->scl_pin, GPIO_PIN_SET);i2c_delay();if(HAL_GPIO_ReadPin(config->sda_port, config->sda_pin) == GPIO_PIN_SET) {data |= (1 << i);}HAL_GPIO_WritePin(config->scl_port, config->scl_pin, GPIO_PIN_RESET);i2c_delay();}// 发送应答位HAL_GPIO_WritePin(config->sda_port, config->sda_pin, ack ? GPIO_PIN_RESET : GPIO_PIN_SET);i2c_delay();HAL_GPIO_WritePin(config->scl_port, config->scl_pin, GPIO_PIN_SET);i2c_delay();HAL_GPIO_WritePin(config->scl_port, config->scl_pin, GPIO_PIN_RESET);i2c_delay();HAL_GPIO_WritePin(config->sda_port, config->sda_pin, GPIO_PIN_SET); // 释放SDAreturn data;
}

I2C设备地址和寻址

设备地址格式

// I2C地址定义
typedef struct {uint8_t address_7bit;    // 7位地址uint8_t address_8bit;    // 8位地址(包含读写位)bool is_10bit;           // 是否为10位地址
} i2c_device_address_t;// 常见I2C设备地址
#define I2C_ADDR_EEPROM_24C02    (0xA0 >> 1)  // 1010000
#define I2C_ADDR_EEPROM_24C256   (0xA0 >> 1)  // 1010000
#define I2C_ADDR_RTC_DS3231      (0xD0 >> 1)  // 1101000
#define I2C_ADDR_MPU6050         (0x68 << 1)  // 1101000
#define I2C_ADDR_BMP280          (0x76 << 1)  // 1110110
#define I2C_ADDR_OLED_SSD1306    (0x3C << 1)  // 0111100// 地址计算工具函数
uint8_t i2c_make_write_address(uint8_t device_addr_7bit) {return (device_addr_7bit << 1) | 0x00;  // 写操作
}uint8_t i2c_make_read_address(uint8_t device_addr_7bit) {return (device_addr_7bit << 1) | 0x01;  // 读操作
}// 10位地址处理
void i2c_10bit_address_demo(i2c_pin_config_t *config, uint16_t device_addr_10bit) {// 10位地址分两次发送uint8_t first_byte = 0xF0 | ((device_addr_10bit >> 7) & 0x06);uint8_t second_byte = device_addr_10bit & 0xFF;i2c_start(config);i2c_send_byte(config, first_byte);   // 11110xx0i2c_send_byte(config, second_byte);  // xxxxxxxx// ... 后续数据传输
}

硬件I2C控制器配置

#include "stm32f1xx_hal.h"I2C_HandleTypeDef hi2c1;// I2C初始化配置
void i2c_hardware_init(void) {hi2c1.Instance = I2C1;hi2c1.Init.ClockSpeed = 100000;           // 100kHzhi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;   // 占空比hi2c1.Init.OwnAddress1 = 0;               // 主设备地址(可设为0)hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;hi2c1.Init.OwnAddress2 = 0;hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;if (HAL_I2C_Init(&hi2c1) != HAL_OK) {Error_Handler();}printf("硬件I2C初始化完成,时钟频率: %lu Hz\n", hi2c1.Init.ClockSpeed);
}// I2C MSP初始化(GPIO和时钟配置)
void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c) {GPIO_InitTypeDef gpio_init = {0};if(hi2c->Instance == I2C1) {__HAL_RCC_GPIOB_CLK_ENABLE();__HAL_RCC_I2C1_CLK_ENABLE();// PB6 - I2C1_SCL, PB7 - I2C1_SDAgpio_init.Pin = GPIO_PIN_6 | GPIO_PIN_7;gpio_init.Mode = GPIO_MODE_AF_OD;gpio_init.Pull = GPIO_PULLUP;gpio_init.Speed = GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init(GPIOB, &gpio_init);}
}

常用I2C设备驱动实例

EEPROM 24C02驱动

// 24C02 EEPROM驱动
#define EEPROM_24C02_ADDR     (0xA0 >> 1)  // 7位地址
#define EEPROM_PAGE_SIZE      8
#define EEPROM_TOTAL_SIZE     256// EEPROM写操作
bool eeprom_write_byte(uint16_t address, uint8_t data) {uint8_t buffer[2];if(address >= EEPROM_TOTAL_SIZE) return false;buffer[0] = (uint8_t)(address & 0xFF);  // 地址低8位buffer[1] = data;HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(&hi2c1, EEPROM_24C02_ADDR << 1,buffer, 2, 1000);// EEPROM写周期需要延时if(status == HAL_OK) {HAL_Delay(5);  // 24C02写周期最大5msreturn true;}return false;
}// EEPROM读操作
bool eeprom_read_byte(uint16_t address, uint8_t *data) {if(address >= EEPROM_TOTAL_SIZE) return false;// 先发送地址uint8_t addr_byte = (uint8_t)(address & 0xFF);HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(&hi2c1, EEPROM_24C02_ADDR << 1,&addr_byte, 1, 1000);if(status != HAL_OK) return false;// 然后读取数据status = HAL_I2C_Master_Receive(&hi2c1, EEPROM_24C02_ADDR << 1,data, 1, 1000);return (status == HAL_OK);
}// EEPROM页写(更高效)
bool eeprom_write_page(uint16_t start_address, uint8_t *data, uint8_t length) {if(start_address + length > EEPROM_TOTAL_SIZE) return false;if(length > EEPROM_PAGE_SIZE) return false;uint8_t buffer[EEPROM_PAGE_SIZE + 1];buffer[0] = (uint8_t)(start_address & 0xFF);memcpy(&buffer[1], data, length);HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(&hi2c1, EEPROM_24C02_ADDR << 1,buffer, length + 1, 1000);if(status == HAL_OK) {HAL_Delay(5);  // 写周期延时return true;}return false;
}

MPU6050陀螺仪驱动

// MPU6050寄存器定义
#define MPU6050_ADDR           (0x68 << 1)  // 7位地址为0x68
#define MPU6050_WHO_AM_I       0x75
#define MPU6050_PWR_MGMT_1     0x6B
#define MPU6050_ACCEL_XOUT_H   0x3B
#define MPU6050_GYRO_XOUT_H    0x43// MPU6050数据结构
typedef struct {int16_t accel_x;int16_t accel_y;int16_t accel_z;int16_t temp;int16_t gyro_x;int16_t gyro_y;int16_t gyro_z;
} mpu6050_data_t;// MPU6050初始化
bool mpu6050_init(void) {uint8_t data;// 检查设备IDif(HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, MPU6050_WHO_AM_I, I2C_MEMADD_SIZE_8BIT, &data, 1, 1000) != HAL_OK) {return false;}if(data != 0x68) {  // MPU6050的WHO_AM_I寄存器值应为0x68printf("MPU6050设备ID错误: 0x%02X\n", data);return false;}// 唤醒设备data = 0x00;  // 退出睡眠模式if(HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR, MPU6050_PWR_MGMT_1,I2C_MEMADD_SIZE_8BIT, &data, 1, 1000) != HAL_OK) {return false;}printf("MPU6050初始化成功\n");return true;
}// 读取传感器数据
bool mpu6050_read_data(mpu6050_data_t *sensor_data) {uint8_t buffer[14];// 一次性读取所有传感器数据if(HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, MPU6050_ACCEL_XOUT_H,I2C_MEMADD_SIZE_8BIT, buffer, 14, 1000) != HAL_OK) {return false;}// 解析数据(大端序)sensor_data->accel_x = (buffer[0] << 8) | buffer[1];sensor_data->accel_y = (buffer[2] << 8) | buffer[3];sensor_data->accel_z = (buffer[4] << 8) | buffer[5];sensor_data->temp = (buffer[6] << 8) | buffer[7];sensor_data->gyro_x = (buffer[8] << 8) | buffer[9];sensor_data->gyro_y = (buffer[10] << 8) | buffer[11];sensor_data->gyro_z = (buffer[12] << 8) | buffer[13];return true;
}// 温度转换(摄氏度)
float mpu6050_convert_temperature(int16_t temp_raw) {return (temp_raw / 340.0f) + 36.53f;
}

高级特性和错误处理

I2C总线仲裁和时钟同步

// 多主设备冲突检测
typedef enum {I2C_ARBITRATION_WON,     // 仲裁获胜I2C_ARBITRATION_LOST,    // 仲裁失败I2C_ARBITRATION_ERROR    // 仲裁错误
} i2c_arbitration_result_t;// 总线状态监控
typedef struct {uint32_t total_transactions;uint32_t arbitration_lost_count;uint32_t nack_received_count;uint32_t timeout_count;uint32_t bus_error_count;
} i2c_bus_statistics_t;static i2c_bus_statistics_t i2c_stats = {0};// 错误处理函数
void i2c_handle_error(I2C_HandleTypeDef *hi2c) {uint32_t error_code = HAL_I2C_GetError(hi2c);if(error_code & HAL_I2C_ERROR_AF) {i2c_stats.nack_received_count++;printf("I2C错误: 从设备无应答(NACK)\n");}if(error_code & HAL_I2C_ERROR_ARLO) {i2c_stats.arbitration_lost_count++;printf("I2C错误: 仲裁丢失\n");}if(error_code & HAL_I2C_ERROR_BERR) {i2c_stats.bus_error_count++;printf("I2C错误: 总线错误\n");}if(error_code & HAL_I2C_ERROR_TIMEOUT) {i2c_stats.timeout_count++;printf("I2C错误: 超时\n");}// 清除错误标志并重新初始化HAL_I2C_DeInit(hi2c);HAL_Delay(10);HAL_I2C_Init(hi2c);
}

I2C总线扫描工具

// I2C设备扫描
void i2c_scan_devices(void) {printf("开始I2C设备扫描...\n");printf("地址   | 设备\n");printf("-------+-----------------\n");uint8_t found_count = 0;for(uint8_t address = 1; address < 127; address++) {HAL_StatusTypeDef status = HAL_I2C_IsDeviceReady(&hi2c1, address << 1, 3,  // 重试次数10); // 超时(ms)if(status == HAL_OK) {printf("0x%02X  | ", address);// 常见设备识别switch(address) {case (0x68): printf("MPU6050/MPU9250"); break;case (0x76): printf("BMP280/BME280"); break;case (0x27): printf("LCD/IO扩展器"); break;case (0x50): printf("EEPROM"); break;case (0x57): printf("AT24Cxx EEPROM"); break;case (0x48): printf("PCF8591 ADC"); break;case (0x38): printf("FT6236触摸屏"); break;case (0x1D): printf("MMA8452加速度计"); break;case (0x29): printf("TCS34725颜色传感器"); break;case (0x40): printf("SHT30温湿度"); break;case (0x60): printf("MCP4725 DAC"); break;default: printf("未知设备"); break;}printf("\n");found_count++;}}printf("扫描完成,找到 %d 个设备\n", found_count);
}

实际应用案例

传感器数据采集

// 多传感器数据采集
typedef struct {mpu6050_data_t imu_data;float temperature;float humidity;uint32_t pressure;uint32_t timestamp;
} sensor_fusion_data_t;// 传感器管理器
typedef struct {bool mpu6050_present;bool bmp280_present;bool sht30_present;uint32_t last_read_time;sensor_fusion_data_t current_data;
} sensor_manager_t;bool sensor_manager_init(sensor_manager_t *manager) {// 扫描I2C总线上的传感器i2c_scan_devices();// 初始化各传感器manager->mpu6050_present = mpu6050_init();// manager->bmp280_present = bmp280_init();// manager->sht30_present = sht30_init();printf("传感器初始化状态:\n");printf("MPU6050: %s\n", manager->mpu6050_present ? "就绪" : "未找到");printf("BMP280: %s\n", manager->bmp280_present ? "就绪" : "未找到");printf("SHT30: %s\n", manager->sht30_present ? "就绪" : "未找到");return (manager->mpu6050_present || manager->bmp280_present || manager->sht30_present);
}void sensor_manager_read_all(sensor_manager_t *manager) {manager->timestamp = HAL_GetTick();if(manager->mpu6050_present) {if(mpu6050_read_data(&manager->current_data.imu_data)) {manager->current_data.temperature = mpu6050_convert_temperature(manager->current_data.imu_data.temp);}}// 读取其他传感器...manager->last_read_time = HAL_GetTick();
}void sensor_manager_print_data(sensor_manager_t *manager) {printf("=== 传感器数据 ===\n");printf("时间戳: %lu ms\n", manager->timestamp);if(manager->mpu6050_present) {printf("加速度: X=%6d, Y=%6d, Z=%6d\n", manager->current_data.imu_data.accel_x,manager->current_data.imu_data.accel_y,manager->current_data.imu_data.accel_z);printf("陀螺仪: X=%6d, Y=%6d, Z=%6d\n", manager->current_data.imu_data.gyro_x,manager->current_data.imu_data.gyro_y,manager->current_data.imu_data.gyro_z);printf("温度: %.2f°C\n", manager->current_data.temperature);}
}

总结

I2C通信关键知识点:

  1. 总线结构:两线制(SCL、SDA),开漏输出,需要上拉电阻

  2. 通信协议:起始条件、停止条件、应答机制

  3. 地址寻址:7位地址(常用)和10位地址

  4. 速度模式:标准模式(100kbps)、快速模式(400kbps)、高速模式(3.4Mbps)

  5. 多主仲裁:时钟同步和总线仲裁机制

嵌入式开发建议:

  1. 选择合适的上拉电阻(通常4.7kΩ-10kΩ)

  2. 使用硬件I2C提高效率和可靠性

  3. 实现完整的错误处理和总线恢复机制

  4. 进行设备扫描确认设备连接状态

  5. 优化通信时序减少总线占用时间

SPI总线通信

SPI(Serial Peripheral Interface)是一种高速、全双工、同步的串行通信总线,广泛应用于嵌入式系统中连接各种外设。

1. SPI总线基本概念

SPI总线特点

  • 四线制:SCK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)

  • 全双工:同时进行数据发送和接收

  • 同步通信:由主设备提供时钟信号

  • 主从模式:支持一主多从

  • 高速传输:速度可达几十Mbps

  • 无应答机制:没有内置的错误检查机制

总线拓扑结构

SPI总线拓扑:+-----+     +-----+      +-----+| MCU |     |设备1 |     |设备2 ||(主设备)|   |(从设备)|   |(从设备)|+-----+     +-----+     +-----+|           |           |SCK -+-----------+-----------+MOSI -+-----------+-----------+MISO -+-----------+-----------+CS1  -+-----------+CS2  -+-----------------------+

SPI硬件接口和电气特性

电气特性

// SPI电平标准
#define SPI_VOLTAGE_LEVEL_3V3     33
#define SPI_VOLTAGE_LEVEL_5V      50// SPI速度定义
typedef enum {SPI_SPEED_LOW = 0,      // < 1MHzSPI_SPEED_STANDARD,     // 1-10MHzSPI_SPEED_HIGH,         // 10-20MHzSPI_SPEED_VERY_HIGH     // > 20MHz
} spi_speed_category_t;// SPI引脚配置结构体
typedef struct {GPIO_TypeDef *sck_port;uint16_t sck_pin;GPIO_TypeDef *miso_port;uint16_t miso_pin;GPIO_TypeDef *mosi_port;uint16_t mosi_pin;GPIO_TypeDef *cs_port;uint16_t cs_pin;
} spi_pin_config_t;

GPIO配置

// STM32 SPI引脚配置
void spi_gpio_init(spi_pin_config_t *config) {GPIO_InitTypeDef gpio_init = {0};// SCK, MOSI, CS 配置为推挽输出gpio_init.Mode = GPIO_MODE_AF_PP;gpio_init.Pull = GPIO_NOPULL;gpio_init.Speed = GPIO_SPEED_FREQ_HIGH;gpio_init.Pin = config->sck_pin;HAL_GPIO_Init(config->sck_port, &gpio_init);gpio_init.Pin = config->mosi_pin;HAL_GPIO_Init(config->mosi_port, &gpio_init);gpio_init.Pin = config->cs_pin;HAL_GPIO_Init(config->cs_port, &gpio_init);// MISO 配置为输入gpio_init.Mode = GPIO_MODE_INPUT;gpio_init.Pull = GPIO_NOPULL;gpio_init.Pin = config->miso_pin;HAL_GPIO_Init(config->miso_port, &gpio_init);// 初始时CS为高电平(不选中)HAL_GPIO_WritePin(config->cs_port, config->cs_pin, GPIO_PIN_SET);printf("SPI GPIO初始化完成\n");
}

3. SPI通信协议详解

时钟极性和相位

SPI有4种工作模式,由CPOL(时钟极性)和CPHA(时钟相位)决定:

// SPI模式定义
typedef enum {SPI_MODE_0 = 0,    // CPOL=0, CPHA=0SPI_MODE_1,        // CPOL=0, CPHA=1  SPI_MODE_2,        // CPOL=1, CPHA=0SPI_MODE_3         // CPOL=1, CPHA=1
} spi_mode_t;// SPI模式详细说明
void spi_mode_explanation(void) {printf("=== SPI工作模式 ===\n");printf("模式0 (CPOL=0, CPHA=0):\n");printf("  - 时钟空闲低电平\n");printf("  - 数据在时钟上升沿采样\n");printf("  - 最常用的模式\n\n");printf("模式1 (CPOL=0, CPHA=1):\n");printf("  - 时钟空闲低电平\n");printf("  - 数据在时钟下降沿采样\n\n");printf("模式2 (CPOL=1, CPHA=0):\n");printf("  - 时钟空闲高电平\n");printf("  - 数据在时钟下降沿采样\n\n");printf("模式3 (CPOL=1, CPHA=1):\n");printf("  - 时钟空闲高电平\n");printf("  - 数据在时钟上升沿采样\n");
}

数据帧格式

// 数据位序定义
typedef enum {SPI_FIRSTBIT_MSB,  // 高位在前SPI_FIRSTBIT_LSB   // 低位在前
} spi_bit_order_t;// SPI通信时序
typedef struct {uint32_t clock_polarity;     // CPOLuint32_t clock_phase;        // CPHAuint32_t data_size;          // 数据位宽:8位或16位uint32_t bit_order;          // 位序:MSB或LSBuint32_t baud_rate_prescaler; // 波特率分频
} spi_timing_config_t;

4. 软件模拟SPI实现

#include "stm32f1xx_hal.h"// SPI延时函数
void spi_delay(void) {for(volatile int i = 0; i < 5; i++);  // 调整延时时间控制速度
}// 片选控制
void spi_cs_low(spi_pin_config_t *config) {HAL_GPIO_WritePin(config->cs_port, config->cs_pin, GPIO_PIN_RESET);spi_delay();
}void spi_cs_high(spi_pin_config_t *config) {spi_delay();HAL_GPIO_WritePin(config->cs_port, config->cs_pin, GPIO_PIN_SET);
}// 软件SPI发送接收一个字节(模式0)
uint8_t spi_soft_transfer_mode0(spi_pin_config_t *config, uint8_t data) {uint8_t received = 0;for(int i = 7; i >= 0; i--) {// 设置MOSIHAL_GPIO_WritePin(config->mosi_port, config->mosi_pin, (data & (1 << i)) ? GPIO_PIN_SET : GPIO_PIN_RESET);spi_delay();// 时钟上升沿(从设备采样)HAL_GPIO_WritePin(config->sck_port, config->sck_pin, GPIO_PIN_SET);spi_delay();// 读取MISOreceived <<= 1;if(HAL_GPIO_ReadPin(config->miso_port, config->miso_pin) == GPIO_PIN_SET) {received |= 1;}// 时钟下降沿HAL_GPIO_WritePin(config->sck_port, config->sck_pin, GPIO_PIN_RESET);spi_delay();}return received;
}// 软件SPI发送接收(模式3)
uint8_t spi_soft_transfer_mode3(spi_pin_config_t *config, uint8_t data) {uint8_t received = 0;// 模式3:时钟空闲高电平,上升沿采样HAL_GPIO_WritePin(config->sck_port, config->sck_pin, GPIO_PIN_SET);for(int i = 7; i >= 0; i--) {// 设置MOSIHAL_GPIO_WritePin(config->mosi_port, config->mosi_pin, (data & (1 << i)) ? GPIO_PIN_SET : GPIO_PIN_RESET);spi_delay();// 时钟下降沿HAL_GPIO_WritePin(config->sck_port, config->sck_pin, GPIO_PIN_RESET);spi_delay();// 读取MISO(在下降沿后)received <<= 1;if(HAL_GPIO_ReadPin(config->miso_port, config->miso_pin) == GPIO_PIN_SET) {received |= 1;}// 时钟上升沿(从设备采样)HAL_GPIO_WritePin(config->sck_port, config->sck_pin, GPIO_PIN_SET);spi_delay();}return received;
}// 通用软件SPI传输函数
uint8_t spi_soft_transfer(spi_pin_config_t *config, uint8_t data, spi_mode_t mode) {switch(mode) {case SPI_MODE_0:return spi_soft_transfer_mode0(config, data);case SPI_MODE_3:return spi_soft_transfer_mode3(config, data);// 模式1和2类似实现...default:return spi_soft_transfer_mode0(config, data);}
}

5. 硬件SPI控制器配置

STM32硬件SPI配置

#include "stm32f1xx_hal.h"SPI_HandleTypeDef hspi1;// SPI初始化配置
void spi_hardware_init(void) {hspi1.Instance = SPI1;hspi1.Init.Mode = SPI_MODE_MASTER;hspi1.Init.Direction = SPI_DIRECTION_2LINES;hspi1.Init.DataSize = SPI_DATASIZE_8BIT;hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;        // CPOL = 0hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;            // CPHA = 0hspi1.Init.NSS = SPI_NSS_SOFT;                    // 软件片选hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_64;  // 时钟分频hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;           // MSB先行hspi1.Init.TIMode = SPI_TIMODE_DISABLE;hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;hspi1.Init.CRCPolynomial = 10;if (HAL_SPI_Init(&hspi1) != HAL_OK) {Error_Handler();}printf("硬件SPI初始化完成\n");
}// SPI MSP初始化(GPIO和时钟配置)
void HAL_SPI_MspInit(SPI_HandleTypeDef* hspi) {GPIO_InitTypeDef gpio_init = {0};if(hspi->Instance == SPI1) {__HAL_RCC_SPI1_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();// PA5-SPI1_SCK, PA6-SPI1_MISO, PA7-SPI1_MOSIgpio_init.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7;gpio_init.Mode = GPIO_MODE_AF_PP;gpio_init.Pull = GPIO_NOPULL;gpio_init.Speed = GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init(GPIOA, &gpio_init);}
}// SPI速度配置
void spi_set_speed(SPI_HandleTypeDef *hspi, uint32_t prescaler) {hspi->Init.BaudRatePrescaler = prescaler;HAL_SPI_Init(hspi);
}// 常用波特率分频
void spi_configure_speed(SPI_HandleTypeDef *hspi, spi_speed_category_t speed) {uint32_t prescaler;switch(speed) {case SPI_SPEED_LOW:prescaler = SPI_BAUDRATEPRESCALER_256;  // ~280kHz @72MHzbreak;case SPI_SPEED_STANDARD:prescaler = SPI_BAUDRATEPRESCALER_32;   // ~2.25MHz @72MHzbreak;case SPI_SPEED_HIGH:prescaler = SPI_BAUDRATEPRESCALER_8;    // ~9MHz @72MHzbreak;case SPI_SPEED_VERY_HIGH:prescaler = SPI_BAUDRATEPRESCALER_2;    // ~36MHz @72MHzbreak;default:prescaler = SPI_BAUDRATEPRESCALER_64;}spi_set_speed(hspi, prescaler);printf("SPI速度配置为分频: %lu\n", prescaler);
}

6. 常用SPI设备驱动实例

SPI Flash (W25Q128) 驱动

// W25Q128指令集
#define W25Q128_CMD_WRITE_ENABLE     0x06
#define W25Q128_CMD_WRITE_DISABLE    0x04
#define W25Q128_CMD_READ_STATUS_REG1 0x05
#define W25Q128_CMD_PAGE_PROGRAM     0x02
#define W25Q128_CMD_READ_DATA        0x03
#define W25Q128_CMD_SECTOR_ERASE     0x20
#define W25Q128_CMD_CHIP_ERASE       0xC7
#define W25Q128_CMD_POWER_DOWN       0xB9
#define W25Q128_CMD_RELEASE_POWER_DOWN 0xAB
#define W25Q128_CMD_MANUFACTURER_DEVICE_ID 0x90// Flash容量信息
#define W25Q128_PAGE_SIZE          256
#define W25Q128_SECTOR_SIZE        4096
#define W25Q128_BLOCK_SIZE         65536
#define W25Q128_TOTAL_SIZE         16777216  // 16MB// 读取制造商和设备ID
bool w25q128_read_id(uint16_t *manufacturer_id, uint16_t *device_id) {uint8_t cmd = W25Q128_CMD_MANUFACTURER_DEVICE_ID;uint8_t data[4] = {0};spi_cs_low(&spi_config);HAL_SPI_Transmit(&hspi1, &cmd, 1, 1000);HAL_SPI_Receive(&hspi1, data, 4, 1000);spi_cs_high(&spi_config);*manufacturer_id = (data[0] << 8) | data[1];*device_id = (data[2] << 8) | data[3];return true;
}// 写使能
void w25q128_write_enable(void) {uint8_t cmd = W25Q128_CMD_WRITE_ENABLE;spi_cs_low(&spi_config);HAL_SPI_Transmit(&hspi1, &cmd, 1, 1000);spi_cs_high(&spi_config);
}// 读取状态寄存器
uint8_t w25q128_read_status_reg1(void) {uint8_t cmd = W25Q128_CMD_READ_STATUS_REG1;uint8_t status;spi_cs_low(&spi_config);HAL_SPI_Transmit(&hspi1, &cmd, 1, 1000);HAL_SPI_Receive(&hspi1, &status, 1, 1000);spi_cs_high(&spi_config);return status;
}// 等待写操作完成
void w25q128_wait_busy(void) {while(w25q128_read_status_reg1() & 0x01);  // 检查BUSY位
}// 页编程(写一页,最多256字节)
bool w25q128_page_program(uint32_t address, uint8_t *data, uint16_t length) {if(length > W25Q128_PAGE_SIZE) return false;uint8_t cmd[4] = {W25Q128_CMD_PAGE_PROGRAM,(address >> 16) & 0xFF,(address >> 8) & 0xFF,address & 0xFF};w25q128_write_enable();spi_cs_low(&spi_config);HAL_SPI_Transmit(&hspi1, cmd, 4, 1000);HAL_SPI_Transmit(&hspi1, data, length, 1000);spi_cs_high(&spi_config);w25q128_wait_busy();return true;
}// 读取数据
bool w25q128_read_data(uint32_t address, uint8_t *data, uint32_t length) {uint8_t cmd[4] = {W25Q128_CMD_READ_DATA,(address >> 16) & 0xFF,(address >> 8) & 0xFF,address & 0xFF};spi_cs_low(&spi_config);HAL_SPI_Transmit(&hspi1, cmd, 4, 1000);HAL_SPI_Receive(&hspi1, data, length, 1000);spi_cs_high(&spi_config);return true;
}// 扇区擦除(4KB)
bool w25q128_sector_erase(uint32_t address) {uint8_t cmd[4] = {W25Q128_CMD_SECTOR_ERASE,(address >> 16) & 0xFF,(address >> 8) & 0xFF,address & 0xFF};w25q128_write_enable();spi_cs_low(&spi_config);HAL_SPI_Transmit(&hspi1, cmd, 4, 1000);spi_cs_high(&spi_config);w25q128_wait_busy();return true;
}// Flash测试函数
void w25q128_test(void) {uint16_t manufacturer_id, device_id;if(w25q128_read_id(&manufacturer_id, &device_id)) {printf("Flash ID: 制造商=0x%04X, 设备=0x%04X\n", manufacturer_id, device_id);}// 测试读写uint8_t write_data[16] = {0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0};uint8_t read_data[16] = {0};// 擦除第一个扇区w25q128_sector_erase(0x000000);// 写入数据if(w25q128_page_program(0x000000, write_data, 8)) {printf("数据写入成功\n");}// 读取数据if(w25q128_read_data(0x000000, read_data, 8)) {printf("读取数据: ");for(int i = 0; i < 8; i++) {printf("%02X ", read_data[i]);}printf("\n");}
}

7. 高级特性和错误处理

SPI DMA传输

// 使用DMA进行SPI传输(提高效率)
void spi_transmit_dma(uint8_t *data, uint16_t size) {spi_cs_low(&spi_config);HAL_SPI_Transmit_DMA(&hspi1, data, size);// 注意:需要在传输完成中断中拉高CS
}void spi_receive_dma(uint8_t *data, uint16_t size) {spi_cs_low(&spi_config);HAL_SPI_Receive_DMA(&hspi1, data, size);
}// DMA传输完成回调
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {if(hspi->Instance == SPI1) {spi_cs_high(&spi_config);printf("SPI DMA发送完成\n");}
}void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {if(hspi->Instance == SPI1) {spi_cs_high(&spi_config);printf("SPI DMA接收完成\n");}
}// DMA错误处理
void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi) {printf("SPI DMA错误: 0x%08lX\n", HAL_SPI_GetError(hspi));spi_cs_high(&spi_config);
}

SPI错误处理

// SPI错误统计
typedef struct {uint32_t total_transactions;uint32_t dma_errors;uint32_t timeout_errors;uint32_t mode_errors;uint32_t crc_errors;
} spi_error_stats_t;static spi_error_stats_t spi_stats = {0};// 错误处理
void spi_handle_error(SPI_HandleTypeDef *hspi) {uint32_t error_code = HAL_SPI_GetError(hspi);if(error_code & HAL_SPI_ERROR_CRC) {spi_stats.crc_errors++;printf("SPI错误: CRC校验失败\n");}if(error_code & HAL_SPI_ERROR_MODF) {spi_stats.mode_errors++;printf("SPI错误: 模式错误\n");}if(error_code & HAL_SPI_ERROR_OVR) {printf("SPI错误: 溢出错误\n");}if(error_code & HAL_SPI_ERROR_DMA) {spi_stats.dma_errors++;printf("SPI错误: DMA传输错误\n");}if(error_code & HAL_SPI_ERROR_FLAG) {printf("SPI错误: 标志错误\n");}if(error_code & HAL_SPI_ERROR_TIMEOUT) {spi_stats.timeout_errors++;printf("SPI错误: 超时\n");}// 重新初始化SPIHAL_SPI_DeInit(hspi);HAL_Delay(10);HAL_SPI_Init(hspi);
}// SPI性能统计
void spi_performance_report(void) {printf("=== SPI性能统计 ===\n");printf("总事务数: %lu\n", spi_stats.total_transactions);printf("DMA错误: %lu\n", spi_stats.dma_errors);printf("超时错误: %lu\n", spi_stats.timeout_errors);printf("模式错误: %lu\n", spi_stats.mode_errors);printf("CRC错误: %lu\n", spi_stats.crc_errors);if(spi_stats.total_transactions > 0) {float error_rate = (float)(spi_stats.dma_errors + spi_stats.timeout_errors + spi_stats.mode_errors + spi_stats.crc_errors) / spi_stats.total_transactions * 100.0f;printf("错误率: %.2f%%\n", error_rate);}
}

8. 实际应用案例

多从设备SPI系统

// 多从设备SPI管理器
typedef struct {SPI_HandleTypeDef *hspi;GPIO_TypeDef *cs_flash_port;uint16_t cs_flash_pin;GPIO_TypeDef *cs_lcd_port;uint16_t cs_lcd_pin;GPIO_TypeDef *cs_sensor_port;uint16_t cs_sensor_pin;GPIO_TypeDef *cs_sd_port;uint16_t cs_sd_pin;
} spi_multislave_manager_t;// 选择SPI设备
void spi_select_device(spi_multislave_manager_t *manager, uint8_t device) {// 先取消所有片选HAL_GPIO_WritePin(manager->cs_flash_port, manager->cs_flash_pin, GPIO_PIN_SET);HAL_GPIO_WritePin(manager->cs_lcd_port, manager->cs_lcd_pin, GPIO_PIN_SET);HAL_GPIO_WritePin(manager->cs_sensor_port, manager->cs_sensor_pin, GPIO_PIN_SET);HAL_GPIO_WritePin(manager->cs_sd_port, manager->cs_sd_pin, GPIO_PIN_SET);// 选择指定设备switch(device) {case 0:  // FlashHAL_GPIO_WritePin(manager->cs_flash_port, manager->cs_flash_pin, GPIO_PIN_RESET);break;case 1:  // LCDHAL_GPIO_WritePin(manager->cs_lcd_port, manager->cs_lcd_pin, GPIO_PIN_RESET);break;case 2:  // 传感器HAL_GPIO_WritePin(manager->cs_sensor_port, manager->cs_sensor_pin, GPIO_PIN_RESET);break;case 3:  // SD卡HAL_GPIO_WritePin(manager->cs_sd_port, manager->cs_sd_pin, GPIO_PIN_RESET);break;}HAL_Delay(1);  // 片选稳定时间
}// 多设备数据采集系统
typedef struct {spi_multislave_manager_t spi_manager;bool flash_available;bool lcd_available;bool sensor_available;bool sd_available;
} spi_system_t;bool spi_system_init(spi_system_t *system) {// 初始化SPIspi_hardware_init();// 检测各设备system->flash_available = w25q128_detect();system->lcd_available = lcd_detect();// system->sensor_available = sensor_detect();// system->sd_available = sd_detect();printf("SPI系统初始化完成:\n");printf("Flash: %s\n", system->flash_available ? "就绪" : "未找到");printf("LCD: %s\n", system->lcd_available ? "就绪" : "未找到");printf("传感器: %s\n", system->sensor_available ? "就绪" : "未找到");printf("SD卡: %s\n", system->sd_available ? "就绪" : "未找到");return (system->flash_available || system->lcd_available || system->sensor_available || system->sd_available);
}

总结

SPI通信关键知识点:

  1. 总线结构:四线制(SCK、MOSI、MISO、CS),全双工同步通信

  2. 工作模式:4种模式(由CPOL和CPHA决定)

  3. 通信时序:时钟极性和相位的组合

  4. 多从设备:通过片选信号选择从设备

  5. 高速传输:相比I2C速度更快,但需要更多引脚

嵌入式开发建议:

  1. 根据从设备要求设置正确的SPI模式

  2. 使用DMA进行大数据量传输提高效率

  3. 注意片选信号的管理,避免总线冲突

  4. 实现错误处理和恢复机制

  5. 在高速通信时注意信号完整性

http://www.dtcms.com/a/436291.html

相关文章:

  • 衡阳网站设计ss0734做个人网站怎么赚钱
  • rk3566泰山派uart串口基础使用(应用层)
  • 直播网站功能怎么做东莞专业做网站
  • 判断网站学生做微商怎么加入
  • 商业网站后缀名徐州网站建设 网站推广
  • 免费打广告网站中国建信网官方网站
  • 网站建设中的问题wordpress影视主题下载
  • 中小企业建站可以怎么做90自己做网站
  • 一个空间怎么放两个网站企业网站模板建立流程
  • 第六章 与学习相关的技巧
  • 电商设计网站有哪些内容佛山新网站建设效果
  • 网站改版 总结小程序源代码免费模板
  • 泰安高端网站设计建设包头网站建设易通
  • 黑名单校验功能测试指南
  • 管家二维码预约免审核功能测试指南
  • 百度怎么做网站排名海外网络连接器
  • 佛山微信网站建设虚拟主机 两个网站
  • 黑山网站制作公司中天建设集团有限公司山东分公司
  • 兰州网站优化软件网页设计实训报告word
  • 建筑网站大全玻璃自己做的网站怎么搜不到
  • 手机网站引导页js企业开发网站用什么技术
  • 长沙市师德师风建设网站工业产品设计图片欣赏
  • 对象创建位置差异的详细分析
  • 建设网站多少钱 2017深圳企业官方网站建设
  • 有哪些做婚礼平面设计的网站网站建设备案需要什么
  • 资阳视频网站建设厦门仿站定制模板建站
  • 做网站外包价格报ui设计班
  • 合肥网站制作方案园林景观设计公司年度运营方案
  • 深圳网站优化方案北京做网站的外包公司
  • 住房和城乡建设部网站干部学院郑州官方最新通告