
简介基于STM32的自动喂食器设计是一份面向嵌入式课程设计或毕业设计的完整项目资料包适合正在学习STM32、需要参考实际项目流程的电子或计算机专业学生。项目围绕STM32F401微控制器展开涵盖硬件选型、Keil uVision5环境下的程序开发、Proteus仿真验证以及LCD显示、按键输入、PWM电机控制、传感器检测等典型功能模块具备较高的工程参考价值。资源包共201个文件包含大量HAL库C源文件与头文件、Keil工程配置文件、编译生成的hex/axf文件、Proteus仿真工程及设计说明文档压缩包约11.75MB结构完整可帮助读者快速对照学习从初始化到外设驱动的代码组织方式。资料中还包括定时器中断、串口通信、SPI/I2C交互等关键代码便于理解嵌入式系统设计流程。已有7576人学习下载适合需要完整方案参考、仿真验证或查看代码排错思路的用户尤其有助于快速上手STM32项目开发。1. 基于STM32自动喂食器出差的最后一晚终于不用赌“它会不会卡粮”出差一周前我把自动喂食器放在猫砂盆旁边回来发现粮仓少了一半猫瘦了一圈——出粮口卡住系统却照样显示“已投喂”。这件事让我意识到一个被嵌入式课程设计反复选中的STM32项目难点从来不在“喂食”这个动作而在定时是否可靠、掉电会不会丢配置、出粮够不够顺畅。这份基于STM32的自动喂食器设计核心是F103主控驱动SG90舵机定时定量出粮配合内部RTC做全天多时段调度OLED显示下一次投喂倒计时三个按键现场调参掉电后参数自动从内部Flash恢复。适合正在做嵌入式课程设计的同学也适合想把手头GPIO、PWM、I2C、RTC、Flash模拟EEPROM一次性串起来的开发者。2. 硬件架构拆解F103最小系统、SG90舵机与供电方案的取舍2.1 器件清单课程设计里最省心的组合主控我选STM32F103C8T6最小系统板48引脚、72MHz主频内置RTC、USART、I2C、TIMCubeMX生成初始化代码后基本不用手写寄存器。板上带一个蓝色LED调试时能当心跳灯用。舵机选SG90 9g三根线——红5V、棕GND、橙信号靠PWM角度控制推粮臂比直流电机加螺旋出粮的结构简单得多。扭矩不够时换MG995代码接口不用动。RTC用芯片内部的外挂32.768kHz晶振和CR1220纽扣电池断电后时间继续走。精度一个月差十几秒普通家用完全够。想要更高精度就换DS3231模块I2C接口和OLED挂同一条总线硬件改动很小。显示用0.96寸SSD1306 OLED四线I2C显示倒计时和状态一屏能看完所有关键信息。输入是三个轻触按键设置、加、减加上一颗可选的无源蜂鸣器做投喂完成提示。电源采用12V/1A适配器经降压模块转5V给舵机、OLED和最小系统板供电。注意SG90堵转电流能到500mA降压模块输出至少要有1A余量否则舵机一转板子就复位。2.2 引脚分配一张接线表把外设地址定死模块模块引脚STM32引脚说明舵机橙线信号PA0 / TIM2_CH1PWM输出50Hz周期舵机红线独立5V与主控分路供电舵机棕线GND与主控共地OLEDSCL / SDAPB6 / PB7I2C1速率降到100kHz按键SW1~SW3PB12 / PB13 / PB14低电平触发内部上拉USART1TX / RXPA9 / PA10串口调试115200RTC晶振OSC32_IN / OUTPC14 / PC1532.768kHz配纽扣电池蜂鸣器信号PB15可选投喂完成提示接线有个老生常谈但总有人翻车的点舵机必须和主控共地但电源要分开走。把舵机红线直接插在最小系统板3.3V上一启动舵机拉低电压主控当场复位。我一般从5V降压模块单独拉一路给舵机信号线直接连PA0控制共地即可。I2C上拉电阻一般OLED模块已自带4.7kΩ不用外接。按键接GND和GPIO两脚靠内置上拉读低电平省掉了外接上拉电阻。LSE晶振的两个引脚走线要短靠得太近会互相干扰导致RTC不起振板上预留的焊盘就是给这块用的。2.3 上电到出粮一整个控制链路的运行顺序整机动作顺序是这样的上电后主控先初始化时钟和所有外设从内部Flash读回上次保存的配置再启动RTC然后OLED亮起显示当前时间和下次喂食剩余时间。主循环每秒读一次RTC时间比对各投喂时段是否到达到点就进入投放状态机蜂鸣器短响舵机转到出粮角度停留250ms回位后再等粮粒落稳。这段流程里最容易被低估的是“等待粮粒落稳”。舵机回位后粮仓里的粮可能还在滚动如果紧接着做下一次出粮分量会偏大或卡在出粮口。我在两次出粮之间加了400ms以上的间隔实际测试中填充不同粒径猫粮表现也不一样颗粒越大间隔越要拉长。3. 核心代码解析从定时到出粮的完整链路3.1 软件分层别把业务逻辑写进main.c我用CubeMX生成HAL库工程后会把代码拆成三个层面驱动层放bsp_servo.c、bsp_oled.c、bsp_key.c只做外设的初始化和底层的读写动作服务层放rtc_service.c和flash_store.c负责时间读取与配置的掉电保存应用层放app_task.c里面是状态机调度和定时触发逻辑。这么分层的直接原因是CubeMX每次重新生成代码都可能覆盖main.c如果业务逻辑全堆在main.c的用户代码保护段之外重新生成一遍工程代码就全丢了。我踩过这个坑之后main.c只剩下HAL初始化和一个循环调度入口业务改动都进app_task.c。应用层状态机就三个状态IDLE空闲、FEEDING投放中、SETTING设置中。FEEDING状态下按键不响应防止一次投喂还没结束又按出第二次SETTING状态用短按切换字段、长按确认退出。状态机的好处是逻辑链路清晰按键扫描、RTC触发、舵机动作各自只在自己状态里生效排查问题的时候看一眼状态值就能定位。3.2 RTC到点触发分钟级比对配一位防重入锁#define FEED_TASK_MAX 4 typedef struct { uint8_t hour; uint8_t minute; uint8_t portions; /* 单次投喂份数1~10 */ uint8_t enabled; uint8_t triggered; /* 本分钟内是否已投过 */ } FeedTask; FeedTask tasks[FEED_TASK_MAX]; void Check_Feed_Tasks(void) { RTC_TimeTypeDef cur; HAL_RTC_GetTime(hrtc, cur, RTC_FORMAT_BIN); for (uint8_t i 0; i FEED_TASK_MAX; i) { if (tasks[i].enabled !tasks[i].triggered cur.Hours tasks[i].hour cur.Minutes tasks[i].minute) { Start_Feed(tasks[i].portions); tasks[i].triggered 1; } } }逻辑说明主循环每一秒都会执行这个函数如果直接比较“时分秒完全相等”那一秒过去就再也匹配不上投喂必然漏掉。所以这里只比对小时和分钟命中后再用triggered位保证同一分钟内只执行一次。下一次时间走到新分钟triggered位在下一轮的判断里自然复位不需要额外清零逻辑。FEED_TASK_MAX设为4够覆盖早中晚加一顿夜宵的场景。参数说明portions是单次投喂的份数内部循环调舵机时按这个值决定转几次角度。HAL_RTC_GetTime读出的时间是BCD还是二进制由第三个参数控制RTC_FORMAT_BIN表示直接给十进制数值方便直接和tasks里的hour、minute比较。3.3 舵机投放PWM角度计算与防卡粮抖粮动作void Start_Feed(uint8_t portions) { feeding_active 1; for (uint8_t i 0; i portions; i) { Set_Servo_Angle(135); /* 出粮角度推粮臂把粮推出仓口 */ HAL_Delay(250); Set_Servo_Angle(5); /* 回位角度低于水平等待粮流落稳 */ HAL_Delay(400); } feeding_active 0; } void Set_Servo_Angle(uint8_t angle) { /* 时钟72MHzPSC71得到1MHz计数ARR19999得到50Hz周期 */ __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 500 (uint32_t)angle * 2000 / 180); }逻辑说明SG90舵机以20ms为周期工作脉宽0.5ms对应0度2.5ms对应180度。上面公式里500对应0.5ms2000是0到180度对应的脉宽增量范围除以180得到每度的脉宽步进。之所以放在系数最后算先乘后除是避免整数运算里先除再乘丢掉精度。出粮角度我用了135度而不是90度因为推粮臂带粮粒过来时135度能形成更大的倾角粮粒更容易滚落。如果你用的粮桶结构不同这个值要重新调。防卡粮的“抖粮”逻辑在重复循环里已经体现每次出粮都从135度回5度颗粒会来回松动。实际测试遇到受潮结块的粮可以在两个角度之间往返三次后再进入下一份代价是单次投喂时间变长。基础版没有反馈传感器只能靠动作冗余降低卡粮概率这点在第五章排查里会详细讲。3.4 按键配置与Flash掉电保存先擦除再编程typedef struct { uint16_t magic; /* 固定0xA5A5用于区分首次上电和有效配置 */ uint8_t hour[FEED_TASK_MAX]; uint8_t minute[FEED_TASK_MAX]; uint8_t portions[FEED_TASK_MAX]; uint8_t enabled[FEED_TASK_MAX]; } FeedConfig; #define FLASH_USER_ADDR 0x0800FC00 /* 最后一页1KB */ void Save_Config(FeedConfig *cfg) { HAL_FLASH_Unlock(); FLASH_ErasePage(FLASH_USER_ADDR); /* 内部Flash只能1写0必须先整页擦除 */ HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, FLASH_USER_ADDR, cfg-magic); /* 其余字段按半字依次写入 */ HAL_FLASH_Lock(); }逻辑说明内部Flash的擦写规则是“只能把1写成0”所以写入前必须先把整页擦除否则写进去的数据全是错的。这是掉电保存最常见的翻车点。FLASH_ErasePage的参数必须传递页起始地址0x0800FC00刚好是一页的开头向后1KB是0x08010000正好是64KB Flash的末尾。读回时先看magic是不是0xA5A5不是就按出厂默认值初始化防止第一次上电读到Flash里的随机数据。参数说明半字16位编程是F103的基本写入单位magic这种uint16_t变量可以直接写。Save_Config这个函数不能在每次改配置时都调用Flash擦写寿命只有约一万次设置页只在按键确认退出SETTING状态时写一次。正常使用十年也到不了一万次但如果你在调试时反复进设置界面手滑退出就可能提前耗掉寿命。3.5 OLED显示调度变则刷新不变则静默void OLED_Task(void) { static uint8_t last_minute 0xFF; RTC_TimeTypeDef cur; HAL_RTC_GetTime(hrtc, cur, RTC_FORMAT_BIN); if (cur.Minutes ! last_minute) { OLED_ShowString(0, 0, Time: 12:30); OLED_ShowString(0, 2, Next: 18:00 #2); last_minute cur.Minutes; } }我用“分钟变了才刷新”的策略而不是每秒刷一次。HAL库的I2C传输是阻塞式的在1080×64的OLED上刷一帧要几毫秒如果每秒刷一次主循环里其他任务的执行会被反复打断舵机PWM的实时性也会受牵连。对喂食器这种产品倒计时精确到分钟就够了没必要让屏幕每秒钟跳一次数字。4. 从CubeMX到Keil工程生成、编译烧录与串口实测4.1 CubeMX配置要点外设全在这里一次配完CubeMX工程里我按下面这张表配置外设时钟树保持默认的8MHz外部晶振倍频到72MHz不要直接选24MHz之类的内部时钟否则后面串口波特率会差得离谱。外设配置项关键参数RCCHSECrystal/Ceramic ResonatorTIM2Channel1PWM Generation CH1PA0I2C1Standard Mode时钟100kHzPB6/PB7USART1Asynchronous1152008N1RTCActivate Clock SourceLSE 32.768kHzGPIOPB12/PB13/PB14Input modePull-upFlash无直接用HAL的Flash接口生成工程时勾上“Generate peripheral initialization as a pair of .c/.h files”这样每个外设都有自己的初始化文件后续想调I2C速率或者改引脚直接改bsp层对应文件不用去CubeMX重新生成。生成的报告里能看到引脚冲突提示OLED占用PB6/PB7后SWD调试口仍保留PA13/PA14烧录不受影响。工程目录结构我习惯这样摆CubeMX生成的部分和手写业务代码分开重生成代码时只覆盖Core和Drivers下的HAL文件app_task.c和bsp_servo.c单独放一文件夹两颗目录互不干扰。AutoFeeder/ ├── Core/ │ ├── Src/main.c # CubeMX生成的初始化入口 │ └── Src/app_task.c # 业务状态机自己维护 ├── BSP/ │ ├── bsp_servo.c │ ├── bsp_oled.c │ └── bsp_key.c ├── Service/ │ ├── rtc_service.c │ └── flash_store.c ├── Drivers/ # HAL库由CubeMX维护 └── MDK-ARM/AutoFeeder.uvprojx4.2 Keil编译与ST-Link烧录四根线就够烧录用ST-Link的SWD模式只需要接四根线3.3V、SWDIO、SWCLK、GND。Keyil里在Options for Target的Debug选项卡选ST-Link Debugger然后进Settings的Flash Download勾选“Reset and Run”烧完自动复位运行省得每次手动按复位键。整体流程是打开工程后先核对Device选择的是STM32F103C8然后点Build编译输出窗口看到0 Error 0 Warning就可以下载。第一次烧录前确认SWDIO和SWCLK两根线没有接反我接反过一次Keil直接报找不到目标芯片。此外最小系统板上的BOOT0跳线要在0位置否则芯片进不了运行模式烧录后程序不执行。如果是自制板子烧录前还要确保NRST引脚没有被下拉到地。有些设计为了抗干扰给NRST加了电容电容太大时ST-Link复位拉不高一样会报连接失败。把电容限制在100nF以内或者干脆不接。4.3 printf重定向串口是喂食器的黑匣子#include stdio.h int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0x64); return ch; }HAL库默认把printf输出到ITM或RTT想要从USART1发到电脑串口助手必须重定向fputc。重定向后串口助手设置115200、8N1就能看到运行日志了。我一般上电先看这行输出确认RTC晶振有没有起振起振失败的话日志里会看到“RTC NOT READY”再检查PC14/PC15的晶振焊盘和纽扣电池电压。/* 实测串口输出示例 */ RTC OK, system time 12:00:00 Feed task #1 triggered at 12:00 Start feed, portions 2 Servo action 135 - 5 done Feed finished, fallback to IDLE KEY SETTING enter, hour18 minute30 Config saved to flash at 0x0800FC00这套日志几乎把关键事件全打印了时间同步、任务触发、舵机动作、配置保存。我调试时遇到“喂食没反应”就先看日志里有没有进入投放状态如果状态机没进入问题出在定时比对如果进入了但食盆是空的问题出在舵机或机械结构。串口把问题从玄学变成了可定位的代码逻辑。5. 常见问题排查定时失准、舵机卡死与掉电丢配置的坑5.1 五条高频踩坑记录每条都是血泪经验现象串口工具什么都收不到OLED也黑屏不亮。 原因CubeMX里选的是外部高速晶振HSE但最小系统板用的是内部RC或者焊接了不同频率的晶振系统时钟没起振。 解决先用串口波形的逻辑分析仪看TX脚有没有数据没有就把CubeMX时钟配置改成由HSI 8MHz倍频到72MHz程序能跑之后再用外部晶振。现象系统显示“已投喂”食盆里却一颗粮都没有。 原因舵机135度推粮臂没把粮粒推过仓口或者粮仓出口结拱粮粒堵在出口上方舵机转了但没粮下来。 解决把出粮角度继续加大到150度在Start_Feed循环里增加抖粮动作让推粮臂在135度和5度之间来回多走两次。如果还不行排查仓口挡板结构出口内径不能小于粮粒最大粒径的1.5倍。现象断电重上电设置好的时间全部回到默认值。 原因Save_Config执行前没有先擦除Flash页或者读配置时忘了校验magic字段直接把随机数据当成有效配置加载了。 解决写入前调用FLASH_ErasePage写完后读出magic回读校验读配置时settlement先比对magic。调试时反复擦写还会遇到Flash寿命损耗的问题改配置只在确认退出设置页时写一次。现象舵机不转到指定角度而是在抖像电锯一样响。 原因PWM周期配置成了500Hz而不是50Hz。计数频率1MHz配ARR1999得到的是500Hz周期2msSG90要求20ms。 解决把ARR改成19999重新生成代码后编译烧录。这个坑几乎人人都踩因为HAL库生成TIM的时候默认ARR很小不改的话舵机必抖。现象OLED偶尔花屏开机时白屏几秒。 原因I2C速率设成了400kHz加上杜邦线过长上升沿太慢导致数据错位或者OLED模块地址不对。 解决CubeMX里把I2C时钟改为100kHz检查模块的地址选择电阻默认0x3C换了模块要重新确认。OLED供电必须接3.3V接5V虽然不至于烧但长时间稳定性和亮度都会出问题。5.2 排查顺序先供电、再时钟、后日志上面五条单独出现还好解决麻烦的是症状叠加。舵机抖、串口乱码、OLED花屏一起出现时我按固定顺序排查先量5V和3.3V电压有没有跌落用万用表看舵机动作瞬间电压波形再查晶振是否起振用示波器看OSC32_IN有没有32.768kHz正弦波最后才看串口日志判断是应用层逻辑错误还是初始化失败。有一次我做统调时舵机每隔几秒自己抽动一下查了半天的代码最后发现是面包板上那根信号线虚插只搭到了半个孔位接触电阻时大时小。从那以后我每次接线都先固定端子再上电每根信号线逐一按压确认再也不敢用“看起来插进去了”这种判断。6. 二次开发思路从本地定时到WiFi远程与称重闭环6.1 WiFi远程改造ESP8266串口透传断网自动降级用一个ESP8266模块接USART2的PA2/PA3STM32通过AT指令控制连接手机App或者MQTT云平台下发指令指令格式做成两段T18:30表示新增一条18:30的定时任务F2表示立即喂两份。STM32解析完就写进tasks数组修改后触发Save_Config掉电不丢。关键逻辑在断网降级主循环里每5秒给ESP8266发一次心跳查询连续两次没收到回复就认定外网断了WiFi通道自动关闭喂食器回到本地定时表继续工作。这个设计一定要做因为远程功能一旦失灵设备不能连基础的定时喂食能力都丢掉。6.2 称重闭环用HX711把“卡粮”从玄学变成数据HX711加压力传感器放在食盆底下或粮仓底部。上电后先标定空盆读数作为基准每次投喂完成后重新读一次重量计算差值。如果差值小于设定分量的八成判定这次投喂卡粮了自动触发一次抖粮补偿。补偿逻辑在Start_Feed里加一个反馈分支称重差值在阈值内才结束投放否则循环做135度和5度的往返动作最多补偿三次。这个改造比WiFi更值得做因为喂食器最怕的就是“转了但粮没出来”有了重量数据设备的可信度完全不一样。从那以后我每次改喂食器程序都会先跑一遍“断电上电、改配置、再断电”的测试再把串口日志从头拉一遍才收工。定时、状态机、外设调度这三件事理顺了这个项目往智能家居上挪也只是换个壳而已希望帮到你。本文还有配套的精品资源点击获取