上节课问题

用谷歌的最新模型问:

​ 提问方式:背景+讲解+问题 问题越详细越好

举例

​ 我是一个刚学习嵌入式开发的小白 请你帮我讲解一下什么是硬件抽象层 什么是HAL库 同样都是调用API 为什么HAL库比标准库更好一致

答案

​ HAL就是一个逻辑层 1. 它像一个翻译官或者适配器,提供接口 2.提高了可移植性

举例:1.点灯 不需要再管GPIO的模型了

  1. F0系列的代码很容易移植到F4系列

一、任务调度器概念

  • 为什么要减速:因为while循环扫描太快了我们不需要那么频繁的执行外设程序

准确的告诉你什么时候干什么,官家

我们嵌入式资源有限,肯定要间断执行

  1. 定时执行任务(按键10ms 数码管50ms 超声波100ms ADDA 80ms ds1302160ms ds18b20 300ms)
  2. 循环执行(都在while里面都是循环的)
  3. 非阻塞设计(没有while)
  4. 模块化编程

二、如何撰写任务调度器

1. C语言结构体基础

  1. 结构体定义
1
2
3
4
struct Task{//关键字+结构体名字


}
  1. 结构体内存布局
  • 连续存储
  • 结构体的地址是自身大小的整数倍
  1. 变量声明与初始化(我定义之后肯定要求声明,这个定义结构体和普通的定义变量还有区别,就是说它定义结构体定义的是一种数据类型,还忒再按照这个类型去声明)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//声明
//1.数据类型表示为struct Task
struct Task{


};
struct Task task1={"",""}//用一个列表去按顺序定义

//2. 用typedef定义一个数据类型
typedef struct{

}Task_t
Task_t task2={"",""}//挺常用的,调用时就只需要在前面加上Task_t
//3. 用初始化进行调控
Task_t task3 ={
"name" = "压力检测"
"priority" = 3;
}//不推荐,因为定义和声明合在一起了,一点都不直观

2.访问结构体成员

!!!点task1.name访问的就是结构体成员,而箭头运算符task->name访问的是结构体指针的成员

1
2
3
4
5
6
7
8
//使用点运算符访问成员
Task_t task={“显示刷新",5};
printf(“任务名称:%s\n",task.name);
task.priority=4//修改优先级
//使用箭头运算符访问指针成员
Task_t *p_task = &task;
printf(“任务优先级:%d\n",p_task->priority);
p_task->priority++;//增加优先级

现在对我的C语言进行补充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//首先在这段代码里,task是声明后的结构体变量,而p_task是指针,Task_t *是类型声明,也就是告诉编译器p_task的类型是Task_t的结构体指针,取地址符&是取变量task的地址,取到的地址才能作为指针变量
//所以经过声明(或者说是初始化),p_task就是结构体变量task的指针,可通过->访问结构体成员,而task就是结构体变量,可以通过.来访问结构体成员
//如果我硬要通过点来访问,我可以通过解运算符星号(*p_task).priority来进行访问,也就是这样进行解算
//对比着普通的访问方式
//普通变量指针:
int num = 10;
int *p_num = #
*p_num = 20;
printf("%d",*p_num);
//同样我们可以看出,在普通变量定义结构体指针时同样需要int*以及取地址符号&进行配合
//可以看出在普通变量指针中,只能通过*作为解引用运算符进行数据读取
//结构体变量指针:
Task_t task = {"显示刷新", 5};
Task_t *p_task = &task;
p_task->priority = 4; // 使用箭头操作符访问成员
// 或者
(*p_task).priority = 4; // 使用解引用+点操作符也可以
//在结构体变量指针中,可以通过这两种方法同时进行引用
  • 为什么要用地址去做(经典问题)

函数有时需要传入参数进行处理,有的函数只需要传入数据(也就是变量)进行运算然后返回一个特定的值,在这种情况下,我不需要修改传入参数的值,所以我直接传入变量

蓝桥杯里:float rd_temperature unsigned int Ut_Wave_Data

而有些函数需要修改我传入数据的值,这时候就需要传入指针,最典型的例子:

Read_Rtc(*Rtc);

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
//值传递,显示出普通变量以及指针变量不同的效果
#include <stdio.h>

// 值传递示例 - 计算平方
int square(int num) {
int result = num * num;
num = 0; // 这里修改参数不会影响原始变量
return result;
}

// 结构体值传递示例
typedef struct {
char name[50];
int priority;
} Task_t;

// 显示任务信息但不修改它
void displayTask(Task_t task) {
printf("任务名称: %s, 优先级: %d\n", task.name, task.priority);
task.priority = 99; // 这个修改只影响局部副本,不影响原始结构体
}

int main() {
int x = 5;
printf("x的平方是: %d\n", square(x));
printf("函数调用后x的值: %d\n", x); // 输出5,x未被修改

Task_t myTask = {"数据备份", 3};
displayTask(myTask);
printf("函数调用后优先级: %d\n", myTask.priority); // 输出3,未被修改

return 0;
}

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
//指针传递
#include <stdio.h>

// 指针传递示例 - 交换两个数
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}

// 结构体指针传递示例
typedef struct {
char name[50];
int priority;
} Task_t;

// 提升任务优先级
void upgradePriority(Task_t *task) {
// 使用箭头运算符访问指针成员
task->priority += 1;
printf("任务 \"%s\" 优先级已提升到 %d\n", task->name, task->priority);
}

int main() {
int m = 10, n = 20;
printf("交换前: m = %d, n = %d\n", m, n);

swap(&m, &n); // 传递地址
printf("交换后: m = %d, n = %d\n", m, n); // 输出 m = 20, n = 10

Task_t myTask = {"系统更新", 2};
printf("原始优先级: %d\n", myTask.priority);

upgradePriority(&myTask); // 传递结构体地址
printf("函数调用后优先级: %d\n", myTask.priority); // 输出3,被修改

return 0;
}

所以指针传递可以改变原始数据,也就是传进来的数据

指针传递的另一个优点:

效率高

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
>#include <stdio.h>
>#include <time.h>

>typedef struct {
int data[10000];
char text[50000];
>} BigStruct_t;

>void processByValue(BigStruct_t data) {
// 简单处理避免优化
data.data[0] += 1;
>}

>void processByPointer(BigStruct_t *data) {
// 相同的操作
data->data[0] += 1;
>}

>int main() {
BigStruct_t bigData = {0};
clock_t start, end;
int i, iterations = 100000;

// 测试值传递
start = clock();
for(i = 0; i < iterations; i++) {
processByValue(bigData);
}
end = clock();
printf("值传递耗时: %f 秒\n", (double)(end - start) / CLOCKS_PER_SEC);

// 测试指针传递
start = clock();
for(i = 0; i < iterations; i++) {
processByPointer(&bigData);
}
end = clock();
printf("指针传递耗时: %f 秒\n", (double)(end - start) / CLOCKS_PER_SEC);

return 0;
>}

实验结果:

指针传递用时:几秒钟

值传递用时:几分钟

3. 结构体拓展

结构体嵌套以及结构体数组//暂时先不重点了解,等到后面深入了再了解

4.调度器结构体详解

1
2
3
4
5
6
//定义任务结构体
typedef struct {
void (*task_func)(void); // 函数指针
uint32_t rate_ms;
uint32_t last_run;
} scheduler_task_t;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//关于函数指针的写法
//普通指针
int *p_num;
//结构体指针
Task *p_task; // Task结构体已经用typedef提前声明过了
//函数指针
void (*task_func)(void); // 函数指针
//函数定义
void task_func(void) {
// 函数实现代码
}
//如果函数传递了参数
void task_func(int time){}
//函数指针
void (*task_func_ptr)(int)
//使用时
void (*my_function_ptr)(int) = task_func;
// 使用函数指针调用函数
my_function_ptr(1000); // 等同于task_func(1000);
  • 问题:为什么普通指针(包括结构体指针)需要解码后引用,函数指针不需要解码后引用,函数指针不需要加*
1
2
3
4
5
6
7
8
9
// 普通数据指针
int x = 10;
int *p_num = &x;
*p_num = 20; // 需要使用*解引用操作符

// 函数指针
void (*my_function_ptr)(int) = task_func;
my_function_ptr(1000); // 直接调用,无需使用*

解答:1. 语法上便利 2. 实际上两种写法完全等价

1
2
my_function_ptr(1000);     // 简洁形式
(*my_function_ptr)(1000); // 显式解引用形式

本质上,数据指针指向的是内存,而函数指针就是一个入口点,直接就可以使用,使用时不会产生歧义

具体分析:

  1. task_func: void (*task_func)(void)

​ 函数指针,指向任务的执行函数,在调度时被调用

  1. rate_ms任务执行周期
  2. last_run记录上一次执行的时间戳

5.具体代码

1
2
3
4
5
6
7
//定义任务结构体
typedef struct {
void (*task_func)(void); // 函数指针
uint32_t rate_ms;
uint32_t last_run;
} scheduler_task_t;

1
2
3
4
5
6
7
8
9
10
11
12
13
//通过任务结构体定义一下数组结构体
//任务数组结构体是辅助调度器进行轮询工作的
// 全局变量,用于存储任务数量
uint8_t task_num;

// 静态任务数组,每个任务包含任务函数、执行周期(毫秒)和上次运行时间(毫秒)
static scheduler_task_t scheduler_task[] =
{
{Led_Proc, 1, 0}, // LED控制任务:周期1ms
{Key_Proc, 10, 0}, // 按键扫描任务:周期10ms
{Sensor_Proc, 100, 0}, // 传感器读取任务:周期100ms
{Comm_Proc, 50, 0} // 通信处理任务:周期50ms
};
1
2
3
4
5
6
7
8
9
10
//初始化函数
/**
* @brief 调度器初始化函数
* 计算任务数组的元素个数,并将结果存储在 task_num 中
*/
void scheduler_init(void)
{
// 计算任务数组的元素个数,并将结果存储在 task_num 中
task_num = sizeof(scheduler_task) / sizeof(scheduler_task_t);
}//task_num是全局变量可以进行改变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @brief 调度器运行函数
* 遍历任务数组,检查是否有任务需要执行。如果当前时间已经超过任务的执行周期,则执行该任务并更新上次运行时间
*/
void scheduler_run(void)
{
// 遍历任务数组中的所有任务
for (uint8_t i = 0; i < task_num; i++)
{
// 获取当前的系统时间(毫秒)
uint32_t now_time = HAL_GetTick();

// 检查当前时间是否达到任务的执行时间
if (now_time >= scheduler_task[i].rate_ms + scheduler_task[i].last_run)
{
// 更新任务的上次运行时间为当前时间
scheduler_task[i].last_run = now_time;

// 执行任务函数
scheduler_task[i].task_func();
}
}
}

HAL_GetTick()函数的调用使用

  • 后面的代码为什么就不解释了,因为我感觉实战中会用就行了

三、实例代码:

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
#include "scheduler.h"

// 全局变量,用于存储任务数量
uint8_t task_num;

typedef struct {
void (*task_func)(void);
uint32_t rate_ms;
uint32_t last_run;
} task_t;

// 任务函数声明
void led_proc(void);
void key_proc(void);
void sensor_proc(void);
void comm_proc(void);

// 静态任务数组,每个任务包含任务函数、执行周期(毫秒)和上次运行时间(毫秒)
static task_t scheduler_task[] =
{
{led_proc, 1000, 0}, // LED闪烁任务:周期1000ms (1秒)
{key_proc, 10, 0}, // 按键扫描任务:周期10ms
{sensor_proc, 100, 0}, // 传感器读取任务:周期100ms
{comm_proc, 50, 0} // 通信处理任务:周期50ms
};

/**
* @brief 调度器初始化函数
* 计算任务数组的元素个数,并将结果存储在 task_num 中
*/
void scheduler_init(void)
{
// 计算任务数组的元素个数,并将结果存储在 task_num 中
task_num = sizeof(scheduler_task) / sizeof(task_t);
}

/**
* @brief 调度器运行函数
* 遍历任务数组,检查是否有任务需要执行。如果当前时间已经超过任务的执行周期,则执行该任务并更新上次运行时间
*/
void scheduler_run(void)
{
// 遍历任务数组中的所有任务
for (uint8_t i = 0; i < task_num; i++)
{
// 获取当前的系统时间(毫秒)
uint32_t now_time = HAL_GetTick();

// 检查当前时间是否达到任务的执行时间
if (now_time >= scheduler_task[i].rate_ms + scheduler_task[i].last_run)
{
// 更新任务的上次运行时间为当前时间
scheduler_task[i].last_run = now_time;

// 执行任务函数
scheduler_task[i].task_func();
}
}
}

// 任务函数实现
void led_proc(void)
{
// 切换LED状态
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}

void key_proc(void)
{
// 读取按键状态
if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
{
// 按键被按下,执行相应操作
}
}

void sensor_proc(void)
{
// 读取传感器数据
uint16_t sensor_value = HAL_ADC_GetValue(&hadc1);

// 处理传感器数据
}

void comm_proc(void)
{
// 处理通信数据
if (HAL_UART_GetState(&huart1) == HAL_UART_STATE_READY)
{
// 发送数据
}
}

四、进阶代码

1. 优先级调度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//优先级调度
//为任务添加优先级属性,使重要任务优先执行。在某些场景下,我们需要确保关键任务能够及时响应,如安全监控、通信处理等。
typedef struct {
void (*task_func)(void);
uint32_t rate_ms;
uint32_t last_run;
uint8_t priority; // 优先级,数值越小优先级越高
} priority_task_t;

// 任务按优先级排序函数
void sort_tasks_by_priority(void) {
// 使用冒泡排序按优先级排序
for (uint8_t i = 0; i < task_num - 1; i++) {
for (uint8_t j = 0; j < task_num - i - 1; j++) {
if (scheduler_task[j].priority > scheduler_task[j + 1].priority) {
// 交换任务
priority_task_t temp = scheduler_task[j];
scheduler_task[j] = scheduler_task[j + 1];
scheduler_task[j + 1] = temp;
}
}
}
}

2. 低功耗管理

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
//低功耗管理
//通过调度器实现低功耗模式的切换与管理。在电池供电的设备中,电源管理至关重要。当没有任务需要立即执行时,系统可以进入低功耗模式以延长电池寿命。
// 在调度器运行函数中添加低功耗管理
void scheduler_run(void)
{
bool all_tasks_idle = true;
uint32_t time_to_next_task = UINT32_MAX;
uint32_t now_time = HAL_GetTick();

// 检查是否有任务需要立即执行
for (uint8_t i = 0; i < task_num; i++)
{
uint32_t time_to_task = (scheduler_task[i].last_run +
scheduler_task[i].rate_ms) - now_time;

if (time_to_task == 0) {
// 有任务需要立即执行
scheduler_task[i].last_run = now_time;
scheduler_task[i].task_func();
all_tasks_idle = false;
} else if (time_to_task < time_to_next_task) {
// 记录最近需要执行的任务时间
time_to_next_task = time_to_task;
}
}

// 如果所有任务都不需要立即执行,进入低功耗模式
if (all_tasks_idle && time_to_next_task > MIN_SLEEP_TIME) {
// 进入低功耗模式直到下一个任务时间或外部中断
HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
}
}

3. 动态任务管理

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
//动态任务管理
//在运行时添加、删除或修改任务的能力。静态任务数组在某些场景下可能不够灵活,比如根据系统状态需要动态增减任务时。动态任务管理使调度器更加灵活。
#define MAX_TASKS 10

typedef struct {
void (*task_func)(void);
uint32_t rate_ms;
uint32_t last_run;
bool active; // 任务是否激活
} dynamic_task_t;

dynamic_task_t scheduler_task[MAX_TASKS];
uint8_t task_num = 0;

// 添加任务
uint8_t add_task(void (*task_func)(void), uint32_t rate_ms) {
if (task_num >= MAX_TASKS) {
return 0xFF; // 任务已满
}

scheduler_task[task_num].task_func = task_func;
scheduler_task[task_num].rate_ms = rate_ms;
scheduler_task[task_num].last_run = HAL_GetTick();
scheduler_task[task_num].active = true;

return task_num++;
}

// 删除任务
bool remove_task(uint8_t task_id) {
if (task_id >= task_num) {
return false;
}

// 移动数组元素以填补空缺
for (uint8_t i = task_id; i < task_num - 1; i++) {
scheduler_task[i] = scheduler_task[i + 1];
}

task_num--;
return true;
}

// 暂停任务
bool pause_task(uint8_t task_id) {
if (task_id >= task_num) {
return false;
}

scheduler_task[task_id].active = false;
return true;
}

// 恢复任务
bool resume_task(uint8_t task_id) {
if (task_id >= task_num) {
return false;
}

scheduler_task[task_id].active = true;
scheduler_task[task_id].last_run = HAL_GetTick();
return true;
}


课下作业

  1. 为什么for循环中的两句代码不能调换顺序

  2. 将V4T模版修改为调度器模版