
简介基于STM32的自动喂食器课程设计资源包面向嵌入式相关专业的学生与开发者适用于学习STM32外设驱动、定时器中断、PWM控制及Proteus仿真等典型嵌入式开发环节。资源共201个文件压缩包约11.75MB包含完整的Keil工程文件、ST官方HAL库源码如stm32f4xx_hal_tim.c、stm32f4xx_hal_uart.c、编译生成的hex/axf文件以及Proteus仿真工程和说明文档便于直接查看与二次修改。已有7576人学习下载。从中可掌握STM32F401定时器配置来设定喂食周期、利用LCD显示状态、按键输入和电机PWM调速等关键设计思路仿真工程可在无硬件条件下先行验证逻辑降低调试门槛。整体方案涵盖硬件选型、软件编程、系统集成与功能实现对完成课程设计或入门智能家居类项目有实际参考价值。1. 为什么用STM32做自动喂食器一个嵌入式课程设计能落地的关键点很多嵌入式课程设计选自动喂食器因为它的功能链路足够完整传感器采集、电机控制、RTC定时、人机交互、掉电存储几乎把STM32的核心外设都过了一遍。但同样是这个课题有人交出的是“能转的舵机延时函数”有人交的是“能跑一个月不出错的完整系统”差别不在芯片型号而在方案设计。这篇笔记从一个能直接复现的STM32自动喂食器方案入手把硬件选型、状态机设计、驱动代码和实际踩坑串一遍目标是让新手照着搭能点亮让熟手直接换参数就能改造成自己的版本。2. 硬件选型与模块接线从舵机到称重传感器的整套搭配2.1 主控选型为什么默认选STM32F103C8T6而不是F407自动喂食器的控制逻辑并不复杂不需要浮点运算不需要大量缓存F103C8T6的72MHz主频、64KB Flash、20KB SRAM完全够用。更重要的是它的资源配套成熟HAL库和标准库的例程多课程设计阶段遇到问题搜起来快。F407的优势在更高速的ADC和更多定时器但这个项目里用不上成本还高一截。选型时还要看引脚数量C8T6是LQFP48封装GPIO刚好够用。我一般会预留两个空闲引脚一个接调试用的LED一个留作后续扩展低功耗唤醒功能。如果手上的板子是C6T632KB Flash也能跑但代码编译完接近极限不建议省这十几块钱。2.2 执行机构对比舵机、步进电机与直流减速电机怎么选出粮机构是整个系统里最容易翻车的部分选型要看食料形态和出粮精度。舵机SG90/MG995适合旋转拨片式出粮SG90扭矩不够拨片插进粮仓经常卡死我用的是MG995扭矩大一些但需要外部5V供电不能直接从板子3.3V取电。步进电机28BYJ-48 ULN2003驱动板适合转盘式定量出粮控制精准每次转固定角度缺点是速度和噪音夜间喂食会吵醒人。直流减速电机带霍尔编码器适合螺旋推进式出粮但闭环控制代码复杂课程设计周期内不建议碰。从“能交差”和“能改进”两个角度我建议用步进电机。它的逻辑清晰出粮量可以通过脉冲数反复校准写报告时也有数据可以展开。MG995舵机做备选但要注意堵转电流能拉到1A以上电源纹波会干扰ADC采样。2.3 传感器与RTC模块称重、时钟、显示一个都不能少喂食器不能只“定时转一下电机”至少要有三个反馈维度称重模块HX711 压力传感器放在粮仓底部或食盆下方检测余粮量和是否已进食。HX711用24位ADC两路差分输入接传感器四根线E、E-、A、A-SCK和DOUT接GPIO。RTC模块DS3231比DS1302贵但温度补偿晶振的漂移量小很多一个月误差几秒。DS1302便宜但走时漂移明显如果喂食器连续运行两周喂食时间会偏出二十分钟。课程设计如果周期短DS1302能用如果要做长期测试还是DS3231靠谱。显示与交互0.96寸OLEDI2C显示当前时间、下次喂食倒计时、余粮百分比。三个按键分别是模式切换、手动出粮、参数设置。这块不做复杂菜单三个按键足够覆盖所有操作路径。2.4 接线表与电源分配烧板子的坑都在电源这里模块引脚连接供电备注STM32F103C8T6 最小系统—3.3V用AMS1117-3.3稳压OLED SSD1306SCL→PB6, SDA→PB73.3VI2C1DS3231SCL→PB6, SDA→PB73.3V与OLED共用I2C总线地址不同HX711SCK→PA1, DOUT→PA03.3V传感器激励接5V也可看模块说明步进电机驱动ULN2003IN1→PA4, IN2→PA5, IN3→PA6, IN4→PA75V电机供电必须独立走线三按键分别接PB0/PB1/PB10另一端接GND3.3V内部上拉蜂鸣器PA83.3V喂食完成短鸣两次继电器/电子开关PA95V控制食盆加热或照明可选电源是这里最大的坑。步进电机启动瞬间电流能到300mA以上如果和STM32共用一条细杜邦线电压会被拉低MCU直接复位。我的做法是5V适配器进来先分两路一路给ULN2003驱动板和电机另一路经过AMS1117给MCU供电两路地在电源入口处单点汇合。接完线先别上电用万用表量一遍VCC和GND之间有没有短路再量AMS1117输出是不是3.3V。这个习惯能救回一半以上的“上电冒烟”事故。3. 状态机驱动的喂食逻辑定时、手动与补食三种模式怎么切3.1 为什么用状态机而不是一堆if-else自动喂食器的行为看起来简单到点转电机、按按钮转电机、余粮少了提示。但如果把这些动作写成线性if-else会出现一个典型问题——用户按“手动出粮”时正好碰到定时喂食触发两个流程同时操作电机步进电机相位冲突轻则卡转重则烧驱动。用状态机可以把系统行为收敛成三个稳定状态待机IDLE、喂食中FEEDING、设置中SETTING。所有按键事件和定时器事件都先进入事件队列状态机根据当前状态决定要不要响应从根上避免了事件竞争。对课程设计来说状态机还有一个额外的好处答辩时画状态转移图非常直观评审老师一眼就能看出系统结构比看一堆散落的函数好讲得多。3.2 核心状态定义与事件映射这段是系统的骨架状态枚举和事件枚举先定义清楚typedef enum { STATE_IDLE 0, // 待机显示时间等待事件 STATE_FEEDING, // 喂食中电机转动屏蔽重复触发 STATE_SETTING // 设置中循环修改喂食时间和手动出粮量 } SysState; typedef enum { EVENT_NONE 0, EVENT_BTN_MODE, // 模式键短按切换状态长按进入设置 EVENT_BTN_FEED, // 手动出粮键 EVENT_BTN_SETUP, // 参数调整键 EVENT_TIMER_PLAN, // 定时喂食触发 EVENT_WEIGHT_LOW // 余粮不足 } SysEvent;事件产生后统一丢进一个环形缓冲状态机主循环只从缓冲取事件不在中断里直接处理业务逻辑。这样做的目的是让状态机永远只面对“一个事件”不会出现中断嵌套导致的全局变量错乱。参数说明EVENT_BTN_MODE和EVENT_BTN_SETUP分开是因为三个按键里有一个需要复用——短按切换状态长按进入参数修改。如果按键逻辑直接写在状态机里后面加“长按”功能时代码会很别扭我这里把按键扫描独立成模块只向上报事件状态机不关心按键怎么检测的。3.3 状态转移与喂食动作实现状态转移逻辑放在主循环里每10ms跑一次void SM_Process(SysEvent evt) { switch (state) { case STATE_IDLE: if (evt EVENT_BTN_MODE) { state STATE_SETTING; Display_EnterSetting(); } else if (evt EVENT_BTN_FEED) { Feed_Once(MANUAL_FEED_AMOUNT); } else if (evt EVENT_TIMER_PLAN) { Feed_Once(TIMER_FEED_AMOUNT); } else if (evt EVENT_WEIGHT_LOW) { Buzzer_Alert(3); } break; case STATE_FEEDING: // 喂食中不响应任何新事件防止重复出粮 if (evt EVENT_TIMER_PLAN) { Log_AddBacklog(); // 记录一次未执行的定时喂食 } break; case STATE_SETTING: if (evt EVENT_BTN_MODE) { state STATE_IDLE; RTC_SaveConfig(); Display_BackToMain(); } else if (evt EVENT_BTN_SETUP) { Setting_AdjustValue(1); // 每次1分钟 } break; } }Feed_Once内部实现是先检查当前是不是STATE_IDLE是的话把状态切到STATE_FEEDING然后以旋转步进电机N圈的方式出粮完成后回到STATE_IDLE。喂食期间如果定时器又触发了一次说明用户设置的间隔太短系统只记录不执行避免电机连续转。这个“记录未执行事件”的思路是从实际操作里长出来的——测试时我把定时间隔设成1分钟结果喂食还没结束下一次触发就来了电机嗡嗡响个不停。加上Log_AddBacklog后至少知道错过了一次喂食后面做统计也有数据。3.4 步进电机驱动与出粮量计算28BYJ-48步进电机是减速电机减速比1/64步距角5.625°/64转一圈需要4096个脉冲。出粮量不能直接按脉冲数算得先实测让电机每次转200个脉冲称一次出粮重量连续测5次取平均得到“每脉冲出粮克数”再反推目标喂食量对应的脉冲数。#define PULSE_PER_REV 4096 // 电机转一圈的脉冲数 #define GRAMS_PER_CIRCLE 12.5f // 实测转一圈出粮12.5g需自己校准 #define TIMER_FEED_GRAMS 25.0f // 定时喂食目标量可配置 void Feed_Once(float target_grams) { float need_revs target_grams / GRAMS_PER_CIRCLE; uint32_t need_pulses (uint32_t)(need_revs * PULSE_PER_REV); state STATE_FEEDING; for (uint32_t i 0; i need_pulses; i) { Stepper_Step(1); // 单步旋转 delay_us(800); // 速度控制太快会失步 if ((i % 100) 0) { Check_Weight(); // 每100步看一次称重防止过量 } } state STATE_IDLE; }参数说明GRAMS_PER_CIRCLE这个值千万别照抄我的粮仓、叶轮形状、粮食颗粒大小跟你不一样出粮量差20%是正常的。校准方法程序里写一个“测试模式”按一次手动出粮转一圈拿厨房秤称重改宏定义反复三次取平均值。delay_us(800)对应步进频率约1.25kHz28BYJ-48在这个速度下扭矩比较稳再快容易失步。实际运行中如果听到电机“哒哒”声变脆但不转动就是失步了把延时拉长到1000us。4. 驱动层与上层逻辑分离HAL库代码组织与中断优先级处理4.1 代码目录结构与模块划分课程设计代码最容易写成“一个main.c走天下”查bug时从头翻到尾。这个项目规模不大但功能模块不少我建议按这个结构组织User/ ├── main.c // 主循环调度器 ├── sys_state.c // 状态机模块 ├── sensor_weight.c // HX711驱动与滤波 ├── motor_stepper.c // 步进电机驱动 ├── rtc_ds3231.c // 时钟模块 ├── oled_ssd1306.c // 显示模块 ├── key_scan.c // 按键扫描 ├── buzzer.c // 蜂鸣器 └── config_store.c // 参数存储喂食时间、喂食量每个模块提供两个接口xxx_Init()和xxx_Process()初始化在main最开始调用Process在状态机需要时调用。这样做的边界价值在于——如果后面想换成别家的OLED屏只需要改oled_ssd1306.c上层调用接口完全不变。答辩时也能明确说出“驱动层与业务层解耦”这是加分项。4.2 主循环调度10ms时基与事件轮询不要用HAL_Delay做所有事情。HAL_Delay在中断里会出问题而且多个模块各自延时会导致时间片混乱。我的做法是开一个SysTick中断每1ms产生一次时基用一个全局计数变量tick_ms累加主循环里按时间片轮询各任务volatile uint32_t tick_ms 0; void SysTick_Handler(void) { tick_ms; } void main_loop(void) { uint32_t last_key 0, last_disp 0, last_weight 0; while (1) { if (tick_ms - last_key 20) { // 每20ms扫一次按键 Key_Scan(); last_key tick_ms; } if (tick_ms - last_disp 200) { // 每200ms刷新一次OLED Display_Update(); last_disp tick_ms; } if (tick_ms - last_weight 500) { // 每500ms采样一次称重 Weight_Sample(); last_weight tick_ms; } SM_Process(GetNextEvent()); // 事件队列取事件 } }时基参数说明按键扫描20ms是防抖的经典窗口小于10ms会漏掉快速按键大于50ms手感肉。OLED刷新200ms是因为SSD1306是I2C接口全屏刷新一次要传1KB数据在72MHz主频下大概占用几毫秒刷新太快主循环就卡了。称重500ms采样是因为HX711每次转换要100ms左右而且压力传感器的噪声需要多次平均太快反而波动大。这是整个系统的主干后面加功能都往这个循环里挂任务就行。4.3 HX711称重读取24位ADC的滤波为什么必须做HX711读取本身不复杂SCK拉低、DOUT变低表示数据准备好然后连续读24个脉冲拿数据。但原始数据直接用的话数字能跳几百甚至上千因为电机的震动、桌面的晃动都会影响传感器。#define HX711_SCK GPIO_PIN_1 #define HX711_DOUT GPIO_PIN_0 uint32_t HX711_Read(void) { uint32_t value 0; HAL_GPIO_WritePin(GPIOA, HX711_SCK, GPIO_PIN_RESET); while (HAL_GPIO_ReadPin(GPIOA, HX711_DOUT) ! GPIO_PIN_RESET); // 等待数据就绪 for (uint8_t i 0; i 24; i) { HAL_GPIO_WritePin(GPIOA, HX711_SCK, GPIO_PIN_SET); value value 1; if (HAL_GPIO_ReadPin(GPIOA, HX711_DOUT) GPIO_PIN_SET) { value; } HAL_GPIO_WritePin(GPIOA, HX711_SCK, GPIO_PIN_RESET); } // 第25个脉冲表示通道A增益128本项目不用切换 HAL_GPIO_WritePin(GPIOA, HX711_SCK, GPIO_PIN_SET); delay_us(1); HAL_GPIO_WritePin(GPIOA, HX711_SCK, GPIO_PIN_RESET); return value; } float Weight_GetGrams(void) { // 简单滑动平均滤波取最近5次读取结果去掉最大最小值再平均 uint32_t buf[5]; for (uint8_t i 0; i 5; i) { buf[i] HX711_Read(); HAL_Delay(100); } // 冒泡排序取中间三个值 for (uint8_t i 0; i 4; i) { for (uint8_t j i 1; j 5; j) { if (buf[i] buf[j]) { uint32_t tmp buf[i]; buf[i] buf[j]; buf[j] tmp; } } } uint32_t sum buf[1] buf[2] buf[3]; float avg sum / 3.0f; // 标定系数用已知砝码校准系数 砝码克数 / (读取值 - 零点值) return (avg - zero_offset) / scale_factor; }滤波逻辑说明HX711原始读数受噪声影响很大滑动平均去掉极值能有效抑制偶发尖峰。注意zero_offset和scale_factor两个参数需要标定零点值就是空仓时读到的数据系数用100g砝码压上去算。常见误用是直接用HX711库返回的原始值当重量这个值受传感器个体差异和安装位置影响极大不标定的话余粮判断完全不靠谱。4.4 RTC与掉电存储断了一周电喂食计划不能丢DS3231自带电池备份断电后时间不丢但喂食计划和出粮量参数存在MCU的Flash里断电就没了。课程设计阶段很多人忽略这个问题实际使用中一次意外断电所有配置清零用户就得重新设一遍。我把配置存在STM32内部Flash的最后一个扇区typedef struct { uint16_t feed_hour; // 定时喂食小时 uint16_t feed_min; // 定时喂食分钟 uint16_t feed_amount; // 每次出粮克数乘以10存储 uint16_t crc16; // 校验值 } FeedConfig; void Config_Save(FeedConfig *cfg) { // 先擦除扇区再写入 FLASH_EraseInitTypeDef erase {0}; uint32_t sector_error 0; erase.TypeErase FLASH_TYPEERASE_PAGES; erase.PageAddress CONFIG_FLASH_ADDR; erase.NbPages 1; HAL_FLASHEx_Erase(erase, sector_error); HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, CONFIG_FLASH_ADDR, cfg-feed_hour); HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, CONFIG_FLASH_ADDR 2, cfg-feed_min); HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, CONFIG_FLASH_ADDR 4, cfg-feed_amount); HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, CONFIG_FLASH_ADDR 6, cfg-crc16); }Flash写入次数有限F103的数据闪存一般标称10k次擦写所以只有用户修改参数时才调用Config_Save喂食完成不写Flash。校验用sail16或简单累加和都行目的是上电时检测配置有效无效就恢复默认值。这里最容易翻车的点是Flash写入地址越界我用的最后一个扇区是0x0800F80064KB型号不同容量芯片地址不一样务必查手册再写。5. 避坑记录上电复位、粮食卡轴与RTC跑飞的典型排查5.1 现象一上电步进电机疯转停不下来第一次把代码烧进去电机在初始化没完成时就开始转而且完全不受控制按停止键也停不下来。原因排查步进电机的四个控制引脚IN1到IN4在MCU复位期间是高阻态ULN2003输入悬空时输出会乱跳导致电机自由转动。另外我的初始化代码里先开了电机驱动器的使能再初始化GPIO顺序反了。解决把GPIO初始化放到系统时钟配置之后、任何外设使能之前同时把四个控制引脚初始化为推挽输出低电平确保电机上电处于锁止状态。代码顺序调整为HAL_Init → 系统时钟 → GPIO初始化电机引脚先拉低→ 其他外设。从那以后我每次写新工程都把执行机构引脚的电平状态列出来逐项检查不再相信“默认低电平”这种话。5.2 现象粮食出口堵塞电机持续转动但不出粮测试时发现定时喂食触发后电机在转也能听到“咔咔”声但出粮口几乎没有粮食落下。原因排查粮仓底部是平的粮食堆在角落形成拱桥效应叶轮转过去时没有粮食可带。这是机械结构问题不是代码问题。解决把粮仓底部改成漏斗斜面倾斜角度至少45度在出粮口上方加一个竖直的搅拌棒跟随电机一起旋转把搭桥的粮食打散。这个改动之后出粮重量稳定性从“每次差10g”改善到“每次差2g以内”。如果你用3D打印做外壳设计时直接加这根搅拌结构省得后面改造。5.3 现象DS3231时间偶尔跳变喂食时间偏差超过半小时系统运行两天后RTC读取到的时间比实际快了半小时以上而且不是稳定快是突然跳变到某个错误值。原因排查我用的是I2C总线OLED和DS3231挂在同一组引脚上。OLED刷新每200ms一次占用I2C总线时间如果恰好被中断打断DS3231的读操作可能读到半个字节导致时间寄存器数据错乱。解决把I2C通信改用硬件I2CHAL库的I2C1并提高优先级。更彻底的方案是把OLED和DS3231分开DS3231用软件模拟I2C挂在另外两个引脚上OLED用硬件I2C挂原引脚。两者物理隔离后干扰再没出现过。注意软件模拟I2C要开编译优化否则时序可能慢到DS3231不响应。5.4 现象手动按键偶尔触发两次喂食按键按一下有时会出粮两次。原因排查按键扫描周期是20ms我用的机械按键没有做完整的“按下释放”状态检测按下一次的抖动能被两个20ms周期各采样到一次产生两个按键事件。解决按键扫描里增加状态机识别“按下沿”而不是电平。按下时登记事件释放前不再重复登记同时加一个300ms的全局锁定时间按键后300ms内的任何按键事件直接丢弃。代码上改一行判断但效果明显。之前在实机上统计过不带沿检测每秒能多触发0.5到2次误事件带上之后基本为0。5.5 现象称重数值漂移余粮误报空粮仓时称重显示30g跑了一会儿变成40g又过了几分钟回到25g。原因排查HX711的参考电压受供电波动影响电机启动时电流大电源电压跌落了100mV左右导致ADC读数偏移。另一个因素是温度漂移和机械结构的热胀冷缩。解决从两个方向处理。电源上电机供电和MCU供电完全分开两个5V入口接一个大电容1000uF电解电容 0.1uF陶瓷电容做储能。代码上不要直接用单次称重结果而是在电机停止运行后300ms再采样避开电源波动窗口。同时做软件校准每次喂食完成后等待3秒连续读5次取中间值更新余粮显示。6. 进阶玩法加一个“无RTC离线喂食日志”功能课程设计答辩时评审经常会问“你怎么证明系统按时喂食了”。光靠“我看到的”不够有说服力我给这个项目加了一个离线喂食日志模块不依赖外部存储芯片用STM32内部Flash剩余空间记录最近50次喂食记录。每条记录的结构就四个字段typedef struct { uint32_t timestamp; // 从RTC读出的Unix时间 uint16_t feed_amount; // 实际出粮克数 uint8_t source; // 0定时触发 1手动触发 2补食 uint8_t weight_after; // 喂食后食盆重量单位g } FeedLogEntry;每条记录12字节50条共600字节加上前面配置项Flash占用不到1KB。F103C8T6的13KB数据Flash有足够空间。写入用环形覆盖策略满了就把最旧的一条重写掉。void Log_AddEntry(FeedLogEntry *entry) { uint16_t next_idx log_next_index; uint32_t addr LOG_BASE_ADDR (next_idx % MAX_LOG_COUNT) * sizeof(FeedLogEntry); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, *(uint32_t*)entry-timestamp); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr 4, *(uint32_t*)entry-feed_amount); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr 8, *(uint32_t*)entry-weight_after); log_next_index; }查询时用串口输出或者用OLED翻页显示最近几条。这个功能在答辩前实测了三天日志里有每一条喂食记录的时间戳和出粮重量评审老师直接对着数据看不用问“怎么证明”了。我后来把这个日志模块沿用到了其他嵌入式项目上发现“给系统一个能自证的数据出口”比任何口头解释都省事。在那以后我每次做嵌入式课程设计都会先想一个问题——“如果评审只给我5分钟我拿什么证明系统真的在按预期工作”。有这个功能兜底心里踏实很多希望帮到你。本文还有配套的精品资源点击获取