第十五届国赛
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 节编写的。