最近在做一个光伏发电的小型监控项目需要实时检测光伏板的输出电压和电流并计算出发电量。在选型时发现STM32F103C8T6这款“国民MCU”性价比极高配合一些常见的传感器和OLED显示屏就能搭建一套完整的检测系统。网上资料虽然多但要么只讲硬件要么代码不完整调试起来很费劲。本文将手把手带你完成从硬件选型、电路设计、代码编写到Proteus仿真的全流程并提供可直接复用的核心代码。无论你是学生做课程设计还是工程师做原型验证都能从中找到清晰的步骤和避坑指南。1. 项目背景与核心概念1.1 光伏发电量检测的意义光伏发电系统将太阳能直接转换为电能其核心部件是光伏板。为了评估系统性能、进行故障诊断和实现能量管理实时、准确地检测光伏板的发电量即输出功率至关重要。发电量通常通过测量光伏板的输出电压V和输出电流I并依据公式P V × I计算得出瞬时功率再对功率进行时间积分得到累计发电量如瓦时 Wh。一个完整的检测系统需要完成数据采集电压、电流、数据处理计算、滤波、数据显示实时数值、累计值以及可能的远程通信等功能。本设计旨在构建一个低成本、高可靠性的本地化检测终端。1.2 核心器件选型为什么是STM32F103C8T6STM32F103C8T6属于意法半导体ST的STM32F1系列基于ARM Cortex-M3内核。它成为众多嵌入式开发者和学生项目的首选原因如下性价比极高价格亲民性能足以应对大多数控制、采集和通信任务。资源丰富拥有64KB Flash、20KB RAM、多达37个GPIO、3个USART、2个SPI、2个I2C、2个ADC等外设满足本项目所有需求。生态完善拥有成熟的开发环境Keil、IAR、STM32CubeIDE、丰富的库函数标准库、HAL库和海量的社区资源学习成本和开发门槛低。引脚兼容其“最小系统板”在市场上非常普及引脚排列标准方便焊接和调试。1.3 系统方案概述本设计采用模块化思想系统框图如下[光伏板] - [电压采样电路] - [STM32F103C8T6 ADC1] - [电流采样电路] - [STM32F103C8T6 ADC1] | V [数据处理与计算] | V [OLED12864显示屏] (通过I2C/SPI) | V [本地显示电压、电流、功率、累计电量]电压采样采用电阻分压网络将光伏板较高的输出电压如0-30V线性降低到STM32 ADC可测量的安全范围0-3.3V。电流采样采用霍尔电流传感器如ACS712或采样电阻运放方案将电流信号转换为电压信号供ADC采集。主控STM32F103C8T6负责ADC采样、数值计算、OLED驱动和逻辑控制。显示OLED12864显示屏具有自发光、高对比度、低功耗、接口简单I2C或SPI的优点非常适合嵌入式显示。2. 硬件设计与电路原理2.1 STM32F103C8T6最小系统最小系统是MCU工作的基础必须包含以下部分电源电路通过AMS1117-3.3等LDO芯片将5V输入转换为稳定的3.3V为MCU及周边器件供电。注意电源滤波电容的布置。复位电路采用RC复位保证上电稳定。时钟电路外部接入8MHz晶振HSE和32.768kHz晶振LSE可选为系统提供精准时钟源。启动模式选择通过BOOT0和BOOT1引脚设置启动方式通常BOOT0接低电平GND从主Flash启动。调试接口预留SWDSWDIO SWCLK接口用于程序下载和调试比传统的JTAG占用引脚更少。2.2 电压采样电路设计光伏板开路电压可能较高例如12V、18V、24V板。直接测量会损坏ADC。采用电阻分压是最简单可靠的方法。Vin (0-30V) | R1 (例如 100kΩ) | --- Vmeasure - ADC_Input_Pin (0-3.3V) | R2 (例如 10kΩ) | GND分压比计算Vmeasure Vin * R2 / (R1 R2)。为确保Vin最大时Vmeasure不超过3.3V需选择合适的R1和R2。例如Vin_max30V 令Vmeasure_max3.0V留有余量则分压比应为 3.0/30 0.1。若R210kΩ则R1 R2*(1/0.1 - 1) 10k*(10-1)90kΩ取标称值100kΩ。滤波与保护在Vmeasure点对地并联一个0.1uF的电容用于滤除高频噪声。在ADC输入引脚前串联一个100Ω电阻并并联一个TVS二极管如3.6V可提供过压保护。校准由于电阻存在精度误差实际应用中需要通过标准电压表测量真实Vin和ADC读到的Vmeasure计算出一个校准系数。2.3 电流采样电路设计对于小功率光伏板推荐使用ACS712系列霍尔电流传感器模块。它隔离性好使用方便。型号选择ACS712-05A量程±5A、ACS712-20A量程±20A等根据光伏板最大短路电流选择。接口模块通常有VCC5V、GND、OUT、IP、IP-五个引脚。VCC接5VGND接共地OUT接STM32的ADC输入引脚。IP和IP-串联到光伏板的输出回路中。原理ACS712输出一个与输入电流成比例的电压。以ACS712-05B5A量程灵敏度185mV/A为例当输入电流为0A时OUT引脚电压为VCC/2 2.5V。电流每增加1A输出电压增加185mV。因此STM32 ADC需要测量这个0.5V~4.5V对应-5A~5A的电压。由于STM32 ADC参考电压为3.3V直接测量4.5V会超量程。因此需要对ACS712的输出电压进行二次分压将其范围压缩到0-3.3V内或者在软件中只使用其线性区间的一部分例如仅测量正电流且保证最大电流时电压不超过3.3V。更稳妥的方法是使用运放搭建一个电平移位和缩放电路。简化方案软件处理如果光伏板电流始终为正且最大电流对应ACS712输出电压小于3.3V则可直接连接。假设VCC5V灵敏度185mV/A最大电流Imax2A。则最大输出电压Vout_max 2.5V 2A*0.185V/A 2.87V小于3.3V安全。此时ADC测量值对应的电流计算公式为I (ADC_Voltage - 2.5) / 0.185。2.4 OLED12864显示接口OLED模块常见接口有I2C和SPI。I2C接口只需2根线SCL SDA节省IO更常用。电路连接OLED VCC - 3.3VOLED GND - GNDOLED SCL - STM32的PB6I2C1_SCL或PB10I2C2_SCLOLED SDA - STM32的PB7I2C1_SDA或PB11I2C2_SDA上拉电阻I2C总线需要上拉电阻通常4.7kΩ~10kΩ很多OLED模块已内置若未内置需在STM32引脚接上拉电阻到3.3V。2.5 整体电路与Proteus仿真准备在动手焊接实物前使用Proteus进行仿真可以验证电路逻辑和程序流程的正确性。Proteus元件库需要添加STM32F103C6C8T6的仿真模型常用C6代替、OLED显示器如SSD1306、电压/电流信号源模拟光伏板输出、运算放大器等。仿真思路用直流电压源和电流控制电流源模拟光伏板的输出。用电阻网络模拟电压分压电路。用Proteus的“模拟电压源”直接输出一个0-3.3V的电压来模拟ACS712的输出简化仿真。将这两个电压连接到STM32的ADC引脚。编写程序读取ADC计算功率并通过虚拟终端Virtual Terminal或仿真的OLED模型显示结果。3. 软件开发环境与工程配置3.1 开发环境搭建IDE选择推荐使用Keil uVision5 MDK因为它对ARM芯片支持好生态完善。也可以使用免费的STM32CubeIDE。安装器件包在Keil中需要安装STM32F1系列的器件支持包DFP。STM32CubeMX这是一个图形化配置工具可以自动生成初始化代码基于HAL库极大简化GPIO、ADC、I2C、定时器等外设的配置过程。强烈建议使用。串口调试助手如SSCOM、XCOM用于通过串口打印调试信息。3.2 使用STM32CubeMX创建工程选择芯片打开CubeMX选择STM32F103C8Tx。配置时钟RCC-High Speed Clock (HSE)-Crystal/Ceramic Resonator.在Clock Configuration标签页将HSE输入设为8MHz然后配置PLL将系统时钟SYSCLK设置为72MHz这是F103的最高主频。配置ADC在Analog-ADC1中启用IN0和IN1通道假设电压接PA0电流接PA1。参数设置Resolution设为12位4096级Scan Conversion Mode和Continuous Conversion Mode根据需求使能。如果只采样两个通道可以开启扫描模式和连续转换。在Parameter Settings中配置Sampling Time采样时间对于中等阻抗源设为239.5 Cycles可提高精度。配置I2C在Connectivity-I2C1中选择I2C模式。将PB6和PB7配置为I2C1_SCL和I2C1_SDA。参数通常保持默认标准模式100kHz。配置USART用于调试在Connectivity-USART1中选择Asynchronous模式。将PA9和PA10配置为USART1_TX和USART1_RX。参数波特率115200数据位8停止位1无校验。配置项目Project Manager-Project设置项目名称、路径、IDEMDK-ARM。Code Generator选择“Copy only necessary library files”并勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files”。生成代码点击GENERATE CODE生成Keil工程。4. 核心代码实现4.1 ADC采样与数据处理在CubeMX生成的工程基础上我们编写ADC采样和计算代码。/* 文件 main.c */ /* 在USER CODE BEGIN Includes 区域添加 */ #include stdio.h #include string.h /* 在USER CODE BEGIN PV 区域添加 */ // 定义校准系数和变量 #define VOLTAGE_DIVIDER_RATIO (30.0 / 3.0) // 假设分压电路将30V映射到3V实际需校准 #define CURRENT_SENSOR_SENSITIVITY 0.185 // ACS712灵敏度V/A #define CURRENT_SENSOR_QUIESCENT_VOLTAGE 2.5 // ACS712零电流输出电压V float g_fVoltage 0.0; // 电压值 (V) float g_fCurrent 0.0; // 电流值 (A) float g_fPower 0.0; // 瞬时功率 (W) float g_fEnergy 0.0; // 累计电量 (Wh) uint32_t g_u32LastTick 0; // 用于电量累计计时 // ADC采样值缓冲区 uint32_t adc_voltage_raw 0; uint32_t adc_current_raw 0; /* 在USER CODE BEGIN 4 区域添加函数 */ // 获取ADC平均值软件滤波 uint32_t Get_ADC_Average(ADC_HandleTypeDef* hadc, uint32_t ch, uint8_t times) { uint32_t temp_val 0; uint8_t t; for(t 0; t times; t) { HAL_ADC_Start(hadc); HAL_ADC_PollForConversion(hadc, 10); // 等待转换完成超时10ms temp_val HAL_ADC_GetValue(hadc); HAL_ADC_Stop(hadc); } return temp_val / times; } // 更新电参量计算 void Update_Power_Parameters(void) { // 1. 读取ADC原始值多次平均 adc_voltage_raw Get_ADC_Average(hadc1, ADC_CHANNEL_0, 10); // 假设电压在通道0 adc_current_raw Get_ADC_Average(hadc1, ADC_CHANNEL_1, 10); // 假设电流在通道1 // 2. 将ADC值转换为电压值STM32 ADC参考电压为3.3V12位分辨率 float adc_voltage (float)adc_voltage_raw * 3.3f / 4095.0f; float adc_current (float)adc_current_raw * 3.3f / 4095.0f; // 3. 根据硬件电路计算真实电压和电流 g_fVoltage adc_voltage * VOLTAGE_DIVIDER_RATIO; // 电压分压还原 g_fCurrent (adc_current - CURRENT_SENSOR_QUIESCENT_VOLTAGE) / CURRENT_SENSOR_SENSITIVITY; // 注意如果电流可能为负如ACS712双向测量这里需要判断。光伏板一般只输出正电流。 // 4. 计算瞬时功率 g_fPower g_fVoltage * g_fCurrent; if(g_fPower 0) g_fPower 0; // 功率不可能为负 // 5. 计算累计电量简易积分假设每秒调用一次此函数 uint32_t current_tick HAL_GetTick(); if(g_u32LastTick ! 0) { float delta_hours (current_tick - g_u32LastTick) / 3600000.0f; // 毫秒转小时 g_fEnergy g_fPower * delta_hours; // 能量 (Wh) 功率 (W) * 时间 (h) } g_u32LastTick current_tick; }4.2 OLED12864显示驱动我们需要一个OLED驱动库。通常使用ssd1306或oled的驱动代码。这里以常见的4针I2C接口OLED为例集成一个简化驱动。/* 文件 oled.c 和 oled.h */ /* oled.h */ #ifndef __OLED_H #define __OLED_H #include main.h #include i2c.h #define OLED_ADDRESS 0x78 // OLED的I2C地址通常为0x78或0x7A void OLED_Init(void); void OLED_Clear(void); void OLED_ShowString(uint8_t x, uint8_t y, char *str); void OLED_Refresh(void); #endif /* oled.c */ #include oled.h #include font.h // 需要一个字库文件包含ASCII字符点阵数据 // 内部显示缓冲区 static uint8_t OLED_GRAM[128][8]; // 128x64像素每8行一个字节 // 向OLED发送命令 void OLED_Write_Cmd(uint8_t cmd) { uint8_t buf[2] {0x00, cmd}; // 控制字节0x00表示命令 HAL_I2C_Master_Transmit(hi2c1, OLED_ADDRESS, buf, 2, 100); } // 向OLED发送数据 void OLED_Write_Data(uint8_t data) { uint8_t buf[2] {0x40, data}; // 控制字节0x40表示数据 HAL_I2C_Master_Transmit(hi2c1, OLED_ADDRESS, buf, 2, 100); } // 初始化OLED void OLED_Init(void) { HAL_Delay(100); OLED_Write_Cmd(0xAE); // 关闭显示 OLED_Write_Cmd(0xD5); // 设置时钟分频因子 OLED_Write_Cmd(0x80); OLED_Write_Cmd(0xA8); // 设置驱动路数 OLED_Write_Cmd(0x3F); OLED_Write_Cmd(0xD3); // 设置显示偏移 OLED_Write_Cmd(0x00); OLED_Write_Cmd(0x40); // 设置显示开始行 // ... 更多初始化命令参考OLED数据手册 OLED_Write_Cmd(0x8D); // 电荷泵设置 OLED_Write_Cmd(0x14); // 开启电荷泵 OLED_Write_Cmd(0xA1); // 段重映射设置 OLED_Write_Cmd(0xC8); // 行扫描方向 OLED_Write_Cmd(0xDA); // 设置COM硬件引脚配置 OLED_Write_Cmd(0x12); OLED_Write_Cmd(0x81); // 对比度设置 OLED_Write_Cmd(0xCF); OLED_Write_Cmd(0xD9); // 预充电周期 OLED_Write_Cmd(0xF1); OLED_Write_Cmd(0xDB); // VCOMH电压倍率 OLED_Write_Cmd(0x40); OLED_Write_Cmd(0xA4); // 全局显示开启 OLED_Write_Cmd(0xA6); // 设置正常显示 OLED_Write_Cmd(0xAF); // 开启显示 OLED_Clear(); OLED_Refresh(); } // 清空显示缓冲区 void OLED_Clear(void) { for(int i0; i128; i) { for(int j0; j8; j) { OLED_GRAM[i][j] 0x00; } } } // 在指定位置显示一个字符 void OLED_ShowChar(uint8_t x, uint8_t y, char chr) { if(chr || chr ~) return; // 只支持ASCII可见字符 uint8_t c chr - ; for(uint8_t i0; i6; i) { // 字符宽度6像素 uint8_t line F6x8[c][i]; // 从字库获取点阵数据 for(uint8_t j0; j8; j) { // 字符高度8像素 if(line 0x01) { OLED_DrawPoint(xi, yj); } else { OLED_ClearPoint(xi, yj); } line 1; } } } // 显示字符串 void OLED_ShowString(uint8_t x, uint8_t y, char *str) { uint8_t x0 x; while(*str) { OLED_ShowChar(x, y, *str); x 6; // 字符宽度间距 if(x 122) { // 换行判断 x x0; y 8; // 行高 } str; } } // 刷新整个显存到OLED void OLED_Refresh(void) { for(uint8_t page0; page8; page) { OLED_Write_Cmd(0xB0 page); // 设置页地址 OLED_Write_Cmd(0x00); // 设置列地址低4位 OLED_Write_Cmd(0x10); // 设置列地址高4位 for(uint8_t col0; col128; col) { OLED_Write_Data(OLED_GRAM[col][page]); } } }4.3 主程序逻辑整合在主循环中周期性地采样、计算、显示。/* 文件 main.c 的 main函数部分 */ int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ADC1_Init(); MX_I2C1_Init(); MX_USART1_UART_Init(); OLED_Init(); OLED_Clear(); OLED_ShowString(0, 0, PV Monitor V1.0); OLED_Refresh(); HAL_Delay(1000); OLED_Clear(); char display_buffer[64]; // 显示缓冲区 while (1) { // 1. 更新电参量 Update_Power_Parameters(); // 2. 格式化显示字符串 OLED_Clear(); sprintf(display_buffer, V: %.2f V, g_fVoltage); OLED_ShowString(0, 0, display_buffer); sprintf(display_buffer, I: %.3f A, g_fCurrent); OLED_ShowString(0, 16, display_buffer); // 第二行 sprintf(display_buffer, P: %.2f W, g_fPower); OLED_ShowString(0, 32, display_buffer); // 第三行 sprintf(display_buffer, E: %.1f Wh, g_fEnergy); OLED_ShowString(0, 48, display_buffer); // 第四行 // 3. 刷新OLED显示 OLED_Refresh(); // 4. 通过串口打印数据调试用 printf(Voltage: %.2f V, Current: %.3f A, Power: %.2f W, Energy: %.1f Wh\r\n, g_fVoltage, g_fCurrent, g_fPower, g_fEnergy); // 5. 延时控制采样和显示频率例如1秒一次 HAL_Delay(1000); } } // 重定向printf到串口 #ifdef __GNUC__ #define PUTCHAR_PROTOTYPE int __io_putchar(int ch) #else #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f) #endif PUTCHAR_PROTOTYPE { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; }5. Proteus仿真验证在硬件制作前用Proteus仿真可以验证逻辑。绘制原理图从元件库放置STM32F103C6、RES电阻、POT-HG滑动变阻器模拟电压变化、DC VOLTMETER直流电压表用于监测、I2C DEBUGGERI2C调试器模拟OLED等。连接电路将两个滑动变阻器的中间抽头分别连接到STM32的PA0和PA1模拟ADC输入电压。将PA9连接到VIRTUAL TERMINAL的RXD用于查看串口打印。为STM32提供3.3V电源和GND。加载程序在STM32元件属性中指定编译生成的.hex文件路径。运行仿真点击运行调节滑动变阻器改变模拟的电压和电流值。观察VIRTUAL TERMINAL中打印的电压、电流、功率值是否随滑动变阻器变化而正确变化。注Proteus中可能没有完美的OLED模型常用I2C DEBUGGER或Virtual Terminal来观察I2C发送的数据或者用SPI/I2C DEBUGGER工具解析数据。6. 常见问题与调试技巧6.1 ADC采样值不稳定或不准现象数值跳动大或者与万用表测量值偏差大。原因与解决电源噪声确保模拟部分电源稳定尽量使用LDO单独供电并增加滤波电容如10uF电解电容并联0.1uF陶瓷电容。参考电压不稳STM32的VDDA模拟电源和VSSA模拟地必须接稳定干净的3.3V和地最好与数字电源通过磁珠或0Ω电阻隔离。采样时间不足ADC对高阻抗信号源采样需要足够的时间。在CubeMX中增加Sampling Time如设为239.5 Cycles。软件滤波采用多次采样取平均、中值滤波或一阶低通滤波算法。上文代码中的Get_ADC_Average就是简单平均滤波。校准由于分压电阻精度和ADC本身存在偏移/增益误差需要进行软件校准。方法测量两个已知精确电压点如0V和满量程附近一个电压记录ADC读数计算出实际电压与ADC读数的线性关系ykxb用这个公式修正所有读数。6.2 OLED不显示或显示乱码现象屏幕不亮、全亮、显示杂乱方块。原因与解决电源与接线首先检查VCC和GND是否接对I2C的SDA和SCL是否接反。用示波器或逻辑分析仪查看I2C总线是否有波形。I2C地址错误常见OLED地址是0x78写地址或0x7A。尝试修改OLED_ADDRESS宏定义。有些模块需要将地址左移一位即0x3C驱动里发送时再左移一位。最稳妥的方法是查看模块手册或用I2C扫描程序。初始化序列错误不同厂商的OLED初始化命令可能有细微差别。确保初始化命令序列完整且正确特别是开启电荷泵0x8D, 0x14的命令。时序问题在OLED_Write_Cmd和OLED_Write_Data函数中HAL_I2C_Master_Transmit的最后一个参数是超时时间如果太短可能导致发送失败可以适当增大如1000。字库问题显示乱码通常是字库数据不对或取模方式不匹配。确保font.h中的字库数组与OLED_ShowChar函数中的取模逻辑一致如取模顺序是逐列还是逐行高位在前还是低位在前。6.3 电流测量值为零或负值现象有光照时电流读数为0或很小的负数。原因与解决ACS712零点漂移ACS712零电流时输出理论上是VCC/22.5V但可能存在偏差。在无电流时测量其OUT引脚实际电压并更新代码中的CURRENT_SENSOR_QUIESCENT_VOLTAGE常量。接线错误电流传感器需要串联在回路中且方向要正确电流从IP流入IP-流出。调换IP和IP-试试。量程过大如果光伏板电流很小如小于0.1A而使用了量程大的传感器如20A其输出电压变化极小可能被噪声淹没。应选择量程匹配的传感器如5A。ADC参考电压确认STM32的ADC参考电压是稳定的3.3V。如果VDDA低于3.3V会导致所有ADC读数比例错误。6.4 程序无法下载或调试现象Keil提示“No ULINK/ST-Link found”或“Flash Download failed”。解决驱动安装确保ST-Link/V2的USB驱动已正确安装。接线检查SWDIO、SWCLK、GND、3.3V四根线是否连接牢固。STM32的NRST引脚最好也接到调试器的RESET。Keil配置在Options for Target-Debug中选择正确的调试器ST-Link。在Settings中确认Port是SWMax Clock可以调低一点如1MHz。Boot引脚确认BOOT0已接地低电平。芯片锁死如果之前程序异常导致芯片锁死可以尝试在下载时先按住复位键点击下载后再松开或者通过BOOT01进入系统存储器进行擦除。7. 优化与扩展建议7.1 软件优化更精确的电量累计上文使用HAL_GetTick()进行简易积分误差较大。更好的方法是使用一个硬件定时器如TIM2产生固定的中断如1秒一次在中断服务程序中进行电量累加这样时间基准更准。数字滤波算法除了平均滤波可以引入一阶低通滤波IIR来平滑数据减少显示数值的跳动。#define ALPHA 0.1f // 滤波系数越小越平滑响应越慢 float filtered_value 0; filtered_value ALPHA * new_adc_value (1 - ALPHA) * filtered_value;数据存储加入EEPROM如AT24C02或STM32内部的Flash用于存储累计电量防止断电丢失。报警功能设置电压、电流的上下限阈值当超过阈值时通过LED或蜂鸣器报警。7.2 硬件优化信号调理在ADC输入前增加电压跟随器运放以提高输入阻抗减少对采样电路的影响。增加RC低通滤波电路滤除高频噪声。多路检测利用STM32F103C8T6的多个ADC通道可以同时检测多块光伏板。通信扩展增加ESP8266 Wi-Fi模块或NB-IoT模块将数据上传到云平台实现远程监控。MPPT功能这是一个高级扩展。通过一个DC-DC电路如Buck电路并实时调整其占空比使光伏板始终工作在最大功率点MPPT需要更复杂的算法如扰动观察法、电导增量法。7.3 工程实践建议模块化编程将ADC驱动、OLED驱动、计算逻辑、数据存储等分成独立的.c/.h文件提高代码可读性和可维护性。版本管理使用Git对项目代码进行版本管理。注释与文档关键函数、重要参数和硬件连接一定要写清楚注释。绘制最终的电路原理图并存档。测试分模块测试。先测试ADC采样用可调电源输入固定电压再测试电流传感器最后整合测试。使用万用表、示波器进行对比验证。从选型到仿真再到代码实现和调试完成一个完整的嵌入式系统项目需要耐心和细致的排查。STM32F103C8T6丰富的资源和活跃的社区让光伏发电量检测这类应用开发变得可行。本设计提供了一个坚实的起点你可以在此基础上增加数据记录、无线通信甚至简单的MPPT算法。动手过程中遇到的坑往往是最好的学习材料。