首页 / 资讯中心 / 文章详情

STM32F103+FreeRTOS最小可运行工程:串口CLI/ADC/LED闭环实战

STM32F103+FreeRTOS最小可运行工程:串口CLI/ADC/LED闭环实战 ★ FEATURED ARTICLE
简介本资源是一个基于STM32F407平台的完整嵌入式C语言开发项目面向嵌入式初学者与进阶开发者聚焦裸机驱动开发、中断处理、内存管理及低功耗设计等核心能力训练。项目涵盖气体检测、主从通信等典型工业应用场景配套Makefile构建系统、ARM汇编启动文件、链接脚本.ld、调试映射文件.map/.elf及实操视频便于理解从编译链接到烧录调试的全流程。压缩包共94个文件以50个头文件.h和29个C源码.c为主体辅以3个目标文件.o、3个链接脚本、1个Makefile、1个Word文档说明、1个MP4演示视频及多个芯片配置文件.ioc/.s/.md总大小15.04MB结构清晰、模块分层明确。已有1671人学习下载读者可直接复现软硬件协同开发过程掌握GPIO/ADC/串口驱动编写、中断服务例程实现、Flash/RAM内存布局配置等关键技能并通过视频与文档快速定位常见编译与运行问题。1. 嵌入式项目一个能烧进STM32F103C8T6、带FreeRTOS任务调度、串口命令行交互、ADC采样LED反馈闭环的完整可运行工程你手上有块蓝色小板子俗称“蓝 pill”主控 STM32F103C8T6J-Link 或 ST-Link 已接好但打开 IDE 后面对一整个 HAL 库 CMSIS FreeRTOS Middlewares 的文件夹树连 main.c 都不敢改——这不是你技术不行是嵌入式项目天然存在「启动即崩溃」的玄学门槛。这个「嵌入式项目」就是专为这种场景设计的它不讲原理图、不画 PCB、不扯 RTOS 调度算法推导而是直接给你一个从 Keil MDK 或 STM32CubeIDE 双环境验证过的最小可运行工程上电后串口打印 ready输入led on灯亮adc read返回当前 PA0 电压值毫伏级task list显示 3 个 FreeRTOS 任务状态。所有外设初始化封装成bsp_xxx_init()函数中断服务函数全在stm32f1xx_it.c里留了干净钩子没有魔数、没有隐藏宏、没有依赖未公开 SDK 版本。适合刚学完《C 语言程序设计》想落地第一块 MCU 的开发者也适合需要快速复现传感器采集逻辑的嵌入式老手——它不是教学模板是能焊到你硬件上、跑通再改的生产级起点。2. 工程结构与核心模块拆解从 startup.s 到 freertos_task.c 的真实链路2.1 目录结构说明为什么Core/Inc和Core/Src必须严格分离该工程采用 STM32CubeMX 生成骨架后手工精简的结构共 7 个一级目录无冗余中间件EmbeddedProject/ ├── Drivers/ # 标准外设库仅 STM32F1xx_HAL_Driver不含 LL 库 ├── Core/ │ ├── Inc/ # 所有 .h 文件bsp_led.h、bsp_adc.h、cli.h、freertos_config.h │ └── Src/ # 所有 .c 文件main.c、stm32f1xx_hal_msp.c、freertos_task.c、cli_cmd.c ├── Middleware/ │ └── FreeRTOS/ # 官方 V10.4.6 源码仅 Tasks、Queue、Timers、EventGroups 四个核心目录 ├── Startup/ # 启动文件startup_stm32f103xb.sKeil和 startup_stm32f103xb.sGCC 兼容版 ├── User/ # 用户应用层adc_sensor.c、led_control.c、system_monitor.c ├── Inc/ # 全局头文件main.h、stm32f1xx_hal_conf.h、stm32f1xx_it.h └── Src/ # 全局源文件system_stm32f1xx.c、stm32f1xx_it.c、main.c入口提示Core/Inc下的头文件全部用#pragma once且无extern C包裹纯 C 项目避免 Keil 与 GCC 编译器差异导致的链接失败User/目录下每个.c文件都对应一个功能闭环如adc_sensor.c封装采样、滤波、单位换算三步不依赖Core/Src以外的任何源文件方便抽离复用。2.2 启动流程关键点从复位向量到 FreeRTOS 调度器启动的四步跳转嵌入式项目最易翻车的环节不在业务逻辑而在启动链。本工程明确固化以下四步复位后执行Reset_HandlerStartup/startup_stm32f103xb.s初始化栈指针 → 拷贝.data段到 RAM → 清零.bss段 → 调用SystemInit()位于system_stm32f1xx.cSystemInit()配置系统时钟强制使用 HSE外部晶振 8MHz经 PLL 倍频至 72MHz禁用内部 RC 振荡器RCC-CR ~RCC_CR_HSION避免不同批次芯片 RC 偏差导致 UART 波特率漂移。main()中完成硬件抽象层初始化int main(void) { HAL_Init(); // 初始化 HAL 库时间基准SysTick SystemClock_Config(); // 调用上面第2步的时钟配置 MX_GPIO_Init(); // LED、按键等 GPIO MX_ADC1_Init(); // ADC1 单通道 PA0 连续采样 MX_USART1_UART_Init(); // USART1 115200bps无流控 bsp_led_init(); // 封装 LED 控制GPIOx, PinY bsp_adc_init(); // 封装 ADC 启动与 DMA 配置 cli_init(); // 初始化命令行解析器环形缓冲区状态机 freertos_task_create_all(); // 创建所有 FreeRTOS 任务见 2.3 节 vTaskStartScheduler(); // 启动调度器 —— 此后永不返回 }vTaskStartScheduler()后进入 SVC 中断由 FreeRTOS 内核接管此时main()上下文被压入初始任务栈CPU 完全交由xPortPendSVHandler和xPortSysTickHandler管理。注意本工程禁用configUSE_TIMERS未启用软件定时器所有延时均用vTaskDelay()避免 SysTick 被多层抢占。2.3 FreeRTOS 任务设计三个硬实时任务的职责与优先级分配任务定义在Core/Src/freertos_task.c全部使用静态内存分配xTaskCreateStatic()规避动态内存碎片风险任务名优先级栈大小字核心职责关键同步机制cli_task3512从 USART1 接收字符解析命令调用cmd_handler()执行动作如led on串口空闲中断 二值信号量adc_task2384每 100ms 触发一次 ADC 采样计算均值后存入全局g_adc_mv变量ADC 转换完成中断 任务通知led_blink_task1256每 500ms 翻转 LED同时检查g_system_health标志决定是否快闪告警vTaskDelay()// Core/Src/freertos_task.c 片段 StaticTask_t xCLI_TaskBuffer; StackType_t xCLI_TaskStack[512]; TaskHandle_t xCLI_Handle; void cli_task(void *pvParameters) { for(;;) { if (xSemaphoreTake(xCLI_Semaphore, portMAX_DELAY) pdTRUE) { cli_parse_input(); // 解析环形缓冲区中的命令 } } } void freertos_task_create_all(void) { xCLI_Handle xTaskCreateStatic( cli_task, CLI, 512, NULL, 3, xCLI_TaskStack, xCLI_TaskBuffer ); // ... 其他任务同理 }注意xCLI_Semaphore在cli_init()中创建由USART1_IRQHandler在检测到空闲中断IDLE line detect时释放确保命令输入完整后再解析避免字符粘包。3. 编译与烧录实操指南Keil MDK 与 STM32CubeIDE 双环境适配3.1 Keil MDKv5.37配置要点必须关闭的三个默认选项本工程在 Keil 下已预配置好 DeviceSTM32F103C8、PackKeil.STM32F1xx_DFP.2.4.0、DebugST-Link Debugger。但新手常因以下三项默认开启而烧录失败Options for Target → Output → Create HEX File必须勾选否则无法用 ST-Link Utility 烧录本工程提供.hex作为备用方案Options for Target → C/C → Define只保留USE_HAL_DRIVER, STM32F103xB务必删除__STARTUP_CLEAR_BSS和__STARTUP_COPY_MULTIPLE—— 这两个宏会干扰startup_stm32f103xb.s中的手动.bss清零逻辑导致全局变量未初始化Options for Target → Linker → Use Memory Layout from Target Dialog必须取消勾选改用工程根目录下的STM32F103C8Tx_FLASH.ldGCC 链接脚本或STM32F103C8Tx_FLASH.sctKeil 散列文件—— 本工程已提供二者Keil 使用.sctCubeIDE 使用.ld。提示若编译报错L6218E: Undefined symbol xxx90% 是因为HAL_GPIO_WritePin()等函数未链接stm32f1xx_hal_gpio.c请检查Target → Files中是否已添加该文件默认已加但手动删过需补回。3.2 STM32CubeIDEv1.13导入与调试如何绕过 CubeMX 重生成污染CubeIDE 原生支持.project和.cproject但本工程为避免 CubeMX 自动生成代码覆盖人工优化不包含.ioc文件。导入步骤如下File → Import → General → Existing Projects into Workspace选择工程根目录勾选Copy projects into workspace防止路径污染右键项目 →Properties → C/C Build → Settings → Tool Settings → MCU SettingsDevice:STM32F103C8TxFlash size:64 KBRAM size:20 KB取消勾选Generate peripheral initialization code using CubeMX关键C/C Build → Settings → Tool Settings → MCU GCC Compiler → Includes确认-I../Drivers/STM32F1xx_HAL_Driver/Inc和-I../Core/Inc已存在Run → Debug Configurations → STM32 Cortex-M C/C Application选择你的 ST-LinkDebugger 标签页中勾选Connect under reset和Reset and Run。注意首次调试时若提示No source available请右键main.c→Debug As → Debug Configurations→Source标签页 →Add→Workspace→ 勾选当前项目否则调试器找不到源码映射。3.3 烧录验证三步法从串口回显到 ADC 实测的逐级确认不要一上来就烧整个固件按顺序验证每层基础通信层验证耗时 30 秒烧录后打开串口工具推荐 Tera Term 或 XShell波特率 115200无校验1 停止位。上电瞬间应看到 ready若无输出立即检查① USB-TTL 模块是否接在 PA9/PA10非 PB6/PB7②MX_USART1_UART_Init()中huart1.Init.BaudRate 115200是否被误改③ 板载 3.3V 是否稳定万用表测 TP1 对 GND。LED 控制层验证命令级输入led on回车板载 LED通常为 PC13应点亮输入led off应熄灭。若无效检查bsp_led_init()中__HAL_RCC_GPIOC_CLK_ENABLE()是否被注释或HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET)参数是否写反SET高电平点亮部分开发板低电平点亮需改RESET。ADC 采样层验证物理量级用万用表测 PA0 对 GND 电压建议接 1kΩ 电位器分压输入adc read返回值应为ADC: 2485 mV类似格式。若偏差 ±50mV检查HAL_ADC_ConfigChannel()中sConfig.Channel ADC_CHANNEL_0PA0是否误配为ADC_CHANNEL_1PA1或HAL_ADCEx_Calibration_Start()是否被跳过校准失败会导致增益误差。4. 常见问题排查五个血泪经验总结出的必踩坑点4.1 现象串口打印乱码如 re?dy或 rady原因系统时钟配置错误导致 USART 波特率计算偏差。本工程强制使用 HSE8MHz 经 PLL9 倍频得 72MHz若实际晶振虚焊或电路未起振HAL_RCC_GetSysClockFreq()返回值为 0huart1.Init.PeriphClk RCC_PERIPHCLK_USART1会按默认 16MHz 计算波特率寄存器造成 4.5 倍误差。解决用示波器测 OSC_INPH0引脚是否有 8MHz 正弦波若无检查晶振两端 12pF 负载电容是否焊接、晶振型号是否为 8MHz ±20ppm临时方案在SystemClock_Config()中将RCC_OscInitStruct.PLL.PLLMUL RCC_PLL_MUL9改为RCC_PLL_MUL6得 48MHz重新计算波特率。4.2 现象freertos_task_create_all()后程序卡死vTaskStartScheduler()不返回原因FreeRTOS 堆栈溢出或 SysTick 中断未使能。本工程使用configTOTAL_HEAP_SIZE 8192字节静态堆若任务栈总和超限如误将led_blink_task栈设为 1024xTaskCreateStatic()返回NULL但代码未检查直接调用vTaskStartScheduler()内核因无有效任务而挂起。解决在freertos_task_create_all()每个xTaskCreateStatic()后加判断if (xCLI_Handle NULL) { Error_Handler(); // 此处可点亮红灯或死循环 }同时确认HAL_Init()中HAL_IncTick()被正确调用且SysTick_Config()返回非零值。4.3 现象adc read始终返回0或固定值如4095原因ADC 通道未正确连接或采样时间不足。STM32F1 系列 ADC 最小采样时间为 1.5 周期对 1μs 采样需 ≥1.5μs若sConfig.SamplingTime ADC_SAMPLETIME_1CYCLE_5但系统时钟未达 72MHz实际采样时间不足导致转换失败。解决在MX_ADC1_Init()中显式设置sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; // 最长采样时间兼容低速时钟并确认HAL_ADC_Start_DMA()的Length参数为 1单次采样而非BUFFERSIZEDMA 循环模式需额外配置。4.4 现象Keil 编译报错Error: L6218E: Undefined symbol HAL_Delay原因HAL_Delay()依赖HAL_GetTick()而后者需HAL_IncTick()在 SysTick 中断中调用。本工程stm32f1xx_it.c中SysTick_Handler()被注释或未调用HAL_IncTick()。解决打开stm32f1xx_it.c确认SysTick_Handler()内容为void SysTick_Handler(void) { HAL_IncTick(); osSystickHandler(); // FreeRTOS 的 SysTick 处理 }且main()中HAL_Init()必须在vTaskStartScheduler()之前调用。4.5 现象CubeIDE 下调试时断点无法命中freertos_task.c中的函数原因CubeIDE 默认启用Optimize for Size (-Os)导致函数内联或变量优化掉调试信息丢失。解决Properties → C/C Build → Settings → Tool Settings → MCU GCC Compiler → Optimization将Optimization Level改为-O0无优化重新编译。发布版本再切回-Os。5. 二次开发实战如何安全接入 DHT22 温湿度传感器并上报串口5.1 硬件接入规范单总线协议的物理层约束DHT22 为单总线器件需严格满足时序要求启动信号 80μs 低 80μs 高响应信号 80μs 低 80μs 高。STM32F103C8T6 的 GPIO 输出速度最高 50MHz但普通推挽模式翻转延迟约 100ns不能直接用HAL_GPIO_WritePin()模拟时序。本工程采用TIM2 PWM 输出 GPIO 复用方案将 DHT22 数据线接 PB10TIM2_CH3 复用功能在MX_TIM2_Init()中配置 TIM2 为 PWM 模式Prescaler0Counter Period7172MHz/721MHz即 1μs 分辨率通过__HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_3, pulse_width)动态设置高低电平持续时间读取响应时将 PB10 切换为浮空输入模式GPIO_MODE_INPUT用HAL_GPIO_ReadPin()采样。提示DHT22 启动后需等待 80μs 再拉高总线此期间 CPU 不能被中断打断故在dht22_start()中插入__disable_irq()响应读取完成后__enable_irq()。5.2 软件模块封装dht22.c的三层抽象设计为避免污染核心任务新建User/dht22.c并遵循以下分层驱动层dht22_hal.c纯硬件操作只含dht22_init()、dht22_start_signal()、dht22_read_byte()不依赖 FreeRTOS中间层dht22_core.c实现 CRC 校验、数据解析、温度/湿度单位换算返回typedef struct { float temp; float humi; } dht22_data_t应用层dht22_task.c创建独立 FreeRTOS 任务优先级设为 2与adc_task同级每 2 秒执行一次dht22_read(data)成功则调用cli_printf(DHT22: %.1fC %.1f%%\r\n, data.temp, data.humi)。// User/dht22_core.c 片段 dht22_err_t dht22_read(dht22_data_t *out) { uint8_t raw[5] {0}; if (dht22_hal_read(raw) ! DHT22_OK) return DHT22_ERR_TIMEOUT; if ((raw[0] raw[1] raw[2] raw[3]) ! raw[4]) return DHT22_ERR_CRC; out-humi (raw[0] 8 | raw[1]) * 0.1f; // 湿度 ×0.1 out-temp (raw[2] 8 | raw[3]) * 0.1f; // 温度 ×0.1 return DHT22_OK; }5.3 CLI 命令扩展在不修改原有命令解析器的前提下注入新指令本工程 CLI 使用状态机解析扩展命令只需两步在Core/Inc/cli.h中新增命令枚举typedef enum { CMD_LED, CMD_ADC, CMD_TASK_LIST, CMD_DHT22_READ, // 新增 CMD_UNKNOWN } cli_cmd_t;在Core/Src/cli_cmd.c的cli_cmd_handler()中添加分支case CMD_DHT22_READ: dht22_data_t data; if (dht22_read(data) DHT22_OK) { cli_printf(DHT22: %.1fC %.1f%%\r\n, data.temp, data.humi); } else { cli_printf(DHT22: read failed\r\n); } break;注意dht22_read()是阻塞函数若放在cli_task优先级 3中执行会阻塞命令行响应。因此实际实现中CMD_DHT22_READ仅向dht22_task发送xTaskNotify()由其异步执行并回调cli_printf()保证 CLI 响应实时性。6. 从那以后我每次接入新传感器都强制走一遍「三阶验证法」第一次把 DHT22 接上板子时我花了 3 小时在示波器前看波形启动信号宽度是对的但响应信号的 80μs 低电平变成了 120μs导致后续数据位全错。最后发现是HAL_GPIO_ReadPin()采样间隔太长改用__NOP()空循环精准延时才搞定。这件事让我养成了现在写嵌入式驱动的铁律任何外设接入必须过三关。第一关叫「裸机波形关」不跑 FreeRTOS不启中断用while(1)循环模拟单总线时序用示波器抓 DQ 线波形确保启动、响应、数据位的宽度、边沿陡峭度 100% 符合 datasheet。这一关过了硬件连接和基础时序就算稳了。第二关叫「中断精度关」把时序控制交给 TIM 或 SysTick用中断触发高低电平翻转再用示波器对比中断响应延迟比如HAL_TIM_Base_Start_IT()到 GPIO 翻转的时间。STM32F1 的中断延迟典型值是 12 个周期若实测超 20 周期就得查是不是有更高优先级中断在抢资源。第三关叫「RTOS 协同关」把驱动封装成非阻塞接口如dht22_start_conversion()dht22_is_done()在任务中用vTaskDelayUntil()控制采集周期用队列或通知量传递结果。这时要特别注意绝不在中断里调用xQueueSendFromISR()以外的任何 FreeRTOS API否则可能触发assert_failed()。这三关看起来慢但省去了后期 90% 的玄学问题。比如上周同事的项目温湿度数据偶尔跳变查了两天发现是dht22_read()里用了HAL_Delay(1)而HAL_Delay()依赖 SysTick但 SysTick 被vTaskDelay()抢占导致计时不稳——这就是没过第二关的典型后遗症。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站