/* ============================================================================
* 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, ®, 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();
}