ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于C语言的STM32智能送药小车源码解析:FreeRTOS任务调度与PWM控制

基于C语言的STM32智能送药小车源码解析:FreeRTOS任务调度与PWM控制 简介面向电赛参赛者基于C语言的STM32智能送药小车完整资料包含全套源码与KEIL工程配置覆盖运动控制、定时器/PWM、Flash存取等模块并整合FreeRTOS任务调度适合备战电子设计竞赛或学习STM32开发的学生。资源共249个文件大小仅7.52MB核心代码由66个头文件与57个C源文件构成既有stm32f10x外设驱动也有FreeRTOS内核实现涵盖任务调度、消息队列等经典内核机制另有axf、hex编译产物可烧录验证sct文件定义存储布局bat脚本可清理KEIL中间文件。全部资料按KEIL MDK工程组织目录清晰易检索。已有350人学习下载可帮助读者快速搭建STM32送药小车平台或拆解电赛完整方案省去从零建工程与排查编译环境的时间。1. 这份基于C语言的智能送药小车源码到底把什么摊开了这份基于C语言的智能送药小车源码压缩包第一眼看到的就是 tasks.c、queue.c、stream_buffer.c 和 stm32f10x_tim.c、stm32f10x_rcc.c、stm32f10x_flash.c 这些文件排在一起。这说明它不是 CubeMX 拉出来的模板工程而是真正用 C 语言把 FreeRTOS 内核、标准外设库和业务任务写在一起的 Keil 工程。芯片定在 STM32F103 这条产品线上整体代码量不大但启动、时钟、Flash 等待周期、定时器 PWM、编码器接口、任务调度、任务间通信全都能在源码里找到对应位置。对准备电子设计竞赛的团队来说这比对着视频敲一遍更有价值想把手写嵌入式 C 和 FreeRTOS 结合到项目中、又不想看太庞大工程的人从这个小车源码切入也顺。2. STM32 启动流程与时钟、Flash 配置先把核心跑起来2.1 从 system_stm32f10x 到 RCC时钟树的选型逻辑工程进来不要急着看任务代码先确认时钟树。stm32f10x_rcc.c 是标准外设库里的时钟控制源码核心职责就两件事开启总线和外设时钟以及配置 PLL、预分频器。电赛小车里常用外设无非 GPIO、TIM、USART、ADC其中 GPIO 挂在 APB2TIM1 挂在 APB2TIM2-TIM7 挂在 APB1USART1 挂在 APB2。注意 APB1 的最高频率是 36MHz而 TIM2-TIM7 的内部时钟在 APB1 分频系数不为 1 时会自动倍频回 72MHz。这个细节直接决定了 PWM 频率计算公式里的分频系数。RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);这段代码里GPIOB 和 USART1 挂在 APB2TIM3 挂在 APB1。我建议在 main 函数里保留 SystemInit 默认的 72MHz 主频只在这里按需开外设时钟。否则你要同时改 Flash 等待周期、SysTick 重装值和串口波特率三个地方同步出错的概率很大。标准外设库函数里APB2 打开 GPIO、ADC、USARTAPB1 打开普通定时器这个对应关系可以靠记忆装置USART1 和 ADC 在高速总线定时器数量多所以在低速总线但内部倍频。下面是这个工程里最常用外设的总线分布外设所属总线注意事项GPIOA-GPIOGAPB2外设时钟挂在 APB2TIM1/TIM8APB2高级定时器72MHz 直接可到TIM2-TIM7APB1分频后内部自动 ×2USART1APB2支持重映射ADC1-3APB2需要额外配置分频提示改完 RCC 配置后最好调用 SystemCoreClockUpdate() 刷新全局变量否则依赖主频计算的 delay 和串口波特率会全部按旧值工作。2.2 stm32f10x_flash.c 与程序存储布局stm32f10x_flash.c 平时没人读但它决定 72MHz 下程序能否稳定运行。FLASH_SetLatency 设置等待周期0-24MHz 用 024-48MHz 用 1超过 48MHz 用 2。电赛小车直接锁 72MHz工程里大概率会调用 FLASH_SetLatency(FLASH_Latency_2)同时打开预取缓冲 FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable)。预取缓冲的作用是让 CPU 在顺序执行时少等 Flash 的读等待打开之后中断响应和任务切换毛刺会小一些。如果删掉这段代码在高主频下会不定时跑飞而且很难复现。工程根目录下的 LED_sct.Bak 是分散加载文件的备份。原来的 .sct 负责把代码段、只读数据段、RW 段分配到片内 Flash 和 RAM。我遇到过一个真实情况把 512KB Flash 的高容量工程直接烧到 64KB 中容量芯片上.sct 里加载区尾地址超出芯片容量程序一跑到后面就 HardFault。检查分散加载文件时重点看第一个 ROM 区域的 Length 是否小于等于芯片 Flash 大小RAM 区域是否小于芯片 SRAM。这个文件只在 Keil 的 Target 选项里配置了使用 Memory Layout from Target Dialog 时才自动生成如果你看到的是 .sct.Bak说明有人手动改过分散加载移植到新型号时要逐一核对首地址和长度。2.3 keilkilll.bat 清理编译中间文件源码包里的 keilkilll.bat 很有用。作用是把 Keil 编译产生的中间文件清掉同时保留源码和工程配置避免出现“改代码没生效跑的还是旧程序”的尴尬。脚本本身很短echo off del /s /q *.o *.d *.crf *.axf *.htm *.dep del /s /q Objects\* Listings\*命令逐条解释第一个 del 递归删除当前目录下所有 .o、.d、.crf、.axf 等编译和链接产物第二个 del 定向清除 Keil 默认输出目录 Objects 和 Listings。它不会碰 .c、.h、.uvprojx也不会删你的源码文件。LED.axf 是上一次编译生成的 ELF 调试文件跑完这个脚本之后目录会干净很多。我一般拿到别人工程后先跑一次然后全量重新编译这样可以确认当前工程是否依赖了某些旧目标文件。如果工程改过输出目录脚本里的 Objects 和 Listings 路径需要同步修改否则会漏删。另外在 Keil 的 Debug 设置里最好把 Reset and Run 选项打开。脚本执行失败最常见的原因是文件被 Keil 锁定关掉 Keil 再跑一遍就是。3. FreeRTOS 任务划分tasks.c、queue.c、stream_buffer.c 的协同方式3.1 送药小车为什么需要实时内核小车裸机开发最头疼的问题是大循环里时序互相打架。速度环需要稳定周期读取编码器循迹传感器希望 10ms 采样一次OLED 刷新是慢操作如果全部串行跑一遍按键和状态翻转会被拖到几十毫秒才响应。送药小车要求送药机构到位检测不能因为屏幕刷新延后所以任务调度比裸机超级循环更合适。tasks.c 提供任务创建、调度和上下文切换queue.c 提供定长消息缓冲stream_buffer.c 提供字节流缓冲。这三个文件组合起来正好覆盖了周期性采集、事件驱动控制、串口命令处理三类典型场景。如果对照嵌入式内核源码看 tasks.c 的调度逻辑你会发现它本质上维护的就是一个就绪链表通过 SysTick 周期触发 PendSV 来做上下文切换。C 语言指针在这里被用到极致TCB 是一个大结构体链表节点嵌入在结构体内部按优先级插入就绪链表。理解了这个你才会明白 FreeRTOS 的“任务栈”本质上是 C 语言内存管理的一部分而不是一个抽象概念。3.2 任务怎么拆循迹、避障、称重、送药我习惯按外设类型和时序把任务拆成四个循迹、电机、显示、送药。循迹任务读取灰度传感器把路径偏移量发给电机任务电机任务负责速度环和方向显示任务只在有事件时刷新送药任务等待按键或串口触发。下面是常见的任务参数表任务名职责执行周期/触发优先级vTrackTask灰度传感器采集与路径偏移计算10ms 周期2vMotorTask电机速度闭环PWM 输出5ms 周期3vDispTaskOLED 状态显示队列触发1vSendTask送药舵机/机构控制事件触发2创建任务的模板代码通常写在 main 初始化末尾xTaskCreate(vTrackTask, track, 128, NULL, 2, xTrackHandle); xTaskCreate(vMotorTask, motor, 128, NULL, 3, xMotorHandle); xTaskCreate(vDispTask, disp, 256, NULL, 1, NULL);xTaskCreate 的参数顺序是任务函数、任务名、栈深度、传给任务的参数、优先级、任务句柄。栈深度单位是 wordSTM32 上 128 就是 512 字节。vTrackTask 和 vMotorTask 主要是整数运算128 够用vDispTask 如果调用 printf 或浮点格式化栈最好给到 256 甚至 512。这里有个经验初期把栈往大里给跑稳之后再逐步减小比一上来栈溢出死机再猜哪个任务出问题要省时间。优先级数字越大越先执行vMotorTask 设为 3保证速度环的采样周期不被其它任务挤掉。vTrackTask 和 vSendTask 同为 2FreeRTOS 同优先级任务按时间片轮转默认 configUSE_TIME_SLICING 是开启的。如果 vTrackTask 里用了 vTaskDelay 阻塞vSendTask 就能立即运行反之如果某个任务里写了 while(1) 空转同优先级任务会被饿死。所以周期任务内部不要用死等统一用 vTaskDelayUntil 来卡相位。3.3 队列与流缓冲区怎么选queue.c 的 xQueueSend 适合固定大小的结构体比如打包一次循迹的结果stream_buffer.c 更适合不定长的字节流比如串口命令行。两者不能混用而且创建时需要明确各自参数。创建和使用的范式如下xTrackQueue xQueueCreate(4, sizeof(TrackData_t)); xCmdBuffer xStreamBufferCreate(64, 8); TrackData_t xData {1, 0, 1}; xQueueSend(xTrackQueue, xData, 0); uint8_t rxBuf[16]; size_t len xStreamBufferReceive(xCmdBuffer, rxBuf, sizeof(rxBuf), pdMS_TO_TICKS(10));xQueueCreate 的第一个参数是队列长度第二个是每条消息的字节数。xStreamBufferCreate 的第二个参数是触发级别设为 8 表示缓冲区内积累 8 字节时就唤醒接收任务如果设成 1每来一个字节就做一次任务切换频繁且浪费。xQueueSend 最后一个参数 0 表示非阻塞队列满时立即返回 errQUEUE_FULL这样采集任务不会被阻塞。xStreamBufferReceive 的最后一个参数是阻塞时间pdMS_TO_TICKS(10) 就是最多等 10ms。任务要根据返回值判断是否拿到数据不能假设每次都能读到有效内容。stream_buffer 在写入大块数据时会持有临界区但它把发送拆成多次如果接收任务同时在读发送方可能看到一个不完整的帧。因此我建议传感器结构化数据用队列只有上位机命令这种天然流式、自己能提取帧的数据才用 stream_buffer。4. 定时器与 PWM从 stm32f10x_tim.c 看电机调速和编码器反馈4.1 定时器时基与 PWM 输出模式stm32f10x_tim.c 是标准外设库的定时器驱动送药小车里它承担电机 PWM 和编码器反馈两个活。PWM 输出前先确定时基PSC 分频得到计数时钟ARR 决定计数周期。如果 APB1 外设时钟是 36MHz内部定时器时钟已经是 72MHzPSC71 时计数频率为 1MHz。ARR99 时 PWM 频率是 10kHz占空比分辨率为 100 级。这个分辨率对普通直流减速电机够用但如果你希望速度环更细腻可以把 PSC 调小、ARR 调大比如 PSC7、ARR899同样接近 10kHz分辨率能做到 900 级。代价是计数频率更高定时器更新中断更频繁CPU 占用会上升。PWM 频率不能随便选。频率太低电机转子会发出明显啸叫转速波动也大频率超过 20kHz 后MOS 驱动芯片的开关损耗明显增加占空比分辨率也可能不够。实际项目中我一般锁定在 10kHz 到 20kHz 之间先满足电机驱动板的手册要求再确定具体 ARR 和 PSC。4.2 编码器模式与速度测量TIM 不仅能输出 PWM还能接正交编码器。TIM_EncoderInterfaceConfig 把定时器的两个输入通道配置成编码器接口硬件自动根据 A、B 相电平变化加/减计数。这个功能避免了在中断里软件解析 A、B 相脉冲极大降低 CPU 占用。要注意不是所有定时器都支持编码器模式STM32F103 上 TIM1、TIM2、TIM3、TIM4、TIM5 和 TIM8 可以。编码器模式下CNT 寄存器会随电机正反转增减读取 CNT 后做差分就能得到这个采样周期内的速度。定时器编码器模式PWM 输出典型通道TIM1支持CH1-CH4 互补输出TIM2支持CH1-CH4TIM3支持CH1-CH4TIM4支持CH1-CH4这里会有一个坑如果 PWM 和编码器用了同一个定时器CNT 会被比较输出逻辑干扰。我一般把编码器放在 TIM2PWM 放在 TIM3两个定时器各管各的。编码器计数范围需要控制在一个合理区间如果 CNT 溢出差分结果会是错的值。常见做法是开启编码器定时器的更新中断在中断里维护一个 32 位计数变量这样高速下也不容易溢出。4.3 一个可用的电机初始化模板综合一下一套基本的直流电机控制初始化长这样TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct; TIM_OCInitTypeDef TIM_OCInitStruct; TIM_TimeBaseStruct.TIM_Prescaler 71; TIM_TimeBaseStruct.TIM_Period 99; TIM_TimeBaseStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStruct); TIM_OCInitStruct.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStruct.TIM_Pulse 50; TIM_OCInitStruct.TIM_OutputState TIM_OutputState_Enable; TIM_OC1Init(TIM3, TIM_OCInitStruct); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_Cmd(TIM2, ENABLE);TIM_Prescaler71、TIM_Period99PWM 频率就是 72MHz/(72×100)10kHz。TIM_Pulse50 是占空比初始值对应 50%。TIM_OCMode_PWM1 表示计数值小于 TIM_Pulse 时输出有效电平大于时输出失效。TIM_OC1PreloadConfig 开启比较预装载这样在定时器运行中修改占空比不会产生瞬间毛刺。编码器接口这边TIM_EncoderMode_TI12 表示同时使用 TI1 和 TI2四倍频输入极性可以都设上升沿。最后 TIM_Cmd 使能相应定时器缺了它硬件外设不会开始工作。有几点补充PWM 引脚要配置成复用推挽输出编码器引脚必须是浮空输入模式。如果你发现电机只转一次或编码器计数方向相反先交换 A、B 相接法不要急着改代码。代码里如果把 RCC_APB2Periph_AFIO 漏掉重映射功能也不会生效。如果换用高级定时器 TIM1必须额外调用 TIM_CtrlPWMOutputs(TIM1, ENABLE)否则互补输出和主输出都不会有波形这是 TIM1 和普通定时器最明显的差异。5. 从电赛现场到平时调试送药小车调参顺序与几个验证技巧5.1 从开环到闭环的调参顺序拿到工程不要直接调 PID。正确的顺序是先点灯确认时钟和 Flash 配置没问题然后给一个固定占空比让电机开环转确认 PWM 频率、极性和 GPIO 复用是否正确再用编码器测量实际转速确认计数方向和速度换算系数开环跑稳之后再加速度环。每一步都要有一个明确的验证手段否则最后出了 Bug 你会分不清是逻辑问题还是硬件问题。5.2 用串口打印任务状态和队列水位FreeRTOS 在调试阶段可以输出任务状态表。vTaskList 会把任务名、状态、栈剩余大小、优先级一次性打印出来。用法如下char pcWriteBuffer[512]; vTaskList(pcWriteBuffer); USART_SendString(USART1, pcWriteBuffer);调用前要把 configUSE_TRACE_FACILITY 和 configUSE_STATS_FORMATTING_FUNCTIONS 打开不然 vTaskList 不会被编译进去。打印出来的每行都包含任务栈水位如果某个任务栈剩余长期低于 20%就把该任务的栈配置调大一点。同时可以用 xPortGetFreeHeapSize() 观察堆剩余确认任务创建阶段没有把堆耗尽。5.3 几个容易翻车的细节编码器用到的 A、B 相引脚STM32 内部弱上拉在某些霍尔编码器上不够用板子上不加 10k 上拉会导致丢脉冲症状是电机转但速度反馈明显偏低。FreeRTOS 的中断回调里只能调用带 FromISR 后缀的 API比如 xQueueSendFromISR直接调用 xQueueSend 会触发断言或死机。另外确认 configCHECK_FOR_STACK_OVERFLOW 已经打开栈溢出时会强制进入 vApplicationStackOverflowHook比任务跑飞后靠串口打印猜现场要快得多。电赛现场没有在线调试条件时把 LED 状态灯、串口任务表和按键触发标志位这三样配合起来定位任务挂起和队列堵塞的速度比在线调试还快。本文还有配套的精品资源点击获取
返回列表