
简介这款WiFi网络摄像头开发例程基于STM32F103RC单片机搭配ESP8266模块与OV2640摄像头是一套面向物联网开发者的完整工程可直接在KEIL中编译运行适合学习无线图传、远程监控以及STM32外设驱动应用的读者参考。资源包共195个文件大小约5.19MB其中包含48个H头文件与45个C源文件覆盖STM32标准库外设驱动、OV2640图像采集、ESP8266数据透传等关键模块另有KEIL工程文件、hex/axf烧录文件及bat批处理脚本分别对应源码阅读、程序固化与工程清理。目前已有207人学习下载。代码中清晰定义了单片机与各模块的接线并配有注释换用其他STM32F103型号时只需调整芯片型号、引脚和FLASH容量即可同时提供的批处理脚本可一键清除KEIL编译中间文件便于保持工程整洁也适合在智能家居、安防监控等场景中快速搭建原型验证。1. STM32F103RCESP8266OV2640做WIFI网络摄像头核心不在摄像头在数据路径先泼一盆冷水STM32F103RC没有DCMI数字摄像头接口那是F4/F7系列才有的外设。所以这颗72MHz的Cortex-M3想做OV2640摄像头项目第一步不是在点开CubeMX里勾外设而是确定图像的获取方式。常见做法是买带AL422B FIFO缓冲的OV2640模块用GPIO模拟FIFO读时序把JPEG数据拿回MCU再通过串口交给ESP8266ESP8266以TCP客户端身份把数据发到上位机显示。这条链路里最耗时的不是采集而是串口带宽和WiFi传输。这篇文章把这套方案的每个环节拆开讲OV2640的JPEG配置和读数时序、ESP8266的AT透传、图像数据的分帧协议最后落到花屏排查和帧率调优。适合正在做类似项目的嵌入式工程师也适合想了解图像数据在一颗没有DCMI的MCU上如何流转的读者。整个系统没有操作系统、没有大缓存硬实时地把一帧一帧JPEG从传感器挪到网络对端每一步都有明确的取舍。2. OV2640的图像采集JPEG模式配置、AL422B FIFO读时序与帧判定2.1 SCCB时序与JPEG输出模式的寄存器配置OV2640通过SCCB总线配置内部寄存器SCCB时序和I2C高度相似但读写流程有差异。所有寄存器都挂在bank下面bank选择写入0xFF寄存器取值范围0x00到0x07。JPEG模式时关键寄存器集中在bank 0x05。写寄存器的函数我一般长这样int ov2640_write_reg(uint8_t bank, uint8_t addr, uint8_t val) { sccb_start(); sccb_write_byte(OV2640_SLAVE_WR); // 0x608位写地址 sccb_write_byte(0xFF); // 先切bank sccb_write_byte(bank); sccb_write_byte(addr); // 寄存器地址 sccb_write_byte(val); // 寄存器值 sccb_stop(); return 0; }SCCB和I2C有个关键差异SCCB没有I2C那种读字节后主机回ACK的机制读操作要拆成两次传输完成。这里我直接用GPIO模拟不用STM32的I2C外设。F1系列I2C外设本身在总线繁忙时容易锁死调试OV2640这种对时序敏感的外设软件模拟反而可靠。时钟频率选200kHz左右配置完整套寄存器大概花几百毫秒对开机流程无感。JPEG模式的核心是寄存器0x12在bank 0x05下写0x40则输出JPEG写0x00则回到RGB565/YUV。切换后必须重新设置分辨率相关的分频寄存器顺序不能反void ov2640_set_jpeg(uint8_t enable) { if (enable) { ov2640_write_reg(0x05, 0x12, 0x40); // 输出JPEG流 } else { ov2640_write_reg(0x05, 0x12, 0x00); // RGB565 } }不少初学者在这里踩坑只写bank 0x05的0x12不够OV2640上电默认bank是0x00直接写0x12等于改的是bank 0x00里的未知寄存器模块毫无反应。正确顺序永远是先写0xFF再写bank再写目标寄存器地址和值。另外OV2640上电后PWDN引脚要拉低而且必须等待至少10ms再开始SCCB配置否则寄存器写入时模块还处于复位态。2.2 为什么用AL422B FIFOF103没有DCMI但GPIO模拟足够读STM32F103系列所有型号都没有DCMI外设直接让OV2640的8位数据总线怼到MCU的GPIO上等PCLK采样在72MHz主频下完全不现实。OV2640的PCLK在JPEG模式下随分辨率变化高分辨率下能到几十兆赫兹GPIO中断采样会直接把CPU打满且丢数据。带AL422B的模块解决了这个问题。AL422B是256KB的异步FIFOOV2640的像素数据持续写入FIFOMCU想读的时候用任意速度读出来。这样把高速实时采集问题转成低速串行读取问题。AL422B的读时序比想象中简单每个RCLK上升沿输出一个字节读指针自动递增。static uint8_t fifo_read_byte(void) { uint8_t data 0; for (uint8_t bit 0; bit 8; bit) { FIFO_RCLK_LOW(); __NOP(); __NOP(); FIFO_RCLK_HIGH(); __NOP(); __NOP(); data (data 1) | (uint8_t)FIFO_DIN(); } return data; }每次先把RCLK拉低再拉高中间插两个空转时钟是为了让数据线上的电平稳定下来。F103工作在72MHz两个__NOP()大约提供28ns的建立时间对AL422B这种老芯片已经足够。如果读出来的数据有毛刺第一步不是改代码而是用示波器看RCLK高低电平和数据线的建立时间把__NOP()加到四个试试。模块上电后要复位FIFO指针。AL422B有两个复位信号RRST复位读指针WRST复位写指针。大部分模块把两个信号合并成一个RST引脚拉低再拉高即完成指针归零。复位后读到的第一个字节就是OV2640最先写入的数据这样从头开始扫描JPEG流才有意义。2.3 在FIFO里找帧头FFD8与帧尾FFD9OV2640在JPEG模式下输出的是一段连续的JPEG流JPEG标准要求每帧以FFD8开头、FFD9结尾。问题是FIFO是连续缓存的模块可能已经写入了半帧或者多帧直接从头读会拿到上次帧的残影。常见做法是先复位FIFO读指针然后丢弃一个完整旧帧对齐到新帧起点bool find_jpeg_frame(uint8_t *buf, uint32_t max_len) { uint32_t idx 0; uint8_t prev 0, cur 0; // 复位读指针 FIFO_RRST_LOW(); __NOP(); __NOP(); FIFO_RRST_HIGH(); // 跳过当前残帧直到收完一个完整帧的帧尾 while (1) { prev cur; cur fifo_read_byte(); if (prev 0xFF cur 0xD9) break; } // 从下一帧头开始拷贝 prev 0; while (idx max_len) { cur fifo_read_byte(); buf[idx] cur; if (prev 0xFF cur 0xD9) break; // 到帧尾结束 prev cur; } return idx 2; }第一段循环读到FFD9就停意味着把FIFO里已有的一整帧读完了读指针停在这个帧的尾部。再次读取时新的一帧数据会在DMA写入的同时被我们读走。第二段循环拷贝直到帧尾。注意idx 2这个判断如果只读到一两个字节就遇到FFD9说明这个FIFO里根本没有完整帧大概率是摄像头初始化失败或者帧率设置导致写入中断。OV2640在JPEG模式下帧头附近会有多个FFD8实际用到的只有一个。严谨的做法是检查FFD8后面跟着的SOF标记FFC0/FFC2但工程上多数项目只要FFD8到FFD9区间内的数据能解码成图就够了不用过度校验。3. ESP8266的数据通道从AT指令到TCP透传的稳定实现3.1 STM32与ESP8266的UART接线与电平匹配ESP8266两种用法SDK二次开发和AT指令透传。做网络摄像头这类数据量固定的项目AT透传足够且省去固件开发成本。代价是帧率被串口波特率锁死。ESP8266模组IO电平是3.3VSTM32F103也是3.3V理论上可以直连但实际接法要注意STM32的TXPA2或PA9接ESP8266的RXDSTM32的RX接ESP8266的TXD。我一般用USART2引脚PA2/PA3。USART1留给调试打印避免调试信息和图像数据抢同一个串口。初始化代码// USART2初始化默认115200 8N1 void usart2_init(uint32_t baud) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef gpio; gpio.GPIO_Pin GPIO_Pin_2; // PA2 TX gpio.GPIO_Speed GPIO_Speed_50MHz; gpio.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, gpio); gpio.GPIO_Pin GPIO_Pin_3; // PA3 RX gpio.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, gpio); USART_InitTypeDef usart; usart.USART_BaudRate baud; usart.USART_WordLength USART_WordLength_8b; usart.USART_StopBits USART_StopBits_1; usart.USART_Parity USART_Parity_No; USART_Init(USART2, usart); USART_Cmd(USART2, ENABLE); }波特率可选115200、230400、460800。ESP8266默认出厂是115200模块内部波特率可以改但改波特率的AT指令ATUART_DEF必须和当前波特率匹配才能生效。我一般直接留在115200不为省那零点几秒去冒波特率错乱的风险。如果确实要升到230400配置完立即切换后MCU侧也要同步改USART2的波特率顺序是先发ATUART_DEF230400,8,1,0,0然后延时50msMCU重新初始化串口。3.2 AT指令初始化序列与进入透传模式ESP8266上电后需要等待固件启动串口上会输出一堆乱码固件字符串然后出现ready。代码里发AT\r\n等OK判断就绪int esp8266_wait_ready(uint32_t timeout_ms) { uint32_t start now_ms(); while (now_ms() - start timeout_ms) { uart_send_str(USART2, AT\r\n); if (strstr(esp8266_rx_buf, OK)) return 0; delay_ms(500); } return -1; }AT\r\n每条指令都以回车换行结尾这是硬性要求。只发AT不加换行模块不会回复。就绪后依次执行以下序列esp_send_wait_ok(ATCWMODE1\r\n); // Station模式 esp_send_wait_ok(ATCWJAP\SSID\,\PASSWORD\\r\n); // 连接路由器 esp_send_wait_ok(ATCIPSTART\TCP\,\192.168.1.100\,8080\r\n);ATCWJAP这一步最考验耐心路由器认证方式不同连接耗时从2秒到15秒不等超时设短了会误判失败导致反复重启模块。ATCIPSTART是建立TCP连接连不上会返回CONNECT FAIL此时要检查目标IP和端口是否可达以及路由器的AP隔离是否开启。AP隔离开启时局域网内设备互访被禁止模块永远连不上同一WiFi下的上位机。透传模式是AT固件特有的状态进入后模块不再解析AT指令所有串口数据原样通过TCP发出esp_send_wait_ok(ATCIPMODE1\r\n); // 开启透传 esp_send_wait_ok(ATCIPSEND\r\n); // 进入发送状态返回ATCIPSEND一旦成功模块返回字符之后MCU往UART发送的所有字节都被当作TCP数据发出。退出透传的方式是发送注意前后不能有其它数据否则固件不识别。这个特性在调试时很有用透传模式下想发AT指令必须先退出来。3.3 环形缓冲、串口中断与数据完整性图像数据发送过程中ESP8266会随时回推信息比如TCP断开时输出CLOSEDWiFi掉线时输出WIFI DISCONNECT。这些ASCII字符串会插在图像字节流里接收端如果按纯数据解析会把CLOSED当成图像数据收进去导致花屏。我在工程里用环形缓冲区收集USART2的RX数据主循环里统一处理#define RING_SIZE 2048 static volatile uint8_t ring[ RING_SIZE]; static volatile uint16_t head 0, tail 0; void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE)) { uint8_t c USART_ReceiveData(USART2); uint16_t next (head 1) % RING_SIZE; if (next ! tail) { ring[head] c; head next; } // 满则丢弃新字节不阻塞中断 } }环形缓冲区满时丢新数据而不是覆盖旧数据目的是优先保证已收到的内容完整。图像传输的场景下丢掉几个字节最多造成当前帧局部花屏覆盖旧数据则会把之前完整的帧破坏。主循环里读取缓冲区匹配CLOSED、WIFI DISCONNECT等关键字一旦命中就执行重连序列。4. 帧协议设计JPEG分包、序号与断线重连策略4.1 数据量估算为什么一帧必须拆成多个包ESP8266的透传底层是TCP但串口到WiFi的转发有内部缓冲限制。AT固件在透传模式下单次写入的数据超过约2KB到4KB就会触发TCP分段模块会等上一个TCP分段发送完成才接收下一段这期间串口数据被堵在FIFO里一旦溢出就丢。正确的做法是把一帧JPEG拆成多个固定大小的数据包包与包之间做流控。JPEG的大小和分辨率不呈线性关系场景复杂度影响很大分辨率JPEG典型大小115200波特率耗时230400波特率耗160x120 QQVGA4-8KB0.4-0.8s0.2-0.4s320x240 QVGA10-25KB0.9-2.2s0.5-1.1s640x480 VGA30-60KB2.7-5.4s1.4-2.7s800x600 SVGA50-100KB4.5-9.0s2.3-4.5s串口理论速率115200波特率下每秒约11520字节这是上限。实际能到90%就算不错因为传输间隙要处理AT回显、TCP ACK。所以320x240分辨率在115200下一秒一帧已经很紧张。图像内容越复杂JPEG体积越大实际帧率波动很大。4.2 自定义帧协议字段magic、seq、offset、length有了分包机制接收端需要知道每个包属于哪一帧、在这一帧的什么位置。我的协议头是7字节定长typedef struct __attribute__((packed)) { uint8_t magic[2]; // 0x5A 0xA5同步标记 uint16_t seq; // 帧序号同一帧内相同 uint16_t offset; // 本包在帧内的偏移量 uint16_t length; // 数据长度1024 } net_pkt_hdr_t;magic占两个字节避免单字节0xA5在数据流中频繁误判。接收端先找magic找到后读hdr再读length字节的数据。seq解决乱序重组和丢帧检测的问题接收端发现seq跳变说明中间帧丢失直接丢弃当前不完整帧。offset则是为接收端提供拼帧位置信息收到包后直接按offset写入缓冲区。CRC在这种协议里要不要加我的原则是不加。原因有两点。第一ESP8266走的是TCP链路层和传输层已经有CRC32校验数据在WiFi链路上出错会被网络协议栈重传或丢弃。第二图像数据本身容忍少量错误一帧花几毫秒校验只会拖慢发送速度。真正需要关注的错误类型是丢包不是字节翻转所以seq比CRC更值钱。4.3 发送一帧的完整流程与加权流控发送函数在发送每个包前检查ESP8266的RX缓冲确保上一次发送的包没有触发底层阻塞void send_jpeg_frame(uint8_t *jpg, uint32_t jpg_len, uint16_t seq) { uint32_t pos 0; uint16_t pkt_no 0; while (pos jpg_len) { uint32_t todo MIN(PKT_MAX_DATA, jpg_len - pos); net_pkt_hdr_t hdr { {0x5A, 0xA5}, seq, (uint16_t)(pkt_no * PKT_MAX_DATA), (uint16_t)todo }; uart_send_bytes(USART2, (uint8_t *)hdr, sizeof(hdr)); uart_send_bytes(USART2, jpg pos, todo); pos todo; pkt_no; // 等待模块完成TCP发送避免缓冲堆积 delay_ms(5); } }每一包之间延时5ms这个值不是拍脑袋定的。ESP8266内部TCP栈发送一个1KB包后需要时间等远端ACK和释放发送缓冲区。5ms在115200波特率下意味着约57个字节的空隙正好补偿串口发送和WiFi发送的速度差。如果场景允许先测一下上位机实际收包间隔再微调这个参数。调大的代价是帧率下降调小则可能出现模块发送缓冲区溢出表现是图像中随机缺一块数据。断线重连的处理放在帧级而不是包级。发送过程中发现TCP断开比如串口缓冲区出现CLOSED关键字立即放弃当前帧转入重连流程。重连成功后从下一帧开始发不补偿已丢失的帧数据。视频流场景下用户感知到的是画面卡了零点几秒但不会积累延迟。这个取舍和文件传输完全不同值得明确写进代码注释里。5. 调试WIFI摄像头帧率测量、花屏排查与自动重连技巧5.1 花屏和绿屏的排查顺序从Sensor到FIFO到TCP遇到花屏先别改代码按照数据链路从前到后排查。绿屏是OV2640输出RGB/YUV模式时上位机按JPEG解码的典型表现把0x12寄存器值读回来确认是否真的是0x40。花屏则大概率是帧边界错误FIFO读起始位置不对把上一帧的尾部当成了当前帧头部。检查逻辑分析仪抓到的RCLK和帧头FFD8的对应关系如果FIFO复位和帧头检测之间有旧数据残留先跳过一整帧再开始拷贝。图像只显示上半部分通常是分辨率配置和FIFO读速度不匹配。OV2640的JPEG数据写入FIFO是持续进行的读速度太慢导致读指针追不上写指针。这时要么降低分辨率要么加大包间延时的等价物来降低发送速度。5.2 用GPIO翻转法测量真实帧率想知道系统实际一秒能传几帧加两个GPIO翻转最直观GPIO_SetBits(GPIOB, GPIO_Pin_0); // 帧开始 send_jpeg_frame(jpg, jpg_len, seq); GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 帧结束用示波器或者逻辑分析仪量脉冲宽度。脉冲宽度等于一串JPEG数据的总发送耗时除以单帧字节数得到波特率利用率。例如320x240分辨率、JPEG大小15KB测到脉冲宽度1.4秒115200波特率下的理论耗时1.33秒说明系统已经接近串口瓶颈优化空间在降低JPEG大小而不是调整代码。如果脉冲宽度明显大于理论值先检查每包之间的delay是不是设大了再检查ESP8266的透传是否被AT回显干扰。5.3 透传模式下TCP断线的检测与自动恢复透传模式下模块本身不会主动告诉MCU连接断了只有收到远端RST或长时间空闲才输出CLOSED。可靠的办法是使用AT固件的ATPING命令或者让上位机周期性发送心跳但透传模式下串口收到的所有字节都作为TCP数据发出没法直接发AT指令。我的方案是应用层心跳STM32每3秒在上位机可见的特定端口发一个特殊短包上位机收到后回一个ACK连续3次没收到ACK就判定链路异常退出透传执行重连。这个方案的好处是不依赖AT固件版本行为代价是上层协议多了一类心跳消息。如果不想增加应用层协议可以定时调用ATCIPSTATUS但它会输出多行字符串要处理解析逻辑透传模式下还容易和图像数据混在一起。工程上我倾向心跳方案简单、可靠、不受固件版本影响。最后强调一个细节AT固件版本差异很大1.7.x和2.x对ATCIPSEND回显的超时行为不同调试前先确认固件版本别把固件行为当成代码bug来排查。本文还有配套的精品资源点击获取