标签 AUTOSAR 下的文章

MCU 的两种重启姿态

在嵌入式系统中,MCU 的重启不是简单的"断电再来"。冷启动和热启动是两种完全不同的机制,它们对系统状态的影响、恢复速度、以及适用场景都有本质区别。搞混这两种重启,轻则系统行为异常,重则丢失关键数据。

冷启动(Cold Boot)是 MCU 从完全断电状态上电,所有寄存器、SRAM、外设状态都被复位到默认值。热启动(Warm Boot)则是 MCU 在保持供电的情况下触发复位,部分寄存器和 SRAM 内容可能被保留。

从设计意图看,冷启动是"从零开始",热启动是"带着记忆重启"。两者的核心差异在于:冷启动必须完整执行所有初始化流程,而热启动可以跳过部分初始化,快速恢复到工作状态。

硬件层面的差异

STM32 的复位源决定了启动类型。通过查看 RCC_CSR 寄存器的复位标志位,可以判断上次重启的原因:

// 实测于 STM32F103C8T6, STM32Cube HAL 1.17.0
void CheckResetSource(void) {
    if (RCC_CSR & RCC_CSR_PINRSTF) {
        // NRST 引脚复位 → 冷启动
        printf("Reset by NRST pin\n");
    }
    if (RCC_CSR & RCC_CSR_PORRSTF) {
        // 上电/掉电复位 → 冷启动
        printf("Reset by POR/PDR\n");
    }
    if (RCC_CSR & RCC_CSR_SFTRSTF) {
        // 软件复位 → 热启动
        printf("Reset by software\n");
    }
    if (RCC_CSR & RCC_CSR_IWDGRSTF) {
        // 独立看门狗复位 → 热启动
        printf("Reset by IWDG\n");
    }
    if (RCC_CSR & RCC_CSR_WWDGRSTF) {
        // 窗口看门狗复位 → 热启动
        printf("Reset by WWDG\n");
    }

    // 清除复位标志
    RCC_CSR |= RCC_CSR_RMVF;
}

关键点:NRST 引脚复位和上电复位一定是冷启动,而软件复位和看门狗复位通常是热启动。但这个"通常"有例外——如果看门狗复位时 VDD 掉电又恢复,实际效果等同于冷启动。

SRAM 保留机制

热启动最核心的价值在于 SRAM 数据保留。STM32F1 系列在热启动时,SRAM 内容会被保留(前提是 VDD 保持在有效范围内)。这意味着你可以在 SRAM 中保存关键状态,重启后恢复。

但这里有个坑:SRAM 保留不是无条件的。手册规定 VDD 必须保持在 1.8V 以上(STM32F103 的最低工作电压),否则 SRAM 内容会丢失。在实际产品中,电源跌落的时序很难精确控制,所以不能 100% 依赖 SRAM 保留。

// 实测于 STM32F103C8T6
// 在 SRAM 末尾预留 1KB 用于状态保存
#define SRAM_PERSIST_ADDR  ((uint32_t)0x20003C00)  // SRAM 末尾 1KB
#define SRAM_PERSIST_SIZE  1024

typedef struct {
    uint32_t boot_count;
    uint32_t last_error;
    uint8_t  config[64];
} SystemState;

SystemState* pState = (SystemState*)SRAM_PERSIST_ADDR;

void SaveState(void) {
    pState->boot_count++;
    // 注意:这里没有写入 Flash,因为 Flash 写入速度慢且有寿命限制
}

void RestoreState(void) {
    if (pState->boot_count > 0) {
        // 热启动,恢复状态
        printf("Warm boot, count: %lu\n", pState->boot_count);
    } else {
        // 冷启动,初始化状态
        printf("Cold boot, initializing\n");
        memset(pState, 0, SRAM_PERSIST_SIZE);
        pState->boot_count = 1;
    }
}

实测记录

理论预期:热启动后 SRAM 内容保留,boot_count 递增。

实际结果:
- 正常热启动(软件复位):SRAM 保留,boot_count 正确递增
- 快速断电恢复(< 10ms):SRAM 部分保留,boot_count 有时递增有时归零
- 慢速断电(> 100ms):SRAM 丢失,等同于冷启动

偏差分析:快速断电时 SRAM 保留不稳定,怀疑是电源跌落过程中 VDD 在临界值附近波动,导致部分 SRAM 单元数据丢失。

AUTOSAR 中的应用

在 AUTOSAR 架构中,冷启动和热启动的处理有明确规范。ECU 状态管理器(EcuM)负责管理启动流程:

  • 冷启动:执行完整的 ECU 初始化,包括所有 BSW 模块的初始化
  • 热启动:跳过部分初始化,直接恢复到上次运行状态

实际项目中,热启动通常用于看门狗复位后的快速恢复。比如一个正在处理 CAN 报文的 ECU,如果看门狗触发复位,热启动可以在几十毫秒内恢复通信,而冷启动可能需要几百毫秒。

但热启动有个风险:如果上次运行状态本身就有问题(比如内存泄漏、状态机卡死),热启动会带着这些问题重启,可能再次触发看门狗。所以 AUTOSAR 规定,连续热启动超过 N 次(通常 3 次)后,必须执行冷启动。

选型建议

场景 推荐启动类型 理由
首次上电 冷启动 无历史状态可恢复
看门狗复位 热启动 快速恢复通信
软件异常复位 热启动 保留调试信息
电源跌落后恢复 冷启动 SRAM 可能已丢失
量产测试 冷启动 确保每次测试条件一致

遗留问题

  • STM32L4 系列支持 SRAM 保留模式(SRAM2 在低功耗模式下可保持),但 STM32F1 不支持。不同系列的 SRAM 保留机制差异较大,后续需要对比验证。
  • 热启动时外设状态如何恢复?手册说 GPIO 会复位到默认状态,但 DMA 和定时器的行为未明确说明,待实测确认。

关联笔记

关联笔记:STM32 ADC 采样时间计算:从手册到实测 中关于 ADC 初始化时序的讨论

ADC 采样时间的本质

在嵌入式系统中,ADC(模数转换器)是连接模拟世界和数字世界的桥梁。而采样时间,决定了这座桥梁的"宽度"——它直接影响测量精度和转换速度之间的权衡。

很多工程师在配置 STM32 ADC 时,只是照着例程改个采样周期值,却很少去算这个值到底合不合适。结果就是:低阻抗信号源用 1.5 个周期也能跑,高阻抗信号源用 239 个周期还是测不准。

采样时间的本质是:ADC 内部采样保持电路需要多长时间来"抓住"输入电压。这个时间取决于两个因素:信号源内阻和 ADC 内部采样电容。信号源内阻越大,需要的采样时间越长。

STM32F1 系列的 ADC 采样时间可配置为 1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5 个时钟周期。注意,这些数字不是随便给的,每个值对应不同的采样电容充电时间。

计算公式与手册依据

根据 STM32F1 参考手册(RM0008, Rev 21, Page 245),采样时间的计算公式为:

T_S ≥ (R_AIN + R_ADC) × C_ADC × ln(2^(N+2))

其中:
- T_S:采样时间
- R_AIN:信号源内阻(需要外部计算或测量)
- R_ADC:ADC 内部开关电阻(约 1kΩ,手册给出)
- C_ADC:ADC 内部采样电容(约 8pF,手册给出)
- N:ADC 分辨率(12 位)

简化后:对于 12 位 ADC,ln(2^14) ≈ 9.7,所以:

T_S ≥ (R_AIN + 1kΩ) × 8pF × 9.7

举个例子:如果信号源内阻是 10kΩ,那么:

T_S ≥ (10kΩ + 1kΩ) × 8pF × 9.7 ≈ 853ns

而 STM32F1 的 ADC 时钟最大为 14MHz(72MHz/6),每个周期约 71.4ns。所以需要的采样周期数为:

853ns / 71.4ns ≈ 12 个周期

这时候选 13.5 个周期就刚好够用。如果选 1.5 个周期(约 107ns),采样时间只有需求的 1/8,测量结果肯定会偏低。

实测验证

我用 STM32F103C8T6 做了一个简单测试。硬件配置:

  • ADC1,通道 0(PA0)
  • 外部接一个 10kΩ 电阻到 3.3V(模拟高阻抗信号源)
  • 参考电压 3.3V
// 实测于 STM32F103C8T6, STM32Cube HAL 1.17.0
void ADC_Config(void) {
    ADC_InitTypeDef ADC_InitStruct = {0};

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

    ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStruct.ADC_ScanConvMode = DISABLE;
    ADC_InitStruct.ADC_ContinuousConvMode = DISABLE;
    ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStruct.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &ADC_InitStruct);

    // 配置采样时间:239.5 个周期
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5);

    ADC_Cmd(ADC1, ENABLE);

    // 校准
    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));
}

实测记录

理论预期:10kΩ 信号源接 3.3V,ADC 应该读到 4095(12 位满量程)。

实际结果:
- 采样时间 1.5 周期:读数约 3800-3900(偏低 5-7%)
- 采样时间 13.5 周期:读数约 4050-4090(基本准确)
- 采样时间 239.5 周期:读数稳定在 4095

偏差分析:1.5 周期时读数偏低,符合预期——采样电容没充满就开始转换了。239.5 周期时读数稳定,但转换时间变长(约 17μs vs 1.5 周期的 0.1μs)。

(这里我一开始以为只要选最大采样时间就行,后来发现采样时间太长会影响转换速率,多通道扫描时尤其明显。)

选型建议

根据信号源阻抗选择采样时间:

信号源内阻 推荐采样时间 转换时间(单次)
< 1kΩ 1.5 周期 1.5μs
1kΩ - 10kΩ 13.5 周期 2.1μs
10kΩ - 50kΩ 55.5 周期 5.4μs
> 50kΩ 239.5 周期 18.1μs

如果需要高速采样(> 100kHz),信号源内阻必须控制在 1kΩ 以内。否则要么加运放缓冲,要么接受精度损失。

遗留问题

  • 多通道扫描时,不同通道的采样时间能否独立配置?手册说可以,但 HAL 库似乎不支持,待验证。
  • 温度漂移对采样电容的影响有多大?Datasheet 给出了范围但没给典型值,量产时需要实测。

关联笔记

暂未找到强关联笔记

做工业项目时,STM32 需要和 PLC 通信。Modbus RTU 是最常见的工业协议,libmodbus 是最流行的开源实现。但 libmodbus 是为 Linux 设计的,移植到 STM32 需要替换底层硬件操作。

为什么选 libmodbus

  • 成熟稳定,社区活跃
  • 支持 Modbus RTU/TCP
  • API 设计清晰,易于使用
  • 有丰富的示例代码

坑在于:libmodbus 依赖 POSIX API(openreadwrite),STM32 裸机环境下没有这些。

移植思路

libmodbus 的架构分两层:

  • 上层:Modbus 协议处理(CRC 计算、报文组装/解析)
  • 底层:串口收发、定时器、延时

移植的关键是保留上层,替换底层。

第一步:简化源码

# 下载 libmodbus
git clone https://github.com/stephane/libmodbus.git

# 只保留核心文件
src/
├── modbus.c          # Modbus 协议核心
├── modbus.h          # 公共头文件
├── modbus-private.h  # 私有头文件
├── modbus-rtu.c      # RTU 模式实现
├── modbus-rtu.h      # RTU 模式头文件
└── config.h          # 配置文件

删除 TCP 相关文件(modbus-tcp.cmodbus-tcp.h),只保留 RTU 模式。

第二步:配置 config.h

// config.h
#define HAVE_STRLCPY 1
#define HAVE_STRERROR 1
#define HAVE_ARPA_INET_H 0  // STM32 没有
#define HAVE_NETINET_IN_H 0
#define HAVE_SYS_SOCKET_H 0
#define HAVE_WINSOCK2_H 0

坑在于:很多宏默认开启,STM32 环境下要手动关闭。

第三步:替换串口收发

libmodbus 的底层操作在 modbus-rtu.c_step 函数里:

// 原始代码(Linux)
static ssize_t _step(modbus_t *ctx, uint8_t *msg, int msg_length)
{
    return write(ctx->s, msg, msg_length);
}

// 替换为 STM32 HAL
static ssize_t _step(modbus_t *ctx, uint8_t *msg, int msg_length)
{
    modbus_rtu_t *ctx_rtu = ctx->backend_data;
    HAL_StatusTypeDef status;

    // 发送
    status = HAL_UART_Transmit(&huart1, msg, msg_length, 1000);
    if (status != HAL_OK) {
        return -1;
    }

    // 接收
    status = HAL_UART_Receive(&huart1, msg, msg_length, 1000);
    if (status != HAL_OK) {
        return -1;
    }

    return msg_length;
}

第四步:替换延时函数

// 原始代码(Linux)
static void usleep_ms(int ms)
{
    usleep(ms * 1000);
}

// 替换为 STM32 HAL
static void usleep_ms(int ms)
{
    HAL_Delay(ms);
}

第五步:替换互斥锁(可选)

如果是单线程环境(裸机或 FreeRTOS 单任务),可以简化互斥锁:

// 简化为无操作
#define LOCK(ctx)
#define UNLOCK(ctx)

第六步:初始化和使用

#include "modbus.h"

int main(void)
{
    // 初始化硬件
    HAL_Init();
    SystemClock_Config();
    MX_USART1_UART_Init();

    // 创建 Modbus RTU 上下文
    modbus_t *ctx = modbus_new_rtu("/dev/ttyS0", 9600, 'N', 8, 1);
    if (ctx == NULL) {
        printf("无法创建 Modbus 上下文\n");
        return -1;
    }

    // 设置从站地址
    modbus_set_slave(ctx, 1);

    // 连接(RTU 模式不需要,但要调用)
    modbus_connect(ctx);

    // 读取保持寄存器(功能码 0x03)
    uint16_t tab_reg[10];
    int rc = modbus_read_registers(ctx, 0, 10, tab_reg);
    if (rc == 10) {
        printf("读取成功:");
        for (int i = 0; i < 10; i++) {
            printf("%d ", tab_reg[i]);
        }
        printf("\n");
    } else {
        printf("读取失败,错误码:%d\n", rc);
    }

    // 写入单个寄存器(功能码 0x06)
    rc = modbus_write_register(ctx, 0, 12345);
    if (rc == 1) {
        printf("写入成功\n");
    }

    // 关闭连接
    modbus_close(ctx);
    modbus_free(ctx);

    return 0;
}

常见问题

1. CRC 校验失败

// 检查波特率配置
modbus_t *ctx = modbus_new_rtu("/dev/ttyS0", 9600, 'N', 8, 1);
//                                    波特率    校验 数据位 停止位

坑在于:PLC 和 STM32 的波特率、校验位、数据位、停止位必须一致。

2. 超时问题

// 设置超时时间
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 100000;  // 100ms
modbus_set_response_timeout(ctx, &timeout);

3. 从站地址不对

// 设置从站地址(要和 PLC 配置一致)
modbus_set_slave(ctx, 1);  // 从站地址 1

踩坑总结

  1. libmodbus 依赖 POSIX API,STM32 需要替换底层操作
  2. 核心替换:串口收发、延时函数、互斥锁
  3. 保留上层协议处理(CRC、报文组装/解析)
  4. 波特率、校验位、数据位、停止位必须和 PLC 一致
  5. 从站地址要和 PLC 配置一致

从"libmodbus 不能用"到"STM32 和 PLC 通信成功",只需要替换底层操作。坑在于:很多人被 POSIX API 吓退了,其实替换量不大。

做嵌入式项目时,I2C 通信总是不稳定,有时候能通有时候不通。排查了半天,最后发现是开漏输出没加上拉电阻。这个问题很常见,90% 的工程师都忽略了这一点。

问题现象

// 配置 GPIO 为开漏输出
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_6;  // SCL
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;  // 开漏输出
GPIO_InitStruct.Pull = GPIO_NOPULL;  // 没有上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

I2C 通信时好时坏,有时候能通,有时候直接卡死。用逻辑分析仪看波形,发现 SCL 线有时候拉不上去。

原因分析

说白了,就是开漏输出的特性决定的。

推挽输出(Push-Pull)

  • 可以主动输出高电平和低电平
  • 内部有两个 MOS 管,一个拉高,一个拉低
  • 不需要外部上下拉电阻

开漏输出(Open-Drain)

  • 只能主动输出低电平
  • 高电平是靠外部上拉电阻拉上去的
  • 如果没有上拉电阻,高电平就是浮空状态

坑在于:开漏输出的"高电平"不是 MCU 主动输出的,而是靠外部上拉电阻拉上去的。如果没有上拉,高电平就是浮空,电平不确定。

解决方案

方案一:配置内部上拉

// 配置 GPIO 为开漏输出 + 内部上拉
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_6;  // SCL
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;  // 开漏输出
GPIO_InitStruct.Pull = GPIO_PULLUP;  // 内部上拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

坑在于:内部上拉电阻通常在 20kΩ-50kΩ,阻值比较大。对于 I2C 这种需要快速上升沿的场景,内部上拉可能不够。

方案二:外部上拉电阻(推荐)

// 在硬件上加外部上拉电阻
// SCL 线:4.7kΩ 上拉到 VCC
// SDA 线:4.7kΩ 上拉到 VCC

I2C 协议推荐使用 4.7kΩ 上拉电阻。如果总线上设备多或者线长,可以适当减小阻值(比如 2.2kΩ)。

方案三:根据场景选择

// 场景 1:普通 GPIO 控制 LED
// 推挽输出,不需要上下拉
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;

// 场景 2:I2C 通信
// 开漏输出 + 外部上拉
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;  // 用外部上拉

// 场景 3:按键输入
// 输入模式 + 内部上拉
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;

// 场景 4:UART TX
// 推挽输出,空闲状态保持高电平
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;

常见场景配置

I2C 通信

// I2C GPIO 配置
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;  // SCL, SDA
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;  // 开漏输出
GPIO_InitStruct.Pull = GPIO_PULLUP;  // 内部上拉(建议用外部 4.7kΩ)
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

SPI 通信

// SPI GPIO 配置
GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_7;  // SCK, MOSI
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;  // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

UART 通信

// UART TX 配置
GPIO_InitStruct.Pin = GPIO_PIN_9;  // TX
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;  // 推挽输出
GPIO_InitStruct.Pull = GPIO_PULLUP;  // 空闲状态高电平
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

踩坑总结

  1. 开漏输出只能主动拉低,高电平靠外部上拉
  2. I2C 必须用开漏输出 + 上拉电阻
  3. SPI、UART 用推挽输出,不需要上下拉
  4. 内部上拉电阻阻值大,高速通信建议用外部上拉
  5. 记住口诀:"推挽上下拉是白搭,开漏不拉就抓瞎"

这个问题的坑在于:开漏输出的高电平是靠上拉电阻实现的,没有上拉就是浮空。理解了这个原理,配置就很清晰了。

用Python写了个批量下载脚本,可以实现批量爬取AUTOSAR官网标准文档。没有做多线程,异常处理逻辑也基本没有,仅能在网络良好情况下实现基本功能而已。
默认下载R20-11版本,分类保存到D盘根目录,修改categorycategory_names 理论上可以下载其他版本(AUTOSAR官网不改版的前提下),具体category的值可以打开Chrome/Edge浏览器开发者模式找。

- 阅读剩余部分 -