STM32F103+INA226电流电压采集与串口透传实战
简介本资源是一套基于STM32F103的嵌入式信息采集与串口通信完整工程面向嵌入式初学者及电子类课程设计、毕业设计实践者解决传感器数据实时采集、标定与远传的核心问题。项目以INA226高精度电流电压传感器为数据源通过I2C接口读取原始值结合ScaleV1G标度因子完成物理量转换并利用STM32内置ADC与USART实现模拟信号采集与串口周期性发送覆盖硬件驱动、协议处理与系统集成关键环节。压缩包含171个文件主体为41个.h头文件与39个.c源码文件含stm32f10x_adc.c、stm32f10x_i2c.c、stm32f10x_usart.c等核心外设驱动辅以.o/.d/.crf等编译中间文件及Keil工程配置uvprojx、axf、hex等整体大小4.04MB结构完整可直接编译下载运行。目前已有397人学习下载提供从硬件连接、CubeMX配置、HAL库调用到数据打包发送的全链路参考代码特别适合理解嵌入式多模块协同开发流程。1. 这不是普通串口发数据STM32F103 INA226 实时电流电压采集与串口透传专为工业传感场景设计你手头有一块 STM32F103C8T6 最小系统板接了 INA226 电流/电压双路高精度传感器模块想把实时采样值比如 0.001V 分辨率的电压、0.1mA 级电流稳定、低延迟地通过串口发给上位机做记录或监控——但发现裸机串口发送总丢数、AD 采样值跳变大、INA226 的 I²C 读取偶尔超时、甚至串口调试助手收不到完整帧。这不是驱动没装CH340 驱动已确认正常、也不是接线错误TX/RX 已交叉而是典型「多源异步数据流协同」问题ADC 采样定时、I²C 总线访问、串口发送缓冲区管理、帧格式封装四者未对齐。本方案不依赖 HAL 库 v3.50 或 CubeMX 自动生成代码全程基于标准外设库固件库 v3.5.0手动配置聚焦在INA226 寄存器精确读取、AD 采样与 I²C 访问的时序隔离、串口发送的非阻塞环形缓冲区实现、scalev1g 标定系数嵌入逻辑四个硬核环节。适合已能点亮 LED、会用 Keil 编译、但卡在传感器融合通信的中级嵌入式开发者。2. 从 INA226 数据手册到寄存器级读取避开 I²C 时序陷阱的三步法INA226 是 TI 推出的高精度电流/电压监测芯片其核心价值在于 16 位 ADC 分辨率和内置校准寄存器但直接套用通用 I²C 驱动极易失败——原因在于它对 SCL 低电平时间tLOW、SCL 高电平时间tHIGH、起始条件建立时间tSU;STA等参数要求严苛而 STM32F103 的标准外设库 I²C 初始化若按默认 100kHz 配置实际波形可能不满足 INA226 手册要求的 tLOW ≥ 4.7μs详见 INA226 datasheet Rev.E 第 9.2.2 节。必须手动计算并重设 I²C 时钟分频因子。2.1 精确计算 I²C 时钟分频值以 72MHz APB1 为例STM32F103 的 I²C 挂在 APB1 总线上典型主频为 72MHz。标准外设库中I2C_Init()函数通过I2C_InitStructure.I2C_ClockSpeed设置目标速率但该值仅控制 SCL 频率上限真实波形由CCRClock Control Register和TRISEMaximum Rise Time Register共同决定。INA226 要求 100kHz 模式下 tLOW ≥ 4.7μstHIGH ≥ 4.0μs。计算步骤如下先确定 APB1 频率RCC_GetAPB1Freq()返回 72,000,000 Hz计算 CCR 值CCR (APB1_Freq / (2 * I2C_Speed)) - 1代入得CCR (72000000 / (2 * 100000)) - 1 359计算 TRISE 值TRISE (APB1_Freq / 1000000) 1 72 1 73单位ns需 ≤ 1000ns提示若实测示波器显示 SCL 低电平时间不足 4.7μs需增大 CCR 值即降低实际 SCL 频率例如设为CCR 400此时实际频率约为72000000 / (2 * 401) ≈ 89.8kHz完全兼容 INA226。2.2 INA226 寄存器映射与关键字段解析INA226 的数据存储在连续地址空间最常用的是0x00Shunt Voltage、0x01Bus Voltage、0x02Power、0x03Current四个 16 位只读寄存器。注意所有寄存器均为MSB 在前Big-Endian读取时需先读高字节再读低字节Bus Voltage寄存器值需乘以1.25mV得到实际电压如0x0A00→2560 * 1.25mV 3.2VCurrent寄存器值需乘以Current_LSB由校准寄存器0x05决定得到实际电流而Current_LSB 0.00512 / RSHUNT单位A其中RSHUNT是你电路中使用的分流电阻阻值如 0.01Ω。2.2.1 校准寄存器0x05的写入逻辑仅首次上电需执行// 假设 RSHUNT 0.01Ω则 Current_LSB 0.00512 / 0.01 0.512mA // 对应 Calibration 寄存器值 0.00512 / (Current_LSB * RSHUNT) 0.00512 / (0.000512 * 0.01) 1000 // 注意Calibration 寄存器是 16 位无符号整数写入值为 10000x03E8 uint16_t cal_val 1000; // 根据实际 RSHUNT 计算 uint8_t cal_data[2]; cal_data[0] (cal_val 8) 0xFF; // 高字节 cal_data[1] cal_val 0xFF; // 低字节 I2C_WriteBuffer(INA226_ADDR, 0x05, cal_data, 2); // 向地址 0x05 写入 2 字节此步骤必须在首次上电后执行一次后续掉电不丢失INA226 内部 EEPROM 存储。若跳过Current寄存器读数将严重失真。2.3 带超时保护的 I²C 读取函数避免死锁在 BUSY 标志标准库I2C_CheckEvent()在总线异常时可能无限等待。以下函数强制加入 10ms 超时并返回具体错误码#define I2C_TIMEOUT_MS 10 uint8_t INA226_ReadReg(uint8_t reg_addr, uint16_t* data) { uint32_t timeout SystemCoreClock / 1000 * I2C_TIMEOUT_MS; // 转为 SysTick ticks uint8_t buffer[2]; // 1. 发送 START I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) timeout--) { if (timeout 0) return 1; // TIMEOUT_START } // 2. 发送器件地址写方向 I2C_Send7bitAddress(I2C1, INA226_ADDR, I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) timeout--) { if (timeout 0) return 2; // TIMEOUT_ADDR } // 3. 发送寄存器地址 I2C_SendData(I2C1, reg_addr); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED) timeout--) { if (timeout 0) return 3; // TIMEOUT_REGADDR } // 4. 重复 STARTRe-START I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) timeout--) { if (timeout 0) return 4; // TIMEOUT_RESTART } // 5. 发送器件地址读方向 I2C_Send7bitAddress(I2C1, INA226_ADDR, I2C_Direction_Receiver); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) timeout--) { if (timeout 0) return 5; // TIMEOUT_READADDR } // 6. 读取高字节NACK 前 while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED) timeout--) { if (timeout 0) return 6; // TIMEOUT_RXH } buffer[0] I2C_ReceiveData(I2C1); // 7. 读取低字节发送 NACK STOP I2C_AcknowledgeConfig(I2C1, DISABLE); I2C_GenerateSTOP(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED) timeout--) { if (timeout 0) return 7; // TIMEOUT_RXL } buffer[1] I2C_ReceiveData(I2C1); *data (buffer[0] 8) | buffer[1]; return 0; // SUCCESS }该函数返回0表示成功非零值对应具体超时阶段便于定位 I²C 物理层问题如上拉电阻不足、线路干扰。3. AD 采样与串口发送的协同调度用定时器触发 环形缓冲区解耦STM32F103 的 ADC 通常用于采集片内温度传感器或外部模拟信号但本项目标题中 “AD采集” 更可能指利用 ADC 监控 INA226 的 REF 引脚电压或辅助电源轨而非替代 INA226 的精密测量。此处我们构建一个独立于 I²C 的 ADC 采集通道例如 PA0与 INA226 数据并行获取再统一打包发送。关键矛盾在于ADC 转换完成中断EOC和串口发送完成中断TC不能直接嵌套调用printf类函数占用栈空间大、不可重入必须用缓冲区解耦。3.1 定时器触发 ADC确保严格周期性采样使用 TIM2 定时器APB1产生 100Hz 触发信号即每 10ms 采样一次避免软件延时不准void TIM2_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_DeInit(TIM2); TIM_TimeBaseStructure.TIM_Period 7199; // 自动重装载值 (72MHz / 10000Hz - 1) TIM_TimeBaseStructure.TIM_Prescaler 9; // 预分频 10 (72MHz / 10 7.2MHz) TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // 使能 TIM2 触发 ADC TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); TIM_Cmd(TIM2, ENABLE); } void ADC1_Config(void) { ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); // PA0 配置为模拟输入 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); ADC_DeInit(ADC1); ADC_StructInit(ADC_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; // 单通道 ADC_InitStructure.ADC_ContinuousConvMode DISABLE; // 单次转换 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_TRGO; // TIM2 更新事件触发 ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55_5Cycles); // PA0 ADC_Cmd(ADC1, ENABLE); ADC_DMACmd(ADC1, ENABLE); // 可选启用 DMA 减轻 CPU 负担 }注意ADC_ExternalTrigConv_T2_TRGO将 TIM2 的更新事件Update Event作为 ADC 启动源确保每次 TIM2 计数溢出时精确触发一次 ADC 转换消除软件延时抖动。3.2 串口发送环形缓冲区实现支持非阻塞、防覆盖定义一个 256 字节的环形缓冲区由 ADC 中断和 INA226 读取任务共同写入由 USART1 发送中断TC持续消费#define UART_TX_BUFFER_SIZE 256 typedef struct { uint8_t buffer[UART_TX_BUFFER_SIZE]; volatile uint16_t head; volatile uint16_t tail; } RingBuffer; RingBuffer uart_tx_buffer {0}; // 写入缓冲区供 ADC 和 INA226 任务调用 uint8_t UART_TxBufferWrite(uint8_t *data, uint16_t len) { uint16_t i; for (i 0; i len; i) { if ((uart_tx_buffer.head 1) % UART_TX_BUFFER_SIZE ! uart_tx_buffer.tail) { uart_tx_buffer.buffer[uart_tx_buffer.head] data[i]; uart_tx_buffer.head (uart_tx_buffer.head 1) % UART_TX_BUFFER_SIZE; } else { return 1; // Buffer full } } return 0; // Success } // 从缓冲区读取一个字节供 USART1 中断调用 uint8_t UART_TxBufferRead(uint8_t *byte) { if (uart_tx_buffer.head ! uart_tx_buffer.tail) { *byte uart_tx_buffer.buffer[uart_tx_buffer.tail]; uart_tx_buffer.tail (uart_tx_buffer.tail 1) % UART_TX_BUFFER_SIZE; return 0; } return 1; // Buffer empty }3.3 数据帧封装协议适配 scalev1g 标定与上位机解析标题中的scalev1g暗示数据需按特定比例缩放后发送。我们定义一帧固定格式共 12 字节字段长度说明Header2B0xAA 0x55Bus Voltage (V)4Bfloat经scalev1g缩放如raw_v * 0.00125fShunt Voltage (mV)4Bfloat原始值INA2260x00寄存器 * 10μVADC Value2BPA0 ADC 原始值0-4095typedef __packed struct { uint8_t header[2]; // 0xAA, 0x55 float bus_v; // 单位V float shunt_mv; // 单位mV uint16_t adc_raw; // 0-4095 } SensorFrame; SensorFrame frame; // 在 ADC 转换完成中断中填充 frame void ADC1_IRQHandler(void) { if (ADC_GetITStatus(ADC1, ADC_IT_EOC) ! RESET) { uint16_t adc_val ADC_GetConversionValue(ADC1); frame.adc_raw adc_val; // 触发 INA226 读取放在主循环此处仅标记 new_data_ready 1; ADC_ClearITPendingBit(ADC1, ADC_IT_EOC); } } // 主循环中当 new_data_ready 为真时读取 INA226 并组帧 if (new_data_ready) { uint16_t bus_reg, shunt_reg; if (INA226_ReadReg(0x01, bus_reg) 0 INA226_ReadReg(0x00, shunt_reg) 0) { frame.header[0] 0xAA; frame.header[1] 0x55; frame.bus_v (float)bus_reg * 0.00125f; // scalev1g: 1.25mV per LSB frame.shunt_mv (float)shunt_reg * 0.01f; // 10μV per LSB → mV UART_TxBufferWrite((uint8_t*)frame, sizeof(frame)); } new_data_ready 0; }提示__packed关键字确保结构体无内存对齐填充保证sizeof(SensorFrame)精确为 12 字节避免上位机解析错位。4. 串口初始化与中断驱动发送解决“串口烧写失败”之外的真实瓶颈很多开发者卡在“串口烧写失败”但本项目真正的瓶颈是串口发送速率与数据生成速率不匹配导致缓冲区溢出。例如若上位机波特率设为 9600bps每秒最多发送 960 字节而我们的SensorFrame每 10ms 一帧100Hz每帧 12 字节理论需 1200 字节/秒带宽 —— 此时 9600bps 不足必须升至 115200bps。且发送不能用while(!USART_GetFlagStatus(USART1, USART_FLAG_TC));阻塞等待必须用中断。4.1 USART1 初始化明确指定引脚复用与波特率误差void USART1_Config(void) { USART_InitTypeDef USART_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // PA9: TX, PA10: RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_DeInit(USART1); USART_InitStructure.USART_BaudRate 115200; // 必须 ≥ 1200 Bps USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Tx | USART_Mode_Rx; USART_Init(USART1, USART_InitStructure); // 使能发送完成中断和空闲中断可选 USART_ITConfig(USART1, USART_IT_TC, ENABLE); // TC: Transmission Complete USART_Cmd(USART1, ENABLE); }4.2 串口发送中断服务程序从环形缓冲区取数发送volatile uint8_t tx_busy 0; void USART1_IRQHandler(void) { uint8_t byte; USART_TypeDef* USARTx USART1; uint32_t status USARTx-SR; // 处理发送完成中断TC if (status USART_SR_TC) { if (UART_TxBufferRead(byte) 0) { USART_SendData(USARTx, byte); tx_busy 1; } else { tx_busy 0; // 缓冲区空停止发送 } } // 处理接收中断可选用于上位机命令 if (status USART_SR_RXNE) { uint8_t rx_byte USART_ReceiveData(USARTx); // 解析命令如 R 请求立即发送一帧 if (rx_byte R) { // 立即触发一次 INA226 读取并组帧 uint16_t bus_reg, shunt_reg; if (INA226_ReadReg(0x01, bus_reg) 0 INA226_ReadReg(0x00, shunt_reg) 0) { frame.header[0] 0xAA; frame.header[1] 0x55; frame.bus_v (float)bus_reg * 0.00125f; frame.shunt_mv (float)shunt_reg * 0.01f; frame.adc_raw ADC_GetConversionValue(ADC1); UART_TxBufferWrite((uint8_t*)frame, sizeof(frame)); } } } }注意tx_busy标志用于区分“首次发送”和“后续发送”。首次需手动触发USART_SendData()之后由 TC 中断自动驱动避免在主循环中轮询。4.3 关键参数表不同波特率下的最大帧率与缓冲区安全阈值波特率理论最大字节/秒每帧字节数最大安全帧率Hz对应 TIM2 周期缓冲区最小长度256B 下9600960128012.5ms20 帧240B11520011520129601.04ms21 帧252B230400230401219200.52ms21 帧252B若实际帧率超过表中“最大安全帧率”环形缓冲区必然溢出。此时必须① 升高波特率② 减少单帧数据量如只发 int16_t 而非 float③ 在主循环中增加if (uart_tx_buffer.head - uart_tx_buffer.tail 64)判断丢弃旧帧保实时性。5. 实战验证与排错用串口调试助手和逻辑分析仪定位三类高频故障部署完成后常见故障并非代码逻辑错误而是硬件连接与时序配合问题。以下方法可快速定位5.1 故障一串口调试助手收到乱码或部分数据现象0xAA 0x55头正确但后续 float 字段显示为0.000000或极大值如1.234e38。根因float类型在 ARM Cortex-M3 上默认使用软浮点SoftFP而 Keil MDK 默认链接fpu.lib若未开启 FPU 支持memcpy复制 float 会因字节序或对齐问题出错。验证在 Keil 中 Project → Options → Target → Floating Point Hardware → 选择Use FPU若芯片有 FPU或Use Software FloatSTM32F103 无 FPU必须选此项。修复改用定点数传输。将frame.bus_v改为int32_t bus_v_mV发送前frame.bus_v_mV (int32_t)(bus_reg * 1.25f);上位机除以 1000 得 V。5.2 故障二INA226 读数始终为 0 或恒定值现象INA226_ReadReg(0x01, bus_reg)总返回0x0000。排查路径用万用表测 INA226 的 VBUS 引脚电压是否正常应为待测电源电压用逻辑分析仪抓 I²C 波形确认 SCL/SDA 电平是否符合规范高电平 ≥ 2.0V低电平 ≤ 0.4V检查INA226_ADDR是否正确默认0x40A0 引脚接地若 A0 接 VCC 则为0x41在INA226_ReadReg()函数开头添加I2C_GenerateSTOP(I2C1, ENABLE);强制释放总线排除前次操作遗留 BUSY 状态。5.3 故障三ADC 采样值跳变剧烈不受控现象PA0 接 3.3V 时 ADC 值在 4000~4095 间跳变。根因未启用 ADC 电源稳压器VREFINT或未等待稳定。STM32F103 的 ADC 需要内部参考电压稳定。修复代码// 在 ADC_Init() 之前添加 ADC_VrefintCmd(ENABLE); Delay_ms(10); // 等待 VREFINT 稳定同时确保ADC_SampleTime_55_5Cycles足够长≥ 55.5 个 ADC 时钟周期避免采样时间不足。5.3.1 串口数据记录仪使用技巧捕获长期趋势若需验证 24 小时电流波动不要依赖串口调试助手易丢帧改用专业工具WindowsModbusPoll设置为 ASCII 模式导入自定义帧格式.csvLinuxscreen /dev/ttyUSB0 115200script -c cat session.logPython 脚本用pyserial读取二进制帧struct.unpack(2sffH, data)解析SensorFramematplotlib绘图。关键点所有工具必须设置无硬件流控No RTS/CTS否则 STM32F103 未实现流控信号会阻塞发送。提示scalev1g的最终体现是在上位机解析环节。例如若 INA226 的Bus Voltage寄存器值为0x19A06560乘以1.25mV得8.200V此即scalev1g的物理意义——将原始寄存器值线性映射为工程单位。务必在上位机代码中固化此系数而非在 MCU 端做浮点运算增加负担。本文还有配套的精品资源点击获取