基于STM32与FreeRTOS的智能水箱水位监测系统设计与实现

📅 2026/8/19 6:08:28
基于STM32与FreeRTOS的智能水箱水位监测系统设计与实现
1. 项目缘起为什么需要一个基于RTOS的智能水箱水位指示器几年前我接手过一个农业灌溉系统的维护项目其中一个核心痛点就是储水罐的水位监测。当时用的是简单的浮球开关配合继电器只能提供“高”和“低”两个极限报警点。灌溉高峰期工人需要频繁跑到现场查看水位或者依赖不可靠的无线模块发送的简单状态经常出现水箱空了水泵还在干抽或者水满了还在继续注水导致溢出的情况。这不仅浪费水电对设备损耗也很大。后来尝试用Arduino加超声波模块做个简易的指示器但发现一旦要同时处理超声波测距、数据显示、无线通信和逻辑控制那个简单的loop()函数很快就变得臃肿不堪响应迟钝还容易因为某个阻塞操作比如等待串口导致整个系统“卡住”。这正是实时操作系统RTOS大显身手的地方。这次我决定用更专业的组合——STM32微控制器和FreeRTOS——来重新设计这个智能水箱水位指示器。STM32提供了丰富的定时器、ADC和通信接口性能足够而FreeRTOS则能将测距、显示、通信、控制逻辑分解成独立的任务让它们并行、有序地运行互不干扰。这个项目不只是把传感器数据读出来更是构建一个可靠、实时、可扩展的小型嵌入式系统。如果你也在为如何管理多个外设、确保系统实时性而头疼或者想从裸机编程迈向RTOS那么这次从硬件选型到软件架构的完整实战应该能给你不少启发。2. 核心硬件选型与电路设计思路一个项目的硬件是骨架选型决定了系统的能力边界和成本。对于水箱水位监测我们需要考虑测量精度、环境适应性、人机交互和远程监控需求。2.1 主控芯片为什么是STM32F103C8T6在众多STM32型号中我选择了经典的“蓝色药丸”——STM32F103C8T6。原因很实际性能与资源平衡基于Cortex-M3内核主频72MHz完全能满足FreeRTOS和多个任务运行的需求。它拥有64KB Flash和20KB SRAM对于我们这个项目包含RTOS内核、多个任务、外设驱动和业务逻辑绰绰有余。外设丰富我们需要至少一个定时器用于超声波模块的脉冲计时、一个USART用于调试输出或连接无线模块、一个I2C或SPI用于连接OLED显示屏以及若干GPIO。STM32F103C8T6完全满足甚至还有富余。生态与成本这款芯片资料极其丰富社区支持好价格也亲民。无论是用STM32CubeMX初始化还是直接操作寄存器都有海量的参考。2.2 水位检测HC-SR04超声波模块的利与弊最常见的非接触式液位检测就是超声波模块。HC-SR04价格低廉使用简单但其工作原理决定了使用时必须注意细节。工作原理向Trig引脚发送一个至少10us的高电平脉冲模块会自动发射8个40kHz的超声波脉冲并检测回波。当接收到回波时Echo引脚会输出一个高电平其持续时间与距离成正比。距离 (高电平时间 * 声速340m/s) / 2。为什么选择它非接触式无需侵入水体避免了腐蚀、污染和结垢问题尤其适合水质复杂或需要卫生防护的场景。成本极低对于精度要求不极端厘米级的应用它是性价比最高的方案。必须注意的坑实操心得测量盲区HC-SR04通常有2cm-3cm的盲区模块不能紧贴水面安装必须留出至少3-4cm以上的距离。波束角超声波有一定的扩散角约15度。这意味着如果水箱壁不平滑或者有障碍物容易产生杂波反射导致测量值跳动。安装时尽量让模块正对液面并远离水箱内壁和进水/出水口。环境干扰泡沫、蒸汽、强气流会影响声波传播。在水箱顶部这个相对封闭、稳定的环境里影响较小但设计时仍需考虑。电路连接VCC- 5V (模块工作电压4.5-5.5VSTM32的3.3V可能无法驱动需电平转换或单独5V供电)GND- GNDTrig- 连接STM32的某个GPIO如PA1配置为推挽输出。Echo- 连接STM32的某个GPIO如PA2配置为浮空输入或上拉输入用于捕获高电平脉冲。注意Echo脚输出是5V电平不能直接接STM32的3.3V GPIO否则可能损坏芯片必须使用电阻分压如1kΩ和2kΩ或电平转换芯片如TXS0108E降到3.3V。2.3 人机交互0.96寸OLED显示屏SSD1306我们需要一个本地显示设备实时查看水位、系统状态。0.96寸的I2C接口OLED屏是绝佳选择功耗低、显示清晰、接口简单。连接与驱动VCC- 3.3VGND- GNDSCL- STM32的I2C1时钟线PB6SDA- STM32的I2C1数据线PB7在FreeRTOS中对OLED的写操作应放在一个独立的任务中或者通过队列将需要显示的数据发送给显示任务。避免在多个任务中直接调用显示函数防止I2C总线访问冲突。2.4 通信接口USART2的预留设计项目标题和热词都提到了USART2。在这里我们可以将其设计为系统的“调试与扩展通道”。调试输出在开发阶段通过USB转TTL模块连接USART2PA2-TX, PA3-RX在PC端使用串口助手如Putty、SecureCRT打印任务状态、传感器数据、系统日志这是RTOS调试的利器。未来扩展预留USART2可以轻松接入Wi-Fi模块如ESP-01S、4G模块或LoRa模块将水位数据上传到云平台或发送给远程监控中心实现真正的“智能”监控。这也是为什么在系统设计初期就要考虑通信接口。2.5 电源与PCB布局考虑整个系统可以由一个5V/2A的直流电源适配器供电。通过AMS1117-3.3V稳压芯片为STM32和OLED提供3.3V。如果水箱现场只有12V或24V电源则需要相应的DC-DC降压模块。PCB或洞洞板布局提示将STM32的模拟电源VDDA和数字电源VDD通过磁珠或0Ω电阻隔离并靠近芯片放置去耦电容通常为100nF 10uF。HC-SR04的电源走线要粗一些因为其发射超声波瞬间电流较大。如果使用无线模块天线部分要严格按照模块手册布局远离模拟电路和时钟线。3. FreeRTOS任务分解与软件架构设计这是项目的核心。裸机编程是“一切尽在掌握”的轮询而RTOS是“各司其职”的协作。我们的目标是创建一个响应及时、模块清晰、易于维护的系统。3.1 任务划分单一职责原则根据系统功能我划分了四个主要任务并为每个任务分配了合理的优先级。优先级数字越大优先级越高FreeRTOS默认配置。DistanceMeasure_Task(优先级: 3)职责驱动HC-SR04模块完成一次完整的测距并将结果写入一个共享变量或发送到消息队列。实现要点任务主体是一个无限循环每次循环开始先触发Trig引脚然后挂起任务等待一个信号量或直接使用vTaskDelay进行间隔测量如每500ms测量一次。使用信号量可以由定时器中断来精确触发测量周期。测量Echo高电平时间。这里有两种方法a) 使用输入捕获模式精度高但占用定时器资源b) 使用简单的GPIO读取配合FreeRTOS的xTaskGetTickCount()计算时间差对于厘米级精度足够。我选择方法b更简单。计算出的距离值单位cm通过一个线程安全的队列QueueHandle_t发送给显示任务和通信任务。Display_Task(优先级: 2)职责从队列中获取水位数据刷新OLED显示屏。实现要点任务阻塞在队列接收函数上xQueueReceive一旦收到新数据就调用OLED驱动库进行显示。显示内容可以包括当前水位厘米或百分比、水箱总高度、水位状态正常/低/高、系统运行时间等。注意I2C通信本身不是线程安全的因此要确保整个显示更新过程不被其他任务打断。可以通过互斥信号量SemaphoreHandle_t保护I2C总线或者更简单地将所有显示操作都集中在这个任务中。Comm_Task(优先级: 2)职责处理USART2通信。目前主要实现调试日志输出未来可扩展为数据上传。实现要点可以创建一个日志队列。其他任何任务如测距任务、系统监控任务都可以将格式化的日志字符串指针发送到这个队列。Comm_Task从队列中取出日志通过USART2发送出去。这样避免了多个任务同时操作串口造成的数据错乱。未来集成无线模块时此任务可以负责组包、添加协议头、处理重发等网络逻辑。Control_Task(优先级: 4)职责根据水位数据执行控制逻辑。这是系统的“大脑”。实现要点从队列获取水位数据。实现简单的状态机例如水位低于20%时通过一个GPIO点亮红色LED或触发继电器启动水泵水位高于90%时点亮黄色LED或触发继电器关闭进水阀水位在20%-90%之间为绿色正常状态。可以加入 hysteresis迟滞逻辑防止在临界点附近频繁开关。例如低于20%开启水泵但直到水位上升到30%才关闭水泵避免震荡。3.2 内核对象数据交换与同步的桥梁FreeRTOS提供了丰富的内核对象来协调任务。队列Queue我们至少需要两个。xDistanceQueue用于从DistanceMeasure_Task向Display_Task和Control_Task传递距离数据。数据类型可以是一个简单的uint16_t厘米数也可以是一个结构体包含时间戳、状态等。xLogQueue用于从各个任务向Comm_Task传递日志字符串。注意传递的是字符串指针要确保指针所指的内存空间有效通常使用静态或全局数组。信号量SemaphorexI2CSemaphore二进制信号量用作互斥锁。当Display_Task或未来其他需要操作I2C的任务如读取其他I2C传感器访问总线前先获取这个信号量操作完成后释放。保证同一时间只有一个任务使用I2C。xMeasurementSemaphore二进制信号量。可以由一个硬件定时器中断服务程序ISR定期释放DistanceMeasure_Task等待这个信号量。这样可以实现精确的定时测量如每1秒一次而不受任务调度和vTaskDelay精度的影响。软件定时器Software Timer可以创建一个软件定时器定期执行一些轻量级操作比如闪烁一个心跳LED指示系统运行正常。3.3 系统初始化流程在main函数中硬件和RTOS的初始化顺序至关重要int main(void) { // 1. HAL库初始化如果使用CubeMX/HAL HAL_Init(); // 2. 系统时钟配置72MHz SystemClock_Config(); // 3. 初始化所有用到的外设GPIO、定时器、I2C1、USART2等 MX_GPIO_Init(); MX_I2C1_Init(); MX_USART2_UART_Init(); // ... 初始化HC-SR04的Trig/Echo GPIO // 4. 创建FreeRTOS内核对象先于任务创建 xDistanceQueue xQueueCreate(5, sizeof(uint16_t)); // 队列深度5存储uint16_t xLogQueue xQueueCreate(10, sizeof(char*)); // 存储字符串指针 xI2CSemaphore xSemaphoreCreateBinary(); xSemaphoreGive(xI2CSemaphore); // 初始化为可用状态 xMeasurementSemaphore xSemaphoreCreateBinary(); // 5. 创建所有任务 xTaskCreate(DistanceMeasure_Task, Measure, 128, NULL, 3, NULL); xTaskCreate(Display_Task, Display, 128, NULL, 2, NULL); xTaskCreate(Comm_Task, Comm, 256, NULL, 2, NULL); // 通信任务栈设大点 xTaskCreate(Control_Task, Control, 128, NULL, 4, NULL); // 6. 启动调度器永不返回 vTaskStartScheduler(); while (1) { } }4. 关键代码实现与避坑指南理论说完来看具体代码实现。这里我会聚焦几个最容易出错的环节。4.1 HC-SR04测距任务的精准实现测距的准确性是整个系统的基础。下面是一个使用GPIO轮询方式的DistanceMeasure_Task示例void DistanceMeasure_Task(void *pvParameters) { uint32_t startTick, endTick; float distance_cm; uint16_t distance_to_send; const TickType_t xDelay pdMS_TO_TICKS(500); // 测量间隔500ms for(;;) { // 等待测量信号量如果使用定时器触发或简单延时 // xSemaphoreTake(xMeasurementSemaphore, portMAX_DELAY); vTaskDelay(xDelay); // 1. 触发测距Trig高电平至少10us HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET); ets_delay_us(12); // 自定义微秒延时函数可用SysTick或空循环实现 HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET); // 2. 等待Echo变为高电平超声波发出 while(HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) GPIO_PIN_RESET); startTick xTaskGetTickCount(); // 记录开始时刻 // 3. 等待Echo变为低电平回波接收 while(HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) GPIO_PIN_SET); endTick xTaskGetTickCount(); // 记录结束时刻 // 4. 计算高电平持续时间Tick数和距离 // 注意xTaskGetTickCount()返回的是RTOS的Tick计数每个Tick的时长由configTICK_RATE_HZ决定通常为1ms uint32_t pulseWidthTicks endTick - startTick; float pulseWidthMs (float)pulseWidthTicks / portTICK_PERIOD_MS; // 转换为毫秒 distance_cm (pulseWidthMs * 0.001 * 340.0 / 2.0) * 100.0; // 距离 时间 * 声速 / 2 // 5. 数据滤波与校验 if(distance_cm 2.0 distance_cm 400.0) { // HC-SR04有效量程通常2cm-400cm // 简单移动平均滤波 static float history[5] {0}; static uint8_t index 0; history[index] distance_cm; index (index 1) % 5; float sum 0; for(int i0; i5; i) sum history[i]; distance_cm sum / 5.0; distance_to_send (uint16_t)distance_cm; // 6. 发送到队列 if(xQueueSend(xDistanceQueue, distance_to_send, 0) ! pdPASS) { // 发送失败队列可能已满可以记录日志 char* logMsg Distance Queue Full!\n; xQueueSend(xLogQueue, logMsg, 0); } } else { // 无效数据记录错误 char* logMsg Invalid Distance Reading!\n; xQueueSend(xLogQueue, logMsg, 0); } } }避坑指南ets_delay_us的实现HAL库的HAL_Delay()是毫秒级。实现微秒延时通常用空循环但要注意编译器优化。一个简单的方法是使用SysTick定时器或者利用STM32的DWT数据观察点跟踪单元中的CYCCNT计数器这是最准的。优先级与阻塞DistanceMeasure_Task中两个while循环等待Echo引脚变化这是忙等待会阻塞整个任务。如果Echo引脚因故障始终为高或低任务将永远卡住。因此这个任务的优先级不宜设得太高避免影响其他低优先级任务。更好的方法是使用输入捕获中断在中断中记录时间完全消除忙等待。Tick计数溢出xTaskGetTickCount()是32位计数器大约每49.7天假设1ms Tick会溢出归零。如果一次Echo高电平时间极长虽然不太可能endTick - startTick的计算在溢出时会出错。对于测距这种短时间操作可以忽略。但对于长时间间隔应使用xTaskGetTickCountFromISR()并处理溢出逻辑。4.2 显示任务与I2C互斥访问显示任务需要安全地操作共享资源——I2C总线。void Display_Task(void *pvParameters) { uint16_t current_distance 0; char display_buffer[32]; // 初始化OLED OLED_Init(); // 这个函数内部会进行I2C通信 for(;;) { // 阻塞等待新的距离数据 if(xQueueReceive(xDistanceQueue, current_distance, portMAX_DELAY) pdPASS) { // 准备显示内容 float water_level_percent 100.0 * (TANK_HEIGHT - current_distance) / TANK_HEIGHT; snprintf(display_buffer, sizeof(display_buffer), Level: %3d cm, current_distance); // 或者显示百分比: Level: %2.0f%%, water_level_percent // 获取I2C互斥锁确保显示过程不被其他任务打断 if(xSemaphoreTake(xI2CSemaphore, pdMS_TO_TICKS(100)) pdTRUE) { OLED_Clear(); OLED_ShowString(0, 0, (uint8_t *)Water Tank Monitor, 16); OLED_ShowString(0, 2, (uint8_t *)display_buffer, 16); // 可以添加更多信息如状态栏 if(water_level_percent 20.0) { OLED_ShowString(0, 4, (uint8_t *)Status: LOW!, 16); } else if (water_level_percent 90.0) { OLED_ShowString(0, 4, (uint8_t *)Status: HIGH!, 16); } else { OLED_ShowString(0, 4, (uint8_t *)Status: Normal, 16); } OLED_Refresh(); // 更新显示到屏幕 xSemaphoreGive(xI2CSemaphore); // 释放锁 } else { // 获取信号量超时说明I2C总线被长时间占用记录错误 char* logMsg Display: I2C Busy Timeout!\n; xQueueSend(xLogQueue, logMsg, 0); } } } }实操心得互斥信号量超时xSemaphoreTake设置了100ms超时。这是非常重要的防御性编程。如果Control_Task或其他任务占用了I2C锁却因为某种原因崩溃没有释放显示任务不会永远死等而是超时后可以记录错误系统部分功能受损但不会完全僵死。OLED驱动库网上有大量针对SSD1306的STM32驱动代码。选择一个并适配到你的工程中。注意这些驱动库里的I2C_Write函数需要用我们封装好的、受信号量保护的版本替换。4.3 串口通信任务的异步日志处理让串口打印变得线程安全且非阻塞是RTOS调试的必备技能。void Comm_Task(void *pvParameters) { char* receivedLogMsg; // 初始化USART2... printf(System Booted. FreeRTOS Scheduler Started.\n); // 重定向printf到USART2 for(;;) { // 阻塞等待日志消息 if(xQueueReceive(xLogQueue, receivedLogMsg, portMAX_DELAY) pdPASS) { // 安全地通过串口发送字符串 HAL_UART_Transmit(huart2, (uint8_t*)receivedLogMsg, strlen(receivedLogMsg), HAL_MAX_DELAY); // 注意这里使用了HAL_MAX_DELAY意味着如果串口发送被阻塞如线被拔掉这个任务也会被阻塞。 // 对于更可靠的设计应该使用DMA发送或者设置一个合理的超时时间。 } } } // 一个方便的日志封装函数可以被任何任务调用 void log_printf(const char* format, ...) { static char log_buffer[128]; va_list args; va_start(args, format); int len vsnprintf(log_buffer, sizeof(log_buffer), format, args); va_end(args); if(len 0) { // 注意这里传递的是log_buffer的地址。由于log_buffer是函数内静态变量 // 所有任务共用同一个缓冲区。如果多个任务同时调用log_printf内容会被覆盖。 // 更安全的做法是动态分配内存或使用线程本地存储。对于小型项目可以接受此风险 // 因为队列传递很快或者通过临界段保护。 taskENTER_CRITICAL(); // 进入临界区防止多任务同时调用 xQueueSend(xLogQueue, log_buffer, 0); taskEXIT_CRITICAL(); } }关键点内存管理上面log_printf的实现有潜在风险。更健壮的做法是在log_printf中动态分配pvPortMalloc一块内存来存放格式化后的字符串然后将这块内存的指针送入队列。在Comm_Task中发送完毕后必须释放vPortFree这块内存。否则会导致内存泄漏。串口发送阻塞HAL_UART_Transmit使用轮询方式如果硬件故障导致发送失败任务会永远阻塞。可以使用HAL_UART_Transmit_IT中断方式或HAL_UART_Transmit_DMADMA方式让串口发送在后台进行任务只需启动发送即可继续执行。5. 系统调试、优化与问题排查将代码烧录进去只是第一步让系统稳定运行才是挑战的开始。5.1 FreeRTOS相关调试技巧栈溢出检测FreeRTOS的每个任务都有独立的栈。栈溢出是RTOS最常见也最隐蔽的问题。务必在FreeRTOSConfig.h中启用栈溢出检测钩子函数。#define configCHECK_FOR_STACK_OVERFLOW 2然后实现vApplicationStackOverflowHook函数一旦检测到溢出立刻通过串口打印出错的任务名并让系统挂起或重启。void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { (void)xTask; printf([FATAL] Stack Overflow in Task: %s\n, pcTaskName); while(1); // 死循环或触发看门狗复位 }使用uxTaskGetStackHighWaterMark在任务运行一段时间后调用这个函数可以查看任务历史最小剩余栈空间。这是调整任务栈大小的金标准。在调试阶段在每个任务循环里打印一下这个值确保有足够的余量建议至少保留20%的栈空间。任务状态监控通过vTaskList()函数需要启用configUSE_TRACE_FACILITY和configUSE_STATS_FORMATTING_FUNCTIONS可以获取所有任务的名称、状态、优先级、栈高水位等信息并通过串口打印出来。这是分析任务调度是否正常、是否有任务阻塞的利器。5.2 超声波测距的稳定性优化原始的距离数据往往有毛刺。除了前面提到的软件滤波硬件和算法上也可以优化多次测量取中值在DistanceMeasure_Task中每次触发后连续测量5次去掉最大最小值取中间3次的平均值。这能有效抑制偶然的跳变。硬件滤波在HC-SR04的Echo信号线上对地加一个10-100pF的小电容可以滤除一些高频噪声。超时处理在等待Echo高/低电平的while循环中加入超时机制。如果超过一个合理的时间比如100ms对应约17米远超量程就跳出循环认为本次测量失败返回一个错误值。uint32_t timeout xTaskGetTickCount() pdMS_TO_TICKS(100); while(HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) GPIO_PIN_RESET xTaskGetTickCount() timeout); if(xTaskGetTickCount() timeout) { /* 处理超时 */ }5.3 功耗考虑如果项目是电池供电功耗就至关重要。外设电源管理可以通过MOS管控制HC-SR04和OLED的电源在不测量、不显示时彻底断电。任务调度优化将Display_Task的刷新率降低比如每2秒刷新一次。DistanceMeasure_Task的测量间隔也可以根据水位变化快慢动态调整稳定时延长快速变化时缩短。利用FreeRTOS的低功耗模式在idle任务钩子函数vApplicationIdleHook中可以让STM32进入SLEEP或STOP模式。当所有任务都在等待事件如信号量、队列时系统会自动进入idle任务此时触发低功耗模式有中断发生时再唤醒。这需要仔细配置外设的中断唤醒源。5.4 常见问题排查表现象可能原因排查步骤系统上电后无任何反应1. 电源问题2. 晶振未起振3. 启动文件/向量表错误1. 测量各点电压3.3V, 5V2. 用示波器看晶振引脚波形3. 检查Keil/IAR中的启动文件配置、堆栈大小设置只有部分任务运行1. 某个任务崩溃或陷入死循环2. 任务栈溢出3. 优先级设置不当高优先级任务不释放CPU1. 使用vTaskList查看任务状态2. 检查栈高水位标记3. 在任务中增加点灯或打印定位哪个任务卡住超声波测量值恒为0或超大1. Echo引脚电平转换电路故障2. Trig触发脉冲宽度不够3. 声波被吸收或遮挡如水面泡沫1. 用逻辑分析仪或示波器抓取Trig和Echo波形2. 确保Trig脉冲10us3. 调整模块安装位置和角度OLED显示乱码或不显示1. I2C上拉电阻缺失通常4.7kΩ2. I2C地址错误SSD1306通常0x78或0x7A3. 初始化序列不正确1. 检查SCL/SDA线上拉到3.3V2. 用I2C扫描程序确认设备地址3. 核对OLED驱动初始化代码串口无输出1. TX/RX线接反2. 波特率不匹配3. 串口助手的流控设置错误1. 交换TX/RX线试试2. 确认代码和串口助手波特率一致如1152003. 关闭串口助手的RTS/CTS流控6. 项目扩展与进阶思考一个基础系统搭建完成后可以从以下几个方向进行扩展让它更“智能”、更可靠增加无线通信ESP8266/ESP32利用预留的USART2连接一个ESP-01S模块。在STM32上实现一个简单的AT指令解析器定期将水位数据通过ESP8266发送到MQTT服务器如阿里云、腾讯云或私有TCP服务器。这样就能在手机App或网页上远程监控水位。加入更多的传感器水温传感器DS18B20单总线协议可以创建一个新的Temperature_Task来读取数据。水质传感器如pH、浊度通常是模拟输出需要STM32的ADC来读取。创建一个ADCSampling_Task定时采集多个通道的模拟量。这些新传感器的数据都可以通过队列发送给Display_Task和Comm_Task进行统一处理和上报。使用状态机框架如QP管理复杂逻辑如果控制逻辑变得复杂比如多级水泵、阀门联动、定时灌溉裸的if-else会很难维护。可以引入轻量级的状态机框架如QP-nano让每个任务内部的状态迁移更加清晰。移植到其他STM32系列或RTOS这个项目的架构是通用的。你可以很容易地将其移植到性能更强的STM32F4/F7或者资源更省的STM32G0系列。也可以尝试将FreeRTOS替换为RT-Thread、Azure RTOS等比较它们的特点。使用CubeMX图形化配置对于初学者强烈推荐使用STM32CubeMX来初始化引脚、时钟、外设和FreeRTOS。它能自动生成初始化代码和FreeRTOS任务骨架让你专注于业务逻辑避免底层配置错误。这个基于STM32和FreeRTOS的智能水箱水位指示器项目从一个具体的需求出发串联了硬件选型、电路设计、RTOS任务划分、内核对象使用、驱动编写、系统调试等嵌入式开发的核心环节。它不仅仅是一个水位计更是一个展示如何用RTOS构建小型、实时、可靠嵌入式系统的完整范例。在实际部署中你可能还需要考虑防水外壳、防雷击、电源浪涌保护等工程细节。希望这次详细的拆解能为你自己的RTOS项目提供一个坚实的起点。