第0x02講 使用ESP32開發(fā)板進行CAN總線通信

上一講我們使用Arduino DUE進行CAN通信,Arduino DUE進行CAN通信的優(yōu)勢在于,它本身集成了兩個CAN控制器模塊,可以在板內(nèi)實現(xiàn)互發(fā)。本講使用另外一個開發(fā)板,ESP32,其內(nèi)置了總線控制器,同樣可以實現(xiàn)CAN總線通信。

ESP32開發(fā)板

ESP32是樂鑫Espressif旗下的一款集成了32位MCU、2.4Ghz Wi-Fi、藍牙Bluetooth 5 (LE)的開發(fā)板,其系列分為ESP32-S2、ESP32-C3、ESP32-C6、ESP32-H2、ESP32-S3等系列。推薦使用ESP32-S3,理論上其他ESP32系列的開發(fā)也可以。

ESP32-S3配置

以ESP32-S3為例,其主要配置如下,

系列名稱 ESP32-S3
微控制器MCU Xtensa? 32 位 LX7 雙核處理器,主頻高達 240 MHz。
存儲器 512KB SRAM、384KB ROM存儲空間。并支持外部SPI、Dual SPI、Quad SPI、Octal SPI、QPI、OPI Flash和片外RAM。
接口 45個可編程GPIO,支持常見外設接口,如SPI、I2S、I2C、PWM、RMT、ADC、UART、SD/MMC主機控制器和TWAI??控制器等。
安全 基于AES-XTS算法Flash加密和基于RSA算法的安全啟動,數(shù)字簽名和HMAC模塊,“世界控制器(World Controller)“莫模塊。
開發(fā)板 ESP32-S3-DevKitM-1、ESP32-S3-DevKitM-1等
圖1 某款ESP32-S3開發(fā)板
針腳定義

作為開發(fā)板,開發(fā)者最為關心的是針腳定義。ESP32-S3提供了非常豐富的接口。具體如下,

圖2 ESP32-S3針腳定義

ESP32 CAN總線模塊

上一講中,Arduino Due本身集成了CAN控制器模塊,這點反映在Arduino Due的官方文檔里,同時,MCU也從底層角度提供了支持。也許您會有疑問,上一節(jié)關于ESP32的介紹中沒有提到ESP32對CAN總線的支持。

注意在ESP32配置表里,有“TWAI??控制器”,TWAI全稱Two-Wire Automobile Interface,這是樂鑫研發(fā)的通信協(xié)議,對標Bosch的CAN Bus協(xié)議。據(jù)我閱讀兩者的文檔,后來者“借鑒”先驅(qū)頗多。

不管如何,從協(xié)議角度來看,TWAI與CAN Bus可以互通。但與Arduino Due不同,ESP32僅僅支持一個TWAI控制器,也就是說,該開發(fā)板只能外接一個CAN收發(fā)器。我們可以使用上一講Arduino Due里配置的CAN Bus節(jié)點,只要將Arduino Due的CAN收發(fā)器(至少一個)的CAN High、CAN Low分別連接起來形成一條”雙絞線“就可以完成多者互通。

如何使用Two-Wire Automobile Interface

如前面所述,通過TWAI控制器實現(xiàn)了TWAI協(xié)議,但ESP32 TWAI控制器和Arduino Due一樣,僅僅是控制器,而不包含收發(fā)器,至于兩者的關系可以參考上一講。也就是說,還要額外使用一個TWAI收發(fā)器。還行,TWAI協(xié)議和CAN Bus本身可以互通,所以, 上一講中提到的CAN收發(fā)器也可以在ESP32開發(fā)板里使用。

圖3 某款CAN收發(fā)器

另外,Arduino Due的CAN總線針腳固定,這個可以理解,畢竟MCU引出的針腳“透傳”到外設,而開發(fā)板本身不會增加額外的處理邏輯。但ESP32不同,畢竟芯片和TWAI出自同一家廠商,比Bosch僅僅定義協(xié)議可操作空間要大,其中一點就是其TWAI可以自定義GPIO針腳。下面通過Arduino IDE實現(xiàn)TWAI消息傳送。

具體針腳連線如下,

ESP32-S3(或其他ESP32系列) CAN收發(fā)器 CAN總線“雙絞線(可使用面包板)” 其他CAN收發(fā)器
GND GND
3V3 3.3V
CAN RX GPIO4或其他未使用IO針腳
CAN TX GPIO4或其他未使用IO針腳
CAN H CAN High CAN H
CAN L CAN Low CAN L

為ESP32配置Arduino IDE開發(fā)環(huán)境

在安裝了Arduino IDE后,通過以下步驟添加ESP32開發(fā)板,

  • 依次打開文件(File)、首選項(Preferences),打開首選項對話框。

  • 其他開發(fā)板管理器地址(Additional boards manager URLS),輸入https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

  • 點擊確定,等待更新配置文件更新。

  • 打開開發(fā)板管理器(Boards Manager),搜索esp32,點擊安裝(Install)。(我的Arduino IDE已經(jīng)安裝ESP32開發(fā)板,顯示REMOVE)


    圖4 在Arduino IDE里添加ESP32開發(fā)板
  • 等待幾分鐘,直到安裝完成。

TWAI消息傳送的代碼實現(xiàn)

完成了環(huán)境配置,下面著手編寫代碼, 在Arduino IDE里新建Sketch,可以命名為CANMessager,添加一個MessageManager.hpp的c++類文件,具體代碼如下,

CANMessager.ino

#include "MessageManager.hpp"

MessageManager messageManager;

void setup()
{
  // put your setup code here, to run once:
  Serial.begin(115200);

  while(!Serial);

  messageManager.setup();
}

void loop()
{
  // put your main code here, to run repeatedly:

  messageManager.loop();
}

MessageManager.hpp

#include <driver/twai.h>

// GPIOs utilized to connect to the CAN Bus Transceivers
#define RX_GPIO_PIN     GPIO_NUM_4
#define TX_GPIO_PIN     GPIO_NUM_5

#define INTERVAL        1000

typedef struct MessageManager
{
    public:

        typedef enum MessageType
        {
            normal = 0,
            loopback

        } MessageType;

    private:
    
        bool driver_installed           = false;
        MessageType messageType         = normal;
        unsigned long long lastTime     = 0;
        unsigned long long now          = 0;

    protected:

    public:
    
        void setup()
        {
            twai_general_config_t general_config = TWAI_GENERAL_CONFIG_DEFAULT(TX_GPIO_PIN, RX_GPIO_PIN, TWAI_MODE_NO_ACK);

            general_config.tx_queue_len = 0;
            general_config.rx_queue_len = 1000;

            // Baud Rate
            twai_timing_config_t timing_config = TWAI_TIMING_CONFIG_500KBITS();

            // Transparent transmission
            twai_filter_config_t filter_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();

            // Install the TWAI driver
            if (twai_driver_install(&general_config, &timing_config, &filter_config) == ESP_OK)
            {
                printf("TWAI driver installed.");

                // Start the TWAI driver
                if (twai_start() == ESP_OK)
                {
                    printf("Driver started.");

                    // Reconfigure the alerts to detect the arrival of new message, errors of frames received, Bus errors and Received Queue Full errors.
                    uint32_t alerts_to_enable = TWAI_ALERT_RX_DATA | TWAI_ALERT_ERR_PASS | TWAI_ALERT_BUS_ERROR | TWAI_ALERT_RX_QUEUE_FULL;

                    if (twai_reconfigure_alerts(alerts_to_enable, NULL) == ESP_OK)
                    {
                        printf("TWAI alerts reconfigured.");

                        // All set
                        driver_installed = true;
                    }
                    else
                        printf("Failed to reconfigure the TWAI alerts.");
                }
                else
                    printf("Failed to start the TWAI driver.");
            }
            else
                printf("Failed to install the TWAI driver.");
        }

        void loop()
        {            
            if (driver_installed)
            {
                uint32_t alert_triggered;
                twai_status_info_t status_info;

                // Check if new alert available.
                twai_read_alerts(&alert_triggered, portMAX_DELAY);
                twai_get_status_info(&status_info);

                // Tackle with the alerts.

                // New message arrived.
                if (alert_triggered & TWAI_ALERT_ERR_PASS)
                {
                    Serial.println("Alert: An (Bit, Stuff, CRC, Form, ACK) error has occurred on the bus.");
                    Serial.print("Bus error count: ");
                    Serial.println(status_info.bus_error_count);
                }
                else if (alert_triggered & TWAI_ALERT_RX_QUEUE_FULL)
                {
                    Serial.println("Alert: the RX queue is full causing the received frames t be lost.");

                    Serial.printf("RX buffered: %d\n", status_info.msgs_to_rx);
                    Serial.printf("RX misused: %d\n", status_info.rx_missed_count);
                    Serial.printf("RX overrun: %d\n", status_info.rx_overrun_count);
                }
                else if (alert_triggered & TWAI_ALERT_RX_DATA)
                {
                    // Upon the new message arrival.
                    twai_message_t message;

                    while(twai_receive(&message, 0) == ESP_OK)
                        handle_received_message(message);
                }
                else
                {
                    now = millis();

                    if (now - lastTime > INTERVAL)
                    {
                        // No errors and messages, simulate a loopback message.
                        switch (messageType)
                        {
                            case loopback:
                            {
                                uint8_t data[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
                                transmit_loopback_message(0x01, data);
                            }
                            
                            break;
                            
                            default:
                            break;

                        }

                        lastTime = now;
                    }
                }
            }
        }

        void handle_received_message(const twai_message_t message)
        {
            Serial.print("Received new ");
            Serial.print(message.extd ? "extended ": "standard ");
            Serial.print("message, ");

            Serial.printf("Identifier: %x, ", message.identifier);

            if (!message.rtr)
            {
                // Not a Remote Transmission Request message.
                for (uint8_t i = 0; i < message.data_length_code; i ++)
                {
                    if (i < message.data_length_code - 1)
                        Serial.printf("Data[%d] = %02x, ", i, message.data[I]);
                    else
                        Serial.printf("Data[%d] = %02x", i, message.data[I]);
                }

                Serial.println();
            }
        }

        void transmit_loopback_message(const uint32_t identifier, const uint8_t *data, const uint8_t data_length_code = TWAI_FRAME_MAX_DLC)
        {
            // Configure the loopback message
            twai_message_t message = {
                {
                    {
                        .self = 1,
                    }
                },
                .identifier = identifier,
                .data_length_code = data_length_code,
            };

            memcpy(message.data, data, data_length_code);

            // Transmit the loopback message
            transmit_message(message);
        }

        void transmit_message(const twai_message_t message)
        {
            String string = "Data with ID [###] will be transmitted: ";

            string.replace("###", String(message.identifier, HEX));

            Serial.print(string);

            for (uint8_t i = 0; i < message.data_length_code; i ++)
            {
                Serial.printf("Data[%d] = %#02x", i, message.data[I]);

                if (i < message.data_length_code - 1)
                    Serial.print(", ");
            }

            Serial.println();

            // Queue the message for transmission.
            esp_err_t result = twai_transmit(&message, portMAX_DELAY);

            if(result == ESP_OK)
                Serial.printf("%s: Message queued for transmission.\n", esp_err_to_name(result));
            else
                Serial.printf("%s: Failed to queue the message for transmission.\n", esp_err_to_name(result));

            Serial.println();
        }

} MessageManager;

簡單解釋一下,

  • MessageManager.hpp存在的目的就是為了接管CANMessager.ino的代理方法setup()loop() ,起到分散代碼的目的。
  • #define RX_GPIO_PIN GPIO_NUM_4#define TX_GPIO_PIN GPIO_NUM_5定義TWAI使用的IO針腳
    *twai_general_config_t general_config ,TWAI的主要配置信息,比如針腳,消息模式、接收和發(fā)送最大隊列。
  • twai_timing_config_t timing_config ,主要配置CAN通信的波特率,背后有系列通過各種段(Segment)、時鐘頻率計算波特率的算法,后續(xù)章節(jié)會解釋。
  • twai_filter_config_t filter_config ,消息過濾設置。
  • twai_driver_install() 安裝驅(qū)動。
  • twai_start() 開始服務。
  • twai_reconfigure_alerts 信息啟用機制,比如啟用某種類型的消息,后續(xù)只會收到啟用的消息。
  • twai_read_alerts 讀取消息到隊列;twai_get_status_info 總線狀態(tài)信息。
  • alert_triggered & TWAI_ALERT_ERR_PASS、alert_triggered & TWAI_ALERT_RX_QUEUE_FULL、alert_triggered & TWAI_ALERT_RX_DATA 各種新消息處理方法。
  • twai_receive(&message, 0) ,接收消息;handle_received_message 處理接收到的消息。
  • transmit_message ,傳輸消息。

上傳運行程序

完成了代碼書寫,下面開始燒錄到開發(fā)板,運行程序。注意,Arduino Due端的CAN收發(fā)器確保連接到總線,并且也要運行起來。

運行結(jié)果打印輸出如下,

發(fā)送端

Data with ID [18] will be transmitted: Data: [0] = 00, [1] = 0x1, [2] = 0x2, [3] = 0x3, [4] = 0x4, [5] = 0x5, [6] = 0x6, [7] = 0x7
ESP_OK: Message queued for transmission.

Received new standard message, Identifier: 18, Data: [0] = 00, [1] = 01, [2] = 02, [3] = 03, [4] = 04, [5] = 05, [6] = 06, [7] = 07

Data with ID [2c] will be transmitted: Data: [0] = 00, [1] = 0x1, [2] = 0x2, [3] = 0x3, [4] = 0x4, [5] = 0x5, [6] = 0x6, [7] = 0x7
ESP_OK: Message queued for transmission.

Received new standard message, Identifier: 2c, Data: [0] = 00, [1] = 01, [2] = 02, [3] = 03, [4] = 04, [5] = 05, [6] = 06, [7] = 07

Data with ID [ee] will be transmitted: Data: [0] = 00, [1] = 0x1, [2] = 0x2, [3] = 0x3, [4] = 0x4, [5] = 0x5, [6] = 0x6, [7] = 0x7
ESP_OK: Message queued for transmission.

Received new standard message, Identifier: ee, Data: [0] = 00, [1] = 01, [2] = 02, [3] = 03, [4] = 04, [5] = 05, [6] = 06, [7] = 07

接收端


can0  018   [8]  00 01 02 03 04 05 06 07
can0  02C   [8]  00 01 02 03 04 05 06 07
can0  0EE   [8]  00 01 02 03 04 05 06 07

小結(jié)

本講通過ESP32的TWAI協(xié)議,實現(xiàn)CAN總線通信的應用層傳輸,與上一講Arduino Due的互通,接近于CAN總線在實際使用的場景。相信通過這兩講的介紹,特別親手操刀,讓您真切體會到了CAN通信沒那么復雜。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容