简介:本资源是一套面向嵌入式开发初学者与物联网实践者的STM32F10x咖啡机控制系统完整源码工程,聚焦于微控制器在智能家电中的典型应用,解决硬件驱动、多传感器协同与上下位机通信等核心问题。压缩包共130个文件,涵盖17个C源文件(含stm32f10x_spi.c、spi.c等关键驱动)、18个头文件(h)、33个汇编启动与底层支持文件(s),以及17个Python脚本(用于Raspberry Pi上位机逻辑与SocketIO实时反馈)、5个HTML/JS前端页面(状态可视化界面)和配套配置文件(cfg/sh/ld等),整体体积3.2MB,结构清晰,模块划分明确。已有73人学习下载,适合通过真实项目掌握STM32外设编程(SPI/DMA/ADC/TIM)、传感器数据融合(温度/重量)、嵌入式与Linux平台通信(SPI+SocketIO)及自动化流程控制(烧水→闷蒸→冲泡)的全流程开发能力。
1. 项目概述与核心价值
最近在整理旧硬盘时,翻到了一个多年前做的项目——“基于STM32F10x微控制器的咖啡机控制系统”的源码包。这个项目源于当时一个朋友开小型咖啡馆的需求,他希望有一台能自定义冲泡参数、稳定可靠且成本可控的商用咖啡机。市面上成熟的方案要么是封闭系统,要么价格高昂,于是我们决定用当时正火的STM32F103系列芯片自己动手搞一套。这个压缩包里,不仅包含了完整的工程源码,还藏着从电路设计到PID温控调参的一整套实战经验。对于正在学习嵌入式开发,尤其是想从点灯进阶到综合项目实战的朋友来说,这套代码和背后的设计思路,或许能给你提供一个非常具体的参考样板。它不仅仅是一个“咖啡机”程序,更是一个涵盖了定时器、PWM、ADC采样、PID算法、状态机以及人机交互的典型嵌入式系统微缩模型。
2. 系统整体架构与设计思路拆解
2.1 核心需求与方案选型
当时的需求很明确:要能精确控制冲泡水温(目标92±1℃)、可设定预浸泡和萃取时间、具备锅炉水位检测与防干烧保护、有一个简单直观的操作界面,并且系统要能7x24小时稳定运行。基于这些需求,我们选择了意法半导体的STM32F103C8T6作为主控芯片,也就是大家常说的“蓝色小药丸”或“最小系统板”的那颗芯。选它的理由很直接:性价比极高,拥有72MHz的Cortex-M3内核,性能足够;外设丰富,自带多路ADC、高级定时器(支持互补PWM输出,正好驱动加热管继电器)、多个通用定时器以及USART、I2C等通信接口,完全满足需求;而且生态完善,标准库和HAL库资料都很多,开发调试方便。
整个系统的架构是典型的“传感器-控制器-执行器”模式。传感器层包括DS18B20数字温度传感器(测量锅炉水温)、水位检测电极(简单的通断检测)、流量计脉冲信号。控制器层就是STM32,它负责采集所有传感器数据,运行控制算法。执行器层则包括继电器(控制加热管和抽水泵)、电磁阀(控制水路开关)以及一个128x64的OLED显示屏和几个按键作为人机界面。方案确定后,硬件上我们画了一块简单的底板,将STM32最小系统板和各个外设模块连接起来;软件上则采用基于时间片轮询的前后台系统架构,没有上RTOS,以保持简洁和可控性。
2.2 硬件电路设计关键点
硬件设计上,有几个坑是后来调试时才深刻体会到的。首先是加热管和泵的驱动电路。加热管功率通常在1000W以上,水泵也是感性负载,直接用STM32的GPIO口驱动继电器是绝对不行的。我们采用了经典的三极管(如S8050)驱动继电器线圈的方案,并在继电器线圈两端并联了续流二极管(1N4148),以吸收断开时产生的反向电动势,保护驱动三极管。STM32的GPIO口通过一个1kΩ的限流电阻连接到三极管的基极。
注意:驱动大功率负载时,务必做好电源隔离。我们的设计是,控制部分(STM32、传感器、OLED)使用一套5V/1A的开关电源模块供电,而继电器、加热管、水泵则直接由220V交流电通过继电器触点控制。两套电源的“地”在PCB上单点连接,避免了大的干扰电流窜入控制回路。
其次是温度传感器的布置。DS18B20的探头最初直接拧在锅炉金属外壳上,结果发现测温滞后严重,水温都变化好几度了,传感器读数才慢慢跟上。后来改成了将探头通过导热硅脂嵌入一个紫铜套管内,再将套管浸入锅炉水中,响应速度大大提升。水位检测用的是最简单的电极式,利用水的导电性,但电极长期在高温水环境中容易结垢导致误判,所以软件上增加了周期性短时通电清洁和多次采样表决的逻辑。
3. 核心模块软件实现详解
3.1 温度PID控制算法的实现与整定
咖啡的口感很大程度上取决于水温的稳定性,因此温度控制是核心。我们采用了增量式数字PID算法。在pid.c和pid.h文件中,你可以找到完整的实现。
// pid.h 中定义的结构体 typedef struct { float Target; // 目标值 float Kp, Ki, Kd; // PID参数 float Error; // 当前误差 float LastError; // 上次误差 float PrevError; // 上上次误差 float Integral; // 积分项 float Output; // 输出值 float OutMax; // 输出上限 float OutMin; // 输出下限 } PID_TypeDef; // pid.c 中的增量式PID计算函数 float PID_Calculate(PID_TypeDef *pid, float current) { pid->Error = pid->Target - current; // 积分分离:误差较大时,取消积分作用,防止积分饱和 if(fabs(pid->Error) > 20.0) { pid->Integral = 0; } else { pid->Integral += pid->Error; // 积分限幅 if(pid->Integral > 100) pid->Integral = 100; if(pid->Integral < -100) pid->Integral = -100; } float increment = pid->Kp * (pid->Error - pid->LastError) + pid->Ki * pid->Integral + pid->Kd * (pid->Error - 2*pid->LastError + pid->PrevError); pid->PrevError = pid->LastError; pid->LastError = pid->Error; pid->Output += increment; // 输出限幅 if(pid->Output > pid->OutMax) pid->Output = pid->OutMax; if(pid->Output < pid->OutMin) pid->Output = pid->OutMin; return pid->Output; }这个PID控制器被一个1秒定时器中断服务程序调用。PID的输出值是一个0-100的百分比,通过PWM占空比的形式控制加热继电器的通断周期(即周期为10秒的PWM,输出30%就意味着加热3秒,停止7秒)。参数整定是个耐心活,我们当时用的是经典的试凑法:先设Ki=0, Kd=0,逐渐增大Kp直到系统出现等幅振荡,此时记下Kp为Ku,振荡周期为Tu。然后根据Ziegler-Nichols经验公式,取Kp = 0.6*Ku,Ki = 2*Kp/Tu,Kd = Kp*Tu/8作为初值,再微调。实测下来,对于我们的锅炉系统,一组比较稳定的参数是Kp=25.0, Ki=0.5, Kd=80.0。
实操心得:在嵌入式系统里做温度控制,采样周期和计算周期非常关键。我们的DS18B20每次转换需要约750ms,所以PID计算周期定为1秒是合适的。太快了传感器数据没更新,计算无意义;太慢了系统响应迟钝。另外,加热管有热惯性,PWM周期不能太短,否则继电器会频繁动作缩短寿命,我们选择10秒作为一个完整的PWM周期。
3.2 基于状态机的冲泡流程管理
咖啡冲泡不是一个简单的顺序执行过程,它包含等待、预浸泡、正式萃取、停止等多个状态,并且需要根据用户设定或传感器反馈(如流量计到达设定值)进行跳转。用一堆if-else或者switch-case硬编码会非常混乱且难以维护。因此,我们实现了一个简单的状态机(Finite State Machine, FSM)。
在coffee_fsm.c中,我们定义了所有的系统状态枚举:
typedef enum { STATE_IDLE, // 待机 STATE_HEATING, // 加热中 STATE_READY, // 就绪(水温达标) STATE_PREINFUSION, // 预浸泡 STATE_BREWING, // 正式萃取 STATE_COMPLETE, // 完成 STATE_ERROR // 错误(如缺水) } BrewState_t;每个状态都是一个独立的函数,负责处理该状态下的具体任务并判断是否满足状态转移条件。主循环中只需调用当前状态对应的处理函数即可。
void BrewStateMachine_Run(void) { switch(currentState) { case STATE_IDLE: State_Idle_Handler(); break; case STATE_HEATING: State_Heating_Handler(); break; // ... 其他状态 default: currentState = STATE_IDLE; break; } } static void State_Heating_Handler(void) { // 1. 执行加热PID控制 PID_Calculate(&heatingPID, currentTemperature); // 2. 检查是否到达目标温度 if(fabs(currentTemperature - targetTemperature) < 1.0) { currentState = STATE_READY; OLED_ShowString(0, 2, "Ready! Press Start"); } // 3. 检查是否发生错误(如超时) if(heatingTimer > MAX_HEATING_TIME) { currentState = STATE_ERROR; errorCode = ERROR_HEAT_TIMEOUT; } }这种设计使得程序逻辑非常清晰,添加新的状态或修改某个状态的行为变得很容易,大大增强了代码的可读性和可维护性。
3.3 人机交互(OLED与按键)实现
我们选用了一款0.96寸的I2C接口OLED显示屏(SSD1306驱动)来显示状态、温度和设定参数。为了不阻塞主循环,显示刷新被放在一个低优先级的后台任务中,通过一个flag标志位来触发。例如,当温度更新后,设置tempUpdateFlag = 1,后台任务检测到该标志位,则调用OLED_ShowTemperature()函数更新屏幕特定区域,然后清除标志位。
按键处理采用了经典的“扫描+消抖+状态识别”方法。在key.c中,我们为每个按键定义了PRESS、LONG_PRESS、RELEASE等状态。主循环每10ms扫描一次按键引脚,通过状态机识别出有效的按键事件,并将其放入一个事件队列。其他模块(如状态机、菜单系统)只需从队列中读取事件进行处理,实现了输入与处理的解耦。
// 按键事件结构体 typedef struct { Key_ID_t id; // 哪个按键 Key_Event_t event; // 什么事件 } Key_Event_t; // 在定时器中断或主循环中调用 void Key_Scan_Task(void) { static uint16_t keyCount[KEY_NUM] = {0}; for(int i=0; i<KEY_NUM; i++) { if(ReadKeyPin(i) == PRESSED) { keyCount[i]++; if(keyCount[i] == 1) { // 首次按下,可标记为短按预备 } else if(keyCount[i] > 100) { // 持续1秒(10ms*100) // 触发长按事件 PutKeyEvent(i, EVT_LONG_PRESS); } } else { if(keyCount[i] > 0 && keyCount[i] < 100) { // 释放且未达到长按时间,触发短按事件 PutKeyEvent(i, EVT_SHORT_PRESS); } keyCount[i] = 0; } } }4. 工程源码结构剖析与关键文件解读
解压(源码)基于STM32F10x微控制器的咖啡机控制系统.zip后,你会看到一个标准的STM32标准外设库工程目录结构。这里挑几个核心文件说说。
Coffee_Machine_Controller/ ├── CMSIS/ // Cortex-M3核心支持文件 ├── FWlib/ // STM32F10x标准外设库 ├── User/ │ ├── main.c // 主函数,硬件初始化,主循环 │ ├── stm32f10x_it.c // 中断服务程序 │ ├── system_stm32f10x.c // 系统时钟配置 │ ├── coffee_fsm.c/.h // 咖啡冲泡状态机 │ ├── pid.c/.h // PID控制算法 │ ├── temperature.c/.h // DS18B20驱动与温度处理 │ ├── water_flow.c/.h // 流量计脉冲计数(使用外部中断) │ ├── key.c/.h // 按键扫描与事件处理 │ ├── oled_ssd1306.c/.h // OLED显示驱动 │ ├── relay.c/.h // 继电器控制 │ ├── menu.c/.h // 简易菜单系统 │ └── ... // 其他外设驱动 ├── README.txt // 工程简要说明 └── Project.uvprojx // Keil MDK工程文件main.c:程序的入口。这里完成了所有硬件的初始化(时钟、GPIO、定时器、ADC、中断等),然后进入一个无限的while(1)主循环。主循环的结构非常清晰,就是一个简单的时间片轮询调度器:
int main(void) { System_Init(); // 系统初始化 Hardware_Init(); // 外设初始化 FSM_Init(); // 状态机初始化 while(1) { // 1. 按键扫描(10ms周期) if(tick_10ms) { tick_10ms = 0; Key_Scan_Task(); } // 2. 状态机运行(50ms周期) if(tick_50ms) { tick_50ms = 0; BrewStateMachine_Run(); } // 3. 显示刷新(200ms周期,避免闪烁) if(tick_200ms) { tick_200ms = 0; OLED_Refresh_Task(); } // 4. 其他低优先级任务... // 系统空闲时,可以进入低功耗模式(本项目未使用) // __WFI(); } }所有的时间标志位(tick_10ms,tick_50ms等)都在一个1ms的SysTick定时器中断里进行累加和判断。这种架构保证了关键任务的周期性执行,又避免了在某个任务中长时间阻塞。
stm32f10x_it.c:这是中断服务程序的集中地。你需要重点关注以下几个中断:
- SysTick_Handler:系统心跳,产生各种时间片标志。
- TIMx_IRQHandler(例如TIM2):可能用于产生PID计算周期或PWM波形。
- EXTIx_IRQHandler:外部中断,用于捕获流量计的脉冲信号。每产生一个脉冲,意味着固定体积的水流过,计数器加一,从而可以计算总出水量。
temperature.c:这里包含了DS18B20的底层读写时序驱动。DS18B20使用单总线协议,对时序要求非常严格。代码里使用了__nop()空指令进行微秒级延时,并禁用了中断来保证时序的精确性。读取的温度值经过简单的滤波(比如一阶滞后滤波)后,才提供给PID控制器使用。
5. 开发环境搭建与项目编译烧录指南
这个工程是基于Keil MDK(Version 5)开发的,使用了STM32的标准外设库(Standard Peripheral Library)。如果你用的是其他IDE(如IAR、STM32CubeIDE),需要进行项目迁移。
第一步:准备开发环境
- 安装Keil MDK-ARM(建议V5.30以上版本)并破解。
- 安装STM32F1系列的Device Family Pack(DFP)。
- 工程本身已包含标准外设库,无需额外安装。
第二步:打开与配置工程
- 直接双击
Project.uvprojx文件打开工程。 - 检查目标设备是否正确(STM32F103C8T6)。
- 在
Options for Target->C/C++->Define中,确保定义了USE_STDPERIPH_DRIVER和STM32F10X_MD(对于中等容量芯片)。 - 在
Debug选项卡配置你的调试器(如ST-Link、J-Link)。
第三步:编译与下载
- 点击
Rebuild按钮编译整个工程。确保0错误,0警告(个别未使用的变量警告可忽略)。 - 连接好ST-Link调试器和板子,点击
Load按钮下载程序到Flash。 - 复位或重新上电,系统开始运行。
注意事项:如果你用的是HAL库或LL库,标准外设库的寄存器操作方式会完全不同。标准库的函数像
GPIO_SetBits(GPIOA, GPIO_Pin_0),在HAL库里是HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET)。如果你打算移植到HAL库,需要重写底层驱动部分,但上层的业务逻辑(如状态机、PID算法)可以基本复用。
6. 系统调试与性能优化实战记录
6.1 温度控制波动大的排查与解决
系统第一次上电运行时,水温波动范围超过了±5℃,完全达不到要求。排查过程如下:
- 检查传感器数据:通过串口打印DS18B20的原始读数,发现数值本身是稳定的,排除了传感器噪声问题。
- 检查PWM输出:用示波器查看控制加热继电器的PWM波形,发现占空比变化剧烈,说明PID输出不稳定。
- 检查PID参数:最初参数设置过于激进(
Kp太大),导致系统超调严重,形成振荡。通过前文提到的Z-N法重新整定参数。 - 检查执行器延迟:发现继电器从线圈得电到触点完全闭合有约10ms的机械延迟,而加热管本身的热惯性更大。这相当于在控制回路中引入了一个滞后环节。解决方案是降低PWM频率,我们将PWM周期从1秒延长到了10秒,让加热管有足够的时间响应变化,系统立刻稳定下来。
- 加入输出限幅与积分抗饱和:为了防止在刚开始加热(温差大)时积分项累积过快导致“积分饱和”,我们在PID计算函数中加入了积分分离逻辑(见3.1节代码)。当误差大于某个阈值时,取消积分作用,仅用PD控制快速接近目标;当误差进入小范围时,再引入积分作用以消除静差。
6.2 流量计量不准的问题
我们用的霍尔流量计,输出脉冲频率与流速成正比。最初将脉冲信号接到STM32的普通GPIO口,用外部中断计数。但在水泵启动的瞬间,水锤效应会导致叶轮高速抖动,产生一连串的误脉冲。解决方法有两个,我们同时采用了:
- 硬件滤波:在流量计信号线和地之间加一个0.1uF的电容,滤除高频毛刺。
- 软件滤波:在外部中断服务函数中,不直接计数,而是记录下中断发生的时间戳(通过读取一个微秒级定时器的值)。在主循环中判断,如果两次脉冲的时间间隔小于某个阈值(例如20ms),则认为此次脉冲是抖动,予以忽略。
// 改进后的流量计处理伪代码 volatile uint32_t lastPulseTime = 0; void EXTI0_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line0) != RESET) { uint32_t now = Get_Microsecond_Timer(); if((now - lastPulseTime) > 20000) { // 间隔大于20ms才认为是有效脉冲 waterPulseCount++; } lastPulseTime = now; EXTI_ClearITPendingBit(EXTI_Line0); } }6.3 系统长期运行的稳定性保障
为了让咖啡机能像商用设备一样长时间稳定工作,我们在软件层面做了以下加固:
- 看门狗:开启了STM32内部的独立看门狗(IWDG),设置约1秒的溢出时间。在主循环的合适位置喂狗。一旦程序跑飞,系统会自动复位。
- 参数存储:用户设定的温度、时间等参数需要掉电保存。我们使用了STM32内部的Flash(EEPROM模拟)来存储。为了防止频繁擦写损坏Flash,只在参数确实改变并确认后,才执行保存操作,并且同一个参数页有磨损均衡的简单设计。
- 异常状态监测与恢复:状态机中有一个
STATE_ERROR状态。当检测到锅炉缺水、加热超时、通信异常等情况时,系统会跳转到错误状态,停止所有执行器,在OLED上显示错误代码,并鸣响蜂鸣器。只有排除故障并手动复位后,系统才能恢复。
7. 项目扩展与进阶思考
这个基础版本实现后,我们根据朋友的反馈,还规划了几个扩展方向,这些思路或许能给你更多启发:
- 联网与智能化:增加一个ESP8266 Wi-Fi模块,通过AT指令与STM32串口通信。这样可以通过手机APP远程开机、设定个性化冲泡方案(如“我的拿铁”:92℃、预浸5秒、萃取30秒)。甚至可以将冲泡数据上传到云端,进行简单的数据分析。
- 多锅炉与多冲煮头控制:商用咖啡机往往有多个冲煮头,可以同时制作不同种类的咖啡。这需要STM32管理更多路传感器和执行器,状态机也需要升级为支持多实例并行运行。可以考虑引入一个轻量级的RTOS(如FreeRTOS),为每个冲煮头创建一个独立的任务。
- 压力控制:高端咖啡机强调“9 bar压力萃取”。这需要在冲煮头附近增加压力传感器,并引入一个控制泵转速或电磁阀开度的压力PID环,形成“温度-压力”串级控制,复杂度会提升一个等级。
- 图形化界面:将OLED换成一块小的TFT液晶屏,可以显示更丰富的图形、动画和菜单,用户体验会好很多。但这需要更多的RAM和Flash空间,可能要考虑升级到STM32F103RET6这类大容量型号。
回过头看,这个项目麻雀虽小五脏俱全。它强迫你去综合考虑硬件选型、电路设计、传感器特性、控制算法、软件架构以及用户体验。调试过程中,示波器、逻辑分析仪、串口调试助手是你最忠实的伙伴。当你看到PID参数调好后,水温曲线平稳地维持在一条直线附近时,那种成就感是单纯点个灯无法比拟的。希望这份源码和这些絮絮叨叨的经验,能帮你少走些弯路,更快地享受到嵌入式系统开发的乐趣。
本文还有配套的精品资源,点击获取