裸机任务调度框架、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 版权协议。
感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言。*