espnow是乐鑫的自定义通信协议,乐鑫的模组都可以使用这个协议。一般乐鑫的esp32模组都支持wifi,蓝牙和espnow协议,但是同一个时间只能使用其中一种无线协议。
在我考虑的espnow自定义组网协议里,默认是只使用espnow协议,其他的协议就暂时不使用。
所以如果这个esp32设备要和Linux开发板通信,就得用串口或者spi口了。目前我手头上常用的是esp32c6开发板,最简单的办法就是用usb jtag/serial 通信的方式。
把c6的usb插到HZ-T536开发板上,可以看到能正确识别到设备。


能正确识别到设备的话,但是没有出现ttyACM0设备,重新编译内核,添加了cdc_acm模块之后,就可以正常识别到设备了。
接下来,我们需要让esp32c6可以通过jtag/serial口接收数据。默认情况下是可以通过jtag/serial输出串口数据的(默认情况下,串口输出和jtag/serial都是启用了的)。
以下是官方对esp32c6的USB jtag/serial 串口的一些描述。

大部分情况下,我们并不会在意,因为默认的串口输出,在未明确不使用jtag serial输出的情况下,都会转发一份给jtag serial。
所以很自然的,我们也会觉得输入也不需要进行任何设置,直接当做常规串口输入,就能使用。实际上并不是这样,想要准确的使用jtag serial的话,需要使用特定的函数。
首先需要使用以下的代码,初始化usb jtag serial设备。
usb_serial_jtag_driver_config_t usb_serial_jtag_config = {
.rx_buffer_size = UART_BUF_SIZE,
.tx_buffer_size = UART_BUF_SIZE,
};
ESP_ERROR_CHECK(usb_serial_jtag_driver_install(&usb_serial_jtag_config));
ESP_LOGI(TAG, "usb serial/jtag init done");
然后读取jtag串口的部分,也需要使用对应的函数才可以。
while (true) {
int len = usb_serial_jtag_read_bytes(data, (UART_BUF_SIZE - 1),
20 / portTICK_PERIOD_MS);
if (len && data[UART_FRAME_OFFSET] == UART_FRAME_START) {
uint8_t *offset = data + UART_FRAME_DATA_OFFSET;
smn_packet_t *packet = (smn_packet_t *)offset;
uint16_t buff_len = packet->frame_head.payload_len + sizeof(smn_packet_t);
uint16_t head_len = sizeof(smn_addr_t) + sizeof(smn_frame_head_t);
if (len <= (head_len + buff_len + UART_FRAME_DATA_OFFSET)) {
uint16_t crc_result = crc16_le(0, offset,
head_len);
crc_result = crc16_le(crc_result, offset + head_len + sizeof(uint16_t),
packet->frame_head.payload_len);
if (packet->crc16 == crc_result) {
if (UART_ADDR_TARGET(packet->addr.dest_addr, packet->addr.scr_addr)) {
process_local_frame(offset, buff_len);
} else {
forward_remote_frame(data + 3, buff_len);
}
} else {
ESP_LOGE(TAG, "packet crc count err");
}
} else {
ESP_LOGE(TAG, "uart recv len %d not fit %d", len - 6, buff_len);
}
}
if (debug_config->uart) {
ESP_LOG_BUFFER_HEX("Recv str: ", data, len);
}
}
输出的话,也是需要使用这个usb jtag串口输出函数就可以了。
int usb_serial_jtag_write_bytes(const void* src, size_t size, TickType_t ticks_to_wait);
这样就可以正常的使用/dev/ttyACM0这个设备,用来和esp32c6通信了。上位机这边可以使用相同的数据结构打包好数据发送给esp32。例如我这边初步定义好的数据包结构。
#ifndef SMN_PACKET_H_INCLUDE
#define SMN_PACKET_H_INCLUDE
#include "SMN_err.h"
#include "SMN_port.h"
#include "../lib/SMN_proto.pb.h"
#include "../lib/nanopb/pb_decode.h"
#include "../lib/nanopb/pb_encode.h"
typedef struct smn_addr {
uint8_t scr_addr[ESPNOW_ADDR_LEN];
uint8_t dest_addr[ESPNOW_ADDR_LEN];
} __attribute__((packed)) smn_addr_t;
typedef struct smn_frame_head {
SMNCommand type : 7;
uint32_t is_response : 1;
uint32_t payload_len : 8;
uint32_t packet_seq : 8;
uint32_t reserve : 8;
} smn_frame_head_t;
typedef struct smn_device_status {
uint32_t net_status : 8;
uint32_t error : 8;
uint32_t debug_mode : 16;
} smn_device_status_t;
typedef struct smn_packet {
smn_addr_t addr;
smn_frame_head_t frame_head;
uint16_t crc16;
uint8_t payload[0];
} __attribute__((packed)) smn_packet_t;
通过串口发送的数据基本上也是这样构成的,只是需要增加数据帧头和尾部标识符(用于准确判断数据帧的开头,例如常见的0xAA开头,0x55结尾),这样就可以了。

按固定的帧头和尾取出帧数据后,就可以根据数据包的结构进行解析,例如addr地址段的地址是否本机的mac地址(又或者是源、目的都是0xFF,就默认是上位机直连发送之类的)。取到地址后,就可以根据目的地址进行转发或者处理本机数据包的工作。
然后根据帧头数据结构,就可以判断出后续的是什么指令,接下来再使用对应的protobuf解序列函数,对payload数据进行解包,获得准确的指令的数据。
通过这种方式,上位机也好,web也好,esp32设备也好,都只需要简单的做好地址段,帧头,crc校验的这些内容,其他的指令和数据都可以通过标准化的protobuf协议来序列化和解序列化(也就是封包和解包),这样我们就可以使用最少的开发时间,实现多平台的自定义数据传输了。