最近在使用can总线,由于这个以前接触的比较少,所以调试代码的时候直接是下载的正点原子的例程,在这个基础上修改调试的。现在将调试中遇到的问题,总结一下,避免以后踩坑。目前写了一个查询方式的,一个中断方式的。项目代码下载地址:
https://download.csdn.net/download/qq_20222919/87793221
查询方式
首先说查询模式,查询模式直接使用原子的例程就可以使用。初始化代码如下:
CAN_HandleTypeDef g_canx_handler; /* CANx句柄 */
CAN_TxHeaderTypeDef g_canx_txheader; /* 发送参数句柄 */
CAN_RxHeaderTypeDef g_canx_rxheader; /* 接收参数句柄 */
uint8_t can_init( uint32_t tsjw, uint32_t tbs2, uint32_t tbs1, uint16_t brp, uint32_t mode )
{
g_canx_handler.Instance = CAN1;
g_canx_handler.Init.Prescaler = brp; /* 分频系数(Fdiv)为brp+1 */
g_canx_handler.Init.Mode = mode; /* 模式设置 */
g_canx_handler.Init.SyncJumpWidth = tsjw; /* 重新同步跳跃宽度(Tsjw)为tsjw+1个时间单位 CAN_SJW_1TQ~CAN_SJW_4TQ */
g_canx_handler.Init.TimeSeg1 = tbs1; /* tbs1范围CAN_BS1_1TQ~CAN_BS1_16TQ */
g_canx_handler.Init.TimeSeg2 = tbs2; /* tbs2范围CAN_BS2_1TQ~CAN_BS2_8TQ */
g_canx_handler.Init.TimeTriggeredMode = DISABLE; /* 非时间触发通信模式 */
g_canx_handler.Init.AutoBusOff = DISABLE; /* 软件自动离线管理 */
g_canx_handler.Init.AutoWakeUp = DISABLE; /* 睡眠模式通过软件唤醒(清除CAN->MCR的SLEEP位) */
//报文自动传送开启后,当数据发送失败时,can芯片会自动重发数据,直到数据发送成功,会造成程序假死状态。
g_canx_handler.Init.AutoRetransmission = DISABLE; /* 禁止报文自动传送 */
g_canx_handler.Init.ReceiveFifoLocked = DISABLE; /* 报文不锁定,数据溢出后新的数据覆盖旧的,如果使能锁定,数据溢出后新的数据会被丢掉 */
g_canx_handler.Init.TransmitFifoPriority = DISABLE; /* 优先级由报文标识符决定 */
if ( HAL_CAN_Init( &g_canx_handler ) != HAL_OK )
{
return 1;
}
CAN_FilterTypeDef sFilterConfig;
/*配置CAN过滤器*/
sFilterConfig.FilterBank = 0; /* 过滤器0 */
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
sFilterConfig.FilterIdHigh = 0x0000; /* 32位ID */
sFilterConfig.FilterIdLow = 0x0000;
sFilterConfig.FilterMaskIdHigh = 0x0000; /* 32位MASK */
sFilterConfig.FilterMaskIdLow = 0x0000;
sFilterConfig.FilterFIFOAssignment = CAN_FILTER_FIFO0; /* 过滤器0关联到FIFO0 */
sFilterConfig.FilterActivation = CAN_FILTER_ENABLE; /* 激活滤波器0 */
sFilterConfig.SlaveStartFilterBank = 14;
/* 过滤器配置 */
if ( HAL_CAN_ConfigFilter( &g_canx_handler, &sFilterConfig ) != HAL_OK )
{
return 2;
}
/* 启动CAN外围设备 */
if ( HAL_CAN_Start( &g_canx_handler ) != HAL_OK )
{
return 3;
}
return 0;
}
void HAL_CAN_MspInit( CAN_HandleTypeDef *hcan )
{
if ( CAN1 == hcan->Instance )
{
CAN_RX_GPIO_CLK_ENABLE(); /* CAN_RX脚时钟使能 */
CAN_TX_GPIO_CLK_ENABLE(); /* CAN_TX脚时钟使能 */
__HAL_RCC_CAN1_CLK_ENABLE(); /* 使能CAN1时钟 */
GPIO_InitTypeDef gpio_init_struct;
gpio_init_struct.Pin = CAN_TX_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
gpio_init_struct.Alternate = GPIO_AF9_CAN1;
HAL_GPIO_Init( CAN_TX_GPIO_PORT, &gpio_init_struct ); /* CAN_TX脚 模式设置 */
gpio_init_struct.Pin = CAN_RX_GPIO_PIN;
HAL_GPIO_Init( CAN_RX_GPIO_PORT, &gpio_init_struct ); /* CAN_RX脚 必须设置成输入模式 */
}
}
接收函数使用查询的方式,在接收函数值会不停的查询FIFO中数据的长度,如果数据长度不为0,说明就接收到了数据。在主函数中不停地读取接收函数返回的数据长度,通过数据长度判断是否接收到了数据。
uint8_t can_receive_msg( uint32_t id, uint8_t *buf )
{
if ( HAL_CAN_GetRxFifoFillLevel( &g_canx_handler, CAN_RX_FIFO0 ) == 0 ) /* 没有接收到数据 */
{
return 0;
}
if ( HAL_CAN_GetRxMessage( &g_canx_handler, CAN_RX_FIFO0, &g_canx_rxheader, buf ) != HAL_OK ) /* 读取数据 */
{
return 0;
}
return g_canx_rxheader.DLC;
}
发送函数
uint8_t can_send_msg( uint32_t id, uint8_t *msg, uint8_t len )
{
uint32_t TxMailbox = CAN_TX_MAILBOX0;
g_canx_txheader.StdId = id; /* 标准标识符 */
g_canx_txheader.ExtId = id; /* 扩展标识符(29位) */
// g_canx_txheader.IDE = CAN_ID_STD; /* 使用标准帧 */
g_canx_txheader.IDE = CAN_ID_EXT; /* 使用扩展帧 */
g_canx_txheader.RTR = CAN_RTR_DATA; /* 数据帧 */
g_canx_txheader.DLC = len;
if ( HAL_CAN_AddTxMessage( &g_canx_handler, &g_canx_txheader, msg, &TxMailbox ) != HAL_OK ) /* 发送消息 */
{
return 1;
}
while (HAL_CAN_GetTxMailboxesFreeLevel(&g_canx_handler) != 3); /* 等待发送完成,所有邮箱为空 */
return 0;
}
在主函数中不停的查询接收数据长度,如果接收到了数据,就把接收到的数据发送出去。
int main( void )
{
uint8_t i = 0, t = 0;
uint8_t cnt = 0;
uint8_t canbuf[8];
uint8_t rxlen = 0;
uint8_t res;
uint8_t mode = 1; /* CAN工作模式: 0,普通模式; 1,环回模式 */
uint16_t count = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init( 336, 10, 2, 7 ); /* 设置时钟,168Mhz */
delay_init( 168 ); /* 延时初始化 */
usart_init( 115200 ); /* 串口初始化为115200 */
// can_init(CAN_SJW_1TQ, CAN_BS2_6TQ, CAN_BS1_7TQ, 6, CAN_MODE_LOOPBACK); /* CAN初始化, 环回模式, 波特率500Kbps 采样点位置占57% */
can_init( CAN_SJW_1TQ, CAN_BS2_6TQ, CAN_BS1_7TQ, 24, CAN_MODE_NORMAL ); /* CAN初始化, 正常模式, 波特率125Kbps 采样点位置占 57.1% */
RE_DE( 1 ); /* 485 设置为发送模式 */
printf( "stm32f407 can test! \r\n" );
while ( 1 )
{
rxlen = can_receive_msg( 0x12, canbuf ); /* CAN ID = 0x12, 接收数据查询 */
if ( rxlen ) /* 接收到有数据 */
{
printf( "接收到数据 %d : ID:0x%08X data: ", count++, g_canx_rxheader.ExtId );
for ( i = 0; i < 8; i++ )
{
printf( "%02X ", canbuf[i] ); /* 输出接收到的数据 */
}
printf( "\r\n" );
can_send_msg( 0x02A300F0 + t, canbuf, 8 ); /* ID = 0x12, 发送8个字节 */
}
t++;
delay_ms( 10 );
if ( t == 200 )
{
t = 0;
cnt++;
for ( i = 0; i < 8; i++ )
{
canbuf[i] = cnt + i; /* 填充发送缓冲区 */
}
// res = can_send_msg(0x12, canbuf, 8); /* ID = 0x12, 发送8个字节 */
}
}
}
这里需要注意一个小问题,在初始化的时候,有一个报文自动传送的的属性g_canx_handler.Init.AutoRetransmission,原子在这里设置的是ENABLE。
经过测试后发现,如果这个属性设置为ENABLE,那么发送数据的过程中,如果硬件电路上出现了故障,比如单片机can口上的连接线松动了,或者是连接线断了。can控制发送数据时就会失败,此时控制器就会一直尝试着继续发送,can口上就会一直有高低电平变化,此时代码就会卡在等待数据发送完成这条语句位置处。
while (HAL_CAN_GetTxMailboxesFreeLevel(&g_canx_handler) != 3);
此时程序就会处于假死状态,如果发送函数后面还要执行其他代码的话,就执行不了了,除非can控制器将数据成功发送出去。
如果不想要这种一直等待发送成功的方式,就将自动传送的属性设置为DISABLE,这样发送数据失败后,还会继续执行下面的代码。程序不会形成假死状态。
g_canx_handler.Init.AutoRetransmission = DISABLE;
中断方式
中断模式原子的代码中虽然有一个宏定义可以开启中断,但是宏定义开启后,中断的功能依然使用不了。
相当于使用中断接收的功能没有实现,于是就自己边查资料边测试,摸索着将中断功能实现了。
首先进行初始化。
CAN_HandleTypeDef g_canx_handler; /* CANx句柄 */
CAN_TxHeaderTypeDef g_canx_txheader; /* 发送参数句柄 */
CAN_RxHeaderTypeDef g_canx_rxheader; /* 接收参数句柄 */
uint8_t can_init( uint32_t tsjw, uint32_t tbs2, uint32_t tbs1, uint16_t brp, uint32_t mode )
{
g_canx_handler.Instance = CAN1;
g_canx_handler.Init.Prescaler = brp; /* 分频系数(Fdiv)为brp+1 */
g_canx_handler.Init.Mode = mode; /* 模式设置 */
g_canx_handler.Init.SyncJumpWidth = tsjw; /* 重新同步跳跃宽度(Tsjw)为tsjw+1个时间单位 CAN_SJW_1TQ~CAN_SJW_4TQ */
g_canx_handler.Init.TimeSeg1 = tbs1; /* tbs1范围CAN_BS1_1TQ~CAN_BS1_16TQ */
g_canx_handler.Init.TimeSeg2 = tbs2; /* tbs2范围CAN_BS2_1TQ~CAN_BS2_8TQ */
g_canx_handler.Init.TimeTriggeredMode = DISABLE; /* 非时间触发通信模式 */
g_canx_handler.Init.AutoBusOff = DISABLE; /* 软件自动离线管理 */
g_canx_handler.Init.AutoWakeUp = DISABLE; /* 睡眠模式通过软件唤醒(清除CAN->MCR的SLEEP位) */
//报文自动传送开启后,当数据发送失败时,can芯片会自动重发数据,直到数据发送成功,会造成程序假死状态。
g_canx_handler.Init.AutoRetransmission = DISABLE; /* 禁止报文自动传送 */
g_canx_handler.Init.ReceiveFifoLocked = DISABLE; /* 报文不锁锁定,FIFO装满后新的覆盖旧的,如果设置报文锁定后,FIFO装满后新的就会被丢弃*/
g_canx_handler.Init.TransmitFifoPriority = DISABLE; /* 优先级由报文标识符决定 */
if ( HAL_CAN_Init( &g_canx_handler ) != HAL_OK )
{
return 1;
}
/* 使用中断接收 */
__HAL_CAN_ENABLE_IT( &g_canx_handler, CAN_IT_RX_FIFO0_MSG_PENDING ); /* FIFO0消息挂号中断允许 */
HAL_NVIC_EnableIRQ( CAN1_RX0_IRQn ); /* 使能CAN中断 */
HAL_NVIC_SetPriority( CAN1_RX0_IRQn, 7, 0 ); /* 抢占优先级7,子优先级0 */
/* HAL_CAN_ActivateNotification() 函数中会调用 __HAL_CAN_ENABLE_IT(hcan, ActiveITs);来开启指定的中断,
所以如果使用了__HAL_CAN_ENABLE_IT()函数开启了中断,就不需要使用HAL_CAN_ActivateNotification()这个函数激活中断 */
/* 如果不使用 __HAL_CAN_ENABLE_IT()函数 也可以单独使用HAL_CAN_ActivateNotification()函数 */ /* 启动CAN1 */
// HAL_CAN_ActivateNotification(&g_canx_handler,CAN_IT_RX_FIFO0_MSG_PENDING); /* 启动CAN接收中断-FIFO0接收新消息*/
CAN_FilterTypeDef sFilterConfig;
/*配置CAN过滤器*/
sFilterConfig.FilterBank = 0; /* 过滤器0 */
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
sFilterConfig.FilterIdHigh = 0x0000; /* 32位ID */
sFilterConfig.FilterIdLow = 0x0000;
sFilterConfig.FilterMaskIdHigh = 0x0000; /* 32位MASK */
sFilterConfig.FilterMaskIdLow = 0x0000;
sFilterConfig.FilterFIFOAssignment = CAN_FILTER_FIFO0; /* 过滤器0关联到FIFO0 */
sFilterConfig.FilterActivation = CAN_FILTER_ENABLE; /* 激活滤波器0 */
sFilterConfig.SlaveStartFilterBank = 14;
/* 过滤器配置 */
if ( HAL_CAN_ConfigFilter( &g_canx_handler, &sFilterConfig ) != HAL_OK )
{
return 2;
}
/* 启动CAN外围设备 */
if ( HAL_CAN_Start( &g_canx_handler ) != HAL_OK )
{
return 3;
}
return 0;
}
中断模式初始化比查询模式初始化多了下面三行代码。
__HAL_CAN_ENABLE_IT( &g_canx_handler, CAN_IT_RX_FIFO0_MSG_PENDING ); /* FIFO0消息挂号中断允许 */
HAL_NVIC_EnableIRQ( CAN1_RX0_IRQn ); /* 使能CAN中断 */
HAL_NVIC_SetPriority( CAN1_RX0_IRQn, 7, 0 ); /* 抢占优先级7,子优先级0 */
首先使用 __HAL_CAN_ENABLE_IT
函数设置中断源,接收中断有下面几种方式。
/* Receive Interrupts */
#define CAN_IT_RX_FIFO0_MSG_PENDING ((uint32_t)CAN_IER_FMPIE0) /*!< FIFO 0 message pending interrupt */
#define CAN_IT_RX_FIFO0_FULL ((uint32_t)CAN_IER_FFIE0) /*!< FIFO 0 full interrupt */
#define CAN_IT_RX_FIFO0_OVERRUN ((uint32_t)CAN_IER_FOVIE0) /*!< FIFO 0 overrun interrupt */
#define CAN_IT_RX_FIFO1_MSG_PENDING ((uint32_t)CAN_IER_FMPIE1) /*!< FIFO 1 message pending interrupt */
#define CAN_IT_RX_FIFO1_FULL ((uint32_t)CAN_IER_FFIE1) /*!< FIFO 1 full interrupt */
#define CAN_IT_RX_FIFO1_OVERRUN ((uint32_t)CAN_IER_FOVIE1) /*!< FIFO 1 overrun interrupt */
这几种中断的差异就不一一解释了,这里使用FIFO0的消息挂起中断。还有一个函数 HAL_CAN_ActivateNotification
也可以设置中断源。
HAL_CAN_ActivateNotification(&g_canx_handler,CAN_IT_RX_FIFO0_MSG_PENDING);
HAL_NVIC_EnableIRQ( CAN1_RX0_IRQn ); /* 使能CAN中断 */
HAL_NVIC_SetPriority( CAN1_RX0_IRQn, 7, 0 ); /* 抢占优先级7,子优先级0 */
其实HAL_CAN_ActivateNotification
函数内部也是通过调用 __HAL_CAN_ENABLE_IT
函数来设置中断源的。
接下来在初始化回调函数中配置IO口。
void HAL_CAN_MspInit( CAN_HandleTypeDef* hcan )
{
if ( CAN1 == hcan->Instance )
{
CAN_RX_GPIO_CLK_ENABLE(); /* CAN_RX脚时钟使能 */
CAN_TX_GPIO_CLK_ENABLE(); /* CAN_TX脚时钟使能 */
__HAL_RCC_CAN1_CLK_ENABLE(); /* 使能CAN1时钟 */
GPIO_InitTypeDef gpio_init_struct;
gpio_init_struct.Pin = CAN_TX_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
gpio_init_struct.Alternate = GPIO_AF9_CAN1;
HAL_GPIO_Init( CAN_TX_GPIO_PORT, &gpio_init_struct ); /* CAN_TX脚 模式设置 */
gpio_init_struct.Pin = CAN_RX_GPIO_PIN;
HAL_GPIO_Init( CAN_RX_GPIO_PORT, &gpio_init_struct ); /* CAN_RX脚 必须设置成输入模式 */
}
}
初始化完成之后,下面就该设置中断函数的入口了,这个中断函数的名称可以在startup_stm32f407xx.s 文件里面找。
在汇编代码的中断向量表里面有中断函数的名称,这里有个CAN1_RX0_IRQHandler 还有个 CAN1_RX1_IRQHandler,那么要使用哪个呢?
在网上找到相关资料如下
经过验证,红线里面圈出来的说法是正确的,同时要注意如果要使用FIFO1,那么程序中所有设置FIFO的地方都得写成FIFO1.
其中包括中断号,中断名称,中断入库函数名称,中断回调函数名称。
由于上面初始化的时候设置的是使用FIFO0,那么这里的函数入口名称就选择CAN1_RX0_IRQHandler。
void CAN1_RX0_IRQHandler( void )
{
/* 调用HAL库 CAN 中断入口函数*/
}
接下来就需要在这个中断入口函数中调用HAL库的通用CAN中断处理函数,那这个函数在哪去找呢? 直接打开 stm32f4xx_hal_can.h
头文件在里面搜索以 IRQHandler
结尾的函数名。
查找后发现这个里面只有一个函数 HAL_CAN_IRQHandler
符合,那么这个函数肯定就是HAL库中CAN中断的通用入口函数了。直接在中断函数里面添加这个通用函数。
void CAN1_RX0_IRQHandler( void )
{
HAL_CAN_IRQHandler( &g_canx_handler ); /* 调用HAL库 CAN 中断入口函数*/
}
接下来根据HAL库的惯例,在中断函数里面肯定还有一个回调函数。接下来就需要去找这个中断回调函数。可以直接跳转到 HAL_CAN_IRQHandler
函数内部进行查看。
但是这个函数内部的代码有些长,找起来不是很方便,那么就可以在 stm32f4xx_hal_can.h
头文件里直接去找。由于回掉函数都是以Callback
结尾的,那么就可以直接在 stm32f4xx_hal_can.h
文件中搜索 Callback
结尾的函数。
可以找到一个Callbacks functions
的注释,那么下面这些函数就是中断回调函数,如果程序写得比较多的时候,可以不用查找的方法,直接用鼠标拖动右边的滚动条就可以定位到回调函数这一块。
但是这么多函数,到底是哪一个呢?别着急,还记得在初始化的时候,使能中断时开启了一个FIFO0的消息挂起中断,可以根据这个中断使能的名字去对比,可以发现在右边HAL_CAN_RxFifo0MsgPendingCallback
这个函数的名字和CAN_IT_RX_FIFO0_MSG_PENDING
名称基本一样,那么HAL_CAN_RxFifo0MsgPendingCallback
这个函数肯定就是FIFO0消息挂起中断的回调函数。
接下来就可以编写中断回调函数了。
void HAL_CAN_RxFifo0MsgPendingCallback( CAN_HandleTypeDef* hcan )
{
uint8_t rxbuf[8];
uint32_t id;
uint8_t i;
HAL_CAN_GetRxMessage( hcan, CAN_RX_FIFO0, &g_canx_rxheader, rxbuf ); /* 读取数据 */
printf( "接收到 %d 位数据 : ID:0x%08X data: ", g_canx_rxheader.DLC, g_canx_rxheader.ExtId );
for ( i = 0; i < 8; i++ )
{
printf( "%02X ", rxbuf[i] ); /* 输出接收到的数据 */
}
printf( "\r\n" );
}
在中断回调函数里面使用HAL_CAN_GetRxMessage
函数读取接收到的数据,并打印出来。到此中断函数的代码就编写完成了。
接下来就是下载验证的过程了,这里就不一一列举了。中断部分的函数,需要注意的地方,在上面基本都分析过了。在调试的过程中,暂时也没发现其他需要注意的地方。