ESP32的CAN Bus通信以及Bus-off问题修复
之前就听过CAN bus,但是从来没用过这种汽车级的通信协议,大多数小型设备用UART都够了,不过这次实在逃不掉了,多设备通信,每台设备多个DCDC,20个设备在一起EMC极其残暴,且需要中长距离传输,目前想到最好的解决办法就是CAN bus,所以就尝试一下使用它来完成通信工作。
声明:非个人项目,无法提供实际的设计图和代码,示例代码来自网络,请注意甄别。
硬件设计

由于是公司项目,我不能直接放出设计图。
芯片采用ESP32S2,带有一个双线汽车总线控制器(TWAI),兼容ISO 11898-1(CAN 规范 2.0),我们需要用的就是这个外设。

注意没用过ESP32独立芯片的,设计电路前务必仔细看参考手册的电路设计,Strapping引脚不能随意配置和接线,还有些内接了存储单元的引脚也请保持悬空!
另外,笔者由于经验不足,之前用STM和ATmel较多,习惯性不使用外部时钟,因此栽了跟头,ESP32-S2裸片有内置时钟,但是用于协处理器的时钟而非主CPU的时钟,所以没有外部时钟芯片无法工作,这个问题卡了我一阵子,表现为无法找到设备,无法烧录,无法输出debug信息,一开始以为电源不稳,然后以为上电时序不对,经过反复查阅,发现是没接时钟,因此提醒那些从模块转到裸片的设计者:ESP32裸片务必记得加外部时钟。
在修改PCB上加入40MHz晶振后,成功找到设备。

验证了烧录和日志输出功能正常,接下来我们需要完成设备连接和端口配置。
硬件上,两个ESP32-S2的TWAI不能像UART那样直接啵嘴,需要有收发器,笔者用的TJA1051T/3,NXP出品的CAN收发芯片,自身需要5V供电,不过通讯端口适配ESP32的3.3V传输,无需电平转换芯片。
将ESP32 TWAI的TX和RX分别接到TJA1051T/3,TJA1051T/3的CAN_H,CAN_L与另一台设备的CAN_H,CAN_L相连,但是注意总线上CAN_H和CAN_L之间要保持60欧的阻值,所以,可以分别给两台设备的CAN bus加一个120欧的电阻,两边一连就自动并联成60欧。
另外注意stub长度,CAN bus的stub长度,1Mbps的情况下不得超过15cm。
软件设计
笔者使用的ESP-IDF是6.0.0版本,配置TWAI用到了esp_twai.h,esp_twai_onchip.h,FreeRTOS.h,其中,freeRTOS用于构建消息队列。
常用API
esp_err_t twai_new_node_onchip(const twai_onchip_node_config_t *node_config, twai_node_handle_t *node_ret)
这个函数的作用在于一站式完成TWAI的所有参数配置。
参数:
@node_config - 节点配置
点击查看代码
struct {
gpio_num_t tx; /**< GPIO pin for twai TX */
gpio_num_t rx; /**< GPIO pin for twai RX */
gpio_num_t quanta_clk_out; /**< GPIO pin for quanta clock output, Set -1 to not use */
gpio_num_t bus_off_indicator; /**< GPIO pin for bus-off indicator, Set -1 to not use */
} io_cfg; /**< I/O configuration */
twai_clock_source_t clk_src; /**< Optional, clock source, remain 0 to using TWAI_CLK_SRC_DEFAULT by default */
twai_timing_basic_config_t bit_timing; /**< Timing configuration for classic twai and FD arbitration stage */
twai_timing_basic_config_t data_timing; /**< Optional, timing configuration for FD data stage */
uint32_t timestamp_resolution_hz; /**< Timebase frequency (in Hz), used for recording the timestamp of RX frame, set 0 to disable the timestamp feature */
int8_t fail_retry_cnt; /**< Hardware retry limit if failed, range [-1:15], -1 for re-trans forever */
uint32_t tx_queue_depth; /**< Depth of the transmit queue */
int intr_priority; /**< Interrupt priority, [0:3] */
struct {
uint32_t enable_self_test: 1; /**< Transmission does not require acknowledgment. Use this mode for self testing */
uint32_t enable_loopback: 1; /**< The TWAI controller receives back frames that it sends out, but does not acknowledge them */
uint32_t enable_listen_only: 1; /**< No transmissions or acknowledgements. The controller only monitors the bus without participating */
uint32_t no_receive_rtr: 1; /**< Don't receive remote frames */
} flags;
} twai_onchip_node_config_t;
这个结构体包含了相当多的参数,我们不需要全部用到,重点关注tx,rx引脚,Baudrate,tx_queue_depth。
@node - 句柄
返回值:
返回:
ESP_OK 配置成功
ESP_ERR_NO_MEM 内存不足
ESP_ERR_NOT_FOUND 没有找到硬件外设
ESP_ERR_INVALID_ARG 无效配置参数
ESP_ERR_INVALID_STATE 硬件状态错误或驱动状态错误
ESP_FAIL 其他错误
esp_err_t twai_node_enable(twai_node_handle_t node)
启动TWAI节点。
参数:
@node - TWAI句柄
返回值:
ESP_OK: 成功
ESP_ERR_INVALID_STATE: 错误状态,重复初始化
esp_err_t twai_node_register_event_callbacks(twai_node_handle_t node, const twai_event_callbacks_t *cbs, void *user_data)
注册TWAI回调函数
参数:
@node – 句柄
@cbs – 回调函数指针
@user_data – 用户数据
返回:
ESP_OK -成功
TWAI接收数据必须通过回调函数。
以上,是我们完成TWAI配置需要用到API,把他们写进一个初始化函数我们就可以实现TWAI的初始化。
示例代码:
twai_node_handle_t s_node_hdl = NULL;
esp_err_t twai_init(void)
{
twai_onchip_node_config_t node_config = {
.io_cfg.tx = TWAI_TX_PIN,
.io_cfg.rx = TWAI_RX_PIN,
.bit_timing.bitrate = 500000,
.tx_queue_depth = 5,
};
twai_new_node_onchip(&node_config, &s_node_hdl);
//Register receive callback
twai_event_callbacks_t cbs = {
.on_rx_done = twai_rx_done_cb,
};
twai_node_register_event_callbacks(s_node_hdl, &cbs, NULL);
twai_node_enable(s_node_hdl);
return ESP_OK;
}
这是一个TWAI初始化的示例代码,根据自己需要进行修改。
[========]
除了初始化函数,我们还要接收数据函数,发送数据函数,其中接收数据要写2个,一个事件回调把接收的数据放进队列,一个从队列中读出数据。
事件回调:
static bool IRAM_ATTR twai_rx_done_cb(twai_node_handle_t handle, const twai_rx_done_event_data_t *edata, void *user_ctx)
{
twai_rx_item_t item = {0};
twai_frame_t rx_frame = {
.buffer = item.data,
.buffer_len = sizeof(item.data),
};
if (twai_node_receive_from_isr(handle, &rx_frame) == ESP_OK) {
// do something
}
return false;
}
接收函数:
esp_err_t twai_receive(twai_frame_t *frame, int timeout_ms)
{
twai_rx_item_t item;
TickType_t ticks = (timeout_ms < 0) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms);
if (xQueueReceive(s_rx_queue, &item, ticks) == pdTRUE)
{
ESP_LOGI(TAG, "Message received from RX queue");
frame->header = item.header;
size_t copy_len = (item.data_len < frame->buffer_len) ? item.data_len : frame->buffer_len;
memcpy(frame->buffer, item.data, copy_len);
frame->buffer_len = copy_len;
return ESP_OK;
}
return ESP_ERR_TIMEOUT;
}
发送函数:
esp_err_t twai_transmit(const twai_frame_t *frame, int timeout_ms)
{
esp_err_t ret = ESP_FAIL;
ret = twai_node_transmit(s_node_hdl, frame, timeout_ms);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Message queued for TX");
return ESP_OK;
}
ESP_LOGE(TAG, "Failed to queue message for TX (err=%d)", ret);
return ret;
}
初始化,接收和发送是CAN通信必须的功能,当然这些是底层的,我们还要写一个协议层,用于定义设备间的各种指令。
示例:
#define TWAI_CMD_SET_DUTY 0x100
#define TWAI_CMD_SET_FREQ 0x101
宏定义你要的指令,比如设置下位设备的频率和占空比。
esp_err_t twai_set_duty(uint8_t dev_addr, uint8_t channel, uint16_t duty);
esp_err_t twai_set_freq(uint8_t dev_addr, uint8_t channel, twai_freq_t freq);
这些函数写你的具体设置的步骤。
然后再写一个调度函数,集中处理收到的指令,并处理。
void dispatch(twai_frame_t *frame);
可以把接收函数和调度函数写进一个后台任务里运行,节省资源。
static void twai_task(void *pvParameter);
esp_err_t twai_task_start(void);
完成了主机的代码,从机用同样的方法写好端口配置和协议(不同的MCU协议相同,配置方式不同),然后我们直接连接硬件测试。
[========]

主机我使用的微雪的LCD-7B触摸屏,我们需要把主机和从机的CAN接口连接,这里我直接焊了两根杜邦线,另外注意共地连接。
打开VSCode,烧录代码并打开Monitor观察调试信息。

出现了一条bus-off的消息,而且消息来源是ESP32的library而不是用户定义的。这是CAN bus里一个比较常见的问题,确认了连线正确后,我们查阅官方资料:

经检查,bus-off的原因在于总线上发送了太多错误的数据,导致总线断连,至于错误的原因,暂时无法确定是硬件还是软件,根据教程,若总线断连,需要主动调用twai_node_recover()恢复,但是不是立刻恢复,需要出现129次连续11个隐性位,这个规则来自于11898规范,大概是为了确认总线在空闲状态。我们把twai_node_recover写入发送函数里:
示例:
for (int attempt = 1; attempt <= max_attempts; attempt++) {
ret = twai_node_transmit(s_node_hdl, frame, timeout_ms);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Message queued for TX");
return ESP_OK;
}
if (ret == ESP_ERR_INVALID_STATE ) {
ESP_LOGW(TAG, "TWAI TX failed (err=%s) (attempt %d/%d)", esp_err_to_name(ret), attempt, max_attempts);
esp_err_t rec_ret = twai_node_recover(s_node_hdl);
if(rec_ret != ESP_OK) {
ESP_LOGE(TAG, "TWAI recovery failed: %d", rec_ret);
break;
}
vTaskDelay(pdMS_TO_TICKS(50 * attempt));
continue;
}
break;
}
这段代码会进行3次bus恢复尝试,不过遗憾的加入这段代码后我们的CAN bus没能恢复。

初步判断是硬件问题,可是连接是正确的,CAN Bus总线间电阻为60欧,那么需要用示波器抓取波形,而且由于CAN有收发器,既要抓芯片引出脚Tx,Rx的波形,也要抓CAN_H,CAN_L上的波形。
首先从Bus上抓取波形,发现Bus上什么信号都没有,示波器单次采样甚至都不能触发。

我们先不考虑TJA1051坏掉的可能,优先考虑ESP32的TWAI端口存在问题,因为ESP32能通过代码调试观察,简单一点,我们给两个ESP32添加同样的回环测试,这里偷懒直接用AI生成了😂,底层原理与TWAI的接收发送一致,先初始化twai节点,然后通过后台任务接收测试的数据,但我们需要配置两个参数enable_self_test = 1,enable_loopback = 1,代码比较长,用AI辅助生成会方便很多,这里不贴代码了。

测试结果是我们自己设计的板子,回环测试屡测屡过,而触摸屏的demo板无法通过回环测试,那么问题应当出在触摸屏的硬件上,我们需要用示波器抓取触摸屏上的ESP32S3的Tx,Rx波形,先测量TJA1051的Tx,Rx,嗯?怎么有一根掉到2.7V了?

查阅ESP32S3的手册,IO口输出电平范围从2.64~VCC为正常范围,所以还不能说明是芯片坏了,但这非常可疑。
我们再测试一下ESP32S3的TWAI端口电平,查阅微雪官方手册,了解到TWAI端口为GPIO19,GPIO20。


两个电平都是正常的3.3V附近,这说明半路上遭遇了不测。我们查看官方提供的原理图,发现GPIO19,GPIO20与USB调试线复用了,通过FSUSB42选择通路,所以通信失败是由于没有选择CAN通路,有一路被其他负载拉到了2.7V。

继续查阅原理图,发现这块FSUSB42是由CH32V003F4驱动的,因为ESP32S3驱动触摸屏已无足够的IO,那么我们需要给CH32V003F4烧录程序让它选通CAN Bus,CH32的调试方法不在这里展开,用过STM32的使用CH32就像外面说普通话,回家说家乡话。
根据原理图,CH32驱动FSUSB42选通线的引脚为PD5,我们只需要让这个引脚输出高电平即可解决问题。我们给CH32烧录程序后,再次进行回环测试。

顺利通过测试! 不过这也只能证明两片ESP32的TWAI端口能正常工作,在收发器上是否存在问题尚不明确,我们需要实际测试通讯。在从机的PCB上用到了雷达,我们尝试通过从机接收雷达数据>从机发送给主机>主机在屏幕上显示,这个过程对于测试CAN的通断已经冗余了,但为了节省时间,我们直接一步到位测试。


拍摄和剪辑条件有限,大伙将就看吧😅。
成功在屏幕上显示距离信息,经过几次断电和上电测试,CAN Bus没有再出现Bus-off错误,问题解决。
浙公网安备 33010602011771号