ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ST7789 MADCTL寄存器深度解析:屏幕显示方向与旋转配置实战

ST7789 MADCTL寄存器深度解析:屏幕显示方向与旋转配置实战 很多人第一次拿到ST7789屏幕时都干过这种事初始化代码照着网上教程敲完屏幕亮了但是画面倒过来了或者像照镜子一样左右反着。更迷惑的是换一个驱动库或者换一块同型号模组同样的代码又变了个方向。折腾一圈最后发现所有问题都指向同一个寄存器——ST7789的MADCTLMemory Data Access Control地址0x36。这篇就把这个寄存器彻底讲透从位定义到旋转配置、从坐标偏移到触摸映射用实战的角度拆解你踩过的和可能踩的所有坑。1. 为什么显示方向问题总是绕不开MADCTL1.1 所有驱动库的旋转功能本质都在写同一个寄存器STM32标准库、ESP-IDF、Arduino的TFT_eSPI、MicroPython的st7789驱动、LovyanGFX你觉得它们差异很大其实到了“设置方向”这一步代码最后都是同一个动作往0x36寄存器写一个字节。比如TFT_eSPI里setRotation函数最终会执行writecommand(TFT_MADCTL)然后跟上对应的madctl值Arduino_GFX里也是类似的逻辑LovyanGFX的rotation配置表本质上还是那四个或八个MADCTL值。我看过不少人改旋转方向是在应用层硬调坐标比如把某个按钮的x改成w-1-x把y改成h-1-y但这只能救急。因为底层画点函数、画图库、字体方向全都被MADCTL的决定性影响覆盖着你上层再怎么补也补不出一个真正顺畅的旋转逻辑。正确做法是先把MADCTL配置好让屏幕的扫描方向符合你的预期上层代码只用处理逻辑坐标不要关心物理像素。1.2 MADCTL控制的是“GRAM地址到屏幕像素”的映射规则ST7789内部有一块GRAM数据手册叫图形显存。你往0x2CRAMWR写像素数据时并不是直接把数据打到屏幕固定位置而是按照一定规则填进GRAM再由控制器把GRAM内容刷新到面板上。MADCTL就是定义这个“一定规则”的。换句话说显示方向问题的本质是GRAM地址递增方向和屏幕物理坐标轴方向不匹配导致同样一块内存内容呈现出来的画面是倒的、镜像的、或者横竖颠倒的。理解了这一点你就不需要靠玄学去试MADCTL值了。1.3 为什么网上同一个屏幕有完全不同的配置值你可能会搜到这样的初始化代码MADCTL写0x00另一个教程写0xC0还有一个写0xA0它们都说自己是对的。其实它们都对只是各自选了不同的“0度基准方向”。有的驱动把竖屏定义为0度有的把横屏定义为0度有的库认为MADCTL0x00时屏幕是竖屏有的库在初始化时已经做了一次旋转。所以不要看到不同值就慌关键是要理清基准方向和旋转方向。下面我直接把MADCTL的每个位拆开然后给出完整推导过程让你能根据自己屏幕的实际表现算出正确的配置值。2. MADCTL每个位到底在干什么从坐标流水线看寄存器行为2.1 寄存器位定义总览先看ST7789数据手册里MADCTL0x36的位定义这里我整理成表格方便对照位名称含义0的作用1的作用bit7MYPage Address Order行地址从上到下top to bottom行地址从下到上bottom to topbit6MXColumn Address Order列地址从左到右left to right列地址从右到左right to leftbit5MVPage/Column Order正常模式不交换行列反转模式行列交换bit4MLLine Address Order刷新方向从上到下刷新方向从下到上bit3BGRColor OrderRGB颜色顺序BGR颜色顺序bit2MHDisplay Data Latch Order数据锁存方向从左到右数据锁存方向从右到左bit1-0—保留不同版本数据手册标注略有差异——需要提醒一下ST7735的MADCTL里bit1和bit0是有实际含义的SS和GS但ST7789的大多数版本数据手册将它们标为保留位所以别把ST7735的配置习惯直接硬套到ST7789上这也是有人“按经验配置翻车”的原因之一。2.2 把屏幕想象成一张坐标纸为了方便理解把屏幕想成一张240x320的坐标纸横轴X对应Column纵轴Y对应Page。默认情况下MADCTL0x00GRAM地址从坐标(0,0)开始递增X先从0走到239然后Y加1X重新从0开始。这就是我们熟悉的“从左到右、从上到下”的光栅扫描顺序此时屏幕看起来是完全正常的。当MY1时相当于把Y轴反向起始位置变成左下角当MX1时相当于把X轴反向起始位置变成右上角当MV1时Column和Page的角色互换相当于把整张坐标纸转了一下。这些位单独使用都很直观组合起来就能构建出任意一个旋转方向。2.3 ML和MH是什么时候才会用到的MV、MX、MY三个位决定了扫描起始点和扫描方向日常调方向主要就是它们在起作用。ML是Line Address Order控制LCD刷新时行扫描的方向MH是Display Data Latch Order控制显示数据锁存方向。这两个位和GRAM地址映射没有直接关系而是和面板的刷新方向有关。一般静态画面或者不追求带TE同步撕裂同步效果时ML和MH保持默认即可。但如果你开启了0x35Tearing Effect On或者用DMA乒乓缓冲做高帧率动画ML和MH设置不对会导致画面刷新顺序异常表现为画面撕裂的位置不太对劲。多数情况下你不用动它们知道有这两个位存在就行。3. 四个旋转方向的配置值从0x00到0xA0的推导过程3.1 明确基准方向为了让推导有意义先约定一个基准MADCTL0x00时屏幕为竖屏240x320左上角是坐标原点X向右递增Y向下递增。这是绝大多数驱动默认的“0度方向”。在这个基准下四个旋转方向对应的MADCTL值如下表方向逻辑宽高MADCTLRGB顺序MADCTLBGR顺序组合含义0度竖屏基准240x3200x000x08MY0, MX0, MV090度顺时针320x2400x600x68MV1, MX1180度240x3200xC00xC8MY1, MX1270度顺时针320x2400xA00xA8MY1, MV13.2 为什么90度方向是0x60而不是其他值用坐标推导就比较清楚了。屏幕顺时针旋转90度后逻辑宽高变成320x240用户看到的左上角其实是原面板的右上角也就是原始坐标(239, 0)这个位置。从用户的视角看他的X方向对应原始Y方向的递增他的Y方向对应原始X方向的递减。要让GRAM的自动递增符合这个行为需要两个条件MV1让行列角色互换这样地址生成器会把Page方向当作X方向MX1让Column方向反向这样垂直方向的扫描是从大到小走。合起来就是0x20 | 0x40 0x60。同样的方法推180度和270度180度左上角在原面板右下角(239,319)用户的X方向对应原始X递减Y方向对应原始Y递减所以MX1、MY1得到0xC0。270度左上角在原面板左下角(0,319)用户的X方向对应原始Y递减Y方向对应原始X递增所以MV1、MY1得到0xA0。3.3 为什么有的库会写0x20、0x70这种值如果你打开某个驱动发现它的90度配置是0x20不要觉得矛盾。这可能是因为它的0度基准方向定义不同或者初始化时已经设过一个自定义的起始方向。比如有的库先把MADCTL设为0xC0作为竖屏基准那它的90度自然就是0x20了。这些值之间并不是孤立的它们构成一个旋转群每次顺时针转90度相当于在当前值基础上做一次特定的位变换。所以我个人建议不要死记数字记住推导方法。每拿到一块新屏幕先设置0x00看看画面然后根据实际情况用“左上角在哪、X往哪走、Y往哪走”来推最多两轮就能确定全部四个值。3.4 完整setRotation代码示例下面给一个STM32 HAL风格的设置旋转函数适用于大部分基于ST7789的屏幕void LCD_SetRotation(uint8_t dir) { dir % 4; switch (dir) { case 0: lcd_dev.w 240; lcd_dev.h 320; LCD_Write_Cmd(0x36); LCD_Write_Data(0x00); // 0度: MY0 MX0 MV0 break; case 1: lcd_dev.w 320; lcd_dev.h 240; LCD_Write_Cmd(0x36); LCD_Write_Data(0x60); // 90度: MV1 MX1 break; case 2: lcd_dev.w 240; lcd_dev.h 320; LCD_Write_Cmd(0x36); LCD_Write_Data(0xC0); // 180度: MY1 MX1 break; case 3: lcd_dev.w 320; lcd_dev.h 240; LCD_Write_Cmd(0x36); LCD_Write_Data(0xA0); // 270度: MY1 MV1 break; } LCD_Address_Set(0, 0, lcd_dev.w - 1, lcd_dev.h - 1); }注意看这个函数里最关键的一步写完MADCTL之后重新设置了CASET/RASET地址窗口。如果这一步忘了旋转后画面可能只显示在屏幕的某个区域这就是接下来要说的联动问题。4. MADCTL不是孤岛与CASET/RASET和初始化序列的联动关系4.1 典型初始化序列里MADCTL该放在什么位置ST7789的初始化序列因模组而异但大结构一般是这样0x01软件复位延时50ms以上0x11退出睡眠SLPOUT延时100ms以上0x36MADCTL设置方向0x3A设置像素格式RGB565写0x55系列电源/帧率寄存器0xB2、0xB7、0xBB、0xC0、0xC2、0xC3、0xC4、0xD0、0xD1、0xE8等0x21反色开启部分模组需要部分不需要0x29开启显示DISPONMADCTL放在SLPOUT之后是常见写法因为SLPOUT之前控制器还在睡眠状态很多寄存器写了也可能不生效。但要注意初始化序列里的MADCTL只是设了一个“初始方向”真正决定你应用层旋转的是后面调用setRotation时写入的值。4.2 CASET/RASET范围如何跟着MADCTL变化旋转90度和270度后逻辑分辨率从240x320变成320x240CASET和RASET的范围必须同步交换。如果代码里只改了MADCTL没有更新宽高地址窗口仍然按240x320设置那320x240的画面就会被截断。这里给一个完整的地址窗口设置函数带偏移量参数void LCD_Address_Set(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { LCD_Write_Cmd(0x2A); // CASET LCD_Write_Data((x1 col_offset) 8); LCD_Write_Data((x1 col_offset) 0xFF); LCD_Write_Data((x2 col_offset) 8); LCD_Write_Data((x2 col_offset) 0xFF); LCD_Write_Cmd(0x2B); // RASET LCD_Write_Data((y1 row_offset) 8); LCD_Write_Data((y1 row_offset) 0xFF); LCD_Write_Data((y2 row_offset) 8); LCD_Write_Data((y2 row_offset) 0xFF); LCD_Write_Cmd(0x2C); // RAMWR }col_offset和row_offset就是屏幕模组的坐标偏移。如果这两个值是固定死写在代码里的旋转后就会出现整体偏移。4.3 旋转后偏移量也要跟着交换这是很多人栽跟头的地方。以1.69寸240x280的ST7789圆角模组为例很多版本需要row_offset20也就是说实际面板的280行有效显示区域在GRAM里是从第20行开始的。竖屏时RASET范围是20到299旋转90度后逻辑宽高变成280x240行列角色互换原本加到RASET上的偏移就变成加到CASET上了即col_offset20row_offset0。所以每次setRotation时最好连offset也一起更新而不是用一个全局常量。你可以把offset定义成两个变量在旋转分支里分别赋值。以我实测过的模组来看不同批次、不同厂家的ST7789模组offset差异可以很大比如同样是1.3寸240x240有(0,0)的也有需要(0,40)的这些信息说明书上不一定写清楚只能实测。4.4 设置完MADCTL后全屏变“缩进去”了先查地址窗口如果你旋转后遇到以下现象画面只有一半、画面整体偏移、四周出现黑边或色块大概率不是MADCTL值本身错了而是CASET/RASET范围或者offset没跟上。调试时优先检查0x2A和0x2B写入的数据再看宽高有没有交换。5. 实战中的三个高频坑BGR顺序、模组偏移和触摸坐标反算5.1 坑一红色和蓝色完全互换绿色正常这是我见过出现频率最高的颜色问题。现象很典型画红色屏幕显示蓝色画蓝色屏幕显示红色绿色基本看不出异常。原因是MADCTL的bit3BGR位没有设置正确。ST7789控制器默认输出RGB顺序但很多模组厂家在排线布局上把R和B的信号调换了导致实际面板要BGR顺序才能显示正常。解决方法是给MADCTL值加上0x08。如果你用的是我前面那张表就把0x00改成0x08、0x60改成0x68、0xC0改成0xC8、0xA0改成0xA8。验证BGR只需要两步往屏幕写纯红色RGB565的0xF800。如果显示成蓝色就把MADCTL的bit3置1重新画。5.2 坑二相同型号屏幕偏移量不一样同一颗ST7789控制器可以驱动不同尺寸的模组240x320、240x240、240x280、135x240都有。即便都是240x240不同模组厂的GRAM起始地址也可能不一样有的从(0,0)开始有的要跳过一段无效区域。有个简单的实测方法全屏填充红色观察四周黑边位置。如果上方和下方有明显黑边说明row_offset需要调整如果左边和右边有黑边说明col_offset需要调整。拿一把直尺或直接在屏幕上放一个已知位置的方块图案就能反推出实际需要的偏移量。在代码里最好把offset做成可配置常量而不是硬编码在地址设置函数里。调试阶段用串口输出当前使用的offset排错会快很多。5.3 坑三带触摸的屏幕显示旋转了触摸却“反”了如果你做的是带触摸方案的完整人机界面显示方向改了触摸坐标也要跟着做同样的几何变换否则点击一个按钮系统却认为你点了另一个位置。触摸控制器比如XPT2046返回的是原始ADC坐标和显示方向没有任何关系所以必须由软件映射。假设触摸芯片原始坐标范围是tx、ty当前显示方向dir对应的坐标映射关系如下显示方向映射后的x映射后的y0度txty90度顺时针ty屏幕高度 - tx180度屏幕宽度 - tx屏幕高度 - ty270度顺时针屏幕宽度 - tytx需要注意这里的具体公式取决于触摸芯片的安装方向不同的模组可能正负相反。实际调试时可以先画一个十字线然后用触摸笔点击十字线的四个方向观察哪个轴反了就翻哪个轴比纯用公式推导更快。5.4 另一个容易被误判为方向问题的坑反色有些模组需要开启0x21INVON有些需要关闭。现象是颜色像“负片”一样红变成青、绿变成紫。很多人误以为是MADCTL的方向配置和颜色配置互相干扰调了半天方向其实是反色开关没设对。ST7789不同版本、不同模组对反色的要求不一样同样以实测为准。6. 快速定位显示方向的调试方法与个人习惯6.1 四角标点法一次看清起点和扫描方向调试显示方向时最忌讳的是在全屏画复杂图像后猜问题。正确做法是先画一个简单且信息明确的测试图案。我的习惯是在四个角分别画不同颜色的点左上角红色点右上角绿色点左下角蓝色点右下角白色点然后看屏幕实际显示效果。如果“左上角”红点跑到了别的位置就能立刻判断出扫描起点和坐标轴方向偏移了多少。比如红点和绿点互换了位置说明X轴反了红点和蓝点上下换了说明Y轴反了如果红点在右上角绿点在右下角说明整体转了90度。6.2 纯色块测试验证RGB顺序和反色开关画四个横向色块分别是红、绿、蓝、白然后观察如果红和蓝互换改MADCTL的BGR位。如果整体颜色像负片检查0x21反色开关。这两个问题经常同时出现最好先解决颜色顺序再做方向调整否则容易互相干扰判断。6.3 对比法用一套已知正常的库做参照如果你调了很久还是不对别闷头改找一个成熟驱动库先跑起来。比如TFT_eSPI在ESP32上Adafruit_ST7789在Arduino上都是开箱即用、验证过无数次的驱动。跑通之后用逻辑分析仪或者调试器抓一下它初始化时给0x36写了什么值、CASET和RASET范围是多少、offset是多少再对照你的代码差异问题基本一眼就能看出来。如果条件允许我建议在驱动的setRotation函数里加一个串口打印把MADCTL值、CASET范围、RASET范围、offset四组数据打出来。这个习惯能省掉大量“怀疑人生”的时间。6.4 排查方向问题的检查清单根据我调过十几块不同ST7789模组的经验方向不对时按这个顺序排查确认MADCTL的四个旋转值是否符合你的基准方向必要时重新做一次坐标推导。确认setRotation里更新了宽高且CASET/RASET范围跟着变了。确认offset在旋转后是否交换了行列分量。确认BGR位是否满足当前模组。确认反色位0x21是否正确。确认RGB565格式0x55是否设置成功如果用RGB666写0x66部分接口下显示会“花”。这套流程走下来99%的显示方向问题都能定位到具体环节。剩下的1%多半是硬件连接或面板本身的问题那就只能换块模组验证了。我个人现在每调一块新屏都会先跑一遍固定的五分钟测试流程纯色测颜色顺序四角标点测方向格子图测镜像全屏填充测偏移。全部通过后再写真正的业务界面。看起来浪费时间实际上是把踩坑成本控制在最低。MADCTL这个东西说白了就是“起点加扫描方向”的组合开关理解了坐标映射你就不会再被各种库的不同配置值绕晕了。
返回列表