C语言嵌入式开发编程篇:typedef与回调函数

typedef关键字


C语言中的 ‌typedef‌ 是用于为已有的数据类型定义新名称(别名)的关键字。它并不创建新的数据类型,而是为现有类型提供更易读、更简洁的标识符,从而提高代码的可读性和可维护性。
typedef 的基本语法非常简单,其格式为:
typedef 原数据类型 新类型名;
例如:可以为基本数据类型int取一个别名Integer:​

typedef int Integer;​

为int定义别名后,在后续的代码中,就可以使用Integer来声明int类型的变量:​

Integer a = 10;​

这和直接使用int a = 10;的效果是完全一样的,在代码中使用Integer比使用int可以让代码阅读者更清晰地理解这个变量的作用。

typedef与函数指针

使用typedef可以定义函数指针类型:

// 定义函数指针类型 CompareFunc
typedef int (*CompareFunc)(const void*, const void*);

// 使用示例
int compare_ints(const void* a, const void* b) {
return (*(int*)a - *(int*)b);
}
CompareFunc cmp = compare_ints; // 直接使用类型名

 

上述代码使用 typedef 创建了名为 CompareFunc 的函数指针类型,该类型指针指向的函数接受两个 const void* 参数并返回 int 值。然后定义了函数compare_ints(),将函数名赋值给函数指针。

使用 typedef 命名函数指针类型可以显著提升代码的可读性和灵活性,下面给出具体的示例。

示例1:简化复杂函数指针声明‌


‌没有 typedef 的写法‌:

int (*func1)(int, float); // 声明一个函数指针变量
int (*func_array[5])(int, float); // 声明包含5个函数指针的数组

‌使用 typedef 的写法‌:

typedef int (*MathFunc)(int, float); // 定义类型别名

MathFunc func1; // 直接声明变量
MathFunc func_array[5]; // 声明数组

‌好处‌:
避免重复书写复杂的函数指针语法。代码更简洁,类型意义更明确(MathFunc 比 int (*)(int, float) 更易理解)。

‌示例2:增强回调函数代码的可维护性‌


‌没有 typedef‌:

void register_callback(int (*callback)(const char*, int)) {
// 使用回调函数
}
‌调用时‌:
int my_callback(const char* s, int x) { /* ... */ }
register_callback(my_callback);

‌使用 typedef‌:

typedef int (*CallbackType)(const char*, int); // 定义类型别名

void register_callback(CallbackType callback) { // 参数类型清晰
// 使用回调函数
}
‌调用时‌:
CallbackType my_callback = /* ... */;
register_callback(my_callback);

‌好处‌:
函数参数类型一目了然,避免歧义。修改回调函数类型时只需修改 typedef 定义,无需逐个修改函数声明。

‌示例3:作为结构体成员‌


‌没有 typedef‌:

struct Calculator {
int (*add)(int, int); // 加法函数指针
int (*subtract)(int, int); // 减法函数指针
};

‌使用 typedef‌:

typedef int (*ArithmeticOp)(int, int); // 定义统一的运算操作类型
struct Calculator {
ArithmeticOp add; // 成员类型直接使用别名
ArithmeticOp subtract;
};

‌好处‌:
结构体成员类型清晰,明确表达设计意图。后续新增乘法、除法等操作时,只需扩展 ArithmeticOp 类型的使用。

‌示例4:函数返回函数指针‌


‌没有 typedef‌:

int (*get_operation(char op))(int, int) { // 返回类型难以阅读
if (op == '+') return &add;
else return &subtract;
}

‌使用 typedef‌:

typedef int (*Operation)(int, int); // 定义类型别名

Operation get_operation(char op) { // 返回类型简洁明了
if (op == '+') return &add;
else return &subtract;
}

‌好处‌:

函数返回值类型清晰,避免嵌套的指针语法。

任务调度器案例

下面实现一个任务调度器,主要实现任务的注册、执行和管理功能。​

一、定义任务的状态枚举类型和任务结构体 ​

// 任务状态枚举​
typedef enum {​
TASK_READY,​
TASK_RUNNING,​
TASK_BLOCKED,​
TASK_FINISHED​
} TaskStatus;​
​
// 任务结构体​
typedef struct Task {​
TaskStatus status;​
void (*taskFunction)(); // 任务函数指针​
struct Task *next;​
} Task;​


上述代码使用 typedef 定义了TaskStatus枚举类型来表示任务的不同状态,同时定义了Task结构体,其中包含任务的状态、指向任务函数的指针taskFunction以及指向下一个任务的指针next 。这里的taskFunction就是一个函数指针,它指向具体的任务执行函数。​

二、实现任务调度器的核心功能,包括任务注册和任务执行​


// 任务链表头指针​
Task *taskList = NULL;​
​
// 注册任务​
void registerTask(void (*task)()) {​
Task *newTask = (Task *)malloc(sizeof(Task));​
newTask->status = TASK_READY;​
newTask->taskFunction = task;​
newTask->next = taskList;​
taskList = newTask;​
}​
​
// 执行任务​
void executeTasks() {​
Task *currentTask = taskList;​
while (currentTask != NULL) {​
if (currentTask->status == TASK_READY) {​
currentTask->status = TASK_RUNNING;​
currentTask->taskFunction(); // 通过函数指针调用任务函数​
currentTask->status = TASK_FINISHED;​
}​
currentTask = currentTask->next;​
}​
}​


在registerTask函数中,通过malloc函数分配内存创建一个新的任务节点,将传入的任务函数指针赋值给newTask->taskFunction,并将新任务节点插入到任务链表的头部。在executeTasks函数中,遍历任务链表,当找到处于TASK_READY状态的任务时,将其状态设置为TASK_RUNNING,然后通过函数指针currentTask->taskFunction调用任务函数,执行任务,任务执行完成后将状态设置为TASK_FINISHED 。​

三、定义具体的任务函数并进行测试


// 具体任务函数1​
void task1() {​
printf("任务1正在执行\n");​
}​
​
// 具体任务函数2​
void task2() {​
printf("任务2正在执行\n");​
}​
​
int main() {​
// 注册任务​
registerTask(task1);​
registerTask(task2);​
​
// 执行任务​
executeTasks();​
​
return 0;​
}​
​

在main函数中,调用registerTask函数注册了两个任务task1和task2,然后调用executeTasks函数执行任务调度器,依次执行注册的任务 。​
通过这个简易操作系统任务调度器的案例,可以看到 typedef 和函数指针的协同工作机制。typedef 为任务状态枚举和任务结构体定义了简洁的别名,提高了代码的可读性;而函数指针则使得任务调度器能够灵活地调用不同的任务函数,实现了任务的动态执行和管理。

中断回调案例

假设我们在一个基于 ARM Cortex-M3 内核的嵌入式系统中,使用 GPIO(通用输入输出)引脚来检测外部按键的按下事件,并通过中断回调函数进行处理。下面是具体的实现步骤和代码示例。

一、定义中断回调函数类型​


使用typedef定义一个函数指针类型,用于表示中断回调函数。在这个案例中,中断回调函数没有返回值,也没有参数:​

typedef void (*InterruptCallback)(void);​

上述代码定义了一个名为InterruptCallback的函数指针类型,它指向的函数返回值为void,参数列表也为void。

二、初始化 GPIO 与中断配置​


对GPIO 和中断进行初始化配置。这里假设使用的是 STM32 系列微控制器,相关代码如下:​

#include "stm32f10x.h"​
​
void GPIO_Interrupt_Init(void) {​
GPIO_InitTypeDef GPIO_InitStructure;​
EXTI_InitTypeDef EXTI_InitStructure;​
NVIC_InitTypeDef NVIC_InitStructure;​
​
// 使能GPIO时钟​
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);​
// 配置GPIO引脚为输入模式​
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;​
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;​
GPIO_Init(GPIOA, &GPIO_InitStructure);​
​
// 配置EXTI中断线​
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);​
EXTI_InitStructure.EXTI_Line = EXTI_Line0;​
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;​
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发中断​
EXTI_InitStructure.EXTI_LineCmd = ENABLE;​
EXTI_Init(&EXTI_InitStructure);​
​
// 配置NVIC中断优先级​
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;​
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00;​
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00;​
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;​
NVIC_Init(&NVIC_InitStructure);​
}​
​

上述代码实现了 GPIO 引脚的初始化配置,将 PA0 引脚设置为浮空输入模式,并配置了外部中断线 EXTI0,使其在下降沿触发中断,同时还配置了 NVIC(嵌套向量中断控制器)的中断优先级。

三、注册中断回调函数​


定义一个函数来注册中断回调函数。可以定义一个全局的函数指针变量,并提供一个函数来设置该指针:​

InterruptCallback currentCallback = NULL;​
​
void Register_Interrupt_Callback(InterruptCallback callback) {​
currentCallback = callback;​
}​


上述代码定义了一个全局的InterruptCallback类型的指针变量currentCallback,初始化为NULL,并提供了Register_Interrupt_Callback函数用于注册中断回调函数,将传入的回调函数地址赋值给currentCallback。
四、编写中断服务函数​
中断服务函数是在中断发生时由系统自动调用的函数,在该函数中需要调用注册的中断回调函数:​

void EXTI0_IRQHandler(void) {​
if (EXTI_GetITStatus(EXTI_Line0) != RESET) {​
if (currentCallback != NULL) {​
currentCallback(); // 调用注册的中断回调函数​
}​
EXTI_ClearITPendingBit(EXTI_Line0);​
}​
}​

上述代码中,EXTI0_IRQHandler是外部中断线 EXTI0 对应的中断服务函数。在函数中,首先检查中断标志位是否置位,如果置位且存在已注册的中断回调函数,则调用该回调函数,最后清除中断标志位。
五、使用中断回调函数​
在主函数中,可以进行如下操作来使用中断回调机制:​

int main(void) {​
GPIO_Interrupt_Init();​
// 定义一个具体的中断回调函数​
void Button_Pressed_Callback(void) {​
// 在这里编写按键按下后的处理逻辑,例如点亮LED等​
// 假设LED连接在PB5引脚​
GPIO_SetBits(GPIOB, GPIO_Pin_5);​
}​
Register_Interrupt_Callback(Button_Pressed_Callback);​
​
while (1) {​
// 主循环可以执行其他任务​
}​
}​
​

在上述主函数中,首先调用GPIO_Interrupt_Init函数进行 GPIO 和中断的初始化配置,然后定义了一个具体的中断回调函数Button_Pressed_Callback,该函数实现了按键按下后的处理逻辑(这里假设是点亮连接在 PB5 引脚的 LED),最后通过Register_Interrupt_Callback函数将该回调函数注册到系统中。主循环可以继续执行其他任务,当外部按键按下触发中断时,系统会自动调用中断服务函数,进而执行注册的中断回调函数。