第九周 第十五届国赛

第十五届国赛

1. 核心架构:任务调度器 (Scheduler)

这是整个程序的“大脑”,决定了各功能模块执行的频率。

C

 // 任务结构体定义
 typedef struct{
     void (*task_func)(void);    // 任务函数指针
     unsigned long int rate_ms;  // 执行周期(ms)
     unsigned long int last_ms;  // 上次执行时间
 } task_t;
 ​
 // 任务列表
 idata task_t scheduler_task[]={
     {led_proc,1,0},       // LED处理:1ms一次(响应快)
     {key_proc,10,0},      // 按键扫描:10ms一次(消抖)
     {seg_proc,20,0},      // 数码管刷新:20ms一次
     {ad_da,150,0},        // ADC读取:150ms一次
     {get_distance,100,0}, // 超声波测距:100ms一次(防止余波干扰)
     {uart_proc,10,0},     // 串口解析:10ms一次
     {crol_proc,1,0}       // 运动计算:1ms一次(保证移动平滑)
 };
 ​
 void main(){
     // ... 初始化代码 ...
     while(1){
         scheduler_run(); // 死循环中不断检查是否有任务到期需要执行
     }
 }

作用: 替代了传统的 delay 延时。题目要求数码管不能闪烁、按键响应要快 ,调度器保证了高频任务(如运动计算)优先执行,低频任务(如ADC)不占用过多资源。


2. 核心算法:运动控制 (crol_proc)

这是本题最难的数学逻辑部分,对应题目中“设备沿两点间的直线开始行进”的要求 。

C

 void crol_proc(){
     // ... 变量定义 ...
     if(work_mode==1){ // 只有在“运行”状态下才计算
         // 1. 计算时间差 dt (秒)
         current_time=uwtick;
         dt_ms=current_time-last_time;
         dt=(float)dt_ms/1000.0f; 
 ​
         // 2. 计算当前点到目标点的直线距离 (勾股定理)
         dx=target_location_float[0]-current_location_float[0];
         dy=target_location_float[1]-current_location_float[1];
         distance=sqrt(dx*dx+dy*dy); // 需要引入 math.h
 ​
         if(distance>0){
             // 3. 计算这一瞬间应该走的步长 (速度 * 时间)
             step=((float)speed_10x/10.0f)*dt;
 ​
             // 4. 判断是否到达
             if(distance<=step){
                 // 如果剩余距离小于步长,直接“瞬移”到终点,防止超调
                 current_location[0]=target_location[0];
                 // ... 更新状态为0 (空闲) ...
                 work_mode=0;
                 reach_flag=1; // 标记到达 [cite: 57]
             }
             else{
                 // 5. 向量分解:利用相似三角形计算 X 和 Y 轴的分量
                 move_x=dx/distance*step;
                 move_y=dy/distance*step;
                 
                 // 更新浮点坐标(为了精度)和整形坐标(为了显示)
                 current_location_float[0]+=move_x;
                 current_location_float[1]+=move_y;
                 current_location[0]=current_location_float[0];
                 current_location[1]=current_location_float[1];
             }
         }   
     }
 }
  • 作用: 实时计算坐标。

  • 难点: 必须使用 float 类型 (current_location_float) 进行中间计算,否则因为整数除法的截断误差,设备走出的路线会是歪歪扭扭的折线,而不是直线。


3. 串口通信与指令解析 (uart_proc & uart_server)

题目要求解析 (30,420) 这种复杂格式,且不能添加回车换行 。

C

 // 中断服务函数:负责把接收到的字符塞进缓冲区
 void uart_server() interrupt 4{
     if(RI){
         uart_rx_flag=1; // 标记有新数据
         uart_rx_tick=0; // 重置超时计时器
         // ... 存入 uart_rx_buf ...
         RI=0;
     }
 }
 ​
 // 任务处理函数:解析指令
 void uart_proc()
 {
     // 利用超时机制判断一帧数据结束 (10ms没有新数据认为接收完毕)
     if (uart_rx_tick >= 10) 
     {
         // 1. 查询位置指令 # [cite: 80]
         if(strcmp(uart_rx_buf,"#")==0)
             printf("(%u,%u)",current_location[0],current_location[1]);
         
         // 2. 查询状态指令 ? [cite: 71]
         else if(strcmp(uart_rx_buf,"?")==0){
             // 根据 work_mode 返回 Idle/Busy/Wait
         }
         
         // 3. 设置坐标指令 (x,y) [cite: 62]
         // 技巧:使用 sscanf 自动提取括号内的数字
         else if (uart_rx_buf[0] == '(' && uart_rx_buf[uart_rx_index - 1] == ')'){
             if (sscanf(uart_rx_buf, "(%u,%u)", &x, &y) == 2)
             {
                 if (work_mode == 0) // 只有空闲时才生效 [cite: 68]
                 {
                     target_location[0] = x;
                     target_location[1] = y;
                     printf("Got it");
                 }
                 else printf("Busy");
             }
             else printf("Error");
         }
         // ... 清空缓冲区 ...
     }
 }
  • 作用: 实现了非阻塞的串口接收。利用 sscanf 函数极大地简化了字符串解析的难度。

4. 频率测量与速度换算 (timer1_serve)

题目要求通过 P34 引脚测量频率,并换算为速度 。

C

 void timer1_serve() interrupt 3{ // 1ms中断
     // ... 数码管刷新代码 ...
 ​
     // 每1000ms (1秒) 计算一次频率
     if(++time_1s_freq==1000){
         time_1s_freq=0;
         
         // 读取定时器0的计数值 (Timer0作为计数器连接NE555)
         freq_value=TH0<<8|TL0; 
         
         // 公式:V = πRF/100 + B [cite: 92]
         // 注意:这里算的是 speed_10x (速度的10倍),为了保留1位小数
         speed_10x = (float)(3.14 * r_value_10x * freq_value) / 100.0f + b_value * 10;
         
         // 边界保护:速度不能为负 [cite: 95]
         if(speed_10x < 0) speed_10x = 0;
         
         // 清零计数器,准备下一次测量
         TH0 = TL0 = 0;
     }
 }
  • 作用: 利用定时器中断实现精准的 1秒 采样周期。这里 Timer0 被配置为外部计数模式 (TMOD |= 0x05),专门用来数 NE555 的脉冲。

5. 状态机与传感器 (get_distance & key_proc)

这部分体现了题目要求的“避障”和状态流转。

C

 void get_distance(){
     obstacle_distance=ut_wave_data(); // 读取超声波
     // 避障逻辑 [cite: 99]
     if(work_mode!=0) // 如果不是空闲
         if(obstacle_distance<30) // 距离小于30cm
             work_mode=2; // 强制切换到“等待”状态
 }
 ​
 void key_proc(){
     // ... 按键读取 ...
     switch(work_mode){
         case 0: // 空闲
             // 按下S4,且已收到坐标 -> 切换到运行 [cite: 162]
             if(key_down==4 && receive_flag==1) work_mode=1;
             // 按下S5 -> 重置坐标 [cite: 164]
             if(key_down==5) { current_location[0]=0; ... }
         break;
         case 1: // 运行
             // 按下S4 -> 暂停 (进入等待)
             if(key_down==4) work_mode=2;
         break;
         case 2: // 等待
             // 按下S4,且障碍物消除 -> 恢复运行 [cite: 163]
             if(key_down==4 && obstacle_distance>=30) work_mode=1;
         break;
     }
     // ... S8, S9, S12, S13 的界面切换和参数修改逻辑 ...
 }

作用: 严格按照题目 3.3 节 的状态流转图编写逻辑。work_mode 是连接所有功能的核心变量。


6. 输出控制:显示与继电器 (seg_proc & led_proc)

C

 void seg_proc(){
     switch(seg_disp_mode){
         case 0: // 坐标界面 [cite: 105]
             // 根据 work_mode 判断显示当前坐标还是目的地坐标
             // 使用取余和除法拆分数字,并处理高位熄灭(赋值为10)
         break;
         case 1: // 速度界面
             if(work_mode==1) { /* 显示速度 */ }
             else if(work_mode==2) { /* 显示障碍物距离 [cite: 124] */ }
         break;
         case 2: // 参数界面 [cite: 131]
              // 显示 R 和 B 参数,注意 B 参数可能是负数
         break;
     }
 }
 ​
 void led_proc(){
     switch(work_mode){
         case 1: // 运行状态
             relay(1); // 继电器吸合 [cite: 194]
             ucled[1]=dark_mode; // 夜间亮灯 L2 [cite: 201]
         break;
         // ... 其他状态的 LED 逻辑 ...
     }
 }
  • 作用: 将内部数据可视化,并控制物理外设。led_proc 中对继电器的控制 (relay(1)) 和照明灯 (dark_mode) 的逻辑是根据题目 3.10 和 3.11 节编写的。