嵌入式开发:C语言回调函数的重要性

什么是回调函数


回调函数,简单来说,就是一个函数作为参数传递给另一个函数,并在特定时刻被调用执行的函数。在 C 语言中,回调函数通常通过函数指针来实现。函数指针是指向函数的指针变量,它存储了函数在内存中的地址。通过函数指针,可以像调用普通函数一样调用它所指向的函数。例如:​​

// 定义一个回调函数​
void callback_function(int data) {​
printf("Callback function is called with data: %d\n", data);​
}​
​
// 定义一个主函数,接受回调函数作为参数​
void main_function(void (*callback)(int), int data) {​
printf("Main function is running\n");​
// 在适当的时候调用回调函数​
callback(data);​
}​
​
int main() {​
int data = 42;​
// 调用主函数,并传递回调函数和数据​
main_function(callback_function, data);​
return 0;​
}​

在这个例子中,callback_function就是回调函数,它被作为参数传递给main_function。当main_function执行到callback(data)这一行时,就会调用callback_function,并将data作为参数传递给它。

与普通函数的差异

回调函数与普通函数在调用方式和使用场景上存在明显的差异。普通函数的调用是直接在代码中通过函数名进行调用,程序的执行流程会直接跳转到被调用函数的代码块中,执行完毕后再返回调用处继续执行后续代码。例如:​​

// 普通函数定义​
int add(int a, int b) {​
return a + b;​
}​
​
int main() {​
int result = add(3, 5);​
printf("The result of addition is: %d\n", result);​
return 0;​
}​

在这个例子中,add函数是普通函数,在main函数中直接通过函数名调用。​
而回调函数则是作为参数传递给其他函数,并在特定条件满足时由该函数进行调用。它的调用时机和方式更加灵活,通常用于实现一些通用的算法或框架,使得具体的业务逻辑可以通过回调函数来定制。比如在一些事件驱动的编程模型中,当某个事件发生时,预先注册的回调函数就会被触发执行,以处理相应的事件。这就好比在一个游戏中,当玩家触发了某个特定的事件(如获得了新的道具、完成了某个任务等),系统就会调用相应的回调函数来处理这些事件,如更新玩家的状态、显示提示信息等。​
此外,回调函数还可以实现代码的解耦和复用。通过将不同的回调函数传递给同一个主函数,我们可以在不修改主函数代码的情况下,实现不同的功能。这就像是一个通用的工具函数,根据传入的不同回调函数,可以完成不同的具体任务,提高了代码的灵活性和可扩展性 。

回调函数的实现基础:函数指针

在 C 语言中,函数指针是实现回调函数的关键。函数指针是一种指向函数的指针变量。它存储了函数在内存中的入口地址,通过这个地址,可以像调用普通函数一样调用它所指向的函数 。​
函数指针的声明方式与普通变量指针有所不同。其一般形式为:返回值类型 (*指针变量名)(参数列表) 。这里的括号是必不可少的,它确保了*先与指针变量名结合,表明这是一个指针,然后再与函数的返回值类型和参数列表相关联。例如,声明一个指向返回值为int类型,且带有两个int类型参数的函数指针:int (*func_ptr)(int, int);​
在 32 位单片机中,任何类型的指针变量都存放的是一个大小为 4 字节的地址。这是因为 32 位单片机的地址总线宽度为 32 位,能够表示的地址范围为 0 到 2^32 - 1,所以无论指针指向的是何种数据类型,其存储的地址值都占用 4 个字节的存储空间。函数指针也不例外,它同样占用 4 个字节来存储函数的入口地址 。​
下面通过一个简单的代码示例来展示函数指针的定义与赋值:​​

#include <stdio.h>​
​
// 定义一个普通函数​
int add(int a, int b) {​
return a + b;​
}​
​
int main() {​
// 定义一个函数指针​
int (*p)(int, int);​
// 将函数add的地址赋值给函数指针p​
p = add;​
​
int result = p(3, 5);​
printf("The result of addition is: %d\n", result);​
​
return 0;​
}​
​

在这个例子中,首先定义了一个普通函数add,用于计算两个整数的和。然后在main函数中定义了一个函数指针p,其类型与add函数的类型相匹配。通过p = add;将add函数的地址赋值给p,此时p就指向了add函数。最后,通过p(3, 5)调用add函数,并将结果打印输出。

用函数指针实现回调

了解了函数指针的基本概念后,接下来看看如何通过函数指针来实现回调函数。在回调函数的实现过程中,我们通常将函数指针作为参数传递给其他函数,在被调用函数的特定逻辑中,通过该函数指针来调用对应的回调函数,从而实现不同的功能逻辑。​
下面通过一个代码实例来加深理解:​​

#include <stdio.h>​
​
// 定义回调函数​
void callback_function(int data) {​
printf("Callback function is called with data: %d\n", data);​
}​
​
// 定义主函数,接受回调函数作为参数​
void main_function(void (*callback)(int), int data) {​
printf("Main function is running\n");​
// 在适当的时候调用回调函数​
callback(data);​
}​
​
int main() {​
int data = 42;​
// 调用主函数,并传递回调函数和数据​
main_function(callback_function, data);​
return 0;​
}​


在上述代码中,首先定义了一个回调函数callback_function,它接受一个int类型的参数,并在函数内部打印出接收到的数据。然后定义了一个main_function函数,它接受一个函数指针callback和一个int类型的数据data作为参数。在main_function函数内部,先打印出 "Main function is running",然后通过callback(data)调用传递进来的回调函数,并将data作为参数传递给回调函数。最后,在main函数中,定义了一个数据data,并调用main_function函数,将callback_function和data作为参数传递进去,从而实现了回调函数的调用。​
从这个例子可以看出,通过函数指针实现回调函数,使得代码的结构更加灵活和可扩展。我们可以根据不同的需求,将不同的回调函数传递给主函数,从而实现不同的功能,而无需修改主函数的核心代码 。

回调函数应用场景:中断处理

在嵌入式开发中,硬件驱动负责实现设备与上层应用程序之间的通信和控制,而回调函数实现了设备与上层应用程序的解耦。以 STM32 等单片机原厂固件库函数中的回调函数为例,在设备初始化过程中,开发者可以通过注册回调函数来定制初始化完成后的操作。例如在使用 STM32 的 SPI 外设时,初始化 SPI 设备后,可以注册一个回调函数,当 SPI 设备初始化完成后,该回调函数会被调用,开发者可以在回调函数中进行一些额外的配置或通知上层应用程序设备已准备就绪 。​
在中断处理方面,回调函数同样应用广泛。当外部设备产生中断时,硬件会触发相应的中断服务程序。在 STM32 的 HAL 库中,许多外设的中断处理都采用了回调函数机制。以串口通信为例,当串口接收到数据时,会触发接收中断,此时 HAL 库会调用预先注册的HAL_UART_RxCpltCallback回调函数。开发者可以在这个回调函数中编写自己的处理逻辑,如读取接收到的数据、进行数据解析等,而不需要直接修改 HAL 库的代码。这样,当需要更换不同的串口应用场景时,只需要修改回调函数的实现,而不需要对底层的驱动代码进行大规模改动,提高了代码的可维护性和可扩展性 。​​

#include "stm32f4xx_hal.h"​
​
UART_HandleTypeDef huart1;​
​
// 串口接收完成回调函数​
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {​
if (huart == &huart1) {​
// 处理接收到的数据​
uint8_t receivedData;​
HAL_UART_Receive(&huart1, &receivedData, 1, 100);​
// 这里可以添加自定义的数据处理逻辑,比如解析接收到的数据帧​
// 根据数据内容执行相应的操作​
}​
}​
​
void SystemClock_Config(void);​
static void MX_GPIO_Init(void);​
static void MX_USART1_UART_Init(void);​
​
int main(void) {​
HAL_Init();​
SystemClock_Config();​
MX_GPIO_Init();​
MX_USART1_UART_Init();​
​
uint8_t rxData;​
HAL_UART_Receive_IT(&huart1, &rxData, 1); // 开启串口接收中断​
​
while (1) {​
// 主循环可以执行其他任务,不必一直等待串口数据接收​
}​
}​
​
void SystemClock_Config(void) {​
// 系统时钟配置代码​
}​
​
static void MX_USART1_UART_Init(void) {​
huart1.Instance = USART1;​
huart1.Init.BaudRate = 115200;​
huart1.Init.WordLength = UART_WORDLENGTH_8B;​
huart1.Init.StopBits = UART_STOPBITS_1;​
huart1.Init.Parity = UART_PARITY_NONE;​
huart1.Init.Mode = UART_MODE_TX_RX;​
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;​
huart1.Init.OverSampling = UART_OVERSAMPLING_16;​
if (HAL_UART_Init(&huart1) != HAL_OK) {​
Error_Handler();​
}​
}​
​
static void MX_GPIO_Init(void) {​
// GPIO 初始化代码​
}​
​
void Error_Handler(void) {​
while (1) {​
}​
}​
​

回调函数应用场景:异步操作处理

在嵌入式系统中,经常会遇到一些耗时的操作,如 I/O 操作、网络请求等。如果这些操作采用同步方式执行,会导致主线程阻塞,使得系统无法及时响应其他任务。回调函数为解决这个问题提供了有效的方案。以读取外部 Flash 存储器的数据为例,读取操作可能需要一定的时间,如果采用同步方式,主线程会一直等待读取操作完成,期间无法执行其他任务。而使用回调函数实现异步操作时,主线程可以在发起读取请求后继续执行其他任务。当 Flash 读取操作完成后,硬件会触发一个中断,在中断服务程序中调用预先注册的回调函数,在回调函数中处理读取到的数据 。​
在网络请求场景中,当嵌入式设备需要与服务器进行数据交互时,发送网络请求和等待响应的过程可能会比较耗时。通过回调函数,设备可以在发送请求后继续执行其他任务,当接收到服务器的响应时,调用回调函数来处理响应数据。这样可以提高程序的执行效率和响应性,使系统能够在处理异步任务的同时,及时响应用户的操作和其他事件 。​

#include <stdio.h>​
#include <stdlib.h>​
#include <string.h>​
#include <unistd.h>​
#include <arpa/inet.h>​
#include <sys/socket.h>​
​
#define SERVER_IP "192.168.1.100"​
#define PORT 8080​
#define BUFFER_SIZE 1024​
​
// 回调函数,处理网络请求的响应​
void handle_response(char *response) {​
printf("Received response: %s\n", response);​
// 这里可以添加对响应数据的处理逻辑,比如解析JSON数据​
// 根据响应内容执行相应的操作​
}​
​
// 模拟异步网络请求​
void send_request_async(void (*callback)(char *)) {​
int sockfd;​
struct sockaddr_in servaddr;​
​
// 创建套接字​
sockfd = socket(AF_INET, SOCK_STREAM, 0);​
if (sockfd == -1) {​
perror("socket creation failed");​
exit(EXIT_FAILURE);​
}​
​
memset(&servaddr, 0, sizeof(servaddr));​
​
// 填充服务器地址和端口​
servaddr.sin_family = AF_INET;​
servaddr.sin_port = htons(PORT);​
servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);​
​
// 连接服务器​
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0) {​
perror("connection failed");​
close(sockfd);​
exit(EXIT_FAILURE);​
}​
​
char request[] = "GET /data HTTP/1.1\r\nHost: 192.168.1.100\r\n\r\n";​
// 发送请求​
if (send(sockfd, request, strlen(request), 0) == -1) {​
perror("send failed");​
close(sockfd);​
exit(EXIT_FAILURE);​
}​
​
char buffer[BUFFER_SIZE];​
memset(buffer, 0, BUFFER_SIZE);​
// 接收响应​
ssize_t bytes_received = recv(sockfd, buffer, BUFFER_SIZE - 1, 0);​
if (bytes_received == -1) {​
perror("recv failed");​
close(sockfd);​
exit(EXIT_FAILURE);​
}​
​
buffer[bytes_received] = '\0';​
close(sockfd);​
​
// 调用回调函数处理响应​
callback(buffer);​
}​
​
int main() {​
send_request_async(handle_response);​
// 主线程可以继续执行其他任务​
printf("Main thread is doing other tasks.\n");​
​
return 0;​
}​