简介:本资源是一套面向嵌入式开发初学者与进阶工程师的STM32F10x实战项目源码,聚焦咖啡机自动化控制这一典型工业场景,解决微控制器驱动多传感器协同、实时通信与流程闭环控制等核心问题。压缩包共130个文件,含17个C源文件(如stm32f10x_spi.c、spi.c等底层驱动)、18个头文件(h)、33个汇编文件(s)及17个Python脚本(用于上位机交互与测试),辅以HTML/JS前端页面和Makefile构建配置,整体3.2MB,结构清晰,便于分模块学习与调试。已有73人下载学习,资源完整覆盖硬件抽象层、SPI通信协议栈、温度/重量数据采集逻辑及SocketIO状态反馈机制,并内置STMSPI单元测试代码,可直接验证关键模块可靠性。读者可获得从STM32固件开发、Raspberry Pi上位机联调到Web端实时监控的全链路工程实践能力。
1. 项目概述:从零到一,打造一台智能咖啡机
最近在整理过去的项目资料,翻到了一个基于STM32F10x的咖啡机控制系统源码。这个项目虽然不算复杂,但麻雀虽小五脏俱全,涵盖了嵌入式开发中从硬件选型、外设驱动到业务逻辑整合的完整流程。对于想从点灯、按键入门,进阶到完成一个综合性实物项目的朋友来说,是个非常不错的练手选择。它解决的问题很具体:如何用一颗成本可控的微控制器,精准地控制加热、水泵、磨豆等模块,实现从咖啡豆到一杯意式浓缩或美式咖啡的自动化流程。无论你是电子爱好者、自动化专业的学生,还是刚接触STM32的嵌入式工程师,这个项目都能帮你把分散的知识点串联起来,形成一套完整的开发思维。
2. 核心需求与系统设计解析
2.1 咖啡机工作流程与核心需求拆解
要设计控制系统,首先得搞清楚一台半自动/全自动咖啡机是怎么工作的。抛开品牌和外观,其核心流程可以抽象为几个关键阶段:
- 就绪与参数设置:用户通过按键或旋钮选择咖啡类型(如浓缩、美式)、杯量、浓度,系统初始化并等待启动指令。
- 研磨与定量:收到启动信号后,控制磨豆电机工作特定时长,将定量的咖啡粉送入冲泡头。
- 加热与温度稳定:同时或提前,加热器开始工作,将锅炉或即热模块的水加热至设定温度(通常意式咖啡需要92-96°C)。
- 冲泡与压力控制:水泵启动,将热水以一定压力(意式通常需要9-15Bar)通过咖啡粉饼,进行萃取。
- 完成与清洁提示:达到设定的萃取时间或杯量后,停止水泵和加热,提示用户取杯,并可能进入自清洁或待机模式。
基于这个流程,我们对STM32F10x控制器提出了明确的需求:
- 多路PWM输出:用于控制磨豆电机(调速)、水泵电机(调压/流量)以及可能用于加热器的功率调节(如使用PID控制)。
- 高精度定时器:精准控制研磨时长、冲泡时长,以及作为PWM生成的时基。
- 模数转换器(ADC):采集温度传感器(如NTC热敏电阻或DS18B20)、水位传感器、压力传感器的模拟信号。
- 多路GPIO控制:用于驱动继电器控制加热器、水泵启停,读取按键、开关状态,控制指示灯和显示屏。
- 通信接口:可能需要UART连接蓝牙/Wi-Fi模块实现手机控制,或用于调试信息输出。
2.2 硬件系统架构设计
根据需求,我们围绕STM32F103C8T6(一款性价比极高的主流型号)搭建了最小系统及外围电路。
主控核心:STM32F103C8T6。它拥有72MHz主频、64KB Flash、20KB RAM,足够应对此项目逻辑。具备多达3个通用定时器(可用于PWM)、2个ADC、多个USART和GPIO,资源富余。
电源与驱动模块:
- 主电源:12V/5A直流输入,为加热器和水泵供电。
- 控制电源:通过DC-DC降压模块(如LM2596)得到稳定的5V和3.3V,为MCU、传感器和逻辑电路供电。
- 功率驱动:这是关键安全环节。加热器(通常几百瓦)和水泵电机必须通过继电器或固态继电器(SSR)来控制。MCU的GPIO口通过一个三极管(如S8050)或光耦驱动继电器线圈,实现强弱电隔离。磨豆电机若为直流有刷电机,可使用MOS管(如IRF540)配合PWM进行调速控制。
传感器模块:
- 温度传感:方案一是采用NTC热敏电阻配合精密电阻分压,利用STM32的ADC读取。成本低,但需要复杂的温度-电阻换算和校准。方案二是使用数字温度传感器DS18B20,单总线通信,精度高、编程简单,是更稳妥的选择。
- 水位检测:通常使用浮子开关或光电式水位传感器,输出开关量信号给MCU的GPIO。
- 压力传感:如需监测冲泡压力,可选用薄膜压力传感器,输出模拟信号由ADC读取。
人机交互模块:
- 输入:采用矩阵键盘或独立按键,用于模式选择、参数调整、启动/停止。
- 输出:使用一块0.96寸OLED显示屏(SSD1306驱动,I2C接口)来显示状态、温度、倒计时等信息。配合几个LED作为状态指示灯。
注意:在原理图设计时,务必为每个继电器线圈并联一个续流二极管(如1N4148),以吸收继电器断开时产生的反向电动势,保护驱动三极管。这是硬件设计中的一个经典避坑点。
3. 软件开发环境与工程架构搭建
3.1 开发工具链选择
对于STM32F10x系列,常见的开发环境有Keil MDK、IAR Embedded Workbench和基于GCC的STM32CubeIDE。在这个项目中,我选择了Keil MDK(ARMCC编译器)配合STM32标准外设库(StdPeriph_Lib V3.5.0)。原因如下:首先,标准外设库的寄存器级操作对于理解STM32硬件工作原理非常有帮助,代码透明度高。其次,相关的教程和社区资源极其丰富,遇到问题容易找到解决方案。虽然HAL/LL库更现代,但对于学习底层和在一个资源明确的项目上,标准库足够且直观。
工程目录结构组织清晰是后期维护的关键:
Coffee_Machine_Controller/ ├── CMSIS/ # Cortex微控制器软件接口标准文件,由ST提供 ├── STM32F10x_StdPeriph_Driver/ # STM32标准外设库源码 ├── User/ │ ├── main.c # 主程序入口 │ ├── stm32f10x_it.c # 中断服务程序 │ ├── system_stm32f10x.c # 系统时钟初始化 │ ├── bsp/ # 板级支持包 │ │ ├── bsp_gpio.c/.h │ │ ├── bsp_timer.c/.h │ │ ├── bsp_pwm.c/.h │ │ ├── bsp_adc.c/.h │ │ ├── bsp_uart.c/.h │ │ └── bsp_i2c_oled.c/.h # OLED驱动 │ ├── app/ # 应用层逻辑 │ │ ├── app_coffee_fsm.c/.h # 咖啡机状态机 │ │ ├── app_pid.c/.h # 温度PID控制算法 │ │ ├── app_user_interface.c/.h # 用户界面处理 │ │ └── app_scheduler.c/.h # 简单任务调度器 │ └── config/ # 配置文件 │ └── stm32f10x_conf.h # 外设库头文件包含配置 └── MDK-ARM/ # Keil工程文件3.2 核心外设驱动实现要点
1. GPIO与继电器驱动: 驱动继电器本质上就是控制GPIO输出高低电平。关键在于初始化时正确配置推挽输出模式,并考虑上电初始状态,确保继电器处于安全断开状态。
// bsp_gpio.c 片段 void BSP_Relay_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RELAY_HEATER_GPIO_CLK, ENABLE); GPIO_InitStructure.GPIO_Pin = RELAY_HEATER_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(RELAY_HEATER_PORT, &GPIO_InitStructure); GPIO_ResetBits(RELAY_HEATER_PORT, RELAY_HEATER_PIN); // 初始化为低电平,继电器断开 }2. PWM输出控制电机与加热: 使用通用定时器(如TIM2或TIM3)的PWM模式。以控制磨豆电机速度为例。
// bsp_pwm.c 片段 void BSP_Grinder_PWM_Init(uint16_t arr, uint16_t psc) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; GPIO_InitTypeDef GPIO_InitStructure; // 开启时钟和GPIO RCC_APB1PeriphClockCmd(GRINDER_TIM_CLK, ENABLE); RCC_APB2PeriphClockCmd(GRINDER_GPIO_CLK, ENABLE); // 配置GPIO为复用推挽输出 GPIO_InitStructure.GPIO_Pin = GRINDER_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GRINDER_PORT, &GPIO_InitStructure); // 时基单元配置 TIM_TimeBaseStructure.TIM_Period = arr; // 自动重装载值,决定PWM频率 TIM_TimeBaseStructure.TIM_Prescaler = psc; // 预分频系数 TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(GRINDER_TIM, &TIM_TimeBaseStructure); // PWM模式配置 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比为0,电机不转 TIM_OC1Init(GRINDER_TIM, &TIM_OCInitStructure); TIM_OC1PreloadConfig(GRINDER_TIM, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(GRINDER_TIM, ENABLE); TIM_Cmd(GRINDER_TIM, ENABLE); } // 设置占空比函数 void BSP_Set_Grinder_Speed(uint16_t compare) { TIM_SetCompare1(GRINDER_TIM, compare); }参数计算:若定时器时钟为72MHz,预分频psc=71,则计数器时钟为1MHz。设置arr=999,则PWM频率为 1MHz / (999+1) = 1kHz。compare值范围0-999,对应0%-100%占空比。
3. ADC采集温度: 以NTC热敏电阻为例,它通常与一个固定电阻串联分压。ADC读取分压点的电压。
// bsp_adc.c 片段 float BSP_Get_Temperature(void) { uint16_t adc_value; float voltage, resistance, temperature; // 启动ADC转换并获取结果 (假设使用ADC1, 通道0) ADC_SoftwareStartConvCmd(ADC1, ENABLE); while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); adc_value = ADC_GetConversionValue(ADC1); // 计算电压值 (假设参考电压Vref=3.3V, 12位ADC) voltage = (adc_value / 4095.0) * 3.3; // 计算NTC电阻值 (假设固定电阻Rref=10kΩ) // 公式: V_adc = Vcc * (R_NTC / (R_NTC + R_ref)) // 推导: R_NTC = (V_adc * R_ref) / (Vcc - V_adc) resistance = (voltage * 10000.0) / (3.3 - voltage); // 使用Steinhart-Hart方程或查表法将电阻值转换为温度 // 这里简化处理,实际需要根据NTC的B值参数计算或使用预存的查找表 temperature = 1.0 / (log(resistance / 10000.0) / 3950.0 + 1.0 / 298.15) - 273.15; // B=3950, 25°C时R=10kΩ return temperature; }实操心得:NTC的温度计算非线性且受个体差异影响。在产品化项目中,必须在多个已知温度点(如冰水混合物0°C、沸水100°C、室温)进行校准,生成查找表或拟合出更精确的公式系数。使用DS18B20这类数字传感器可以省去大量校准工作,提高可靠性。
4. OLED显示驱动: SSD1306 OLED屏通常通过I2C或SPI通信。使用硬件I2C(STM32的I2C外设)或软件模拟I2C均可。关键在于实现一个基本的画点函数,然后在此基础上构建字符、字符串和图形显示函数。网络上成熟的驱动库很多,集成时注意适配自己的硬件引脚和延时函数。
4. 应用层逻辑与状态机实现
4.1 咖啡机控制状态机设计
嵌入式系统中,状态机是管理复杂流程的神器。我们将咖啡机的工作流程抽象为一个有限状态机(FSM)。
定义状态枚举:
// app_coffee_fsm.h typedef enum { STATE_IDLE, // 空闲待机 STATE_HEATING, // 加热中 STATE_GRINDING, // 研磨中 STATE_BREWING, // 冲泡中 STATE_COMPLETE, // 制作完成 STATE_ERROR, // 错误状态(如缺水、超温) STATE_CLEANING // 清洁中 } CoffeeState_t;每个状态都有对应的入口动作、状态内循环执行的动作以及退出动作。状态迁移由事件触发,例如按键事件、定时器超时事件、传感器事件(温度到达、水位低)。
// app_coffee_fsm.c 简化示例 CoffeeState_t current_state = STATE_IDLE; CoffeeState_t next_state; Event_t current_event; void CoffeeFSM_Process(Event_t event) { current_event = event; switch(current_state) { case STATE_IDLE: if(event == EVENT_START_BREW) { // 退出IDLE状态的动作,如关闭待机灯 ExitIdleState(); // 进入HEATING状态 EnterHeatingState(); current_state = STATE_HEATING; } break; case STATE_HEATING: // 状态内动作:PID控制加热,刷新温度显示 PID_Heater_Control(); Display_Current_Temp(); // 状态迁移判断 if(IsTemperatureReached()) { ExitHeatingState(); EnterGrindingState(); current_state = STATE_GRINDING; } else if(event == EVENT_USER_CANCEL) { ExitHeatingState(); EnterIdleState(); current_state = STATE_IDLE; } break; case STATE_GRINDING: // 启动研磨定时器,控制PWM占空比 if(GrindingTimer_Expired()) { ExitGrindingState(); EnterBrewingState(); current_state = STATE_BREWING; } break; // ... 其他状态处理 default: break; } }4.2 温度PID控制算法集成
稳定的水温是咖啡好喝的关键。我们使用经典的PID(比例-积分-微分)算法进行闭环控制。
// app_pid.c typedef struct { float Kp, Ki, Kd; // PID系数 float integral; // 积分项累计 float prev_error; // 上一次误差 float setpoint; // 目标温度 float output_max; // 输出限幅(如对应PWM的ARR值) float output_min; } PID_Controller_t; float PID_Compute(PID_Controller_t *pid, float measured_value) { float error, derivative, output; error = pid->setpoint - measured_value; // 比例项 float proportional = pid->Kp * error; // 积分项(抗饱和处理) pid->integral += error; // 积分限幅,防止积分饱和导致系统超调过大 if(pid->integral > pid->output_max / pid->Ki) pid->integral = pid->output_max / pid->Ki; if(pid->integral < pid->output_min / pid->Ki) pid->integral = pid->output_min / pid->Ki; float integral = pid->Ki * pid->integral; // 微分项(通常用误差的差分) derivative = pid->Kd * (error - pid->prev_error); pid->prev_error = error; output = proportional + integral + derivative; // 总输出限幅 if(output > pid->output_max) output = pid->output_max; if(output < pid->output_min) output = pid->output_min; return output; }在STATE_HEATING状态中,周期性地(例如每100ms)调用PID_Compute,根据当前测量温度和目标温度(如93°C)计算出控制量(即PWM的占空比值),然后通过BSP_Set_Heater_PWM()函数调整加热功率。
PID参数整定心得:这是一个经验活。可以先设Ki=0, Kd=0,逐渐增大Kp直到系统出现等幅振荡,此时Kp记为Ku,振荡周期记为Tu。然后采用经典的齐格勒-尼科尔斯法则:Kp = 0.6*Ku,Ki = 2*Kp/Tu,Kd = Kp*Tu/8。在实际咖啡机中,由于热惯性大,微分项Kd可以设得很小甚至为0,主要靠比例和积分。务必在软件中做输出限幅和积分限幅,防止加热器全功率开启过久导致危险。
4.3 基于定时器的任务调度器
为了避免在main函数的while(1)中使用delay阻塞,我们实现一个简单的基于SysTick定时器的任务调度器,实现多任务“并行”执行。
// app_scheduler.c typedef struct { void (*task_func)(void); // 任务函数指针 uint32_t interval_ticks; // 执行间隔(SysTick滴答数) uint32_t last_run_ticks; // 上次运行时刻 uint8_t enabled; // 任务使能标志 } Sched_Task_t; #define MAX_TASKS 10 static Sched_Task_t task_list[MAX_TASKS]; static uint8_t task_count = 0; volatile uint32_t system_ticks = 0; // SysTick中断中自增 void SysTick_Handler(void) { system_ticks++; } void Sched_Init(void) { // 配置SysTick为1ms中断一次 if(SysTick_Config(SystemCoreClock / 1000)) { while(1); // 初始化失败 } } uint8_t Sched_AddTask(void (*func)(void), uint32_t interval_ms) { if(task_count >= MAX_TASKS) return 0; task_list[task_count].task_func = func; task_list[task_count].interval_ticks = interval_ms; task_list[task_count].last_run_ticks = 0; task_list[task_count].enabled = 1; task_count++; return 1; } void Sched_Run(void) { uint8_t i; uint32_t current_ticks = system_ticks; // 注意:在中断外读取,需考虑32位溢出问题 for(i=0; i<task_count; i++) { if(task_list[i].enabled) { // 检查是否到达执行时间 if((current_ticks - task_list[i].last_run_ticks) >= task_list[i].interval_ticks) { task_list[i].task_func(); task_list[i].last_run_ticks = current_ticks; } } } }在main.c中,我们可以这样组织:
int main(void) { // 系统时钟、外设初始化 SystemInit(); BSP_All_Init(); Sched_Init(); // 添加任务 Sched_AddTask(Task_KeyScan, 20); // 20ms扫描一次按键 Sched_AddTask(Task_UpdateDisplay, 100); // 100ms刷新一次显示 Sched_AddTask(Task_PIDControl, 100); // 100ms执行一次PID计算 Sched_AddTask(Task_CoffeeFSM, 50); // 50ms运行一次状态机 while(1) { Sched_Run(); // 在主循环中不断执行调度器 // 这里可以放置低优先级的后台任务 } }5. 系统调试、问题排查与优化实录
5.1 硬件联调常见问题
问题1:继电器动作时,单片机复位或程序跑飞。
- 排查:这是典型的电源干扰问题。继电器吸合瞬间,线圈需要大电流,可能导致电源电压瞬间跌落。电机启动时亦然。
- 解决:
- 强弱电隔离:确保控制板(弱电)与继电器、电机驱动板(强电)在物理布局和走线上分开,电源入口处做好隔离。
- 电源去耦:在单片机的VDD和GND引脚就近放置一个100nF的陶瓷电容和一个10uF的钽电容。
- 独立供电:如果可能,为继电器和电机使用独立的电源模块,与控制电源仅在信号地单点共地。
- 续流二极管:再次检查每个继电器线圈是否都并联了二极管,方向是否正确(阴极接电源正极)。
问题2:ADC读取的温度值跳动剧烈,不稳定。
- 排查:可能是电源噪声、模拟信号走线受干扰或ADC参考电压不稳。
- 解决:
- 软件滤波:采用滑动平均滤波或中值滤波。例如,连续采样10次,排序后去掉最大最小值,再取平均。
#define ADC_SAMPLE_SIZE 10 uint16_t adc_samples[ADC_SAMPLE_SIZE]; uint16_t get_filtered_adc_value(void) { // ... 采集ADC_SAMPLE_SIZE个数据到数组 ... // 对数组进行简单排序(冒泡即可) bubble_sort(adc_samples, ADC_SAMPLE_SIZE); // 去掉头尾各两个值,取中间的平均 uint32_t sum = 0; for(int i=2; i<ADC_SAMPLE_SIZE-2; i++) { sum += adc_samples[i]; } return sum / (ADC_SAMPLE_SIZE - 4); }- 硬件滤波:在温度传感器的信号输出端与地之间,并联一个0.1uF的电容,构成低通滤波器。
- 参考电压:如果使用VDDA作为参考,确保其供电稳定。可以使用一个简单的LC滤波电路。
问题3:OLED显示乱码或不显示。
- 排查:I2C通信失败。首先用逻辑分析仪或示波器抓取SCL和SDA波形,看是否有起始信号、地址应答和数据。
- 解决:
- 检查地址:SSD1306的I2C地址通常是0x78(写)或0x79(读),但有些模块是0x7A。确认地址是否正确。
- 检查上拉电阻:I2C总线必须接上拉电阻(通常4.7kΩ到10kΩ),如果模块上没有,需要在主板上添加。
- 检查初始化序列:确保严格按照SSD1306数据手册的初始化流程发送命令,包括设置对比度、显示模式、扫描方向等。
- 延时问题:在发送命令和数据后,给予足够的延时。有些驱动库的延时太短,在72MHz主频下可能导致初始化不完整。
5.2 软件逻辑调试技巧
1. 利用串口打印调试信息: 这是最直接有效的方法。初始化一个USART,连接USB转TTL模块到电脑,用串口助手查看。
// bsp_uart.c void USART1_Init(void) { // ... 初始化代码 ... } void USART1_SendString(char *str) { while(*str) { while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, *str++); } } // 在需要的地方调用 printf("Current State: %d, Temp: %.1f\r\n", current_state, temperature);注意,标准库的printf需要重定向fputc函数到串口。也可以自己实现一个简单的格式化发送函数。
2. 状态可视化: 在OLED屏上专门开辟一个调试区域,实时显示关键变量,如当前状态、ADC原始值、PWM占空比、PID输出等。这比串口更直观,尤其在没有连接电脑时。
3. 使用调试器进行单步和断点调试: 通过ST-LINK或J-LINK连接Keil/IAR的调试器,可以实时查看变量值、寄存器状态,单步执行代码,这是定位复杂逻辑错误的终极武器。例如,在状态机切换的条件判断处设置断点,观察事件变量是否正确。
5.3 系统优化与功能扩展思考
1. 低功耗优化: 在STATE_IDLE时,可以关闭不必要的时钟(如ADC时钟),将未使用的GPIO设为模拟输入模式以降低功耗,甚至让MCU进入睡眠模式(Stop Mode),通过外部按键中断唤醒。
2. 参数存储与记忆功能: 用户设定的咖啡浓度、杯量等参数,掉电后不应丢失。可以使用STM32内部的Flash(需注意擦写寿命)或外挂一个小的EEPROM芯片(如AT24C02)来存储这些参数。实现一个简单的参数管理系统,在初始化时读取,修改后写入。
3. 增加网络连接: 通过ESP-01S这类Wi-Fi模块(AT指令固件),利用STM32的UART与其通信,可以接入局域网,实现手机APP远程控制、固件OTA升级、甚至接入智能家居平台。这需要设计一套简单的串口通信协议。
4. 提高系统可靠性:
- 看门狗:启用独立看门狗(IWDG)或窗口看门狗(WWDG),防止程序跑飞。
- 异常处理:在状态机中增加
STATE_ERROR状态,当检测到温度超限、水泵堵转(通过电流检测或超时判断)、缺水等情况时,立即跳转到错误状态,关闭所有执行机构,并在屏幕上显示错误代码。 - 上电自检:系统启动时,依次检查关键外设(如OLED、温度传感器)是否响应,继电器能否正常动作(可以设计一个测试模式),给出明确的故障指示。
这个基于STM32F10x的咖啡机控制系统项目,从硬件焊接、驱动编写到应用逻辑整合,完整地走了一遍嵌入式产品开发流程。过程中遇到的每一个问题,从电源干扰到PID整定,都是宝贵的实战经验。源码的价值不仅在于它能控制一台咖啡机,更在于它提供了一个清晰的框架,你可以轻松地将这套控制逻辑移植到其他需要时序和温度控制的设备上,比如恒温箱、酸奶机、小型孵化器等。动手做一遍,远比只看文档收获大得多。
本文还有配套的精品资源,点击获取