第0x02讲 使用ESP32开发板进行CAN总线通信

上一讲我们使用Arduino DUE进行CAN通信,Arduino DUE进行CAN通信的优势在于,它本身集成了两个CAN控制器模块,可以在板内实现互发。本讲使用另外一个开发板,ESP32,其内置了总线控制器,同样可以实现CAN总线通信。

ESP32开发板

ESP32是乐鑫Espressif旗下的一款集成了32位MCU、2.4Ghz Wi-Fi、蓝牙Bluetooth 5 (LE)的开发板,其系列分为ESP32-S2、ESP32-C3、ESP32-C6、ESP32-H2、ESP32-S3等系列。推荐使用ESP32-S3,理论上其他ESP32系列的开发也可以。

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算法的安全启动,数字签名和HMAC模块,“世界控制器(World Controller)“莫模块。
开发板 ESP32-S3-DevKitM-1、ESP32-S3-DevKitM-1等
图1 某款ESP32-S3开发板
针脚定义

作为开发板,开发者最为关心的是针脚定义。ESP32-S3提供了非常丰富的接口。具体如下,

图2 ESP32-S3针脚定义

ESP32 CAN总线模块

上一讲中,Arduino Due本身集成了CAN控制器模块,这点反映在Arduino Due的官方文档里,同时,MCU也从底层角度提供了支持。也许您会有疑问,上一节关于ESP32的介绍中没有提到ESP32对CAN总线的支持。

注意在ESP32配置表里,有“TWAIᐪᔿ控制器”,TWAI全称Two-Wire Automobile Interface,这是乐鑫研发的通信协议,对标Bosch的CAN Bus协议。据我阅读两者的文档,后来者“借鉴”先驱颇多。

不管如何,从协议角度来看,TWAI与CAN Bus可以互通。但与Arduino Due不同,ESP32仅仅支持一个TWAI控制器,也就是说,该开发板只能外接一个CAN收发器。我们可以使用上一讲Arduino Due里配置的CAN Bus节点,只要将Arduino Due的CAN收发器(至少一个)的CAN High、CAN Low分别连接起来形成一条”双绞线“就可以完成多者互通。

如何使用Two-Wire Automobile Interface

如前面所述,通过TWAI控制器实现了TWAI协议,但ESP32 TWAI控制器和Arduino Due一样,仅仅是控制器,而不包含收发器,至于两者的关系可以参考上一讲。也就是说,还要额外使用一个TWAI收发器。还行,TWAI协议和CAN Bus本身可以互通,所以, 上一讲中提到的CAN收发器也可以在ESP32开发板里使用。

图3 某款CAN收发器

另外,Arduino Due的CAN总线针脚固定,这个可以理解,毕竟MCU引出的针脚“透传”到外设,而开发板本身不会增加额外的处理逻辑。但ESP32不同,毕竟芯片和TWAI出自同一家厂商,比Bosch仅仅定义协议可操作空间要大,其中一点就是其TWAI可以自定义GPIO针脚。下面通过Arduino IDE实现TWAI消息传送。

具体针脚连线如下,

ESP32-S3(或其他ESP32系列) CAN收发器 CAN总线“双绞线(可使用面包板)” 其他CAN收发器
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开发环境

在安装了Arduino IDE后,通过以下步骤添加ESP32开发板,

  • 依次打开文件(File)、首选项(Preferences),打开首选项对话框。

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

  • 点击确定,等待更新配置文件更新。

  • 打开开发板管理器(Boards Manager),搜索esp32,点击安装(Install)。(我的Arduino IDE已经安装ESP32开发板,显示REMOVE)


    图4 在Arduino IDE里添加ESP32开发板
  • 等待几分钟,直到安装完成。

TWAI消息传送的代码实现

完成了环境配置,下面着手编写代码, 在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的主要配置信息,比如针脚,消息模式、接收和发送最大队列。
  • twai_timing_config_t timing_config ,主要配置CAN通信的波特率,背后有系列通过各种段(Segment)、时钟频率计算波特率的算法,后续章节会解释。
  • twai_filter_config_t filter_config ,消息过滤设置。
  • twai_driver_install() 安装驱动。
  • twai_start() 开始服务。
  • twai_reconfigure_alerts 信息启用机制,比如启用某种类型的消息,后续只会收到启用的消息。
  • twai_read_alerts 读取消息到队列;twai_get_status_info 总线状态信息。
  • 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 ,传输消息。

上传运行程序

完成了代码书写,下面开始烧录到开发板,运行程序。注意,Arduino Due端的CAN收发器确保连接到总线,并且也要运行起来。

运行结果打印输出如下,

发送端

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

小结

本讲通过ESP32的TWAI协议,实现CAN总线通信的应用层传输,与上一讲Arduino Due的互通,接近于CAN总线在实际使用的场景。相信通过这两讲的介绍,特别亲手操刀,让您真切体会到了CAN通信没那么复杂。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容