尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CYW240128驱动移植实战:从ESP32到FPGA的联合调试全解析

CYW240128驱动移植实战:从ESP32到FPGA的联合调试全解析 先说结论CYW240128 厂商提供的驱动例程通常不会直接给你一份 ESP32 FPGA 的双端完整调试工程。别急着失望这几乎是所有液晶模组厂的“通病”因为对屏厂来说他们交付的是一块模组、一颗控制器、一个初始化方案而不是一套跨平台的参考设计。驱动例程里真正值钱的是那三样东西初始化寄存器序列、底层读写函数、字模数据。只要这三样拿到了ESP32 端和 FPGA 端各花半天到一天时间就能完全调通。这篇文章就把 CYW240128 这块屏从底层原理到双端移植、再到联合调试的全过程拆开讲透全程都是可落地的代码思路和实测经验没有一句虚的。1. 先回答标题厂商例程的“完整”和你要的“完整”不是一回事我最早被问到这个问题时也以为厂家能提供“全套能跑”的代码实际去要了一番才发现所谓驱动例程通常是一个 Keil 工程主控还是 8051 或 STM32跑的就是最基础的读写 demo初始化屏幕、清屏、显示几个字符、画几条线。就这么多。你想要的那种 ESP32Espressif IoT Development Framework 或者 Arduino完整工程没有FPGA 侧的 Verilog 控制器也没有。但这里面有个关键认知屏幕驱动这件事底层从来不是 CPU 平台问题而是总线时序问题。CYW240128 是一块 240×128 分辨率的单色图形点阵液晶模组常见的控制方案是 RA8806 或 RA8822 这类自带显示缓存的控制器芯片支持 8080 和 6800 两种并口时序。无论你在 ESP32 上写 C 代码还是在 FPGA 里写 Verilog最终都是同一件事按照芯片手册的时序要求向控制寄存器写入数据、向显存写入像素数据。所以厂商给不给完整 ESP32 代码并不重要重要的是手册里那张时序图和初始化流程它给到了没有。而驱动例程恰恰就是这两样东西的最佳学习范本。我的判断标准很直接一份驱动例程值不值得参考就看它能不能回答三个问题——屏幕上电后哪些寄存器必须先写、读写时序的最小脉冲宽度是多少、显存地址和屏幕坐标怎么对应。这三个问题解决了平台移植只是体力活。下文就按这三个问题展开。2. 拆解 CYW240128 的驱动底层控制芯片、总线时序和初始化依赖2.1 先翻模组背面确认控制器型号拿到 CYW240128 模组第一件事不是翻资料而是看模组背面那颗控制器。RA8806 和 RA8822 的初始化流程不一样寄存器地址也不一样S1D13700 又是另一套。你照着 RA8806 的例程去调 RA8822 的屏屏幕可能完全没反应或者出现乱码这不是代码写错了是寄存器表根本不匹配。我手头这块是 RA8806 控制器带中文字库支持文本模式和图形模式叠加。RA8806 的显存是内置的容量 64KB实际使用只占一部分通过行列起始地址来映射 240×128 的显示区域。理解这个映射关系很重要因为后面在 FPGA 里做显存地址换算时本质上就是把“屏幕坐标 → 显存地址”这个公式翻译成硬件逻辑。2.2 8080 并口时序到底在传输什么RA8806 支持 8080 和 6800 两种接口由引脚的硬件电平选择。CYW240128 模组一般出厂已经配好接口大多数情况是 8080 并口。8080 并口有这几个关键信号片选 CS、寄存器选择 RS、读写控制 WR/RD、8 根数据线 D0-D7。写一个字节的时序过程是这样的拉低 CS选中芯片设置 RS 电平低电平代表写命令高电平代表写数据把要发送的数据放到 D0-D7 上拉低 WR再拉高 WR在 WR 的上升沿或者下降沿芯片采样数据。写 CPU 和写屏幕的逻辑几乎一样区别只是 RS 的状态以及写入的目标地址不同。读时序则是把数据线方向从输出切到输入然后拉低 RD在 RD 的上升沿读取总线上的数据。整个流程没有任何魔法就是电平顺序对不对、时间长短够不够。2.3 为什么初始化寄存器序列是雷区RA8806 的初始化寄存器序列大概包括这些功能设置系统设置、显示开关、扫描方向、偏压比、对比度、光标/图标设置、文本/图形显示起始地址。任何一项写错屏幕的表现都不一样——有可能是白屏有可能是只亮半边也有可能是全屏灰底黑块。很多入门的朋友在这里踩坑照着厂商例程抄了一遍初始化但屏幕就是不出东西。因为初始化顺序是有依赖关系的。比如你必须先设置偏压和扫描方向再开显示你先开了显示再改扫描方向屏幕上会瞬间闪一下或者出现一整屏噪点。更隐蔽的是“复位后延时”和“上电稳定时间”。RA8806 在 VDD 稳定后需要拉低 RST 再释放释放后要等内部振荡器稳定这个延时不够后续寄存器写得再对也不生效。2.4 读忙标志看起来是细节其实是稳定性关键RA8806 内部有时序控制器每次写完一个字节后芯片需要几十微秒的时间处理内部命令。处理期间如果你立刻写下一个寄存器数据可能被丢弃。于是就有了“读忙标志”Busy FlagBF这个机制读状态寄存器时最高位表示芯片当前是否忙。厂商的 8051 例程里通常有一条 Wait_Busy 函数不断读状态寄存器直到 BF 位为 0。移植到 ESP32 和 FPGA 时很多人都把它简化掉了我有段时间也图省事直接固定延时。实测下来固定延时在室温下没毛病但环境温度一降、驱动电压一变延时就可能不够出现间歇性花屏。所以读忙标志不能省这是整块屏幕稳定运行的前提。3. ESP32 侧移植从厂商裸机 C 代码到 IDF 组件的完整改造3.1 为什么不能直接抄 Keil 例程厂商给的 8051 代码用的是 sbit 定义引脚、_nop_()做延时、data/idata这种 Keil 特有的存储类型。这些到了 ESP-IDF 上全部不兼容连编译都过不了。所以移植要做的是“语义移植”把原来的总线操作翻译成 ESP32 的 GPIO 操作把原来的寄存器表原样保留。这里有一个小技巧把厂商例程里的寄存器写入表提取出来整理成一张结构体数组每个元素包含“命令还是数据”和“字节值”。这样初始化代码就变成了一段循环遍历数组的代码逻辑非常清晰也方便后面移植到 FPGA 时做逐条对照。3.2 引脚规划8 根数据线 4 根控制线ESP32 的 GPIO 比较充裕CYW240128 用掉 12 根引脚完全没压力。下面是推荐分配避开了一路 ADC 的默认引脚和 Flash 相关引脚方便后续扩展信号ESP32 GPIO说明D0-D7GPIO 4、16、17、18、19、21、22、23数据总线八根必须连续吗不必须CSGPIO 15片选低有效RSGPIO 20命令1数据WRGPIO 0写信号RDGPIO 5读信号读忙标志时用RSTGPIO 14复位低有效用 ESP-IDF 的gpio_set_direction配置成输出或输入。写数据时要保证 8 根数据线同时配置为输出读忙标志时要切成输入这个切换建议封装成函数不要在业务代码里散落着gpio_set_direction调用。3.3 核心代码一次总线读写的时间真相下面这段是核心的写字节逻辑用 GPIO 模拟 8080 时序。重点在延时控制。void cyw_write_byte(uint8_t is_cmd, uint8_t dat) { gpio_set_level(CS_PIN, 0); gpio_set_level(RS_PIN, is_cmd ? 0 : 1); for (int i 0; i 8; i) { gpio_set_level(data_pins[i], (dat i) 0x01); } gpio_set_level(WR_PIN, 0); WR_DELAY(); // 维持低电平至少 t_wl gpio_set_level(WR_PIN, 1); WR_DELAY(); // 维持高电平至少 t_wh gpio_set_level(CS_PIN, 1); }RA8806 手册上写的写脉冲最小宽度通常在 80ns 左右而 ESP32 的 GPIO 翻转一次大约要几十纳秒到一两百纳秒取决于主频和编译优化。这里用WR_DELAY()宏来提供几个空指令的延时实测下来完全够用。如果你用的是 ESP32-S3 这种主频更高的芯片反而要注意延时太短导致写入失败保守起见在 WR 拉低和拉高之间各加一个 CPU 空转。要注意的是**ESP32 跑 Wi-Fi/蓝牙时 CPU 频率可能被变频软件延时 cycle 数不能太靠边界值。**我一般会把最小值做到手册要求的 2 到 3 倍这样既能解决变频影响也不至于慢到影响整体刷新率。3.4 初始化序列的搬运和验证把厂商例程里的寄存器序列搬过来后建议在每条写入之间加一个Wait_CYW_Ready()调用也就是读忙标志检测static void cyw_wait_ready(void) { // 先把数据线切到输入方向 for (int i 0; i 8; i) { gpio_set_direction(data_pins[i], GPIO_MODE_INPUT); } gpio_set_level(CS_PIN, 0); gpio_set_level(RS_PIN, 0); // 状态寄存器 while (1) { gpio_set_level(RD_PIN, 0); RD_DELAY(); uint8_t st read_data_pins(); gpio_set_level(RD_PIN, 1); if (!(st 0x80)) break; } gpio_set_level(CS_PIN, 1); // 恢复数据线方向为输出 for (int i 0; i 8; i) { gpio_set_direction(data_pins[i], GPIO_MODE_OUTPUT); } }这里有一个容易踩的坑读忙标志读的是状态寄存器不是命令寄存器RS 必须置 0并且数据线方向必须改为输入。如果你之前一直只写了“写”的逻辑忘了方向切换读回来的永远是 0x00 或者 0xFF看起来像“永远不忙”实则完全没读到有效状态。初始化跑完的验证方法很简单调用一次清屏然后往显存地址写 0xFF 再做整行填充屏幕应该出现一条全黑的横线。如果颜色反了黑底白线说明数据极性或者显存数据反了很多屏都支持极性翻转寄存器先别急着改代码查一下控制器的显示极性配置项。3.5 封装成 ESP-IDF 组件建议直接把驱动封装成components/cyw240128/组件对外暴露下面这几个接口就行别把 GPIO 操作暴露到业务层void cyw240128_init(void); void cyw240128_clear(uint8_t fill); void cyw240128_draw_pixel(uint8_t x, uint8_t y, uint8_t color); void cyw240128_fill_rect(uint8_t x0, uint8_t y0, uint8_t w, uint8_t h, uint8_t color); void cyw240128_self_test(void);cyw240128_self_test()这个函数值得专门写它做的是全屏填充、反色闪烁、扫描显示用来确认屏幕本体有没有问题。每次联调、排查问题前先跑一遍自检能最快把故障范围划到你自己的代码还是屏幕上。4. FPGA 侧控制器用 Verilog 重写并口主控与显存映射4.1 先想清楚FPGA 在这里到底扮演什么角色FPGA 端驱动 CYW240128有两种工作形态。第一种比较简单FPGA 只当主控自己内部状态机生成像素数据直接驱动屏幕。第二种是嵌入式开发场景里最常见的形态外部 MCU比如 ESP32通过总线接口把数据写进 FPGA 内部的显存 RAMFPGA 再根据显示时序从 RAM 读取数据逐字节刷新到屏幕上。你问的“完整调试代码”大概率是第二种——因为如果只让 FPGA 自己显示根本不需要 ESP32 参与。所以 FPGA 端的架构是三层总线从机接口模块、显存 DPRAM、屏幕刷新控制模块。ESP32 通过 GPIO 模拟的 8080 总线接到 FPGA 的 IOFPGA 侧用一个解析状态机把总线上传来的地址和数据写入 DPRAM屏幕刷新控制模块则不断扫描 DPRAM把数据按 RA8806 的显存映射输出到屏幕总线。4.2 双端口 RAM解决“写入”和“刷新”打架的唯一方案如果只用一个单口 RAMESP32 写入显存和 FPGA 刷新屏幕会抢同一根地址总线处理不好就会出现显示毛刺。双端口 RAMDPRAM有两个独立的端口A 口给总线从机写入B 口给屏幕刷新读取读写互不干扰这是显存设计最稳妥的写法。Xilinx 系的 FPGA 用 Block Memory Generator 生成 True Dual Port RAM 就行Altera/Intel 系用 Quartus 的 RAM: 2-PORT 生成。注意设置写优先WRITE_FIRST还是读优先READ_FIRST我推荐写优先因为 ESP32 写入时的实时性更关键刷新读一边只差几个时钟周期视觉上完全无感。4.3 总线从机模块把 ESP32 的写时序翻译成 RAM 写使能这里给出关键模块的结构化代码思路核心逻辑是检测 WR 的上升沿同时在地址总线上读取目标地址在数据总线上读取数据然后产生一个写使能脉冲写入 DPRAM。module bus_slave #( parameter ADD_WIDTH 15 )( input wire sys_clk, input wire rst_n, // 外部总线接口 input wire cs_n, input wire rs, input wire wr_n, input wire rd_n, inout wire [7:0] db, // 显存接口 output reg ram_we, output reg [ADD_WIDTH-1:0] ram_addr, output reg [7:0] ram_wdata ); reg wr_n_d0; wire wr_rise wr_n_d0 ~wr_n; // 检测低脉冲后的恢复沿 always (posedge sys_clk or negedge rst_n) begin if (!rst_n) begin ram_we 1b0; end else begin ram_we 1b0; if (!cs_n wr_rise) begin ram_we 1b1; ram_addr db[ADD_WIDTH-1:0]; // 示例简化实际地址来自地址锁存 ram_wdata db; end end end endmodule上面这个代码只是一个示意结构实际工程里还得处理地址锁存问题。RA8806 的并口总线RS 为低时传输的是命令RS 为高时传输的是数据。当你想写某个显存地址时通常要先写地址寄存器再写显存数据。所以在 FPGA 从机里你要在内部定义一组“寄存器空间”控制寄存器、地址寄存器、数据寄存器。ESP32 写命令时写入控制寄存器或地址寄存器写数据时写入数据寄存器数据寄存器的写使能同时触发 DPRAM 的写操作。4.4 屏幕刷新控制把显存里的数据按坐标扫出去屏幕刷新模块本质上是一个不断循环的状态机从 DPRAM 的 B 口读出 8 位像素数据再按照 8080 时序写到屏幕上。给定显示坐标 (x, y)RA8806 的显存地址换算公式是mem_addr y / 8 * 240 x具体公式以你的初始化扫描方向为准不同方向设置会镜像或旋转坐标刷新控制状态机分四步拉低 CS、设置 RS、设置数据总线、产生 WR 脉冲。这个循环每一步的持续时间由计数器控制计数周期取决于你的系统时钟频率。系统时钟 50MHz一个写周期 20ns而屏幕需要的写脉冲最小宽度 80ns所以每个写状态保持 4 个时钟以上就安全了。调试刷新控制模块时一个特别有效的技巧先别管从 ESP32 写入的数据直接在 FPFA 内部做一个计数器向显存里循环写入递增数据然后在屏幕上观察条纹。如果条纹是渐变的、连续的说明刷新通路没问题如果条纹断裂、乱序那就是地址换算公式错了优先检查扫描方向设置。4.5 时钟约束和跨时钟域最容易忽略的一步FPGA 工程里总线从机模块的时钟域和外部总线时序是跨时钟域关系。如果 ESP32 的 GPIO 信号直接进 FPGA 的普通 IO没有做同步处理就有亚稳态风险。稳妥做法是所有来自外部总线的信号CS、RS、WR、RD、D0-D7先打两拍同步到 FPGA 系统时钟域再参与逻辑处理。付出的代价是几个时钟周期的响应延迟换来的是可靠性。约束方面在 Xilinx 工程里要给外部总线信号加set_input_delay约束约束值来自 ESP32 输出数据的建立保持时间和 PCB 走线延迟。但如果你是学生或者快速原型验证不约束通常也能跑通因为 GPIO 模拟总线的边沿足够慢。真正到了要布 PCB、提高刷新率的时候再做 input delay 约束也不迟。5. 联合调试三板斧逻辑分析仪、片内抓波和显示效果对照5.1 不要让屏幕一个人背锅CYW240128 联合调试最常见的现象是屏幕显示异常于是怀疑屏幕坏了。实际上屏幕坏的几率很低更可能的是初始化没生效、写时序出错、地址换算错误或者电源不稳。所以调试的第一步不是换屏而是确认故障范围。我给一个排查链路照着走能帮你少走很多弯路用 ESP32 直接接屏幕先跑厂商例程的初始化序列确认屏幕本体正常确认屏幕正常后再把 ESP32 接到 FPGA 的外设总线FPGA 不接屏幕先用逻辑分析仪抓 ESP32 发出的总线波形确认 FPGA 收到了正确的数据和地址用 FPGA 片内逻辑分析仪ILA / SignalTap抓总线从机模块和 DPRAM 写端口的信号确认数据真的进了 RAM再抓屏幕刷新状态机的信号确认从 RAM 读出来的数据和写进去的数据一致最后才接上屏幕观察实际显示效果。每一步确认一个缓冲区五步走完故障点肯定能定位到模块级。5.2 逻辑分析仪测出来的波形重点关注四个时间参数接上逻辑分析仪重点看这四个参数WR 低电平持续时间t_wlWR 高电平持续时间t_whCS 拉低到 WR 拉低之间的建立时间t_stab数据线在 WR 沿前后是否稳定ESP32 模拟的并口时序波形上 WR 是一个很窄的负脉冲数据线在 WR 拉高前就稳定了。如果数据线在 WR 拉高的时刻还在抖动大概率是数据建立时间不够需要在驱动代码里把数据放到总线上之后再延迟一小段时间再抬 WR。这个问题在 FPGA 里也能修FPGA 从机模块对数据线信号多做一拍寄存数据稳定性会显著改善。5.3 一个典型的花屏案例复盘有一次调试ESP32 往显存写数据屏幕出现规律性花屏每隔 240 个像素出现一列乱码相当于每一行开头都是乱的后面正常。查了 DPRAM 地址映射发现屏幕刷新控制里行地址的步进写错了。原本公式应该是“内存地址 y/8 * 240 x”我写成了“y/8 * 256 x”相当于每一行的起始地址多偏移了 16 个字节。因为 240 不是 256 的整数倍所以每行开头都会错位花屏位置正好是 256-24016 个像素的偏移。这类问题用 ILA 抓地址计数器一眼就能发现。5.4 白屏和单点亮线的区分白屏往往是初始化没有完成或者偏压没有建立。单点亮线则是显存扫描和屏幕刷新不同步比如你写入了整屏数据但扫描地址范围只覆盖了 1/8屏幕上就只会出现一条亮线。这两种问题在排查思路上完全不同白屏优先查初始化序列和硬件复位亮线优先查地址范围配置和刷新状态机的扫描边界。如果你是第一次做这个屏我的建议是先用最简单的方式打通通路。FPGA 内部直接用计数器生成一个递增数写入显存屏幕显示条纹图案然后再接 ESP32 写数据。这样两边的代码可以独立验证避免了“两边都有问题”的叠加态。6. 工程组织与避坑经验一份能跑的调试代码应该长什么样6.1 一个完整调试工程的最小文件清单如果让我评价一份代码“是不是完整调试代码”我会看它是不是具备下面这些文件缺一个都谈不上完整模块文件作用ESP32 驱动cyw240128.c / .h总线读写、初始化、图形接口ESP32 业务main.c调用驱动跑自检验证 FPGA 通路FPGA 总线从机bus_slave.v接收 ESP32 写时序FPGA 显存dpram IP / ram_tb双端口 RAM 生成配置FPGA 屏幕刷新lcd_ctrl.v按扫描时序输出到屏幕FPGA 顶层top.v例化以上模块仿真环境tb_top.v测试总线解析、DPRAM 读写约束文件top.xdc / top.sdc引脚分配 时钟约束很多朋友只做了前面四项没有仿真环境直接上板调。我的建议是至少给总线从机模块写一个仿真这个模块的接口时序是纯数字逻辑仿真跑起来非常快能提前发现绝大多数解析问题。屏幕刷新控制模块的价值更多在上板改扫描方向时体现仿真效果反而不如上板直观。6.2 上电顺序和复位时序一个代价昂贵的教训当时我为了省时间把 FPGA 和屏幕用同一个电源轨供电结果屏幕经常上电后不回显。查了很久才发现是复位时序问题FPGA 和屏幕同时上电FPGA 配置完成需要几百毫秒而屏幕的复位信号已经在这段时间里被释放了屏幕等不到完整复位。解决方案是给屏幕的复位信号加一个 RC 延时或者由 FPGA 引脚控制在 FPGA 配置完成后再释放屏幕复位。任何情况下都不要让屏幕和主控共享一个硬复实现直接连上你不是在省成本是在埋雷。正确的上电顺序是给屏幕供电VDD 稳定拉低 RST 至少 10ms拉高 RST等待内部振荡器稳定手册上通常写 100ms 以上开始写初始化寄存器序列。6.3 读忙标志和固定延时的取舍再提醒回到之前提过的读忙标志在 FPGA 屏驱的情况下等待忙标志的代码不能直接在 Verilog 里写while(st 0x80)因为状态机里不能有阻塞等待。正确处理是设计一个状态查询忙标志如果忙保持当前状态直到下一次读到的状态为不忙。有些 FPGA 工程师图省事用“写完就固定延时 100us”的方式替代。在一些场景下没问题但我还是建议把忙标志检测做进去因为 RA8806 的命令处理时间不是均匀的清屏、画线这种带内部运算的命令花费的时间比其他寄存器写入长得多。固定延时必须按最坏情况估计否则屏幕响应速度会被拖慢而忙标志检测可以在保证可靠性的同时做到最快。6.4 数据总线方向切换一个隐蔽的 bug 源ESP32 侧的读写切换FPGA 侧的 inout 三态口都是容易出 bug 的地方。FPGA 的设计中数据总线是 inout 类型当读使能有效时需要把数据总线从高阻态切换为输出状态把读到的数据放到总线上。这个三态门的切换时序如果控制不好总线冲突一次可能只是数据错误严重时会损伤 IO。一个安全策略是**读总线信号的输出使能只在 RD 信号有效期内打开其他时间保持高阻态。**ESP32 侧的数据方向切换同理读忙标志期间才把数据线切到输入模式。我曾经因为读忙标志之后忘了切换回输出模式导致后续所有写入都不生效屏幕屏幕定格在最后一帧查了两小时才反应过来是方向切换漏了。6.5 关于获取驱动例程的实际渠道如果你手头没有厂商例程直接去芯片厂家官网找对应控制器的 datasheet 和 Application Note同时去 GitHub 搜 RA8806 的关键词经常能搜到别人移植好的 STM32 驱动。屏幕厂家的 FAE 那里也通常会有一份相对完整的 demo只是不会挂在官网下载页面。找 FAE 要的时候别只问 ESP32 版本直接问他“有没有任何平台的参考代码”只要是能跑通这套控制器时序的样例都能作为移植参考。如果你准备长期开发这类屏幕我有个习惯性的做法广每个屏的工程都会建一个vendor/文件夹把原始例程原封不动放进去不做任何修改。后续需要查寄存器表、查某个命令的默认值直接翻原始工程避免被我自己改过的代码带偏。7. 最后再分享一个提速技巧CYW240128 的 RA8806 控制器显存写入官方推荐的写周期是纳秒级实际上 ESP32 的 GPIO 模拟能做到几微秒一个字节就已经很好了。如果整屏刷新率不够不要急着优化写时序先看你的软件架构里有没有做“预打包”把一行的像素数据拼成连续字节数组再用一次 DMA 或者连写循环批量写入比逐像素写入快了不是一点点。FPGA 端的刷新控制也一样可以考虑把 8 个像素打包成一个字节后一次写入但这要求显存布局从“每像素 1 bit”改成“每字节 8 像素”。这个优化我后来做了画一条 240 像素的水平线从原来的 240 次写操作变成 30 次写操作刷新率提升非常明显。不过这个优化会加大显存地址换算的复杂度建议先把基础版调通再考虑。两块很是实用的建议一是所有调试阶段都在显存固定区域写一个帧分隔符比如 0xAA、0x55 交替屏幕出现规律性交替条纹说明通路正常二是 FPGA 的屏驱状态机可以在屏幕上预留一行专门显示内部状态寄存器的值。这两个小技巧配合起来每次屏显异常五分钟就能定位到是数据通路、控制状态机还是 ESP32 的逻辑问题。
返回列表