当前位置: 首页 > news >正文

裸机任务调度框架、DMA、空闲中断

DMA和空闲中断

来一个字节,就会触发中断,一个字节有多少个字节,就会进入多少次中断。
这种会频繁进入中断,打断主流程。目前开发的产品没有影响,
但是如果实时性要求较高,是否会有影响?
因此使用DMA和空闲中断去解决这个问题,实现一包数据只需要进入一次中断。

根据空闲中断判断?
需要说明的是一帧内(包含多个字节)
也就是这一字节和下一个字节之间的传输时间是很短的,可以忽略不计。
但是帧与帧空闲的时间窗就很长,让时间超过传输一个字符(一个字节)帧所需要的时间时,就会产生一个空闲标志IDLEF。(这个可以理解为空闲中断)
以计算的例子为说明:9600波特率,传输一个字节需要1ms,那么只要两帧数据之间传输时间超过1ms就会认为是一帧数据传输完了,然后会产生一个空闲中断IDLEF。

DMA 直接存储器访问 Direct Memory Access
这个就是为了解决我们在这个空闲中断处,收到一整包数据以后,不需要CPU干预,直接通过DMA将串口的数据寄存器搬移到内存里面,DMA就是数据搬运工。

在这里插入图片描述

在串口通信过程中,怎么判断是0和1?
假如我传输一个bit的时间窗是1ms,那么我在这1ms内去判断高低电平,如果判断10次,有8次都是高电平,那么就认为是高电平。
传输二进制的速度是波特率
一般是9600、115200,也就是每秒钟传输多少位数据。
假设一个帧(只传输一个字节)的数据包含10位,1s是9600位
那么一个字节数据传输的大概时间是1ms时间。

使能DMA时钟
复位DMA通道
配置传输方向:片上外设到内存
配置数据源地址:例如是串口的数据寄存器地址
配置源地址是固定还是增长的:是固定的,因为数据缓存寄存器是固定的
配置源数据传输位宽:位宽,8位宽度
配置数据目的地址:也就是数组的首地址,
配置目的地址是固定的还是增长的:需要存在数组,因此是增长的,不然怎么存的完。注意要进行强制数据转换,因为是在ARM,因此要搞成32位的地址。
配置目的数据传输位宽:位宽,8位宽度
配置数据传输最大次数:根据芯片手册查看,
配置DMA通道优先级:(可能拓展多个通道,那么就需要判断先搬运哪一个)
使能UART接受数据使用DMA
使能DMA通道

设置DAM需要先关闭,然后再设置,最后在打开,这样才有效。

裸机任务调度方案

时间的标志位在时间中断定时器产生

时间片调度,按照需要调度,就是当前开发使用的思路,

在这里插入图片描述
会出现很多if判断,导致代码不美观?
还需要进一步优化

实现一个调度框架,
将其封装起来。
首先将时间片放在结构体数组中,
接着将任务也同样放在结构体中,

在主函数中main.c中:

typedef struct
{uint8_t run;                // 调度标志,1:调度,0:挂起uint16_t timCount;          // 时间片计数值uint16_t timRload;          // 时间片重载值void (*pTaskFuncCb)(void);  // 函数指针变量,用来保存业务功能模块函数地址
} TaskComps_t;

包含是否被调度,时间片计数器,时间片重载值,保存业务功能模块
需要说明的是

 void (*pTaskFuncCb)(void);   这样写的目的就是告诉编译器,这就是一个函数指针变量。如果以后你想定义一个函数指针,那么这个是必不可少的形式。缺一不可。void  可以换成数据类型等,

该函数被主函数调用:static void TaskHandler(void)
{for (uint8_t i = 0; i < TASK_NUM_MAX; i++){if (g_taskComps[i].run)                  // 判断时间片标志{g_taskComps[i].run = 0;              // 标志清零g_taskComps[i].pTaskFuncCb();        // 执行调度业务功能模块}}
}//可以看出函数TaskScheduleCb已经具有static标记了,说明这个函数具有以下属性
//只能在这个文件有效,
//函数不会被优化掉,内存中一直有位置给这个函数使用
//而想在其他地方调用函数,只能通过指针的形式调用,因此使用了回调函数的方式。
//将TaskScheduleCb函数的地址传过去,
//需要说明的是一个函数的首地址就是通过函数名进行读取的,所以传递的首地址就是
//直接将这个TaskScheduleCb首地址作为形参传递进去了。static void AppInit(void)
{Usb2ComAppInit();TaskScheduleCbReg(TaskScheduleCb);
}

主要通过run调度标志位,看是否只能该功能模块,
然后直接清零
最后是根据函数指针,调用对应的业务功能。

这段函数是在定时器中调用,也就是滴答定时器(位于驱动层)

需要将这个函数的地址传递出去

static void TaskScheduleCb(void)
{for (uint8_t i = 0; i < TASK_NUM_MAX; i++){if (g_taskComps[i].timCount){g_taskComps[i].timCount--;if (g_taskComps[i].timCount == 0){g_taskComps[i].run = 1;g_taskComps[i].timCount = g_taskComps[i].timRload;}}}
}
static TaskComps_t g_taskComps[] = 
{{0, 5,   5,   HmiTask},{0, 200, 200, Usb2ComTask},/* 添加业务功能模块 */
};

这里可以看出,如果我们的业务模块很多的话,这个框架还是不行的,但是这样做的目的给我们提供一个思路。一部分可以使用。

定时器.cstatic volatile uint64_t g_sysRunTime = 0;//这是定义指针变量的标准方式。
static void (*g_pTaskScheduleFunc)(void);        // 函数指针变量,保存任务调度的函数地址/**
***********************************************************
需要注意的是这里形参是一个函数指针变量,说白了需要给这个
函数传递的就是一个指针,只不过是一个函数指针。通过地址进行传递,从而使驱动层函数进行调用应用层函数。**在应用层进行调用该函数*************************************************************
*/
void TaskScheduleCbReg(void (*pFunc)(void))
{g_pTaskScheduleFunc = pFunc;
}/**
***********************************************************
定时中断服务函数,1ms产生一次中断
***********************************************************
*/
void SysTick_Handler(void)
{g_sysRunTime++;g_pTaskScheduleFunc();
}/**
***********************************************************
获取系统运行时间
***********************************************************
*/
uint64_t GetSysRunTime(void)
{return g_sysRunTime;
}

补充:static:

访问的属性还是局部的,但是数据放在全局段中(静态段)。
原本这个地址就是存放的是a,不会被释放掉,这个地址一直会存放a这个变量。 占据整个代码量的大小。

1. ​隐藏作用(限制作用域)​

  • 全局变量或函数​:用static修饰全局变量或函数时,它们的作用域被限制在当前源文件内,其他文件无法通过extern引用。这种隐藏特性可避免不同文件间的命名冲突

    // 文件a.c
    static int global_var;  // 仅本文件可见
    static void func() { }   // 仅本文件可调用
    
  • 对比​:未加static的全局变量和函数默认具有全局可见性,其他文件可通过声明extern访问

2. ​保持变量内容的持久性

  • 局部静态变量​:在函数内部用static声明局部变量时,其生命周期从程序启动到结束,而非随函数调用结束释放。变量的值在多次调用间保持连续性

    void increment() {static int count = 0;  // 仅初始化一次count++;printf("%d\n", count); // 每次调用递增
    }
    
  • 存储位置​:静态变量(包括全局和局部)存储在静态数据区,与自动变量(栈区)和动态分配变量(堆区)不同

3. ​默认初始化

  • 自动初始化为0​:未显式赋值的静态变量(包括全局和局部静态变量)会被编译器自动初始化为0或空值(如字符串为"",指针为NULL

    static int num;      // 默认初始化为0
    static char str[10]; // 默认初始化为空字符串
    

4. ​静态函数

  • 限制函数可见性​:用static修饰函数时,该函数只能在定义它的文件中调用,不可被其他文件访问。这增强了模块化设计,防止函数名污染全局命名空间

    // 文件utils.c
    static void helper() { }  // 仅本文件内部使用
    

总结对比表

应用场景作用示例/说明
全局变量+static限制作用域至当前文件,避免跨文件冲突static int x; 仅在定义文件中可见
局部变量+static变量生命周期延长至程序结束,值在多次调用间保留函数内static int count=0;,每次调用count递增
函数+static函数仅在本文件内可调用static void func() {} 对其他文件不可见
未初始化静态变量自动初始化为0或空值static int a; 默认a=0

其他注意事项

  • auto对比​:auto变量(默认)存储在栈区,每次函数调用时重新分配,而static变量在程序启动时分配一次
  • 多文件编程​:合理使用static可提高代码封装性,减少全局变量滥用带来的维护问题

static在C语言中实现了作用域控制、数据持久化和内存管理的优化,是模块化编程和资源管理的重要工具。

补充说明

但是这个是否会应为for循环在中断里面,导致一些问题?

本次使用的是系统滴答定时器,这个滴答定时器还没有进一步了解。

关于调度框架,该文章会进一步更新,目前还在思考最优解。

因为并不是所有的都是使用滴答定时器。

文章仍需要进一步补充。

文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。

【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。
示例:
如果您在博客或文章中引用了本文的内容,请在显著位置标注类似以下声明:
本文部分内容参考自 [博主名称] 的原创文章,原文链接:[文章链接],遵循 CC 4.0 BY-SA 版权协议。
感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言。*

相关文章:

  • Linux安装SRILM
  • HCIP(广域网)
  • 养生指南:五维守护身心健康
  • Spring Boot 注解 @ConditionalOnMissingBean是什么
  • 【Bluedroid】蓝牙 HID Host connect全流程源码解析
  • 通过云服务器实现异地组网 部署WireGuard
  • LeetCode Hot100 (双指针)
  • F5《2025年应用战略现状》报告:AI 落地加速,企业战略从讨论迈向行动
  • 【风控】模型算法区分度指标KS
  • 【C++进阶篇】红黑树的实现(赋源码)
  • 【软件使用】RSS(Really Simple Syndication)
  • 养生指南:五维提升健康品质
  • 重拾童年,用 CodeBuddy 做自己的快乐创作者
  • S7-1500PLC通过工艺对象实现V90总线伺服定位控制(105报文)
  • JavaWeb Web基础
  • Git命令汇总(自用,持续更新update 5/23)
  • Gartner《如果有效评估Generative AI项目的投资回报》学习心得
  • 账号冻结风险升级!亚马逊视频验证新增3大审核标准
  • Linux进程信号(五)之捕捉信号
  • c++11特性——lambda对象、包装器
  • 长沙县政务网站/软文投放平台有哪些
  • 网站开发流程比较合理/创建网站花钱吗
  • 济南建网站/万词优化
  • 网站建设公司能信吗/企业官方网站有哪些
  • 5种免费一级域名的申请方法/哪些网站可以seo
  • 做服装有哪些好的网站有哪些方面/淘宝如何提升关键词排名