BMS1–BMS基础(硬件电路+软件配置 原理 + 项目实例)
BMS2–BMS底软HVIL (硬件电路+软件配置 原理 + 项目实例)[万字图解 含ADC PWM Port配置及原理详解]
BMS3–BMS充电管理(国标+原理+项目)
AutoSar1–先导篇 AutoSar 模块及分层开发活动详解
AutoSar2–BMS编译调试工具介绍
AutoSar3–Polyspace 代码常犯错误汇总
AutoSar4–AUTOSAR平台OS模块配置
AutoSar5–CAN通信栈(Can通讯原理+配置+实战项目需求)
动力总成1 OBC DCDC(万字原理+BMS VCU交互+国标详解)
C代码规范及AutoSar架构BUG分享
- 一、C代码规范
- 1 宏定义
- 1.1 #define X (0u)/(0x00u)
- 2 函数定义
- 2.1定义函数的AUTOSAR宏FUNC(void, X_CODE)X()
- 二、BUG分享
- 1. NM休眠唤醒插枪防呆策略
- 2. 定时器外部清零规范及定时器历史累加问题
- 嵌入式软件定时器的底层原理
- 定时器耦合的三大设计流派
- 符合 AUTOSAR 与主机厂规范的定时器军规
一、C代码规范
1 宏定义
1.1 #define X (0u)/(0x00u)
举例:
Infineon AURIX2G 平台 Std_Type.h
/******************************************************************************* ** ** ** Copyright (C) Infineon Technologies (2018) ** ** ** ** All rights reserved. ** ** ** ** This document contains proprietary information belonging to Infineon ** ** Technologies. Passing on and copying of this document, and communication ** ** of its contents is not permitted without prior written authorization. ** ** ** ******************************************************************************** ** ** ** FILENAME : Std_Types.h ** ** ** ** VERSION : 2.0.0 ** ** ** ** DATE : 2018-03-26 ** ** ** ** VARIANT : NA ** ** ** ** PLATFORM : Infineon AURIX2G ** ** ** ** AUTHOR : DL-AUTOSAR-Engineering ** ** ** ** VENDOR : Infineon Technologies ** ** ** ** DESCRIPTION : AUTOSAR Standard Tyes ** ** ** ** SPECIFICATION(S) : AUTOSAR_SWS_StandardTypes.pdf, AUTOSAR Release 4.2.2 ** ** ** ** MAY BE CHANGED BY USER : no ** ** ** *******************************************************************************/#ifndefSTD_TYPES_H#defineSTD_TYPES_H/* Traceability: [cover parentID={9B86C499-F591-4d3c-8A6C-5ED8FCAD72A1}] Include protection [/cover]*//******************************************************************************* ** Includes ** *******************************************************************************//* Traceability: [cover parentID={B6DD2FBA-E4D7-4e6f-9065-124B8D8B5F50}] Files inclusion [/cover]*/#include"Compiler.h"#include"Platform_Types.h"/******************************************************************************* ** Global Macro Definitions ** *******************************************************************************//* Published information */#defineSTD_TYPES_VENDOR_ID(17u)#defineSTD_TYPES_AR_RELEASE_MAJOR_VERSION(4u)#defineSTD_TYPES_AR_RELEASE_MINOR_VERSION(2u)#defineSTD_TYPES_AR_RELEASE_REVISION_VERSION(2u)#defineSTD_TYPES_SW_MAJOR_VERSION(1u)#defineSTD_TYPES_SW_MINOR_VERSION(0u)#defineSTD_TYPES_SW_PATCH_VERSION(0u)/* [cover parentID={D3B7A33C-16F3-4536-948B-9A6B08314102}]Std_StatusType */#ifndefSTATUSTYPEDEFINED#defineSTATUSTYPEDEFINED#defineE_OK0x00utypedefunsignedcharStatusType;/* OSEK compliance */#endif/* [/cover] */#defineE_NOT_OK0x01u/* [cover parentID={48655FD7-0240-489e-9A11-02B5238A1A86}] STD_HIGH, STD_LOW*/#defineSTD_HIGH0x01u/* Standard HIGH */#defineSTD_LOW0x00u/* Standard LOW *//* [/cover] *//* [cover parentID={80CAA429-4E81-4c18-9C31-F38F97D4B9A2}]STD_ACTIVE, STD_IDLE*/#defineSTD_ACTIVE0x01u/* Logical state active */#defineSTD_IDLE0x00u/* Logical state idle *//* [/cover] *//* [cover parentID={4861BF77-68F1-4433-BF83-505E4DB52C88}]STD_ON, STD_OFF */#defineSTD_ON0x01u/* Standard ON */#defineSTD_OFF0x00u/* Standard OFF *//* [/cover] *//******************************************************************************* ** Global Type Definitions ** *******************************************************************************//* Describes the standard Type Definitions used in the project *//* Standard return type *//* [cover parentID={B22BF253-F56E-4f6e-9EF6-E6F924E968C9}]Std_ReturnType */typedefuint8 Std_ReturnType;/*[/cover]*//* Structure for the Version of the module.This is requested by calling <Module name>_GetVersionInfo() *//* [cover parentID={267E034F-2D9A-4ef5-9453-2D429CAEE5BC}]Std_VersionInfoType*/typedefstruct{uint16 vendorID;uint16 moduleID;uint8 sw_major_version;uint8 sw_minor_version;uint8 sw_patch_version;}Std_VersionInfoType;/*[/cover]*//******************************************************************************* ** Global Constant Declarations ** *******************************************************************************//******************************************************************************* ** Global Variable Declarations ** *******************************************************************************//******************************************************************************* ** Global Function Declarations ** *******************************************************************************/#endif/* STD_TYPES_H*/0u通常用于值在一般整数上下文中概念上仅为“零”的情况。例如,STD_TYPES_SW_MINOR_VERSION和STD_TYPES_SW_PATCH_VERSION是版本号,此时简单使用0即可。
当值表示字节或位掩码时,即使该字节值为零,也更推荐使用0x00u。使用十六进制(0x…)能更明确地表明该值需按字节或位级别解释,或表明其属于一组相关常量(如 0x01u、0x02u、0x04u 等),这些常量均采用十六进制定义。在给定代码片段中,E_OK、STD_LOW和STD_IDLE作为状态指示符,使用0x00u可与其他相关状态码(如E_NOT_OK 0x01u或STD_HIGH 0x01u)保持十六进制表示的一致性。
编码风格与一致性:在AUTOSAR基础软件(BSW)模块等大型项目中,通常存在严格的编码规范(如MISRA C)和既定惯例。
综上所述,尽管0u与0x00u在数值和类型上等价,但使用0x00u通常意味着十六进制表示能提升可读性——尤其在处理字节级值、状态码或位掩码时——并有助于在模块内保持一致的编码风格。
2 函数定义
2.1定义函数的AUTOSAR宏FUNC(void, X_CODE)X()
举例:
FUNC(void,X_CODE)X_vidSchedulerInit(uint8 u8X){#if(ASW_DEBUG==STD_ON)X_u8X=u8X;#else(void)u8X;#endifreturn;}FUNC(void, X_CODE):此为定义函数的AUTOSAR宏。区别于一般FUNC(void)函数
X_CODE:指定函数可执行代码的存储区域。确保X组件的所有代码集中存储于微控制器内存的特定区域。
X_vidSchedulerInit(uint8 u8X):函数签名。
二、BUG分享
1. NM休眠唤醒插枪防呆策略
整车厂(OEM)关于“300秒”的真实策略是什么?
在新能源汽车国标(GB/T)和各大主机厂(如奇瑞、吉利、长安等)的电源管理策略中,这个300秒(5分钟)被称为“插枪防呆超时(CC Wakeup Timeout)”。
它的设计初衷是这样的:
场景 A(防呆): 车主把充电枪插上去了(触发 CC 唤醒 ECU),但车主去跟人聊天了,迟迟不刷卡/扫码启动充电。如果不休眠,OBC和VCU一直傻醒着,几个小时就把小电瓶(12V)耗亏电了。所以策略规定:光插枪不干活,只给你5分钟(300s)的等待时间,超时立刻休眠。
场景 B(正常充电结束): 车主插枪,10秒后刷卡开始充电。充了2个小时(早就远超300s了)。现在充满电了,VCU 撤销了充电请求(Req -> No Req)。此时,系统应该立刻休眠,还是再等300s?
答案是:应该立刻休眠(严格说是走完 NM 标准退网时间后休眠)。
为什么? 因为充电这个“活儿”已经干完了,高压已经断开了,这时候每多醒一秒钟都是在浪费 12V 蓄电池的电量,没有任何意义再等 5 分钟。
问题代码:
/* 原有错误代码逻辑简述 */if(FALSE==Loc_bCC_Connected){// 没插枪:允许休眠,清零300s计时器TIMER_vidClearTimer(&CombSatMach_pstrAutoTimer[TMR_NMREQ_2_2_5MIN]);}elseif((ComUsr_u8VcuBMSAcChrgPerm!=TRUE)&&...){// 插了枪,且没有充电加热请求:检查计时器有没有到300sif(TRUE==TIMER_bIsAutoTimeout(&CombSatMach_pstrAutoTimer[TMR_NMREQ_2_2_5MIN])){Loc_bRes2=TRUE;}}else{/* Bug 就在这里!只要有充电/加热请求,就会疯狂清零300s计时器! */TIMER_vidClearTimer(&CombSatMach_pstrAutoTimer[TMR_NMREQ_2_2_5MIN]);}充电/加热请求取消后,DUT多等了300s才休眠
【现象本质】 需求原意是:“300秒”衡量的是插枪后的总闲置时间。只要插上枪超过了300秒,且当前没有充/放电/加热请求,就应该立刻释放网络休眠。 但实测发现:当取消请求(Req -> No Req)时,DUT 并没有立刻休眠,而是重新从 0 开始等了 300 秒!
修复后代码:
if(FALSE==Loc_bCC_Connected){Loc_bRes2=TRUE;TIMER_vidClearTimer(&CombSatMach_pstrAutoTimer[TMR_NMREQ_2_2_5MIN]);}else{/* Requirement 2:if CC connect,Timer count++;Until have no charge/discharge Req,cheak count *//* charge finish->VCU(Req->No Req)->Contactor BK */if((ComUsr_u8VcuBMSAcChrgPerm!=TRUE)&&(ComUsr_u8VcuBMSPreheatKeepWarmAllow!=CSM_u8ALLOWACPREHEAT)&&(ComUsr_u8VcuBMSPreheatKeepWarmAllow!=CSM_u8ALLOWKEEPWARM)){if(TRUE==TIMER_bIsAutoTimeout(&CombSatMach_pstrAutoTimer[TMR_NMREQ_2_2_5MIN])){Loc_bRes2=TRUE;}}}【场景推演:正常充电中】
枪插着(CC Connected)。
VCU发来了充电请求(ComUsr_u8VcuBMSAcChrgPerm == TRUE)。
此时,代码执行到 if(ComUsr_u8VcuBMSAcChrgPerm != TRUE) 时,发现条件为假(False)!
既然外层 if 为假,里面判断300s的逻辑根本就不会执行。
因此,Loc_bRes2 永远是 FALSE。只要它为 FALSE,CSM_bIsNMRelease() 就会返回 FALSE,调用 ComM_RequestComMode(…, COMM_FULL_COMMUNICATION) 持续请求网络,绝对不会休眠。
2. 定时器外部清零规范及定时器历史累加问题
比如需求是充放电中在某一状态维持xx秒,比如充完电后维持在POWERDOWN或者AFTERRUN模式等待电压泄放60s
举例:STANDBY -> POWERDOWN 的 60s 定时器
危险场景:当处于充电(CHARGERING)模式时,单片机跑了几个小时。因为没在 STANDBY 状态,代码从未清零定时器 TMR_STANDBY_TO_POWERDOWN_LAST_60s。等充电结束一进入 STANDBY,一查定时器发现已经几小时(>60s)了,状态机会瞬间下电闪穿,完全失去 60s 的泄放防抖缓冲作用!
修复方法:只要不在 STANDBY 模式,必须在外部将其清零。
/* External Reset Timer: if not in STANDBY mode,clear 60s Timer */if(CSM_u8WorkMode!=CSM_u8_WORKING_STANDBY){TIMER_vidClearTimer(&CombSatMach_pstrAutoTimer[TMR_STANDBY_TO_POWERDOWN_LAST_60s]);}嵌入式软件定时器的底层原理
在 AUTOSAR 应用层中,我们通常不会使用硬件定时器中断(Hardware Timer ISR),而是使用基于 OS Task 调度周期的软件定时器(Software Timer)。
1. 软件定时器的基本构成
typedefstruct{uint8 u8Index;boolean bIsTimeout;boolean bTiming;uint32 u32Timer;uint32 u32Counter;}TIMER_tstrAutoTimer;typedefstruct{uint8 u8Index;boolean bIsTimeout;uint32 u32Timer;uint32 u32Counter;}TIMER_tstrTimer;TIMER_tstrTimer CombSatMach_pstrTimer[TMR2_MAX]={{TMR2_SYS_REBOOT_MORETHAN_1S_FIT,FALSE,0u,CNT_1S_IN_TASK_10MS}};TIMER_tstrAutoTimer CombSatMach_pstrAutoTimer[TMR_MAX]={{TMR_OBC_FAULT_TO_POWERDOWN_LAST_5s,FALSE,FALSE,0u,CNT_5S_IN_TASK_50MS},{TMR_OBC_STANDBY_TO_POWERDOWN_LAST_60s,FALSE,FALSE,0u,CNT_60S_IN_TASK_50MS},{TMR_NMREQ_2_2_5MIN,FALSE,FALSE,0u,CNT_300S_IN_TASK_50MS},{TMR_NMREQ_2_3_60s,FALSE,FALSE,0u,CNT_60S_IN_TASK_50MS},{TMR_DIAG_PROG_WAIT_100ms,FALSE,FALSE,0u,CNT_100MS_IN_TASK_50MS},{TMR_OBC_PWRDN_TO_STBY_500ms,FALSE,FALSE,0u,CNT_500MS_IN_TASK_50MS},{TMR_OBC_PWRDN_TO_AFTERRUN_120s,FALSE,FALSE,0u,CNT_60S_IN_TASK_50MS*2}/* 120s */};FUNC(boolean,TIMER_CODE)TIMER_bIsTimeout(TIMER_tstrTimer*pstrTimer){if(pstrTimer!=NULL_PTR){if(pstrTimer->bIsTimeout!=TRUE){if(pstrTimer->u32Timer>=pstrTimer->u32Counter){pstrTimer->bIsTimeout=TRUE;}else{pstrTimer->u32Timer=pstrTimer->u32Timer+1u;}}}returnpstrTimer->bIsTimeout;}FUNC(void,TIMER_CODE)TIMER_vidClearTimer(TIMER_tstrTimer*pstrTimer){if(pstrTimer!=NULL_PTR){pstrTimer->bIsTimeout=FALSE;pstrTimer->u32Timer=0u;}}FUNC(boolean,TIMER_CODE)TIMER_bIsAutoTimeout(TIMER_tstrAutoTimer*pstrTimer){boolean Loc_bRet=FALSE;if(pstrTimer!=NULL_PTR){Loc_bRet=pstrTimer->bIsTimeout;pstrTimer->bTiming=TRUE;}returnLoc_bRet;}FUNC(void,TIMER_CODE)TIMER_vidAutoTimerRuning(TIMER_tstrAutoTimer*pstrTimer){if(pstrTimer!=NULL_PTR){if(pstrTimer->bTiming==TRUE){if(pstrTimer->u32Timer>=pstrTimer->u32Counter){pstrTimer->bIsTimeout=TRUE;}else{pstrTimer->u32Timer=pstrTimer->u32Timer+1u;}}else{pstrTimer->u32Timer=0u;pstrTimer->bIsTimeout=FALSE;}pstrTimer->bTiming=FALSE;}return;}}比如上述示例中 TIMER_tstrAutoTimer 结构体就是一个非常标准的软件定时器封装,它通常包含三个核心元素:
当前计数值(Counter):记录已经过去了多少个周期。
目标阈值(Threshold / Timeout):需要等待的总周期数。比如在 50ms 的 Task 中,等待 5s 的阈值就是 5000 / 50 = 100。
使能/状态标志(Enable/Status Flag):标记这个定时器是在运行中,还是处于空闲/超时状态。
2. “滴答(Tick)”驱动机制
AUTOSAR OS 会按固定的周期(如 10ms, 50ms)调用Runnable。 在这个函数内部调用 TIMER_vidAutoTimerRuning() 时,它只做极其简单的加法:
if(Timer->Enable==TRUE){Timer->Counter++;if(Timer->Counter>=Timer->Threshold){Timer->IsTimeout=TRUE;}}定时器耦合的三大设计流派
为什么代码之前会出 Bug?本质上是因为 C 语言没有原生的面向对象(OOP)特性,无法自动管理状态的“进入(Entry)”和“退出(Exit)”动作。管理软件定时器通常有以下三种流派:
流派 1:局部自理派(初学者常用,不推荐)
在每个状态内部自己维护静态变量。
caseSTATE_POWERDOWN:staticuint16 s_u16Timer=0;s_u16Timer++;if(s_u16Timer>=120){s_u16Timer=0;// 必须手动清零State=STATE_AFTERRUN;}break;致命缺点:如果状态机因为外部中断被强行切走到其他状态(比如发生 FAULT),s_u16Timer 就会保留上次计到一半的值。下次再进入 POWERDOWN 时,就会发生“历史累加闪穿”。
流派 2:事件驱动派(严谨,AUTOSAR 推荐)
严格区分状态的 ENTRY(进入)、DURING(运行中)和 EXIT(退出)动作。
// 当检测到需要切换状态时if(Condition_Powerdown_Met){TIMER_vidClearTimer(&Timer_120s);// ENTRY动作:进入新状态前,把定时器清零State=STATE_POWERDOWN;}// 在处理该状态时caseSTATE_POWERDOWN:TIMER_vidAutoTimerRuning(&Timer_120s);// DURING动作:只在当前状态累加if(TIMER_bIsAutoTimeout(&Timer_120s)){State=STATE_AFTERRUN;}break;优点:逻辑极其清晰,定时器绝对不会在后台乱跑。
流派 3:全局扫盲派(目前工程采用的架构)
把所有的定时器累加动作放到函数的最后统一执行(TIMER_vidAutoTimerRuning),而在特定状态内部只做超时判断。必须配套的防护法则:如果不在此刻,必置为零!这正是我们之前修复 Bug 时补上的逻辑。
// 全局防护墙(外部清零)if(State!=STATE_POWERDOWN){TIMER_vidClearTimer(&Timer_120s);// 只要我不在这个状态,你就必须归零!}符合 AUTOSAR 与主机厂规范的定时器军规
主机厂(如奇瑞、吉利、长城等)在做代码审查(Code Review)和静态分析(QAC/Polyspace)时,对定时器有极严的规范要求:
规范 1:绝对禁止阻塞式延时(No Blocking Delay)
在 AUTOSAR OS 环境下,绝对禁止使用 while(time < 1000) 这种死循环延时。
原因:OS 任务是协同调度或抢占式的,一个 Task 死等会导致同优先级或低优先级任务全部饿死,直接触发 Watchdog(看门狗)复位。
规范 2:魔术数字的消除(No Magic Numbers)
不能在代码里直接写 if(Counter >= 100)。
原因:如果将来 OS Task 的调度周期从 50ms 改成了 10ms,原本的 5s 延时就会变成 1s,引发严重的整车事故。
比如使用宏定义: {TMR_OBC_PWRDN_TO_AFTERRUN_120s, FALSE, FALSE, 0u, CNT_60S_IN_TASK_50MS * 2}。 这使得无论 Task 周期怎么变,只需修改底层的基准宏即可,业务层无需修改。
规范 3:防范整型溢出(Integer Overflow Protection)
如果是使用绝对时间戳计算延时(比如 if(GetTick() > TargetTick)),很容易在 49 天后发生 32位无符号整数溢出导致严重 Bug。
主机厂强制要求:如果必须用系统 Tick,必须使用减法进行无符号回绕计算: if((uint32)(CurrentTick - StartTick) >= TimeoutTick)。
可以通过自定义结构体,当 IsTimeout == TRUE 时停止累加,天然避开了溢出问题。
规范 4:功能安全(ISO 26262)的容错机制
定时器不能成为单点故障源。
Jitter(时间抖动):50ms 的 Task 在重负载下可能会有 ±5ms 的抖动。如果对时间精度要求极高(如逆变器 PWM 控制),不能依赖应用层的 Task 定时器,必须交到底层 MCAL(GPT/GTM 模块)用硬件定时器处理。
失效安全(Fail-Safe):像 POWERDOWN(高压下电)这种涉及人身安全的状态。120s 超时是作为“兜底(Timeout Backup)”存在的。正常的逻辑是:高压降到安全阈值以下,立即下电;只有当主动泄放回路损坏时,才硬等 120s 然后强行下电。这种**“条件达成优先,超时兜底在后”的双重判定,是典型的高等级功能安全设计。
四、 总结:培养“上帝视角”
在阅读和编写包含定时器的状态机代码时,一定要培养出“时间轴上的上帝视角”**:
诞生:它在何时、什么条件下开始计时?(是否被正确复位为0)
存活:它在什么环境下累加?(有没有在不需要它的时候偷偷累加)
死亡/触发:它超时后触发什么动作?触发后是否不再影响后续逻辑?