2026年CCUT电子设计竞赛报告

2026年CCUT电子设计竞赛报告

智能病区物资配送小车(A题)

小车全貌

摘要 此设计以STM32F407为主控模块,完成了包含视觉循迹小车,视觉字母判断,自动路径规划在内的药品搬运小车。整个装置在视觉MaixCam循迹小车的基础上通过JetsonNano计算机判断字母来进行目标识别,通过串口与主控通信,对小车定位。

1.系统硬件方案

本题硬件方案有多种选择,我们的方案只是其一不是唯一。

本部分只用来说明我们作品的硬件部分。

1.1 主控

1.1.1 主控芯片

主控我们选择了STM32F407ZGT6,从性能上来说针对循迹,串口数据解析绰绰有余,我认为F103也足够胜任本次任务,从具体型号来说选择ZGT6具体原因如下

  • 引脚众多,即使八路循迹,加上四路编码器与马达仍能满足
  • 资源较为丰富,三个串口能够满足此次我们与maixcam和Jetson计算机进行通信
  • 最主要原因为此型号单片机的驱动库我经过了几次比赛的完善已经较为可用

综上我推荐片上资源较多,操作较为熟练的单片机。

我的f407git仓库

1.1.2 主控板

考虑到小车结构简单,无需太多外围模块,我们仅仅使用了电机驱动模块,降压模块。

电机驱动 为tb6612,若追求更高性能可更换为drv8701。 以及一个核心板自带led

板上硬件连接除pcb外 均使用2.54端子接口 可有效防止虚接短接。 主控板

额外一提的是,烧录程序,调试串口可以使用无线调试模块以加快效率。

无线调试模块

1.2 视觉模块

本题可以使用一个模块处理两项任务,若选择恰当,性能足够。

本次我们尝试多摄像头结合,所以选择双摄像头方案。

本题我们的视觉模块分为两部分,一部分专注于循迹相关,另一部分专注于字母识别,两部分图像经过处理后通过串口传输给单片机。

1.2.1 循迹部分

首先,循迹相关视觉算法较为简单,对性能要求较小,甚至不使用SOC使用单片机处理图像也可,但是使用带有SOC的视觉模块会有较为完整的生态支持,比如我们本次的maixcam就完整支持了opencv,具有强大的图像处理函数,并且有许多开源资料和官方平台,能够胜任本次任务。同理,k230 openmv等模块亦能够用来循迹任务,所以并无特别理由选择了maixcam,主要理由为我比较熟悉maixcam。

maixcam maixhub平台 装车maixcam 国赛

1.2.2 字母识别部分

本题中,唯一需要性能的部分为字母识别部分,由于需要部署模型,复杂的预处理和较高的实时性所以对性能要求很高。

我基于实验室的视觉模块分析得出以下三种方案 - maixcam pro - k230 - 香橙派5 - Nvidia jetson nano orin nx 他们的性能排序(基于tops单位)为
\[ Jetson(157tops)>香橙派5(6tops)>k230(6tops)>maixcam(1tops)\]

很显然,要选择jetson了。

其实不然。

如果考虑作品的成本,本题的性能还没有一定需要使用如此强力的芯片 接下来我对价格升序排序。

\[k230<maixcam<香橙派5<Jetson \]

很显然,使用k230为最佳,本题我推荐使用k230。 但是我们最终使用了jetson,得益于其完整的linux为我们带来更多的可拓展性

jetson装车图

可见其有多个usb,网口等接口,拓展性强大。

摄像头使用了120°可视角度的usb摄像头,需要注意摄像头的帧率不得低于30帧以及摄像头的角度需要满足在4个字母并排时的识别,其他对于摄像头的特殊要求没有,颜色不要太过失真的摄像头均可。

字母部分摄像头

1.3 其他硬件部分

1.3.1 底盘部分

轮子选择

本题的小车结构十分简单,对于尺寸限制也较为宽松,为25年电赛相同尺寸。若在搭车时考虑尺寸,需要注意的部分有两点,第一,若摄像头视角窄需要高度则不可超高,第二,小车若使用大编码器电机,需要考虑轮距。我们选择了一种减速机构较小的电机使的轮间距较小。 0eb805bcd48598af07b1ec05b43bb68b.webp

轮子数量

轮子数量主要考虑的为驱动轮数量,本题我更推荐四轮,四轮即使在任意两个轮子速度不一致时,前轮具有导向作用,会让小车更加平稳。但考虑到转弯顺滑程度两轮较好但并不胜出太多,所以推荐四轮。

但是两轮对主控资源占用少且pid速度环易调试,我们选择了两轮驱动。 前轮为牛眼万向轮。

万向轮

主体框架

主体框架为一块亚克力板,其上模块使用m3螺丝固定 小车板子

主板与视觉模块使用铜柱堆叠。 小车后方

1.3.2 供电

供电分为两部分

第一部分 主电池 12v - maixcam - 主控机器模块 - 电机驱动 主电池

第二部分 视觉jetson专用电池 底层为电池模块,两部分分离是为了防止主电池部分模块突然断电或损坏影响jetson 小车后方

1.3.3 药品部分

药品部分 分为三部分 架子,药品,药品检测

架子使用3D打印 药品使用干电池 药品检测使用微动开关(一般用途为鼠标按键) 药品

1.3.4 其他硬件

电池外壳与摄像头支架 均为3D打印 字母部分摄像头 主电池 装车maixcam

小结

在使熟练使用3D打印条件下,本车框架无较大难度。 对主控和视觉的模块选择较多,视觉需要稍加选择。

2.系统主控软件方案

2.1 总体说明

本部分说明主控软件部分。

首先分析题目对应主控软件实现目标。

工作过程:参赛者手动将小车摆放在药房处,手持字母标号纸张由小车识别病房号(串口数据解析),将药品一次性装载到智能病区物资配送小车上(药品拿取识别);小车检测到药品装载完成后自动开始运送(循迹,底盘控制);小车根据走廊上的标识信息自动识别(循迹,串口数据解析)、寻径将药品送到指定病房(循迹),点亮红色指示灯,等待卸载药品(药品拿取识别);病房处人工卸载药品后,小车自动熄灭红色指示灯,开始 返回;小车自动返回到药房(循迹)(车头投影在门口区域内,面向药房)后,点亮绿色。(车上须配备一键关闭循迹的开关)

基础部分要求: - 循迹 - 底盘控制(pid) - 串口数据解析 - 药品拿取识别 - 按键检测

发挥部分要求 - 路径规划

根据分析可得基础部分只有stm32的基础代码。 发挥部分有很多写法。

2.2 基础部分

2.2.1 循迹

本次循迹我们仅使用了摄像头来进行循迹。

循迹核心思路与普通循迹模块一致。

第一步 提取图像红色区域 在HSV色域范围下提取红色,若在RGB色域下提取颜色,不易区分红色与深色类颜色,所以使用HSV

1
2
3
4
5
6
7
8
9
10
11
12
13
#红色色域
LOWER_RED1 = np.array([0, 60, 60])
UPPER_RED1 = np.array([18, 255, 255])
LOWER_RED2 = np.array([165, 60, 60])
UPPER_RED2 = np.array([180, 255, 255])
while True:
img = cam.read()


mask1 = cv2.inRange(hsv, LOWER_RED1, UPPER_RED1)
mask2 = cv2.inRange(hsv, LOWER_RED2, UPPER_RED2)
mask = cv2.bitwise_or(mask1, mask2) # 合并两段红色

第二步 取出处理后图像 一行像素

绿色行 为取出像素

这一行就可以看作循迹模块,区别就是这一行比普通循迹模块多许多像素

Snipaste_2026-04-21_20-58-32.webp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scan_line = mask[TRACK_LINE_Y, :] #取出一行像素
red_points = np.where(scan_line == 255)[0] #若像素值为255就是红色
point_num = len(red_points)

if point_num > min_width:
line_center = int(np.mean(red_points)) # 红线中心X
error = line_center - CENTER
if(point_num >= CENTER-5):
# print("找到中线")
crossing_flag = 1
else:
crossing_flag = 0
else:
line_center = -1
error = 0 # 没找到线

这么处理后就得到了循迹误差,可以传给单片机后进行pid计算。

由于后面的路径规划还需检测较远的字母,所以还需要设置一行检测来判断是否接近十字路口, 原理与前代码相同。

2.2.2 底盘控制

由于小车较重,且需要循迹,所以需要速度pid串上循迹pid 具体pid结构可以见以下框图 Snipaste_2026-04-21_21-09-24.webp

具体pid代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void pidTimerCallback(TimerHandle_t xTimer)
{
int track_error = 0;
char track_flag = 0;
cam_getIR_data(&track_error, &track_flag, NULL);
fp32 lb_motor_output = PID_calc(&motor_bl_pid, (fp32)encoder_bl_spd, motor_bl_set_spd);
fp32 rb_motor_output = PID_calc(&motor_br_pid, (fp32)encoder_br_spd, motor_br_set_spd);
if (ir_en == 1)
{
fp32 ir_correction = PID_calc(&ir_pid, (fp32)track_error, 0);
if (ir_motor_en == 1)
{
pidTurn(ir_correction, NORMAL_SPEED);
}
}
else
{
pidTurn(0, 0); // 停车
}

Motor_Set_Duty(lb_motor_output, MOTOR_LEFT_BACK);
Motor_Set_Duty(rb_motor_output, MOTOR_RIGHT_BACK);

}

详细pid库实现不说明。 需要一提的是,循迹pid需要使用位置式pd控制, 原因见 卓大解释

2.2.3 药品与按键检测

这两部分检测均是基础gpio输入扫描 不细说。

1
2
3
4
5
6
7
8
uint8_t Drug_Array[3]; // 药品检测数组

void Drug_Scan()
{
Drug_Array[0] = HAL_GPIO_ReadPin(DRUG_DETECT_1_GPIO_Port, DRUG_DETECT_1_Pin) ? 1 : 0;
Drug_Array[1] = HAL_GPIO_ReadPin(DRUG_DETECT_2_GPIO_Port, DRUG_DETECT_2_Pin) ? 1 : 0;
Drug_Array[2] = HAL_GPIO_ReadPin(DRUG_DETECT_3_GPIO_Port, DRUG_DETECT_3_Pin) ? 1 : 0;
}

2.3 发挥部分

2.3.1 小车核心控制部分

发挥部分要解决的核心点只有一个,就是路径规划。

按识别顺序完成多病房依次配送

以下为我们组的运行流程 这里画一个不标准的流程图:

流程图

以下为流程图的代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
void task(int max_target_num)
{
static int route_pointer = 0; // 路线执行指针
static int cross_flag = 0; // 十字路口标志
static int target_idx = 0; // 目标索引
static int letter_side = 0;
static int last_turn = 0; // 上一次转弯方向
static int start_pos_lev = 0;
Node_Name wardsToSave[2];

switch (status_counter)
{
case 0:
if (target_idx >= max_target_num)
{
//目标病房已走完
generate_return_route(route, &route_length); // 生成返回路线
taskStatusToggle();
printf("current pos %d %d ", current_pos[0], current_pos[1]);
printf("回家路线 ");
printRoute(route, route_length);
}
else
{
//目标病房未走完
start_pos_lev = current_pos[0]; // 存一下出发层数 用来未知下寻找目的地

if (generate_route(route, &route_length, current_pos, target_arr[target_idx]))
{//是否寻找到目标病房
taskStatusToggle();//切换status_counter
}
else
{
status_counter = 4;
}
}
break;
case 1:
if (route_pointer < route_length)
{
Move_Action action = route[route_pointer];
switch (action)
{
case ACT_STRAIGHT:
openIRAndMotor();
if ((route_pointer == route_length - 1) && isAwayFromTract()) // 处理路径最后一段直线
{
route_pointer++;
}
else
{
if (crossDetect() && cross_flag == 0)
{
osDelay(CROSS_STOP_DELAY); // 让车过十字路口
cross_flag = 1; // 标记已检测到十字路口
route_pointer++;
}
else if (!crossDetect())
{
cross_flag = 0; // 没有检测到十字路口,重置标志
}
}
break;
case ACT_RIGHT:
cross_flag = 0;
openIROnly();
pidTurn(-TURN_SPD, 0);
if (crossAfterDetect())
{
route_pointer++;
}
break;
case ACT_LEFT:
cross_flag = 0;
openIROnly();
pidTurn(TURN_SPD, 0);
if (crossAfterDetect())
{
route_pointer++;
}
break;
default:
closeIRAndMotor();
pidTurn(0, 0); // 停车
}
}
else
{
printf("Arrived at warehouse\r\n");
osDelay(CROSS_STOP_DELAY); // 让车停在终点
printf("Arrived at warehouse\r\n");
closeIRAndMotor();
pidTurn(0, 0); // 停车
findWardPos(target_arr[target_idx], current_pos); // 更新当前位置为目标位置
printf("task_idx:%d\r\n", target_idx);
taskStatusToggle();
}
break;
case 2:
if (Drug_Array[target_idx] == 1)
{
pidTurn(TURN_SPD, 0);
if (crossAfterDetect() == 1)
{
taskStatusToggle(); // 切换到下一状态
}
}
break;
case 3:
target_idx++;
route_pointer = 0;
route_length = 0;
status_counter = 0;
led0Toggle();
break;
case 4:
// 从这里开始就是找不到目的地的逻辑了
// 流程: 找不到=>回主路 5 =>到十字路口(更新层数)6=>找到字母 对应转向和存储7
// =>找不到字母 执行存储 直行8
generate_back_main_route(route, &route_length, current_pos, target_arr[target_idx]);
taskStatusToggle();
break;
case 5:
if (route_pointer < route_length)
{
Move_Action action = route[route_pointer];
switch (action)
{
case ACT_STRAIGHT:
openIRAndMotor();
if (crossDetect() && cross_flag == 0)
{
osDelay(CROSS_STOP_DELAY); // 让车过十字路口
cross_flag = 1; // 标记已检测到十字路口
route_pointer++;

if (current_pos[0] >= 3)
{
current_pos[0] = 3;
}
else
{
current_pos[0]++;
}
}
else if (!crossDetect())
{
cross_flag = 0; // 没有检测到十字路口,重置标志
}
// }
break;
case ACT_RIGHT:
cross_flag = 0;
openIROnly();
pidTurn(-TURN_SPD, 0);
if (crossAfterDetect())
{
route_pointer++;
}
break;
case ACT_LEFT:
cross_flag = 0;
openIROnly();
pidTurn(TURN_SPD, 0);
if (crossAfterDetect())
{
route_pointer++;
}
break;
default:
closeIRAndMotor();
pidTurn(0, 0); // 停车
}
}
else
{
printf("back to road\r\n");
printf("current level:%d\r\n", current_pos[0]);

last_turn = 0;
taskStatusToggle();
}
break;
case 6:
// 这里就是已回主路了 开始根据十字路口和字母来判断目的地了 并且存地图 若找到了就不停车

openIRAndMotor();
if (crossDetect() && cross_flag == 0)
{
printf("检测到十字路口 last_turn:%d\r\n", last_turn);
cross_flag = 1; // 标记已检测到十字路口
if (current_pos[0] == 3 && start_pos_lev <= 3)
{
if (last_turn > 0)
{
current_pos[0] = 5;
}
else if (last_turn < 0)
{
current_pos[0] = 4;
}
}
else
{
if (current_pos[0] < 3)
{
current_pos[0]++; // 更新层数
}
}
}
else if (!crossDetect())
{
cross_flag = 0; // 没有检测到十字路口,重置标志
}

// 这里是识别字母的 (这里应该也要存地图) 条件就是车前方一段距离碰到路口
if (letter_detetct_flag == 1)
{
// 这里检测到路口就要判断目的地在左侧还是右侧了
// 若这个变量大于0则为右 小于为左

for (int i = 0; i < 10; i++) // 进行一个消抖操作
{
letter_side += cam_getTargetSide(target_arr[target_idx]);
}

if (letter_side != 0)
{

printf("gotTarget\r\n");
// printf("%d\r\n",letter_side);
// closeIRAndMotor();
led0Toggle();
taskStatusToggle();
cam_getTwoLetter(wardsToSave);

if (current_pos[0] < 2)
{
// printf("target ll:%d %d %d\r\n", current_pos[0] + 1, current_pos[1], status_counter);

for (int i = 0; i < 2; i++)
{
printf("%d ", wardsToSave[i]);
mapAppend(wardsToSave[i], current_pos[0] + 1, i);
}
// printf("\r\n");
}
else if (current_pos[0] >= 3)
{

if (last_turn > 0)
{
for (int i = 0; i < 2; i++)
{
printf("%d ", wardsToSave[i]);
mapAppend(wardsToSave[i], 5, i);
}
// printf("target ll:%d %d %d\r\n", 5, current_pos[1], status_counter);
}
else
{
for (int i = 0; i < 2; i++)
{
// printf("%d ", wardsToSave[i]);
mapAppend(wardsToSave[i], 4, i);
}
printf("target ll:%d %d %d\r\n", 4, current_pos[1], status_counter);
}
}
}
else
{
// 这里说明没找到目标?
cam_getTwoLetter(wardsToSave);

// printf("%d\r\n", last_turn);
if (current_pos[0] < 2)
{
// printf("target ll:%d %d %d\r\n", current_pos[0] + 1, current_pos[1], status_counter);

for (int i = 0; i < 2; i++)
{
// printf("%d ", wardsToSave[i]);
mapAppend(wardsToSave[i], current_pos[0] + 1, i);
}
}
else if (current_pos[0] > 3)
{
if (last_turn > 0)
{
for (int i = 0; i < 2; i++)
{
// printf("%d ", wardsToSave[i]);
mapAppend(wardsToSave[i], 5, i);
}
// printf("target ll:%d %d %d\r\n", 5, current_pos[1], status_counter);
}
else
{
for (int i = 0; i < 2; i++)
{
printf("%d ", wardsToSave[i]);
mapAppend(wardsToSave[i], 4, i);
}
// printf("target ll:%d %d %d\r\n", 4, current_pos[1], status_counter);
}
}
}
}

break;
case 7:
openIRAndMotor();
if (crossDetect() && cross_flag == 0)
{
if (start_pos_lev == 4)
{
// 说明起点到终点 肯定是4 5
current_pos[0] = 5;
}
else if (start_pos_lev == 5)
{
current_pos[0] = 4;
}
osDelay(CROSS_STOP_DELAY); // 让车过十字路口
cross_flag = 1; // 标记已检测到十字路口
if (current_pos[0] == 3)
{
if (last_turn > 0)
{
current_pos[0] = 5;
}
else if (last_turn < 0)
{
current_pos[0] = 4;
}
}
else
{
if (current_pos[0] < 3)
{
current_pos[0]++; // 更新层数
}
}
taskStatusToggle();
}
else if (!crossDetect())
{
cross_flag = 0; // 没有检测到十字路口,重置标志
}
break;
case 8:

// 这里开始转弯
// printf("%d\r\n",letter_side);
if (letter_side > 0)
{
cross_flag = 0;
openIROnly();
pidTurn(-TURN_SPD, 0);
if (crossAfterDetect())
{
taskStatusToggle();
}
}
else if (letter_side < 0)
{
cross_flag = 0;
openIROnly();
pidTurn(TURN_SPD, 0);
if (crossAfterDetect())
{
taskStatusToggle();
}
}

break;
case 9:
if (current_pos[0] < 3 || current_pos[0] > 3)
{
openIRAndMotor();
if (isAwayFromTract())
{
osDelay(CROSS_STOP_DELAY); // 让车停在终点
// for (int i = 0; i < 2; i++)
// {
// printf("待存药房 %d ", wardsToSave[i]);
// mapAppend(wardsToSave[i], current_pos[0], i);
// }

printf("Arrived at warehouse\r\n");
closeIRAndMotor();
pidTurn(0, 0);
// printf("%d\r\n", last_turn);
if (last_turn > 0)
{
current_pos[1] = 1;
}
else if (last_turn < 0)
{
current_pos[1] = 0;
}
taskStatusToggle();
}
}
else
{
status_counter = 6; // 这是去往45两层的逻辑
}
if (letter_side != 0)
{
last_turn = letter_side;
}
letter_side = 0; // 清零转弯变量
printMap();
break;
case 10:
// printf("ll:%d %d %d\r\n", current_pos[0], current_pos[1], status_counter);
if (Drug_Array[target_idx] == 1)
{
// osDelay(1000);
pidTurn(TURN_SPD, 0);
if (crossAfterDetect() == 1)
{
target_idx++; // 更新目标索引为下一个目标
route_pointer = 0; // 重置路线执行指针
route_length = 0; // 重置路线长度
status_counter = 0; // 重置状态计数器以便下次执行
led0Toggle();
}
}
break;
}
}
// 阻塞式闪烁

2.2.2 小车路径规划部分

第一步,存图。 我们将地图分为以下层数: 地图

对应数组如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef enum
{
WARD_A = 0,
WARD_B,
WARD_C,
WARD_D,
WARD_E,
WARD_F,
WARD_G,
WARD_H,
TERMINAL,
AMPTY
} Node_Name;

Node_Name map[6][2] = {{TERMINAL, TERMINAL},
{WARD_A, WARD_B}, // 靠前为左
{WARD_C, WARD_D},
{AMPTY, AMPTY},
{WARD_E, WARD_G},
{WARD_F, WARD_H}};

第二步,获得起点坐标,和终点坐标

1
2
3
uint8_t current_pos[2] = {0, 0}; // 当前所在位置,初始为终点/药房 第一个为层数 第二个为病房位置
// Node_Name target_arr[3] = {WARD_D, WARD_C, WARD_B}; // 任务目标数组,存储需要访问的病房节点 按照优先级排列
Node_Name target_arr[3] = {AMPTY, AMPTY, AMPTY}; // 比赛时数组
接下来就是生成路线了。

generate_route

原理十分简单,就是针对起点在不同位置写不同的路径段,每个路径段以路口作为分割。

最终生成类似如下路径:

1
2
3
4
5
6
7
8
9
10
11
typedef enum
{
ACT_STRAIGHT = 0,
ACT_LEFT,
ACT_RIGHT,
ACT_BACK,
ACT_NONE
} Move_Action;


[ACT_STRAIGHT,ACT_LEFT,ACT_STRAIGHT,ACT_RIGHT,ACT_STRAIGHT,ACT_STRAIGHT,ACT_STRAIGHT]

具体代码以其中一段为例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
if (start_pos[0] == 2) //第二层作为起点
{
if (start_pos[1] == 0)
{
if (start_pos[0] > target_pos[0])
{
routeAppend(ACT_RIGHT, routeToAppend, route_length_addr);
}
else if (start_pos[0] < target_pos[0])
{
routeAppend(ACT_LEFT, routeToAppend, route_length_addr);
}
}
else if (start_pos[1] == 1)
{
if (start_pos[0] > target_pos[0])
{
routeAppend(ACT_LEFT, routeToAppend, route_length_addr);
}
else if (start_pos[0] < target_pos[0])
{
routeAppend(ACT_RIGHT, routeToAppend, route_length_addr);
}
}
routeAppend(ACT_STRAIGHT, routeToAppend, route_length_addr);
if (target_pos[0] >= 4 && target_pos[0] <= 5)
{
generate_layer45_route(target, ideal_pos, routeToAppend, route_length_addr);
}
else
{
if (target_pos[1] == 1)
{
routeAppend(ACT_LEFT, routeToAppend, route_length_addr);
}
else if (target_pos[1] == 0)
{
routeAppend(ACT_RIGHT, routeToAppend, route_length_addr);
}
routeAppend(ACT_STRAIGHT, routeToAppend, route_length_addr);
}
}

但是写这么多也是比较冗杂,本题也有许多路线是可以封装的例如第四五两层路线

只要目的地为四五两层,那必然经过第三层路口,所以从第三层路口到四五两层病房路线都是固定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* @brief
*
* @param target
* @param ideal_pos
*/
void generate_layer45_route(Node_Name target, uint8_t ideal_pos[2], Move_Action routeToAppend[], int *route_length_addr)
{
uint8_t target_pos[2];
findWardPos(target, target_pos);

if (target_pos[0] == 4)
{
routeAppend(ACT_LEFT, routeToAppend, route_length_addr);
routeAppend(ACT_STRAIGHT, routeToAppend, route_length_addr);
if (target_pos[1] == 0)
{
routeAppend(ACT_LEFT, routeToAppend, route_length_addr);
}
else if (target_pos[1] == 1)
{
routeAppend(ACT_RIGHT, routeToAppend, route_length_addr);
}

routeAppend(ACT_STRAIGHT, routeToAppend, route_length_addr);
}
else if (target_pos[0] == 5)
{
routeAppend(ACT_RIGHT, routeToAppend, route_length_addr);
routeAppend(ACT_STRAIGHT, routeToAppend, route_length_addr);
if (target_pos[1] == 0)
{
routeAppend(ACT_LEFT, routeToAppend, route_length_addr);
}
else if (target_pos[1] == 1)
{
routeAppend(ACT_RIGHT, routeToAppend, route_length_addr);
}
routeAppend(ACT_STRAIGHT, routeToAppend, route_length_addr);
}
}

这就是我们这次的路径规划。

那么这些都是当目的地为病房时,去程的路径规划,当小车要返回时的路径如何生成? 其实很简单,返程的路径就是去程路径的巡回。

例如以下药房去往C路径:

直走,直走,左转,直走

当小车掉头后去往起点路径就是

直走,右转,直走,直走

不同处就是转弯相反而已。

这就有如下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void generate_reverse_route(Move_Action routeToreverse[], Move_Action routeToAppend[], int routeToReverseLength, int *route_length_addr)
{
for (int i = routeToReverseLength - 1; i >= 0; i--)
{
Move_Action action = routeToreverse[i];
Move_Action reversed_action;
switch (action)
{
case ACT_STRAIGHT:
reversed_action = ACT_STRAIGHT;
break;
case ACT_LEFT:
reversed_action = ACT_RIGHT;
break;
case ACT_RIGHT:
reversed_action = ACT_LEFT;
break;
case ACT_BACK:
reversed_action = ACT_BACK;
break;
default:
reversed_action = ACT_NONE;
break;
}
routeAppend(reversed_action, routeToAppend, route_length_addr);
}
}
这就生成了回药房路线。 ## 2.4 多任务

如此多任务如何安排呢? 若都放在main函数里也能运行,但是比较耦合,所以我使用了freertos来处理多任务。

如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
/* USER CODE BEGIN Header */
/**
******************************************************************************
* File Name : freertos.c
* Description : Code for freertos applications
******************************************************************************
* @attention
*
* Copyright (c) 2025 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "timers.h"
#include "queue.h"

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

float pitch = 0;
float roll = 0;
float yaw = 0;
float motor_bl_set_spd = 0;
float motor_br_set_spd = 0;
float yaw_set = 0;
int speed_set = 0;
uint8_t is_yaw_initialized = 0;
uint8_t yaw_pid_en = 1;
uint8_t ir_en = 0;
uint8_t ir_pid_en = 1;
uint8_t ir_blk;
uint8_t ir_blk_en = 1;
fp32 ir_output = 0;
uint8_t ir_motor_en = 0;
static TimerHandle_t pidTimerHandle;
pid_type_def motor_bl_pid;
pid_type_def motor_br_pid;
pid_type_def ir_pid;
fp32 pid_motor_bl_params[3] = {1.2f, 0.1f, 0.0f};
fp32 pid_motor_br_params[3] = {1.2f, 0.1f, 0.0f};
fp32 pid_ir_params[3] = {0.15f, 0.0f, 0.07f};

extern char task_flag;
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */

/* USER CODE END Variables */
/* Definitions for defaultTask */
osThreadId_t defaultTaskHandle;
const osThreadAttr_t defaultTask_attributes = {
.name = "defaultTask",
.stack_size = 256 * 4,
.priority = (osPriority_t)osPriorityLow,
};
/* Definitions for routeTask */
osThreadId_t routeTaskHandle;
const osThreadAttr_t routeTask_attributes = {
.name = "routeTask",
.stack_size = 512 * 4,
.priority = (osPriority_t)osPriorityNormal,
};
/* Definitions for irTask */
osThreadId_t irTaskHandle;
const osThreadAttr_t irTask_attributes = {
.name = "irTask",
.stack_size = 256 * 4,
.priority = (osPriority_t)osPriorityNormal,
};
/* Definitions for IRBlkQueue */
osMessageQueueId_t IRBlkQueueHandle;
const osMessageQueueAttr_t IRBlkQueue_attributes = {
.name = "IRBlkQueue"};

/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */

void pidTurn(float angle, float speed)
{

motor_bl_set_spd = -(speed - angle);
motor_br_set_spd = -(speed + angle);
}

void pidTimerCallback(TimerHandle_t xTimer)
{
int track_error = 0;
char track_flag = 0;
cam_getIR_data(&track_error, &track_flag, NULL);
// printf("task_flag: %d\r\n", task_flag);
// 200Hz PID计算
fp32 lb_motor_output = PID_calc(&motor_bl_pid, (fp32)encoder_bl_spd, motor_bl_set_spd);
fp32 rb_motor_output = PID_calc(&motor_br_pid, (fp32)encoder_br_spd, motor_br_set_spd);
if (ir_en == 1)
{
fp32 ir_correction = PID_calc(&ir_pid, (fp32)track_error, 0);
if (ir_motor_en == 1)
{
pidTurn(ir_correction, NORMAL_SPEED);
}

}
else
{
pidTurn(0, 0); // 停车
}

Motor_Set_Duty(lb_motor_output, MOTOR_LEFT_BACK);
Motor_Set_Duty(rb_motor_output, MOTOR_RIGHT_BACK);

}
void imuTimerCallback(TimerHandle_t xTimer)
{
}
int compareTaskName(const void *a, const void *b)
{
TaskStatus_t *taskA = (TaskStatus_t *)a;
TaskStatus_t *taskB = (TaskStatus_t *)b;
return strcmp(taskA->pcTaskName, taskB->pcTaskName);
}

/* USER CODE END FunctionPrototypes */

void StartDefaultTask(void *argument);
void routeTaskFunction(void *argument);
void IRTaskFunction(void *argument);

void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */

/* Hook prototypes */
void vApplicationStackOverflowHook(xTaskHandle xTask, signed char *pcTaskName);

/* USER CODE BEGIN 4 */
void vApplicationStackOverflowHook(xTaskHandle xTask, signed char *pcTaskName)
{
/* Run time stack overflow checking is performed if
configCHECK_FOR_STACK_OVERFLOW is defined to 1 or 2. This hook function is
called if a stack overflow is detected. */
}
/* USER CODE END 4 */

/**
* @brief FreeRTOS initialization
* @param None
* @retval None
*/
void MX_FREERTOS_Init(void)
{
/* USER CODE BEGIN Init */
PID_init(&motor_bl_pid, PID_DELTA, pid_motor_bl_params, 100.0f, 50.0f);
PID_init(&motor_br_pid, PID_DELTA, pid_motor_br_params, 100.0f, 50.0f);
PID_init(&ir_pid, PID_POSITION, pid_ir_params, 100.0f, 100.0f);
pidTimerHandle = xTimerCreate("PIDCalculator", 10, pdTRUE, NULL, pidTimerCallback);
xTimerStart(pidTimerHandle, 0);

/* USER CODE END Init */

/* USER CODE BEGIN RTOS_MUTEX */
/* add mutexes, ... */
/* USER CODE END RTOS_MUTEX */

/* USER CODE BEGIN RTOS_SEMAPHORES */
/* add semaphores, ... */
/* USER CODE END RTOS_SEMAPHORES */

/* USER CODE BEGIN RTOS_TIMERS */
/* start timers, add new ones, ... */
/* USER CODE END RTOS_TIMERS */

/* Create the queue(s) */
/* creation of IRBlkQueue */
IRBlkQueueHandle = osMessageQueueNew(20, sizeof(uint8_t), &IRBlkQueue_attributes);

/* USER CODE BEGIN RTOS_QUEUES */
/* add queues, ... */
/* USER CODE END RTOS_QUEUES */

/* Create the thread(s) */
/* creation of defaultTask */
defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);

/* creation of routeTask */
routeTaskHandle = osThreadNew(routeTaskFunction, NULL, &routeTask_attributes);

/* creation of irTask */
irTaskHandle = osThreadNew(IRTaskFunction, NULL, &irTask_attributes);

/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
/* USER CODE END RTOS_THREADS */

/* USER CODE BEGIN RTOS_EVENTS */
/* add events, ... */
/* USER CODE END RTOS_EVENTS */
}

/* USER CODE BEGIN Header_StartDefaultTask */
/**
* @brief Function implementing the defaultTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */
for (;;)
{
osDelay(30); // 保持原有延时,平衡响应速度和CPU占用
Drug_Scan();
cam2_getData();
uart1Clear();
}
/* USER CODE END StartDefaultTask */
}

/* USER CODE BEGIN Header_routeTaskFunction */
/**
* @brief Function implementing the routeTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_routeTaskFunction */
void routeTaskFunction(void *argument)
{
/* USER CODE BEGIN routeTaskFunction */
// Move_Action test_route[ROUTE_MAX_LENGTH] = {ACT_NONE}; // 路径测试

/* Infinite loop */
for (;;)
{
osDelay(7);

// generate_return_route(route, &route_length); // 生成返回路线

routeProcess();
}
/* USER CODE END routeTaskFunction */
}

/* USER CODE BEGIN Header_IRTaskFunction */
/**
* @brief Function implementing the irTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_IRTaskFunction */
void IRTaskFunction(void *argument)
{
/* USER CODE BEGIN IRTaskFunction */
uint8_t key_state = 0; // 0: not pressed, 1: pressed
/* Infinite loop */
for (;;)
{
cam1_getData();
// printf("key_state: %d\r\n", key_state);
if ((!HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin)) && (key_state == 0)) // 检测到按键按下且之前未按下
{
taskStatusToggle();
key_state = 1;
}
else if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) && (key_state == 1)) // 检测到按键松开且之前按下
{
key_state = 0;
}
// printf("IR error: %d, flag: %d\r\n", track_error, track_flag);
osDelay(10);
}
/* USER CODE END IRTaskFunction */
}

/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */

/* USER CODE END Application */


2026年CCUT电子设计竞赛报告
http://jiangno.com/2026/04/19/26_04_19_ccut_edc/
作者
江の
发布于
2026年4月19日
许可协议