本方案面向 STM32F103C8T6:串口接收上位机的 8 字节命令,BQ27427 提供电池数据,三位 GPIO 控制放大器档位,可选 EEPROM 保存上次设置。重点是把任务、协议、外设和验收条件连接起来;原记录中的代码是设计骨架,不能据其中的勾选项认定驱动、硬件或整机已经完成。
本文给出可独立验证的组帧代码与 FreeRTOS 通信层示例。BQ27427 初始化、实际增益真值表、EEPROM 型号和板级 DMA 实现仍需由具体硬件工程提供,不把未知部分补成能运行的假固件。
硬件资源与 RAM 预算
跳转到“硬件资源与 RAM 预算”STM32F103C8 的额定资源是最高 72 MHz、64 KiB Flash、20 KiB SRAM。不要把部分板卡上碰巧可访问的额外 Flash 当成 C8 的产品保证。ST DS5319
原方案把原生 xTaskCreate() 的栈深度写成了字节。Cortex-M3 端口的 StackType_t 为 32 位,原配置实际为:
| 项目 | 栈深度(word) | 栈存储(byte) | 是否必需 |
|---|---|---|---|
| UART 任务 | 512 | 2048 | 本方案使用 |
| Battery 任务 | 256 | 1024 | 本方案使用 |
| Gain 任务 | 256 | 1024 | 本方案使用 |
| Idle 任务 | 128 | 512 | 内核必需 |
| Timer 服务任务 | 256 | 1024 | 仅启用软件定时器时 |
| 合计 | 1408 | 5632 | 含可选 Timer |
因此 configTOTAL_HEAP_SIZE = 4096 连这组栈都装不下,更不用说 TCB、队列和分配器元数据;25600 字节堆又超过了整颗芯片的 20 KiB SRAM。使用 heap_4.c 时,任务栈和动态队列通常已经包含在其堆数组内,计算物理 RAM 时不能把“整个 heap”和“heap 中的栈”再次相加。
可把 8 KiB FreeRTOS 堆作为测量起点,关闭本方案不需要的软件定时器后,四个任务栈占 4608 字节。余下空间仍须容纳四个 TCB、队列控制块、消息存储、互斥量及分配开销;最终以真实端口、编译选项和对象的 sizeof、链接 map、最低剩余堆与栈高水位为准,不能预先保证 8 KiB 一定足够。FreeRTOS tasks.c、queue.c 与内存管理实现
本页使用 FreeRTOS V11.1.0、ARM_CM3、5 个原生优先级、开启 trace 和静态分配支持、关闭 Timer 的编译探针,得到 sizeof(StaticTask_t)=92、sizeof(StaticQueue_t)=80;下文 UartEvent 为 16 字节,CmdMessage 为 3 字节。因此 32 项通信队列的纯消息区为 512 字节,两条 5 项命令队列合计 30 字节;控制块、对齐和分配开销另算。这些数字是该配置的对象尺寸,不是完整固件 RAM 测量。
物理 RAM 的完整式子还要包括 .data/.bss、HAL 句柄、DMA/环形缓冲、主栈 MSP、链接脚本里的 C 库堆等。原稿的“约 25 KB Flash、11 KB RAM、资源充足”没有完整构建测量支持;这里保留预算方法,不把估值列为测试结果。任务数和队列数也受内存、优先级以及处理能力限制。
三个任务与消息所有权
跳转到“三个任务与消息所有权”| 任务 | 原生优先级 | 工作方式 | 唯一职责 |
|---|---|---|---|
| UART | 2 | 等待统一通信事件 | 组帧、路由命令、提交全部协议 TX;其他任务不能直接发协议串口 |
| Battery | 1 | 1 s 采样截止时间与命令共同驱动 | 管理电池缓存、有效性及采样时间,响应电量查询 |
| Gain | 1 | 等待增益命令 | 管理当前档位与可选持久化状态 |
| Idle | 0 | 内核管理 | 空闲处理;钩子不得阻塞 |
这里使用 原生 FreeRTOS API,优先级 0~4 足以表达本例。若工程选择 CMSIS-RTOS2,保留其包装层需要的优先级映射和配置;不能照搬原生的 configMAX_PRIORITIES = 5,同时仍向包装层传入其较大的优先级枚举。CMSIS-RTOS2 的 stack_size 单位与原生栈深度也应分别核对。
flowchart LR RX[USART RX 中断或 DMA 接收] -->|字节事件| EVT[通信事件队列] EVT --> UART[UART 任务] UART -->|电量命令| BQ[Battery 命令队列] UART -->|增益命令| GQ[Gain 命令队列] BQ --> BAT[Battery 任务] GQ --> GAIN[Gain 任务] BAT -->|响应事件| EVT GAIN -->|响应事件| EVT BAT --> I2C[I2C 互斥量与有界事务] GAIN -->|可选 EEPROM| I2C UART --> TX[复制到 TX 缓冲后启动 DMA]不能让 Battery 和 Gain 从同一个命令队列分别取消息、发现“不属于自己”就忽略:队列取走的是一个元素,不是向全部消费者广播。两个工作任务各有专用命令队列。UART 的等待点同时接收 RX 与响应事件,所以设备主动产生的响应能直接唤醒它,不需要再等上位机发送下一个字节。
建议从命令队列各 5 项、通信事件队列 32 项开始测量,队列项大小始终使用 sizeof(实际结构),不要把原稿出现的 3、8、12 字节混为同一种消息。若所有协议发送都归 UART 管理,不需要额外 UART 互斥量;I2C 由 Battery 与 EEPROM 共享时才需要 I2C 互斥量。增益命令队列已经能唤醒 Gain,另加只表示同一事件的二值信号量没有必要。
8 字节协议与完整组帧器
跳转到“8 字节协议与完整组帧器”原协议字节排列固定如下:
| 索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| 含义 | AA 帧头 | CMD | DATA1 | DATA2 | 保留 00 | 保留 00 | SUM | 55 帧尾 |
SUM = (byte[1] + ... + byte[5]) mod 256,不包含帧头与帧尾。这只是错误检测能力有限的 8 位加和,不能提供认证或强纠错。解析器先验证封装,再由命令层检查命令码、参数和权限;未知命令不因校验通过就执行。
| 命令/响应 | 数据定义与仍需确认的事项 |
|---|---|
01 查询电量 → 81 | DATA1 为 SOC 百分比;原稿把 DATA2 定义为充电状态,但必须补齐该状态的判定依据与失效表示 |
02 设置增益 → 82 | DATA1 为目标/实际增益;原稿 DATA2=00 表示成功、01 表示参数错误;持久化失败等尚未定义 |
03 查询增益 → 83 | DATA1 为当前有效增益,原稿 DATA2=00 |
F0 心跳 | 原样的零数据心跳请求与响应;无须为此创建软件定时器任务 |
保留原记录的可核对帧:
AA F0 00 00 00 00 F0 55 心跳AA 01 00 00 00 00 01 55 电量查询AA 02 0A 00 00 00 0C 55 请求 10 倍增益AA 82 0A 00 00 00 8C 55 成功设置 10 倍的原协议响应下面的纯 C 代码接收任意分段到达的字节,支持连续帧、垃圾前缀、错误帧滑动重同步与帧间隔超时。保留字段严格为 0 是本页选定的协议约束;若日后使用这些字段,必须同步修改版本约定与校验。
#include <stdbool.h>#include <stdint.h>#include <stddef.h>#include <string.h>
typedef struct { uint8_t cmd, data1, data2; } CmdMessage;typedef struct { uint8_t bytes[8]; uint8_t used; bool has_stamp; uint32_t last_stamp;} FrameParser;
void Protocol_Reset(FrameParser *parser){ memset(parser, 0, sizeof *parser);}
static uint8_t protocol_sum(const uint8_t *frame){ unsigned sum = 0u; for (unsigned i = 1u; i <= 5u; ++i) sum += frame[i]; return (uint8_t)sum;}
bool Protocol_Validate(const uint8_t frame[8]){ return frame[0] == 0xAAu && frame[7] == 0x55u && frame[4] == 0u && frame[5] == 0u && frame[6] == protocol_sum(frame);}
void Protocol_Build(CmdMessage message, uint8_t frame[8]){ frame[0] = 0xAAu; frame[1] = message.cmd; frame[2] = message.data1; frame[3] = message.data2; frame[4] = 0u; frame[5] = 0u; frame[6] = protocol_sum(frame); frame[7] = 0x55u;}
/* stamp 与 max_gap 使用相同单位;0 < max_gap < 2^31。 */bool Protocol_Feed(FrameParser *parser, uint8_t byte, uint32_t stamp, uint32_t max_gap, CmdMessage *out){ if (parser->has_stamp && (uint32_t)(stamp - parser->last_stamp) > max_gap) parser->used = 0u; parser->last_stamp = stamp; parser->has_stamp = true; if (parser->used == 0u && byte != 0xAAu) return false; parser->bytes[parser->used++] = byte; if (parser->used < 8u) return false; if (Protocol_Validate(parser->bytes)) { out->cmd = parser->bytes[1]; out->data1 = parser->bytes[2]; out->data2 = parser->bytes[3]; parser->used = 0u; return true; } unsigned next = 1u; while (next < 8u && parser->bytes[next] != 0xAAu) ++next; parser->used = (uint8_t)(8u - next); memmove(parser->bytes, parser->bytes + next, parser->used); return false;}调用者提供非空、尺寸正确的缓冲与已初始化解析器。时间戳在接收时记录,不能用任务稍后消费字节的时间替代;一次静默时间不得达到完整 32 位时基周期。max_gap 是协议参数:本文通信示例选择 20 ms,仅作初始设计,需按主机发送方式确认。串口中断接收一个字节时,队列就只能按一个字节或明确的事件结构处理,不能取一次队列后把尚未填满的 8 字节数组当完整帧。
能被 RX 和响应共同唤醒的通信层
跳转到“能被 RX 和响应共同唤醒的通信层”下面与上面的纯 C 块一起编译,面向 FreeRTOS 原生 API、至少 32 位的 tick 类型、1 kHz tick,接收时间戳保存 tick 的低 32 位;F103 的配置使用 32 位 tick。Board_TxTryEnqueue() 必须在返回前复制 8 字节到它自己管理的 TX 缓冲;实际 DMA 发送完成前保持缓冲有效。它不能保留本函数的栈地址,也不能阻塞等待串口发完。示例不提供该硬件实现。
#include "FreeRTOS.h"#include "task.h"#include "queue.h"
_Static_assert(sizeof(TickType_t) >= sizeof(uint32_t), "at least 32-bit tick required");_Static_assert(configTICK_RATE_HZ == 1000, "this example uses 1 ms ticks");
typedef enum { EVENT_RX, EVENT_RESPONSE } UartEventKind;typedef struct { UartEventKind kind; union { struct { uint32_t stamp, sequence; uint8_t byte; } rx; CmdMessage response; } payload;} UartEvent;typedef struct { FrameParser parser; uint32_t last_sequence; bool has_sequence;} UartState;
QueueHandle_t uartEvents, batteryCommands, gainCommands;static uint32_t rx_sequence; /* 唯一 USART RX ISR 写;每收到一字节都递增。 */static uint32_t rx_dropped; /* 同一 ISR 写,诊断读取另行提供一致性方法。 */
extern bool Board_TxTryEnqueue(const uint8_t frame[8]);extern void Board_RecordCommunicationFault(void);
bool Communication_CreateQueues(void){ uartEvents = xQueueCreate(32, sizeof(UartEvent)); batteryCommands = xQueueCreate(5, sizeof(CmdMessage)); gainCommands = xQueueCreate(5, sizeof(CmdMessage)); if (uartEvents != NULL && batteryCommands != NULL && gainCommands != NULL) return true; if (uartEvents != NULL) vQueueDelete(uartEvents); if (batteryCommands != NULL) vQueueDelete(batteryCommands); if (gainCommands != NULL) vQueueDelete(gainCommands); uartEvents = batteryCommands = gainCommands = NULL; return false;}
/* 仅从允许调用 FromISR API 的 USART RX 中断路径调用。 */void Communication_RxByteFromISR(uint8_t byte){ BaseType_t wake = pdFALSE; UartEvent event = {0}; event.kind = EVENT_RX; event.payload.rx.byte = byte; event.payload.rx.stamp = (uint32_t)xTaskGetTickCountFromISR(); event.payload.rx.sequence = ++rx_sequence; if (xQueueSendFromISR(uartEvents, &event, &wake) != pdPASS) ++rx_dropped; portYIELD_FROM_ISR(wake);}
/* 工作任务检查返回值;满队列不假装响应已交付。 */bool Communication_Respond(CmdMessage response){ UartEvent event = {0}; event.kind = EVENT_RESPONSE; event.payload.response = response; return xQueueSend(uartEvents, &event, pdMS_TO_TICKS(10)) == pdPASS;}
static void communication_tx(CmdMessage response){ uint8_t frame[8]; Protocol_Build(response, frame); if (!Board_TxTryEnqueue(frame)) Board_RecordCommunicationFault();}
bool Communication_ProcessOne(UartState *state, TickType_t wait){ UartEvent event; if (xQueueReceive(uartEvents, &event, wait) != pdPASS) return false; if (event.kind == EVENT_RESPONSE) { communication_tx(event.payload.response); return true; } if (event.kind != EVENT_RX) { Board_RecordCommunicationFault(); return true; } if (state->has_sequence && event.payload.rx.sequence != state->last_sequence + 1u) Protocol_Reset(&state->parser); state->last_sequence = event.payload.rx.sequence; state->has_sequence = true; CmdMessage command; if (!Protocol_Feed(&state->parser, event.payload.rx.byte, event.payload.rx.stamp, 20u, &command)) return true; QueueHandle_t destination = NULL; if (command.cmd == 0x01u) destination = batteryCommands; else if (command.cmd == 0x02u || command.cmd == 0x03u) destination = gainCommands; else if (command.cmd == 0xF0u && command.data1 == 0u && command.data2 == 0u) communication_tx(command); else Board_RecordCommunicationFault(); if (destination != NULL && xQueueSend(destination, &command, 0) != pdPASS) Board_RecordCommunicationFault(); return true;}
void UART_Task(void *argument){ (void)argument; UartState state = {0}; for (;;) (void)Communication_ProcessOne(&state, portMAX_DELAY);}队列创建函数只在冷启动时调用一次,失败后进入工程错误路径,不重复创建而泄漏旧对象。启动顺序为:初始化硬件但保持 RX 事件源未开启 → 创建全部队列、互斥量与任务并检查返回值 → 启动调度器 → 由启动任务准备 RX 缓冲并开启接收。UART 中断入口还须调用所选 HAL/DMA 接收流程、处理硬件 ORE/FE/NE 和重新接收失败;本函数只负责投递已经接到的一个字节。
序号在投递失败时也继续递增,下一个成功事件就能让解析器丢弃不连续的残帧。应用应监控 rx_dropped、命令队列满和 TX 缓冲满;这仍无法保证任意丢字节都被 8 位加和检测到。协议未定义忙、缓存无效和存储失败应答,示例只记内部故障;要对上位机返回新状态码,先达成协议版本约定,不能冒充已有兼容行为。
115200 baud、8N1 每字节约 86.8 μs,10 个字节缓冲只覆盖约 0.87 ms,32 个约 2.78 ms;统一队列中夹有响应时,RX 可用空间更少。每字节一个事件还放大了 RAM 与调度开销。需要持续满速接收时,应测量最坏处理间隔并采用 DMA/环形缓冲加批量通知,定义溢出恢复;将队列从 10 改为 32 不能单独证明不会丢包。
电池采样、缓存与 I2C 驱动
跳转到“电池采样、缓存与 I2C 驱动”Battery 不能先睡 1 s、再仅取一个命令:这样响应平白增加等待,连续查询还会积压。应用循环应等待“下一次采样截止时间或命令先到”,收到命令后可立即使用已验证的缓存;到期则执行一次有上限的采样,并推进下一截止时间。
初始化缓存:valid=false,记录上次成功时间、上次错误next_sample = 当前 tick(启动后尽快第一次采样)循环: 若采样已到期: 在超时内取得 i2cMutex 在总事务时限内读取并校验电池数据,所有退出路径释放锁 成功才更新 snapshot、valid 和 sampled_at 失败记录错误;根据已约定最大 age 判断旧快照能否继续使用 推进 next_sample 到未来,过期采样合并一次 等待 batteryCommands,超时为距 next_sample 的剩余时间 收到合法 01 查询时: 缓存有效且未过期 → 创建 81 响应并检查投递结果 否则 → 走约定的无效/超时策略,不能把初值 0% 当测量结果BQ27427 的 7 位地址为 0x55;部分 STM32 HAL API 要传入左移后的 0xAA。不要把分析仪显示的 7 位地址与含读写位的总线地址混用。SOC 位于 0x1C/0x1D,应按芯片规定的字节顺序组成无符号值并检查 0~100;AverageCurrent() 是有符号电流值。
原文的 is_charging 不能直接由 Flags[CHG] 命名推断:CHG 表示允许快速充电;DSG 是电量算法状态,也不等同于充电器插入检测。若产品要区分“插入、实际充电、充满”,需要明确结合充电器状态、电流符号及阈值的定义。初始化还应核对电池存在、上电状态与电池参数配置流程,不能仅凭 I2C ACK 就声称电量准确。TI BQ27427 技术参考手册
电量芯片支持 100/400 kHz I2C,但寄存器访问和配置更新必须符合对应时序。SCL、SDA 分别经上拉连接到兼容的逻辑电源,不能把两线互连;上拉根据总线电容、速率和灌电流选取,不把 4.7 kΩ 当普适定值。BQ27427 数据引脚的上拉电压范围需按手册核对,本方案的 3.3 V 接口在规定范围内。TI BQ27427 数据手册
若同总线还有 EEPROM,互斥量只保护完整的必要总线事务;不要持锁等待其他任务响应。EEPROM 页写后的器件内部写周期可以采用有期限的 ACK 轮询,具体页边界、写周期和地址宽度以实际器件为准。
增益控制与掉电保存
跳转到“增益控制与掉电保存”三位 GPIO 只能编码最多 8 个档位,不能仅检查 1 <= gain <= 200 就宣称每个整数增益都可实现。原记录展示了前四个示意档位,仍需原理图确认:
| PA2:PA1:PA0 | 原稿示意增益 | 使用条件 |
|---|---|---|
000 | 1 | 核对放大器真值表与上电默认 |
001 | 2 | 核对实际电阻与控制极性 |
010 | 5 | 核对实际电阻与控制极性 |
011 | 10 | 原协议示例使用此增益,但 GPIO 映射仍需实测 |
100~111 | 未给出 | 不编造 20/50/100/200 等映射 |
drv_gain 应只接受已经验证的表项,一次更新同端口三个锁存位,并按放大器要求处理切换瞬态、静音/使能与稳定时间。GPIO 写入成功不等于模拟增益经过测量校准。查询返回当前实际采用的档位,不返回一次失败请求的目标值。
持久化应明确成功的定义:只完成本次硬件切换,还是掉电后也能恢复。若协议把设置成功定义为“已保存”,EEPROM 获取锁失败、写失败或校验失败都不能仍回复 82 ... 00。可使用版本、范围、校验及断电一致性记录;启动读取失败或值不在映射表中时采用经过硬件确认的默认档位并记录原因。避免每次重复设置都写 EEPROM,寿命与掉电中断测试应成为验收项目。原稿“20 倍掉电恢复”用例需等待 20 倍档位确实存在后才适用。
CubeMX、时钟与中断核对表
跳转到“CubeMX、时钟与中断核对表”| 对象 | 本方案的连接与初始选择 | 核对点 |
|---|---|---|
| USART1 | PA9 TX 接 USB 串口 RX;PA10 RX 接对方 TX;115200、8N1 | 3.3 V 逻辑、共地;不是直接连接 RS-232 电平 |
| I2C1 | PB6 SCL、PB7 SDA,先从 100 kHz 调通 | F1 复用开漏和外部上拉;不要套用其他 STM32 系列的数字滤波配置项 |
| 增益 | PA0、PA1、PA2 推挽,按板卡选初值 | 上电先预置输出锁存位,再开启输出;初始化不应使模拟链路意外跃迁 |
| 指示灯 | PC13(仅当板上确有此连接) | 常见板为低有效,但以原理图为准;满足 PC13 的驱动能力限制 |
| 调试 | PA13/PA14 保留 SWD | CubeMX 选择 Serial Wire,保留下载调试通道 |
| 外部时钟 | 8 MHz HSE、PLL ×9 → SYSCLK/HCLK 72 MHz | 芯片供电、Flash 等待周期与晶振实物必须匹配 |
| 总线 | APB1 36 MHz;APB2 72 MHz | I2C1、USART2/3、TIM2 在 APB1;USART1、GPIO、AFIO 在 APB2 |
| 定时器时钟 | APB1 分频不为 1 时,对应定时器通常取 2×PCLK1 | 该配置 TIM2 为 72 MHz;PSC=71、ARR=999 可得到 1 kHz 更新事件 |
| DMA(可选) | USART1 TX:DMA1 Channel 4;RX:Channel 5 | TX 正常模式、RX 可用循环模式;处理半满/全满/空闲事件与读写游标 |
SysTick 通常由 Cortex-M FreeRTOS 端口提供内核 tick。TIM2 可以被选为 HAL 独立时基,这不等于 TIM2 是 FreeRTOS 默认时基;也可使用经过验证的其他配置。保持每一时基只有一个更新来源,避免 SysTick 与 TIM2 都推进 HAL_IncTick()。相关时基说明
STM32F1 实现 4 个优先级位时,常用全抢占优先级分组。若库级阈值设为 5,寄存器格式的 configMAX_SYSCALL_INTERRUPT_PRIORITY 才是 5 << 4 = 0x50。使用 ...FromISR() 的 USART/DMA 中断,其逻辑优先级数值必须 大于或等于 5;0~4 不能调用这些 API。不能拿 CubeMX 中的 5 直接与寄存器值 80 比较,也不能写成必须严格大于 5。PendSV 与内核 tick 通常使用最低优先级 15;HAL 时基应按是否需要在其他 ISR 中计时等约束选择,不照抄一个“越高越好”的数值。中断优先级说明
3.3 V 电源、去耦、VDDA/VSSA、VBAT、BOOT0、NRST 和晶振均按 ST 硬件设计建议与实际板卡处理。晶振负载电容由晶体 CL 与寄生电容决定,不固定为 20 pF;NRST 的内部上拉、外部复位电路及下载器连接也不能用一只固定 10 kΩ 取代完整设计。
原生 FreeRTOS 的建议起点如下,最终仍以实际内核版本生成的配置与编译检查为准:
| 类别 | 本方案选择/解释 |
|---|---|
| 内核调度 | 抢占与时间片启用;1 kHz、32 位 tick;原生 5 个优先级;任务名 16 字节 |
| 内存 | 选择且只编译一种 heap 实现;可从 heap_4 与 8192 byte 起测;所有创建结果检查 |
| 同步 | 仅使用需要的互斥量/通知功能;递归互斥量、计数信号量非必需 |
| 静态分配 | 由 configSUPPORT_STATIC_ALLOCATION 控制,并提供相应存储;configENABLE_BACKWARD_COMPATIBILITY 不是静态分配开关 |
| 软件定时器 | 本架构可关闭;开启后另计服务任务栈与命令队列,优先级依据响应要求,不强制最高 |
| 诊断 | 开启分配失败和栈溢出诊断;调试使用 configASSERT;记录堆余量和栈高水位 |
| 钩子与 API | Idle/Tick 钩子默认不用;若启用不得阻塞;删除、暂停、统计等 INCLUDE 选项按实际调用开启 |
溢出钩子只能检测部分栈破坏情形,不能代替栈预算和最坏路径测试。CubeMX 的 C 库 heap/MSP 配置与 FreeRTOS heap 是不同预算项;生成代码时确认 .ioc、目标器件、工具链、用户区保留、Include 路径和源文件清单,不手工复制一套冲突的异常处理函数。集成与配置说明
文件边界与两轮开发计划
跳转到“文件边界与两轮开发计划”保留原方案的分层结构。下表中的“一对”表示 .h/.c 两个文件,不把目录或 CubeMX 自动文件计作新建业务文件。
| 目录 | 文件/数量 | 职责 |
|---|---|---|
Application/Inc,Src | app_uart_task、app_battery_task、app_gain_task 各一对,另有 app_common.h:7 个 | 任务循环、公共消息和句柄声明 |
Protocol/Inc,Src | protocol.h/c:2 个 | 纯协议校验、构帧、流解析,不直接操作 HAL |
Drivers_Custom/Inc,Src | bsp_uart、bsp_i2c、bsp_gpio、drv_bq27427、drv_gain 各一对:10 个 | 板级传输与器件语义分离 |
Drivers_Custom/Inc,Src | 可选 drv_eeprom.h/c:2 个 | 页写、参数记录与掉电恢复 |
Utilities/Inc,Src | 可选 util_ringbuffer.h/c:2 个 | RX 流缓冲及溢出处理 |
Core | 生成的 main、stm32f1xx_it、FreeRTOSConfig 等 | 增加用户区启动顺序、IRQ 接入与核验配置 |
Drivers、Middlewares | HAL、CMSIS、FreeRTOS 及其端口 | 保持版本和构建来源可追踪 |
按上表基础业务为 19 个文件,启用 EEPROM 为 21 个,再加环形缓冲为 23 个;原稿展开的完整树不是“22 个”。不要为了凑数量创建无职责的空文件。头文件写公开类型、状态码、有效输入、返回值与所有权;源文件包含实际实现。TODO 模板只作为接口草案,不能标记验收完成。
原稿先给出 P1~P12,再细化为 T001~T016,属于两种粒度的计划,不能作为两套已经完成的交付。下表保留对应关系、分工和旧估时供追踪,验收以证据为准:
| 原任务 | 细化任务与原建议分工 | 原估时(人日) | 交付和依赖 |
|---|---|---|---|
| P1 配置 | T001 配置、T002 生成验证:负责人 | 0.5 + 0.5 | .ioc、时钟/优先级记录、最小链接运行;先于全部驱动 |
| P2 UART | T003:A | 1 | 字节/流收发、错误与溢出计数;依赖 T002 |
| P4 的总线部分 | T004 I2C:A | 1 | 有期限读写、ACK/超时与总线恢复;依赖 T002 |
| P5 的 GPIO 部分 | T005:B | 0.5 | 初值、引脚电平和原子位更新;依赖 T002 |
| P4 电量驱动 | T006:B | 2 | 芯片识别、参数、SOC/电流/状态与错误;依赖 T004 |
| P6 EEPROM | T007:A | 1 | 可选参数记录;依赖 T004,型号与页写约束先确定 |
| P5 增益驱动 | T008:B | 0.5 | 已核验真值表与稳定时间;依赖 T005 |
| P3 协议 | T009:B | 2 | 独立纯 C 测试、错误帧与重同步;实串口联调依赖 T003 |
| P7~P9 公共定义 | T010:A | 0.5 | 消息结构、队列容量、对象所有权;依赖 T002,可先于任务实现 |
| P7 UART 任务 | T011:A | 2 | RX/响应共同唤醒、路由与单 TX 所有权;依赖 T003/T009/T010 |
| P8 电池任务 | T012:B | 2 | 周期采样、即时缓存查询、失效与年龄;依赖 T006/T010 |
| P9 增益任务 | T013:B | 2 | 档位、失败反馈与可选保存;依赖 T008/T010,保存还依赖 T007 |
| P10 集成 | T014:A | 1 | 检查对象创建、IRQ 启动顺序、链接内存;依赖 T010~T013 |
| P11 单元测试 | T015:全员 | 2 | 模块开发时同步进行;原计划列在集成后,不能因此拖延模块验证 |
| P12 系统测试 | T016:全员 | 3 | 硬件、故障注入、负载与长稳;依赖集成与模块通过 |
第一轮原估时 P1~P12 分别为 0.5、1、1、1.5、0.5、1、1.5、1、1、1、2、2 人日;细化轮增加了职责拆分和部分工作量。它们是历史估算,不是承诺工期。原甘特图把同一人在同一天同时安排多项全天任务,不能直接得出 12 或 13 天完工;实际排期应先定硬件和协议,再按依赖、人员容量与并行测试重排。
验收与排错记录
跳转到“验收与排错记录”| 范围 | 必须能复现的检查 |
|---|---|
| 协议单元 | 上述四个样例、分段与连续输入、垃圾前缀、错误校验/帧尾、保留位、超时、丢字节后的恢复 |
| 队列集成 | 只发一次请求后停止 RX,响应仍能发出;Battery/Gain 命令不会互相吞掉;满队列有可观测结果 |
| BQ27427 | 连续至少 100 次读取并记录成功/错误、SOC 范围和状态依据;断开、超时和陈旧缓存不能伪装为成功数据 |
| 增益 | 每个有效档位测 GPIO 和实际模拟增益;无效档位不改变当前状态;切换瞬态符合模拟前端要求 |
| EEPROM(若用) | 复位恢复、损坏记录、无器件、写超时、写中断电与重复设置寿命策略 |
| 实时性 | 在目标输入速率与最坏业务负荷下记录 RX 丢失、响应延迟分布、任务栈高水位、最低堆余量 |
| 启动/长稳 | 创建失败与调度器启动失败进入可诊断路径;至少 24 h 运行记录计数、内存和错误,无证据不写“无泄漏” |
| 最终交付 | .ioc、原理图/引脚表、配置版本、源码、链接 map、烧录说明和可复测测试报告 |
时钟初始化失败时先核实 HSE 频率、晶振是否起振、供电及 PLL/Flash 配置;串口无数据时检查交叉 TX/RX、逻辑电平、时钟、IRQ 与重启接收流程;I2C HAL_BUSY/HAL_TIMEOUT 时检查两线是否分别被上拉、7/8 位地址、总线占用与设备供电,不把无限重试当恢复;任务创建失败时记录具体返回值、最低剩余堆与所需栈单位,再检查内核和异常处理接入。
本次对原文全部设计、代码、表格和三个示意图进行了审阅。发布的组帧与通信代码经过主机测试和 Cortex-M3 编译;使用真实 FreeRTOS V11.1.0 的 POSIX 端口验证响应无需下一次 RX 即可唤醒发送。该主机端口使用 64 位 tick 和 64 KiB 测试堆,RX 事件由测试任务注入,TX 由捕获函数代替;它验证通信逻辑,不验证 F103 的 8 KiB 候选堆、真实 ISR 或 DMA 时序。没有生成完整板级固件,也未进行芯片烧录、BQ27427 电池标定或模拟增益测量。