一、本文内容
上一节《03-单片机商业项目编程,从零搭建低功耗系统设计-CSDN博客》我们确定了设计思路,并如何更有效的保持低功耗,这节我们就准备来做软件框架设计。在AI飞速发展的时代,我们也会利AI来辅助我们完成,让自己也能慢慢学会和AI的合作。
二、软件设计思路
对于软件代码编写,最重要的还是框架搭建,并不是要一开始就把所有代码一五一十的写出来,而是框架搭建好了,再去填充自己需要的内容,比如单片机的底层驱动之类的;我们软件设计第一步先确定方案构思,第二步开始搭建软件框架,第三步移植到指定芯片。
当前AI发展十分迅速,很多算法类的代码,都可以不需要自己去编写,而我们更多的是发现问题,修正问题,这样既能够让AI给我们提供思路,又能充分发现AI的思路错误,来理顺自己的思路。我们今天利用Chat GPT来帮我们以STM32F030C8T6构建基础软件框架设计,我们不要求他能够直接能运行,但是要保障他的代码逻辑没有漏洞。这也是我们作为程序员和AI协同的提前练习;当然你也可以选择国内其它AI工具来实现。
三、软件编写过程
我:能帮我基于STM32F030C8T6,利用systick做一个基于事件触发的低功耗伪时间片调度算法吗?
GPT:当然可以。以下是一个基于STM32F030C8T6的低功耗伪时间片调度算法的示例。这个例子使用了systick定时器来触发事件。
#include "stm32f0xx.h"
// 定义任务结构体
typedef struct {
void (*taskFunc)(void); // 任务函数指针
uint32_t delay; // 任务延迟
uint32_t period; // 任务周期
uint8_t run; // 运行标志
} sTask;
#define SCH_MAX_TASKS 5 // 最大任务数
sTask SCH_tasks_G[SCH_MAX_TASKS]; // 任务数组
void SysTick_Handler(void) {
uint32_t index;
for (index = 0; index < SCH_MAX_TASKS; index++) {
if ((SCH_tasks_G[index].taskFunc != 0) && (SCH_tasks_G[index].delay == 0)) {
SCH_tasks_G[index].run = 1; // 标记任务为可运行
if (SCH_tasks_G[index].period) {
SCH_tasks_G[index].delay = SCH_tasks_G[index].period; // 重置任务延迟
}
}
if (SCH_tasks_G[index].delay > 0) {
SCH_tasks_G[index].delay--; // 任务延迟递减
}
}
}
void SCH_Dispatch_Tasks(void) {
uint32_t index;
for (index = 0; index < SCH_MAX_TASKS; index++) {
if (SCH_tasks_G[index].run == 1) {
(*SCH_tasks_G[index].taskFunc)(); // 运行任务
SCH_tasks_G[index].run = 0; // 清除运行标志
}
}
}
void SCH_Add_Task(void (*fPtr)(void), const uint32_t DELAY, const uint32_t PERIOD) {
uint32_t Index = 0;
while ((SCH_tasks_G[Index].taskFunc != 0) && (Index < SCH_MAX_TASKS)) {
Index++;
}
if (Index < SCH_MAX_TASKS) {
SCH_tasks_G[Index].taskFunc = fPtr;
SCH_tasks_G[Index].delay = DELAY;
SCH_tasks_G[Index].period = PERIOD;
SCH_tasks_G[Index].run = 0;
}
}
int main(void) {
// 初始化systick定时器
SysTick_Config(SystemCoreClock / 1000);
// 添加任务
SCH_Add_Task(Task1, 0, 1000);
SCH_Add_Task(Task2, 0, 2000);
// 主循环
while (1) {
SCH_Dispatch_Tasks();
__WFI(); // 进入低功耗模式
}
}
这个例子中,我们定义了一个任务结构体,包含了任务函数指针、任务延迟、任务周期和运行标志。在systick定时器中断服务程序中,我们检查每个任务,如果任务延迟为0,我们就标记任务为可运行,并重置任务延迟。在主循环中,我们检查每个任务,如果任务被标记为可运行,我们就运行任务,并清除运行标志。
这个例子只是一个基本的框架,你可能需要根据你的具体需求进行修改。希望这个例子能对你有所帮助!
我:低功耗对我非常重要,你写的代码需要每1ms就要进行一次中断判断,实际还是做了很多无效判断,我希望是动态的调整systick的定时任务,计算出下一任务还需要多长时间执行,就设定多长时间的中断,这样保障系统尽可能的在休眠状态,提高执行效率
GPT:我理解你的需求,你希望系统能够动态地调整systick的定时任务,以便在尽可能多的时间里让系统处于休眠状态,从而提高执行效率。以下是一个修改后的示例,它会动态地调整systick的定时任务:
#include "stm32f0xx.h"
typedef struct {
void (*taskFunc)(void);
uint32_t delay;
uint32_t period;
uint8_t run;
} sTask;
#define SCH_MAX_TASKS 5
sTask SCH_tasks_G[SCH_MAX_TASKS];
void SysTick_Handler(void) {
uint32_t index;
for (index = 0; index < SCH_MAX_TASKS; index++) {
if ((SCH_tasks_G[index].taskFunc != 0) && (SCH_tasks_G[index].delay == 0)) {
SCH_tasks_G[index].run = 1;
if (SCH_tasks_G[index].period) {
SCH_tasks_G[index].delay = SCH_tasks_G[index].period;
}
}
if (SCH_tasks_G[index].delay > 0) {
SCH_tasks_G[index].delay--;
}
}
// 重新计算下一次中断的时间
uint32_t nextInterrupt = UINT32_MAX;
for (index = 0; index < SCH_MAX_TASKS; index++) {
if (SCH_tasks_G[index].taskFunc != 0 && SCH_tasks_G[index].delay < nextInterrupt) {
nextInterrupt = SCH_tasks_G[index].delay;
}
}
// 设置下一次中断的时间
SysTick->LOAD = nextInterrupt;
SysTick->VAL = 0;
}
void SCH_Dispatch_Tasks(void) {
uint32_t index;
for (index = 0; index < SCH_MAX_TASKS; index++) {
if (SCH_tasks_G[index].run == 1) {
(*SCH_tasks_G[index].taskFunc)();
SCH_tasks_G[index].run = 0;
}
}
}
void SCH_Add_Task(void (*fPtr)(void), const uint32_t DELAY, const uint32_t PERIOD) {
uint32_t Index = 0;
while ((SCH_tasks_G[Index].taskFunc != 0) && (Index < SCH_MAX_TASKS)) {
Index++;
}
if (Index < SCH_MAX_TASKS) {
SCH_tasks_G[Index].taskFunc = fPtr;
SCH_tasks_G[Index].delay = DELAY;
SCH_tasks_G[Index].period = PERIOD;
SCH_tasks_G[Index].run = 0;
}
}
int main(void) {
SysTick_Config(SystemCoreClock / 1000);
SCH_Add_Task(Task1, 0, 1000);
SCH_Add_Task(Task2, 0, 2000);
while (1) {
SCH_Dispatch_Tasks();
__WFI();
}
}
在这个修改后的示例中,我在SysTick_Handler函数中添加了一段代码,用于计算下一次中断的时间,并设置systick定时器的加载值。这样,systick定时器的中断时间就会根据任务的需求动态地进行调整,从而使系统能够在尽可能多的时间里处于休眠状态,提高执行效率。希望这个修改后的示例能满足你的需求!
我:SysTick_Handler()函数能够把每句话加上注释吗,有点看不懂
GPT:当然可以,我会为SysTick_Handler()函数中的每一行代码添加注释,以帮助你理解。以下是带有注释的代码:
void SysTick_Handler(void) {
uint32_t index;
// 遍历任务数组
for (index = 0; index < SCH_MAX_TASKS; index++) {
// 如果任务函数指针不为空且任务延迟为0
if ((SCH_tasks_G[index].taskFunc != 0) && (SCH_tasks_G[index].delay == 0)) {
// 标记任务为可运行
SCH_tasks_G[index].run = 1;
// 如果任务周期不为0,则重置任务延迟
if (SCH_tasks_G[index].period) {
SCH_tasks_G[index].delay = SCH_tasks_G[index].period;
}
}
// 如果任务延迟大于0,则递减任务延迟
if (SCH_tasks_G[index].delay > 0) {
SCH_tasks_G[index].delay--;
}
}
// 初始化下一次中断的时间为最大值
uint32_t nextInterrupt = UINT32_MAX;
// 再次遍历任务数组
for (index = 0; index < SCH_MAX_TASKS; index++) {
// 如果任务函数指针不为空且任务延迟小于下一次中断的时间
if (SCH_tasks_G[index].taskFunc != 0 && SCH_tasks_G[index].delay < nextInterrupt) {
// 更新下一次中断的时间
nextInterrupt = SCH_tasks_G[index].delay;
}
}
// 设置systick定时器的加载值为下一次中断的时间
SysTick->LOAD = nextInterrupt;
// 清零systick定时器的当前值
SysTick->VAL = 0;
}
这段代码的主要目的是在每次systick定时器中断时更新任务的状态,并计算下一次中断的时间。希望这些注释能帮助你理解这段代码!
我:我发现这段代码由逻辑错误,因为我们的下一次中断是按最短延时来计算,也就是可能是不定值的毫秒数,而代码这里却每次只是减1操作:// 如果任务延迟大于0,则递减任务延迟
if (SCH_tasks_G[index].delay > 0) {
SCH_tasks_G[index].delay--;
}
GPT:你的观察非常准确,这确实是一个问题。在这个代码中,我们假设每次中断的时间间隔是1ms,所以每次中断我们只减少1ms的延迟。但是,如果我们动态地改变中断的时间间隔,那么我们就需要减少相应的延迟时间,而不是只减少1ms。我们可以通过修改SysTick_Handler函数来解决这个问题。具体来说,我们可以在计算出下一次中断的时间后,将这个时间间隔从所有任务的延迟中减去。以下是修改后的代码:
void SysTick_Handler(void) {
uint32_t index;
// 初始化下一次中断的时间为最大值
uint32_t nextInterrupt = UINT32_MAX;
// 遍历任务数组
for (index = 0; index < SCH_MAX_TASKS; index++) {
// 如果任务函数指针不为空且任务延迟小于下一次中断的时间
if (SCH_tasks_G[index].taskFunc != 0 && SCH_tasks_G[index].delay < nextInterrupt) {
// 更新下一次中断的时间
nextInterrupt = SCH_tasks_G[index].delay;
}
}
// 遍历任务数组
for (index = 0; index < SCH_MAX_TASKS; index++) {
// 如果任务函数指针不为空
if (SCH_tasks_G[index].taskFunc != 0) {
// 如果任务延迟大于下一次中断的时间
if (SCH_tasks_G[index].delay >= nextInterrupt) {
// 减去下一次中断的时间
SCH_tasks_G[index].delay -= nextInterrupt;
// 如果任务延迟为0
if (SCH_tasks_G[index].delay == 0) {
// 标记任务为可运行
SCH_tasks_G[index].run = 1;
// 如果任务周期不为0,则重置任务延迟
if (SCH_tasks_G[index].period) {
SCH_tasks_G[index].delay = SCH_tasks_G[index].period;
}
}
}
}
}
// 设置systick定时器的加载值为下一次中断的时间
SysTick->LOAD = nextInterrupt;
// 清零systick定时器的当前值
SysTick->VAL = 0;
}
希望这个修改后的代码能满足你的需求!
我:感觉还是有点问题,nextInterrupt在前面就获得值了,然后再去遍历任务数组,那不等于本次调用SysTick_Handler中断处理函数,就立即对应的任务标记可运行了?应该是中断下一轮,才到吧,实际上nextInterrupt要定义成static变量,然后更新其值要放后面吧
GPT:你的观察非常准确,我理解你的担忧。确实,我们应该在下一轮中断时才标记任务为可运行。为了解决这个问题,我们可以将nextInterrupt
定义为静态变量,并在每次中断时使用它来更新任务的延迟,然后在中断的最后更新nextInterrupt
的值。以下是修改后的代码:
void SysTick_Handler(void) {
static uint32_t nextInterrupt = 1; // 初始化为1,以便在第一次中断时运行
uint32_t index;
// 遍历任务数组
for (index = 0; index < SCH_MAX_TASKS; index++) {
// 如果任务函数指针不为空
if (SCH_tasks_G[index].taskFunc != 0) {
// 如果任务延迟大于等于下一次中断的时间
if (SCH_tasks_G[index].delay >= nextInterrupt) {
// 减去下一次中断的时间
SCH_tasks_G[index].delay -= nextInterrupt;
// 如果任务延迟为0
if (SCH_tasks_G[index].delay == 0) {
// 标记任务为可运行
SCH_tasks_G[index].run = 1;
// 如果任务周期不为0,则重置任务延迟
if (SCH_tasks_G[index].period) {
SCH_tasks_G[index].delay = SCH_tasks_G[index].period;
}
}
}
}
}
// 初始化下一次中断的时间为最大值
nextInterrupt = UINT32_MAX;
// 再次遍历任务数组
for (index = 0; index < SCH_MAX_TASKS; index++) {
// 如果任务函数指针不为空且任务延迟小于下一次中断的时间
if (SCH_tasks_G[index].taskFunc != 0 && SCH_tasks_G[index].delay < nextInterrupt) {
// 更新下一次中断的时间
nextInterrupt = SCH_tasks_G[index].delay;
}
}
// 设置systick定时器的加载值为下一次中断的时间
SysTick->LOAD = nextInterrupt;
// 清零systick定时器的当前值
SysTick->VAL = 0;
}
这样,我们就可以确保只有在下一轮中断时才会标记任务为可运行。希望这个修改后的代码能满足你的需求!
我:嗯,太好了,这个中断处理函数目前没问题了,那么对应的SCH_Add_Task函数也要做一些修正,新增了任务后,对应的nextInterrupt参数和SysTick也要更新
GPT:你的观察非常准确,当我们添加新的任务时,我们确实需要更新nextInterrupt
和systick定时器。以下是修改后的SCH_Add_Task
函数:
void SCH_Add_Task(void (*fPtr)(void), const uint32_t DELAY, const uint32_t PERIOD) {
uint32_t Index = 0;
// 寻找任务数组中的空闲位置
while ((SCH_tasks_G[Index].taskFunc != 0) && (Index < SCH_MAX_TASKS)) {
Index++;
}
// 如果找到空闲位置
if (Index < SCH_MAX_TASKS) {
// 添加新任务
SCH_tasks_G[Index].taskFunc = fPtr;
SCH_tasks_G[Index].delay = DELAY;
SCH_tasks_G[Index].period = PERIOD;
SCH_tasks_G[Index].run = 0;
// 如果新任务的延迟小于下一次中断的时间
if (DELAY < nextInterrupt) {
// 更新下一次中断的时间
nextInterrupt = DELAY;
// 设置systick定时器的加载值为下一次中断的时间
SysTick->LOAD = nextInterrupt;
// 清零systick定时器的当前值
SysTick->VAL = 0;
}
}
}
在这个修改后的函数中,当我们添加新的任务时,我们会检查新任务的延迟是否小于下一次中断的时间。如果是,我们就更新下一次中断的时间,并设置systick定时器的加载值为下一次中断的时间。希望这个修改后的函数能满足你的需求!
我:那再把上面确定的这些,全部写出来,我再梳理下
GPT:当然,以下是我们讨论并确定的代码:
#include "stm32f0xx.h"
typedef struct {
void (*taskFunc)(void);
uint32_t delay;
uint32_t period;
uint8_t run;
} sTask;
#define SCH_MAX_TASKS 5
sTask SCH_tasks_G[SCH_MAX_TASKS];
static uint32_t nextInterrupt = 1; // 初始化为1,以便在第一次中断时运行
void SysTick_Handler(void) {
uint32_t index;
// 遍历任务数组
for (index = 0; index < SCH_MAX_TASKS; index++) {
// 如果任务函数指针不为空
if (SCH_tasks_G[index].taskFunc != 0) {
// 如果任务延迟大于等于下一次中断的时间
if (SCH_tasks_G[index].delay >= nextInterrupt) {
// 减去下一次中断的时间
SCH_tasks_G[index].delay -= nextInterrupt;
// 如果任务延迟为0
if (SCH_tasks_G[index].delay == 0) {
// 标记任务为可运行
SCH_tasks_G[index].run = 1;
// 如果任务周期不为0,则重置任务延迟
if (SCH_tasks_G[index].period) {
SCH_tasks_G[index].delay = SCH_tasks_G[index].period;
}
}
}
}
}
// 初始化下一次中断的时间为最大值
nextInterrupt = UINT32_MAX;
// 再次遍历任务数组
for (index = 0; index < SCH_MAX_TASKS; index++) {
// 如果任务函数指针不为空且任务延迟小于下一次中断的时间
if (SCH_tasks_G[index].taskFunc != 0 && SCH_tasks_G[index].delay < nextInterrupt) {
// 更新下一次中断的时间
nextInterrupt = SCH_tasks_G[index].delay;
}
}
// 设置systick定时器的加载值为下一次中断的时间
SysTick->LOAD = nextInterrupt;
// 清零systick定时器的当前值
SysTick->VAL = 0;
}
void SCH_Add_Task(void (*fPtr)(void), const uint32_t DELAY, const uint32_t PERIOD) {
uint32_t Index = 0;
// 寻找任务数组中的空闲位置
while ((SCH_tasks_G[Index].taskFunc != 0) && (Index < SCH_MAX_TASKS)) {
Index++;
}
// 如果找到空闲位置
if (Index < SCH_MAX_TASKS) {
// 添加新任务
SCH_tasks_G[Index].taskFunc = fPtr;
SCH_tasks_G[Index].delay = DELAY;
SCH_tasks_G[Index].period = PERIOD;
SCH_tasks_G[Index].run = 0;
// 如果新任务的延迟小于下一次中断的时间
if (DELAY < nextInterrupt) {
// 更新下一次中断的时间
nextInterrupt = DELAY;
// 设置systick定时器的加载值为下一次中断的时间
SysTick->LOAD = nextInterrupt;
// 清零systick定时器的当前值
SysTick->VAL = 0;
}
}
}
void SCH_Dispatch_Tasks(void) {
uint32_t index;
for (index = 0; index < SCH_MAX_TASKS; index++) {
if (SCH_tasks_G[index].run == 1) {
(*SCH_tasks_G[index].taskFunc)();
SCH_tasks_G[index].run = 0;
}
}
}
int main(void) {
SysTick_Config(SystemCoreClock / 1000);
SCH_Add_Task(Task1, 0, 1000);
SCH_Add_Task(Task2, 0, 2000);
while (1) {
SCH_Dispatch_Tasks();
__WFI();
}
}
这段代码实现了一个基于STM32F030C8T6的低功耗伪时间片调度算法,使用了systick定时器来触发事件,并且能够动态地调整systick的定时任务,以...(GPT卡壳了,但不重要)
我:我觉得在SysTick_Handler()函数中if (SCH_tasks_G[index].delay >= nextInterrupt) 的else情况也写一下,这样更健全点儿,当不知名原因导致delay小于nextInterrupt,定时时间肯定也是到了
GPT:你的建议非常好,确实应该考虑到这种情况。我们可以在if (SCH_tasks_G[index].delay >= nextInterrupt)
后面添加一个else
语句,处理delay
小于nextInterrupt
的情况。以下是修改后的代码:
#include "stm32f0xx.h"
typedef struct {
void (*taskFunc)(void);
uint32_t delay;
uint32_t period;
uint8_t run;
} sTask;
#define SCH_MAX_TASKS 5
sTask SCH_tasks_G[SCH_MAX_TASKS];
static uint32_t nextInterrupt = 1; // 初始化为1,以便在第一次中断时运行
void SysTick_Handler(void) {
uint32_t index;
// 遍历任务数组
for (index = 0; index < SCH_MAX_TASKS; index++) {
// 如果任务函数指针不为空
if (SCH_tasks_G[index].taskFunc != 0) {
// 如果任务延迟大于等于下一次中断的时间
if (SCH_tasks_G[index].delay >= nextInterrupt) {
// 减去下一次中断的时间
SCH_tasks_G[index].delay -= nextInterrupt;
// 如果任务延迟为0
if (SCH_tasks_G[index].delay == 0) {
// 标记任务为可运行
SCH_tasks_G[index].run = 1;
// 如果任务周期不为0,则重置任务延迟
if (SCH_tasks_G[index].period) {
SCH_tasks_G[index].delay = SCH_tasks_G[index].period;
}
}
} else {
// 如果任务延迟小于下一次中断的时间
// 标记任务为可运行
SCH_tasks_G[index].run = 1;
SCH_tasks_G[index].delay = 0;
// 如果任务周期不为0,则重置任务延迟
if (SCH_tasks_G[index].period) {
SCH_tasks_G[index].delay = SCH_tasks_G[index].period;
}
}
}
}
// 初始化下一次中断的时间为最大值
nextInterrupt = UINT32_MAX;
// 再次遍历任务数组
for (index = 0; index < SCH_MAX_TASKS; index++) {
// 如果任务函数指针不为空且任务延迟小于下一次中断的时间
if (SCH_tasks_G[index].taskFunc != 0 && SCH_tasks_G[index].delay < nextInterrupt) {
// 更新下一次中断的时间
nextInterrupt = SCH_tasks_G[index].delay;
}
}
// 设置systick定时器的加载值为下一次中断的时间
SysTick->LOAD = nextInterrupt;
// 清零systick定时器的当前值
SysTick->VAL = 0;
}
void SCH_Add_Task(void (*fPtr)(void), const uint32_t DELAY, const uint32_t PERIOD) {
uint32_t Index = 0;
// 寻找任务数组中的空闲位置
while ((SCH_tasks_G[Index].taskFunc != 0) && (Index < SCH_MAX_TASKS)) {
Index++;
}
// 如果找到空闲位置
if (Index < SCH_MAX_TASKS) {
// 添加新任务
SCH_tasks_G[Index].taskFunc = fPtr;
SCH_tasks_G[Index].delay = DELAY;
SCH_tasks_G[Index].period = PERIOD;
SCH_tasks_G[Index].run = 0;
// 如果新任务的延迟小于下一次中断的时间
if (DELAY < nextInterrupt) {
// 更新下一次中断的时间
nextInterrupt = DELAY;
// 设置systick定时器的加载值为下一次中断的时间
SysTick->LOAD = nextInterrupt;
// 清零systick定时器的当前值
SysTick->VAL = 0;
}
}
}
void SCH_Dispatch_Tasks(void) {
uint32_t index;
for (index = 0; index < SCH_MAX_TASKS; index++) {
if (SCH_tasks_G[index].run == 1) {
(*SCH_tasks_G[index].taskFunc)();
SCH_tasks_G[index].run = 0;
}
}
}
void Task1(void)
{
}
void Task2(void)
{
}
int main(void) {
SysTick_Config(SystemCoreClock / 1000);
SCH_Add_Task(Task1, 0, 1000);
SCH_Add_Task(Task2, 0, 2000);
while (1) {
SCH_Dispatch_Tasks();
__WFI();
}
}
在这个修改后的代码中,如果任务延迟小于下一次中断的时间,我们会立即标记任务为可运行,并重置任务延迟。这样,我们就可以确保在任何情况下,只...(GPT又卡壳了,不重要)
四、总结
是不是和我们平时单片机编程很不一样,实际上AI编程它总结了很多前任的编程经验,以及足够的框架规范,所以它的整体框架还是不错的,但是它并不会很好的完成每一个细节,甚至有一些bug,这反而正是我们自己已经梳理好的,所以我们给它指导出来即可;这样就和AI协同完成了一个基本框架,接下来自己要做的就是移植到指定的芯片上,完成基本的底层驱动开发即可。