news 2026/9/11 13:54:54

STM32F407+OV2640裸机网络摄像头:LWIP UDP传输JPEG帧实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F407+OV2640裸机网络摄像头:LWIP UDP传输JPEG帧实战

简介:基于 STM32F407 微控制器、OV2640 摄像头模块、数字摄像头接口 DCMI 和静态随机存储器 SRAM,并通过轻量级 TCP/IP 协议栈 LWIP 实现网络图像传输的嵌入式工程源码,适用于熟悉 STM32 底层开发与网络协议栈的工程师,也可作为工业视觉、远程监控、智能图像处理等项目的参考方案。工程完整演示了摄像头图像采集、SRAM 数据缓存、DCMI 接口读取、以及经过 LWIP 协议栈打包上传至网络的实现流程,支持最大分辨率一千六百万像素(1600*1200),当前在该分辨率下约每秒一至两帧,适合分析带宽和处理性能的优化空间。压缩包包含两千个文件,总体积约十二兆,其中 C 源码与 H 头文件约五百余个,构成 STM32 固件主体,另外大量 HTML、JavaScript、CSS 文件多为工程说明文档、上位机网页或调试界面,TXT 与 PDF 则提供辅助说明,整体结构比较清晰。该分享已有约一百五十五人学习,源码中包含寄存器级配置、DCMI 数据流读取、SRAM 缓冲管理、LWIP 网卡适配与通信等关键实现,能帮助开发者快速理解并复用 MCU 加摄像头的联网方案。

1. 从 OV2640 到 LWIP,一帧图像怎么从摄像头走到网线

先给一个反直觉的结论:STM32F407 自带 128KB 主 SRAM 加 64KB CCM,总共 192KB,却连一张 640×480 的 RGB565 原图(614KB)都装不下。而这套组合要干的事,恰恰是把 OV2640 的数据经 DCMI 接口收进来,放进外部 SRAM,再交给 LWIP 发到网线上。所以整套方案的难点不在 Cortex-M4 的主频,而在三段链路的错峰:JPEG 压缩让帧体积从 600KB 量级掉到几十 KB;外部 SRAM 给 DCMI DMA 提供乒乓缓冲区;LWIP 的内存池在裸机环境下负责把这几十 KB 切成包发出去。

适合三类人:做图像采集网关、要给设备加网络摄像头功能的工程师,以及把“STM32F407+LWIP+OV2640”当毕设题目的学生。下面按数据流方向拆开讲,从 SCCB 配 sensor,到 DCMI 同步时序,再到 FMC 挂 SRAM 和裸机 LWIP 发包,最后收在几个最容易翻车的边界条件上。

2. 采集侧数据通道:OV2640 的 JPEG 输出、DCMI 同步与 SRAM 帧存

2.1 先用 SCCB 让 OV2640 切到 JPEG 模式

OV2640 上电默认输出的是 RGB/YUV 原始像素,走 8 位并行口直接喂给 DCMI 的话,VGA 一帧就是 600KB 以上。192KB 片内内存加上 1MB 外部 SRAM 也经不起这么造,更别说 100Mbps 网线每秒只能吞 10MB 级别。所以第一步是让 sensor 输出 JPEG 压缩流,把帧体积压到 10~60KB,网络才发得动。

OV2640 的寄存器配置走 SCCB 总线,电气上完全可以拿 STM32 的硬件 I2C1 去驱动,但实践里模拟 I2C 更常见,因为 SCCB 没有 I2C 的复杂时序状态机,两个 GPIO 用延时翻转就能稳定跑 100kHz 左右。“stm32f407模拟i2c”这个写法在各大板卡例程里出现的频率,远高于硬件 IIC,原因就是 SCCB 的停止条件、应答时序都更宽松,模拟实现反而少踩硬件 I2C 的忙检测坑。SCCB 和 I2C 有个关键差别:每次写寄存器都是一个独立的 start/stop 周期,不支持连续写多个寄存器。

static void ov2640_wr_reg(uint8_t reg, uint8_t val) { i2c_start(); i2c_write_byte(0x60); /* OV2640 器件地址:7 位地址 0x30 左移一位 */ i2c_wait_ack(); i2c_write_byte(reg); /* 寄存器号,0x00 ~ 0xFF */ i2c_wait_ack(); i2c_write_byte(val); /* 寄存器值 */ i2c_wait_ack(); i2c_stop(); } static void ov2640_setup(void) { ov2640_wr_reg(0xFF, 0x01); /* 页面切换到 sensor 组 */ ov2640_wr_reg(0x11, 0x01); /* CLKRC:内部分频,关系 PCLK 上限 */ ov2640_wr_reg(0x12, 0x40); /* COM7:分辨率与输出方向的高位控制 */ /* …… 中间是模块自带的 JPEG 参数表,条目格式都是 {reg, val} …… */ ov2640_wr_reg(0xFF, 0x00); /* 切回 DSP 组,继续写压缩质量相关寄存器 */ }

参数说明:0xFF 是页面切换寄存器,OV2640 内部把寄存器分成 sensor 和 DSP 两组,任何一条驱动代码都必须先确认当前在哪一页,这也是新手改例程时最常见的“改了没反应”原因。0x11(CLKRC)决定输入时钟分频,直接影响 PCLK 和帧率;0x12(COM7)的低位是分辨率档位。完整的OV2640_JPEG表通常上百条,直接采用模块例程或原厂 demo 里那一份,只把 I2C 读写底座换成自己的即可,不建议自己逐个推导值。验证方法很简单:初始化完成后读帧缓存前两个字节,如果是FF D8,说明 JPEG 模式已经生效。

提示:模拟 I2C 的两个 GPIO 要避开 DCMI 的 D0~D7、PCLK、HSYNC、VSYNC 引脚,否则初始化 sensor 的瞬间 DCMI 会收到一串毛刺帧,后续的“丢首帧”逻辑反而更难判断。

2.2 DCMI 的极性配置:四组组合里只有一组不出花屏

DCMI 是 STM32F407 专门给并行摄像头留的接口,8 位模式下信号有 D7~D0、PIXCLK、HSYNC、VSYNC。像素时钟边沿、行同步和场同步的有效电平,分别由 DCMI 控制寄存器里的 PCPOL、HSPOL、VSPOL 三个位决定。OV2640 模块的典型接法里,VSYNC 低有效、PCLK 上升沿采样,但不同卖家模块的串阻和布线会引入相移,板子一换就可能翻车。

PCPOLVSPOL现象处理办法
00画面上下错位半帧,顶部有杂色带VSPOL 改为 1 再测
01画面正常但偶发整帧闪烁VSPOL 改回 0,检查 VSYNC 是否真接了
10画面呈斜纹或锯齿,但颜色正常PCPOL 改回 0,采样沿选错
11帧率减半或花屏严重同时翻转两位,逐项测
hdcmi.Init.SynchroMode = DCMI_SYNCHRO_HARDWARE; hdcmi.Init.PCKPolarity = DCMI_PCKPOLARITY_RISING; hdcmi.Init.VSPolarity = DCMI_VSPOLARITY_LOW; hdcmi.Init.HSPolarity = DCMI_HSPOLARITY_HIGH; hdcmi.Init.CaptureRate = DCMI_CR_ALL_FRAME; hdcmi.Init.ExtendedDataMode = DCMI_EXTEND_DATA_8B; HAL_DCMI_Init(&hdcmi);

参数说明:SynchroMode必须选硬件同步而不是内嵌同步。这是个特别容易踩的坑——很多人看到 JPEG 流里有FF D8FF D9标记,就以为可以不用接 HSYNC/VSYNC,让 DCMI 走内嵌同步自己去“找头”。但 DCMI 的内嵌同步找的是FF 00 xx这类固定四字节码,由 ESCR/ESUR 寄存器定义,OV2640 的 JPEG 流里并没有插入这种码,所以必须接好两根同步线,选硬件同步模式。PCKPolarity选上升沿还是下降沿,以 2.1 末尾那步 FFD8 验证为准,花屏优先查这张表,比查 DMA 配置快得多。

2.3 用 FMC 把 SRAM 挂到外部总线,帧存怎么划分

外部 SRAM 要挂在 FMC(Flexible Memory Controller)的 NOR/PSRAM Bank1 上,片选用 NE1 时首地址是 0x60000000。常见芯片是 IS62WV51216,容量 512K×16 即 1MB,16 位数据总线,访问时序由AddressSetupTimeDataSetupTime两个参数控制,单位是 HCLK 周期。主频 168MHz 时一个周期约 6ns,IS62WV51216 的读写周期典型值在 10ns 左右,所以时序给宽一点不会成为瓶颈,反而能避开低温或走线过长导致的偶发读错。

FMC_NORSRAM_TimingTypeDef timing = {0}; timing.AddressSetupTime = 1; /* 地址建立:2 个 HCLK ≈ 12ns */ timing.DataSetupTime = 4; /* 数据建立:5 个 HCLK ≈ 30ns */ timing.AccessMode = FMC_ACCESS_MODE_A; sram.Init.NSBank = FMC_NORSRAM_BANK1; /* NE1,首地址 0x60000000 */ sram.Init.MemoryType = FMC_MEMORY_TYPE_SRAM; sram.Init.MemoryDataWidth = FMC_NORSRAM_MEM_BUS_WIDTH_16; sram.Init.WriteOperation = FMC_WRITE_OPERATION_ENABLE; HAL_SRAM_Init(&sram, &timing, NULL);

参数说明:AddressSetupTimeDataSetupTime越宽越稳,但会占住 AHB 总线。DCMI DMA 每 4 个像素触发一次外部 SRAM 写入,如果 FMC 时序拖得太长,DMA 请求会排队,高 PCLK 下就直接丢数据。所以这两个参数要在稳定性和带宽之间找平衡,1/4 这个组合在绝大多数 168MHz 板子上都能跑,再低就建议用示波器量片选和写使能的实际宽度了。

内存布局按“乒乓采集 + 单帧邮箱”的模型划分,1MB 空间很宽裕:

区域地址大小用途
Ping 缓冲0x6001000064KBDCMI DMA 目标 A
Pong 缓冲0x6002000064KBDCMI DMA 目标 B
帧邮箱0x60030000256KB组装完整 JPEG 帧,供 LWIP 读取
预留0x60070000576KB多帧队列或未来的 OSD 叠加

DMA 目标直接指向外部 SRAM,而不是先收进片内 SRAM 再 memcpy 到外部。少一次搬运省下的时间,在“stm32f407 dcmi 超高速率”这个搜索词对应的场景里非常可观——外部 SRAM 虽然比片内慢,但 16 位总线背靠背读写也有几十 MB/s,远高于 OV2640 模块常见的 10~25MHz PCLK。

提示:16 位 SRAM 的地址线接法要看原理图,有的板把 SRAM 的 A0 接到 FMC_A0,有的接 FMC_A1。拿到新板子先对 0x60000000 和 0x60000002 两个地址各写一个不同的 16 位字再读回来,能区分就说明地址映射没问题,别死记地址公式。

2.4 DMA 双缓冲:帧就绪信号怎么给到主循环

DCMI 在 F407 上关联 DMA2 的某个流(CubeMX 生成的代码里能看到),8 位模式下 DCMI 内部会把每 4 个像素打包成一个 32 位字再触发 DMA 请求,所以 DMA 的数据长度必须按字计数,缓冲字节数必须是 4 的倍数。采集不能停,主循环又要同时处理 LWIP 发包,因此这里用 DMA 双缓冲模式:一块缓冲在接收,另一块由主循环拿去发,两个角色靠帧中断里切换。

volatile uint8_t *g_frame_ready = NULL; void HAL_DCMI_FrameEventCallback(DCMI_HandleTypeDef *hdcmi) { /* 帧结束中断里判断 DMA 当前要写哪块,另一块就是刚收满的 */ if (LL_DMA_GetCurrentTargetMem(hdcmi->DMA_Handle) == LL_DMA_CURRENTTARGETMEM0) g_frame_ready = CAM_PONG; /* 下一帧写 ping,pong 可读 */ else g_frame_ready = CAM_PING; }

逻辑说明:双缓冲开启后,DMA 硬件会在两个内存地址之间自动切换,GetCurrentTargetMem读出的是“当前正在写入”的目标。帧中断触发时,另一块必然是完整的一帧,把它的地址赋给g_frame_ready就完成了“采集→主循环”的交接。双缓冲的使能在 HAL 里对应HAL_DMAEx_ConfigDoubleBuffer,也可以直接操作 DMA2 流控制寄存器的 DBM 位,使用LL_DMA_EnableDoubleBuffer。这里只写交接逻辑,是因为改流配置属于 CubeMX 生成代码的范畴,不同固件版本生成的初始化顺序不一样,直接改寄存器最容易和自动生成代码打架。

这个方案里没有互斥锁,因为裸机下只有主循环一个消费者,帧中断只负责“置一个指针”,不会有并发写。代价是:如果主循环处理太慢,新帧会直接覆盖旧缓冲,所以丢帧策略必须在主循环侧处理,而不是中断里。

3. 裸机 LWIP 网络出口:NO_SYS 下的缓冲规划与 JPEG 分包

3.1 为什么裸机 LWIP 的常见模型是 UDP

很多搜“lwip 裸机移植模型”的人,最终落到两个选择上:NO_SYS=1 用 RAW API,或 NO_SYS=0 配一个精简 sys 层跑 netconn。前者是干净利落的裸机方案,RAW API 不需要邮箱、信号量,中断处理和主循环轮询天然串行;后者要补 sys 层的大量桩函数,移植成本高,收益只是换来类 BSD socket 的调用风格,对一帧几十 KB 的 JPEG 传输并不划算。

传输层选 UDP 还是 TCP,取决于任务需要“实时预览”还是“可靠单帧”。UDP 无 ACK、无窗口、无重传,发出去就不管,帧率稳定;TCP 有窗口控制和 ACK 节流,报文要等确认才能释放 pbuf,在裸机单线程里一旦 ACK 延迟,下一个窗口就卡住。图像采集对丢包容忍度高,下一帧马上就来,所以实时预览型项目普遍用 UDP。TCP 适合“这张图必须完整到达”的抓拍场景,但要在 tcp_sent 回调里维护一个发送队列,代码量直接翻倍。

顺带说一句,裸机 F407 上给 LWIP 集成 TLS 不现实:没有硬件加解密加速,握手要占几十 KB 堆内存,图像流的重传机制还会拖垮帧实时性。常见替代做法是应用层做轻量加密,比如在 UDP 载荷里加一组预共享的混淆字节。

3.2 LWIP 内存参数和以太网 DMA 描述符的放置

LWIP 在裸机上跑,内存规划比栈配置更影响稳定性。JPEG 帧要先从外部 SRAM 拷进 pbuf,才能交给以太网 DMA 发出去,所以 pbuf 池的大小直接决定一帧最多拆多少片。ETH 外设的 DMA 描述符和收发缓冲区必须在片内 SRAM1 里,不能放 0x10000000 开头的 CCM RAM——ETH、DCMI、DMA1/2 全都访问不到 CCM。

参数推荐值说明
MEM_SIZE24KBLWIP 堆大小,TCP 和 PBUF_RAM 都从这里切
MEMP_NUM_PBUF16裸机下够用,太小会偶发 ERR_MEM
PBUF_POOL_SIZE16静态 pbuf 池数量,接收路径主要消耗它
PBUF_POOL_BUFSIZE1536对齐到 4 字节,必须能装下 1518 字节以太网帧
TCP_MSS14601500 减 IP/UDP 头,TCP 用
TCP_WND8 * TCP_MSS局域网下这个窗口就能跑满吞吐

参数说明:MEM_SIZEPBUF_POOL_SIZE都吃片内 SRAM,调大确实能减少丢包,但每加 1KB 都在挤压 DCMI 和 ETH 的描述符空间。我的习惯是先按上表跑通,再观察stats里的lwip_stats,哪个池子归零就只加哪个。PBUF 池和 JPEG 发送是两条独立路径:接收方向的 pbuf 池是给 ARP、ICMP 控制和偶尔的回环用的,发送方向上我是直接用PBUF_RAM分配一整段连续内存再 memcpy,避免链式 pbuf 的内存碎片问题。

以太网 PHY 用 LAN8720A 还是 DP83848,对 LWIP 这一层没有任何区别,RMII 接口寄存器配置好后,协议栈只看到 ETH MAC。真正要注意的是 DMA 描述符数组要放在 0x20000000 到 0x2001BFFF 这段 SRAM1 里,别让链接脚本把它排到 SRAM2 或 CCM,否则以太网收发会时好时坏,而且没有任何报错。

3.3 UDP 分包发送一帧 JPEG 的最小代码

RAW API 发 UDP 的路径比 netconn 短:udp_new建 PCB,udp_bind绑本地端口,主循环里把一帧按 1400 字节切片,每片pbuf_alloc填入数据,udp_sendto发出再释放。以太网 MTU 是 1500,IP 头 20 字节、UDP 头 8 字节,单包最大载荷 1472。留 1400 而不是顶满 1472,是为了在首包前面加一个 4 字节的帧长度头时不至于拆成两个包。

static struct udp_pcb *g_upcb; void lwip_udp4_start(void) { g_upcb = udp_new(); udp_bind(g_upcb, IP_ADDR_ANY, 6666); } err_t send_jpeg_frame(uint8_t *jpeg, uint32_t len) { uint32_t off = 0; static ip_addr_t remote = IPADDR4_INIT_BYTES(192, 168, 1, 50); /* 接收端 IP */ while (off < len) { uint16_t chunk = (len - off > 1400) ? 1400 : (uint16_t)(len - off); struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, chunk, PBUF_RAM); if (p == NULL) return ERR_MEM; memcpy(p->payload, jpeg + off, chunk); err_t err = udp_sendto(g_upcb, p, &remote, 8888); pbuf_free(p); if (err != ERR_OK) return err; off += chunk; } return ERR_OK; }

逻辑说明:每次循环先算出剩余长度和本片大小,pbuf_alloc的第二个参数是载荷长度,协议栈会自动在前面补出以太网、IP、UDP 三层头部。udp_sendto是异步的,底层把包交给 ETH DMA 描述符后就返回,所以pbuf_free在这个位置是安全的。如果ERR_MEM频发,说明MEM_SIZE不够或上一帧的 pbuf 还没被 DMA 消费完,这时优先检查发送描述符数量,而不是盲目加大内存。

接收端要能正确组帧,必须在第一片里带帧长度。最简单可靠的做法:第一片 payload 前 4 字节放len的小端表示,接收端收满len字节即为一帧,之后遇到的未对齐数据直接丢弃。上位机用 Python 的 socket 收到第一个包后,按这个长度循环 recv 就行。

3.4 发不过来就丢帧:主循环的仲裁策略

摄像头采集是硬实时,网络发送是尽力而为,两者之间必须有一个明确的“谁让谁”的规则。典型策略是“最新帧优先”:主循环发现g_frame_ready还没处理完,就直接放弃上一帧,立即接手新帧。丢弃的判断不放在中断里,而是主循环每次循环进来,发现g_frame_ready指向的缓冲已经被新帧覆盖了一个字节,就知道这帧已经废了,连找 FFD9 的功夫都省下,直接把指针清空等下一帧。

为什么不在中断里做判断?因为“发现被覆盖”本身需要读多个字节,中断里做这件事会抢占 DCMI 的 DMA 带宽,高 PCLK 下反而加剧丢帧。裸机模型下中断只做一件事:置指针。仲裁、拷贝、发送全部放主循环。

带宽核算也很直观:QVGA 分辨率的 JPEG 一帧约 10~20KB,按 15fps 算只有 300KB/s 上下,100Mbps 网线完全无压力;VGA 分辨率 30fps 时,JPEG 码率随画面复杂度波动很大,运动剧烈画面一帧能到 80KB 以上,预算要按静止画面的三倍留。真正卡死帧率的往往不是网络,而是 OV2640 的 PCLK 和 DCMI 的搬运能力。

4. 源码怎么组织:主循环状态机、内存划分与参数表

4.1 主循环状态机捕捉帧就绪事件

这套源码的组织核心是一个三状态主循环:等待帧、解析长度、发送。没有 RTOS,状态之间的转换全由g_frame_ready指针驱动。

int main(void) { SystemClock_Config(); /* 168MHz */ MX_GPIO_Init(); MX_DCMI_Init(); /* 极性按 2.2 的排查表调整 */ fmc_sram_init(); /* 挂载外部 SRAM */ ov2640_setup(); /* SCCB 配 sensor 到 JPEG 模式 */ dcm_dma_double_buffer_start(); lwip_eth_init(); /* PHY 复位 + LWIP 协议栈初始化 */ g_frame_ready = NULL; while (1) { if (g_frame_ready != NULL) { uint32_t len = find_jpeg_len(g_frame_ready, MAX_FRAME_SIZE); if (len > 4) { send_jpeg_frame(g_frame_ready, len); } g_frame_ready = NULL; /* 丢帧策略:本帧发完立即释放 */ } sys_check_timeouts(); /* 裸机 LWIP 必须周期性喂定时器 */ } }

逻辑说明:find_jpeg_len在外部 SRAM 里扫描 FFD8 和 FFD9,找到合法的 JPEG 结束标记才返回长度,找不到说明这帧被截断,直接丢弃。g_frame_ready = NULL永远放在发送之后,放在发送之前也没问题,因为 DCMI 的连续采集会覆盖旧缓冲,发一个被覆盖的帧反而是浪费。sys_check_timeouts是裸机 LWIP 的定时器泵,负责 ARP 缓存老化、TCP 重传等周期任务,主循环每轮调用一次即可,不需要精确到毫秒。

这里有个性能细节值得留意:send_jpeg_frame是从外部 SRAM 读数据再拷进片内 pbuf,整个发送过程占主循环。如果一帧 40KB 在网络侧耗时 5ms,而 DMA 双缓冲半帧时间只有 6ms,那么这个循环已经贴边了。解决办法要么降帧率,要么把 JPEG 质量寄存器调低让帧体积掉到 20KB 以下,这就是“JPEG 质量换帧率”的具体落点。

4.2 关键参数表:DCMI、FMC、LWIP 三处联动

这三处参数不是独立的。DCMI 的窗口大小决定一帧有多少数据,FMC 的时序决定 DMA 写入外部 SRAM 的速度,LWIP 的 pbuf 池决定发送能切多碎。任何一处改大,都会挤压另外两处的余量。

参数位置参数推荐值联动影响
DCMI窗口宽高320×240 或 640×480分辨率提高一倍,帧数据量翻四倍
DCMI扩展数据模式8 位决定 DMA 按字打包
FMCAddressSetupTime1太大会拖慢 DMA 写入
FMCDataSetupTime4太小偶发读错位
LWIPPBUF_POOL_BUFSIZE1536必须大于最大帧长
LWIPMEM_SIZE24KB影响 PBUF_RAM 发送路径
以太网TX/RX 描述符各 4 个太少会丢发送请求

DCMI 的捕获率参数CaptureRate选了DCMI_CR_ALL_FRAME,意思是每一帧都产生一次帧中断并写入缓冲。也可以选只采奇数场或偶数场来降帧率,但 JPEG 模式下不建议这么干,因为 sensor 端并没有真正降低输出帧率,DCMI 只是隔帧丢弃,浪费的采集带宽还在。

4.3 一帧数据的完整搬迁路径与帧率核算

把整条链路按字节过一遍:OV2640 输出 JPEG 字节流,每个字节跟随一个 PCLK 被 DCMI 采样,每 4 字节打包成 32 位字,经 DMA2 写入外部 SRAM 的 ping/pong 缓冲。主循环发现g_frame_ready后,扫描 FFD9 确认帧边界,然后循环 memcpy 到 pbuf,每个 pbuf 经udp_sendto挂到 ETH DMA 描述符,由 MAC 按帧率发出。

开销大头有三个:memcpy 从外部 SRAM 读、pbuf 分配和释放、ETH DMA 等待上一次发送完成。以 QVGA 15fps、每帧 15KB 为例,memcpy 约 0.4ms,UDP 发送约 2ms,剩余时间全在主循环空转,帧率轻松达标。VGA 30fps 则要重新核算,因为 JPEG 帧体积可能到 60KB 以上,单帧发送时间逼近半帧采集时间,就会频繁触发丢帧策略。

核算公式就一句:总耗时 = 采集时间 + memcpy 时间 + 发包时间,采集时间由 PCLK 和帧字节数决定,发包时间由网线和udp_sendto的单片开销决定。把这三个数算清楚,就能判断瓶颈在 sensor、总线还是网络,而不是盲目加内存。

5. 几个容易翻车的边界条件:极性、CCM 与首帧

5.1 极性排查顺序:先 PCLK,再 VSYNC

花屏问题按 2.2 的表格排查时,顺序建议固定为:先翻 PCPOL 看斜纹是否消失,再翻 VSPOL 看上下错位是否解决,最后才是 HSPOL。原因是 PCLK 采样沿错了,后面所有判断都没意义。手边没有示波器时,用一个白墙固定画面来测,斜纹和错位会非常明显。

/* 快速切换采样沿的两种写法,改完立即看效果 */ __HAL_DCMI_DISABLE(&hdcmi); hdcmi.Init.PCKPolarity = DCMI_PCKPOLARITY_FALLING; HAL_DCMI_Init(&hdcmi); __HAL_DCMI_ENABLE(&hdcmi);

这段代码演示的是“改极性不必重启整个外设”,先关 DCMI 再改再开即可。注意HAL_DCMI_Init在使能状态下调用可能触发断言,所以要先把外设关闭。

5.2 CCM 的 64KB 别放 DMA 缓冲区,但有个安全用途

CCM RAM 在 0x10000000,主核访问它是全速的,但 DMA1、DMA2、ETH、DCMI 全都碰不到。链接脚本如果不小心把大数组排到 CCM,DCMI DMA 写它的结果是“静默失败”——没有任何错误标志,数据就是不动。排查方法:定义一个带初值的数组放在 CCM,DMA 搬运后对比,值没变就说明地址没生效。

CCM 的正确用途是放纯 CPU 访问的数据:JPEG 初始化表、printf 缓冲、协议解析的临时结构体。LWIP 的发送描述符和 pbuf 池绝不进 CCM,这是 F407 上最容易踩的地址陷阱。

5.3 首帧脏数据和 FFD9 兜底

上电后 sensor 的 PLL 和自动曝光都在收敛期,前几帧经常是半黑、花屏或只有上半帧。最省事的办法是在 VSYNC 中断里数帧,前三帧直接丢弃,第四帧再开放g_frame_ready。DCMI 是连续采集,不存在“重新同步”的说法,丢帧只是让主循环不认它,硬件层面照常走。

uint32_t find_jpeg_len(const uint8_t *buf, uint32_t max) { uint32_t end = 0; for (uint32_t i = 0; i + 1 < max; i++) { if (buf[i] == 0xFF && buf[i + 1] == 0xD8) end = 0; /* 发现新的 SOI,重新累计 */ else if (buf[i] == 0xFF && buf[i + 1] == 0xD9) end = i + 2; /* EOI,帧结束 */ } return end; /* 返回 0 表示未找到完整帧 */ }

这段扫描有两个值得留意的点:一是循环里把最后一个 FFD9 当作帧尾,因为双缓冲后半帧往往和下一帧的前半帧粘连,取最后一个 EOI 才能去重;二是返回值为 0 时主循环直接释放,绝不上报“错误帧”状态给上位机。这样即使 sensor 偶发丢行,网络侧拿到的永远是完整 JPEG。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 13:53:40

macOS 安装 OpenCV 的实用指南:从路线选择到跑通第一张图

macOS 安装 OpenCV 的实用指南&#xff1a;从路线选择到跑通第一张图 【免费下载链接】opencv Open Source Computer Vision Library 项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv 你大概率不是来研究计算机视觉理论的&#xff0c;你只是想在 Mac 上…

作者头像 李华
网站建设 2026/9/11 13:53:28

CMSIS-6源码静态工程:嵌入式构建范式的工业级重构

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 13:52:03

Python+CNN+OpenCV:驾驶员分心检测系统落地实战

简介&#xff1a;面向计算机视觉与深度学习初学者的一份驾驶员分心检测实战资源&#xff0c;基于Python、CNN和OpenCV构建&#xff0c;覆盖数据准备、模型训练到推理部署的关键流程。压缩包内共5个文件&#xff0c;包含2个Python脚本承担训练与测试功能、1个Shell脚本用于转换并…

作者头像 李华
网站建设 2026/9/11 13:49:06

内存泄露 Bug 的自动定位:基于 pprof 采样结果与 AI 堆栈分析

内存泄露 Bug 的自动定位&#xff1a;基于 pprof 采样结果与 AI 堆栈分析 在 Go 语言编写的后台长期运行微服务中&#xff0c;内存泄露&#xff08;Memory Leak&#xff09;往往是最折磨工程师的“慢性毒药”。它不像空指针解引用那样会立即触发 panic 并留下清晰的堆栈&#x…

作者头像 李华