大橙子疯

  • 2024-10-08
  • 发表了主题帖: 轻量级多级菜单控制框架程序

    本帖最后由 大橙子疯 于 2024-10-9 09:01 编辑 # 轻量级菜单框架(C语言) 作为嵌入式软件开发,可能经常会使用命令行或者显示屏等设备实现人机交互的功能,功能中通常情况都包含 UI 菜单设计;很多开发人员都会有自己的菜单框架模块,防止重复造轮子,网上有很多这种菜单框架的代码,但是大多耦合性太强,无法独立出来适配不同的菜单设计。 本文介绍一个降低了耦合性,完全独立的菜单框架,菜单显示风格和显示平台完全由自己根据需求设计,而菜单操作统一由菜单模块处理即可,提高程序的移植性。 ## 介绍 主要特点有: - 移植性强,无需修改即可使用 - 采用链表方式实现多级菜单(通过配置选择采用动态分配或者数组实现) - 菜单控制框架作为独立模块,完全不会和按键模块、显示模块进行耦合 - 不受菜单的显示风格和显示平台影响,可自由选择设计显示风格和显示平台 - 快捷菜单操作等 - 可以采用表驱动的方式初始化菜单,提高代码的可读性 ### 多级菜单 同级菜单以数组的方式体现,父菜单和子菜单的关联则使用链表实现。 > 数组元素内容有: > > * 菜单选项描述方式可选字符串或文本ID(多国语言时,可以统一管理翻译文件,而文本ID可以适用于数组管理的翻译文件) > * 菜单选项进入回调函数:当前菜单选项进入时(从父菜单进入)需要执行一次的函数 > * 菜单选项退出回调函数:当前菜单选项进入后退出时(退出至父菜单)需要执行一次的函数 > * 菜单选项重加载回调函数:当前菜单选项每次加载时(从父菜单进入或子菜单退出)需要执行一次的函数 > * 菜单选项周期调度回调函数:当前菜单选项的周期调度函数 > * 菜单选项的扩展数据 > > 链表内存可以选择采用动态内存分配或者数组实现 方便对不同菜单界面功能解耦 > 大部分菜单采用的都是数组中包含了所有不同级别的菜单选项内容实现,无法做到很好的解耦方式; > > 该模块通过动态绑定子菜单和链表的方式可以达到较好的解耦状态 ### 显示效果 该框架只负责菜单选项控制操作,不负责在图像界面显示效果,需要在对应的回调函数中实现菜单显示效果。 > 设置对应的效果显示函数,即可为不同的菜单设置不同的菜单显示效果,比如图标式、列表式或右侧弹窗式等。 > > 可以在不同显示平台体现,比如LCD、OLED或终端界面等。 ### 多语种 没有直接的语种设置,而是通过联合体提供选择多种选择。 > 文本ID: 适用于多语种采用数组定义的翻译内容 > 字符串: 适用于单语种或者多语种,采用映射的方式实现的翻译内容 ### 可扩展 每级菜单选项都可以设置自定义数据,用来实现更多的菜单操作或者显示效果等。 > 不同级别的菜单可以设置自定义数据(比如菜单选项隐藏/图标数据等) ### 可配置 | 配置选项                    | 描述                                                          | | --------------------------- | ------------------------------------------------------------- | | `_COT_MENU_USE_MALLOC_`   | 定义则采用 malloc/free 的方式实现多级菜单, 否则通过数组的形式 | | `_COT_MENU_USE_SHORTCUT_` | 定义则启用快捷菜单选项进入功能                                | | `COT_MENU_MAX_DEPTH`      | 多级菜单深度                                                  | | `COT_MENU_MAX_NUM`        | 菜单支持的最大选项数目                                        | ### 功能多样化 * [X] 支持快速进入指定菜单界面。   > - 可以通过相对选项索引或者绝对选项索引路径实现   > * [X] 可以实现有限界面内显示少量的菜单选项内容。   > - 有现成的函数可用,无需担心使用不同尺寸重新实现菜单选项部分可见   > ## 使用说明 ### 菜单初始化和使用 ```c // 定义菜单信息,函数由主菜单模块定义并提供 static cotMainMenuCfg_t sg_tMainMenu = {"主菜单", Hmi_EnterMainHmi, NULL, NULL, NULL}; int main(void) {     cotMenu_Init(&sg_tMainMenu);     while (1)     {         ...         if (timeFlag)         {             timeFlag = 0;             cotMenu_Task(); // 周期调度         }     } } ``` ### 主菜单定义和绑定 定义一个主菜单选项内容、主菜单显示效果函数和主菜单进入函数等 ```c // 扩展数据为图标文件名字,每个菜单选项描述为文本ID cotMenuList_t sg_MainMenuTable[] = {     COT_MENU_ITEM_BIND(TEXT_MUSIC,  Hmi_MusicEnter, Hmi_MusicExit, Hmi_MusicLoad, Hmi_MusicTask, (MenuImage_t *)&sgc_MusicImage),        COT_MENU_ITEM_BIND(TEXT_VIDEO,  NULL, Hmi_VideoExit, Hmi_VideoLoad, Hmi_VideoTask, (MenuImage_t *)&sgc_VideoImage),        COT_MENU_ITEM_BIND(TEXT_CAMERA,  Hmi_CameraEnter, Hmi_CameraExit, Hmi_CameraLoad, Hmi_CameraTask, (MenuImage_t *)&sgc_CameraImage),        COT_MENU_ITEM_BIND(TEXT_SETTING,  Hmi_SetEnter, Hmi_SetExit, Hmi_SetLoad,   Hmi_SetTask, (MenuImage_t *)&sgc_SettingImage), }; /* 主菜单显示效果 */ static void ShowMainMenu(cotMenuShow_t *ptShowInfo) {     char *pszSelectDesc = get_text(ptShowInfo->pszItemsDesc[ptShowInfo->selectItem].textId); // 根据文本ID找到对应的字符串(多语言)     oledsize_t idx = (128 - 6 * strlen(pszSelectDesc)) / 2;     cotOled_DrawGraphic(40, 0, (const char *)ptShowInfo->pItemsListExtendData[ptShowInfo->selectItem], 1);        cotOled_SetText(0, 50, "                ", 0, FONT_12X12);     cotOled_SetText(idx, 50, pszSelectDesc, 0, FONT_12X12); } void Hmi_EnterMainHmi(const cotMenuItemInfo_t *pItemInfo) {     cotMenu_Bind(sg_MainMenuTable, COT_GET_MENU_NUM(sg_MainMenuTable), ShowMainMenu); } ``` ### 子菜单定义和绑定 如果菜单选项有子菜单,则该菜单选项调用 `cotMenu_Enter`,进入回调函数**不能为NULL**,且该回调函数需调用 `cotMenu_Bind`进行绑定 ```c /* 设置的子菜单内容,每个菜单选项描述为字符串 */ cotMenuList_t sg_SetMenuTable[] = {     COT_MENU_ITEM_BIND("语言", NULL, NULL, NULL, OnLanguageFunction, NULL),     COT_MENU_ITEM_BIND("蓝牙", NULL, NULL, NULL, OnBluetoothFunction, NULL),     COT_MENU_ITEM_BIND("电池", NULL, NULL, NULL, OnBatteryFunction, NULL),     COT_MENU_ITEM_BIND("储存", NULL, NULL, NULL, OnStorageFunction, NULL),     COT_MENU_ITEM_BIND("更多", Hmi_MoreSetEnter, Hmi_MoreSetExit, Hmi_MoreSetLoad, Hmi_MoreSetTask, NULL), }; /* 设置菜单显示效果 */ static void ShowSetMenu(cotMenuShow_t *ptShowInfo) {     uint8_t showNum = 3;     menusize_t  tmpselect;     cotMenu_LimitShowListNum(ptShowInfo, &showNum);     // 这里直接使用字符串     printf("\e[0;30;46m ------------- %s ------------- \e[0m\n", tParentMenuShowInfo.uMenuDesc.pTextString);     for (int i = 0; i < showNum; i++)     {         tmpselect = i + ptShowInfo->showBaseItem;         if (tmpselect == ptShowInfo->selectItem)         {             printf("\e[0;30;47m %d. %-16s\e[0m |\n", tmpselect + 1, ptShowInfo->pszItemsDesc[tmpselect].pTextString);         }         else         {             printf("\e[7;30;47m %d. %-16s\e[0m |\n", tmpselect + 1, ptShowInfo->pszItemsDesc[tmpselect].pTextString);         }     } } void Hmi_SetEnter(const cotMenuItemInfo_t *pItemInfo) {     // 进入设置选项后绑定子菜单,同时为当前绑定的菜单设置显示效果函数     cotMenu_Bind(sg_SetMenuTable, COT_GET_MENU_NUM(sg_SetMenuTable), ShowSetMenu); } ``` ### 菜单控制 通过调用相关函数实现菜单选项选择、进入、退出等 ```c // 需要先进入主菜单 cotMenu_MainEnter(); // 选择上一个,支持循环选择(即第一个可跳转到最后一个) cotMenu_SelectPrevious(true); // 选择下一个,不支持循环选择(即最后一个不可跳转到第一个) cotMenu_SelectNext(false); // 进入,会执行菜单选项的 pfnEnterCallFun 回调函数 cotMenu_Enter(); // 退出,会执行父菜单该选项的 pfnExitCallFun 回调函数,并在退出后父菜单选项列表复位从头选择 cotMenu_Exit(true); ``` 定义了多个菜单选项表后,用来区分上下级,在某个菜单选项进入时绑定 1. 使用前初始化函数 cotMenu_Init, 设置主菜单内容 2. 周期调用函数 cotMenu_Task, 用来处理菜单显示和执行相关回调函数 3. 使用其他函数对菜单界面控制 ## 代码链接 [轻量级多级菜单控制框架程序](https://gitee.com/cot_package/cot_menu) STM32 + OLED 效果图:demo样式代码链接:[stm32 菜单效果demo](https://gitee.com/cot_package/demo_stm32) 命令行终端效果:

  • 2024-02-20
  • 回复了主题帖: 轻量级参数管理框架(C语言)

    LitchiCheng 发表于 2024-2-20 16:29 我们是用sqlite或者level_db管理参数,类似ROS的参数管理 这里更适合用于单片机中进行参数管理

  • 回复了主题帖: 轻量级参数管理框架(C语言)

    lugl4313820 发表于 2024-2-19 17:27 这个有什么用处呀,楼主再普及一知识。 一开始就说了,参数或者各种数据管理

  • 2024-02-19
  • 发表了主题帖: 轻量级参数管理框架(C语言)

    嵌入式软件中的系统数据参数是指在嵌入式系统中用于实现系统功能和控制的各种数据,如用户参数、状态、配置信息等;那么如何管理这些数据对于嵌入式系统的正确运行和维护非常重要。 该参数管理框架代码就是如何统一管理软件中的各类系统数据参数。 > 该参数管理并不涉及数据是如何储存的。因为有些系统数据并不需要储存起来,只需要进行管理而已。 ## 介绍 ### 参数管理 * [X] 通过将已定义变量(**全局变量**)添加到参数表进行参数的统一管理,单个参数包括了当前值、缺省值、最小值、最大值、参数名和属性等信息。   > - 当前值:已定义的变量   > - 缺省值:默认值,并非变量初值   > - 最小值:该参数的最小值   > - 最大值:该参数的最大值   > - 参数名:对该参数的描述   > - 属性:有多种属性信息,方便后续功能扩展   >   - 读/写权限:参数模块中无具体作用,可用于UI或者其他方式显示时使用   >   - 重置权限:设置默认值后则存在该属性   >   - 校验权限:设置最大最小值后则存在该属性   > * [X] 根据不同的场景管理不同的参数   > - 无缺省值、最小值和最大值限制,适合于记录类型的参数,比如状态数据或历史数据等   > - 有缺省值,但无最小值和最大值限制,适合于配置类型的参数   > - 有缺省值,最小值和最大值限制,适合于关键性类型的参数,比如用户参数或者关键的状态数据等   > * [X] 同时若单个参数表无法满足参数数目或者参数分类管理,可定义多张参数表   > - 每张参数表中的参数ID唯一,不可重复;   > - 不同参数表ID可以重复定义   > ### 参数类型 * [X] 数值类型参数   > `int`、`float`、`double` 等基本类型的参数   > * [X] 字符串类型参数   > `char` 定义用来储存字符串的数组   > ### 参数校验 为了更好的统一管理参数,可以对参数设置的范围进行校验,防止参数设置超出预期范围,导致程序不可控。 * [X] 范围校验   > 根据参数的最大和最小值进行判断,数值类型的参数则根据数值超出范围判断。而字符串则是根据字符串长度超出范围判断。   > * [X] 自定义校验   > 提供回调函数,每个参数可设置自定义的校验方式,比如某个参数需要设置为多少的倍数,或者根据其他参数决定当前参数的取值范围等。   > 上述两种校验方式均需要参数设置缺省值,最小值和最大值后才有效。 ### 兼容性 * [X] 提供了参数表的序列化和反序列化操作。   > - 方便在本地储存设备(如flash、eeprom等)保存/读取二进制数据,甚至还可以跨设备传输使用   > - 提供了两种方式:   >   - 第一种:只需要提供参数数据保存/加载的回调函数,调用相关接口函数完成参数的序列化保存和反序列化加载;该方式会多次触发回调函数保存数据和读取数据,适用于小内存的平台使用(不需要申请内存)   >   - 第二种:需要提前申请内存用来保存参数表序列化的数据或者读取即将反序列化的数据;一次性完成操作(需要申请较大的内存完成)   > * [X] 支持启用键值对功能   > - 每个参数都需要指定唯一的ID,在后期版本迭代对参数表删除、插入或添加参数时也能向下兼容,不会影响其他参数。   > - 即使更新了参数表的最大容纳范围,也能保证兼容,比如上个版本配置参数只支持16个,当前版本配置支持了256个,也能正确处理。   > - 启用键值对后序列化的数据长度也会比较大,因为每个参数序列化时包含了ID和长度信息。   > ### 可裁剪 根据不同的平台,可以对部分功能裁剪,或者修改配置适用于不同容量的芯片进行开发。 | 配置选项                        | 描述                                                                                                                                                                                                                                                                                                                                                   | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `COT_PARAM_USE_KEY_VALUE`     | 是否采用键值对方式序列化和反序列化参数数据                                                                                                                                                                                                                                                                                                             | | `COT_PARAM_USE_CUSTOM_CHECK`  | 是否启用参数自定义校验功能                                                                                                                                                                                                                                                                                                                             | | `COT_PARAM_USE_STRING_TYPE`   | 是否启用字符串参数类型                                                                                                                                                                                                                                                                                                                                 | | `COT_PARAM_USE_64_BIT_LENGTH` | 是否启用64bit的参数类型                                                                                                                                                                                                                                                                                                                                | | `COT_PARAM_NAME_MAX_LENGTH`   | 参数名字最大定义长度,小于或等于1则禁用参数名功能                                                                                                                                                                                                                                                                                                      | | `COT_PARAM_STRING_MAX_LENGTH` | 字符串类型的参数取值最大定义长度(包括结束符),需启用 `COT_PARAM_USE_STRING_TYPE`                                                                                                                                                                                                                                                                   | | `COT_PARAM_SUPPORT_NUM`       | 单张参数表最多添加多少个参数,需启用 `COT_PARAM_USE_KEY_VALUE`,可选:`COT_PARAM_SUPPORT_16`:ID取值范围0-15,即最多16个参数`COT_PARAM_SUPPORT_256`:ID取值范围0-255,即最多256个参数 `COT_PARAM_SUPPORT_4096`:ID取值范围0-4095,即最多4096个参数注:若没有启用 `COT_PARAM_USE_KEY_VALUE` 键值对方式,则无限制 | ## 软件设计 略 ## 使用说明 ### 参数表定义 ```c typedef struct {     uint16_t usValue;     uint8_t ucValue;     uint32_t uiValue;     float fValue;     char szString_1[12];     double dValue;     int16_t sValue;     int8_t cValue;     int32_t iValue;     char szString_2[10]; }ParamDemo_t; ParamDemo_t g_tTestVal = {     .usValue = 20,     .ucValue = 10,     .uiValue = 1000,     .fValue = 3.14,     .szString_1 = "abcd",     .dValue = 5.12,     .sValue = -100,     .cValue = -2,     .iValue = 300,     .szString_2 = "12234", }; int8_t g_cTest = 50; char g_szString[10] = "qwer"; static int CheckSValue(const void *pCurParam); cotParamInfo_t sg_ParamTable[] = {     COT_PARAM_ITEM_BIND(1, g_tTestVal.usValue, COT_PARAM_UINT16, COT_PARAM_ATTR_WR),     COT_PARAM_ITEM_BIND(2, g_tTestVal.ucValue, COT_PARAM_UINT8, COT_PARAM_ATTR_WR, 20),     COT_PARAM_ITEM_BIND(3, g_tTestVal.uiValue, COT_PARAM_UINT32, COT_PARAM_ATTR_WR, 1000, 1000, 10000),     COT_PARAM_ITEM_BIND(4, g_tTestVal.fValue, COT_PARAM_FLOAT, COT_PARAM_ATTR_WR, 10, -10.5, 10.5),     COT_PARAM_ITEM_BIND(5, g_tTestVal.szString_1, COT_PARAM_STRING, COT_PARAM_ATTR_WR, "abcd", 3, sizeof(g_tTestVal.szString_1)),     COT_PARAM_ITEM_BIND(6, g_tTestVal.dValue, COT_PARAM_DOUBLE, COT_PARAM_ATTR_WR, 0, -90.10, 100.10),     COT_PARAM_ITEM_BIND(7, g_tTestVal.sValue, COT_PARAM_INT16, COT_PARAM_ATTR_WR, 100, -200, 200, CheckSValue), // 添加自定义校验     COT_PARAM_ITEM_BIND_WITH_NAME(8, "g_cTest", g_cTest, COT_PARAM_INT8, COT_PARAM_ATTR_WR, 50, -100, 100), // 另取参数名     COT_PARAM_ITEM_BIND(9, g_szString, COT_PARAM_STRING, COT_PARAM_ATTR_WR, "XXX", 3, 6), }; static int CheckSValue(const void *pCurParam) {     const int16_t *p_sValue = (const int16_t *)pCurParam;     if ((*p_sValue) % 2 != 0)     {         return -1;     }     return 0; } int mian() {     cotParam_Init(&sg_tParamManager, sg_ParamTable, COT_PARAM_TABLE_SIZE(sg_ParamTable)); } ``` ### 参数保存/加载 将参数表的部分信息(包括当前值)进行序列化,方便进行数据保存,等下次开机再读取数据后反序列化,还原成参数表的这部分信息。 > 甚至将这部分数据传输给另一台设备,进行参数数据备份或同步等功能 将参数表序列化完成后,可以对数据进行二次封装,比如可以增加头信息,crc校验等再进行数据保存,同样在加载时也需要解析,将额外添加的信息去除后才能进行反序列化。 ##### 第一种 提供参数数据保存/加载的回调函数,调用相关接口函数完成参数的序列化保存和反序列化加载;该方式会多次触发回调函数保存数据和读取数据,适用于小内存的平台使用(不需要额外申请内存) ```c // 所有参数校验出错时恢复默认处理 int OnCheckErrorResetHandle(const cotParamInfo_t *pParamInfo, cotParamCheckRet_e eCheckResult) {     cotParam_SingleParamResetDefValue(pParamInfo);     return 0; } // 从储存空间读取数据 int OnLoadCallback(uint8_t *pBuf, uint16_t bufSize, uint16_t *pLength) {     uint16_t length;     static uint32_t s_offset = 0;     if (s_offset < sg_length)     {         if (s_offset + bufSize 0)     {         memcpy(&sg_buf[s_offset], pBuf, len);         s_offset += len;         sg_length = s_offset;     }     else     {         s_offset = 0;     }     return 0; } // 加载参数 void LoadParam(void) {     cotParam_Load(&sg_tParamManager, OnLoadCallback);        // 加载后全部参数进行校验(可选)     cotParam_Check(&sg_tParamManager, OnCheckErrorResetHandle); } // 保存参数 void SaveParam(void) {     // 保存前全部参数进行校验(可选)     cotParam_Check(&sg_tParamManager, OnCheckErrorResetHandle);     cotParam_Save(&sg_tParamManager, OnSaveCallback); } ``` #### 第二种 需要提前申请内存用来保存参数表序列化的数据或者读取即将反序列化的数据。 1. 定义buf ```c // 加载参数 void LoadParam(void) {     uint8_t buf[500];     uint32_t length;     length = read(buf);     cotParam_Deserialization(&sg_tParamManager, buf, length);        // 加载后全部参数进行校验(可选)     cotParam_Check(&sg_tParamManager, OnCheckErrorResetHandle); } // 保存参数 void SaveParam(void) {     uint8_t buf[500];     uint32_t length;     // 保存前全部参数进行校验(可选)     cotParam_Check(&sg_tParamManager, OnCheckErrorResetHandle);     length = cotParam_Serialize(&sg_tParamManager, buf);     write(buf, length); } ``` 2. buf通过内存申请 ```c // 加载参数 void LoadParam(void) {     uint8_t pBuf = (uint8_t *)malloc(500);  // 申请足够大的内存     uint32_t length;     length = read(pBuf, length); // 获取数据的实际长度     cotParam_Deserialization(&sg_tParamManager, pBuf, length);        // 加载后全部参数进行校验(可选)     cotParam_Check(&sg_tParamManager, OnCheckErrorResetHandle); } // 保存参数 void SaveParam(void) {     uint8_t pBuf = (uint8_t *)malloc(cotParam_GetSerializeSize(&sg_tParamManager));     uint32_t length;     // 保存前全部参数进行校验(可选)     cotParam_Check(&sg_tParamManager, OnCheckErrorResetHandle);     length = cotParam_Serialize(&sg_tParamManager, pBuf);     write(buf, length); } int main() {     // 保存     uint8_t *p_wbuf = (uint8_t *)malloc(cotParam_GetSerializeSize(&sg_tParamManager));        uint32_t length = cotParam_Serialize(&sg_tParamManager, p_wbuf);     write(p_wbuf, length);     free(p_wbuf);     p_wbuf = NULL;     // 加载     uint8_t *p_rbuf = (uint8_t *)malloc(cotParam_GetSerializeSize(&sg_tParamManager));        uint32_t length = read(r_buf);     cotParam_Deserialization(&sg_tParamManager, p_rbuf, length);     write(p_rbuf, length);     free(p_rbuf);     p_rbuf = NULL; } ``` ### 校验方式 校验需要提前在参数表中设置缺省值,最小值和最大值后才有效。 #### 范围校验 根据参数设置范围进行校验。 ```c // 对某个变量当前参数进行范围校验,得到校验结果后自行处理 cotParam_SingleParamSelfCheck(cotParam_FindParamByParamPtr(&sg_tParamManager, &g_test_3), &eCheckResult); if (eCheckResult != COT_PARAM_CHECK_OK) // 修改后检查 {     cotParam_SingleParamResetDefValue(cotParam_FindParamByParamPtr(&sg_tParamManager, &g_test_3)); // 如果校验失败,则恢复为默认值 } // 对某个变量参数变更后(当前值已经变化)进行校验处理,若超出范围则恢复默认 g_test_3 = 1000; cotParam_SingleParamCheckProcess(cotParam_FindParamByParamPtr(&sg_tParamManager, &g_test_3), COT_PARAM_RESET_DEF); // 对某个变量参数在需要变更前(当前值没有变化)进行校验处理,得到校验结果后自行处理 double tmp = 1000; cotParam_SingleParamCheckInput(cotParam_FindParamByParamPtr(&sg_tParamManager, &g_test_3), &tmp, &eCheckResult); if (eCheckResult == COT_PARAM_CHECK_OK) {     g_test_3 = tmp;// 如果校验成功,则修改 } // 对某个变量参数在需要变更前(当前值没有变化)进行校验处理,若新的值超出范围则不更新变量参数当前的值 double tmp = 1000; cotParam_SingleParamUpdate(cotParam_FindParamByParamPtr(&sg_tParamManager, &g_test_3), &tmp, COT_PARAM_RESET_NONE) ``` #### 自定义校验 在符合参数范围内,根据参数设置的自定义校验函数进行校验。 ```c static int CheckTestS16(const void *pCurParam) {     const int16_t *p_test_s16 = (const int16_t *)pCurParam;     /* 需要校验 g_test_s16 为2的倍数(建议不能直接使用 g_test_s16 去判断,因为回调函数入参不一定时该值,        虽然类型是一样的)*/     if ((*p_test_s16) % 2 != 0)       {         return -1;     }     return 0; // 0表示自定义校验成功,其他表示失败 } cotParamInfo_t sg_ParamTable[] = {     COT_PARAM_ITEM_BIND(1, g_test_1, COT_PARAM_INT16, COT_PARAM_ATTR_WR),     COT_PARAM_ITEM_BIND(2, g_test_2, COT_PARAM_UINT16, COT_PARAM_ATTR_WR, 20),     COT_PARAM_ITEM_BIND(3, g_test_float, COT_PARAM_FLOAT, COT_PARAM_ATTR_READ, 3.15, -10, 10),     COT_PARAM_ITEM_BIND(6, g_test_s8, COT_PARAM_INT8, COT_PARAM_ATTR_WR, 10, -10, 15),     COT_PARAM_ITEM_BIND(7, g_test_s16, COT_PARAM_INT16, COT_PARAM_ATTR_WR, 100, -100, 3000, CheckTestS16), // 设置自定义校验     COT_PARAM_ITEM_BIND(8, g_test_s32, COT_PARAM_INT32, COT_PARAM_ATTR_WR, 1000, -900, 10000), } ``` ## 代码链接 [轻量级参数管理框架(C语言)](https://gitee.com/cot_package/cot_param)

  • 2024-02-07
  • 回复了主题帖: 使用C语言构建一个多任务协程调度系统

    辛昕 发表于 2024-2-6 16:02 哟,这个有点牛逼哦 不过,其实我觉得,photothread挺好的 protothreads确实不错,不过属于是非对称协程了,而且应该没办法临时创建任务吧

  • 2024-02-05
  • 发表了主题帖: 使用C语言构建一个多任务协程调度系统

    使用了标准库头文件 ``中的 `setjmp` 和 `longjmp`两个函数,构建了一个简单的查询式协作多任务系统,支持**独立栈**和**共享栈**两种任务。 > 1. 其中涉及到获取和设置栈的地址操作,因此还需要根据不同平台提供获取和设置栈的地址操作(一般是汇编语言,因为涉及到寄存器) > 2. 该调度系统仅运行在一个实际的线程中,因此本质上属于协程 > 3. 独立栈任务都有自己独立的运行栈空间,互不干扰;共享栈任务共用一个运行栈空间。 ## 特点 - 无任务优先级抢占的功能。 - 任务切换的时机完全取决于正在运行的任务,体现**协作**。 - 支持**独立栈**和**共享栈**两种任务,根据不同的应用场景决定。 - 查询式的调度方式,当前任务切换时,查询下个任务是否需要执行。 - 移植性强,只需要修改设置栈和获取当前栈地址的宏即可。 - 相对于**时间片论法**的任务调度来说,查询式协作多任务系统有以下特点:     > - 无需使用定时器做为任务调度     > - 每个任务都可以使用`while`循环,用于执行任务并保持程序的运行,程序结构清晰     > - 每个任务都可以随时阻塞等待,甚至可以在嵌套的子函数中阻塞等待     > - 通过阻塞等待,无需使用状态机等较为复杂的方式来优化缩减每个任务的执行时长 - 相对于**RTOS操作系统**来说,查询式协作多任务系统有以下特点:     > - 没有任务优先级抢占式的功能,因此临界资源(中断除外)和优先级反转的问题也不存在     > - 允许用户或应用程序根据需要自由地切换到下一个就绪任务     > - 通过自主调度和管理任务,查询式协作多任务系统可以提高工作效率     > - 没有操作系统的复杂 ## 功能设计 运行栈空间:程序运行中发生函数调用等情况需要使用的栈内存空间 ### 独立栈任务(有栈任务) 每个独立栈任务**都拥有**自己独立的运行栈空间,可以随时随地阻塞等待,保存上下文后切换到下一个任务执行 > 独立栈任务在切换下一个任务时,不会操作运行栈,只对上下文切换 ### 共享栈任务(无栈任务) 每个共享栈任务**都没有**自己独立的运行栈空间,虽然也能阻塞等待,但是仅限于在任务入口函数中使用,禁止在任务的子函数(嵌套函数)中阻塞等待;并且在该任务入口函数中不建议定义相关变量。 > - 每个任务有自己的独立备份栈(用来备份运行栈的栈顶部分数据);运行栈通常比备份栈要大很多,否则任务函数无法正常运行多级嵌套的函数 > - 共享栈任务在切换下一个任务时会将当前运行栈(共享栈)提前设置好的备份栈大小(宏配置)拷贝到内存备份起来,等下次即将执行时再从内存中拷贝到运行栈(共享栈)进行恢复 > - 通过修改加大备份栈大小(宏配置)的值,可以在共享栈任务入口函数定义变量,这样可以避免这些变量的值没有备份导致丢失,或者通过 static 定义局部变量 > - 该类型任务适合于轻量的任务处理,一般都是调用封装好的函数即可 注:这里的共享栈任务和常规的实现有一些差异,常规的实现是使用堆申请内存保存栈的数据,用多少申请多少进行保存,而这里的实现仅仅保存了一部分数据。 ### 任务创建 1. 在调度系统启动前,至少要先创建一个任务,否则直接退出 2. 可以在任务中创建新的任务,不管是独立栈任务还是共享栈任务     > - 独立栈任务中可以创建新的独立栈任务和共享栈任务     > - 共享栈任务中同样可以创建新的独立栈任务和共享栈任务,而且在创建共享栈任务时可以使用同一个共享栈 3. 独立栈任务和共享栈任务一共可以创建最多32个任务(需要修改宏配置) ### 任务销毁 - 没有提供该功能接口函数,任务入口函数主动退出则自动将任务销毁。 - 可以通过等待任务退出接口函数在其他任务中等待该任务退出。 ### 任务阻塞 当前任务阻塞提供两种方式: - 时间阻塞:需要阻塞多长时间,等时间满足后才会继续执行 - 事件阻塞:通过事件阻塞,只有事件触发后才会继续执行 ## 使用说明 ### 任务创建/退出 对于创建独立栈任务还是共享栈任务的示例代码: ```c uint8_t g_task1Stack[1024 * 2]; uint8_t g_task2Stack[1024 * 2]; uint8_t g_task3Stack[1024 * 2]; uint8_t g_sharedStack[1024 * 2]; // 执行完成就退出的任务 void taskfunc3(int arg) {     ...     cotOs_Wait(1000);     ...     cotOs_Wait(1000); } void taskfunc1(int arg) {    /* 不管taskfunc1是独立栈任务还是共享栈任务,都支持创建子任务 */    cotOs_CreatTask(taskfunc3, COT_OS_UNIQUE_STACK, g_task3Stack, sizeof(g_task3Stack), 0);  // 创建独立栈任务    cotOs_CreatTask(taskfunc3, COT_OS_SHARED_STACK, g_sharedStack, sizeof(g_sharedStack), 0); // 创建共享栈任务     while (1)     {         ...         cotOs_Wait(1000);     } } void taskfunc2(int arg) {     while (1)     {         ...         cotOs_Wait(10);     } } int main(void) {     cotOs_Init(GetTimerMs); #if 0     /* 创建独立栈任务 */     cotOs_CreatTask(taskfunc1, COT_OS_UNIQUE_STACK, g_task1Stack, sizeof(g_task1Stack), 0);     cotOs_CreatTask(taskfunc2, COT_OS_UNIQUE_STACK, g_task2Stack, sizeof(g_task2Stack), 0); #else     /* 创建共享栈任务 */     cotOs_CreatTask(taskfunc1, COT_OS_SHARED_STACK, g_sharedStack, sizeof(g_sharedStack), 0);     cotOs_CreatTask(taskfunc2, COT_OS_SHARED_STACK, g_sharedStack, sizeof(g_sharedStack), 0); #endif     cotOs_Start(); } ``` ### 任务限制 对于创建独立栈任务还是共享栈任务,共享栈任务有限制要求,禁止在任务入口函数的嵌套函数中阻塞 ```c uint8_t g_task1Stack[1024 * 2]; uint8_t g_sharedStack[1024 * 2]; void func1_1(void) {     ...     cotOs_Wait(1000);     ...     cotOs_Wait(1000); } /* 独立栈任务 */ void taskfunc1(int arg) {     int arr[10];   // 可以直接定义变量使用     while (1)     {         func1_1();  // 可以在嵌套函数中使用阻塞等待         ...         cotOs_Wait(1000);     } } void func2_1(void) {     ... } /* 共享栈任务 */ void taskfunc2(int arg) {     static int arr[10];  // 建议使用static定义任务内变量或者不定义变量     while (1)     {         func2_1();  // 禁止在嵌套函数中使用阻塞等待         ...         cotOs_Wait(10);     } } int main(void) {     cotOs_Init(GetTimerMs);     /* 创建独立栈任务 */     cotOs_CreatTask(taskfunc1, COT_OS_UNIQUE_STACK, g_task1Stack, sizeof(g_task1Stack), 0);     /* 创建共享栈任务 */     cotOs_CreatTask(taskfunc2, COT_OS_SHARED_STACK, g_sharedStack, sizeof(g_sharedStack), 0);     cotOs_Start(); } ``` ### 任务阻塞/退出 通过时间和事件的方式阻塞 ```c uint8_t g_task1Stack[1024 * 2]; uint8_t g_task2Stack[1024 * 2]; uint8_t g_task3Stack[1024 * 2]; uint8_t g_sharedStack[1024 * 2]; CotOSCondition_t g_eventCv; // 执行完成就退出的任务 void taskfunc3(int arg) {     ...     cotOs_ConditionWait(&g_eventCv);     ... } void taskfunc1(int arg) {    cotOsTask_t task = cotOs_CreatTask(taskfunc3, COT_OS_UNIQUE_STACK, g_task3Stack, sizeof(g_task3Stack), 0);     while (1)     {         ...         cotOs_Wait(1000);         if (...)         {             // 等待 taskfunc3 任务运行结束后才退出 taskfunc1             cotOs_Join(task);             break;         }     } } void taskfunc2(int arg) {     while (1)     {         ...         cotOs_Wait(10);         if (...)         {             cotOs_ConditionNotify(&g_eventCv);  // 通知 taskfunc3 继续执行         }     } } int main(void) {     cotOs_Init(GetTimerMs);     cotOs_CreatTask(taskfunc1, COT_OS_SHARED_STACK, g_sharedStack, sizeof(g_sharedStack), 0);     cotOs_CreatTask(taskfunc2, COT_OS_SHARED_STACK, g_sharedStack, sizeof(g_sharedStack), 0);     cotOs_Start(); } ``` ### 不同栈类型任务应用场景 - 独立栈任务(有栈任务)    - 重量级任务: 提供更多的控制,适用于需要更精确地管理任务状态的情况和执行计算密集型任务的场景    - 更可预测的内存使用: 在创建时分配栈空间,可以更好地控制内存使用,适用于需要更可预测内存行为的场景    - 递归调用: 更容易处理递归调用,因为每个任务都有独立的栈空间 - 共享栈任务(无栈任务)    - 轻量级任务: 通常更轻量,适用于大量小任务的场景。    - 内存效率: 适用于内存受限的环境,因为不需要为每个任务分配各自的栈空间(备份栈除外)。 ## 代码链接 [查询协作式多任务系统](https://gitee.com/cot_package/cot_os)

最近访客

< 1/1 >

统计信息

已有4人来访过

  • 芯积分:166
  • 好友:--
  • 主题:5
  • 回复:4

留言

你需要登录后才可以留言 登录 | 注册


现在还没有留言