這段時間折騰stm32與樹莓派之間的can總線通訊遇到了不少問題,樹莓派那端的已經寫在樹莓派外掛MCP2515模塊爬坑記錄裏面了。這次來總結下CAN總線協議和講講stm32如何使用CAN總線。
can總線協議基礎
首先我們來大概看看CAN總線協議是怎樣的。
完整的CAN電路是由CAN控制器和CAN收發器組成的。協議相關的內容由CAN控制器完成。CAN 控制器和CAN收發器用CAN TX和CAN RX兩根線傳輸TTL電平信號。低電平代表二進制的0,高電平代表二進制的1。
CAN H 和CAN L就是CAN總線,所有設備的CAN 收發器都會掛在這兩根線上。數據通過差分信號在這兩個線間傳輸:
- CAN H - CAN L < 0.5V 表示二進制的1
- CAN H - CAN L > 0.9V 表示二進制的0
爲了避免信號的反射和干擾,還需要在CAN H和CAN L之間接上120歐姆的終端電阻。
CAN收發器將CAN控制器通過CAN TX線傳來的二進制碼流轉換爲差分信號用CAN H、CAN L兩根線發送出去。同時接收端設備的CAN接收器會監聽CAN H、CAN L兩根線的差分信號,轉換成二進制碼流通過CAN RX線傳給CAN控制器
位時序
can總線並沒有主從之分,當can總線上的一個設備發送數據時,它以廣播的形式在can總線上發送報文給所有的設備。其他設備通過過濾報文的id,處理自己感興趣的報文。
由於CAN通訊協議並沒有時鐘信號線,所以要求發送端與接收端的波特率是一致的,而can總線的數據發送效率需要我們去自定義。
要設置設置速率,我先要了解CAN的位時序概念。CAN協議把每個bit分成了四個時間段。我們可以用示波器在CAN TX或者CAN RX上量到下面的10101的波形,把每個bit的波形放大其實會有四段時間:
- ss : 同步段(Synchronization Segment)固定爲1Tq
- pts : 傳播段(Propagation Time Segment)1~8Tq
- pbs1 : 相位緩衝段1(Phase Buffer Segment 1)1~8Tq
- pbs2 : 相位緩衝段2(Phase Buffer Segment 2)2~8Tq
每個段的時長單位是Tq(Time Quantum),這個Tq可以由我們去設置,例如設置爲1000ns。
stm32 位時序配置
上面的是標準的CAN總線位時序,具體每一段的意義我沒有深入去了解,但是對於使用來講並不重要。而stm32裏面將pts和pbs1合併了,所以它剩下了三段:
- ss : Synchronization Segment固定爲1Tq
- ts1 : Time segment 1, 即 pts + pbs1
- ts2 : Time segment 2, 即pbs2
由於ss固定爲1Tq,所以我們在STM32CubeMX裏面可以設置的是Tq、ts1和ts2:
Tq並不能直接設置,要通過Prescaler設置分頻去設置。
例如我們將can的時鐘頻率設置爲36MHz:
所以Prescaler設置成36的時候Tq可以這樣計算:
1Tq = 36 MHz / 36 = 1 MHz = 1000 ns
這個時候我們就能去計算一個bit的時間了,如上圖我們把ts1設置爲4,ts2設置爲5,再加上ss固定的1Tq:
1Tq + 4Tq + 5Tq = 10 Tq = 10000 ns = 10 us
波特率爲 1 s / 10 us = 100k
於是我們可以在樹莓派中設置CAN的波特率爲100k:
sudo ip link set can0 up type can bitrate 100000
當然也可以設置每一段的時間:
sudo ip link set can0 up type can tq 1000 prop-seg 3 phase-seg1 1 phase-seg2 5 sjw 4
prop-seg和phase-seg1加起來等於ts1即可。當然有同學會看到還有另外一個sjw(ReSynchronization Jump Width)的參數,這個時間是用於同步的不影響波特率,範圍是1~4Tq,我這裏設置成4Tq。
標準幀與拓展幀
如此配置之後樹莓派就能接收到stm32通過CAN總線發送的數據了,發送的代碼如下:
HAL_StatusTypeDef Can_TxMessage(uint8_t ide,uint32_t id,uint8_t len,uint8_t *data)
{
uint32_t TxMailbox;
CAN_TxHeaderTypeDef CAN_TxHeader;
HAL_StatusTypeDef HAL_RetVal;
uint16_t i=0;
if(ide == 0)
{
CAN_TxHeader.IDE = CAN_ID_STD;
CAN_TxHeader.StdId = id;
}
else
{
CAN_TxHeader.IDE = CAN_ID_EXT;
CAN_TxHeader.ExtId = id;
}
CAN_TxHeader.DLC = len;
CAN_TxHeader.RTR = CAN_RTR_DATA;
CAN_TxHeader.TransmitGlobalTime = DISABLE;
while(HAL_CAN_GetTxMailboxesFreeLevel(&hcan) == 0)
{
i++;
if(i>0xfffe)
{
return HAL_ERROR;
}
}
HAL_Delay(500);
HAL_RetVal = HAL_CAN_AddTxMessage(&hcan,&CAN_TxHeader,data,&TxMailbox);
if(HAL_RetVal != HAL_OK)
{
return HAL_ERROR;
}
return HAL_OK;
}
// 發送數據
uint8_t data[8]={170,170,170,170,170,170,170,170};
Can_TxMessage(0,0x222,8,data);
Can_TxMessage的第一個參數可以配置CAN報文是標準幀還是拓展幀。它們其實基本只有id的長度不一樣而已。這個id就是上面我們提到的用於過濾CAN廣播的標識符。
標準幀的id有11位,這11位被命名爲STDID。拓展幀在標準幀的基礎上增加了18位所以有29位,這個拓展的18位被命名爲EXID。
stm32 CAN id過濾器
stm32 提供了一組過濾器,可以用於過濾CAN報文,只要符合某一個過濾器的規則,該報文即被接收。
過濾器過濾報文有兩種模式: 列表模式與掩碼模式
掩碼模式
掩碼模式下我們需要配置屏蔽寄存器和標識符寄存器,屏蔽寄存器用於配置需要匹配的CAN id的比特位。屏蔽碼寄存器某位爲1表示接收到的CAN ID對應的位必須和標識符寄存器對應的位相同。
例如我們將屏蔽碼寄存器配置爲0xF,意味着我們只關心CAN ID二進制的後4位,此時再將標識符寄存器配置爲0xa,意味着所有二進制後四位爲1010的CAN ID都能能被接收(例如0xa/0xaa/0xffa等)。
0000 0000 ffff # 掩碼寄存器
0000 0000 1010 # 標識符寄存器
--------------
0000 0000 1010 # 0xa
0000 1010 1010 # 0xaa
1111 1111 1010 # 0xffa
原理是這個原理,但是是stm32的配置還是需要了解一下的。雖然CAN報文的id長度只有標準幀的11位或者拓展幀的29位,但是stm32中卻是用了16位寬或者32位寬的寄存器去保存掩碼和標識符。所以會有除了id和mask之外還會有其他的位需要配置。
32位寬的掩碼模式
我們先來看下面這附圖,它說明了32位寬的掩碼模式的寄存器每一位的作用:
id和mask皆由4個字節組成,第一個字節存放了STDID的10~3bit,第二個字節放了STDID的2~0bit還有EXID的17~13bit,第三個字節放了EXID的12~5bit,第四個字節放了EXID的4~0bit、IDE(擴展幀標識)、RTR(遠程幀標誌)和一個預留的0。
我們在代碼中通過FilterMaskIdHigh、FilterIdLow、FilterMaskIdHigh、FilterMaskIdLow去設置掩碼和標識符:
uint32_t ext_id =0xa;
uint32_t mask =0xf;
CAN_FilterTypeDef CAN_FilterType;
// 過濾器的id,STM32F072RBTx提供了14個過濾器所以id可以配置成0~13
CAN_FilterType.FilterBank=0;
// 設置位寬爲32位
CAN_FilterType.FilterScale=CAN_FILTERSCALE_32BIT;
// 設置爲掩碼模式
CAN_FilterType.FilterMode=CAN_FILTERMODE_IDMASK;
// 設置前兩個字節的STDID[10:3]、STDID[2:0]、EXID[17:13]
CAN_FilterType.FilterIdHigh=((ext_id<<3) >>16) &0xffff;
// 設置後兩個字節的EXID[12:5]、EXID[4:0]、IDE、RTR、預留的一個0
CAN_FilterType.FilterIdLow=(ext_id<<3) | CAN_ID_EXT;
// 設置掩碼前兩個字節,左移3位再或CAN_ID_EXT是因爲最後的三位並不是ID,而是IDE、RTR和預留的0
CAN_FilterType.FilterMaskIdHigh=((mask<<3|CAN_ID_EXT)>>16)&0xffff;
// 設置掩碼後兩個字節,左移3位再或CAN_ID_EXT是因爲最後的三位並不是ID,而是IDE、RTR和預留的0
CAN_FilterType.FilterMaskIdLow=(mask<<3|CAN_ID_EXT)&0xffff;
// 將消息放到FIFO0這個隊列裏
CAN_FilterType.FilterFIFOAssignment=CAN_RX_FIFO0;
// 激活過濾器
CAN_FilterType.FilterActivation=ENABLE;
// 設置過濾器
if(HAL_CAN_ConfigFilter(&hcan,&CAN_FilterType)!=HAL_OK)
{
Error_Handler();
}
16位寬的掩碼模式
16位寬的寄存器示意圖如下:
id和mask都是兩個字節,但是真正使得標準id起作用的只有第一個字節和第二個自己的前3位。這裏各只用了兩個字節,也就是說一個過濾器可以設置兩組id和mask,FilterMaskIdHigh和FilterMaskIdHigh一組FilterIdLow和FilterMaskIdLow一組:
uint32_t std_id1 =0xa;
uint32_t mask1 = 0xf;
uint32_t std_id2 =0xbb;
uint32_t mask2 = 0xff;
CAN_FilterTypeDef CAN_FilterType;
// 過濾器的id,STM32F072RBTx提供了14個過濾器所以id可以配置成0~13
CAN_FilterType.FilterBank=0;
// 設置位寬爲16位
CAN_FilterType.FilterScale=CAN_FILTERSCALE_16BIT;
// 設置爲掩碼模式
CAN_FilterType.FilterMode=CAN_FILTERMODE_IDMASK;
// 設置第一組的id,左移5位是因爲最後的5bit是RTR、IDE和EXID[17:15]
CAN_FilterType.FilterIdHigh=(std_id1<<5) | CAN_ID_STD;
// 設置第一組的mask,左移5位是因爲最後的5bit是RTR、IDE和EXID[17:15]
CAN_FilterType.FilterMaskIdHigh= ((mask1<<5)|CAN_ID_STD)
// 設置第二組的id,左移5位是因爲最後的5bit是RTR、IDE和EXID[17:15]
CAN_FilterType.FilterIdLow=(std_id2<<5)|CAN_ID_STD;
// 設置第二組的mask,左移5位是因爲最後的5bit是RTR、IDE和EXID[17:15]
CAN_FilterType.FilterMaskIdLow=(mask2<<5|CAN_ID_STD);
// 將消息放到FIFO0這個隊列裏
CAN_FilterType.FilterFIFOAssignment=CAN_RX_FIFO0;
// 激活過濾器
CAN_FilterType.FilterActivation=ENABLE;
// 設置過濾器
if(HAL_CAN_ConfigFilter(&hcan,&CAN_FilterType)!=HAL_OK)
{
Error_Handler();
}
列表模式
列表模式意味着我們將想要接收的CAN id直接配置到過濾器。
32位寬的列表模式
32位寬的列表模式下,可以設置兩個id,FilterMaskIdHigh和FilterMaskIdHigh一個,FilterIdLow和FilterMaskIdLow一個:
uint32_t ext_id1 =0xa;
uint32_t ext_id2 =0xbb;
CAN_FilterTypeDef CAN_FilterType;
// 過濾器的id,STM32F072RBTx提供了14個過濾器所以id可以配置成0~13
CAN_FilterType.FilterBank=0;
// 設置位寬爲32位
CAN_FilterType.FilterScale=CAN_FILTERSCALE_32BIT;
// 設置爲列表模式
CAN_FilterType.FilterMode=CAN_FILTERMODE_IDLIST;
// 設置第一個id的高字節,左移三位是因爲最後的三位是IDE、RTR和預留的0
CAN_FilterType.FilterIdHigh=((ext_id1<<3)>>16)&0xffff;
// 設置第一個id的低字節,左移三位是因爲最後的三位是IDE、RTR和預留的0
CAN_FilterType.FilterIdLow=((ext_id1<<3)&0xffff)|CAN_ID_EXT;
// 設置第二個id的高字節,左移三位是因爲最後的三位是IDE、RTR和預留的0
CAN_FilterType.FilterMaskIdHigh=((ext_id2<<3)>>16)&0xffff;
// 設置第二個id的低字節,左移三位是因爲最後的三位是IDE、RTR和預留的0
CAN_FilterType.FilterMaskIdLow=((ext_id2<<3)&0xffff)|CAN_ID_EXT;
// 將消息放到FIFO0這個隊列裏
CAN_FilterType.FilterFIFOAssignment=CAN_RX_FIFO0;
// 激活過濾器
CAN_FilterType.FilterActivation=ENABLE;
// 設置過濾器
if(HAL_CAN_ConfigFilter(&hcan,&CAN_FilterType)!=HAL_OK)
{
Error_Handler();
}
16位寬的列表模式
16位寬的列表模式下,可以設置四個id,FilterMaskIdHigh、FilterMaskIdHigh、FilterIdLow和FilterMaskIdLow各一個:
uint16_t ext_id1 =0xa;
uint16_t ext_id2 =0xb;
uint16_t ext_id3 =0xc;
uint16_t ext_id4 =0xd;
CAN_FilterTypeDef CAN_FilterType;
// 過濾器的id,STM32F072RBTx提供了14個過濾器所以id可以配置成0~13
CAN_FilterType.FilterBank=0;
// 設置位寬爲16位
CAN_FilterType.FilterScale=CAN_FILTERSCALE_16BIT;
// 設置爲列表模式
CAN_FilterType.FilterMode=CAN_FILTERMODE_IDLIST;
// 設置第一個id,左移五位是因爲最後的五位是RTR、IDE和EXID[17:15]
CAN_FilterType.FilterIdHigh=(ext_id1<<5)|CAN_ID_STD;
// 設置第二個id,左移五位是因爲最後的五位是RTR、IDE和EXID[17:15]
CAN_FilterType.FilterIdLow=(ext_id2<<5)|CAN_ID_STD;
// 設置第三個id,左移五位是因爲最後的五位是RTR、IDE和EXID[17:15]
CAN_FilterType.FilterMaskIdHigh=(ext_id3<<5)|CAN_ID_STD;
// 設置第四個id,左移五位是因爲最後的五位是RTR、IDE和EXID[17:15]
CAN_FilterType.FilterMaskIdLow=(ext_id4<<5)|CAN_ID_STD;
// 將消息放到FIFO0這個隊列裏
CAN_FilterType.FilterFIFOAssignment=CAN_RX_FIFO0;
// 激活過濾器
CAN_FilterType.FilterActivation=ENABLE;
// 設置過濾器
if(HAL_CAN_ConfigFilter(&hcan,&CAN_FilterType)!=HAL_OK)
{
Error_Handler();
}
接收數據
我們可以看到設置過濾器的時候,會配置將過濾出來的數據放到FIFO0這個隊裏裏面:
// 將消息放到FIFO0這個隊列裏
CAN_FilterType.FilterFIFOAssignment=CAN_RX_FIFO0;
然後我們還有兩步需要操作:
- 激活這個隊列的通知
if(HAL_CAN_ActivateNotification(&hcan,CAN_IT_RX_FIFO0_MSG_PENDING)!=HAL_OK)
{
Error_Handler();
}
- 在STM32CubeMX中使能CAN的接收中斷:
然後就能重寫HAL_CAN_RxFifo0MsgPendingCallback函數去處理接收的數據了:
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
printf("HAL_CAN_RxFifo0MsgPendingCallback\r\n");
CAN_RxHeaderTypeDef CAN_RxHeader;
HAL_StatusTypeDef HAL_Retval;
uint8_t Rx_Data[8];
uint8_t Data_Len = 0;
uint32_t ID = 0;
uint8_t i;
HAL_Retval = HAL_CAN_GetRxMessage(hcan,CAN_RX_FIFO0,&CAN_RxHeader,Rx_Data);
if(HAL_Retval == HAL_OK)
{
Data_Len = CAN_RxHeader.DLC;
if(CAN_RxHeader.IDE)
{
ID = CAN_RxHeader.ExtId;
}
else
{
ID = CAN_RxHeader.StdId;
}
printf("id:%x\r\n",ID);
printf("Data_Len:%d\r\n",Data_Len);
for(i=0;i<8;i++)
{
printf("Rx_Data[%d]=%x\r\n",i,Rx_Data[i]);
}
}
}
NORMAL和LOOPBACK模式
正常模式下設備是收不到自己發送的報文的,我們可以設置LOOPBACK模式實現自發自收,但是注意該模式只用於調試,此時報文其實不會在總線上傳播,所以其他設備是收不到發送的報文的:
//hcan.Init.Mode = CAN_MODE_NORMAL;
hcan.Init.Mode = CAN_MODE_LOOPBACK;
NORMAL模式下收不到數據
我在調CAN總線的最後遇到了這樣一個問題:LOOPBACK模式下能收到自己發的數據,但NORMAL模式下收不到樹莓派發送的數據。
通過示波器測量: stm32的CAN RX可以量到樹莓派發送的數據波形了,波特率是100k,甚至CAN TX都量到有stm32響應的波形。
而且有個奇怪的現象,stm32在loopback下發數據,CAN TX可以量到完整的數據波形,但是如果改成NORMAL模式,就只能量到一個bit的數據。
於是我懷疑是收發器哪裏出問題了,導致發送失敗,在接收到數據的時候由於發送響應失敗,導致接收的流程也斷掉了,沒有去到回調函數那裏。
最終定位到收發器TJA1042T的STB腳沒有拉低,導致收發器處於Standby模式:
除了這個原因之外在網上也有看到這種情況的其他可能原因:
迴環下應該與GPIO無關
GPIO是否初始化正確,時鐘啓用
是否複用,AFIO時鐘是否啓用
迴環下是否有CAN_Tx應該有輸出
終端電阻是否有
CAN收發器電路電壓是否正常
波特率是否標準
換塊板試一下