乐鑫的ESP32系列芯片是项目中扩展WIFI和蓝牙通讯连接的不错的选择,一般我们使用官方的AT固件通过UART通讯开发,比较简单。但是UART一般速度较低,在有更高速度要求的场景时希望使用更快的SDIO或者SPI接口,官方是支持不同接口方式的。ESP32的芯片一般都有SDIO ,SPI从接口所以除了可以使用UART还可以使用SDIO,SPI进行通讯。ESP32可以使用两种方式接入,一种还是传统的AT模式,协议栈运行在ESP32上,接口可以选择UART,SDIO和SPI; 还有一种是ESP32作为网卡的模式,透传以太网包,协议栈跑在主机上,接口也可以使用UART,SDIO,SPI。两者官方都在githun上有开源项目。这一篇就来分享使用SDIO接口的AT模式将ESP32接入自己的项目中,以ESP32-C6为例。后面再分享ESP32使用SDIO接口作为网卡模式接入的方式。
下载源码
git clone --recursive https://gitee.com/EspressifSystems/esp-at.git
打开ESP-IDF 5.4 PowerShell (ESP-IDF的开发环境搭建可以参考官方论坛)
进入到上述下载的项目中cd D:\BOARD\esp32-c6\esp32-at\esp-at
安装开发环境,python build.py install
按照如下选择
5 PLATFORM_ESPC6
1 选择ESP32C6-4MB
0 NO 先不silence,即有打印方便调试,调试没问题关闭日志设置为1,减少打印
此时开始自动clone esp-idf和其他模块
完成后如下
配置
python build.py menuconfig
配置为SDIO接口
Component config -> AT -> communicate method for AT command -> AT throughSDIO
s回车,保存
q退出
python build.py build
按住boot不松,按下rst松开复位。
实际上上述操作可以不需要,直接输入以下命令会重启进入下载模式。
下载时注意如果已经有其他串口助手连接该串口需要先断开。
其中COM167是USB枚举出的串口号.
python build.py -p COM167 flash
如果提示
File "D:\BOARD\esp32-c6\esp32-at\esp-at\esp-idf\tools\idf_py_actions\tools.py", line 267, in generate_hints
yield from generate_hints_buffer(file.read(), hints)
UnicodeDecodeError: 'gbk' codec can't decode byte 0x82 in position 2869: illegal multibyte sequence
修改文件
D:\BOARD\esp32-c6\esp32-at\esp-at\esp-idf\tools\idf_py_actions\tools.py
如果无法运行可以尝试擦除
python build.py -p (PORT) erase_flash
接上ESP32的串口(非USB枚举的串口)
回车可以看到
Help可以查看命令,ps可以查看任务信息
此时可以查看SDIO的寄存器
寄存器基地址如下
如果要看寄存器SLCHOST_PKT_LEN_REG的内容
printmem hex 60018060 1 32 0
偏移0x58的中断ST状态寄存器,可以看到如下
esp32c6> printmem hex 60018058 1 32 0
[60018058]:00800000
esp32c6>
偏移0xD4寄存器是清除标志,setmem 600180d4 800000再回读,看到标志被清除
为了方便调试,比如查看SDIO的寄存器,这里添加一个shell任务,实现任意地址的内存读写功能,以及任务信息查看的功能,
添加以下文件
app_shell.c
static const char* TAG = "shell";
static int ps_command(int argc, char **argv)
{
uint32_t total_time;
uint32_t task_cnt = uxTaskGetNumberOfTasks();
TaskStatus_t * p_task_array = pvPortMalloc(sizeof(TaskStatus_t) * task_cnt);
if (NULL == p_task_array) {
return -1;
}
task_cnt = uxTaskGetSystemState(p_task_array, task_cnt, &total_time);
printf("name id\tpri\tts\tsta\tsbase\t smark\r\n");
for (uint32_t i = 0; i < task_cnt; i++) {
TaskStatus_t * p_task = p_task_array + i;
printf("%-16s%-8u%-8u%-8lu%-8u%-8lx%8lu\r\n",p_task->pcTaskName,p_task->xTaskNumber,p_task->uxCurrentPriority,p_task->ulRunTimeCounter,
p_task->eCurrentState,(uint32_t)(p_task->pxStackBase),p_task->usStackHighWaterMark);
}
vPortFree(p_task_array);
return 0;
}
static int printmem_command(int argc, char **argv)
{
uint32_t addr;
uint32_t len;
uint8_t mode[64];
int datasize;
uint8_t* tmp8_u;
uint16_t* tmp16_u;
uint32_t* tmp32_u;
int8_t* tmp8_i;
int16_t* tmp16_i;
int32_t* tmp32_i;
int sig;
sscanf((const char*)argv[1], "%s", mode);
sscanf((const char*)argv[2], "%lx",&addr);
sscanf((const char*)argv[3], "%ld",&len);
sscanf((const char*)argv[4], "%d", &datasize);
sscanf((const char*)argv[5], "%d", &sig);
if(6 == argc)
{
if(strncmp((const char*)mode,"hex", 3) == 0)
{
if(datasize == 8)
{
tmp8_u = (uint8_t*)addr;
for(uint32_t i=0; i
{
if(i%16 == 0)
{
printf("\r\n[%08lx]:",addr+i*1);
}
printf("%02x ",tmp8_u[i]);
}
printf("\r\n");
}
else if(datasize == 16)
{
tmp16_u = (uint16_t*)addr;
for(uint32_t i=0; i
{
if(i%16 == 0)
{
printf("\r\n[%08lx]:",addr+i*2);
}
printf("%04x ",tmp16_u[i]);
}
printf("\r\n");
}
else if(datasize == 32)
{
tmp32_u = (uint32_t*)addr;
for(uint32_t i=0; i
{
if(i%16 == 0)
{
printf("\r\n[%08lx]:",addr+i*4);
}
printf("%08lx ",tmp32_u[i]);
}
printf("\r\n");
}
else
{
printf("datasize must be 8/16/32\r\n");
}
}
else if(strncmp((const char*)mode,"dec", 3) == 0)
{
if(datasize == 8)
{
if(sig == 0)
{
tmp8_u = (uint8_t*)addr;
for(uint32_t i=0; i
{
if(i%16 == 0)
{
printf("\r\n");
}
printf("%d ",tmp8_u[i]);
}
printf("\r\n");
}
else
{
tmp8_i = (int8_t*)addr;
for(uint32_t i=0; i
{
if(i%16 == 0)
{
printf("\r\n");
}
printf("%d ",tmp8_i[i]);
}
printf("\r\n");
}
}
else if(datasize == 16)
{
if(sig == 0)
{
tmp16_u = (uint16_t*)addr;
for(uint32_t i=0; i
{
if(i%16 == 0)
{
printf("\r\n");
}
printf("%d ",tmp16_u[i]);
}
printf("\r\n");
}
else
{
tmp16_i = (int16_t*)addr;
for(uint32_t i=0; i
{
if(i%16 == 0)
{
printf("\r\n");
}
printf("%d ",tmp16_i[i]);
}
printf("\r\n");
}
}
else if(datasize == 32)
{
if(sig == 0)
{
tmp32_u = (uint32_t*)addr;
for(uint32_t i=0; i
{
if(i%16 == 0)
{
printf("\r\n");
}
printf("%ld ",tmp32_u[i]);
}
printf("\r\n");
}
else
{
tmp32_i = (int32_t*)addr;
for(uint32_t i=0; i
{
if(i%16 == 0)
{
printf("\r\n");
}
printf("%ld ",tmp32_i[i]);
}
printf("\r\n");
}
}
else
{
printf("datasize must be 8/16/32\r\n");
}
}
else
{
printf("mode must be [hex/dec]\r\n");
}
}
else
{
printf("param err\r\n");
}
return 0;
}
static int setmem_command(int argc, char **argv)
{
uint32_t addr;
uint32_t val;
if(3 == argc){
sscanf((const char*)argv[1], "%lx", &addr);
sscanf((const char*)argv[2], "%lx", &val);
printf("setmem %lx %lx\r\n",addr,val);
if((addr % 4) ==0)
{
*(volatile uint32_t*)addr = val;
printf("%lx\r\n",*(volatile uint32_t*)addr);
}
else
{
printf("addr must be mul of 4\r\n");
}
}else{
printf("param err\r\n");
}
return 0;
}
esp_err_t esp_console_register_ps_command(void)
{
esp_console_cmd_t command = {
.command = "ps",
.help = "Print Task Information",
.func = &ps_command,
.argtable = NULL
};
return esp_console_cmd_register(&command);
}
esp_err_t esp_console_register_printmem_command(void)
{
esp_console_cmd_t command = {
.command = "printmem",
.help = "printmem mode[hex/dec] addr[hex] len datasize[8/16/32] sig[1/0]",
.func = &printmem_command,
.argtable = NULL
};
return esp_console_cmd_register(&command);
}
esp_err_t esp_console_register_setmem_command(void)
{
esp_console_cmd_t command = {
.command = "setmem",
.help = "setmem addr[hex] val[hex]",
.func = &setmem_command,
.argtable = NULL
};
return esp_console_cmd_register(&command);
}
void app_shell_init(void)
{
esp_console_repl_t *repl = NULL;
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
/* Prompt to be printed before each line.
* This can be customized, made dynamic, etc.
*/
repl_config.prompt = PROMPT_STR ">";
repl_config.max_cmdline_length = 1024;
ESP_LOGI(TAG, "shell init");
/* Register commands */
esp_console_register_help_command();
esp_console_register_ps_command();
esp_console_register_printmem_command();
esp_console_register_setmem_command();
esp_console_dev_uart_config_t hw_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_uart(&hw_config, &repl_config, &repl));
esp_console_dev_usb_cdc_config_t hw_config = ESP_CONSOLE_DEV_CDC_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_usb_cdc(&hw_config, &repl_config, &repl));
esp_console_dev_usb_serial_jtag_config_t hw_config = ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_usb_serial_jtag(&hw_config, &repl_config, &repl));
ESP_ERROR_CHECK(esp_console_start_repl(repl));
}
app_shell.h
void app_shell_init(void);
清除
python build.py clean
配置
(Top) → Component config → Console Library
[*] Enable sorted help
(Top) → Component config → FreeRTOS → Kernel
下配置
s保存
q退出
重新编译
python build.py build
重新下载运行
python build.py -p COM167 flash
先以官方支持的STM32为例,先跑一遍,然后再移植到自己的项目。
打开esp-at\examples\at_sdio_host\STM32\MDK-ARM\sdio_at.uvprojx
我这里选择编译器版本6
需要替换port.c和portmacro.h为
FreeRTOS-Kernel\portable\GCC\ARM_CM3下的文件
这里是V10.0.1版本,可以从https://sourceforge.net/projects/freertos/下载对应的版本。
1.如果下载后进入0x1fffxxx位置,注意BOOT的电平是否为进入了bootrom模式,
2.如果不能运行,配置使用微库
Printf使用fputc
STM32引脚
ESP32-C6引脚
修改如下打印等级,开始可以多打印一些调试信息
如下位置,有4种组合,通讯异常时可异常时不同配置,配置使能内部上拉
STM32端配置时钟速率,总线位宽,时钟边沿
我这里使用逻辑分析仪,抓了STM32下的包,可以参考,抓包可以看到CMD5并没有响应,说明SDIO时序还是有点不问题,这里到时再调试。
对应的波形文件如下
通过网盘分享的文件:esp32-c6-at-sdio
链接: https://pan.baidu.com/s/1rkLlNP_HJXA-EMJp3TclQg?pwd=sffg 提取码: sffg
--来自百度网盘超级会员v3的分享
具体可以参考官方AT手册
AT+RESTORE 恢复出厂默认值
AT+CWMODE=1 设置station模式
AT+CWJAP="wifi名","wifi密码" 连接wifi
AT+CIPSTA? 查看IP地址
AT+CIPSTART="TCP","172.16.11.72",8080 连接tcp服务端
AT+CIPSEND=4 准备发送4字节数据
再发送1234
接收
进入透传接收模式
AT+CIPMODE=1
关闭回显
ATE0
透传发送模式
AT+CIPSEND
测试ESP32C6的输出边沿,可以用逻辑分析仪抓到。
而ESP32C6的采样边沿不能直接测量到,只能修改host那边看它是否能接收来判定。
下降沿发送数据
上升沿发送数据
下降沿发送数据
上升沿发送数据
这里需要根据不同平台进行配置和ESP32C6那边对应。
官方的基于STM32做了Demo,但是说实话代码有电乱可移植性不怎么好,这类进行了重构,将需要移植的接口单独拿出,只需要适配即可。命名也加了前缀避免冲突。
其中esp_host_port.h/c实现信号量等接口,sdio_itf.h/c实现SDIO的接口,主要是初始化,CMD52读写寄存器,CMD53读写数据。
F1的0x50 RAW寄存器
F1的0x58 ST寄存器
F1的0xD4 CLR寄存器
比如
wifirddat 1 58 4
rddat fn:1,addr:0x58,len:4
00 00 80 00
Bit23为1表示用新的包待读
比如
wifirddat 1 60 4
rddat fn:1,addr:0x60,len:4
09 00 90 00
表示有9字节待读取。
CHECK也为9.
从F1的地址0x1F800 往前偏移待读取的数据大小
前面待读取数据为9字节,所以从0x1F800-0x09开始读
所以
wifirddat 1 1f7f7 12
rddat fn:1,addr:0x1f7f7,len:12
0d 0a 72 65 61 64 79 0d 0a 00 00 00 :- - r e a d y - - - - -
其中最后3字节是填充0,需要按照4字节对齐读。
读到的即为ready。
对应的是slave的如下代码
Slave发送ready。Master读出之后,slave打印done。
读F1的0x44寄存器
调用接口sdio_host_send_packet
先获取可写大小
然后CMD53写0x1F800(这是结束地址,往前偏移指定位置为开始地址)
参考https://docs.espressif.com/projects/esp-at/zh_CN/latest/esp32/Compile_and_Develop/How_to_optimize_throughput.html
这里使用25MHz频率,1bit模式,
进入透传模式,不断发送数据,上位机接收数据,简单测试下速率。
实际和上位机,发送模式,esp32那边的配置等都有影响。
static void wifitxtestfunc(uint8_t* param)
{
int plen;
int pnum;
uint8_t* buffer;
if(2 == sscanf((const char*)param, "%*s %d %d", &plen, &pnum))
{
xprintf("plen:%d pnum:%d\r\n",plen,pnum);
buffer = os_mem_malloc(0,plen);
if(buffer == NULL){
xprintf("malloc %d err\r\n",plen);
return;
}
for(int i=0; i
buffer[i] = i;
}
uint32_t t0;
uint32_t t1;
uint32_t diff;
long len = (long)plen*(long)pnum;
os_delay(100);
//os_task_suspend_all();
t0 = os_get_ticks();
for(int i=0; i
sdio_host_send_packet(buffer, plen);
}
t1 = os_get_ticks();
os_mem_free(buffer);
if(t1 >= t0){
diff = t1 - t0;
}else{
diff = 0xFFFFFFFF - t0 + t1 +1;
}
//os_task_resume_all();
xprintf("send len %ld\r\n",len);
xprintf("used %dmS\r\n",diff);
xprintf("%dkB/S\r\n",(int)((len*1.000f)/(1.024f*diff)));
}
else
{
xprintf("param err\r\n");
}
}
最主要的需要注意SDIO时序的配置,可以先使能ESP32-C6的内部上拉,为了可靠最好硬件上加上拉。 然后注意HOST和ESP32的时序匹配,比如HOST上升沿发送数据则ESP32下降沿采集数据,反之亦然,这一步一定要使用逻辑分析仪抓包看,调整到采样边沿到数据的正中央是最理想的,这样setup和hold时间都是半个bit宽,冗余度最高。个人调试来看感觉ESP32的SDIO不是很健壮,这里需要HOST时序一定要调整到最优,如果HOST硬件支持turning可以微调CLK和数据的相位的话会更容易适配。