ST7789是控制器名,第一屏还要锁定面板
本篇用Adafruit PID3787的1.54英寸240×240屏,不是135×240、240×320,也不是别的同芯片模块。控制器能寻址240×320 RAM,具体面板只露出其中240×240;所以代码能发数据并不代表窗口位置正确。
先画独立RGB色条、四角编号和1像素边框,再接真实SHT40。这样颜色/方向错不会被复杂界面掩盖,传感器失联也不会继续显示上一条温湿度。四方向照片和真实断线记录仍待实板。
参考:[1] PID3787产品与2022年11月修订[3] Adafruit ST7789固定1.11.0窗口和方向[4] IDF5.5.5 ST7789:RAMCTRL/色序/镜像/偏移
把显示与采样分成两组接线核对
本例走排针接线,使用排针已可靠焊接的同修订PID3787。随附散排针不等于已焊好;未焊时先由具备工具和焊接条件的人完成并检查,不能将排针松插焊孔当作电气连接。EYESPI仍留空,不在本例切换接口。
显示供电用3V3→VIN,模块3V输出留空,GND共地。SCK12、SI11、SO10、TC5、CC13、DC4、RT6、BL7,两个CS分别10kΩ上拉。SD插槽空,TE与EYESPI不用;BL高开低关,背光发亮不等于SPI成功。
SHT40锁PID4885,VIN3V3、GND、SDA8/J1-12、SCL9/J1-15,0x44/100kHz;该模块已有外部上拉,程序内部上拉只是辅助。先断电接好,再经USB-to-UART桥接接口构建/查看115200串口,GPIO43/44留给桥接。
- 做到这里,你应该看到
- 先按F07核对PID与模块排针,绝不把稳压输出3V当作VIN。
参考:[1] PID3787产品与2022年11月修订[2] Adafruit具体排针、电源、背光与SO用途[6] DevKitC-1 v1.1排针[7] Adafruit PID4885 SHT40模块[10] IDF5.5.5 S3新I2C主机API
红蓝相反、像素杂色、边缘裁切分别查什么
RGB565把红绿蓝分别放在16位的5/6/5位域:红0xF800、绿0x07E0、蓝0x001F。RGB/BGR决定红蓝位域如何解释;两个字节的传送顺序又是另一个设置。IDF5.5.5的ST7789驱动确实实现data_endian,设置LITTLE后在RAMCTRL置位,匹配ESP32的uint16_t内存布局。
本例RGB元素顺序为RGB、inversion=true,先按模块参考驱动保持这些条件。若纯红纯蓝对调,先查RGB/BGR;若出现杂色再查16位字节序。不要把invert_color当成红蓝互换开关。
参考:[3] Adafruit ST7789固定1.11.0窗口和方向[4] IDF5.5.5 ST7789:RAMCTRL/色序/镜像/偏移
四种方向逐项对应,不靠屏幕正方形猜
Adafruit ST7789 1.11.0在240×240分支设置rowstart=80、rowstart2=0,然后按方向选择MADCTL和起始窗口。本工程用IDF的swap_xy、mirror和set_gap表达同一组设置,表中值也由同源host fixture校验。
按0/1/2/3时,内容都应保留完整边框和四角,且相对于模块排针转向可记录。数值映射通过不证明面板安装方向;实际照片仍需用同一物理参考方位拍摄。
| 按键 | swap_xy | mirror_x / mirror_y | gap_x / gap_y |
|---|---|---|---|
| 0 | false | true / true | 0 / 80 |
| 1 | true | false / true | 80 / 0 |
| 2 | false | false / false | 0 / 0 |
| 3 | true | true / false | 0 / 0 |
参考:[3] Adafruit ST7789固定1.11.0窗口和方向[4] IDF5.5.5 ST7789:RAMCTRL/色序/镜像/偏移
每轮采样先过两组CRC,失败就撤销valid
SHT40发送高精度命令0xFD,等待10ms后读6字节。前两字节温度、一个CRC,再两字节湿度、一个CRC;CRC初值0xFF、多项式0x31。公式为T=-45+175×raw/65535、RH=-6+125×raw/65535,湿度结果限制在0–100%。这些参数与Sensirion固定1.1.2/0.7.3源一致,本工程用原创小型解码器,不引入Arduino库。
每2秒一轮,I2C发送/接收各有100ms超时。NACK、超时或任何一组CRC坏就将当前valid设false;屏幕显示NO SAMPLE/CHECK I2C,旧数值隐藏。闭合SDA开关后下一次成功采样才恢复数值,没有固定演示温度补空。
参考:[8] Sensirion SHT4x1.1.2:0xFD、10ms与换算[9] Sensirion Core0.7.3:CRC31/FF[10] IDF5.5.5 S3新I2C主机API
完整工程:采样、渲染和DMA各有边界
在IDF5.5.5环境,使用独立构建目录运行下方命令,flash前替换真实桥接端口。下载同时含主CMake、main组件定义、sdkconfig.defaults、versions.lock、host fixture和空观测表。
显示使用7680字节内部DMA条带,等完成回调后复用;串口按键在每轮处理,单次I2C或画面发送仍占用一段时间,本例不承诺硬实时输入。渲染超时停止后续作业且保留缓冲,不把提交返回视作传输完成。
idf.py -B /tmp/d03-build -D SDKCONFIG=/tmp/d03-build/sdkconfig -D IDF_TARGET=esp32s3 build
idf.py -B /tmp/d03-build -p /dev/cu.YOUR_UART_PORT flash monitor参考:[4] IDF5.5.5 ST7789:RAMCTRL/色序/镜像/偏移[5] IDF5.5.5 SPI LCD:DMA事务与完成回调[10] IDF5.5.5 S3新I2C主机API
完整入口、传感器解码和显示组件
文件标签与下载相对路径一致。四种窗口配置、像素测试卡、样本valid与DMA等待都在以下源码里;硬件配置的变更应同时更新锁文件和观测记录。
// ---- main/Sht40Logic.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
inline uint8_t sht_crc(const uint8_t* b){uint8_t c=0xff;for(int k=0;k<2;++k){c^=b[k];for(int j=0;j<8;++j)c=(c&0x80)?uint8_t((c<<1)^0x31):uint8_t(c<<1);}return c;}
inline bool decode_sht40(const uint8_t b[6],int& t,int& rh){
if(sht_crc(b)!=b[2]||sht_crc(b+3)!=b[5])return false;
uint16_t rt=(uint16_t(b[0])<<8)|b[1],rr=(uint16_t(b[3])<<8)|b[4];
t=-45000+int((175000LL*rt)/65535);rh=-6000+int((125000LL*rr)/65535);
if(rh<0)rh=0;
if(rh>100000)rh=100000;
return true;
}
// ---- main/Console.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <unistd.h>
#include <fcntl.h>
#include "driver/uart.h"
#include "esp_vfs_dev.h"
inline void init_console(){
ESP_ERROR_CHECK(uart_driver_install(UART_NUM_0,256,0,0,nullptr,0));
esp_vfs_dev_uart_use_driver(UART_NUM_0);
int flags=fcntl(STDIN_FILENO,F_GETFL);fcntl(STDIN_FILENO,F_SETFL,flags|O_NONBLOCK);
}
inline char console_key(){char c=0;return read(STDIN_FILENO,&c,1)==1?c:0;}
// ---- components/hxdisplay/Graphics.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
#include <stdio.h>
#include <string.h>
namespace hx {
struct View { enum Page { TEST, SENSOR, PROGRESS } page=TEST; unsigned rotation=0,seq=0; bool valid=false; int temperature_mC=0,humidity_mPermille=0; };
// Original 5x7 uppercase/digit bitmap font, one row per byte (5 significant bits).
inline const uint8_t* glyph(char c) {
struct Entry { char c; uint8_t row[7]; };
static const Entry f[]={
{'0',{14,17,19,21,25,17,14}},{'1',{4,12,4,4,4,4,14}},{'2',{14,17,1,2,4,8,31}},
{'3',{30,1,1,14,1,1,30}},{'4',{2,6,10,18,31,2,2}},{'5',{31,16,16,30,1,1,30}},
{'6',{14,16,16,30,17,17,14}},{'7',{31,1,2,4,8,8,8}},{'8',{14,17,17,14,17,17,14}},{'9',{14,17,17,15,1,1,14}},
{'A',{14,17,17,31,17,17,17}},{'B',{30,17,17,30,17,17,30}},{'C',{14,17,16,16,16,17,14}},
{'D',{30,17,17,17,17,17,30}},{'E',{31,16,16,30,16,16,31}},{'F',{31,16,16,30,16,16,16}},
{'G',{14,17,16,23,17,17,14}},{'H',{17,17,17,31,17,17,17}},{'I',{14,4,4,4,4,4,14}},
{'J',{7,2,2,2,2,18,12}},{'K',{17,18,20,24,20,18,17}},{'L',{16,16,16,16,16,16,31}},
{'M',{17,27,21,21,17,17,17}},{'N',{17,25,21,19,17,17,17}},{'O',{14,17,17,17,17,17,14}},
{'P',{30,17,17,30,16,16,16}},{'Q',{14,17,17,17,21,18,13}},{'R',{30,17,17,30,20,18,17}},
{'S',{15,16,16,14,1,1,30}},{'T',{31,4,4,4,4,4,4}},{'U',{17,17,17,17,17,17,14}},
{'V',{17,17,17,17,17,10,4}},{'W',{17,17,17,21,21,27,17}},{'X',{17,17,10,4,10,17,17}},
{'Y',{17,17,10,4,4,4,4}},{'Z',{31,1,2,4,8,16,31}},{'.',{0,0,0,0,0,6,6}},
{'-',{0,0,0,31,0,0,0}},{'+',{0,4,4,31,4,4,0}},{'/',{1,1,2,4,8,16,16}},
{'%',{25,25,2,4,8,19,19}},{',',{0,0,0,0,6,4,8}},{' ',{0,0,0,0,0,0,0}}
};
for(const auto& e:f)if(e.c==c)return e.row;
static const uint8_t unknown[7]={31,17,1,2,4,0,4};return unknown;
}
inline bool ink(int x,int y,const char* text,int left,int top,int scale=2) {
x-=left;y-=top;if(x<0||y<0||y>=7*scale)return false;
unsigned i=unsigned(x/(6*scale));if(i>=strlen(text)||x%(6*scale)>=5*scale)return false;
return glyph(text[i])[y/scale] & (1<<(4-(x%(6*scale))/scale));
}
inline uint16_t pixel(const View& v,int x,int y) {
constexpr uint16_t W=0xffff,R=0xf800,G=0x07e0,B=0x001f,K=0;
if(x==0||y==0||x==239||y==239)return W;
char title[32],line[32];
snprintf(title,sizeof title,"R%u %s",v.rotation,v.page==View::TEST?"RGB TEST":v.page==View::SENSOR?"SHT40":"SPI SHARED");
if(ink(x,y,title,12,20))return W;
if(ink(x,y,"0",4,4,1)||ink(x,y,"1",229,4,1)||ink(x,y,"2",229,229,1)||ink(x,y,"3",4,229,1))return W;
if(v.page==View::TEST){
if(y>=50&&y<130)return x<80?R:x<160?G:B;
if(ink(x,y,"240X240",12,155)||ink(x,y,"RGB565 LE",12,182))return W;
const char* gap=v.rotation==0?"GAP 0,80":v.rotation==1?"GAP 80,0":"GAP 0,0";
return ink(x,y,gap,12,208)?W:K;
}
if(v.page==View::PROGRESS){
snprintf(line,sizeof line,"PASS %03u/100",v.seq);
if(ink(x,y,line,12,80)||ink(x,y,"WRITE READ",12,125)||ink(x,y,"SPI DONE",12,170))return W;
return K;
}
if(!v.valid)return (ink(x,y,"NO SAMPLE",12,80)||ink(x,y,"CHECK I2C",12,125))?W:K;
unsigned t=v.temperature_mC<0?unsigned(-v.temperature_mC):unsigned(v.temperature_mC);
snprintf(line,sizeof line,"T %c%u.%u C",v.temperature_mC<0?'-':'+',t/1000,(t%1000)/100);
if(ink(x,y,line,12,80))return W;
snprintf(line,sizeof line,"RH %u.%u %%",unsigned(v.humidity_mPermille)/1000,(unsigned(v.humidity_mPermille)%1000)/100);
return ink(x,y,line,12,125)?W:K;
}
struct Rotation { bool swap,mx,my; int gx,gy; };
inline Rotation rotation(unsigned r){switch(r&3){case 0:return {false,true,true,0,80};case 1:return {true,false,true,80,0};case 2:return {false,false,false,0,0};default:return {true,true,false,0,0};}}
}
// ---- components/hxdisplay/Tft.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include "esp_err.h"
#include "driver/spi_master.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_ops.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "Graphics.h"
namespace hx {
constexpr int SCK=12,MOSI=11,MISO=10,TCS=5,CCS=13,DC=4,RST=6,BL=7;
constexpr spi_host_device_t HOST=SPI2_HOST;
esp_err_t init_bus();
class Tft {
public:
esp_err_t init();
esp_err_t set_rotation(unsigned r);
esp_err_t draw(const View& v);
void backlight(bool on);
private:
esp_lcd_panel_handle_t panel_=nullptr;
esp_lcd_panel_io_handle_t io_=nullptr;
SemaphoreHandle_t done_=nullptr;
uint16_t* strip_=nullptr;
};
}
// ---- components/hxdisplay/Tft.cpp ----
// SPDX-License-Identifier: MIT
#include "Tft.h"
#include <initializer_list>
#include "driver/gpio.h"
#include "esp_heap_caps.h"
#include "esp_lcd_panel_vendor.h"
namespace hx {
esp_err_t init_bus(){
for(int pin:{TCS,CCS}){ESP_ERROR_CHECK(gpio_set_level(gpio_num_t(pin),1));ESP_ERROR_CHECK(gpio_set_direction(gpio_num_t(pin),GPIO_MODE_OUTPUT));}
ESP_ERROR_CHECK(gpio_set_level(gpio_num_t(BL),0));ESP_ERROR_CHECK(gpio_set_direction(gpio_num_t(BL),GPIO_MODE_OUTPUT));
spi_bus_config_t cfg{};cfg.sclk_io_num=SCK;cfg.mosi_io_num=MOSI;cfg.miso_io_num=MISO;
cfg.quadwp_io_num=-1;cfg.quadhd_io_num=-1;cfg.max_transfer_sz=240*16*2;
return spi_bus_initialize(HOST,&cfg,SPI_DMA_CH_AUTO);
}
static bool done_cb(esp_lcd_panel_io_handle_t,esp_lcd_panel_io_event_data_t*,void* ctx){
BaseType_t wake=pdFALSE;xSemaphoreGiveFromISR((SemaphoreHandle_t)ctx,&wake);return wake==pdTRUE;
}
void Tft::backlight(bool on){gpio_set_level(gpio_num_t(BL),on);}
esp_err_t Tft::init(){
done_=xSemaphoreCreateBinary();strip_=(uint16_t*)heap_caps_malloc(240*16*2,MALLOC_CAP_DMA|MALLOC_CAP_INTERNAL);
if(!done_||!strip_)return ESP_ERR_NO_MEM;
esp_lcd_panel_io_spi_config_t io{};io.cs_gpio_num=TCS;io.dc_gpio_num=DC;io.spi_mode=0;io.pclk_hz=4000000;io.trans_queue_depth=1;io.lcd_cmd_bits=8;io.lcd_param_bits=8;io.on_color_trans_done=done_cb;io.user_ctx=done_;
esp_err_t e=esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)HOST,&io,&io_);if(e!=ESP_OK)return e;
esp_lcd_panel_dev_config_t dev{};dev.reset_gpio_num=RST;dev.rgb_ele_order=LCD_RGB_ELEMENT_ORDER_RGB;dev.data_endian=LCD_RGB_DATA_ENDIAN_LITTLE;dev.bits_per_pixel=16;
if((e=esp_lcd_new_panel_st7789(io_,&dev,&panel_))!=ESP_OK)return e;
if((e=esp_lcd_panel_reset(panel_))!=ESP_OK)return e;
if((e=esp_lcd_panel_init(panel_))!=ESP_OK)return e;
if((e=esp_lcd_panel_invert_color(panel_,true))!=ESP_OK)return e;
if((e=set_rotation(0))!=ESP_OK)return e;
return esp_lcd_panel_disp_on_off(panel_,true);
}
esp_err_t Tft::set_rotation(unsigned r){
auto q=rotation(r);esp_err_t e=esp_lcd_panel_swap_xy(panel_,q.swap);if(e!=ESP_OK)return e;
if((e=esp_lcd_panel_mirror(panel_,q.mx,q.my))!=ESP_OK)return e;
return esp_lcd_panel_set_gap(panel_,q.gx,q.gy);
}
esp_err_t Tft::draw(const View& v){
for(int y=0;y<240;y+=16){
for(int j=0;j<16;++j)for(int x=0;x<240;++x)strip_[j*240+x]=pixel(v,x,y+j);
esp_err_t e=esp_lcd_panel_draw_bitmap(panel_,0,y,240,y+16,strip_);if(e!=ESP_OK)return e;
if(xSemaphoreTake(done_,pdMS_TO_TICKS(1000))!=pdTRUE){backlight(false);return ESP_ERR_TIMEOUT;}
}
backlight(true);return ESP_OK;
}
}
// ---- main/main.cpp ----
// SPDX-License-Identifier: MIT
#include <stdio.h>
#include "esp_log.h"
#include "driver/i2c_master.h"
#include "Tft.h"
#include "Console.h"
#include "Sht40Logic.h"
static hx::Tft screen;
static bool sample(i2c_master_dev_handle_t dev,int& t,int& rh){
uint8_t cmd=0xfd,b[6];esp_err_t e=i2c_master_transmit(dev,&cmd,1,100);
if(e!=ESP_OK){ESP_LOGW("D03","SHT40 command %s",esp_err_to_name(e));return false;}
vTaskDelay(pdMS_TO_TICKS(10));
e=i2c_master_receive(dev,b,6,100);
if(e!=ESP_OK){ESP_LOGW("D03","SHT40 read %s",esp_err_to_name(e));return false;}
if(!decode_sht40(b,t,rh)){ESP_LOGW("D03","SHT40 CRC failed; discard sample");return false;}
return true;
}
extern "C" void app_main(){
init_console();ESP_ERROR_CHECK(hx::init_bus());ESP_ERROR_CHECK(screen.init());
i2c_master_bus_config_t bus_cfg{};bus_cfg.i2c_port=I2C_NUM_0;bus_cfg.sda_io_num=GPIO_NUM_8;bus_cfg.scl_io_num=GPIO_NUM_9;bus_cfg.clk_source=I2C_CLK_SRC_DEFAULT;bus_cfg.glitch_ignore_cnt=7;bus_cfg.flags.enable_internal_pullup=true;
i2c_master_bus_handle_t bus;ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg,&bus));
i2c_device_config_t cfg{};cfg.dev_addr_length=I2C_ADDR_BIT_LEN_7;cfg.device_address=0x44;cfg.scl_speed_hz=100000;
i2c_master_dev_handle_t sensor;ESP_ERROR_CHECK(i2c_master_bus_add_device(bus,&cfg,&sensor));
ESP_LOGI("D03","READY PID3787 240x240 RGB565 LE; keys0..3 rotation, T test card, S live SHT40; keep SD socket empty");
hx::View v;bool dirty=true;uint32_t last=0;v.page=hx::View::TEST;
for(;;){
char c=console_key();
if(c>='0'&&c<='3'){v.rotation=unsigned(c-'0');ESP_ERROR_CHECK(screen.set_rotation(v.rotation));dirty=true;}
if(c=='t'||c=='T'){v.page=hx::View::TEST;dirty=true;}
if(c=='s'||c=='S'){v.page=hx::View::SENSOR;v.valid=false;last=0;dirty=true;}
uint32_t now=uint32_t(xTaskGetTickCount()*portTICK_PERIOD_MS);
if(v.page==hx::View::SENSOR&&(uint32_t(now-last)>=2000||last==0)){
last=now;v.valid=sample(sensor,v.temperature_mC,v.humidity_mPermille);dirty=true;
if(v.valid)ESP_LOGI("D03","sample valid t_mC=%d rh_mPermille=%d",v.temperature_mC,v.humidity_mPermille);
else ESP_LOGW("D03","NO SAMPLE; old values hidden");
}
if(dirty){
esp_err_t e=screen.draw(v);if(e!=ESP_OK){screen.backlight(false);ESP_LOGE("D03","STOP frame SPI=%s; no further jobs; DMA buffer retained; RESET after inspection",esp_err_to_name(e));return;}
auto q=hx::rotation(v.rotation);ESP_LOGI("D03","frame submitted+SPI completed page=%d r=%u gap=%d,%d; optical result not checked",int(v.page),v.rotation,q.gx,q.gy);dirty=false;
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
色卡、四方向、有效后失联分开验
作者真实IDF构建Exit0,CRC端点/错误向量、RGB565/边框与旋转表的原样header fixture通过;没有SHT40/TFT硬件或光学模型。以下实物检查仍pending,记录表未填结果。
- 启动先看R0色卡,核对左红中绿右蓝、完整白色边框、四角0/1/2/3。
- 依次发0、1、2、3,用板上排针作方位参考拍四张,记录gap和哪边朝上。
- 发S,记录真实T/RH与采样日志;发T回色卡。
- 冷启动缺模块:断电移除SHT40,再启动发S,应NO SAMPLE。该场景不能证明旧值撤销。
- 已有样本后失联:提前断电在SDA支路装SPST;有效样本后只断开信号开关,下次失败采样应NO SAMPLE。记录最后有效与首次失败时刻;闭合等下一轮,必要断电恢复。
- 做到这里,你应该看到
- 四方向照片、真实传感器样本、已有值后断线三个证据都要独立保存。
按现象定位,再决定改哪一项
只有背光:查VIN/BL之外的TC、DC、RST与SPI时钟;边缘裁切:查固定模块、窗口和gap;红蓝错:查RGB/BGR,杂色再查字节序。先固定一个变量,不把多项配置同时翻转。
SHT40无值:查0x44、SDA8/SCL9、模块供电和具体NACK/CRC日志。运行中只动预装信号开关,改供电和卡扣要断电。SPI超时后先留日志,RESET恢复;需要重新上传按F06操作BOOT/RESET,不停用欠压或看门狗。
前置E01解决SDK与构建、F07解决模块资料核对;下一步P08沿用同源显示组件和接线,再加入板载microSD,按两设备先分别通过的顺序验收。
参考:[2] Adafruit具体排针、电源、背光与SO用途[3] Adafruit ST7789固定1.11.0窗口和方向[4] IDF5.5.5 ST7789:RAMCTRL/色序/镜像/偏移[10] IDF5.5.5 S3新I2C主机API
资料来源与版本说明
固定版本源与具体模块资料逐项核对;构建与主机逻辑检查通过,所有实板观察待测。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。