一、软件详解1.1 软件架构软件采用模块化设计主要文件包括- bsp_motor.c/h电机PWM调速与方向控制- bsp_tracker.c/h巡线传感器读取与加权位置控制- bsp_encoder.c/h硬件编码器初始化与距离读取- bsp_k230.c/hK230数字识别通信与协议解析- bsp_led.c/h、bsp_key.c/h、bsp_buzzer.c/h、bsp_oled.c/h、bsp_adc.c/h外设驱动- task.c/h任务调度与路径参数- main.c主状态机与系统初始化1.2 电机PWM调速初始化流程1. 开启GPIOA、GPIOB和通用定时器时钟2. PB0和PB1复用为GTIM1的通道3和通道43. 方向引脚配置为推挽输出4. 配置PWM输出模式为PWM1、高电平有效、不反相5. 使能通道输出6. 比较值清零电机不转7. 启动定时器速度归一化- 定义ARR重装载值和速度范围- 速度归一化处理上层逻辑无需关心PWM具体数值- 只需关注-1000~1000的抽象速度调速函数void Cart_Run(int16_t left_speed, int16_t right_speed) { // 限制速度范围 // 计算PWM值 // 配置方向引脚 // 写入比较寄存器 }PWM频率调整- 系统时钟48MHz预分频系数16重装载值400- 定时器时钟频率 48MHz / 16 / 400 7.5kHz- 适当降低PWM频率可提高电机电流连续性- 低占空比下提供更大瞬时电流提高扭矩- 分频系数改为512对应234Hz PWM输出1.3 巡线控制引脚定义- D1~D5分别连接PB12、PB13、PB9、PB15、PB8- D3原接PB14因硬件损坏改接PB9初始化- 五路接收引脚配置为GPIO上拉输入- 上拉输入在模块输出高阻态或信号线干扰时保持高电平- 增强抗干扰能力尤其在电机电磁噪声大的环境核心逻辑void Tracker_Update(void) { // 读取5路传感器高低电平 // 1表示检测到黑线 // 加权平均计算黑线位置偏差 // D1权重-2D2权重-1D3权重0D4权重1D5权重2 // 根据偏差乘以转向系数调整左右轮速度差 // 全丢线时保持基础速度直行 // 限幅后调用Cart_Run }加权平均优势- 多个传感器同时触发时权重相加得到平滑偏差值- D2和D3同时触发位置为-1- D3和D4同时触发位置为1- 转弯时不会突然打大方向有渐变过程- 响应平滑参数调整方便转向系数- 代码中为80- 可理解为比例增益- 偏差越大左右轮速度差越大转弯越猛- 偏差越小速度差越小转弯越柔和- 太小转不过弯太大车子抖动1.4 编码器测距硬件编码器模式- 使用GTIM2配置为四倍频正交编码器模式- 直接对从模式控制寄存器赋值绕过库函数断言- SMS字段设为0x3四倍频正交编码器模式- TS字段设为0x5选择TI1FP1作为触发源- 自动重载值65535初始计数值清零- 使能定时器编码器模块自动工作接口函数int32_t Get_Left_Total(void); // 读取当前计数并转换为有符号数 void Reset_Encoder_Count(void); // 清零计数器 int32_t Get_Right_Total(void); // 右轮暂未接返回0优势- CPU几乎没有负担- 适合需要频繁读距离、同时要求巡线实时性的场景- 正转计数增加反转计数减少1.5 K230数字识别通信通信协议- 帧格式0xAA 数字 0x00 0x55- 帧头帧尾固定有效区分有效数据和干扰乱码- 避免数据粘连、丢包导致解析错误- 波特率1152008N1格式8位数据位、无校验位、1位停止位硬件接线- K230 TX接CW32 RXPA10- 两端共地- 共地是串口通信稳定的基础中断接收void UART1_IRQHandler(void) { // 清除错误标志 // 接收字节并做状态机解析 // 状态0等待帧头0xAA // 状态1存储数字字节 // 状态2检查0x00 // 状态3检查帧尾0x55提取数字置位就绪标志 // 清除接收完成中断标志 }主循环接口uint8_t IsDigitReady(void); // 查询是否有新数字 uint8_t GetDetectedDigit(void); // 取出数字并清除就绪标志调试经验- 初期通信极不稳定要么收不到数据要么全是乱码- 排查发现系统上电后默认使用4MHz HSI作为时钟源- 4MHz分频出的115200波特率误差太大- 添加48MHz时钟配置代码后问题解决- 时钟是通信的灵魂系统时钟直接决定波特率精度1.6 外设驱动LEDvoid LED_Init(void); // PC13、PC14、PC15配置为推挽输出 void LED2_OPEN(void); // PC13输出低电平 void LED2_CLOSE(void); // PC13输出高电平 // LED3、LED4同理蜂鸣器void Buzzer_Init(void); // PA15配置为推挽输出 void Buzzer_Open(void); // PA15输出高电平 void Buzzer_Close(void); // PA15输出低电平按键void Key_Init(void); // PA5配置为上拉输入 uint8_t Key_Scan(void); // 检测下降沿延时消抖返回1表示有效按下 延时 void Delay_us(uint32_t us); // 基于SysTick的微秒延时 void Delay_ms(uint32_t ms); // 基于SysTick的毫秒延时 void Delay_s(uint32_t s); // 基于SysTick的秒延时 OLED void OLED_Init(void); void OLED_Printf(int16_t x, int16_t y, uint8_t fontsize, const char *format, ...); void OLED_Update(void); // 将显存内容刷新到屏幕 ADC电压采集 void ADC_Init(void); uint16_t Get_ADC(uint8_t channel); float Get_Battery_Voltage(void); // 采样5次取平均指数平滑滤波1.7 任务调度task.c是整个送药小车的任务调度层从主程序中抽离出来避免主程序过于冗长。它把电机、循迹、编码器、按键、LED、OLED和蜂鸣器整合成一条条完整的送药路线。基础运动函数void Move_Straight_With_Tracker(int32_t target_pulse); // 边循迹边直行每10ms检查左轮编码器累计脉冲达到目标值停车 void Turn_Left_Encoder(int32_t pulse); // 左右轮反向转动实现原地左转编码器脉冲控制角度 void Turn_Right_Encoder(int32_t pulse); // 左右轮反向转动实现原地右转编码器脉冲控制角度 uint8_t Wait_Key_Press_Simple(void); // 带消抖地检测PA5按键用于人工确认 数字1任务示例 void Task_Digit1(void) { Move_Straight_With_Tracker(DIST_FIRST); // 直行 Cart_Run(0, 0); SysTickDelay(500); Turn_Left_Encoder(PULSE_TURN90); // 左转90° Move_Straight_With_Tracker(DIST_SECOND); // 直行 LED4_OPEN(); Buzzer_Open(); // 提示卸载 while (!Wait_Key_Press_Simple()); // 等待按键 LED4_CLOSE(); Buzzer_Close(); Cart_Run(0, 0); SysTickDelay(200); Cart_Run(-SPEED_TURN, SPEED_TURN); // 掉头180° while (abs(Get_Left_Total()) PULSE_TURN180); SysTickDelay(500); Move_Straight_With_Tracker(DIST_AFTER_TURN); // 返程直行 Turn_Right_Encoder(PULSE_TURN_R90); // 右转90° Move_Straight_With_Tracker(DIST_LAST); // 回到起点 Cart_Run(0, 0); LED2_OPEN(); // 任务完成 while (1); }其他数字任务- 数字2数字1镜像第一段后右转掉头后左转- 数字3直行12000→左转→直行2200→等待→掉头→直行2200→右转→直行11000- 数字4数字3镜像第一段后右转返程左转- 数字5去程左转、右转返程左转、右转- 数字6去程右转、右转返程左转、左转- 数字7去程左转、左转返程右转、右转- 数字8直行18500→右转→直行6500→左转→直行2200→等待→掉头→直行2200→右转→直行7000→左转→直行15000参数集中管理所有距离和转弯脉冲以宏定义形式写在task.c顶部方便根据实际地图调整。每个数字的参数独立定义互不影响。1.8 主状态机主程序采用状态机结构流程清晰初始化font-face{ font-family:Times New Roman; } font-face{ font-family:宋体; } font-face{ font-family:等线; } p.MsoNormal{ mso-style-name:正文; mso-style-parent:; margin-bottom:0.0000pt; mso-pagination:none; text-align:justify; text-justify:inter-ideograph; line-height:20.0000pt; mso-line-height-rule:exactly; font-family:Times New Roman; mso-fareast-font-family:宋体; font-size:12.0000pt; mso-font-kerning:1.0000pt; } span.msoIns{ mso-style-type:export-only; mso-style-name:; text-decoration:underline; text-underline:single; color:blue; } span.msoDel{ mso-style-type:export-only; mso-style-name:; text-decoration:line-through; color:red; } page{mso-page-border-surround-header:no; mso-page-border-surround-footer:no;}page Section0{ } div.Section0{page:Section0;} SystemClock_48M(); // 系统时钟配置为48MHz SysTick_Config(48000000 / 1000); // 1ms中断 Motor_Init(); // 电机初始化 Tracker_Init(); // 巡线初始化 Encoder_Init(); // 编码器初始化 UART_Debug_Init(); // 调试串口 UART1_Init(); // K230串口 LED_Init(); // LED初始化 ADC_Init(); // ADC初始化 OLED_Init(); // OLED初始化 Buzzer_Init(); // 蜂鸣器初始化 // PA5配置为上拉输入 Cart_Run(0, 0); // 电机停止 LED2_CLOSE(); LED3_CLOSE(); LED4_CLOSE(); 状态0等待数字识别 OLED_Printf(0, 0, OLED_8X16, Delivery Cart); OLED_Printf(0, 16, OLED_8X16, Voltage: %.2fV, Get_Battery_Voltage()); OLED_Printf(0, 32, OLED_8X16, Awaiting digit); OLED_Update(); if (IsDigitReady()) { uint8_t digit GetDetectedDigit(); if (digit 1 digit 8) { target_digit digit; LED3_OPEN(); Buzzer_Open(); OLED_Printf(0, 32, OLED_8X16, Awaiting load); OLED_Update(); state 1; } } 状态1等待按键确认装载 if (Wait_Key_Press_Simple()) { LED3_CLOSE(); Buzzer_Close(); OLED_Printf(0, 32, OLED_8X16, Delivering); OLED_Update(); state 2; } 状态2执行任务 switch (target_digit) { case 1: Task_Digit1(); break; case 2: Task_Digit2(); break; case 3: Task_Digit3(); break; case 4: Task_Digit4(); break; case 5: Task_Digit5(); break; case 6: Task_Digit6(); break; case 7: Task_Digit7(); break; case 8: Task_Digit8(); break; default: Cart_Run(0, 0); break; }任务完成- 点亮LED2OLED显示Task complete- 程序进入死循环等待下次复位二、调试经验与问题解决2.1 时钟配置问题现象UART通信极不稳定要么收不到数据要么全是乱码。排查- 检查接线、引脚配置、中断配置均无误- 手动触发中断可正常进入证明中断框架没问题- 改用轮询可收到数据说明硬件通路正常- 电脑USB转TTL与CW32通信正常说明CW32本身没问题- 怀疑K230与CW32波特率偏差原因系统上电后默认使用4MHz HSI作为时钟源UART波特率基于系统时钟分频4MHz分频出的115200波特率误差太大。解决添加完整的48MHz时钟配置代码将系统主时钟、UART外设时钟统一配置为48MHz。2.2 编码器初始化问题现象使用库函数GTIM_SlaverModeCfg配置编码器模式小车经常上电后无法正常运行。原因怀疑初始化阶段触发库函数内部断言失败导致程序卡死。解决参考官方手册直接对定时器的从模式控制寄存器赋值。SMS字段设为0x3四倍频正交编码器模式TS字段设为0x5选择TI1FP1作为触发源。2.3 巡线模块适配问题现象地图上有红线和黑线小车需要只寻红线不受黑线影响。原因原有程序使用简单作差比较无法区分颜色。解决将简单作差比较改为区间判断只有ADC值落入红色区间时判定结果才返回1区间外返回0。需要精确测量各颜色对应的ADC值。2.4 ADC值测量误差误差来源- 传感器晃动导致离地高度不同影响最大- 环境光影响- 地图褶皱解决方法- 使用稳定的支架安装巡线模块- 测量时减少按压和晃动- 保持地图平整洁净- 避免在光照不均匀的地方测量2.5 任务执行混乱问题现象偶发数字3按2走、数字8按2走等错误。排查- 串口打印显示识别数字正确、执行任务编号正确- 怀疑内存被意外覆盖或中断干扰解决- 关键变量加volatile- 增大栈空间- 关闭编译器优化- 在任务执行前用局部变量锁定目标数字三、项目总结3.1 技术亮点1.硬件编码器模式使用GTIM2四倍频正交编码器模式无需CPU中断大幅降低CPU负载提高巡线实时性。2.加权平均巡线算法五路传感器加权位置计算响应平滑参数调整方便适合比赛现场快速调试。3.自定义UART通信协议帧头帧尾固定有效区分有效数据和干扰乱码避免数据粘连、丢包导致解析错误。4.任务参数集中管理所有距离和转弯脉冲以宏定义形式写在task.c顶部方便根据实际地图调整每个数字参数独立定义。5.模块化软件设计各功能模块独立成文件主程序简洁清晰便于维护和扩展。6.状态机主控流程从等待数字、等待装载、到执行任务每一步都有明确的指示灯和OLED提示。3.2 调试经验1.时钟是通信的灵魂UART通信异常多因系统时钟未正确配置导致波特率偏差。2.中断与轮询结合调试通过对比可快速定位问题。3.引脚映射需仔细核对GPIO逻辑编号与物理引脚编号不同。4.电源共地与独立供电保证通信稳定与电机启动可靠。5.编码器硬件模式大幅降低CPU负载提升巡线实时性。6.耐心与方法遇到问题要分层排查逐步缩小范围。3.3 未来优化方向1.巡线算法优化加入PID算法或更精细的位置插值提高平稳性。2.双车协同扩展无线通信模块实现两车协同送药。3.路径规划灵活性支持动态路径规划适应更复杂场景。4.视觉识别增强增加更多数字识别、颜色识别、障碍物检测功能。5.电源管理优化增加低功耗模式延长续航时间。6.人机交互升级增加触摸屏或语音提示提升操作体验。3.4 结语本项目成功实现了智能送药小车的完整功能包括数字识别、巡线行驶、编码器测距、路口转向、自动掉头与往返任务。硬件选型合理软件模块化程度高调试过程积累了宝贵经验。整个系统稳定可靠能够满足题目基本要求并具备良好的扩展潜力。通过这个项目我们深入理解了嵌入式系统开发的全流程从硬件选型、电路设计、软件编程到系统调试每一个环节都需要严谨的态度和科学的方法。希望本总结能为后续开发者提供参考也期待智能送药小车在更多场景中发挥作用。