STM32F4实战:用软件I2C驱动ADS1115实现多通道电压采集(附完整代码)
在嵌入式开发中,精确的模拟信号采集是许多项目的基础需求。当我们需要测量多个通道的电压信号时,ADS1115这款16位精度的ADC芯片就成为了理想选择。本文将带你从零开始,在STM32F4平台上通过软件模拟I2C接口驱动ADS1115,实现稳定可靠的多通道电压采集系统。
1. 硬件准备与连接
在开始编码前,我们需要准备好硬件并正确连接。ADS1115模块通常采用标准的4引脚接口:VCC、GND、SCL和SDA。对于STM32F4开发板,我们可以选择任意两组GPIO引脚来模拟I2C通信。
典型连接方式如下:
| ADS1115引脚 | STM32F4连接 | 备注 |
|---|---|---|
| VCC | 3.3V | 电源正极 |
| GND | GND | 电源地 |
| SCL | PA6 | 时钟线,可自定义 |
| SDA | PA5 | 数据线,可自定义 |
| ADDR | GND | 决定I2C设备地址 |
提示:ADDR引脚的不同连接方式对应不同的I2C地址。接地时地址为0x48(8位写地址0x90)。
实际项目中,我们还需要注意以下硬件细节:
- 电源去耦:在ADS1115的VCC和GND之间添加0.1μF陶瓷电容
- 信号保护:在长距离连接时,考虑添加I2C总线保护电路
- 参考电压:ADS1115内置2.048V参考,无需外接基准源
2. 软件I2C驱动实现
由于硬件I2C可能存在兼容性问题,软件模拟I2C提供了更好的灵活性。下面是一个针对STM32F4优化的软件I2C实现:
// i2c_virtual.h #ifndef __I2C_VIRTUAL_H__ #define __I2C_VIRTUAL_H__ #include "stm32f4xx.h" #define I2C_Virtual_ack 0 #define I2C_Virtual_noack 1 void I2C_Virtual_ConfigPort(char SDA_PORT, uint8_t SDA_PIN, char SCL_PORT, uint8_t SCL_PIN); void I2C_Virtual_Start(void); void I2C_Virtual_Stop(void); void I2C_Virtual_SendByte(uint8_t data); uint8_t I2C_Virtual_RcvByte(void); void I2C_Virtual_Ack(void); void I2C_Virtual_NoAck(void); #endif对应的源文件实现关键时序控制:
// i2c_virtual.c #include "i2c_virtual.h" static GPIO_TypeDef* SDA_PORT; static uint16_t SDA_PIN; static GPIO_TypeDef* SCL_PORT; static uint16_t SCL_PIN; void I2C_Virtual_ConfigPort(char SDA_PORT_char, uint8_t SDA_PIN_num, char SCL_PORT_char, uint8_t SCL_PIN_num) { // 根据字符参数转换为具体的GPIO端口 switch(SDA_PORT_char) { case 'A': SDA_PORT = GPIOA; break; case 'B': SDA_PORT = GPIOB; break; // 其他端口类似 } SDA_PIN = 1 << SDA_PIN_num; // SCL端口配置同理 switch(SCL_PORT_char) { case 'A': SCL_PORT = GPIOA; break; case 'B': SCL_PORT = GPIOB; break; } SCL_PIN = 1 << SCL_PIN_num; // 配置GPIO为开漏输出模式 GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Pin = SDA_PIN; HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct); GPIO_InitStruct.Pin = SCL_PIN; HAL_GPIO_Init(SCL_PORT, &GPIO_InitStruct); // 初始状态拉高 HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_SET); } void I2C_Virtual_Start(void) { // 起始条件:SCL高时SDA从高到低 HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_SET); delay_us(5); HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_RESET); delay_us(5); HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_RESET); delay_us(5); }3. ADS1115驱动开发
ADS1115的驱动开发需要理解其内部寄存器结构。芯片的主要配置通过Config寄存器实现,这是一个16位的寄存器,各位功能如下:
Config寄存器位域详解:
| 位域 | 名称 | 功能描述 | 常用设置值 |
|---|---|---|---|
| [15] | OS | 操作状态/单次转换启动 | 0x8000(启动转换) |
| [14:12] | MUX | 输入多路选择器 | 0x4000(AIN0-GND) |
| [11:9] | PGA | 可编程增益放大器设置 | 0x0200(±4.096V) |
| [8] | MODE | 工作模式 | 0x0100(单次) |
| [7:5] | DR | 数据速率 | 0x0080(128SPS) |
基于这些信息,我们可以定义ADS1115的初始化结构体和配置函数:
// ads1115.h typedef struct { uint16_t OS; uint16_t MUX; uint16_t PGA; uint16_t MODE; uint16_t DataRate; } ADS1115_ConfigTypeDef; void ADS1115_Init(void); uint8_t ADS1115_WriteConfig(ADS1115_ConfigTypeDef *config); uint8_t ADS1115_ReadData(int16_t *data); float ADS1115_ConvertToVoltage(int16_t raw, uint16_t PGA);驱动实现中,最关键的是配置写入和数据读取函数:
// ads1115.c uint8_t ADS1115_WriteConfig(ADS1115_ConfigTypeDef *config) { uint8_t buf[3]; uint16_t configValue = config->OS | config->MUX | config->PGA | config->MODE | config->DataRate; buf[0] = 0x01; // 指向配置寄存器 buf[1] = (uint8_t)(configValue >> 8); buf[2] = (uint8_t)(configValue & 0xFF); I2C_Virtual_Start(); if(I2C_Virtual_SendByte(0x90) == I2C_Virtual_noack) return 0; if(I2C_Virtual_SendByte(buf[0]) == I2C_Virtual_noack) return 0; if(I2C_Virtual_SendByte(buf[1]) == I2C_Virtual_noack) return 0; if(I2C_Virtual_SendByte(buf[2]) == I2C_Virtual_noack) return 0; I2C_Virtual_Stop(); return 1; } uint8_t ADS1115_ReadData(int16_t *data) { uint8_t buf[2]; // 设置指针寄存器为转换结果寄存器 I2C_Virtual_Start(); if(I2C_Virtual_SendByte(0x90) == I2C_Virtual_noack) return 0; if(I2C_Virtual_SendByte(0x00) == I2C_Virtual_noack) return 0; I2C_Virtual_Stop(); // 读取转换结果 I2C_Virtual_Start(); if(I2C_Virtual_SendByte(0x91) == I2C_Virtual_noack) return 0; buf[0] = I2C_Virtual_RcvByte(); I2C_Virtual_Ack(); buf[1] = I2C_Virtual_RcvByte(); I2C_Virtual_NoAck(); I2C_Virtual_Stop(); *data = (int16_t)((buf[0] << 8) | buf[1]); return 1; }4. 多通道采集策略实现
ADS1115虽然有4个输入通道,但内部只有一个ADC,需要通过MUX切换通道。高效的通道切换策略对系统性能至关重要。
4.1 基本轮询方法
最简单的实现方式是顺序切换通道并等待转换完成:
void ADS1115_ReadAllChannels(int16_t *results) { ADS1115_ConfigTypeDef config; for(uint8_t ch = 0; ch < 4; ch++) { config.MUX = 0x4000 | (ch << 12); // 设置通道 config.OS = 0x8000; // 启动单次转换 ADS1115_WriteConfig(&config); // 等待转换完成 delay_ms(10); // 最保守的等待时间 ADS1115_ReadData(&results[ch]); } }这种方法简单但效率低,因为大部分时间都在等待转换完成。
4.2 中断驱动的高效方法
更高效的方式是利用ADS1115的ALERT/RDY引脚作为中断源,当转换完成时触发中断:
// 在配置中启用比较器功能 config.COMP_MODE = 0; // 传统比较器模式 config.COMP_POL = 1; // 高电平有效 config.COMP_QUE = 0; // 转换后触发 // 中断服务例程 void EXTI0_IRQHandler(void) { if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0)) { static uint8_t current_ch = 0; int16_t raw; ADS1115_ReadData(&raw); channel_data[current_ch] = raw; current_ch = (current_ch + 1) % 4; ADS1115_SwitchChannel(current_ch); __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); } }4.3 定时器调度方法
在没有可用外部中断引脚时,可以使用定时器实现伪中断调度:
void TIM2_IRQHandler(void) { if(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE)) { static uint8_t state = 0; static uint8_t current_ch = 0; switch(state) { case 0: // 启动转换 ADS1115_SwitchChannel(current_ch); ADS1115_StartConversion(); state = 1; break; case 1: // 读取结果 int16_t raw; if(ADS1115_ReadData(&raw)) { channel_data[current_ch] = raw; current_ch = (current_ch + 1) % 4; state = 0; } break; } __HAL_TIM_CLEAR_IT(&htim2, TIM_IT_UPDATE); } }5. 完整工程实现与优化
将上述模块整合成一个完整的工程,我们还需要考虑以下方面:
5.1 电压转换与校准
ADS1115的原始数据需要根据PGA设置转换为实际电压值:
float ADS1115_ConvertToVoltage(int16_t raw, uint16_t PGA) { float lsb_size; switch(PGA) { case 0x0000: lsb_size = 6.144f / 32768.0f; break; // ±6.144V case 0x0200: lsb_size = 4.096f / 32768.0f; break; // ±4.096V case 0x0400: lsb_size = 2.048f / 32768.0f; break; // ±2.048V case 0x0600: lsb_size = 1.024f / 32768.0f; break; // ±1.024V case 0x0800: lsb_size = 0.512f / 32768.0f; break; // ±0.512V case 0x0A00: lsb_size = 0.256f / 32768.0f; break; // ±0.256V default: return 0.0f; } return raw * lsb_size; }5.2 数据滤波处理
ADC采样通常需要数字滤波来提高稳定性。下面是一个移动平均滤波的实现:
#define FILTER_WINDOW_SIZE 8 typedef struct { float buffer[FILTER_WINDOW_SIZE]; uint8_t index; float sum; } MovingAverageFilter; void Filter_Init(MovingAverageFilter *filter) { memset(filter->buffer, 0, sizeof(filter->buffer)); filter->index = 0; filter->sum = 0.0f; } float Filter_Update(MovingAverageFilter *filter, float new_value) { filter->sum -= filter->buffer[filter->index]; filter->buffer[filter->index] = new_value; filter->sum += new_value; filter->index = (filter->index + 1) % FILTER_WINDOW_SIZE; return filter->sum / FILTER_WINDOW_SIZE; }5.3 串口输出与调试
为了方便调试,我们可以添加串口输出功能,实时显示各通道电压值:
void Print_VoltageValues(float *voltages) { char buf[64]; int len; len = snprintf(buf, sizeof(buf), "CH0: %.3fV | CH1: %.3fV | CH2: %.3fV | CH3: %.3fV\r\n", voltages[0], voltages[1], voltages[2], voltages[3]); HAL_UART_Transmit(&huart2, (uint8_t*)buf, len, HAL_MAX_DELAY); }5.4 主程序流程
最终的主程序将整合所有模块:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART2_UART_Init(); MX_TIM2_Init(); // 初始化软件I2C I2C_Virtual_ConfigPort('A', 5, 'A', 6); // 初始化ADS1115配置 ADS1115_ConfigTypeDef config = { .OS = 0x8000, .MUX = 0x4000, // 初始通道0 .PGA = 0x0200, // ±4.096V .MODE = 0x0100, // 单次模式 .DataRate = 0x0080 // 128SPS }; ADS1115_WriteConfig(&config); // 初始化滤波器和数据缓冲区 MovingAverageFilter filters[4]; float voltages[4]; for(int i=0; i<4; i++) { Filter_Init(&filters[i]); } // 启动定时器 HAL_TIM_Base_Start_IT(&htim2); while(1) { // 主循环中定期打印电压值 for(int ch=0; ch<4; ch++) { voltages[ch] = Filter_Update(&filters[ch], ADS1115_ConvertToVoltage(channel_data[ch], config.PGA)); } Print_VoltageValues(voltages); HAL_Delay(500); } }6. 常见问题与解决方案
在实际项目中,开发者常会遇到以下典型问题:
6.1 I2C通信失败
症状:读取的数据全为0或0xFF,或无法收到ACK信号。
排查步骤:
- 用逻辑分析仪或示波器检查I2C波形
- 确认上拉电阻已连接(通常4.7kΩ)
- 检查设备地址是否正确(ADDR引脚配置)
- 降低I2C时钟速度测试
6.2 通道间串扰
症状:切换通道后,前几次采样数据不准确。
解决方案:
- 切换通道后丢弃前2次采样
- 增加通道切换后的等待时间
- 在硬件上为每个输入通道添加RC低通滤波
6.3 电源噪声影响
症状:采样值存在周期性波动。
改进措施:
// 软件方面: config.DataRate = 0x00E0; // 设置为860SPS,避开50Hz工频干扰 // 硬件方面: // 在ADS1115电源引脚添加10μF钽电容并联0.1μF陶瓷电容 // 在输入信号线上添加RC滤波(如1kΩ+0.1μF)6.4 多设备共享I2C总线
当系统中有多个I2C设备时,需要注意:
- 为每个ADS1115分配唯一地址(通过ADDR引脚)
- 总线电容不超过400pF(长距离时考虑使用I2C缓冲器)
- 在代码中添加设备选择机制:
void Select_ADS1115(uint8_t device_num) { switch(device_num) { case 0: I2C_Virtual_ConfigPort('A', 5, 'A', 6); // 设备0的SCL/SDA break; case 1: I2C_Virtual_ConfigPort('B', 7, 'B', 8); // 设备1的SCL/SDA break; } }