汽车CAN J1939协议完整编程源码和STM32移植指南。J1939是商用车(卡车、工程机械等)的标准高层CAN协议,基于CAN 2.0B扩展帧。
一、J1939协议核心概念
1. 协议栈架构
应用层 (J1939-71) → 车辆参数、诊断消息
传输层 (J1939-21) → 多帧传输、连接管理
网络层 (J1939-81) → 地址管理、命名
数据链路层 → CAN 2.0B扩展帧
物理层 → CAN_H/CAN_L
2. 29位CAN ID结构
| 位域 | 位数 | 说明 | 示例值 |
|---|---|---|---|
| Priority | 3 | 优先级(0-7, 0最高) | 6 |
| Reserved | 1 | 保留位(固定0) | 0 |
| Data Page | 1 | 数据页(0/1) | 0 |
| PDU Format | 8 | PDU格式(0-255) | 0xF0 |
| PDU Specific | 8 | 目标地址或组扩展 | 0x21 |
| Source Address | 8 | 源地址(0-253) | 0x80 |
PGN计算 :PGN = (Data Page << 16) + (PDU Format << 8) + (PDU Specific)
二、推荐开源J1939协议栈
1. Open-SAE-J1939(最完整)
- 语言:ANSI C (C89/C99)
- 特点:符合MISRA C、无动态内存分配、支持多平台
- 功能:传输层、应用层、诊断层、网络管理
- GitHub :
hzappen/Open-SAE-J1939或dav1977/Open-SAE-J1939
2. SupperWater/J1939-Protocol(中文文档)
- 语言:C语言
- 特点:高可移植性、接口封装良好
- Gitee :
SupperWater/J1939-Protocol
3. xiaoyuan-cloud/CAN2.0-J1939协议栈(RTOS支持)
- 语言:C语言
- 特点:支持裸机和RTOS、1ms心跳
- Gitee :
xiaoyuan-cloud/CAN2.0-J1939协议栈
4. STM32专用移植
- 项目 :
qinyuan/Open-SAE-J1939-STM32 - 基础:基于STM32Cube的CAN_Networking示例
- 特点:直接集成到STM32工程
三、Open-SAE-J1939核心源码解析
1. 项目结构
Open-SAE-J1939/
├── Src/
│ ├── Open_SAE_J1939.c # 主协议栈
│ ├── Open_SAE_J1939.h
│ ├── Hardware.c # 硬件抽象层
│ └── Hardware.h
├── Examples/
│ ├── Startup.txt # 启动示例
│ └── Various_Examples.c
└── Documentation/
└── Open SAE J1939.pdf # 协议文档
2. 核心数据结构
c
// Open_SAE_J1939.h
typedef struct {
/* ECU信息 */
uint8_t this_ECU_address; // 本ECU地址
uint8_t this_ECU_name[8]; // 64位NAME
uint32_t this_identifications; // 身份标识
/* 网络管理 */
uint8_t claimed_addresses[256]; // 已声明地址表
uint8_t number_of_claimed_addresses;
/* 传输协议 */
struct {
uint8_t sequence_number;
uint8_t data[1785]; // 最大数据量
uint16_t data_index;
uint16_t data_length;
} tp_data;
/* 消息队列 */
J1939_MESSAGE messages[50];
uint8_t message_index;
} J1939;
3. 关键API函数
c
// 初始化J1939协议栈
void Open_SAE_J1939_Startup_ECU(J1939 *j1939);
// 监听消息(主循环调用)
void Open_SAE_J1939_Listen_For_Messages(J1939 *j1939);
// 发送请求PGN
void Open_SAE_J1939_Send_Request_PGN(J1939 *j1939, uint32_t PGN,
uint8_t destination_address);
// 发送确认消息
void Open_SAE_J1939_Send_ACK(J1939 *j1939, uint8_t control_byte,
uint32_t PGN, uint8_t destination_address);
// 发送数据消息
void Open_SAE_J1939_Send_Data(J1939 *j1939, uint32_t PGN,
uint8_t destination_address,
uint8_t *data, uint8_t data_length);
四、STM32F103完整移植代码
1. 硬件连接
STM32F103C8T6 CAN引脚:
PA11 → CAN_RX
PA12 → CAN_TX
2. CAN驱动配置
c
// can.c
#include "stm32f1xx_hal.h"
CAN_HandleTypeDef hcan;
CAN_FilterTypeDef sFilterConfig;
void CAN_Init(void)
{
// 使能时钟
__HAL_RCC_CAN1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置CAN引脚
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_11 | GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// CAN初始化
hcan.Instance = CAN1;
hcan.Init.Prescaler = 9; // 1MHz时钟,125kbps
hcan.Init.Mode = CAN_MODE_NORMAL;
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan.Init.TimeSeg1 = CAN_BS1_13TQ;
hcan.Init.TimeSeg2 = CAN_BS2_2TQ;
hcan.Init.TimeTriggeredMode = DISABLE;
hcan.Init.AutoBusOff = DISABLE;
hcan.Init.AutoWakeUp = DISABLE;
hcan.Init.AutoRetransmission = ENABLE;
hcan.Init.ReceiveFifoLocked = DISABLE;
hcan.Init.TransmitFifoPriority = DISABLE;
if (HAL_CAN_Init(&hcan) != HAL_OK) {
Error_Handler();
}
// 配置过滤器(接收所有扩展帧)
sFilterConfig.FilterBank = 0;
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
sFilterConfig.FilterIdHigh = 0x0000;
sFilterConfig.FilterIdLow = 0x0000;
sFilterConfig.FilterMaskIdHigh = 0x0000;
sFilterConfig.FilterMaskIdLow = 0x0000;
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.SlaveStartFilterBank = 14;
HAL_CAN_ConfigFilter(&hcan, &sFilterConfig);
// 启动CAN
HAL_CAN_Start(&hcan);
HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
}
3. J1939移植接口
c
// j1939_port.c - 硬件抽象层
#include "Open_SAE_J1939.h"
#include "can.h"
// CAN发送回调(J1939调用)
void Open_SAE_J1939_Send_Message(uint32_t ID, uint8_t data[8], uint8_t data_length)
{
CAN_TxHeaderTypeDef TxHeader;
uint32_t TxMailbox;
// 配置J1939 CAN ID
TxHeader.StdId = 0;
TxHeader.ExtId = ID; // 29位扩展ID
TxHeader.IDE = CAN_ID_EXT; // 扩展帧
TxHeader.RTR = CAN_RTR_DATA; // 数据帧
TxHeader.DLC = data_length; // 数据长度
TxHeader.TransmitGlobalTime = DISABLE;
// 发送CAN帧
if (HAL_CAN_AddTxMessage(&hcan, &TxHeader, data, &TxMailbox) != HAL_OK) {
// 发送失败处理
}
}
// CAN接收回调(中断中调用)
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
CAN_RxHeaderTypeDef RxHeader;
uint8_t RxData[8];
// 读取CAN消息
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RxData) == HAL_OK) {
// 只处理扩展帧
if (RxHeader.IDE == CAN_ID_EXT) {
// 传递给J1939协议栈
Open_SAE_J1939_Read_Message(RxHeader.ExtId, RxData, RxHeader.DLC);
}
}
}
// 获取时间戳(1ms精度)
uint32_t Open_SAE_J1939_Get_Timestamp(void)
{
return HAL_GetTick();
}
// 延时函数
void Open_SAE_J1939_Delay(uint32_t milliseconds)
{
HAL_Delay(milliseconds);
}
4. J1939配置文件
c
// j1939_config.h
#ifndef J1939_CONFIG_H
#define J1939_CONFIG_H
// 硬件选择
#define PROCESSOR_STM32F103
// 功能配置
#define J1939_ACCEPT_CMD_ADDRESS 1 // 接受命令地址
#define J1939_ACCEPT_ADDRESS_CLAIMED 1 // 接受地址声明
#define J1939_ADDRESS_CLAIMING 1 // 启用地址声明
#define J1939_TP_CM 1 // 传输协议连接管理
#define J1939_TP_DT 1 // 传输协议数据传输
// 网络配置
#define J1939_THIS_ECU_NAME {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
#define J1939_THIS_IDENTIFICATIONS 0x12345678
#define J1939_THIS_ECU_ADDRESS 0x80 // 默认地址
// 内存配置
#define J1939_MESSAGE_QUEUE_SIZE 50 // 消息队列大小
#define J1939_TP_BUFFER_SIZE 1785 // TP缓冲区大小
#endif
5. 主应用程序
c
// main.c
#include "stm32f1xx_hal.h"
#include "Open_SAE_J1939.h"
J1939 j1939_ecu;
int main(void)
{
HAL_Init();
SystemClock_Config();
CAN_Init();
// 初始化J1939 ECU
Open_SAE_J1939_Startup_ECU(&j1939_ecu);
// 设置ECU信息
uint8_t name[8] = {0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0};
memcpy(j1939_ecu.this_ECU_name, name, 8);
j1939_ecu.this_identifications = 0x87654321;
j1939_ecu.this_ECU_address = 0x80;
// 启动地址声明
Open_SAE_J1939_Send_Address_Claimed(&j1939_ecu);
while (1) {
// 监听CAN消息
Open_SAE_J1939_Listen_For_Messages(&j1939_ecu);
// 处理接收到的消息
if (j1939_ecu.message_index > 0) {
J1939_MESSAGE msg = j1939_ecu.messages[0];
Process_J1939_Message(&msg);
// 移除已处理消息
for (uint8_t i = 0; i < j1939_ecu.message_index - 1; i++) {
j1939_ecu.messages[i] = j1939_ecu.messages[i + 1];
}
j1939_ecu.message_index--;
}
// 发送发动机转速(示例)
static uint32_t last_send = 0;
if (HAL_GetTick() - last_send > 100) { // 100ms周期
Send_Engine_Speed(&j1939_ecu);
last_send = HAL_GetTick();
}
HAL_Delay(1); // 1ms延时
}
}
// 处理J1939消息
void Process_J1939_Message(J1939_MESSAGE *msg)
{
uint32_t pgn = (msg->ID >> 8) & 0x3FFFF; // 提取PGN
switch (pgn) {
case 0xF004: // 发动机转速
if (msg->data_length >= 8) {
uint16_t rpm = (msg->data[5] << 8) | msg->data[4];
rpm *= 0.125; // 解析为RPM
// 处理转速数据
}
break;
case 0xFEEE: // 地址声明
// 处理地址声明
break;
case 0x00EA00: // 请求PGN
// 处理PGN请求
break;
}
}
// 发送发动机转速(PGN 0xF004)
void Send_Engine_Speed(J1939 *j1939)
{
uint8_t data[8] = {0};
uint16_t rpm = 1500; // 示例:1500 RPM
// 发动机转速数据格式
data[0] = 0xFF; // 发动机实例
data[1] = 0xFF; // 发动机类型
data[2] = 0xFF; // 保留
data[3] = 0xFF; // 保留
data[4] = (rpm / 0.125) & 0xFF; // 转速低字节
data[5] = ((rpm / 0.125) >> 8) & 0xFF; // 转速高字节
data[6] = 0xFF; // 保留
data[7] = 0xFF; // 保留
// 发送消息(优先级6,目标地址全局)
Open_SAE_J1939_Send_Data(j1939, 0xF004, 0xFF, data, 8);
}
五、常用PGN示例代码
1. 发动机参数(PGN 0xF004)
c
// 发送发动机参数
void Send_Engine_Parameters(J1939 *j1939, uint8_t sa)
{
uint8_t data[8];
// 发动机转速(0.125 RPM/bit)
uint16_t rpm = 1500;
data[0] = 0x00; // 发动机实例
data[1] = 0x00; // 发动机类型
data[2] = 0xFF; // 保留
data[3] = 0xFF; // 保留
data[4] = (rpm / 0.125) & 0xFF;
data[5] = ((rpm / 0.125) >> 8) & 0xFF;
data[6] = 0xFF; // 保留
data[7] = 0xFF; // 保留
// 优先级3,PGN 0xF004,目标地址sa
uint32_t can_id = (3 << 26) | (0xF004 << 8) | sa;
Open_SAE_J1939_Send_Message(can_id, data, 8);
}
2. 车辆速度(PGN 0xFEF1)
c
// 发送车辆速度
void Send_Vehicle_Speed(J1939 *j1939, uint8_t sa, float speed_kmh)
{
uint8_t data[8] = {0};
// 速度(1/256 km/h per bit)
uint16_t speed = speed_kmh * 256;
data[0] = speed & 0xFF;
data[1] = (speed >> 8) & 0xFF;
// 其他参数
data[2] = 0xFF; // 保留
data[3] = 0xFF; // 保留
data[4] = 0xFF; // 保留
data[5] = 0xFF; // 保留
data[6] = 0xFF; // 保留
data[7] = 0xFF; // 保留
// 优先级6,PGN 0xFEF1
uint32_t can_id = (6 << 26) | (0xFEF1 << 8) | sa;
Open_SAE_J1939_Send_Message(can_id, data, 8);
}
3. 诊断消息DM1(PGN 0xFECA)
c
// 发送激活的诊断故障码
void Send_DM1_Active_DTCs(J1939 *j1939, uint8_t sa)
{
uint8_t data[8];
// 灯状态
data[0] = 0x00; // 保护灯:关闭
data[0] |= 0x01; // 红灯:激活
data[0] |= 0x02; // 琥珀灯:激活
data[0] |= 0x04; // 保护灯:关闭
data[0] |= 0x08; // 闪烁:关闭
// SPN 190 - 发动机转速
data[1] = 190 & 0xFF; // SPN低字节
data[2] = (190 >> 8) & 0xFF; // SPN中字节
data[3] = (190 >> 16) & 0x07; // SPN高3位
data[3] |= (4 << 3); // FMI 4 - 数据异常
// 发生次数
data[4] = 1; // 发生1次
// 其他
data[5] = 0xFF;
data[6] = 0xFF;
data[7] = 0xFF;
// 优先级6,PGN 0xFECA
uint32_t can_id = (6 << 26) | (0xFECA << 8) | sa;
Open_SAE_J1939_Send_Message(can_id, data, 8);
}
六、多帧传输(TP)示例
1. 发送长消息(>8字节)
c
// 发送多帧数据
void Send_Long_Message(J1939 *j1939, uint8_t da, uint8_t *data, uint16_t length)
{
// 使用传输协议发送
Open_SAE_J1939_Send_TP_Data(j1939, 0x00EB00, da, data, length);
}
// 接收多帧数据回调
void TP_Data_Received_Callback(uint32_t pgn, uint8_t sa, uint8_t *data, uint16_t length)
{
printf("收到长消息,PGN: 0x%06lX,长度: %d\n", pgn, length);
// 处理数据
switch (pgn) {
case 0x00EB00: // 传输协议连接管理
Process_TP_Message(data, length);
break;
}
}
参考代码 汽车CAN1939协议编程源码 www.youwenfan.com/contentcsu/69982.html
七、调试与测试工具
1. PC端测试工具
- CANoe/CANalyzer:Vector公司的专业工具
- PCAN-View:PEAK-System的免费工具
- SavvyCAN:开源CAN分析仪
- CANable:开源USB转CAN适配器
2. STM32调试配置
c
// 调试输出(通过串口)
void J1939_Debug_Print(const char *format, ...)
{
char buffer[256];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
// 通过串口输出
HAL_UART_Transmit(&huart1, (uint8_t*)buffer, strlen(buffer), 1000);
}
// 在协议栈中添加调试
#define J1939_DEBUG 1
#if J1939_DEBUG
#define J1939_PRINT(...) J1939_Debug_Print(__VA_ARGS__)
#else
#define J1939_PRINT(...)
#endif
八、完整工程获取
1. GitHub/Gitee项目
bash
# Open-SAE-J1939
git clone https://github.com/hzappen/Open-SAE-J1939.git
# STM32移植版
git clone https://gitee.com/qinyuan/Open-SAE-J1939-STM32.git
# 中文协议栈
git clone https://gitee.com/SupperWater/J1939-Protocol.git
2. 快速开始步骤
- 选择协议栈 :推荐
Open-SAE-J1939(功能最全) - 移植硬件层 :修改
Hardware.c中的发送/接收函数 - 配置ECU信息:设置NAME、地址等参数
- 集成到工程:添加到STM32CubeIDE或Keil工程
- 测试验证:使用CAN工具验证通信
九、常见问题解决
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 无法接收消息 | CAN过滤器配置错误 | 检查过滤器掩码设置 |
| 地址声明失败 | NAME冲突 | 修改唯一的64位NAME |
| TP传输失败 | 超时设置太短 | 增加TP超时时间 |
| 内存不足 | 消息队列太大 | 减小J1939_MESSAGE_QUEUE_SIZE |
| 实时性差 | 主循环频率低 | 确保1ms调用Listen_For_Messages |
十、进阶功能扩展
1. 支持ISO 11783(农业机械)
c
// 在Open-SAE-J1939基础上添加ISO 11783支持
#include "ISO_11783/ISO_11783-7_Application_Layer/Application_Layer.h"
// 初始化ISO 11783
ISO_11783 iso11783;
Open_SAE_J1939_Startup_ISO_11783(&iso11783);
2. 添加UDS诊断(ISO 14229)
c
// 集成UDS诊断服务
void UDS_Service_Handler(J1939 *j1939, uint8_t *request, uint8_t length)
{
switch (request[0]) {
case 0x10: // 诊断会话控制
Handle_Diagnostic_Session_Control(request, length);
break;
case 0x22: // 按标识符读取数据
Handle_Read_Data_ByIdentifier(request, length);
break;
case 0x2E: // 按标识符写入数据
Handle_Write_Data_ByIdentifier(request, length);
break;
}
}
十一、性能优化建议
-
内存优化:
- 使用静态内存池
- 合理设置队列大小
- 禁用不需要的功能模块
-
实时性优化:
- 使用CAN接收中断
- 减少
Listen_For_Messages中的处理时间 - 优先级消息立即发送
-
代码优化:
- 启用编译器优化(-O2)
- 使用查表法替代计算
- 内联关键函