一、前言
天气越来越热,于是我把我的狗子牵到屋子里面避暑,为了防止狗子在一个狗的屋子里乱拆,灵机一动便有了这篇文章。(文章末尾附完整工程)
二、硬件选型
发送端采用ESP32-S3,摄像头使用ov2640
功放芯片选择MAX98357A+8Ω2W的喇叭
接收端选择了一个普通的ESP32 开发板,带了一个ILI9431的屏幕
麦克风选择INMP441
三、硬件连接
ESP32-S3硬件连接
CAM_PIN_PWDN - | 1 |
CAM_PIN_RESET | 45 |
CAM_PIN_XCLK | 39 |
CAM_PIN_SIOD | 21 |
CAM_PIN_SIOC | 46 |
CAM_PIN_D7 | 40 |
CAM_PIN_D6 | 38 |
CAM_PIN_D5 | 37 |
CAM_PIN_D4 | 35 |
CAM_PIN_D3 | 33 |
CAM_PIN_D2 | 48 |
CAM_PIN_D1 | 47 |
CAM_PIN_D0 | 34 |
CAM_PIN_VSYNC | 42 |
CAM_PIN_HREF | 41 |
CAM_PIN_PCLK | 36 |
MAX98357_BCK | 18 |
MAX98357_LRC | 16 |
MAX98357_DIN | 17 |
MAX98357_SD | 3.3V |
接收端的ESP32硬件连接
LCD_MOSI | 13 |
LCD_SCK | 14 |
LCD_CS | 15 |
LCD_DC | 2 |
LCD_RST | 4 |
LCD_BLK | 21 |
INMP441_WS | 21 |
INMP441_SD | 22 |
INMP441_SCK | 27 |
INMP441_L/R | GND |
四、功能实现
接收端
接收端使用WiFi STA模式,初始化为TCP Client用于连接发送端发出的热点。(具体代码:略)
接收端的任务主要是:
1.接受WIFI上的JPEG图片并解码显示
2.采集iis音频数据并发送
首先创建两个任务,一个用于iis读取音频,另一个任务用于将音频数据通过WIFI发送出去。两个任务之间共享一个链队用于存储数据,链队用信号量互斥访问。
//初始化链队和互斥信号量
LinkQueue Queue_Wifi_Transmit;
SemaphoreHandle_t Mutex_Wifi_Transmit;
Mutex_Wifi_Transmit = xSemaphoreCreateMutex();
InitLinkQueue( &Queue_Wifi_Transmit);
采集音频的任务放在app_main()中执行
while(1){
if(xSemaphoreTake(Mutex_Wifi_Transmit,portMAX_DELAY) == pdTRUE){
if(GetLength(Queue_Wifi_Transmit) < 20){
memset(Audio_Data,0,1024);
ret = i2s_read(I2S_NUM_0, (void *)Audio_Data, 1024, &bytes_read, 100);
if(ret == ESP_OK){
if(EnLinkQueue(&Queue_Wifi_Transmit,(unsigned char *)Audio_Data) == 1){
}
}
}
xSemaphoreGive(Mutex_Wifi_Transmit);
}
vTaskDelay(10 / portTICK_RATE_MS);
}
创建第二个任务用于发送音频数据
xTaskCreate(Tcp_Transmit,"tcp_transmit_task",4096,NULL,2,&Tcp_Transmit_handle); //创建WIFI发送任务
void Tcp_Transmit(void *pvParameters){
err_t ret;
ESP_LOGI(TAG,"TCP发送任务开始");
for(;;){
if(xSemaphoreTake(Mutex_Wifi_Transmit,portMAX_DELAY) == pdTRUE){
if(GetLength(Queue_Wifi_Transmit) != 0){
if(GetHead(Queue_Wifi_Transmit,(uint8_t *)Wifi_Send_Data) == 1){
ret = send(connect_sock,Wifi_Send_Data,1024,0);
if(ret == ESP_OK){
if(DeLinkQueue(&Queue_Wifi_Transmit)==1){
}
}
}
}
xSemaphoreGive(Mutex_Wifi_Transmit);
}
vTaskDelay(10 / portTICK_RATE_MS);
}
}
处理完音频数据之后,开始处理视频数据,如果传RGB图片的话原始数据太大了,所以让ov2640直接输出JPEG图片,这就涉及到一个JPEG解码。
使用Jpeglib库(移植jpeglib库参考百度)来解码。
因为我这个ESP32 的开发板没有外置PSRAM,而内置的RAM空间不够,所以只能每解码10行就显示一次。屏幕驱动函数使用的是lvgl的驱动(如果RAM空间够的话也可以用LVGL显示)
#define JPEG_Decode_Line 10
int jpegDecode(void)
{
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_decompress(&cinfo);
jpeg_mem_src(&cinfo, (void *)jpeg_data, sizeof(jpeg_data));
jpeg_read_header(&cinfo, TRUE);
cinfo.out_color_space = JCS_RGB;
jpeg_start_decompress(&cinfo);
// printf("height%d\n",cinfo.output_height);
// printf("width%d\n",cinfo.output_width);
// printf("scanline%d\n",cinfo.output_scanline);
// printf("width%d\n",cinfo.output_width);
JSAMPARRAY out_buffer = (JSAMPARRAY)malloc(sizeof(JSAMPROW));
out_buffer[0] = (JSAMPROW)malloc(sizeof(JSAMPLE) * 320 * 3);
int j =0;
while (cinfo.output_scanline < cinfo.output_height)
{
jpeg_read_scanlines(&cinfo, out_buffer, 1); //空间不够,边解析边显示
memcpy(&data[j], out_buffer[0], cinfo.image_width * 3);
j += cinfo.image_width * 3;
if((cinfo.output_scanline+1)%JPEG_Decode_Line==0){
int k = 0;
j = 0;
for(int i = 0;i<JPEG_Decode_Line*320*3;i+=3){
uint16_t rgb =(((data[i+0] >> 3) & 0x1F) << 11)//b
|(((data[i+1] >> 2) & 0x3F) << 5)//g
|(((data[i+2] >> 3) & 0x1F) << 0);//r
data[k++] = (unsigned char)((rgb>>8)&0xff);
data[k++] = (unsigned char)((rgb)&0xff);
}
lcd_flush(0,((cinfo.output_scanline+1)/JPEG_Decode_Line-1)*JPEG_Decode_Line,320-1,(cinfo.output_scanline+1)-1,(uint8_t *)data);
//ESP_LOGW("解码之后","%d %d %d %d",0,((cinfo.output_scanline+1)/10-1)*10,320-1,(cinfo.output_scanline+1)-1);
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
free(out_buffer[0]);
free(out_buffer);
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
return 0;
}
解码函数编写完成后,再创建一个任务用于接受WIFI上的JEPG图片数据
void Tcp_Receive(){
ESP_LOGI(TAG,"TCP接收任务开始");
int len;
unsigned char rx_buffer[1024];
while(1)
{
memset(rx_buffer, 0, sizeof(rx_buffer)); // 清空缓存
len = recv(connect_sock, rx_buffer, 1024, 0); // 阻塞读取接收数据
if(len < 0)
{
ESP_LOGE(TAG, "Error occurred during receiving: errno %d", errno);
break;
}
else if (len == 0)
{
ESP_LOGW(TAG, "Connection closed");
break;
}
else
{
for(int i =0;i<len;i++){
jpeg_data[jpg_point] = rx_buffer[i];
jpg_point = (jpg_point + 1)%10240;
if(jpg_point == 2 || jpeg_zt == 1){
//ESP_LOGW(TAG,"%d %d",jpeg_data[0],jpeg_data[1]);
if(jpeg_data[0] == 0xff && jpeg_data[1] == 0xd8){
jpeg_zt = 1;
if(jpeg_data[jpg_point - 2] == 0xff && jpeg_data[jpg_point - 1] == 0xd9){
jpegDecode();
for(int j=0;j<=jpg_point;j++){
jpeg_data[j] = 0;
}
jpg_point = 0;
jpeg_zt = 0;
}
}else{
jpg_point = 0;
}
}
}
}
vTaskDelay(10 / portTICK_RATE_MS);
}
}
发送端
发送端使用WiFi AP模式,初始化为TCP Server用于监听接收端的连接。(具体代码:略)
发送端的任务主要是:
1.采集ov2640的JPEG数据并发送
2.接受WIFI上的音频数据并用IIS驱动MAX98357a播放
同样是先创建两个任务把音频数据解决掉,任务之间依然共享一个链队用于存储数据,链队用信号量互斥访问。
将接受到的数据存储在队列中
void Tcp_Receive(){
ESP_LOGI(TAG,"TCP接收任务开始");
int len;
unsigned char rx_buffer[1024];
while(1)
{
memset(rx_buffer, 0, sizeof(rx_buffer)); // 清空缓存
len = recv(connect_sock, rx_buffer, 1024, 0); // 阻塞读取接收数据
if(len < 0)
{
ESP_LOGE(TAG, "Error occurred during receiving: errno %d", errno);
break;
}
else if (len == 0)
{
ESP_LOGW(TAG, "Connection closed");
break;
}
else
{
ESP_LOGI(TAG, "Received %d bytes: %s", len, rx_buffer);
if(xSemaphoreTake(Mutex_wifi_RX,portMAX_DELAY)==pdTRUE){
if(GetLength(Queue_wifi_RX)<20){
if(EnLinkQueue(&Queue_wifi_RX,rx_buffer)==1){
}
}
xSemaphoreGive( Mutex_wifi_RX );
}
}
vTaskDelay(10 / portTICK_RATE_MS);
}
}
在app_main()中将数据发送给MAX98357a
while (1)
{
if(xSemaphoreTake(Mutex_wifi_RX,portMAX_DELAY) == pdTRUE){
if(GetLength(Queue_wifi_RX) != 0){
memset(Audio_Data,0,1024);
//memset(Decode_Audio_Data,0,512);
if(GetHead(Queue_wifi_RX,(uint8_t *)Audio_Data) == 1){
err_t ret = i2s_write(I2S_NUM_0, (void *)Audio_Data, 1024, &bytesOut, portMAX_DELAY);
if(ret == ESP_OK){
if(DeLinkQueue(&Queue_wifi_RX)==1){
}
}else{
printf("IIS发送失败\r\n");
}
}
}
xSemaphoreGive( Mutex_wifi_RX );
}
vTaskDelay(3 / portTICK_RATE_MS);
}
最后在创建一个任务将摄像头数据发送出去
xTaskCreate(Tcp_Transmit,"tcp_transmit_task",4096,NULL,2,&Tcp_Transmit_handle); //创建WIFI发送任务
void Tcp_Transmit(void *pvParameters){
ESP_LOGI(TAG,"TCP发送任务开始");
err_t ret;
for(;;){
pic = esp_camera_fb_get();
if(!pic)
{
ESP_LOGE(TAG, "Camera Init Failed");
}
else{
rst_count = 0;
ret = send(connect_sock, pic->buf,pic->len, 0);
//ESP_LOGI(TAG, "len = %d",pic->len);
esp_camera_fb_return(pic);
}
vTaskDelay(3 / portTICK_RATE_MS);
}
}
自此,任务全部完成。
连接完成图。
图传效果图
做的比较仓促,没有拍视频,所以声传没法演示。