简介:这份资源是小米扫地机器人工程源码程序包,基于STM32F103与FreeRTOS构建,面向嵌入式开发学习者、智能家居从业者及机器人控制方向的中高级开发者,帮助理解扫地机器人从硬件驱动到上层算法的完整实现。压缩包共205个文件、约3.37MB,以86个c源文件与92个h头文件为主体,另含txt说明、s汇编、hex固件、uvprojx工程及uvoptx配置等,覆盖GPIO、ADC、SPI等外设驱动、FreeRTOS任务调度、队列与信号量机制、路径规划、传感器数据处理及电源管理策略。已有3987人学习下载,读者可借此掌握STM32外设编程、实时操作系统多任务设计、A*或Dijkstra寻路、卡尔曼滤波降噪与低功耗管理等关键技能,并对照完整工程结构进行编译调试与二次开发,是研究嵌入式系统与物联网设备设计的实用参考。
1. 拆开小米扫地机器人工程源码:STM32F103 + FreeRTOS 能跑出什么
手里拿到一份标着「小米扫地机器人工程源码程序STM32103 freeRTOS.zip」的压缩包,第一反应通常不是兴奋,而是犯嘀咕:这玩意儿到底能不能编译、能不能跑、里面是完整工程还是几段散装驱动。我拆过不少这类扫地机主控固件,结论先放这儿——绝大多数所谓「小米扫地机器人源码」并不是小米官方放出的东西,而是爱好者或方案商基于 STM32F103 系列 MCU 加 FreeRTOS 搭的一套功能演示工程,覆盖电机驱动、传感器采集、路径逻辑的骨架。它能给你的价值,是让你看清一台扫地机主控在资源受限的 MCU 上怎么把 FreeRTOS 的任务调度、外设驱动和业务状态机缝在一起。适合谁?适合正在学 FreeRTOS 移植、想找一个「有真实外设、有多个任务、有中断」的练手工程的中级嵌入式开发者,也适合做小家电主控方案、想参考任务划分思路的工程师。指望它直接刷进你手里那台小米扫地机,基本不现实,硬件对不上。
2. 从压缩包到能编译:STM32F103 工程的环境搭建与目录判读
拿到一个来路不明的 STM32 工程压缩包,最忌讳的就是双击打开直接点编译。先花十分钟把目录结构和构建体系看清楚,能省掉后面几个小时的报错排查。STM32F103 这类工程常见的构建方式有三种:Keil MDK 的.uvprojx、IAR 的.eww、以及基于 Makefile 或 CMake 的 GCC 工程。你解压后第一件事就是找这几个标志文件,它决定了你后面装什么工具链。
2.1 先判构建体系,再决定装什么工具
解压后进根目录,用命令行列一下文件,比在资源管理器里翻要快得多。下面这段命令帮你快速定位关键文件:
# 进入解压后的工程根目录,列出顶层结构 ls -la # 找构建工程文件,判断用的是哪套工具链 find . -maxdepth 3 -iname "*.uvprojx" -o -iname "*.eww" -o -iname "Makefile" -o -iname "CMakeLists.txt" # 找 FreeRTOS 内核目录,确认是不是真带了 RTOS find . -maxdepth 4 -type d -iname "FreeRTOS*" # 找启动文件和链接脚本,这两个决定芯片型号和内存布局 find . -maxdepth 3 -iname "startup_*.s" -o -iname "*.ld" -o -iname "*.sct"逻辑说明:第一条列顶层,看有没有 README、Doc 之类的说明;第二条是核心,find同时匹配三种构建文件,输出里出现哪个,你就装哪套环境。第三条确认 FreeRTOS 是真集成还是只放了个空目录。第四条最关键,startup_stm32f103*.s和.ld/.sct文件直接告诉你目标芯片的具体型号和 Flash/RAM 大小。
参数说明:-maxdepth 3限制搜索深度,避免在大工程里翻太久;-iname忽略大小写,因为不同作者命名习惯不一样,FreeRTOS、freertos、FreeRTOS-Kernel都可能出现。如果第二条命令什么都没输出,说明这个工程可能只给了源码没给工程文件,你得自己用 STM32CubeMX 重新生成一个骨架再把源码挪进去。
2.2 用 STM32CubeMX 补齐缺失的工程骨架
很多流传的源码包只保留了Core、Drivers、Middlewares这几个源码目录,工程文件被删了。这种情况我一般用 STM32CubeMX 重新生成,芯片选 STM32F103 对应型号,把 FreeRTOS 通过 CMSIS-RTOS v1 或 v2 勾上,生成后再把原工程的业务代码合并进来。关键配置项如下表:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| RCC 时钟源 | HSE 外部晶振 | F103 常见 8MHz 晶振,倍频到 72MHz |
| FreeRTOS 接口 | CMSIS-RTOS v2 | 新工程优先 v2,老代码可能是 v1 |
| 时基源 | 除 SysTick 外的定时器 | FreeRTOS 占用 SysTick,HAL 时基要换 TIM |
| Heap 方案 | heap_4 | 带碎片合并,长期运行更稳 |
| 最小栈 | 128 字起 | 按任务实际用量调,别一刀切 |
生成工程后先别急着合并业务代码,编译一次空工程,确认工具链、芯片包、下载器都通了,再往里加东西。这一步是「后悔药」,能帮你把环境问题和代码问题彻底分开。
2.3 编译前必查的三个配置点
工程能打开不代表能编译过。F103 的工程最常见的编译失败集中在三处:芯片型号选错、宏定义缺失、头文件路径不全。芯片型号在 Keil 的Options for Target里选,必须和启动文件、链接脚本一致,选成 F103C8 却用 F103ZE 的启动文件,链接阶段必炸。宏定义重点看STM32F103xB这类器件宏和USE_HAL_DRIVER,缺了 HAL 库直接编不过。头文件路径要把Core/Inc、Drivers/STM32F1xx_HAL_Driver/Inc、Middlewares/.../FreeRTOS/Source/include以及对应移植层的portable目录都加进去,少一个就是满屏的undefined。
提示:合并源码时优先保留原工程的
FreeRTOSConfig.h,这个文件里的configTOTAL_HEAP_SIZE、configMAX_PRIORITIES、configTICK_RATE_HZ直接决定系统行为,换成 CubeMX 默认值可能让原业务逻辑跑不起来。
3. FreeRTOS 任务划分:扫地机主控怎么把外设和业务拆开
扫地机主控的活儿其实很杂:要读陀螺仪和编码器算位姿,要驱动左右轮电机,要扫悬崖传感器和碰撞传感器,要管电池和充电,还要跑路径规划。在 F103 这种 72MHz、20KB RAM 的芯片上,这些活儿不可能塞进一个大循环,必须靠 FreeRTOS 拆成多个任务,用优先级和队列把实时性要求不同的部分隔开。看懂这份源码的任务划分,比看懂任何单个驱动都有价值。
3.1 典型任务清单与优先级分配
一份结构清晰的扫地机固件,任务通常按实时性从高到低排。下面这张表是我从多个类似工程里归纳出来的常见划分,你可以拿它对照手里的源码:
| 任务名 | 优先级 | 周期/触发 | 职责 |
|---|---|---|---|
| MotorCtrl | 最高 | 1ms 定时 | 电机 PWM 更新、编码器读取 |
| SensorTask | 高 | 5ms | 陀螺仪、悬崖、碰撞采集 |
| NavTask | 中 | 20ms | 位姿解算、路径决策 |
| CommTask | 中低 | 事件 | 串口/蓝牙指令收发 |
| PowerTask | 低 | 100ms | 电池监测、充电管理 |
| LedTask | 最低 | 200ms | 状态灯、蜂鸣器 |
优先级分配的核心原则是:周期越短、错过 deadline 后果越严重的任务,优先级越高。电机控制错过 1ms 会导致 PWM 抖动甚至堵转,必须最高;状态灯晚个几十毫秒没人看得出来,放最低。中间的任务用队列或信号量通信,别用全局变量硬传,否则数据竞争会让你调到头秃。
3.2 用队列把传感器数据送到导航任务
任务之间传数据,新手最容易犯的错是直接读写全局结构体。传感器任务在写、导航任务在读,一旦读写不同步,导航拿到的就是半新半旧的位姿,表现出来就是机器人走路「抽搐」。正确做法是用 FreeRTOS 的队列。下面是一段典型的队列收发代码:
/* 定义位姿数据结构,队列里传的是值拷贝,不是指针 */ typedef struct { float x; float y; float theta; uint32_t timestamp; } Pose_t; /* 全局队列句柄,在 main 里创建 */ QueueHandle_t xPoseQueue; /* 初始化:队列深度 4,每个元素一个 Pose_t */ void PoseQueue_Init(void) { xPoseQueue = xQueueCreate(4, sizeof(Pose_t)); configASSERT(xPoseQueue != NULL); /* 创建失败直接断言,别让它悄悄跑 */ } /* 传感器任务里发送,超时 0 表示队列满就丢,不阻塞控制循环 */ void SensorTask(void *pvParameters) { Pose_t pose; for (;;) { pose = ReadImuAndEncoder(); /* 读陀螺仪和编码器 */ pose.timestamp = xTaskGetTickCount(); xQueueSend(xPoseQueue, &pose, 0); vTaskDelay(pdMS_TO_TICKS(5)); } } /* 导航任务里接收,阻塞等待,拿到就算 */ void NavTask(void *pvParameters) { Pose_t pose; for (;;) { if (xQueueReceive(xPoseQueue, &pose, portMAX_DELAY) == pdTRUE) { UpdatePose(&pose); PlanPath(); } } }逻辑说明:队列传的是结构体值拷贝,发送方改了pose不影响队列里的副本,天然避免了共享内存问题。发送用 0 超时,是因为传感器任务不能被队列满拖住,宁可丢一帧也不能让采集周期漂移。接收用portMAX_DELAY,导航任务本来就没别的事,等数据是合理的。
参数说明:队列深度 4 是经验值,够吸收导航任务偶尔的处理延迟,又不至于占太多 RAM——每个Pose_t16 字节,4 个才 64 字节。configASSERT在调试阶段一定要开,队列创建失败通常是堆不够,早发现早改configTOTAL_HEAP_SIZE。
3.3 中断与任务的边界:哪些活儿必须放 ISR
编码器计数、串口收字节这类事必须放中断,但中断里只能做最轻的活儿。我见过不少工程在串口中断里直接解析协议、拼字符串,结果高优先级任务被拖慢,电机控制跟着抖。正确边界是:ISR 只负责把原始数据塞进队列或置个标志,解析和处理全交给任务。比如编码器用定时器的编码器模式硬件计数,ISR 只在一个固定周期读计数差值,通过队列发给电机任务。串口 ISR 收到一个字节就xQueueSendFromISR到字节队列,协议解析任务在任务上下文里慢慢拼。记住一条:ISR 里能调用的 FreeRTOS API 都带FromISR后缀,用错了轻则断言失败,重则系统挂死。
4. 外设驱动与实时性:电机、传感器、通信的落地细节
任务框架搭好后,真正决定扫地机能不能动起来的是外设驱动。F103 的外设不算复杂,但扫地机场景对时序和实时性有额外要求,几个细节处理不好就会翻车。
4.1 电机 PWM 与编码器:定时器配置的取舍
左右轮电机一般用 TIM 的 PWM 输出加方向 GPIO,编码器用另一个 TIM 的编码器模式。F103 的通用定时器 TIM2/3/4 都能干这活儿。PWM 频率选 15kHz 到 20kHz 之间,低于 10kHz 电机会有可闻啸叫,高于 20kHz 又受限于定时器分辨率和驱动芯片响应。以 72MHz 时钟、TIM 预分频 0 为例,20kHz 对应自动重装值 3600,分辨率够用。编码器模式把 TIM 配成编码器接口,硬件自动计数,CPU 零负担,这是 F103 上最省资源的做法。
/* TIM3 通道1/2 输出 PWM 驱动左右轮,20kHz */ void MotorPwm_Init(void) { TIM_HandleTypeDef htim3; TIM_OC_InitTypeDef sConfigOC = {0}; htim3.Instance = TIM3; htim3.Init.Prescaler = 0; /* 72MHz 不分频 */ htim3.Init.Period = 3600 - 1; /* 72M/3600 = 20kHz */ htim3.Init.CounterMode = TIM_COUNTERMODE_UP; HAL_TIM_PWM_Init(&htim3); sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 0; /* 初始占空比 0,电机不转 */ sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_2); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2); }逻辑说明:预分频 0 让计数器跑满 72MHz,自动重装 3600 得到 20kHz。Pulse就是占空比,范围 0 到 3600,改这个值就能调速。两个通道分别对应左右轮,方向由另外的 GPIO 控制。
参数说明:Period写 3600-1 是因为计数器从 0 数到重装值,实际周期是重装值加一。占空比别给满,电机驱动芯片一般有最大占空比限制,留 5% 余量。编码器定时器的Period设成 65535,让它自由计数,任务里定期读差值再清零。
4.2 传感器采集的滤波与去抖
悬崖传感器和碰撞传感器是开关量,但机械碰撞有抖动,直接读会误触发。常见做法是软件去抖:连续 N 次读到同一状态才认。悬崖传感器是红外或光电,受地面反光影响,偶尔会误判,除了去抖还要加「连续多次确认」逻辑。陀螺仪是模拟或 I2C 数字输出,原始数据噪声大,必须滤波。一阶低通滤波在 F103 上开销极小,效果够用:
/* 一阶低通滤波,alpha 越小越平滑但延迟越大 */ float LowPassFilter(float newVal, float oldVal, float alpha) { return alpha * newVal + (1.0f - alpha) * oldVal; } /* 陀螺仪角速度滤波,alpha 取 0.2 左右,兼顾平滑和响应 */ float gyro_filtered = 0.0f; void GyroTask_Update(void) { float raw = ReadGyroZ(); gyro_filtered = LowPassFilter(raw, gyro_filtered, 0.2f); }逻辑说明:alpha是滤波系数,取值 0 到 1。取 0.2 意味着新数据占两成、历史占八成,噪声被压下去,但快速转向时会有轻微延迟。参数说明:alpha要根据实际噪声和响应要求调,噪声大就调小,要求响应快就调大。别用移动平均,F103 上存历史数组费 RAM,一阶低通一个变量搞定。
4.3 通信任务:串口和蓝牙指令的解析
扫地机主控通常要跟 WiFi 模组或蓝牙模组通信,收指令、上报状态。串口 ISR 只收字节进队列,解析放任务里。协议一般是带帧头帧尾和校验的定长或变长帧。解析任务从字节队列里取数据,状态机逐字节判断,凑齐一帧再校验、分发。这里最容易踩的坑是队列深度不够导致丢字节,或者解析任务优先级太低被饿死。我的习惯是字节队列给 128 以上深度,解析任务优先级设成中,比导航略低但比电源高。
5. 避坑与排查:这份源码最容易翻车的五个地方
5.1 编译过了但一跑就进 HardFault
现象:工程编译零警告,下载后复位就卡在 HardFault 中断里。原因:九成是栈溢出或堆不够。FreeRTOS 每个任务独立栈,configMINIMAL_STACK_SIZE给太小,任务里一调printf或浮点运算就爆。解决:把configCHECK_FOR_STACK_OVERFLOW设成 2,实现vApplicationStackOverflowHook打印出错任务名,然后逐个把可疑任务的栈加大,浮点任务至少 256 字。
5.2 电机转起来后串口通信丢包
现象:电机不转时串口正常,电机一启动就丢包甚至死机。原因:电机 PWM 和串口共用中断优先级,或者电机电流干扰了电源导致 MCU 复位。解决:先查中断优先级分组,确保串口中断优先级高于或等于电机相关中断,别让电机 ISR 长时间霸占 CPU;再查电源,电机和 MCU 供电要分开滤波,电机端加续流二极管和电容。
5.3 FreeRTOS 延时不准,任务周期漂移
现象:用vTaskDelay做周期任务,实际周期比设定值长。原因:configTICK_RATE_HZ设太低,比如 100Hz,vTaskDelay(1)就是 10ms,想做 1ms 周期根本做不到。解决:把 tick 频率提到 1000Hz,1ms 周期用vTaskDelay(1)才准。但 tick 越高中断开销越大,F103 上 1000Hz 是常见平衡点。
5.4 队列发送失败但没报错
现象:传感器数据偶尔丢失,导航任务收到的位姿不连续。原因:发送用了 0 超时,队列满时直接返回失败,代码没检查返回值。解决:要么加大队列深度,要么在发送失败时做降级处理(比如覆盖最旧数据),至少加个计数器统计丢包次数,别让它无声无息地丢。
5.5 Flash 写入被 FreeRTOS 调度打断
现象:保存参数到 Flash 时偶发数据损坏。原因:Flash 写入期间如果被任务切换打断,或者写入过程中执行了其他 Flash 访问(比如取指令),会导致写入失败。解决:Flash 操作放在临界区里,用taskENTER_CRITICAL包住,或者干脆挂起调度器。注意临界区别太长,Flash 页写入几毫秒,期间中断被关,要评估对电机控制的影响,必要时把参数保存放到停机状态再做。
6. 进阶:把这份源码改成你自己的验证平台
拆完、跑通、避完坑,这份源码最大的价值不是「复刻一台扫地机」,而是当成一个 FreeRTOS 多任务 + 多外设的验证平台。我一般会做三件事把它变成自己的东西。第一,加一个任务运行统计,用vTaskGetRunTimeStats看每个任务实际占了多少 CPU,F103 上跑满 70% 就该警惕了,留余量给突发。第二,把关键任务的栈使用峰值打出来,用uxTaskGetStackHighWaterMark,哪个任务水位低于 20 字就加栈,这是防 HardFault 的后悔药。第三,把路径规划那块抽象成独立模块,接口固定成「输入位姿、输出速度指令」,这样你换算法不用动任务框架。
下面这段代码把运行统计和栈水位一起打出来,接个串口就能看:
/* 在空闲任务钩子里定期打印,别在钩子里做耗时操作 */ void vApplicationIdleHook(void) { static uint32_t last = 0; if (xTaskGetTickCount() - last < pdMS_TO_TICKS(5000)) { return; } last = xTaskGetTickCount(); /* 打印各任务栈剩余水位,单位是字 */ TaskStatus_t tasks[8]; UBaseType_t n = uxTaskGetSystemState(tasks, 8, NULL); for (UBaseType_t i = 0; i < n; i++) { printf("%s stack left: %u\r\n", tasks[i].pcTaskName, tasks[i].usStackHighWaterMark); } }逻辑说明:空闲钩子每 5 秒跑一次,uxTaskGetSystemState一次性拿到所有任务状态,usStackHighWaterMark是历史最小剩余栈,越小越危险。参数说明:tasks数组大小要大于任务数,8 个够一般工程用;printf在钩子里调要确认串口发送不阻塞,否则会拖慢空闲任务,必要时改成 DMA 发送。
我自己的习惯是,任何一份来路不明的 STM32 工程,先花半天把环境跑通、任务划分看懂、栈和堆摸清楚,再决定要不要往里投时间。这份扫地机源码的价值在于它把 FreeRTOS 的典型用法和真实外设揉在了一起,比纯 demo 有嚼头,但也别指望它开箱即用。把它当教材,别当产品。希望帮到你。
本文还有配套的精品资源,点击获取