esp32c3+atshell测试ov7670

参考

嵌入式终端AtShell.csdn

树莓派Pico‌移植Atshell.csdn

嵌入式终端AtShell的精简版.csdn

时序

bash 复制代码
一帧
VSYNC  ‾‾‾\_______________________________________________/‾‾‾‾‾
            每帧开头一个高脉冲(VGA 30fps ≈ 30Hz),告诉主机"新的一帧开始了"

一行
HREF   ______/‾‾‾‾‾‾‾‾‾‾‾‾‾‾\______________/‾‾‾‾‾‾‾‾‾‾‾‾‾‾\______
            高电平期间才是有效像素,低电平是行消隐

PCLK   _/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_
        每个上升沿对应一个字节

D0~D7  ═╪═X0═╪═X1═╪═X2═╪═X3═╪═X4═╪ ...   HREF 高时才有意义

OV7670 引脚功能

1. 通用 18 脚模块(2×9 排针)

引脚(丝印) 方向 功能 说明
3V3 / VCC 电源输入 模块供电 只能接 3.3V,别接 5V;板载 LDO 转 2.8V 给传感器
GND 地 必须共地 和 ESP32 共地,否则 SCCB 一定不通
SCL / SIOC 输入 SCCB 时钟 类 I2C 总线,≤400kHz,建议 100kHz
SDA / SIOD 双向 SCCB 数据 7bit 地址 0x21(写 0x42 / 读 0x43)
VSYNC 输出 帧同步 每帧一个高脉冲;VGA 30fps ≈ 30Hz
HREF 输出 行有效 高电平期间 D0~D7 上的数据有效
D0~D7 输出 像素数据 8 位并行,D7 为 MSB;YUV422 / RGB565
PCLK 输出 像素时钟 每个边沿送 1 字节;默认 PCLK = XCLK
XCLK / XVCLK 输入 主时钟 6~27MHz(典型 24MHz),由主机提供
PWDN 输入 掉电控制 高电平=掉电,接 GND 才工作
RESET / RST 输入 复位 低电平=复位,接 3V3 或悬空

方向以 OV7670 为基准:输入=主机给它,输出=它给主机。

2. FIFO 版(AL422B 蓝板)额外引脚

引脚 方向 功能 说明
RCLK / RCK 输入 FIFO 读时钟 主机提供,读速决定取图速度
WR 输入 FIFO 写使能 低有效,把摄像头数据写进 FIFO
RRST 输入 读指针复位 低有效,读之前复位
WRST 输入 写指针复位 低有效,写之前复位
OE 输入 输出使能 低有效,允许 FIFO 输出到 D0~D7
FIFO/WR 跳线 --- 模式选择 部分板子用来选直通或 FIFO
5V/3.3V 跳线 --- 供电选择 有的话确认拨在 3.3V

FIFO 版的 VSYNC/HREF 有的被接到 FIFO 上,有的仍引出,看丝印。

3. 本项目实际接线(ESP32-C3)

OV7670 ESP32-C3 用途 必须
3V3 3V3 供电 ✅
GND GND 共地 ✅
SIOC GPIO5 SCCB 时钟 ✅
SIOD GPIO4 SCCB 数据 ✅
XCLK GPIO1 8MHz 主时钟 ✅ 不接连 ID 都读不到
PCLK GPIO3 只做"在不在出图"的检查 可选
PWDN GND 拉低使能 建议接
RST 3V3 释放复位 建议接
D0~D7 / VSYNC / HREF 不接 C3 无 DVP,不能正常采图 ---

at_app.cpp

c 复制代码
/* ============================================================================
 * at_app.cpp ------ AT/msh 命令: OV7670 SCCB 自检 + XCLK / PCLK 检查
 *
 * 本板(ESP32-C3)接线:
 *
 *   OV7670 模块        ESP32-C3     说明
 *   -----------------  -----------  ------------------------------------------
 *   3V3 / VCC          3V3          模块板载 2.8V LDO, 千万别接 5V
 *   GND                GND          必须共地
 *   SIOC (SCL)         GPIO5        SCCB 时钟, 100kHz
 *   SIOD (SDA)         GPIO4        SCCB 数据
 *   XCLK               GPIO1        LEDC 输出 8MHz; 没有它连 SCCB 都不应答
 *   PCLK               GPIO3        只用来判断"有没有在出图", 不采像素
 *   PWDN               GND          低电平=工作, 悬空可能被拉高
 *   RST / RESET        3V3          低电平复位, 悬空一般也行
 *   D0~D7/VSYNC/HREF   不接          C3 没有 DVP/LCD_CAM, 采不了图
 *
 *
 * 命令:
 *   ov7670        扫总线 + 读 PID/VER/MIDH/MIDL, 判断摄像头是否在线
 *   ov7670 xclk   先给 OV7670 输出 XCLK, 再跑上面的完整测试
 *   ov7670 pclk   配 PCLK=XCLK, 并检查 PCLK 引脚(GPIO3)上有没有波形
 *   ov7670 0x12   只读一个寄存器(地址按 16 进制)
 * ========================================================================== */

#include "at_app.h"
#include "AtShell.h"
#include "driver/uart.h"            // UART 驱动
#include "driver/gpio.h"            // GPIO 编号
#include "driver/i2c_master.h"      // I2C 驱动
#include "driver/ledc.h"            // XCLK 用 LEDC 输出
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static const char *TAG = "atApp";

static int user_at_write(uint8_t* srcBuf, uint32_t toSendLen,uint32_t timeout) {
    /* AT shell 输出走板载 USB 虚拟串口,原来用 UART_NUM_0 */
    //return bsp_usb_uart_write(srcBuf, toSendLen, timeout);
    return uart_write_bytes(UART_NUM_0, srcBuf, toSendLen);
}

#define RX_BUF_SIZE         2048    /* 缓冲区大小 */
static unsigned char s_buf[RX_BUF_SIZE] = {0};


/* ---------------- AT shell 的串口(UART0) ----------------
 * 只依赖 IDF 官方驱动。原来调 bsp_uart_init(), 它会把 UART1 一起装到
 * GPIO4/5 上, 跟 OV7670 的 SCCB 抢同一组脚; 这里只装 UART0。
 * 代价: 引脚写死成本板的 GPIO20(RXD)/GPIO21(TXD)。
 */
#define AT_UART_NUM         UART_NUM_0
#define AT_UART_TX_PIN      GPIO_NUM_21
#define AT_UART_RX_PIN      GPIO_NUM_20

static void at_uart_init(void)
{
    if (uart_is_driver_installed(AT_UART_NUM)) {
        return;
    }
    uart_config_t cfg = {};
    cfg.baud_rate  = 115200;
    cfg.data_bits  = UART_DATA_8_BITS;
    cfg.parity     = UART_PARITY_DISABLE;
    cfg.stop_bits  = UART_STOP_BITS_1;
    cfg.flow_ctrl  = UART_HW_FLOWCTRL_DISABLE;
    cfg.source_clk = UART_SCLK_DEFAULT;

    uart_param_config(AT_UART_NUM, &cfg);
    uart_set_pin(AT_UART_NUM, AT_UART_TX_PIN, AT_UART_RX_PIN, -1, -1);
    uart_driver_install(AT_UART_NUM, 1024, 0, 0, NULL, 0);
}



static int s_test(int argc, char** argv) {
    at_printf("s_test argc %d:\r\n", argc);
    return 0;
}



/* ===================== OV7670 SCCB 自检 =====================
 * OV7670 的控制口是 SCCB: 电气/协议跟 I2C 兼容, 但不是标准 I2C 器件。
 *   - 7bit 地址 0x21 (写 0x42 / 读 0x43), SCL 最高 400k, 用 100k 最稳
 *   - 第 9 位是 don't care -> 挂设备时关掉 ACK 检查
 *   - 读寄存器: 先发寄存器地址并 STOP, 再单独读 1 字节(SCCB 不支持重复起始)
 *
 * 命令:
 *   ov7670        扫总线 + 读 PID/VER/MID, 判断摄像头是否在线
 *   ov7670 xclk   先给 OV7670 输出 XCLK, 再跑上面的完整测试
 *   ov7670 pclk   让 PCLK = XCLK, 并检查 PCLK 引脚上有没有信号
 *   ov7670 0x12   只读一个寄存器(地址按 16 进制)
 * ============================================================== */
#define OV7670_ADDR        0x21
#define OV7670_SCL_HZ      100000

/* OV7670 主时钟 XCLK, 由 LEDC 输出。
 * GPIO1 在 Readme 里是 BAT_VOL(电池检测), 但固件里没有用到它;
 * 如果板上确实焊了分压电阻、想避开, 改成 GPIO3(TEST_KEY) 即可。
 * 频率 8MHz 在 OV7670 的 6~27MHz 范围内, 想跑 24MHz 改这里。 */
#define OV7670_XCLK_IO     1
#define OV7670_XCLK_HZ     8000000

static bool s_xclk_on = false;

static esp_err_t ov_xclk_start(void)
{
    if (s_xclk_on) {
        return ESP_OK;
    }

    ledc_timer_config_t tcfg = {};
    tcfg.speed_mode      = LEDC_LOW_SPEED_MODE;
    tcfg.duty_resolution = LEDC_TIMER_1_BIT;    /* 1bit: 占空比 1/2 = 50% */
    tcfg.timer_num       = LEDC_TIMER_0;
    tcfg.freq_hz         = OV7670_XCLK_HZ;
    tcfg.clk_cfg         = LEDC_AUTO_CLK;
    esp_err_t err = ledc_timer_config(&tcfg);
    if (err != ESP_OK) {
        return err;
    }

    ledc_channel_config_t ccfg = {};
    ccfg.gpio_num   = OV7670_XCLK_IO;
    ccfg.speed_mode = LEDC_LOW_SPEED_MODE;
    ccfg.channel    = LEDC_CHANNEL_0;
    ccfg.timer_sel  = LEDC_TIMER_0;
    ccfg.duty       = 1;
    ccfg.hpoint     = 0;
    err = ledc_channel_config(&ccfg);
    if (err != ESP_OK) {
        return err;
    }

    s_xclk_on = true;
    return ESP_OK;
}

static i2c_master_bus_handle_t s_ov_bus = NULL;
static i2c_master_dev_handle_t s_ov_dev = NULL;

/* 摄像头用的 I2C 端口/引脚。ESP32-C3 只有一个 I2C 控制器, 端口固定 0,
 * 所以这里建好之后, 别处不要再对 I2C_NUM_0 调 i2c_new_master_bus()。 */
#define OV7670_I2C_PORT    I2C_NUM_0
#define OV7670_SDA_IO      GPIO_NUM_4
#define OV7670_SCL_IO      GPIO_NUM_5

/* 建 I2C 总线 + 挂上 0x21。
 * 端口上已经有总线时直接复用(引脚以先建的那条为准)。 */
static esp_err_t bsp_ov7670_iic_init(void)
{
    if (s_ov_dev != NULL) {
        return ESP_OK;
    }

    if (s_ov_bus == NULL) {
        i2c_master_bus_config_t bus_cfg = {};
        bus_cfg.i2c_port            = OV7670_I2C_PORT;
        bus_cfg.sda_io_num          = OV7670_SDA_IO;
        bus_cfg.scl_io_num          = OV7670_SCL_IO;
        bus_cfg.clk_source          = I2C_CLK_SRC_DEFAULT;
        bus_cfg.glitch_ignore_cnt   = 7;
        bus_cfg.flags.enable_internal_pullup = true;

        esp_err_t err = i2c_new_master_bus(&bus_cfg, &s_ov_bus);
        if (err != ESP_OK) {
            /* 端口已经被别的模块建过 -> 直接用那条总线 */
            if (i2c_master_get_bus_handle(OV7670_I2C_PORT, &s_ov_bus) != ESP_OK) {
                return err;
            }
        }
    }

    i2c_device_config_t cfg = {};
    cfg.dev_addr_length = I2C_ADDR_BIT_LEN_7;
    cfg.device_address  = OV7670_ADDR;
    cfg.scl_speed_hz    = OV7670_SCL_HZ;
    cfg.flags.disable_ack_check = 1;    /* SCCB 第 9 位是 don't care */

    return i2c_master_bus_add_device(s_ov_bus, &cfg, &s_ov_dev);
}

/* SCCB 读: 写寄存器地址(STOP) + 单独读 1 字节 */
static esp_err_t ov_read_reg(uint8_t reg, uint8_t *val)
{
    esp_err_t err = i2c_master_transmit(s_ov_dev, &reg, 1, 100);
    if (err != ESP_OK) {
        return err;
    }
    return i2c_master_receive(s_ov_dev, val, 1, 100);
}

/* SCCB 写: 寄存器地址 + 数据 */
static esp_err_t ov_write_reg(uint8_t reg, uint8_t val)
{
    uint8_t buf[2] = { reg, val };
    return i2c_master_transmit(s_ov_dev, buf, sizeof(buf), 100);
}


/* ---------------- PCLK 存在性检查 ----------------
 * ESP32-C3 没有 PCNT / MCPWM 捕获单元, 用 RMT 抓 8MHz 连续时钟时,
 * 驱动每收满半个内存块就进一次中断拷贝数据, 8MHz 下间隔只有 3us,
 * 中断耗时超过间隔 -> 任务被饿死, 直接 Task WDT(实测)。
 * 所以这里只做存在性检查: 快速采样 1000 次, 0/1 混着出现就说明在翻转。
 * 频率由 CLKRC(0x11) 决定: 0x80 = PLL 关、1 分频 = XCLK。
 *
 * 接线: OV7670 PCLK -> GPIO3 (TEST_KEY, 本板空闲脚)
 */
#define OV7670_PCLK_IO     GPIO_NUM_3

static int ov_pclk_measure(void)
{
    int ones = 0;
    for (int i = 0; i < 1000; i++) {
        ones += gpio_get_level(OV7670_PCLK_IO) ? 1 : 0;
    }

    at_printf("[ov7670] pclk: GPIO%d level probe %d/1000 high\r\n",
              (int)OV7670_PCLK_IO, ones);
    if (ones == 0 || ones == 1000) {
        at_printf("[ov7670] pclk: pin stuck at %d -> no signal\r\n", ones ? 1 : 0);
        at_printf("  - OV7670 PCLK 接到 GPIO3 了吗? (不是 4/5)\r\n");
        at_printf("  - PWDN 拉低 / RESET 拉高 / XCLK 在跑?\r\n");
        return -1;
    }

    at_printf("[ov7670] pclk: PCLK is running (edges seen on GPIO%d)\r\n",
              (int)OV7670_PCLK_IO);
    at_printf("[ov7670] pclk: CLKRC(0x11)=0x80 -> PCLK = XCLK = %d Hz\r\n",
              OV7670_XCLK_HZ);
    return 0;
}

static int s_ov7670(int argc, char **argv)
{
    esp_err_t err = bsp_ov7670_iic_init();
    if (err != ESP_OK || s_ov_bus == NULL || s_ov_dev == NULL) {
        at_printf("[ov7670] i2c init fail: %s\r\n", esp_err_to_name(err));
        return -1;
    }
    i2c_master_bus_handle_t bus = s_ov_bus;

    /* ---- ov7670 xclk : 先给时钟, 再继续跑完整测试 ---- */
    if (argc >= 2 && strcmp(argv[1], "xclk") == 0) {
        err = ov_xclk_start();
        if (err != ESP_OK) {
            at_printf("[ov7670] xclk start fail: %s\r\n", esp_err_to_name(err));
            return -1;
        }
        at_printf("[ov7670] XCLK %d Hz on GPIO%d -> OV7670 XCLK\r\n",
                  OV7670_XCLK_HZ, OV7670_XCLK_IO);
        vTaskDelay(pdMS_TO_TICKS(50));      /* 等摄像头内部稳住 */
    }
    /* ---- ov7670 pclk : 配 PCLK=XCLK, 再检查 GPIO3 上有没有波形 ---- */
    else if (argc >= 2 && strcmp(argv[1], "pclk") == 0) {
        err = ov_xclk_start();
        if (err != ESP_OK) {
            at_printf("[ov7670] xclk start fail: %s\r\n", esp_err_to_name(err));
            return -1;
        }
        /* CLKRC(0x11)=0x80: PLL 关、1 分频 -> PCLK = XCLK
         * COM7(0x12)=0x00: 正常输出, VGA + YUV (上电默认值, 写上更保险) */
        (void)ov_write_reg(0x11, 0x80);
        (void)ov_write_reg(0x12, 0x00);
        vTaskDelay(pdMS_TO_TICKS(100));
        uint8_t clkrc = 0;
        uint8_t com7  = 0;
        (void)ov_read_reg(0x11, &clkrc);
        (void)ov_read_reg(0x12, &com7);
        at_printf("[ov7670] CLKRC(0x11)=0x%02X  COM7(0x12)=0x%02X\r\n", clkrc, com7);
        at_printf("[ov7670] OV7670 PCLK -> GPIO3, checking...\r\n");
        return ov_pclk_measure();
    }
    /* ---- ov7670 <reg> : 只读一个寄存器 ---- */
    else if (argc >= 2) {
        char *end = NULL;
        long reg = strtol(argv[1], &end, 16);
        if (end == argv[1] || reg < 0 || reg > 0xFF) {
            at_printf("[ov7670] bad reg: %s (e.g. ov7670 0x0a)\r\n", argv[1]);
            return -1;
        }
        uint8_t v = 0;
        esp_err_t e = ov_read_reg((uint8_t)reg, &v);
        if (e != ESP_OK) {
            at_printf("[ov7670] reg 0x%02X read fail: %s\r\n", (unsigned)reg, esp_err_to_name(e));
            return -1;
        }
        at_printf("[ov7670] reg 0x%02X = 0x%02X (%u)\r\n", (unsigned)reg, v, v);
        return 0;
    }

    at_printf("[ov7670] I2C0 SDA=GPIO4 SCL=GPIO5 addr=0x%02X @%dkHz\r\n",
              OV7670_ADDR, OV7670_SCL_HZ / 1000);

    /* ---- 1. 0x21 有没有 ACK ---- */
    esp_err_t perr = i2c_master_probe(bus, OV7670_ADDR, 50);
    if (perr == ESP_ERR_TIMEOUT) {
        at_printf("[ov7670] probe 0x%02X: TIMEOUT, check pull-up/wiring\r\n", OV7670_ADDR);
        at_printf("        SDA(4)/SCL(5) stuck low? need 4.7k pull-up to 3V3\r\n");
        return -1;
    }
    at_printf("[ov7670] probe 0x%02X: %s\r\n", OV7670_ADDR,
              (perr == ESP_OK) ? "ACK" : "NACK");

    /* ---- 2. 扫总线, 看总线上都有谁(顺便确认总线本身是通的) ---- */
    char found[256] = {0};
    int n = 0;
    for (uint8_t a = 0x08; a <= 0x77; a++) {
        if (i2c_master_probe(bus, a, 20) == ESP_OK) {
            int len = (int)strlen(found);
            snprintf(found + len, sizeof(found) - (size_t)len, "0x%02X ", a);
            n++;
        }
    }
    at_printf("[ov7670] scan: %s\r\n", n ? found : "none (nothing on the bus)");

    /* ---- 3. 读 ID 寄存器 ---- */
    static const struct {
        uint8_t     reg;
        uint8_t     expect;
        const char *name;
    } ids[] = {
        {0x0A, 0x76, "PID"},
        {0x0B, 0x73, "VER"},
        {0x1C, 0x7F, "MIDH"},
        {0x1D, 0xA2, "MIDL"},
    };
    int ok = 0;
    for (unsigned i = 0; i < sizeof(ids) / sizeof(ids[0]); i++) {
        uint8_t v = 0;
        esp_err_t e = ov_read_reg(ids[i].reg, &v);
        if (e != ESP_OK) {
            at_printf("[ov7670] 0x%02X %-4s read fail: %s\r\n",
                      ids[i].reg, ids[i].name, esp_err_to_name(e));
            continue;
        }
        bool match = (v == ids[i].expect);
        at_printf("[ov7670] 0x%02X %-4s = 0x%02X expect 0x%02X %s\r\n",
                  ids[i].reg, ids[i].name, v, ids[i].expect, match ? "OK" : "NG");
        if (match) {
            ok++;
        }
    }

    /* ---- 4. 结论 + 排查提示 ---- */
    if (ok == 4) {
        at_printf("[ov7670] PASS: OV7670 online\r\n");
        return 0;
    }

    at_printf("[ov7670] FAIL: no OV7670 ID\r\n");
    at_printf("  1) SIOD->GPIO4  SIOC->GPIO5, 3V3/GND not swapped\r\n");
    at_printf("  2) 4.7k pull-up on SIOC/SIOD (most modules have it, to 2.8V)\r\n");
    at_printf("  3) PWDN low, RESET high (floating is usually ok)\r\n");
    at_printf("  4) some modules need XCLK before they answer: run 'ov7670 xclk'\r\n");
    at_printf("  5) GPIO4/5 are also UART1 TXD/RXD, don't fight with the base board\r\n");
    return -1;
}



void at_app_init(void){
  // bsp_usb_uart_init();
    at_uart_init();
    at_init(user_at_write);
    AT_CMD_EXPORT(s_test);
    /* 命令名用 ov7670, 实现是 s_ov7670 */
    AT_SHELL_EXPORT(ov7670, ov7670 sccb test, s_ov7670, 0);
}

void at_app_loop(void){
    /* 读板载 USB 虚拟串口,原来用 uart_read_bytes(UART_NUM_0, s_buf, 1, pdMS_TO_TICKS(20)) */
   // int len = bsp_usb_uart_read(s_buf, 1, 20);
    int len= uart_read_bytes(UART_NUM_0, s_buf, 1, pdMS_TO_TICKS(20));
    if (len > 0) {
        at_import(s_buf, len, 0);
    }
    at_export();
}

测试

bash 复制代码
$:ov7670 pclk
[ov7670] CLKRC(0x11)=0x80  COM7(0x12)=0x00
[ov7670] OV7670 PCLK -> GPIO3, checking...
[ov7670] pclk: GPIO3 level probe 599/1000 high
[ov7670] pclk: PCLK is running (edges seen on GPIO3)
[ov7670] pclk: CLKRC(0x11)=0x80 -> PCLK = XCLK = 8000000 Hz
相关推荐
橘为己有44124 分钟前
51单片机:流水灯
单片机·51单片机
不脱发的程序猿36 分钟前
STM32和VS1053制作电子琴
stm32·单片机·嵌入式硬件·midi·vs1053
Zwawa14 小时前
C语言函数指针与宏实战:从回调、表驱动菜单到X-Macro,讲透嵌入式代码解耦
c语言·单片机·嵌入式
shimly12345616 小时前
(done) 嵌入式硬件电路图 (2) 熟悉常用元器件
嵌入式硬件
安妮细水长流21 小时前
ad7124
c语言·单片机
花月mmc1 天前
【FPGA】神经网络内容简介预告
嵌入式硬件·深度学习·神经网络·fpga开发
en.en..1 天前
STM32 HAL 库 | 核心知识点整理
单片机·嵌入式硬件
无敌贵点大王1 天前
从寄存器层面重学STM32(9)——DMA理论
stm32·单片机·嵌入式硬件
希和20271 天前
51 单片机入门笔记:江科大课程代码逐行讲解(按键控制 LED 流水灯)
笔记·单片机·mongodb