上一篇《CCP 原理与 ECU 在线标定:从 CRO/DTO 到 DAQ 和标定页》已经解释了协议角色、MTA、DAQ 和标定页的工作模型。本文不再铺陈原理,而是回答更工程化的问题:怎样写出一个可移植、边界安全、能真正接到 CAN 驱动和周期任务上的 CCP 从站骨架。
声明:下文是原创的教学用 C99 样例,只实现 CCP 2.1 的一个明确子集,不是完整量产协议栈,也不是从任何私有项目直接复制。文中的 Station Address、CAN ID、虚拟地址和内存布局均为示例值;不包含项目代号、客户信息、真实 ECU 标识、私有 Seed&Key 常数或量产源码。
一、样例范围与非目标
| 能力 | 本样例覆盖 | 边界 |
|---|---|---|
| 会话 | CONNECT、TEST、DISCONNECT、EXCHANGE_ID | 单会话、首个有效 CONNECT 绑定 CAN 通道 |
| 内存访问 | SET_MTA、UPLOAD、SHORT_UPLOAD、DNLOAD、DNLOAD6 | 只访问应用显式注册的虚拟地址白名单 |
| DAQ | GET_DAQ_SIZE、SET_DAQ_PTR、WRITE_DAQ、START_STOP、START_STOP_ALL | 静态上限;每个经典 CAN DTO 最多 7 字节数据 |
| 调度 | 10 ms / 100 ms Event Channel、Prescaler | 事件由应用任务显式调用,不在协议内创建定时器 |
| 发送 | 带 count 的 DTO/CRM 环形队列、txBusy、TxConfirmation | 队列满记录 overrun,不覆盖未发送帧 |
| 标定页 | 页面选择/读取回调契约 | 核心不绑定 OVC、MPU 或具体芯片寄存器 |
| 扩展能力 | Seed&Key、Flash Programming、Checksum 的命令扩展接口 | 仅提供拒绝默认值和接入点,不伪装成已实现 |
样例采用 8 字节经典 CAN 帧、Little-endian 命令字段和单个活动会话。若上位机/A2L 使用不同字节序、DAQ PID 规划或地址扩展语义,应在配置层明确统一,而不是在收包时猜测。
二、分层与依赖方向
Application / A2L configuration
| MemoryRegion, Event Channel, calibration-page callbacks
v
CCP protocol core
| CRO dispatch, CRM, MTA, DAQ List -> ODT -> Entry, timeout
v
CAN transport adapter
| CRO CAN ID filtering, asynchronous send, TxConfirmation
v
CAN driver / ISR
- CCP 协议核心:只处理协议状态、命令字段、CRM、MTA、DAQ 和发送队列,不直接访问 CAN 寄存器。
- CAN 传输适配:过滤示例 CRO CAN ID,把 8 字节帧交给核心;发送完成后回调确认。
- 地址白名单/内存映射:把“协议虚拟地址”映射到已注册缓冲区,并校验权限和完整访问区间。
- 周期事件入口:10 ms、100 ms 任务只调用统一的
Ccp_Event,Prescaler 在协议上下文内计数。 - 应用与 A2L 配置:双方共同约定虚拟地址、数据类型、换算、Event Channel 与 PID;固件不信任主站传入的裸指针。
三、完整、连续的 C99 样例
下面是一份可单文件编译的教学实现。代码先定义公共类型与配置,再实现核心,最后给出最小 CAN 适配和 main() 调用。真实工程可以按已有模块边界拆成 ccp.c/.h、传输适配和应用配置,但不需要为了文件行数再增加只转发的类或层。
/*
* Portable CCP 2.1 teaching subset (C99).
* All CAN identifiers, station addresses and virtual addresses are examples.
* This code is original teaching material, not copied from a private project.
*/
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#define CCP_FRAME_SIZE 8u
#define CCP_CRM_PAYLOAD_MAX 5u
#define CCP_CRO_DNLOAD_MAX 5u
#define CCP_CRO_DNLOAD6_SIZE 6u
#define CCP_MAX_DAQ_LISTS 2u
#define CCP_MAX_ODTS_PER_LIST 4u
#define CCP_MAX_ENTRIES_PER_ODT 7u
#define CCP_DTO_DATA_MAX 7u
#define CCP_TX_QUEUE_DEPTH 16u
#define CCP_EVENT_CHANNEL_COUNT 2u
#define CCP_EVENT_10MS 0u
#define CCP_EVENT_100MS 1u
#define CCP_SESSION_TIMEOUT_MS 10000u
#define CCP_NO_CHANNEL 0xFFu
#define CCP_MEM_READ 0x01u
#define CCP_MEM_WRITE 0x02u
#define CCP_CMD_CONNECT 0x01u
#define CCP_CMD_SET_MTA 0x02u
#define CCP_CMD_DNLOAD 0x03u
#define CCP_CMD_UPLOAD 0x04u
#define CCP_CMD_TEST 0x05u
#define CCP_CMD_START_STOP 0x06u
#define CCP_CMD_DISCONNECT 0x07u
#define CCP_CMD_START_STOP_ALL 0x08u
#define CCP_CMD_BUILD_CHKSUM 0x0Eu
#define CCP_CMD_SHORT_UPLOAD 0x0Fu
#define CCP_CMD_CLEAR_MEMORY 0x10u
#define CCP_CMD_GET_SEED 0x12u
#define CCP_CMD_UNLOCK 0x13u
#define CCP_CMD_GET_DAQ_SIZE 0x14u
#define CCP_CMD_SET_DAQ_PTR 0x15u
#define CCP_CMD_WRITE_DAQ 0x16u
#define CCP_CMD_EXCHANGE_ID 0x17u
#define CCP_CMD_PROGRAM 0x18u
#define CCP_CMD_PROGRAM6 0x22u
#define CCP_CMD_DNLOAD6 0x23u
#define CCP_PID_CRM 0xFFu
#define CCP_ERR_ACK 0x00u
#define CCP_ERR_CMD_UNKNOWN 0x30u
#define CCP_ERR_PARAM 0x32u
#define CCP_ERR_ACCESS 0x33u
#define CCP_ERR_RESOURCE 0x35u
typedef struct CcpContext CcpContext;
typedef bool (*CcpTryTransmit)(void *user,
uint8_t channel,
const uint8_t frame[CCP_FRAME_SIZE]);
typedef struct {
CcpTryTransmit tryTransmit;
void *user;
} CcpTransport;
typedef struct {
uint8_t addressExtension;
uint32_t virtualBase;
uint8_t *data;
uint32_t size;
uint8_t permissions;
} CcpMemoryRegion;
typedef bool (*CcpSelectCalibrationPage)(void *user, uint8_t page);
typedef bool (*CcpGetCalibrationPage)(void *user, uint8_t *page);
typedef struct {
CcpSelectCalibrationPage selectPage;
CcpGetCalibrationPage getPage;
void *user;
} CcpCalibrationPageOps;
typedef enum {
CCP_EXT_NOT_HANDLED = 0,
CCP_EXT_HANDLED = 1
} CcpExtensionResult;
typedef CcpExtensionResult (*CcpExtensionCommand)(
void *user,
const uint8_t cro[CCP_FRAME_SIZE],
uint8_t *returnCode,
uint8_t responseData[CCP_CRM_PAYLOAD_MAX],
uint8_t *responseLength);
typedef struct {
CcpExtensionCommand seedKeyCommand;
CcpExtensionCommand flashCommand;
CcpExtensionCommand checksumCommand;
void *user;
} CcpExtensionOps;
typedef struct {
uint16_t stationAddress;
uint8_t identityAddressExtension;
uint32_t identityVirtualAddress;
uint8_t identityLength;
} CcpConfig;
typedef struct {
uint8_t addressExtension;
uint32_t address;
} CcpMta;
typedef struct {
uint8_t addressExtension;
uint32_t address;
uint8_t size;
bool valid;
} CcpDaqEntry;
typedef struct {
CcpDaqEntry entry[CCP_MAX_ENTRIES_PER_ODT];
uint8_t totalBytes;
} CcpOdt;
typedef struct {
CcpOdt odt[CCP_MAX_ODTS_PER_LIST];
uint8_t lastOdt;
uint8_t eventChannel;
uint16_t prescaler;
uint16_t prescalerCounter;
bool selected;
bool running;
} CcpDaqList;
typedef struct {
uint8_t channel;
uint8_t data[CCP_FRAME_SIZE];
} CcpTxItem;
typedef struct {
CcpTxItem item[CCP_TX_QUEUE_DEPTH];
uint8_t head;
uint8_t tail;
uint8_t count;
} CcpTxQueue;
struct CcpContext {
CcpConfig config;
CcpTransport transport;
CcpCalibrationPageOps calibrationPage;
CcpExtensionOps extension;
const CcpMemoryRegion *regions;
uint8_t regionCount;
bool connected;
uint8_t boundChannel;
uint32_t inactivityMs;
CcpMta mta[2];
CcpDaqList daq[CCP_MAX_DAQ_LISTS];
uint8_t daqPtrList;
uint8_t daqPtrOdt;
uint8_t daqPtrEntry;
bool daqPtrValid;
CcpTxQueue txQueue;
bool txBusy;
uint8_t txChannel;
uint8_t txInFlight[CCP_FRAME_SIZE];
uint32_t txOverrun;
uint32_t txRejected;
uint32_t txFailed;
uint32_t memoryFaults;
};
static uint16_t Ccp_ReadLe16(const uint8_t *p)
{
return (uint16_t)((uint16_t)p[0] |
((uint16_t)p[1] << 8));
}
static uint32_t Ccp_ReadLe32(const uint8_t *p)
{
return ((uint32_t)p[0]) |
((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) |
((uint32_t)p[3] << 24);
}
static void Ccp_WriteLe32(uint8_t *p, uint32_t value)
{
p[0] = (uint8_t)(value);
p[1] = (uint8_t)(value >> 8);
p[2] = (uint8_t)(value >> 16);
p[3] = (uint8_t)(value >> 24);
}
static uint8_t *Ccp_Map(CcpContext *ctx,
uint8_t addressExtension,
uint32_t address,
uint8_t length,
bool writeAccess)
{
uint32_t end;
uint8_t i;
if ((length == 0u) ||
(address > (UINT32_MAX - (uint32_t)length))) {
return NULL;
}
end = address + (uint32_t)length;
for (i = 0u; i < ctx->regionCount; ++i) {
const CcpMemoryRegion *region = &ctx->regions[i];
uint32_t limit;
uint8_t needed = writeAccess ? CCP_MEM_WRITE : CCP_MEM_READ;
if ((region->data == NULL) ||
(region->size == 0u) ||
(region->addressExtension != addressExtension) ||
((region->permissions & needed) == 0u) ||
(region->virtualBase > (UINT32_MAX - region->size))) {
continue;
}
limit = region->virtualBase + region->size;
if ((address >= region->virtualBase) && (end <= limit)) {
return ®ion->data[address - region->virtualBase];
}
}
return NULL;
}
static void Ccp_StopAllDaq(CcpContext *ctx)
{
uint8_t i;
for (i = 0u; i < CCP_MAX_DAQ_LISTS; ++i) {
ctx->daq[i].running = false;
ctx->daq[i].selected = false;
ctx->daq[i].prescalerCounter = 0u;
}
}
static void Ccp_ClearQueue(CcpContext *ctx)
{
ctx->txQueue.head = 0u;
ctx->txQueue.tail = 0u;
ctx->txQueue.count = 0u;
}
static bool Ccp_QueuePush(CcpContext *ctx,
uint8_t channel,
const uint8_t frame[CCP_FRAME_SIZE])
{
CcpTxItem *slot;
if (ctx->txQueue.count >= CCP_TX_QUEUE_DEPTH) {
++ctx->txOverrun;
return false;
}
slot = &ctx->txQueue.item[ctx->txQueue.tail];
slot->channel = channel;
memcpy(slot->data, frame, CCP_FRAME_SIZE);
ctx->txQueue.tail =
(uint8_t)((ctx->txQueue.tail + 1u) % CCP_TX_QUEUE_DEPTH);
++ctx->txQueue.count;
return true;
}
static void Ccp_SendCrm(CcpContext *ctx,
uint8_t channel,
uint8_t ctr,
uint8_t returnCode,
const uint8_t *data,
uint8_t dataLength)
{
uint8_t frame[CCP_FRAME_SIZE];
memset(frame, 0, sizeof(frame));
frame[0] = CCP_PID_CRM;
frame[1] = returnCode;
frame[2] = ctr;
if ((data != NULL) && (dataLength <= CCP_CRM_PAYLOAD_MAX)) {
memcpy(&frame[3], data, dataLength);
}
(void)Ccp_QueuePush(ctx, channel, frame);
}
static void Ccp_ServiceTx(CcpContext *ctx)
{
CcpTxItem *front;
bool accepted;
if (ctx->txBusy ||
(ctx->txQueue.count == 0u) ||
(ctx->transport.tryTransmit == NULL)) {
return;
}
front = &ctx->txQueue.item[ctx->txQueue.head];
memcpy(ctx->txInFlight, front->data, CCP_FRAME_SIZE);
ctx->txChannel = front->channel;
/*
* Contract: tryTransmit only accepts the request here. Completion is
* reported later through Ccp_TxConfirmation().
*/
ctx->txBusy = true;
accepted = ctx->transport.tryTransmit(ctx->transport.user,
ctx->txChannel,
ctx->txInFlight);
if (accepted) {
ctx->txQueue.head =
(uint8_t)((ctx->txQueue.head + 1u) % CCP_TX_QUEUE_DEPTH);
--ctx->txQueue.count;
} else {
ctx->txBusy = false;
++ctx->txRejected;
}
}
static bool Ccp_ReadMta(CcpContext *ctx,
uint8_t mtaIndex,
uint8_t *destination,
uint8_t length)
{
uint8_t *source;
if (mtaIndex >= 2u) {
return false;
}
source = Ccp_Map(ctx,
ctx->mta[mtaIndex].addressExtension,
ctx->mta[mtaIndex].address,
length,
false);
if (source == NULL) {
++ctx->memoryFaults;
return false;
}
memcpy(destination, source, length);
ctx->mta[mtaIndex].address += (uint32_t)length;
return true;
}
static bool Ccp_WriteMta(CcpContext *ctx,
uint8_t mtaIndex,
const uint8_t *source,
uint8_t length)
{
uint8_t *destination;
if (mtaIndex >= 2u) {
return false;
}
destination = Ccp_Map(ctx,
ctx->mta[mtaIndex].addressExtension,
ctx->mta[mtaIndex].address,
length,
true);
if (destination == NULL) {
++ctx->memoryFaults;
return false;
}
memcpy(destination, source, length);
ctx->mta[mtaIndex].address += (uint32_t)length;
return true;
}
static bool Ccp_DaqListIsValid(const CcpDaqList *list)
{
uint8_t odtIndex;
if ((list->lastOdt >= CCP_MAX_ODTS_PER_LIST) ||
(list->eventChannel >= CCP_EVENT_CHANNEL_COUNT) ||
(list->prescaler == 0u)) {
return false;
}
for (odtIndex = 0u; odtIndex <= list->lastOdt; ++odtIndex) {
const CcpOdt *odt = &list->odt[odtIndex];
uint8_t entryIndex;
uint8_t sum = 0u;
if ((odt->totalBytes == 0u) ||
(odt->totalBytes > CCP_DTO_DATA_MAX)) {
return false;
}
for (entryIndex = 0u;
entryIndex < CCP_MAX_ENTRIES_PER_ODT;
++entryIndex) {
if (odt->entry[entryIndex].valid) {
sum = (uint8_t)(sum + odt->entry[entryIndex].size);
}
}
if (sum != odt->totalBytes) {
return false;
}
}
return true;
}
static bool Ccp_BuildDto(CcpContext *ctx,
uint8_t listIndex,
uint8_t odtIndex,
uint8_t frame[CCP_FRAME_SIZE])
{
const CcpOdt *odt = &ctx->daq[listIndex].odt[odtIndex];
uint8_t offset = 1u;
uint8_t entryIndex;
memset(frame, 0, CCP_FRAME_SIZE);
frame[0] = (uint8_t)(listIndex * CCP_MAX_ODTS_PER_LIST +
odtIndex);
for (entryIndex = 0u;
entryIndex < CCP_MAX_ENTRIES_PER_ODT;
++entryIndex) {
const CcpDaqEntry *entry = &odt->entry[entryIndex];
uint8_t *source;
if (!entry->valid) {
continue;
}
if ((entry->size > (CCP_FRAME_SIZE - offset)) ||
(entry->size == 0u)) {
return false;
}
source = Ccp_Map(ctx,
entry->addressExtension,
entry->address,
entry->size,
false);
if (source == NULL) {
++ctx->memoryFaults;
return false;
}
memcpy(&frame[offset], source, entry->size);
offset = (uint8_t)(offset + entry->size);
}
return true;
}
static void Ccp_AdvanceDaqPointer(CcpContext *ctx)
{
++ctx->daqPtrEntry;
if (ctx->daqPtrEntry >= CCP_MAX_ENTRIES_PER_ODT) {
ctx->daqPtrEntry = 0u;
++ctx->daqPtrOdt;
if (ctx->daqPtrOdt >= CCP_MAX_ODTS_PER_LIST) {
ctx->daqPtrValid = false;
}
}
}
static void Ccp_HandleWriteDaq(CcpContext *ctx,
uint8_t channel,
const uint8_t cro[CCP_FRAME_SIZE])
{
uint8_t size = cro[2];
uint8_t extension = cro[3];
uint32_t address = Ccp_ReadLe32(&cro[4]);
CcpOdt *odt;
CcpDaqEntry *entry;
uint8_t oldSize;
uint8_t newTotal;
if (!ctx->daqPtrValid ||
(ctx->daqPtrList >= CCP_MAX_DAQ_LISTS) ||
(ctx->daqPtrOdt >= CCP_MAX_ODTS_PER_LIST) ||
(ctx->daqPtrEntry >= CCP_MAX_ENTRIES_PER_ODT)) {
Ccp_SendCrm(ctx, channel, cro[1], CCP_ERR_PARAM, NULL, 0u);
return;
}
if ((size != 1u) && (size != 2u) && (size != 4u)) {
Ccp_SendCrm(ctx, channel, cro[1], CCP_ERR_PARAM, NULL, 0u);
return;
}
if (Ccp_Map(ctx, extension, address, size, false) == NULL) {
Ccp_SendCrm(ctx, channel, cro[1], CCP_ERR_ACCESS, NULL, 0u);
return;
}
odt = &ctx->daq[ctx->daqPtrList].odt[ctx->daqPtrOdt];
entry = &odt->entry[ctx->daqPtrEntry];
oldSize = entry->valid ? entry->size : 0u;
newTotal = (uint8_t)(odt->totalBytes - oldSize + size);
if (newTotal > CCP_DTO_DATA_MAX) {
Ccp_SendCrm(ctx, channel, cro[1], CCP_ERR_PARAM, NULL, 0u);
return;
}
entry->addressExtension = extension;
entry->address = address;
entry->size = size;
entry->valid = true;
odt->totalBytes = newTotal;
Ccp_AdvanceDaqPointer(ctx);
Ccp_SendCrm(ctx, channel, cro[1], CCP_ERR_ACK, NULL, 0u);
}
static void Ccp_HandleStartStop(CcpContext *ctx,
uint8_t channel,
const uint8_t cro[CCP_FRAME_SIZE])
{
uint8_t mode = cro[2];
uint8_t listIndex = cro[3];
uint8_t lastOdt = cro[4];
uint8_t eventChannel = cro[5];
uint16_t prescaler = Ccp_ReadLe16(&cro[6]);
CcpDaqList *list;
if ((listIndex >= CCP_MAX_DAQ_LISTS) ||
(lastOdt >= CCP_MAX_ODTS_PER_LIST) ||
(eventChannel >= CCP_EVENT_CHANNEL_COUNT) ||
(prescaler == 0u) ||
((mode != 0u) && (mode != 1u) && (mode != 2u))) {
Ccp_SendCrm(ctx, channel, cro[1], CCP_ERR_PARAM, NULL, 0u);
return;
}
list = &ctx->daq[listIndex];
list->lastOdt = lastOdt;
list->eventChannel = eventChannel;
list->prescaler = prescaler;
list->prescalerCounter = 0u;
if (!Ccp_DaqListIsValid(list)) {
Ccp_SendCrm(ctx, channel, cro[1], CCP_ERR_PARAM, NULL, 0u);
return;
}
if (mode == 0u) {
list->running = false;
list->selected = false;
} else if (mode == 1u) {
list->selected = true;
list->running = true;
} else {
list->selected = true;
list->running = false;
}
Ccp_SendCrm(ctx, channel, cro[1], CCP_ERR_ACK, NULL, 0u);
}
static void Ccp_HandleStartStopAll(CcpContext *ctx,
uint8_t channel,
const uint8_t cro[CCP_FRAME_SIZE])
{
uint8_t mode = cro[2];
uint8_t i;
if ((mode != 0u) && (mode != 1u)) {
Ccp_SendCrm(ctx, channel, cro[1], CCP_ERR_PARAM, NULL, 0u);
return;
}
for (i = 0u; i < CCP_MAX_DAQ_LISTS; ++i) {
if (mode == 0u) {
ctx->daq[i].running = false;
ctx->daq[i].selected = false;
ctx->daq[i].prescalerCounter = 0u;
} else if (ctx->daq[i].selected &&
Ccp_DaqListIsValid(&ctx->daq[i])) {
ctx->daq[i].running = true;
ctx->daq[i].prescalerCounter = 0u;
}
}
Ccp_SendCrm(ctx, channel, cro[1], CCP_ERR_ACK, NULL, 0u);
}
static bool Ccp_TryExtension(CcpContext *ctx,
CcpExtensionCommand command,
uint8_t channel,
const uint8_t cro[CCP_FRAME_SIZE])
{
uint8_t data[CCP_CRM_PAYLOAD_MAX];
uint8_t dataLength = 0u;
uint8_t returnCode = CCP_ERR_CMD_UNKNOWN;
CcpExtensionResult result;
if (command == NULL) {
return false;
}
memset(data, 0, sizeof(data));
result = command(ctx->extension.user,
cro,
&returnCode,
data,
&dataLength);
if (result == CCP_EXT_NOT_HANDLED) {
return false;
}
if (dataLength > CCP_CRM_PAYLOAD_MAX) {
dataLength = 0u;
returnCode = CCP_ERR_PARAM;
}
Ccp_SendCrm(ctx, channel, cro[1], returnCode, data, dataLength);
return true;
}
static void Ccp_Dispatch(CcpContext *ctx,
uint8_t channel,
const uint8_t cro[CCP_FRAME_SIZE])
{
uint8_t data[CCP_CRM_PAYLOAD_MAX];
uint8_t command = cro[0];
memset(data, 0, sizeof(data));
switch (command) {
case CCP_CMD_CONNECT:
case CCP_CMD_TEST:
Ccp_SendCrm(ctx, channel, cro[1], CCP_ERR_ACK, NULL, 0u);
break;
case CCP_CMD_DISCONNECT:
Ccp_StopAllDaq(ctx);
Ccp_ClearQueue(ctx);
ctx->connected = false;
ctx->boundChannel = CCP_NO_CHANNEL;
ctx->inactivityMs = 0u;
Ccp_SendCrm(ctx, channel, cro[1], CCP_ERR_ACK, NULL, 0u);
break;
case CCP_CMD_EXCHANGE_ID:
ctx->mta[0].addressExtension =
ctx->config.identityAddressExtension;
ctx->mta[0].address =
ctx->config.identityVirtualAddress;
data[0] = ctx->config.identityLength;
data[1] = 0u; /* byte-array identifier */
data[2] = 0x03u; /* example: CAL + DAQ available */
data[3] = 0x00u; /* protection is not implemented here */
Ccp_SendCrm(ctx, channel, cro[1], CCP_ERR_ACK, data, 4u);
break;
case CCP_CMD_SET_MTA: {
uint8_t mtaIndex = cro[2];
if (mtaIndex >= 2u) {
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_PARAM, NULL, 0u);
break;
}
ctx->mta[mtaIndex].addressExtension = cro[3];
ctx->mta[mtaIndex].address = Ccp_ReadLe32(&cro[4]);
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_ACK, NULL, 0u);
break;
}
case CCP_CMD_UPLOAD: {
uint8_t length = cro[2];
if ((length == 0u) ||
(length > CCP_CRM_PAYLOAD_MAX) ||
!Ccp_ReadMta(ctx, 0u, data, length)) {
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_ACCESS, NULL, 0u);
break;
}
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_ACK, data, length);
break;
}
case CCP_CMD_SHORT_UPLOAD: {
uint8_t length = cro[2];
CcpMta saved = ctx->mta[0];
if ((length == 0u) ||
(length > CCP_CRM_PAYLOAD_MAX)) {
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_PARAM, NULL, 0u);
break;
}
ctx->mta[0].addressExtension = cro[3];
ctx->mta[0].address = Ccp_ReadLe32(&cro[4]);
if (!Ccp_ReadMta(ctx, 0u, data, length)) {
ctx->mta[0] = saved;
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_ACCESS, NULL, 0u);
break;
}
ctx->mta[0] = saved;
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_ACK, data, length);
break;
}
case CCP_CMD_DNLOAD: {
uint8_t length = cro[2];
if ((length == 0u) ||
(length > CCP_CRO_DNLOAD_MAX)) {
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_PARAM, NULL, 0u);
break;
}
if (!Ccp_WriteMta(ctx, 0u, &cro[3], length)) {
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_ACCESS, NULL, 0u);
break;
}
data[0] = ctx->mta[0].addressExtension;
Ccp_WriteLe32(&data[1], ctx->mta[0].address);
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_ACK, data, 5u);
break;
}
case CCP_CMD_DNLOAD6:
if (!Ccp_WriteMta(ctx, 0u, &cro[2],
CCP_CRO_DNLOAD6_SIZE)) {
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_ACCESS, NULL, 0u);
break;
}
data[0] = ctx->mta[0].addressExtension;
Ccp_WriteLe32(&data[1], ctx->mta[0].address);
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_ACK, data, 5u);
break;
case CCP_CMD_GET_DAQ_SIZE: {
uint8_t listIndex = cro[2];
if (listIndex >= CCP_MAX_DAQ_LISTS) {
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_PARAM, NULL, 0u);
break;
}
data[0] = CCP_MAX_ODTS_PER_LIST;
data[1] = (uint8_t)(listIndex *
CCP_MAX_ODTS_PER_LIST);
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_ACK, data, 2u);
break;
}
case CCP_CMD_SET_DAQ_PTR:
if ((cro[2] >= CCP_MAX_DAQ_LISTS) ||
(cro[3] >= CCP_MAX_ODTS_PER_LIST) ||
(cro[4] >= CCP_MAX_ENTRIES_PER_ODT)) {
ctx->daqPtrValid = false;
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_PARAM, NULL, 0u);
break;
}
ctx->daqPtrList = cro[2];
ctx->daqPtrOdt = cro[3];
ctx->daqPtrEntry = cro[4];
ctx->daqPtrValid = true;
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_ACK, NULL, 0u);
break;
case CCP_CMD_WRITE_DAQ:
Ccp_HandleWriteDaq(ctx, channel, cro);
break;
case CCP_CMD_START_STOP:
Ccp_HandleStartStop(ctx, channel, cro);
break;
case CCP_CMD_START_STOP_ALL:
Ccp_HandleStartStopAll(ctx, channel, cro);
break;
case CCP_CMD_GET_SEED:
case CCP_CMD_UNLOCK:
if (!Ccp_TryExtension(ctx,
ctx->extension.seedKeyCommand,
channel,
cro)) {
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_RESOURCE, NULL, 0u);
}
break;
case CCP_CMD_CLEAR_MEMORY:
case CCP_CMD_PROGRAM:
case CCP_CMD_PROGRAM6:
if (!Ccp_TryExtension(ctx,
ctx->extension.flashCommand,
channel,
cro)) {
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_RESOURCE, NULL, 0u);
}
break;
case CCP_CMD_BUILD_CHKSUM:
if (!Ccp_TryExtension(ctx,
ctx->extension.checksumCommand,
channel,
cro)) {
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_RESOURCE, NULL, 0u);
}
break;
default:
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_CMD_UNKNOWN, NULL, 0u);
break;
}
}
bool Ccp_Init(CcpContext *ctx,
const CcpConfig *config,
const CcpMemoryRegion *regions,
uint8_t regionCount,
const CcpTransport *transport,
const CcpCalibrationPageOps *calibrationPage,
const CcpExtensionOps *extension)
{
uint8_t i;
if ((ctx == NULL) ||
(config == NULL) ||
(regions == NULL) ||
(regionCount == 0u) ||
(transport == NULL) ||
(transport->tryTransmit == NULL)) {
return false;
}
for (i = 0u; i < regionCount; ++i) {
if ((regions[i].data == NULL) ||
(regions[i].size == 0u) ||
(regions[i].virtualBase >
(UINT32_MAX - regions[i].size))) {
return false;
}
}
memset(ctx, 0, sizeof(*ctx));
ctx->config = *config;
ctx->regions = regions;
ctx->regionCount = regionCount;
ctx->transport = *transport;
ctx->boundChannel = CCP_NO_CHANNEL;
ctx->txChannel = CCP_NO_CHANNEL;
if (calibrationPage != NULL) {
ctx->calibrationPage = *calibrationPage;
}
if (extension != NULL) {
ctx->extension = *extension;
}
return true;
}
void Ccp_RxIndication(CcpContext *ctx,
uint8_t channel,
const uint8_t *cro,
uint8_t length)
{
uint8_t command;
uint16_t station;
if ((ctx == NULL) ||
(cro == NULL) ||
(length != CCP_FRAME_SIZE)) {
return;
}
command = cro[0];
if ((command == CCP_CMD_CONNECT) ||
(command == CCP_CMD_TEST)) {
station = Ccp_ReadLe16(&cro[2]);
if (station != ctx->config.stationAddress) {
return;
}
if (command == CCP_CMD_CONNECT) {
if (ctx->connected &&
(ctx->boundChannel != channel)) {
return;
}
ctx->connected = true;
ctx->boundChannel = channel;
ctx->inactivityMs = 0u;
}
Ccp_Dispatch(ctx, channel, cro);
return;
}
if (!ctx->connected ||
(ctx->boundChannel != channel)) {
Ccp_SendCrm(ctx, channel, cro[1],
CCP_ERR_ACCESS, NULL, 0u);
return;
}
ctx->inactivityMs = 0u;
Ccp_Dispatch(ctx, channel, cro);
}
void Ccp_MainFunction(CcpContext *ctx)
{
if (ctx == NULL) {
return;
}
if (ctx->connected) {
if (ctx->inactivityMs < UINT32_MAX) {
++ctx->inactivityMs;
}
if (ctx->inactivityMs >= CCP_SESSION_TIMEOUT_MS) {
Ccp_StopAllDaq(ctx);
Ccp_ClearQueue(ctx);
ctx->connected = false;
ctx->boundChannel = CCP_NO_CHANNEL;
ctx->inactivityMs = 0u;
/*
* Do not reuse txInFlight until a late hardware confirmation
* arrives. Pending queued frames are gone, and no new DAQ is made.
*/
}
}
Ccp_ServiceTx(ctx);
}
void Ccp_TxConfirmation(CcpContext *ctx,
uint8_t channel,
bool success)
{
if ((ctx == NULL) ||
!ctx->txBusy ||
(ctx->txChannel != channel)) {
return;
}
if (!success) {
++ctx->txFailed;
}
ctx->txBusy = false;
ctx->txChannel = CCP_NO_CHANNEL;
}
void Ccp_Event(CcpContext *ctx, uint8_t eventChannel)
{
uint8_t listIndex;
if ((ctx == NULL) ||
!ctx->connected ||
(eventChannel >= CCP_EVENT_CHANNEL_COUNT)) {
return;
}
for (listIndex = 0u;
listIndex < CCP_MAX_DAQ_LISTS;
++listIndex) {
CcpDaqList *list = &ctx->daq[listIndex];
uint8_t odtIndex;
if (!list->running ||
(list->eventChannel != eventChannel)) {
continue;
}
++list->prescalerCounter;
if (list->prescalerCounter < list->prescaler) {
continue;
}
list->prescalerCounter = 0u;
for (odtIndex = 0u;
odtIndex <= list->lastOdt;
++odtIndex) {
uint8_t frame[CCP_FRAME_SIZE];
if (!Ccp_BuildDto(ctx,
listIndex,
odtIndex,
frame)) {
list->running = false;
break;
}
if (!Ccp_QueuePush(ctx,
ctx->boundChannel,
frame)) {
return;
}
}
}
}
/* ---------------- Minimal CAN adapter and executable demo ---------------- */
#define EXAMPLE_CCP_CRO_CAN_ID 0x650u
#define EXAMPLE_CCP_DTO_CAN_ID 0x651u
#define EXAMPLE_CAN_CHANNEL 0u
typedef struct {
CcpContext *ccp;
bool hardwareBusy;
uint8_t pendingChannel;
uint8_t pendingFrame[CCP_FRAME_SIZE];
} DemoCanAdapter;
typedef struct {
uint8_t activeCalibrationPage;
} DemoApplication;
static bool DemoCan_TryTransmit(void *user,
uint8_t channel,
const uint8_t frame[CCP_FRAME_SIZE])
{
DemoCanAdapter *adapter = (DemoCanAdapter *)user;
uint8_t i;
if (adapter->hardwareBusy) {
return false;
}
/*
* A real driver queues EXAMPLE_CCP_DTO_CAN_ID here. This demo copies the
* bytes to stable driver-owned storage and completes it later.
*/
memcpy(adapter->pendingFrame, frame, CCP_FRAME_SIZE);
adapter->pendingChannel = channel;
adapter->hardwareBusy = true;
printf("TX ch%u id=0x%03X :", (unsigned)channel,
(unsigned)EXAMPLE_CCP_DTO_CAN_ID);
for (i = 0u; i < CCP_FRAME_SIZE; ++i) {
printf(" %02X", (unsigned)frame[i]);
}
printf("\n");
return true;
}
static void DemoCan_Rx(DemoCanAdapter *adapter,
uint8_t channel,
uint16_t canId,
const uint8_t data[CCP_FRAME_SIZE])
{
if ((canId == EXAMPLE_CCP_CRO_CAN_ID) &&
(channel == EXAMPLE_CAN_CHANNEL)) {
Ccp_RxIndication(adapter->ccp,
channel,
data,
CCP_FRAME_SIZE);
}
}
static void DemoCan_TxInterrupt(DemoCanAdapter *adapter,
bool success)
{
uint8_t channel;
if (!adapter->hardwareBusy) {
return;
}
channel = adapter->pendingChannel;
adapter->hardwareBusy = false;
Ccp_TxConfirmation(adapter->ccp, channel, success);
}
static void Demo_DrainTx(DemoCanAdapter *adapter)
{
while (adapter->hardwareBusy ||
(adapter->ccp->txQueue.count != 0u)) {
if (adapter->hardwareBusy) {
DemoCan_TxInterrupt(adapter, true);
}
Ccp_MainFunction(adapter->ccp);
}
}
static void Demo_SendCro(DemoCanAdapter *adapter,
const uint8_t cro[CCP_FRAME_SIZE])
{
DemoCan_Rx(adapter,
EXAMPLE_CAN_CHANNEL,
EXAMPLE_CCP_CRO_CAN_ID,
cro);
Ccp_MainFunction(adapter->ccp);
Demo_DrainTx(adapter);
}
static bool Demo_SelectCalibrationPage(void *user, uint8_t page)
{
DemoApplication *app = (DemoApplication *)user;
if (page > 1u) {
return false;
}
/*
* A target-specific adapter may switch OVC/MPU mappings here, verify the
* hardware state, then update activeCalibrationPage only on success.
*/
app->activeCalibrationPage = page;
return true;
}
static bool Demo_GetCalibrationPage(void *user, uint8_t *page)
{
DemoApplication *app = (DemoApplication *)user;
if (page == NULL) {
return false;
}
*page = app->activeCalibrationPage;
return true;
}
int main(void)
{
static uint8_t identity[8] = {
'C', 'C', 'P', '2', '1', '-', 'D', 'E'
};
static uint8_t measurements[32];
static uint8_t calibration[32];
static CcpContext ccp;
static DemoCanAdapter canAdapter;
static DemoApplication app;
uint32_t speedRaw = 123456u;
uint16_t currentRaw = 320u;
uint8_t stateRaw = 5u;
const CcpMemoryRegion regions[] = {
{0u, 0x10000000u, identity, sizeof(identity),
CCP_MEM_READ},
{0u, 0x20000000u, measurements, sizeof(measurements),
CCP_MEM_READ},
{0u, 0x30000000u, calibration, sizeof(calibration),
CCP_MEM_READ | CCP_MEM_WRITE}
};
const CcpConfig config = {
0x0001u, 0u, 0x10000000u, sizeof(identity)
};
CcpTransport transport;
CcpCalibrationPageOps pageOps;
const uint8_t connect[8] =
{0x01u, 0x01u, 0x01u, 0x00u, 0u, 0u, 0u, 0u};
const uint8_t setMtaCalibration[8] =
{0x02u, 0x02u, 0x00u, 0x00u,
0x00u, 0x00u, 0x00u, 0x30u};
const uint8_t downloadValue[8] =
{0x03u, 0x03u, 0x04u, 0x78u,
0x56u, 0x34u, 0x12u, 0x00u};
const uint8_t uploadValue[8] =
{0x04u, 0x04u, 0x04u, 0u, 0u, 0u, 0u, 0u};
const uint8_t getDaqSize[8] =
{0x14u, 0x10u, 0x00u, 0u, 0u, 0u, 0u, 0u};
const uint8_t setDaqPtr[8] =
{0x15u, 0x11u, 0x00u, 0x00u,
0x00u, 0u, 0u, 0u};
const uint8_t writeSpeed[8] =
{0x16u, 0x12u, 0x04u, 0x00u,
0x00u, 0x00u, 0x00u, 0x20u};
const uint8_t writeCurrent[8] =
{0x16u, 0x13u, 0x02u, 0x00u,
0x04u, 0x00u, 0x00u, 0x20u};
const uint8_t writeState[8] =
{0x16u, 0x14u, 0x01u, 0x00u,
0x06u, 0x00u, 0x00u, 0x20u};
const uint8_t startDaq[8] =
{0x06u, 0x15u, 0x01u, 0x00u,
0x00u, CCP_EVENT_10MS, 0x01u, 0x00u};
const uint8_t stopAll[8] =
{0x08u, 0x16u, 0x00u, 0u, 0u, 0u, 0u, 0u};
memcpy(&measurements[0], &speedRaw, sizeof(speedRaw));
memcpy(&measurements[4], ¤tRaw, sizeof(currentRaw));
memcpy(&measurements[6], &stateRaw, sizeof(stateRaw));
canAdapter.ccp = &ccp;
transport.tryTransmit = DemoCan_TryTransmit;
transport.user = &canAdapter;
pageOps.selectPage = Demo_SelectCalibrationPage;
pageOps.getPage = Demo_GetCalibrationPage;
pageOps.user = &app;
if (!Ccp_Init(&ccp,
&config,
regions,
(uint8_t)(sizeof(regions) / sizeof(regions[0])),
&transport,
&pageOps,
NULL)) {
return 1;
}
/* CONNECT -> SET_MTA -> DNLOAD -> UPLOAD */
Demo_SendCro(&canAdapter, connect);
Demo_SendCro(&canAdapter, setMtaCalibration);
Demo_SendCro(&canAdapter, downloadValue);
/* DNLOAD advanced MTA; set it again before reading back. */
Demo_SendCro(&canAdapter, setMtaCalibration);
Demo_SendCro(&canAdapter, uploadValue);
/*
* GET_DAQ_SIZE -> SET_DAQ_PTR -> WRITE_DAQ x3 -> START_STOP.
* One DTO becomes PID + 4-byte speed + 2-byte current + 1-byte state.
*/
Demo_SendCro(&canAdapter, getDaqSize);
Demo_SendCro(&canAdapter, setDaqPtr);
Demo_SendCro(&canAdapter, writeSpeed);
Demo_SendCro(&canAdapter, writeCurrent);
Demo_SendCro(&canAdapter, writeState);
Demo_SendCro(&canAdapter, startDaq);
/* Called by the real 10 ms task. Prescaler=1, so one DTO is queued. */
Ccp_Event(&ccp, CCP_EVENT_10MS);
Ccp_MainFunction(&ccp);
Demo_DrainTx(&canAdapter);
Demo_SendCro(&canAdapter, stopAll);
return 0;
}
四、代码里的安全边界逐项解释
1. 8 字节帧先整体清零
Ccp_SendCrm() 和 Ccp_BuildDto() 都先对完整 8 字节数组执行 memset(..., 0, 8)。因此 CRM/DTO 未使用字节保持为 0,不会把旧栈内容或上一帧残留带到总线上。局部构帧数组只作为同步输入复制进队列,异步驱动实际收到的是 Context 内持续有效的 txInFlight。
2. 地址不是指针,而是白名单中的协议虚拟地址
Ccp_Map() 同时检查地址扩展、读写权限、address + length 的整数溢出以及整个半开区间 [address, address + length)。只有完整区间落在同一个 MemoryRegion 中才返回实际缓冲区指针;仅首地址合法但尾端越界、跨 Region 或越过 UINT32_MAX 都会失败。
3. CRO/CRM 的有效负载上限分开检查
CRM 的 3..7 字节最多承载 5 字节,所以 UPLOAD 和 SHORT_UPLOAD 拒绝 0 或大于 5 的长度;普通 DNLOAD 的 CRO 数据区同样最多 5 字节。DNLOAD6 是独立命令,固定从 CRO 的 2..7 字节写入 6 字节,不能拿普通 DNLOAD 的长度字段套用。
4. DAQ 三层下标和一帧总长度都受控
SET_DAQ_PTR 对 List、ODT、Entry 全部使用 >= 上限判断。WRITE_DAQ 仅接受 1/2/4 字节,先验证完整只读地址区间,再用“旧 Entry 长度扣除 + 新长度加入”计算 ODT 总长度,保证不超过 7。START_STOP 检查 lastOdt、eventChannel、prescaler 和全部待发送 ODT 的一致性。
5. 环形队列明确区分空与满
队列保留 head、tail 和 count。count == 0 是空,count == depth 是满;满时增加 txOverrun 并拒绝新帧,绝不覆盖尚未发送的数据。发送请求被驱动接受后才从队列弹出;硬件完成后由 Ccp_TxConfirmation() 清除 txBusy。
6. 超时清现场,但不破坏在途缓冲区
1 ms 主函数累计 10 秒无有效活动后停止所有 DAQ、清空待发送队列并解除 CAN 通道绑定。若硬件已有一帧在途,样例保留 txInFlight 和 txBusy,直到迟到的 TxConfirmation 返回,避免异步驱动仍引用缓冲区时被复用。量产驱动若支持可靠取消,可在传输适配层增加取消与确认状态机。
五、典型接入顺序
CAN Rx ISR/task -> Ccp_RxIndication(&ccp, channel, data, 8)
1 ms task/main loop -> Ccp_MainFunction(&ccp)
10 ms periodic task -> Ccp_Event(&ccp, CCP_EVENT_10MS)
100 ms periodic task -> Ccp_Event(&ccp, CCP_EVENT_100MS)
CAN Tx complete callback -> Ccp_TxConfirmation(&ccp, channel, success)
Ccp_MainFunction() 的时间基准必须稳定为 1 ms;若工程只能提供其他周期,应把“本次经过的毫秒数”作为参数,或在适配层做累计,不能直接沿用 10 秒常量。10 ms 与 100 ms 事件入口可以在不同任务调用,但共享 Context 时需要按项目并发模型加入短临界区。
六、两组完整交互示例
1. CONNECT → SET_MTA → DNLOAD → UPLOAD
CONNECT
-> SET_MTA(MTA0, ext=0, address=0x30000000) [example address]
-> DNLOAD(4, 78 56 34 12)
-> SET_MTA(MTA0, ext=0, address=0x30000000)
-> UPLOAD(4)
<- CRM payload: 78 56 34 12
成功 DNLOAD 会推进 MTA,所以读回前重新 SET_MTA。示例虚拟地址 0x30000000 只映射到本地 calibration[] 缓冲区;它不代表任何真实 ECU 内存布局。
2. 三个变量以 4 + 2 + 1 字节组成一帧 DTO
GET_DAQ_SIZE(list=0)
-> SET_DAQ_PTR(list=0, odt=0, entry=0)
-> WRITE_DAQ(size=4, address=0x20000000) [example address]
-> WRITE_DAQ(size=2, address=0x20000004) [example address]
-> WRITE_DAQ(size=1, address=0x20000006) [example address]
-> START_STOP(mode=1, list=0, lastOdt=0,
event=10ms, prescaler=1)
10 ms Event:
DTO = PID | speed[4] | current[2] | state[1]
| DTO 字节 | 内容 | 长度 |
|---|---|---|
| 0 | PID(样例 List 0 / ODT 0 为 0) | 1 |
| 1..4 | speedRaw,示例虚拟地址 0x20000000 | 4 |
| 5..6 | currentRaw,示例虚拟地址 0x20000004 | 2 |
| 7 | stateRaw,示例虚拟地址 0x20000006 | 1 |
三个 WRITE_DAQ 完成后,ODT 数据总长刚好是 7;再加入任何 Entry 都会被拒绝。若 Prescaler 设为 5,并绑定 10 ms Event,则每第 5 次事件采集一次,发送周期为 50 ms。要批量启动,可先用 START_STOP(mode=2) 选择多个 List,再用 START_STOP_ALL(mode=1) 启动。
七、标准 CCP 行为与样例简化点
| 主题 | CCP 2.1 通用行为 | 本文样例的选择 |
|---|---|---|
| CRO/CRM | 8 字节命令与响应,CTR 关联请求 | 保留 PID/返回码/CTR;只实现列出的子集 |
| 字节序 | 主从需按约定解释多字节字段 | 固定 Little-endian,接入时必须与工具一致 |
| 地址 | MTA 由地址扩展和地址组成 | 地址被解释为白名单虚拟地址,不允许裸指针 |
| DAQ | List → ODT → Entry,由事件和 Prescaler 触发 | 静态 2 List × 4 ODT × 7 Entry;PID 采用简单连续分配 |
| 会话 | 连接、命令活动、断开 | 单活动会话,10 秒超时,首次 CONNECT 绑定通道 |
| EXCHANGE_ID | 报告身份长度/资源并通过 MTA 读取标识 | 标识位于只读白名单 Region,资源位仅示例 |
| 标定页 | 协议表达页面选择与查询 | 核心只定义回调契约;OVC/硬件动作由应用适配并回读确认 |
| 安全/编程 | 可扩展资源解锁、校验和与编程流程 | 默认返回资源不可用;只有接口,没有伪实现 |
为了让样例保持连续、可读,CRM 与 DAQ DTO 共用一个 FIFO,且没有实现命令优先级、DAQ 动态内存分配、完整 Session Status、事件报文和所有 CCP 可选命令。量产系统通常要给 CRM 更高优先级,并把容量和事件描述从 ECU/A2L 的共同配置生成。
八、标定页、Seed&Key 与编程扩展如何接入
CcpCalibrationPageOps 只定义 selectPage/getPage。目标适配应在 select 回调中执行“请求切换 → 配置 OVC/MPU/软件映射 → 回读硬件状态 → 成功后更新活动页”;协议核心不应该包含具体寄存器地址。本文没有把某款 MCU 的 OVC 写法包装成通用实现。
CcpExtensionOps 分别预留 Seed&Key、Flash Programming 和异步 Checksum 命令处理器。回调为空时明确返回资源不可用。真正接入时,Seed 和 Key 的生成/验证应由安全模块或 HSM 承担;Flash 擦写/编程必须有电压、会话、地址、完整性与回滚状态机;大块 Checksum 应异步分片,而不是在 CAN Rx 上下文阻塞计算。
九、从样例到量产还缺什么
- Seed&Key / HSM:按 CAL、DAQ、PGM 资源分权,加入失败计数、延时、防重放和会话重锁。
- 多核并发与临界区:保护 Rx、Event、MainFunction、TxConfirmation 共享的队列、DAQ 配置和会话状态,定义 ISR/任务可调用关系。
- 异步 Checksum 状态机:分片读取、可取消、可查询进度,并限制最坏执行时间。
- 标定页一致性与掉电保存:硬件映射回读、Working/Reference Page 状态一致、NVM 提交流程与掉电恢复。
- A2L/ELF 版本一致性:构建产物绑定版本、地址自动校验、工具侧拒绝不匹配文件。
- 总线负载与 DAQ 预算:按位填充、仲裁、周期、抖动和最坏峰值计算容量,设计 CRM 优先级和降载策略。
- MISRA、静态分析、单元测试和故障注入:覆盖边界长度、地址溢出、跨区访问、队列满、迟到确认、超时重连、非法 DAQ 下标和并发重配置。
十、落地检查清单
- 先确定工具与 ECU 的 Station Address、CRO/DTO CAN ID、字节序和地址扩展规则;本文值都只是示例。
- 把 A2L 暴露的每段地址转换成最小权限的
MemoryRegion,禁止把整个 MCU 地址空间放进白名单。 - 接通 Rx、1 ms MainFunction、10/100 ms Event 和 TxConfirmation 四个入口。
- 先验证 CONNECT 与 MTA 读写,再配置单 ODT 的 4+2+1 DTO,最后才扩大 DAQ 数量和频率。
- 注入越界地址、长度 0/6、Entry 下标等于上限、ODT 超过 7 字节、队列满和超时断线,确认均可控失败。
- 启用量产安全、并发保护、总线预算、版本一致性和持久化方案后,再把它称为产品级协议栈。
一个可移植 CCP 从站的关键并不在命令 switch 写得多长,而在接口契约是否稳定:协议只接受受控虚拟地址,DAQ 只读取已验证 Entry,发送缓冲区活到硬件确认,会话结束能停止数据流并释放通道。守住这些边界,后续替换 CAN 驱动、内存布局或标定页硬件时,协议核心才不需要跟着重写。
评论