ESP32 S3 OV2640 实现USB摄像头

ESP32 官方提供了一个USB 摄像头的例子,但是他们使用带有 PSRAM 的ESP32,经过研究,不支持 PSRAM的模组可以通过修改代码的方式实现相同的功能。本文以ESP32-S3-WROOM-1-N4模组(DFR0896)【参考1】为例,介绍实现方式。

首先使用这个模组制作一个底板【参考2】

接下来设计给摄像头模块使用的连接器,摄像头选择的是微雪电子的 OV2640模块。OV2640是OmniVision公司生产的一颗1/4寸的CMOS UXGA(1632*1232)图像传感器; 支持自动曝光控制、自动增益控制、自动白平衡、自动消除灯光条纹等自动控制功能。 UXGA最高15帧/秒,SVGA可达30帧,CIF可达60帧; 支持图像压缩,即可直接输出JPEG图像数据.

设计的 OV2640 Shield电路图如下,除了一个用于连接摄像头之外,还预留了一个 SD 卡座,让 ESP32 S3 板子有读写 SD 数据的能力。

PCB 设计如下:

3D预览结果:

焊接好之后的板子和 ESP32 S3 以及 OV2640 的照片:

接下来就可以进行代码的编写了。

通过 idf.py menuconfig 设定OV2640 的引脚,然后去掉PSRAM 的支持。.fb_location = CAMERA_FB_IN_DRAM 这里指定摄像头使用 ESP32 内置 RAM 即可。

连接之后即可工作。

工作的测试视频在

本文提到的电路图和PCB 在:

源代码在:

参考:

  1. https://www.dfrobot.com.cn/goods-3536.html
  2. https://mc.dfrobot.com.cn/thread-315546-1-1.html

Step to UEFI (288)Cpp UEFI 004 C++ 的 New 和 Delete

C++还有两个重要的函数:new 和 delete。根据《UEFI 原理与编程》 10.2.6 讲述,我们需要自行实现函数。

上述书籍对应的代码提供了 new 和 delete 的实现,可以看到基本的思路就是使用 gSt-> BootServices ->AllocatePool 分配和gSt-> BootServices->FreePool回收内存:

#include <UEFI/UEFI.h>
#include <type_traits>

EFI_SYSTEM_TABLE* gSt;

typedef UINTN size_t;

void *  operator new( size_t Size )
{
    void       *RetVal;
    EFI_STATUS  Status;

    if( Size == 0) {
        return NULL;
    }

    Status = gSt-> BootServices ->AllocatePool( EfiLoaderData, (UINTN)Size, &RetVal);
    if( Status != EFI_SUCCESS) {
        RetVal  = NULL;
    }
    return RetVal;
}


void *  operator new[]( size_t cb )
{
    void *res = operator new(cb);
    return res;
}

void operator delete( void * p )
{ 
  if(p != NULL) 
    (void) gSt-> BootServices->FreePool (p);
}

void operator delete[]( void * p )
{
    operator delete(p);
}

void printInt(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL* conOut, int value) {
	CHAR16 out[32];
	CHAR16* ptr = out;
	static_assert(std::is_unsigned_v<char16_t>);
	if (value == 0)
	{
		conOut->OutputString(conOut, u"0");
		return;
	}

	ptr += 31;
	*--ptr = 0;
	int tmp = value;// >= 0 ? value : -value; 

	while (tmp)
	{
		*--ptr = '0' + tmp % 10;
		tmp /= 10;
	}

	if (value < 0) *--ptr = '-';
	conOut->OutputString(conOut, ptr);
}

EFI_STATUS
efi_main(EFI_HANDLE /*image*/, EFI_SYSTEM_TABLE* systemTable)
{
	gSt=systemTable;
	int *p=new int;
	
	*p=123;
	
	printInt(gSt->ConOut,*p);
	gSt->ConOut->OutputString(gSt->ConOut, u"\r\n");

	delete p;
	
	return EFI_SUCCESS;
}

运行之后可以在屏幕上看到 123 的字样。

接下来实验使用 new 和 delete 创建对象的情况,基本的代码如下:

class Time {
 
public:
	Time() {//构造函数
		gSt->ConOut->OutputString(gSt->ConOut, u"Init\n\r");
	}
	~Time(){//析构函数
		gSt->ConOut->OutputString(gSt->ConOut, u"Destroy\n\r");
    }
 
private:
	int _hour;
	int _min;
	int _sec;
};

EFI_STATUS
efi_main(EFI_HANDLE /*image*/, EFI_SYSTEM_TABLE* systemTable)
{
	gSt=systemTable;
	Time *myTime=new Time;
	delete myTime;
	return EFI_SUCCESS;
}

运行结果如下:

可以看到,当我们 new 创建对象的时候,自动运行了 Time 的构造函数。构造函数的作用是:当该类对象被创建的时候,编译系统对象分配内存空间,并自动调用该构造函数,由构造函数完成成员的初始化工作,故:构造函数的作用:初始化对象的数据成员。同样的还有一个“析构函数”,用于做一些清理的洞动作。

有兴趣的朋友可以进一步阅读如下文件:

  1. https://blog.csdn.net/qq_21438461/article/details/129651522 C/C++ 内存分配 new 操作符:剖析new操作符的实现机制和使用技巧
  2. https://blog.csdn.net/sinat_31608641/article/details/102892951 C++关键字new的原理

模组 GD未被安装或已被禁用解决方法

似乎国内使用 Windows IIS 架设 Wordpress 的用户非常少,以至于我遇到问题通常只能在英文网站中搜索到需要的信息。最近遇到了Wordpress 的网站健康提示“模组 GD未被安装或已被禁用”的问题。经过搜索答案非常简单:PHP 的配置文件 php.ini 中默认禁止了 GD2, 但是实际上内置的是GD。因此将对应的那一行取消注释,并且将 GD2 修改为 GD 即可。

Arduino Leonardo PWM 测试

用户可以从串口输入一个在1-255的数字,然后在D9上输出对应的占空比,PWM 频率是 62.5KHz。

需要注意:如果需要输出全低或者全高需要修改代码。

// Leonardo 测试,在 D9 上输出从串口给定的PWM 值
void setup() {
  Serial.begin(115200);

  /* Set speakers as outputs */
  DDRB   |= ((1 << 6) | (1 << 5));

  /* PWM speaker timer initialization */
  TCCR1A  = ((1 << WGM10) | (1 << COM1A1) | (1 << COM1A0)
             | (1 << COM1B1) | (1 << COM1B0)); // Set on match, clear on TOP
  TCCR1B  = ((1 << WGM12) | (1 << CS10));  // Fast 8-Bit PWM, F_CPU speed

}

void loop() {
  if (Serial.available() > 0) {
    //读取一个整数
    int Value = Serial.parseInt();
    Serial.print("Get:");
    Serial.println(Value);
    if (Value > 255)||(Value==0) {
      Serial.println("Please input a 0<number<256");
    } else {
      OCR1A = Value;
    }
  }
}

Step to UEFI (287)Cpp UEFI 002 Cout

我们看到的最简单的 C++ 代码是如下形式:

int main()
{
    std::cout << "Hello World!\n";
}

问题来了:如何在 UEFI 下面实现这种形式的代码?根据【参考1】,cout << n; 中,<< 是个运算符,n 是个变量,运算符应该接的是变量,所以 cout是个变量,但是在C++中这种高级变量叫做对象。cout 是一个对象。

因此,我们可以通过定义 cout 这个对象,然后定义 << 这个运算符即可。完整代码如下:

#include <UEFI/UEFI.h>
#include <type_traits>

#define EFI_ERROR(status) ((status) != EFI_SUCCESS)

EFI_SYSTEM_TABLE* gSystemTable;

void printInt(int value) {
	CHAR16 out[32];
	CHAR16* ptr = out;
	static_assert(std::is_unsigned_v<char16_t>);
	if (value == 0)
	{
		gSystemTable->ConOut->OutputString(gSystemTable->ConOut, u"0");
		return;
	}

	ptr += 31;
	*--ptr = 0;
	int tmp = value;// >= 0 ? value : -value; 

	while (tmp)
	{
		*--ptr = '0' + tmp % 10;
		tmp /= 10;
	}

	if (value < 0) *--ptr = '-';
	gSystemTable->ConOut->OutputString(gSystemTable->ConOut, ptr);
}

class ostream {
public:
    void operator<<(int x);
};

void ostream::operator<<(int x) {
    printInt(x);
    return ;
}

ostream cout;

EFI_STATUS
efi_main(EFI_HANDLE /*image*/, EFI_SYSTEM_TABLE* systemTable)
{
	gSystemTable=systemTable;
	cout << 122;
	gSystemTable->ConOut->OutputString(gSystemTable->ConOut, u"\r\n");
	return EFI_SUCCESS;
}

运行结果如下:

已经非常像了。接下来还有一个 std 的问题。这个可以通过 Namespace来实现。“编写程序过程中,名称(name)可以是符号常量、变量、函数、结构、枚举、类和对象等等。工程越大,名称互相冲突性的可能性越大。另外使用多个厂商的类库时,也可能导致名称冲突。为了避免,在大规模程序的设计中,以及在程序员使用各种各样的 C++ 库时,这些标识符的命名发生冲突,标准 C++ 引入关键字 namespace(命名空间/名字空间/名称空间),可以更好地控制标识符的作用域。

例如,我们在 C 语言中,通过 static 可以限制名字只在当前编译单元内可见,在 C++ 中我们通过 namespace 来控制对名字的访问。”【参考2】

修改代码如下形式:

namespace std {
	
class ostream {
public:
    void operator<<(int x);
};

void ostream::operator<<(int x) {
    printInt(x);
    return ;
}
ostream cout;

}

namespace 是C++中的关键字,用来定义一个命名空间,语法格式为:

namespace name{
    //variables, functions, classes
}

name是命名空间的名字,它里面可以包含变量、函数、类、typedef、#define 等,最后由{ }包围【参考3】。

我们就可以直接使用 std::cout << 122; 这种形式了。接下来,还有如何实现 std::cout << 122 << 13;  有兴趣的朋友可以继续研究。

参考:

  1. https://blog.csdn.net/u011386173/article/details/121085201
  2. https://baijiahao.baidu.com/s?id=1662580430712018597&wfr=spider&for=pc
  3. https://c.biancheng.net/view/2192.html

Windows 内存占用工具

最近因为测试需要一款能够占用内存的软件,于是求助天杀,请他帮忙编写了一个能够占用指定内存大小的代码。

在使用之前因为微软的限制需要对 Windows进行一些设定:

1.运行 gpedit.msc ,打开“本地组策略编辑器”

2.找到位于 “计算机配置”-> “Windows设置”->“安全设置”->“本地策略”->“用户权限分配”中的“锁定内存页”

3.接下来的目标是将“Administrators”加入其中。点击“添加用户组或组”。

4.点击“对象类型”按钮,勾选其中的“组”

5. 之后在“输入对象名称来选择”中输入“Administrators”(注意末尾有“s”),然后点击“检查名称”按钮

6.重启系统后以管理员权限打开 cmd 串口。这时候你可能遇到无法正常显示汉字的问题,例如:

7. 使用 chcp 936 切换到中文,再次运行即可,程序运行之后要求你输入的需要占用的内存,比如,这里输入 1024 ,可以在任务管理器中看到内存使用率升高了。按任意键之后释放占用的内存。

8.还可以运行多个程序方便进行内存调整

源代码和可执行程序:

串口速度测试工具

写了一个简单的串口测试工具,测试的是写入的速度。简单的说,就是打开串口,然后向里面写入数值,计算写入耗费的时间。通常来说,我们使用 USB 转串口设备,决定速度的因素有两个:1. USB 处理数据的时间 2.设备转串口的速度。其中最主要的因素是后者。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO.Ports;                 // For SerialPort
using System.Threading;
using System.IO;
using System.Diagnostics;



namespace _433CMD
{

    class Program
    {
        const int COUNTER = 3;
        static void Main(string[] args)
        {
            Stopwatch stopwatch = new Stopwatch();

            if (args.Count() != 2)
            {
                Console.WriteLine("Please input COM PORT Number and ON or OFF");
                Console.WriteLine("Usage:");
                Console.WriteLine("SST COM3 115200");
                Environment.Exit(1);
            }

            if (args[0].IndexOf("COM") == -1)
            {
                Console.WriteLine("Parameters error");
                Environment.Exit(2);
            }

            int BaudRate;

            if (int.TryParse(args[1], out BaudRate))
            {
                Console.WriteLine(BaudRate);
            }
            else
            {
                Console.WriteLine("BaudRate wrong");
            }

            SerialPort serialPort1 = new SerialPort();
            serialPort1.PortName = args[0];

            serialPort1.BaudRate = BaudRate;
            serialPort1.DataBits = 8;
            serialPort1.Parity = 0;
            serialPort1.StopBits = (StopBits)1;
            serialPort1.Encoding = System.Text.Encoding.GetEncoding(28591);
            serialPort1.DtrEnable = true;
            serialPort1.RtsEnable = true;
            // Buffer 长1秒
            Byte[]  Buffer = new Byte[BaudRate/10];

            stopwatch.Start();
            try
            {
                // 打开串口
                serialPort1.Open();
                for (int i=0;i< COUNTER;i++)
                {
                    serialPort1.Write(Buffer,  0, Buffer.Length);
                }
                
                serialPort1.Close(); 
            }
            catch (IOException e)
            {
                Console.WriteLine("Open " + args[1] + " failed ");
                Console.WriteLine(e.Message);
                Environment.Exit(4);
            }
            stopwatch.Stop();
            TimeSpan ts = stopwatch.Elapsed;
            Console.WriteLine("Send " + (Buffer.Length  *COUNTER /1024).ToString() + "KB in " +(ts.TotalMilliseconds/1000).ToString("F3")+"s");
            Console.WriteLine("Serial speed: "+(Buffer.Length * COUNTER/1024 / (ts.TotalMilliseconds/1000)).ToString("F3") +"KBytes/s");
            Console.ReadLine();
        }
        
    }
}

使用 CH343 进行测试:

编译后的 EXE 下载:

Ch569 项目从EXAM目录独立出来的方法

在使用 MounRiver 的过程中,你可能会遇到将 Exam中的例子搬移到其他路径之后无法工作的问题,这是由于Exam项目都依赖了\EXAM\SRC 下面的文件。解决这个问题的方法是:

  1. 移动目录到你需要的路径下;
  2. 打开项目编译会报错,会给出你找不到的文件名称
  3. 在原始项目中确认这个文件在项目中的位置,比如:下图的 Peripheral目录。
  4. 在Peripheral目录上点击右键查看属性,然后使用  edit 修改 Location。个人推荐直接将\EXAM\SRC放置到你的项目中,然后修改相对路径指向这个新的位置;

重复2-5步骤,直到所有的错误消失。

附件是一个按照上面修改过的 SimulateCDC 的例子, 可以放在任意的位置。

参考:

1.https://www.wch.cn/downloads/CH569EVT_ZIP.html

Step to UEFI (286)Cpp UEFI 001 类构造函数

C++中使用关键字 class 来定义类, 其基本形式如下:

class 类名
{
public:
//行为或属性 
protected:
//行为或属性
private:
//行为或属性
};

有一种比较特别的函数,被称为“构造函数”,名称和类名称相同。在创建类的对象时,编译器就运行一个构造函数。

设计一个Time类如下,其中有2个构造函数,其中是一个是构造函数的重载。如果在创建过程中有加参数,那么会调用重载之后的构造函数。

class Time {
public:
	Time() {//构造函数
		_hour = 9;
		_min = 17;
		_sec = 20;
		gSystemTable->ConOut->OutputString(gSystemTable->ConOut, u"Time init1\n\r");
	}
	Time(int hour,int min,int sec){//对构造函数的重载
        _hour=hour;
        _min=min;
        _sec=sec;
		gSystemTable->ConOut->OutputString(gSystemTable->ConOut, u"Time init2\n\r");
    }
	void Print() {
		printInt(gSystemTable->ConOut,_hour);
		printInt(gSystemTable->ConOut,_min);
		printInt(gSystemTable->ConOut,_sec);
		gSystemTable->ConOut->OutputString(gSystemTable->ConOut, u"\n\r");
	}
 
private:
	int _hour;
	int _min;
	int _sec;
};

完整代码如下:

#include <UEFI/UEFI.h>
#include <type_traits>

#define EFI_ERROR(status) ((status) != EFI_SUCCESS)

EFI_SYSTEM_TABLE* gSystemTable;

	
void printInt(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL* conOut, int value) {
	CHAR16 out[32];
	CHAR16* ptr = out;
	static_assert(std::is_unsigned_v<char16_t>);
	if (value == 0)
	{
		conOut->OutputString(conOut, u"0");
		return;
	}

	ptr += 31;
	*--ptr = 0;
	int tmp = value;// >= 0 ? value : -value; 

	while (tmp)
	{
		*--ptr = '0' + tmp % 10;
		tmp /= 10;
	}

	if (value < 0) *--ptr = '-';
	conOut->OutputString(conOut, ptr);
}


class Time {
 public:
	Time() {//构造函数
		_hour = 9;
		_min = 17;
		_sec = 20;
		gSystemTable->ConOut->OutputString(gSystemTable->ConOut, u"Time init1\n\r");
	}
	Time(int hour,int min,int sec){//对构造函数的重载
        _hour=hour;
        _min=min;
        _sec=sec;
		gSystemTable->ConOut->OutputString(gSystemTable->ConOut, u"Time init2\n\r");
    }
	void Print() {
		printInt(gSystemTable->ConOut,_hour);
		printInt(gSystemTable->ConOut,_min);
		printInt(gSystemTable->ConOut,_sec);
		gSystemTable->ConOut->OutputString(gSystemTable->ConOut, u"\n\r");
	}
 
private:
	int _hour;
	int _min;
	int _sec;
};

EFI_STATUS
efi_main(EFI_HANDLE /*image*/, EFI_SYSTEM_TABLE* systemTable)
{
	EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL* conOut = systemTable->ConOut;
	gSystemTable=systemTable;
	
	Time time1;
	Time time2(1,2,3);
	
	return EFI_SUCCESS;
}

上述代码运行结果如下:

此外,还有析构函数,与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

有兴趣的朋友可以自行研究。

参考:

  1. https://blog.csdn.net/qq_35243382/article/details/124369838
  2. https://blog.csdn.net/qq_39117115/article/details/133484338