[RA4 & RA6] 使用瑞萨移植并验证软件算法-24 通用队列管理篇

[复制链接]
59|1
暮雨之仁 发表于 2026-9-1 19:20 | 显示全部楼层 |阅读模式
移植, 瑞萨, , , ,
        之前分享的环形缓冲区是面向字节流的通信专用工具,擅长处理串口 / CAN 的连续字节数据。但在多任务通信、事件驱动、指令缓存等场景中,我们往往需要存储结构体、整型、事件包等不同类型的定长数据。今天带来的通用队列管理采用元素级抽象设计,支持任意数据类型的入队出队,纯静态内存预分配,标准先进先出逻辑,一套代码适配所有队列场景。
整体架构        通用队列采用「控制块 + 数据存储区」的分离设计,控制块管理队列状态与指针,数据存储区按字节存储任意类型的元素,通过 memcpy 实现元素的原子读写,保证不同类型数据的一致性。
878626a96b283cb041.png
核心特性
  • 任意数据类型:支持字节、整型、结构体等任意定长元素,元素大小可配置
  • 纯静态内存:无需 malloc/free,预分配缓冲区,零内存碎片,适合嵌入式
  • 完整操作集:入队、出队、判空、判满、获取队列长度、清空队列
  • 多队列独立:可同时创建多个不同类型、不同深度的队列,互不干扰
  • 天然线程安全:单生产者 + 单消费者场景无需加锁,中断写 + 主循环读安全
  • 资源开销极低:单个队列控制块仅 16 字节 RAM,代码体积约 1KB Flash

一、函数封装实现头文件(queue.h)
  1. #ifndef QUEUE_H
  2. #define QUEUE_H

  3. #include <stdint.h>
  4. #include <stdbool.h>
  5. #include <stddef.h>

  6. // 队列控制块结构体
  7. typedef struct {
  8.     uint8_t  *buf;          // 数据存储区首地址
  9.     uint16_t elem_size;     // 单个元素大小(字节)
  10.     uint16_t max_elem;      // 最大元素数量
  11.     uint16_t head;          // 头指针(出队位置)
  12.     uint16_t tail;          // 尾指针(入队位置)
  13.     uint16_t count;         // 当前元素数量
  14. } queue_t;

  15. /**
  16. * [url=/u/brief]@brief[/url]  初始化队列
  17. * @param  q        队列控制块指针
  18. * @param  buf      预分配的数据存储区地址
  19. * @param  elem_size 单个元素的字节大小
  20. * @param  max_elem 最大可容纳元素数量
  21. * [url=/u/return]@return[/url] true成功,false参数非法
  22. */
  23. bool queue_init(queue_t *q, void *buf, uint16_t elem_size, uint16_t max_elem);

  24. /**
  25. * @brief  元素入队
  26. * @param  q        队列控制块指针
  27. * @param  elem     待入队元素指针
  28. * @return true成功,false队列已满
  29. */
  30. bool queue_enqueue(queue_t *q, const void *elem);

  31. /**
  32. * @brief  元素出队
  33. * @param  q        队列控制块指针
  34. * @param  elem_out 接收出队元素的缓冲区指针
  35. * @return true成功,false队列为空
  36. */
  37. bool queue_dequeue(queue_t *q, void *elem_out);

  38. /**
  39. * @brief  查看队头元素(不取出)
  40. * @param  q        队列控制块指针
  41. * @param  elem_out 接收队头元素的缓冲区指针
  42. * @return true成功,false队列为空
  43. */
  44. bool queue_peek(const queue_t *q, void *elem_out);

  45. /**
  46. * @brief  判断队列是否为空
  47. */
  48. bool queue_is_empty(const queue_t *q);

  49. /**
  50. * @brief  判断队列是否已满
  51. */
  52. bool queue_is_full(const queue_t *q);

  53. /**
  54. * @brief  获取当前队列元素数量
  55. */
  56. uint16_t queue_get_count(const queue_t *q);

  57. /**
  58. * @brief  清空队列
  59. */
  60. void queue_clear(queue_t *q);

  61. #endif
实现文件(queue.c)
  1. #include "queue.h"
  2. #include <string.h>

  3. bool queue_init(queue_t *q, void *buf, uint16_t elem_size, uint16_t max_elem)
  4. {
  5.     if (q == NULL || buf == NULL || elem_size == 0 || max_elem == 0) {
  6.         return false;
  7.     }

  8.     q->buf = (uint8_t *)buf;
  9.     q->elem_size = elem_size;
  10.     q->max_elem = max_elem;
  11.     q->head = 0;
  12.     q->tail = 0;
  13.     q->count = 0;
  14.     return true;
  15. }

  16. bool queue_enqueue(queue_t *q, const void *elem)
  17. {
  18.     if (q == NULL || elem == NULL) return false;
  19.     if (q->count >= q->max_elem) return false; // 队列满

  20.     // 计算尾指针位置,复制元素
  21.     uint8_t *dst = q->buf + q->tail * q->elem_size;
  22.     memcpy(dst, elem, q->elem_size);

  23.     // 尾指针循环前进
  24.     q->tail = (q->tail + 1) % q->max_elem;
  25.     q->count++;
  26.     return true;
  27. }

  28. bool queue_dequeue(queue_t *q, void *elem_out)
  29. {
  30.     if (q == NULL || elem_out == NULL) return false;
  31.     if (q->count == 0) return false; // 队列空

  32.     // 计算头指针位置,复制元素
  33.     const uint8_t *src = q->buf + q->head * q->elem_size;
  34.     memcpy(elem_out, src, q->elem_size);

  35.     // 头指针循环前进
  36.     q->head = (q->head + 1) % q->max_elem;
  37.     q->count--;
  38.     return true;
  39. }

  40. bool queue_peek(const queue_t *q, void *elem_out)
  41. {
  42.     if (q == NULL || elem_out == NULL) return false;
  43.     if (q->count == 0) return false;

  44.     const uint8_t *src = q->buf + q->head * q->elem_size;
  45.     memcpy(elem_out, src, q->elem_size);
  46.     return true;
  47. }

  48. bool queue_is_empty(const queue_t *q)
  49. {
  50.     return (q == NULL) ? true : (q->count == 0);
  51. }

  52. bool queue_is_full(const queue_t *q)
  53. {
  54.     return (q == NULL) ? true : (q->count >= q->max_elem);
  55. }

  56. uint16_t queue_get_count(const queue_t *q)
  57. {
  58.     return (q == NULL) ? 0 : q->count;
  59. }

  60. void queue_clear(queue_t *q)
  61. {
  62.     if (q == NULL) return;
  63.     q->head = 0;
  64.     q->tail = 0;
  65.     q->count = 0;
  66. }


二、移植到瑞萨 e²s IDE 的方法
  • 将queue.c和queue.h复制到工程src/utils/目录;
  • 在 e²s 工程上右键 → Add Files,将queue.c加入编译路径;
  • 根据业务需求定义对应类型的存储缓冲区与队列控制块;
  • 在生产者(中断、任务)中调用queue_enqueue放入数据;
  • 在消费者(主循环、任务)中轮询判空,调用queue_dequeue取出数据处理。


三、使用方法
  • 定义存储区与控制块:声明一个对应类型的数组作为数据存储区,同时定义一个queue_t控制块;
  1. // 示例:定义一个最多存8个事件结构体的队列
  2. typedef struct {
  3. uint8_t event_id;
  4. uint16_t param;
  5. } event_t;

  6. event_t event_buf[8]; // 数据存储区
  7. queue_t event_queue; // 队列控制块

  • 初始化队列:调用queue_init,传入控制块、存储区地址、单个元素大小、最大元素数;

  1. queue_init(&event_queue, event_buf, sizeof(event_t), 8);
  • 生产者入队:在中断或任务中,构造好元素后调用queue_enqueue入队;
  • 消费者出队:在主循环或任务中,调用queue_dequeue取出元素处理;
  • 状态查询:可随时调用queue_is_empty/queue_get_count判断是否有数据待处理。


四、调用代码与实测验证测试场景设计覆盖数值型队列、结构体队列、边界条件、清空操作四大场景,验证通用性与鲁棒性:
  • 基础数值队列:uint16_t 类型,入队 5 个元素,分批出队
  • 满队列边界:队列深度 4,入队 5 个,验证第 5 个入队失败
  • 结构体队列:自定义事件结构体,验证复杂类型存取正确性
  • 清空与判空:入队后清空,验证状态复位
验证代码(基于瑞萨 RA6M5 + e²s IDE)
  1. #include "hal_data.h"
  2. #include "queue.h"
  3. #include <stdio.h>
  4. #include <string.h>

  5. void uart_print(const char *str) {
  6.     R_SCI_UART_Write(&g_uart0_ctrl, (uint8_t *)str, strlen(str));
  7. }

  8. // 测试用事件结构体
  9. typedef struct {
  10.     uint8_t  id;
  11.     uint16_t data;
  12. } test_event_t;

  13. void queue_test(void)
  14. {
  15.     char buf[128];
  16.     uint16_t val;
  17.     test_event_t evt;

  18.     uart_print("=== 通用队列功能验证 ===\r\n\r\n");

  19.     // ========== 测试1:基础数值队列 ==========
  20.     uart_print("--- 测试1:uint16数值队列 ---\r\n");
  21.     uint16_t num_buf[8];
  22.     queue_t num_q;
  23.     queue_init(&num_q, num_buf, sizeof(uint16_t), 8);

  24.     // 入队5个元素
  25.     for (int i = 1; i <= 5; i++) {
  26.         uint16_t x = i * 10;
  27.         queue_enqueue(&num_q, &x);
  28.     }
  29.     snprintf(buf, sizeof(buf), "入队5个,当前队列长度:%d\r\n", queue_get_count(&num_q));
  30.     uart_print(buf);

  31.     // 分2次出队
  32.     uart_print("出队元素:");
  33.     for (int i = 0; i < 3; i++) {
  34.         queue_dequeue(&num_q, &val);
  35.         snprintf(buf, sizeof(buf), "%d ", val);
  36.         uart_print(buf);
  37.     }
  38.     snprintf(buf, sizeof(buf), "\r\n剩余队列长度:%d\r\n\r\n", queue_get_count(&num_q));
  39.     uart_print(buf);

  40.     // ========== 测试2:满队列边界 ==========
  41.     uart_print("--- 测试2:满队列边界测试(深度4) ---\r\n");
  42.     uint16_t small_buf[4];
  43.     queue_t small_q;
  44.     queue_init(&small_q, small_buf, sizeof(uint16_t), 4);

  45.     bool ret;
  46.     for (int i = 0; i < 5; i++) {
  47.         uint16_t x = i;
  48.         ret = queue_enqueue(&small_q, &x);
  49.         snprintf(buf, sizeof(buf), "入队%d:%s\r\n", i, ret ? "成功" : "失败");
  50.         uart_print(buf);
  51.     }
  52.     uart_print("\r\n");

  53.     // ========== 测试3:结构体队列 ==========
  54.     uart_print("--- 测试3:事件结构体队列 ---\r\n");
  55.     test_event_t evt_buf[4];
  56.     queue_t evt_q;
  57.     queue_init(&evt_q, evt_buf, sizeof(test_event_t), 4);

  58.     test_event_t e1 = {1, 100};
  59.     test_event_t e2 = {2, 200};
  60.     test_event_t e3 = {3, 300};
  61.     queue_enqueue(&evt_q, &e1);
  62.     queue_enqueue(&evt_q, &e2);
  63.     queue_enqueue(&evt_q, &e3);

  64.     uart_print("出队事件:\r\n");
  65.     while (!queue_is_empty(&evt_q)) {
  66.         queue_dequeue(&evt_q, &evt);
  67.         snprintf(buf, sizeof(buf), "  ID:%d  Data:%d\r\n", evt.id, evt.data);
  68.         uart_print(buf);
  69.     }
  70.     uart_print("\r\n");

  71.     // ========== 测试4:清空与判空 ==========
  72.     uart_print("--- 测试4:清空与判空 ---\r\n");
  73.     queue_enqueue(&num_q, &val);
  74.     snprintf(buf, sizeof(buf), "入队1个后,是否为空:%s\r\n", queue_is_empty(&num_q) ? "是" : "否");
  75.     uart_print(buf);
  76.     queue_clear(&num_q);
  77.     snprintf(buf, sizeof(buf), "清空后,是否为空:%s,长度:%d\r\n",
  78.              queue_is_empty(&num_q) ? "是" : "否", queue_get_count(&num_q));
  79.     uart_print(buf);
  80. }

  81. void hal_entry(void) {
  82.     R_SCI_UART_Open(&g_uart0_ctrl, &g_uart0_cfg);
  83.     queue_test();
  84.     while(1);
  85. }

五、结果对比
    我们通过串口助手验证数据与其是否相符:
856976a96b48ea2d38.png
六、注意事项
  • 线程安全说明
    • 单生产者 + 单消费者场景(如中断入队、主循环出队)是天然线程安全的,无需加锁;
    • 多生产者或多消费者场景,需在操作队列前关中断 / 加互斥锁,防止指针错乱。
  • 元素类型约束
    • 单个队列的元素大小固定,不支持变长元素;如需变长可存储指针 + 长度,或使用环形缓冲区;
    • 元素中若包含指针,入队仅复制指针值,不会复制指针指向的内容,需注意生命周期。
  • 缓冲区对齐
    • 若元素为 32 位整型、结构体等,建议存储区按 4 字节对齐,避免非对齐访问导致的性能下降或 HardFault。
  • 与环形缓冲区的区别
    • 通用队列:面向定长元素,适合事件、指令、结构体等结构化数据,使用简单;
    • 环形缓冲区:面向字节流,适合串口 / CAN 等连续数据流,支持批量读写、覆盖模式。
  • 适用场景
    • 事件驱动架构的事件队列、指令队列;
    • 多任务间的数据传递、消息邮箱;
    • 传感器数据缓存、按键事件缓存等。

闪烁黎明 发表于 2026-9-2 17:27 | 显示全部楼层
注意检查buf指针是否为空,初始化时确保elem_size和max_elem正确设置,避免溢出。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

33

主题

68

帖子

0

粉丝
快速回复 在线客服 返回列表 返回顶部
0