C语言嵌入式开发系统架构篇:单任务与多任务
- C语言
- 3天前
- 37热度
- 0评论
所谓“单任务系统”是指系统不能支持多任务并发操作,在同一时间内,系统仅能执行一个任务。。整个系统的运行就像一条有序的生产线,所有操作都按照预先设定的顺序依次执行 。在这个过程中,一个任务从开始执行到结束,期间不会被其他任务打断,只有当前任务完成后,系统才会接着执行下一个任务。
多任务开发,是指在一个系统中,允许同时处理多个任务的开发方式。在多任务开发环境下,系统能够将 CPU 时间合理地分配给各个任务,使得这些任务看似在同时运行。这种开发方式打破了单任务开发中任务顺序执行的限制,提高了系统资源的利用率和响应速度。
例如,在一部智能手机中,它可以同时运行多个应用程序,如用户在使用微信聊天的同时,还能播放音乐、接收邮件通知等。这些任务在操作系统的调度下,能够有序地共享系统资源,实现并行处理 。在工业控制领域,一个工业自动化控制系统可能需要同时处理数据采集、设备控制、故障监测等多个任务。通过多任务开发,系统能够实时采集传感器的数据,根据数据对设备进行精准控制,同时还能及时监测设备是否出现故障,一旦发现故障,立即发出警报并进行相应处理,确保生产过程的顺利进行。
单任务程序结构
单任务程序的执行流程有着明确而固定的模式。当系统启动时,首先从 CPU 复位时的指定地址开始执行。这个指定地址就是程序的起点,所有的操作都从这里开始。接着,程序跳转至汇编代码 startup 处执行。汇编代码负责完成一些底层的初始化工作,例如设置 CPU 的工作模式、初始化系统时钟、配置内存等,完成汇编代码的执行后,程序便跳转至用户主程序 main 执行。
在用户主程序 main 中,首先要完成各项初始化工作。这包括初始化各硬件设备,例如初始化 GPIO(通用输入输出端口),使其能够正确地接收和发送信号;初始化 UART(通用异步收发传输器),以便实现串口通信功能等。同时,还需要初始化各软件模块,如初始化变量、配置数据结构等,为程序的后续运行准备好必要的软件环境。
完成初始化后,程序进入一个无限循环。在这个无限循环中,程序不断地调用各模块的处理函数,持续执行任务,只要系统不断电,它就会一直运行下去,确保系统能够持续地完成各项任务 。例如,在一个简单的温度监测系统中,程序会不断地调用温度传感器的读取函数,获取当前的温度值,然后对温度值进行处理,如判断温度是否超出设定的范围,如果超出则进行相应的报警处理,接着将处理后的温度值进行显示或存储,之后又再次进入循环,读取下一次的温度值,如此反复,实现对温度的持续监测和处理。
单任务应用场景
简单的电子时钟就是一个典型的适用于单任务开发的场景。在电子时钟中,其任务主要是获取时间信息,并将时间准确地显示出来。这个过程相对单一,逻辑简单,只需要设计一个从时钟芯片读取时间的任务即可,然后将数据解析并显示在显示屏上。
再比如基础的温度控制系统,它的主要任务是实时采集温度传感器的数据,然后根据预设的温度值进行比较和判断,进而控制加热或制冷设备的工作状态,以维持环境温度在设定范围内。这个系统的任务明确且单一,使用单任务开发能够有效地实现温度的监测和控制功能。
4.2.3.单任务代码示例
下面通过一个简单的 C 语言代码示例,来直观了解单任务开发的实现方式和逻辑。
#include <stdio.h>
#include <stdlib.h>
// 定义一个函数,用于模拟任务的执行
void task(void) {
static int count = 0;
count++;
printf("Task is running, count: %d\n", count);
}
int main() {
// 初始化硬件设备
printf("Initializing hardware...\n");
// 初始化软件模块
printf("Initializing software modules...\n");
// 进入无限循环,持续执行任务
while (1) {
task();
// 可以在这里添加一些延时,避免任务执行过于频繁
}
return 0;
}
在上述代码中:
初始化部分:在main函数中,首先通过printf函数模拟初始化硬件设备和软件模块的过程。在实际的嵌入式开发中,这部分需要替换为具体的硬件初始化代码,如配置寄存器、设置引脚模式等,以及软件模块的初始化代码,如初始化变量、创建数据结构等 。
任务执行部分:定义了一个task函数,用于模拟任务的执行。在task函数中,使用了一个静态变量count来记录任务执行的次数,并通过printf函数输出任务执行的信息。在main函数的死循环中,不断地调用task函数,从而实现任务的持续执行。
无限循环部分:while (1)构成了一个死循环,这是单任务开发中保证任务持续执行的关键。只要系统运行,这个循环就不会停止,task函数会不断地被调用,任务也就会不断地执行下去 。如果需要控制任务的执行频率,可以在循环中添加适当的延时代码。
多任务实现方式
嵌入式开发中多任务实现方式主要分为裸机环境与RTOS环境两种模式。
裸机环境的实现方式
1、采用轮询方式
通过主循环依次调用不同任务的函数,各任务在固定顺序下交替执行。在嵌入式开发中,轮询方式是一种简单的任务调度模式,以下是一个基于C语言的LED控制案例:
#include <stdio.h>
#include "stm32f1xx_hal.h" // 假设使用STM32硬件库
// 任务函数声明
void LED1_Task(void);
void LED2_Task(void);
void Button_Scan_Task(void);
// 全局状态变量
uint8_t button_pressed = 0;
int main(void) {
HAL_Init();
SystemClock_Config();
// 硬件初始化
MX_GPIO_Init(); // 初始化GPIO
MX_TIM2_Init(); // 初始化定时器
// 主循环轮询调度
while(1) {
Button_Scan_Task(); // 任务1:按键扫描
LED1_Task(); // 任务2:LED1控制
LED2_Task(); // 任务3:LED2呼吸灯
}
}
// 任务1:按键扫描(带防抖)
void Button_Scan_Task(void) {
static uint32_t last_time = 0;
if(HAL_GetTick() - last_time > 20) { // 20ms扫描间隔
last_time = HAL_GetTick();
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) {
button_pressed = 1;
}
}
}
// 任务2:LED1简单闪烁
void LED1_Task(void) {
static uint32_t last_toggle = 0;
if(HAL_GetTick() - last_toggle > 500) { // 500ms间隔
last_toggle = HAL_GetTick();
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_12);
}
}
// 任务3:LED2 PWM呼吸灯
void LED2_Task(void) {
static uint8_t brightness = 0;
static int8_t direction = 1;
if(HAL_GetTick() % 10 == 0) { // 10ms更新一次
brightness += direction;
if(brightness == 0 || brightness == 100) direction *= -1;
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, brightness);
}
}
任务调度机制
主循环依次调用 Button_Scan_Task() → LED1_Task() → LED2_Task()。每个任务通过时间戳(HAL_GetTick())实现非阻塞延迟,所有任务共享CPU时间,按固定顺序轮流执行。
2、中断驱动方式
利用硬件中断触发任务执行,如定时器中断、外设中断等。以下是基于STM32硬件平台使用中断驱动方式的案例,通过定时器中断和外部中断实现任务触发:
#include "stm32f1xx_hal.h"
// 全局变量声明
volatile uint8_t timer_flag = 0; // 定时器中断标志
volatile uint8_t button_flag = 0; // 按键中断标志
int main(void) {
HAL_Init();
SystemClock_Config();
// 硬件初始化
MX_GPIO_Init();
MX_TIM2_Init();
MX_EXTI_Init();
// 启动定时器中断
HAL_TIM_Base_Start_IT(&htim2);
while(1) {
// 主循环处理非实时任务
if(timer_flag) {
timer_flag = 0;
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_12); // LED闪烁
}
if(button_flag) {
button_flag = 0;
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET); // 点亮另一个LED
HAL_Delay(200); // 保持点亮状态
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET);
}
}
}
// 定时器中断回调函数(1kHz频率)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if(htim->Instance == TIM2) {
timer_flag = 1; // 设置定时器标志
static uint16_t counter = 0;
if(++counter >= 1000) { // 1秒计数
counter = 0;
// 可在此执行精确的1秒任务
}
}
}
// 外部中断回调函数(按键PA0)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if(GPIO_Pin == GPIO_PIN_0) {
button_flag = 1; // 设置按键标志
__disable_irq(); // 临时关闭中断(防抖处理)
HAL_Delay(20); // 等待20ms(简单防抖)
__enable_irq();
}
}
上述代码基于 STM32F1 微控制器,使用定时器(TIM2)中断实现周期性任务,使用外部中断(EXTI)处理按键输入。
3、时间片轮转调度
基于定时器中断划分时间片,任务按固定时间间隔切换执行。时间片轮转调度(Round Robin Scheduling)是一种简单的多任务调度算法,以下是一个基于 STM32 的简单时间片轮转调度示例:
#include "stm32f1xx_hal.h"
// 任务函数声明
void Task1(void);
void Task2(void);
void Task3(void);
// 任务数组
typedef void (*TaskFunc)(void);
TaskFunc tasks[] = {Task1, Task2, Task3};
// 全局变量
volatile uint8_t current_task = 0; // 当前任务索引
// 定时器中断回调函数(假设 1kHz 频率)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2) {
// 切换到下一个任务
current_task = (current_task + 1) % (sizeof(tasks) / sizeof(tasks[0]));
// 执行当前任务
tasks:ml-search[current_task];
}
}
// 任务实现
void Task1(void) {
// 示例:切换 LED1 状态
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_12);
}
void Task2(void) {
// 示例:点亮 LED2 一段时间(模拟耗时任务)
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET);
}
void Task3(void) {
// 示例:简单的传感器读取或数据处理
// 这里可以添加实际的传感器读取代码
}
int main(void) {
HAL_Init();
SystemClock_Config();
// 硬件初始化
MX_GPIO_Init();
MX_TIM2_Init();
// 启动定时器中断
HAL_TIM_Base_Start_IT(&htim2);
while (1) {
// 主循环可以处理其他非实时任务
}
}
基于RTOS的多任务实现
基于RTOS的多任务实现是通过实时操作系统(RTOS)的任务调度机制,将多个独立任务动态分配到处理器资源,实现“伪并行”执行的编程模式。
市场上常见的RTOS系统有 FreeRTOS、RT-Thread 和 uC/OS等实时多任务操作系统。在多任务系统中,常见的任务调度方式有抢占调度和轮询调度。
抢占调度是一种基于任务优先级的调度方式。以 UCOS 为例,假设系统中有三个任务,任务 A 优先级最高,任务 B 次之,任务 C 优先级最低。当任务 C 正在执行时,如果任务 A 进入就绪状态,UCOS 会立即暂停任务 C 的执行,将 CPU 资源分配给任务 A,让任务 A 优先执行。只有当任务 A 执行完毕或者进入阻塞状态(如等待某个事件发生或等待资源释放)时,任务 B 或任务 C 才有可能获得 CPU 的控制权,继续执行。这种调度方式确保了关键任务能够及时得到处理,提高了系统的实时性。但它也对任务优先级的合理分配和设计提出了较高的要求,如果优先级设置不合理,可能会导致低优先级任务长时间得不到执行。
轮询调度按照一定的顺序依次为每个任务分配 CPU 时间片。在轮询调度中,每个任务轮流获得 CPU 的使用权,当一个任务的时间片用完后,系统会切换到下一个任务,让其执行。例如,在一个简单的嵌入式系统中,有两个任务 T1 和 T2,系统采用轮询调度方式,为每个任务分配 10 毫秒的时间片。那么 T1 先执行 10 毫秒,然后 T2 执行 10 毫秒,接着又轮到 T1 执行,如此循环往复。这种调度方式的优点是实现简单,每个任务都能得到公平的执行机会,但缺点是无法保证关键任务的实时性,如果某个任务执行时间过长,可能会影响其他任务的响应速度。
多任务应用场景
嵌入式系统一般都采用多任务开发模式。以智能监控设备为例,它需要同时处理多个任务。一方面,要持续采集摄像头传来的视频数据,对视频图像进行实时分析,检测是否有异常情况,如人员闯入、物体移动等;另一方面,还要负责与远程服务器进行通信,将采集到的数据上传到服务器,同时接收服务器发送的控制指令 。
在工业自动化控制系统中,一个工业自动化生产线可能包含多个设备,如传感器、电机、控制器等。系统需要同时采集各种传感器的数据,根据生产工艺要求对电机进行精确控制,实现设备的自动化运行;同时,还要实时监测设备的运行状态,一旦发现故障,及时进行报警和故障诊断。
在智能家居系统中,系统需要同时处理用户的各种控制指令,如开关灯光、调节温度、控制家电设备等,还要实现设备之间的互联互通和智能联动。